-
Notifications
You must be signed in to change notification settings - Fork 1.7k
[DebuggerV2] Add components to show source code #3432
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
monaco editor appears, but needs more styling
|
|
||
| .bottom-section { | ||
| border-top: 1px solid rgba(0, 0, 0, 0.12); | ||
| height: 360px; |
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused symbol.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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. 🤔
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
createSpyObj()allows us to succinctly write a fake implementation.- 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.
| 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, | ||
| }); |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| provideMockStore({ | ||
| initialState: createState(createDebuggerState()), | ||
| }), |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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().
.../plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/source_files_container_test.ts
Show resolved
Hide resolved
caisq
left a comment
There was a problem hiding this 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 |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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'; |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
| provideMockStore({ | ||
| initialState: createState(createDebuggerState()), | ||
| }), |
There was a problem hiding this comment.
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.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, | ||
| }); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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().
.../plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/source_files_container_test.ts
Show resolved
Hide resolved
| let editorSpy: jasmine.SpyObj<any>; | ||
| let monaco: any; | ||
| function setUpMonacoFakes() { | ||
| async function fakeLoadMonaco() { |
There was a problem hiding this comment.
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:
createSpyObj()allows us to succinctly write a fake implementation.- 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.
| private decorations: string[] = []; | ||
|
|
||
| async ngOnChanges(changes: SimpleChanges): Promise<void> { | ||
| await loadMonaco(); |
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| 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, | ||
| }); |
There was a problem hiding this comment.
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.
Unit tests are revised accordingly.
- 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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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> { |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
caisq
left a comment
There was a problem hiding this 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> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
| } | ||
| }) | ||
| ) | ||
| .subscribe(); |
There was a problem hiding this comment.
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.
* 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 *  * 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.
* 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 *  * 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.
sourceLineFocusedaction, 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)SourceCodeComponent, which is a wrapper around monaco.ngOnChange()to detect code content change. Renders code in monaco editor whenever the change happensngOnChange()to detect focus line number change. UsesEditor.revealLineInCenter()to scroll the code to the line of interestEditor.deltaDecorations()to apply highlighting of the focused line, in both the code itself and in the gutter (see screenshot below).