Skip to content

Conversation

@caisq
Copy link
Contributor

@caisq caisq commented Mar 25, 2020

  • Motivation for features / changes
    • Continue developing DebuggerV2 plugin: source code rendering part.
  • Technical description of changes
    • In the existing StackTraceComponent, add a click callback for line numbers (e.g., "Line 15" as in the screenshot below). The click callback dispatches a sourceLineFocused action, which triggers downloading of file content via existing effects. (But see [DebuggerV2] Avoid loading source files that are loaded or loading #3430 for a fix in logic)
    • Create SourceCodeComponent, which is a wrapper around monaco.
      • Uses ngOnChange() to detect code content change. Renders code in monaco editor whenever the change happens
      • Also uses ngOnChange() to detect focus line number change. Uses Editor.revealLineInCenter() to scroll the code to the line of interest
      • Uses Editor.deltaDecorations() to apply highlighting of the focused line, in both the code itself and in the gutter (see screenshot below).
    • Create SourceFilesComponent and SourceFilesContainer, which is composed of a SourceCodeComponent. Additionally, it shows the currently selected file name in a header section. The Container is what communicates with the store.
  • Screenshots of UI changes
    • image
  • Detailed steps to verify changes work correctly (as executed by you)
    • Unit tests added for newly added event emitter, components, and containers.
    • Ran the code against a real logdir with tfdbg2 data.

@caisq caisq changed the title Dbg2 source code ng [DebuggerV2] Use monaco-editor to show source code Mar 25, 2020
@caisq caisq changed the title [DebuggerV2] Use monaco-editor to show source code [DebuggerV2] Add components to show source code Mar 25, 2020
@caisq caisq marked this pull request as ready for review March 25, 2020 20:46
@caisq caisq requested a review from stephanwlee March 25, 2020 21:00
@caisq
Copy link
Contributor Author

caisq commented Mar 25, 2020

cc @psybuzz @baileydesign


.bottom-section {
border-top: 1px solid rgba(0, 0, 0, 0.12);
height: 360px;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this has effectively 367px height :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged. I prefer this to be exactly 360px in height. So I changed height to 353px to componensate.

==============================================================================*/
import {ChangeDetectionStrategy, Component, Input} from '@angular/core';
import {DebuggerRunListing} from './store/debugger_types';
import {DebuggerRunListing, SourceFileContent} from './store/debugger_types';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused symbol.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed it.

runId !== null &&
fileContent !== null &&
fileContent.loadState !== DataLoadState.LOADING
fileContent.loadState === DataLoadState.NOT_LOADED
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about failed? Does network fluke make debugger unusable?

Surprised that there is no test change with this. 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files for which previous loading efforts have failed will have the state NOT_LOADED. So the code here covers it.

The test change happened in another (merged) PR: #3430. This diff should go away after I merge master into this branch.

==============================================================================*/

.code-viewer-container {
height: 336px;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

weird to see height hardcoded like this. where is this magical number from?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. Good point. I've removed this magic number by making its parent a flex container.

On a related note: I've added a HostListener for window resizing in this commit, so that the monaco editor can re-render properly when the window is resized.

private decorations: string[] = [];

async ngOnChanges(changes: SimpleChanges): Promise<void> {
await loadMonaco();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do this on ngOnInit? Not sure how rendering cycle of Angular's is impacted by this. You may want to use some kind of observable to do this instead.

For example:

  • container, onInit, sets this.monaco$ = rxjs.from(loadMonaco()).pipe(map(() => windowWithRequireAndMonaco.monaco);.
  • component takes monaco as input to this component

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my observation, ngOnChange() can be before after ngOnInit(). This makes it hard to call the async loadMonaco() function from ngOnInit(). So I'll keep the code as is.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my suggestion still stands: you can pass a reference to monaco from the parent after using async pipe.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Created SourceCodeContainer to wrap around SourceCodeComponent and to provide the monaco input. Unit tests are adjusted accordingly.

let editorSpy: jasmine.SpyObj<any>;
let monaco: any;
function setUpMonacoFakes() {
async function fakeLoadMonaco() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, another way to have sane test and what not would be to inject the monaco editor and provide a shim implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have refactored the fake/spy code into its own module: views/source_code/testing.ts. It is shared between the source_code_component_test.ts and source_files_container_test.ts (see my other reply).

The rationale for using fake/spies like this are:

  1. createSpyObj() allows us to succinctly write a fake implementation.
  2. This approach is hermetic. I.e., it doesn't involve downloading monaco js/cs/ttf files during unit tests, in either the internal or external build/test environments.

Comment on lines +136 to +144
store.overrideSelector(getFocusedSourceFileContent, {
loadState: DataLoadState.LOADED,
lines: ['import tensorflow as tf', '', 'print("hello, world")'],
});
store.overrideSelector(getFocusedSourceLineSpec, {
host_name: 'localhost',
file_path: '/home/user/main.py',
lineno: 3,
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you override the selectors before you create the fixture, you don't have to refreshState

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the first refreshState() calls in each it. My observation is that the first is unnecessary, but the second (i.e., after overrideSelector() is called again) is necessary. Without it, the test fails as if the selector return value hasn't changed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is more like, overrideSelector before creating component does not require refreshState but all the subsequent overrides require one.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't match what's happening herein this test though. In this test, the component is created before overrideSelector() is called, but no refreshState() is necessary for the first set of assertions. It's only after the second call to overrideSelectors() that refreshState() becomes necessary.

Comment on lines 107 to 109
provideMockStore({
initialState: createState(createDebuggerState()),
}),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, if you are going to use overrideSelector pattern, you don't need to pass the initialState (though I would make sure to set the selectors' values to something reasonable in beforeEach)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Removed the initialState. The SourceFileComponent uses the store in a relatively simple way (only a few fields like focused source-file content and source line spec), so that it's not necessary to set up any initial selector return values here.

});
store.refreshState();
fixture.detectChanges();
await fixture.whenStable();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A bit surprised that you need to wait for stability all the time. Is this absolutely needed? Ah, it must be the async/await in the ngOnChange.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. It's the async ngOnChange().

Copy link
Contributor Author

@caisq caisq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review!

runId !== null &&
fileContent !== null &&
fileContent.loadState !== DataLoadState.LOADING
fileContent.loadState === DataLoadState.NOT_LOADED
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Files for which previous loading efforts have failed will have the state NOT_LOADED. So the code here covers it.

The test change happened in another (merged) PR: #3430. This diff should go away after I merge master into this branch.


.bottom-section {
border-top: 1px solid rgba(0, 0, 0, 0.12);
height: 360px;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged. I prefer this to be exactly 360px in height. So I changed height to 353px to componensate.

==============================================================================*/
import {ChangeDetectionStrategy, Component, Input} from '@angular/core';
import {DebuggerRunListing} from './store/debugger_types';
import {DebuggerRunListing, SourceFileContent} from './store/debugger_types';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed it.

==============================================================================*/

.code-viewer-container {
height: 336px;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah. Good point. I've removed this magic number by making its parent a flex container.

On a related note: I've added a HostListener for window resizing in this commit, so that the monaco editor can re-render properly when the window is resized.

private decorations: string[] = [];

async ngOnChanges(changes: SimpleChanges): Promise<void> {
await loadMonaco();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my observation, ngOnChange() can be before after ngOnInit(). This makes it hard to call the async loadMonaco() function from ngOnInit(). So I'll keep the code as is.

Comment on lines 107 to 109
provideMockStore({
initialState: createState(createDebuggerState()),
}),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Removed the initialState. The SourceFileComponent uses the store in a relatively simple way (only a few fields like focused source-file content and source line spec), so that it's not necessary to set up any initial selector return values here.

Comment on lines +136 to +144
store.overrideSelector(getFocusedSourceFileContent, {
loadState: DataLoadState.LOADED,
lines: ['import tensorflow as tf', '', 'print("hello, world")'],
});
store.overrideSelector(getFocusedSourceLineSpec, {
host_name: 'localhost',
file_path: '/home/user/main.py',
lineno: 3,
});
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the first refreshState() calls in each it. My observation is that the first is unnecessary, but the second (i.e., after overrideSelector() is called again) is necessary. Without it, the test fails as if the selector return value hasn't changed.

});
store.refreshState();
fixture.detectChanges();
await fixture.whenStable();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. It's the async ngOnChange().

let editorSpy: jasmine.SpyObj<any>;
let monaco: any;
function setUpMonacoFakes() {
async function fakeLoadMonaco() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have refactored the fake/spy code into its own module: views/source_code/testing.ts. It is shared between the source_code_component_test.ts and source_files_container_test.ts (see my other reply).

The rationale for using fake/spies like this are:

  1. createSpyObj() allows us to succinctly write a fake implementation.
  2. This approach is hermetic. I.e., it doesn't involve downloading monaco js/cs/ttf files during unit tests, in either the internal or external build/test environments.

@caisq caisq requested a review from stephanwlee March 27, 2020 22:07
private decorations: string[] = [];

async ngOnChanges(changes: SimpleChanges): Promise<void> {
await loadMonaco();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think my suggestion still stands: you can pass a reference to monaco from the parent after using async pipe.

@HostListener('window:resize', ['$event'])
onResize(event: Event) {
if (this.editor !== null) {
this.editor.layout();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debounce this if possible. Resize can happen 60hz and we don't need to layout on every frame.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Using rxjs debounce() at a rate of 20 Hz (50-ms interval) now.

lineno: 3,
});
fixture.detectChanges();
await fixture.whenStable();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe you no longer need this since you aren't rendering monaco component? (source_code_component)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feel free to use import {NO_ERRORS_SCHEMA} from '@angular/core';, too.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

await fixture.whenStable() and async are still necessary here, because the fake loadMonaco() that serves this test is async, just like the reaal loadMonaco(). It gets called because SourceFileContainer is composed of a SourceCodeComponent. I prefer to let the state of the entire container stabilize before making assertions, even though the assertions made in this test file aren't concerned with monaco per se.

Comment on lines +136 to +144
store.overrideSelector(getFocusedSourceFileContent, {
loadState: DataLoadState.LOADED,
lines: ['import tensorflow as tf', '', 'print("hello, world")'],
});
store.overrideSelector(getFocusedSourceLineSpec, {
host_name: 'localhost',
file_path: '/home/user/main.py',
lineno: 3,
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is more like, overrideSelector before creating component does not require refreshState but all the subsequent overrides require one.

caisq added 3 commits March 31, 2020 22:31
- Use string indexing in `ngOnChanges()`.
- Add msissing dependency for views/stack_trace/BUILD
- Remove unreachable typing
- Use const export from source_code/testing.ts
}
})
)
.subscribe();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this component can be created and destroyed a lot, please use takeUntil() to remove the subscription.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using takeUntil() in conjunction with a Subject that emits in ngOnDestroy() now.

.subscribe();
}

async ngOnChanges(changes: SimpleChanges): Promise<void> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need for async anymore! :D

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Contributor Author

@caisq caisq left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking another look.

.subscribe();
}

async ngOnChanges(changes: SimpleChanges): Promise<void> {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}
})
)
.subscribe();
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using takeUntil() in conjunction with a Subject that emits in ngOnDestroy() now.

@caisq caisq merged commit 8eedeed into tensorflow:master Apr 1, 2020
bileschi pushed a commit to bileschi/tensorboard that referenced this pull request Apr 15, 2020
* Motivation for features / changes
  * Continue developing DebuggerV2 plugin: source code rendering part.
* Technical description of changes
  * In the existing StackTraceComponent, add a click callback for line numbers (e.g., "Line 15" as in the screenshot below). The click callback dispatches a `sourceLineFocused` action, which triggers downloading of file content via existing effects. (But see tensorflow#3430 for a fix in logic)
  * Create `SourceCodeComponent`, which is a wrapper around monaco.
    * Uses `ngOnChange()` to detect code content change. Renders code in monaco editor whenever the change happens
    * Also uses `ngOnChange()` to detect focus line number change. Uses `Editor.revealLineInCenter()` to scroll the code to the line of interest
    *  Uses `Editor.deltaDecorations()` to apply highlighting of the focused line, in both the code itself and in the gutter (see screenshot below).
  * Create SourceFilesComponent and SourceFilesContainer, which is composed of a SourceCodeComponent. Additionally, it shows the currently selected file name in a header section. The Container is what communicates with the store.
* Screenshots of UI changes
  * ![image](https://user-images.githubusercontent.com/16824702/77582498-de8add80-6eb5-11ea-89f2-073030e80537.png)
* Detailed steps to verify changes work correctly (as executed by you)
  * Unit tests added for newly added event emitter, components, and containers.
  * Ran the code against a real logdir with tfdbg2 data.
bileschi pushed a commit that referenced this pull request Apr 15, 2020
* Motivation for features / changes
  * Continue developing DebuggerV2 plugin: source code rendering part.
* Technical description of changes
  * In the existing StackTraceComponent, add a click callback for line numbers (e.g., "Line 15" as in the screenshot below). The click callback dispatches a `sourceLineFocused` action, which triggers downloading of file content via existing effects. (But see #3430 for a fix in logic)
  * Create `SourceCodeComponent`, which is a wrapper around monaco.
    * Uses `ngOnChange()` to detect code content change. Renders code in monaco editor whenever the change happens
    * Also uses `ngOnChange()` to detect focus line number change. Uses `Editor.revealLineInCenter()` to scroll the code to the line of interest
    *  Uses `Editor.deltaDecorations()` to apply highlighting of the focused line, in both the code itself and in the gutter (see screenshot below).
  * Create SourceFilesComponent and SourceFilesContainer, which is composed of a SourceCodeComponent. Additionally, it shows the currently selected file name in a header section. The Container is what communicates with the store.
* Screenshots of UI changes
  * ![image](https://user-images.githubusercontent.com/16824702/77582498-de8add80-6eb5-11ea-89f2-073030e80537.png)
* Detailed steps to verify changes work correctly (as executed by you)
  * Unit tests added for newly added event emitter, components, and containers.
  * Ran the code against a real logdir with tfdbg2 data.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants