diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/BUILD b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/BUILD index 2afa322003..98f679c7be 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/BUILD +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/BUILD @@ -27,6 +27,7 @@ ng_module( "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store:types", "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/alerts", "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/inactive", + "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files", "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace", "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/timeline", "//tensorboard/webapp/plugins:plugin_registry", @@ -54,6 +55,7 @@ tf_ts_library( "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/alerts", "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/execution_data", "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/inactive", + "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files", "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace", "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/timeline", "//tensorboard/webapp/angular:expect_angular_core_testing", @@ -74,7 +76,9 @@ tf_ng_web_test_suite( "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/effects:debugger_effects_test_lib", "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store:debugger_store_test_lib", "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/alerts:alerts_container_test_lib", + "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code:source_code_container_test_lib", "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code:source_code_test_lib", + "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files:source_files_container_test_lib", "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/timeline:timeline_test", ], ) diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/debugger_component.css b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/debugger_component.css index 543ae34b0e..87021399bf 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/debugger_component.css +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/debugger_component.css @@ -14,6 +14,9 @@ limitations under the License. ==============================================================================*/ .bottom-section { + border-top: 1px solid rgba(0, 0, 0, 0.12); + height: 353px; + padding-top: 6px; width: 100%; } @@ -28,10 +31,16 @@ tf-debugger-v2-alerts { width: 200px; } +tf-debugger-v2-source-files { + display: inline-block; + height: 100%; + vertical-align: top; + width: 70%; +} + tf-debugger-v2-stack-trace { - /* TODO(cais): Once code-editor component is added to the left, - replace the `float` with a linear layout. */ - float: right; + display: inline-block; + width: 30%; } tf-debugger-v2-timeline { diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/debugger_component.ng.html b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/debugger_component.ng.html index 3b13d4a0cb..08f1a79c0a 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/debugger_component.ng.html +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/debugger_component.ng.html @@ -26,6 +26,7 @@
+
diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/debugger_container_test.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/debugger_container_test.ts index 558c52f4df..eb9c49cdec 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/debugger_container_test.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/debugger_container_test.ts @@ -27,6 +27,7 @@ import { executionScrollLeft, executionScrollRight, executionScrollToIndex, + sourceLineFocused, } from './actions'; import {DebuggerComponent} from './debugger_component'; import {DebuggerContainer} from './debugger_container'; @@ -50,6 +51,7 @@ import {ExecutionDataContainer} from './views/execution_data/execution_data_cont import {ExecutionDataModule} from './views/execution_data/execution_data_module'; import {InactiveModule} from './views/inactive/inactive_module'; import {TimelineContainer} from './views/timeline/timeline_container'; +import {SourceFilesModule} from './views/source_files/source_files_module'; import {StackTraceContainer} from './views/stack_trace/stack_trace_container'; import {StackTraceModule} from './views/stack_trace/stack_trace_module'; import {TimelineModule} from './views/timeline/timeline_module'; @@ -68,6 +70,7 @@ describe('Debugger Container', () => { CommonModule, ExecutionDataModule, InactiveModule, + SourceFilesModule, StackTraceModule, TimelineModule, ], @@ -958,13 +961,13 @@ describe('Debugger Container', () => { ); expect(linenoElements.length).toEqual(3); expect(linenoElements[0].nativeElement.innerText).toEqual( - `Line ${stackFrame0[2]}:` + `Line ${stackFrame0[2]}` ); expect(linenoElements[1].nativeElement.innerText).toEqual( - `Line ${stackFrame1[2]}:` + `Line ${stackFrame1[2]}` ); expect(linenoElements[2].nativeElement.innerText).toEqual( - `Line ${stackFrame2[2]}:` + `Line ${stackFrame2[2]}` ); const functionElements = fixture.debugElement.queryAll( @@ -1022,5 +1025,63 @@ describe('Debugger Container', () => { ); expect(stackFrameContainers.length).toEqual(0); }); + + it('Emits sourceLineFocused when line number is clicked', () => { + const fixture = TestBed.createComponent(StackTraceContainer); + fixture.detectChanges(); + + const stackFrame0 = createTestStackFrame(); + const stackFrame1 = createTestStackFrame(); + const stackFrame2 = createTestStackFrame(); + store.setState( + createState( + createDebuggerState({ + executions: { + numExecutionsLoaded: { + state: DataLoadState.LOADED, + lastLoadedTimeInMs: 111, + }, + executionDigestsLoaded: { + state: DataLoadState.LOADED, + lastLoadedTimeInMs: 222, + pageLoadedSizes: {0: 100}, + numExecutions: 1000, + }, + executionDigests: {}, + pageSize: 100, + displayCount: 50, + scrollBeginIndex: 90, + focusIndex: 98, + executionData: { + 98: createTestExecutionData({ + stack_frame_ids: ['a0', 'a1', 'a2'], + }), + }, + }, + stackFrames: { + a0: stackFrame0, + a1: stackFrame1, + a2: stackFrame2, + }, + }) + ) + ); + fixture.detectChanges(); + + const linenoElements = fixture.debugElement.queryAll( + By.css('.stack-frame-lineno') + ); + linenoElements[1].nativeElement.click(); + fixture.detectChanges(); + expect(dispatchSpy).toHaveBeenCalledWith( + sourceLineFocused({ + sourceLineSpec: { + host_name: stackFrame1[0], + file_path: stackFrame1[1], + lineno: stackFrame1[2], + }, + }) + ); + }); }); }); diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/debugger_module.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/debugger_module.ts index 01a2172755..fa7881984c 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/debugger_module.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/debugger_module.ts @@ -26,6 +26,7 @@ import {reducers} from './store/debugger_reducers'; import {DEBUGGER_FEATURE_KEY} from './store/debugger_types'; import {AlertsModule} from './views/alerts/alerts_module'; import {InactiveModule} from './views/inactive/inactive_module'; +import {SourceFilesModule} from './views/source_files/source_files_module'; import {StackTraceModule} from './views/stack_trace/stack_trace_module'; import {TimelineModule} from './views/timeline/timeline_module'; import {PluginRegistryModule} from '../../../webapp/plugins/plugin_registry_module'; @@ -36,6 +37,7 @@ import {PluginRegistryModule} from '../../../webapp/plugins/plugin_registry_modu AlertsModule, CommonModule, InactiveModule, + SourceFilesModule, StackTraceModule, Tfdbg2ServerDataSourceModule, TimelineModule, diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store/debugger_selectors.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store/debugger_selectors.ts index 6e19939549..182d66b9b8 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store/debugger_selectors.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store/debugger_selectors.ts @@ -28,6 +28,7 @@ import { LoadState, SourceFileContent, SourceFileSpec, + SourceLineSpec, StackFrame, StackFramesById, State, @@ -331,3 +332,10 @@ export const getFocusedSourceFileContent = createSelector( return state.sourceCode.fileContents[fileIndex] || null; } ); + +export const getFocusedSourceLineSpec = createSelector( + selectDebuggerState, + (state: DebuggerState): SourceLineSpec | null => { + return state.sourceCode.focusLineSpec; + } +); diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/alerts/BUILD b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/alerts/BUILD index e0cfc18e41..c984eebaa7 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/alerts/BUILD +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/alerts/BUILD @@ -42,6 +42,7 @@ tf_ts_library( "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/testing", "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/execution_data", "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/inactive", + "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files", "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace", "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/timeline", "//tensorboard/webapp/angular:expect_angular_core_testing", diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/alerts/alerts_container_test.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/alerts/alerts_container_test.ts index 7cc05fd1bd..80c473241c 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/alerts/alerts_container_test.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/alerts/alerts_container_test.ts @@ -35,6 +35,7 @@ import {AlertsContainer} from './alerts_container'; import {AlertsModule} from './alerts_module'; import {ExecutionDataModule} from '../execution_data/execution_data_module'; import {InactiveModule} from '../inactive/inactive_module'; +import {SourceFilesModule} from '../source_files/source_files_module'; import {StackTraceModule} from '../stack_trace/stack_trace_module'; import {TimelineModule} from '../timeline/timeline_module'; @@ -52,6 +53,7 @@ describe('Alerts Container', () => { CommonModule, ExecutionDataModule, InactiveModule, + SourceFilesModule, StackTraceModule, TimelineModule, ], diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/BUILD b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/BUILD index a09fcaab81..8053bdd087 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/BUILD +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/BUILD @@ -1,9 +1,29 @@ package(default_visibility = ["//tensorboard:internal"]) +load("@npm_angular_bazel//:index.bzl", "ng_module") load("//tensorboard/defs:defs.bzl", "tf_ts_library") licenses(["notice"]) # Apache 2.0 +ng_module( + name = "source_code", + srcs = [ + "source_code_component.ts", + "source_code_container.ts", + "source_code_module.ts", + ], + assets = [ + "source_code_component.css", + "source_code_component.ng.html", + ], + deps = [ + ":load_monaco", + "@npm//@angular/common", + "@npm//@angular/core", + "@npm//rxjs", + ], +) + tf_ts_library( name = "load_monaco", srcs = [ @@ -27,3 +47,34 @@ tf_ts_library( "@npm//@types/requirejs", ], ) + +tf_ts_library( + name = "testing", + srcs = [ + "testing.ts", + ], + deps = [ + ":load_monaco", + "@npm//@types/jasmine", + ], +) + +tf_ts_library( + name = "source_code_container_test_lib", + testonly = True, + srcs = [ + "source_code_container_test.ts", + ], + tsconfig = "//:tsconfig-test", + deps = [ + ":load_monaco", + ":source_code", + ":testing", + "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin:debugger_v2", + "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/testing", + "//tensorboard/webapp/angular:expect_angular_core_testing", + "@npm//@angular/compiler", + "@npm//@angular/core", + "@npm//@types/jasmine", + ], +) diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/source_code_component.css b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/source_code_component.css new file mode 100644 index 0000000000..7d70cc305a --- /dev/null +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/source_code_component.css @@ -0,0 +1,27 @@ +/* Copyright 2020 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +==============================================================================*/ + +.code-viewer-container { + height: 100%; +} + +:host ::ng-deep .highlight-gutter { + background: rgba(255, 111, 0, 0.7); + width: 5px !important; +} + +:host ::ng-deep .highlight-line { + background: rgba(255, 111, 0, 0.3); +} diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/source_code_component.ng.html b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/source_code_component.ng.html new file mode 100644 index 0000000000..ad919b4e4b --- /dev/null +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/source_code_component.ng.html @@ -0,0 +1,18 @@ + + +
diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/source_code_component.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/source_code_component.ts new file mode 100644 index 0000000000..1784ad0033 --- /dev/null +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/source_code_component.ts @@ -0,0 +1,155 @@ +/* Copyright 2020 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +==============================================================================*/ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + Input, + SimpleChanges, + ViewChild, + OnDestroy, + OnInit, +} from '@angular/core'; +import {fromEvent, interval, Subject} from 'rxjs'; +import {debounce, takeUntil, tap} from 'rxjs/operators'; + +/** @typehack */ import * as _typeHackRxjs from 'rxjs'; + +const DEFAULT_CODE_LANGUAGE = 'python'; +const DEFAULT_CODE_FONT_SIZE = 10; + +const RESIZE_DEBOUNCE_INTERAVL_MS = 50; + +@Component({ + selector: 'source-code-component', + templateUrl: './source_code_component.ng.html', + styleUrls: ['./source_code_component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SourceCodeComponent implements OnInit, OnDestroy { + @Input() + lines: string[] | null = null; // TODO(cais): Add spinner for `null`. + + @Input() + focusedLineno: number | null = null; + + // TODO(cais): Explore better typing by depending on external libraries. + @Input() + monaco: any | null = null; + + @ViewChild('codeViewerContainer', {static: true, read: ElementRef}) + private readonly codeViewerContainer!: ElementRef; + + private editor: any = null; + + private decorations: string[] = []; + + private readonly ngUnsubscribe = new Subject(); + + ngOnInit(): void { + // Listen to window resize event. When resize happens, re-layout + // monaco editor, so its width is always up-to-date with respect to + // the window size. Do this with `debounce()` to prevent re-layouting + // at too high a rate. + const resizePipe = fromEvent(window, 'resize') + .pipe( + takeUntil(this.ngUnsubscribe), + debounce(() => interval(RESIZE_DEBOUNCE_INTERAVL_MS)), + tap(() => { + if (this.editor !== null) { + this.editor.layout(); + } + }) + ) + .subscribe(); + } + + ngOnDestroy(): void { + this.ngUnsubscribe.next(); + this.ngUnsubscribe.complete(); + } + + ngOnChanges(changes: SimpleChanges): void { + if (this.monaco === null) { + return; + } + const currentLines: string[] | null = changes['monaco'] + ? this.lines + : changes['lines'] + ? changes['lines'].currentValue + : null; + if (currentLines) { + const value = currentLines.join('\n'); + if (this.editor === null) { + this.editor = this.monaco.editor.create( + this.codeViewerContainer.nativeElement, + { + value, + language: DEFAULT_CODE_LANGUAGE, + readOnly: true, + fontSize: DEFAULT_CODE_FONT_SIZE, + minimap: { + enabled: true, + }, + } + ); + } else { + this.editor.setValue(value); + } + } + + const currentFocusedLineno: number | null = changes['monaco'] + ? this.focusedLineno + : changes['focusedLineno'] + ? changes['focusedLineno'].currentValue + : null; + if (currentFocusedLineno && this.lines && this.monaco !== null) { + this.editor.revealLineInCenter( + currentFocusedLineno, + this.monaco.editor.ScrollType.Smooth + ); + const lineLength = this.lines[currentFocusedLineno - 1].length; + this.decorations = this.editor.deltaDecorations(this.decorations, [ + { + range: new this.monaco.Range( + currentFocusedLineno, + 1, + currentFocusedLineno, + 1 + ), + options: { + isWholeLine: true, + linesDecorationsClassName: 'highlight-gutter', + }, + }, + { + range: new this.monaco.Range( + currentFocusedLineno, + 1, + currentFocusedLineno, + lineLength + 1 + ), + options: { + inlineClassName: 'highlight-line', + }, + }, + ]); + } + } +} + +export const TEST_ONLY = { + RESIZE_DEBOUNCE_INTERAVL_MS, +}; diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/source_code_container.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/source_code_container.ts new file mode 100644 index 0000000000..7601ea2089 --- /dev/null +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/source_code_container.ts @@ -0,0 +1,66 @@ +/* Copyright 2020 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +==============================================================================*/ +import {Component, Input} from '@angular/core'; +import {from} from 'rxjs'; +import {map} from 'rxjs/operators'; + +import {loadMonaco} from './load_monaco_shim'; + +/** @typehack */ import * as _typeHackRxjwindowWithRequireAndMonacos from 'rxjs'; + +// TODO(cais): Explore better typing by depending on external libraries. +const windowWithRequireAndMonaco: any = window; + +/** + * SourceCodeContainer displays the content of a source-code file. + * + * It displays the code with visual features including syntax highlighting. + * It additionally provides functionalities to: + * - Scroll to and highlight a given line by its line number. + * + * TODO(cais): Add support for line decoration and symbol decoration. + * + * Unlike SourceFilesComponent, SourceCodeContainer handles only one file at a + * time. + */ +@Component({ + selector: 'tf-debugger-v2-source-code', + template: ` + + `, +}) +export class SourceCodeContainer { + // Lines of the source-code file, split at line breaks. + @Input() + lines: string[] | null = null; // TODO(cais): Add spinner for `null`. + + // Line number to scroll to and highlight, 1-based. + @Input() + focusedLineno: number | null = null; + + monaco$: any | null = null; + + ngOnInit(): void { + this.monaco$ = from(loadMonaco()).pipe( + map(() => windowWithRequireAndMonaco.monaco) + ); + } + + constructor() {} +} diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/source_code_container_test.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/source_code_container_test.ts new file mode 100644 index 0000000000..0c29d224b8 --- /dev/null +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/source_code_container_test.ts @@ -0,0 +1,174 @@ +/* Copyright 2020 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +==============================================================================*/ +/** + * Unit tests for the the Source Files component and container. + */ +import {SimpleChange} from '@angular/core'; +import {TestBed} from '@angular/core/testing'; + +import {SourceCodeComponent, TEST_ONLY} from './source_code_component'; +import {SourceCodeContainer} from './source_code_container'; +import { + fakes, + setUpMonacoFakes, + spies, + tearDownMonacoFakes, + windowWithRequireAndMonaco, +} from './testing'; +import * as loadMonacoShim from './load_monaco_shim'; + +describe('Source Code Component', () => { + beforeEach(async () => { + setUpMonacoFakes(); + await TestBed.configureTestingModule({ + declarations: [SourceCodeComponent, SourceCodeContainer], + }).compileComponents(); + }); + + afterEach(() => { + tearDownMonacoFakes(); + }); + + const lines1 = ['import tensorflow as tf', '', 'print("hello, world")']; + const lines2 = [ + 'model = tf.keras.Sequential', + 'model.add(tf.keras.layers.Dense(1))', + ]; + + it('renders a file and change to a new file', async () => { + const fixture = TestBed.createComponent(SourceCodeComponent); + const component = fixture.componentInstance; + component.lines = lines1; + component.focusedLineno = 3; + await component.ngOnChanges({ + lines: new SimpleChange(null, lines1, true), + focusedLineno: new SimpleChange(null, 3, true), + }); + // Simlulate loading monaco and setting the `monaco` input after loading. + await loadMonacoShim.loadMonaco(); + component.monaco = windowWithRequireAndMonaco.monaco; + await component.ngOnChanges({ + monaco: new SimpleChange(null, windowWithRequireAndMonaco.monaco, true), + }); + + // Initial rendering of code uses monaco editor's constructor instead of + // using `setValue()`. + expect(spies.editorSpy.setValue).not.toHaveBeenCalled(); + expect(spies.editorSpy.revealLineInCenter).toHaveBeenCalledTimes(1); + expect(spies.editorSpy.revealLineInCenter).toHaveBeenCalledWith( + 3, + fakes.fakeMonaco.editor.ScrollType.Smooth + ); + expect(spies.editorSpy.deltaDecorations).toHaveBeenCalledTimes(1); + + component.lines = lines2; + component.focusedLineno = 1; + await component.ngOnChanges({ + lines: new SimpleChange(lines1, lines2, false), + focusedLineno: new SimpleChange(3, 1, false), + }); + + expect(spies.editorSpy.setValue).toHaveBeenCalledTimes(1); + expect(spies.editorSpy.setValue).toHaveBeenCalledWith( + 'model = tf.keras.Sequential\nmodel.add(tf.keras.layers.Dense(1))' + ); + expect(spies.editorSpy.revealLineInCenter).toHaveBeenCalledTimes(2); + expect(spies.editorSpy.revealLineInCenter).toHaveBeenCalledWith( + 1, + fakes.fakeMonaco.editor.ScrollType.Smooth + ); + expect(spies.editorSpy.deltaDecorations).toHaveBeenCalledTimes(2); + }); + + it('switches to a different line in the same file', async () => { + const fixture = TestBed.createComponent(SourceCodeComponent); + const component = fixture.componentInstance; + await loadMonacoShim.loadMonaco(); + component.monaco = windowWithRequireAndMonaco.monaco; + component.lines = lines1; + component.focusedLineno = 2; + await component.ngOnChanges({ + lines: new SimpleChange(null, lines1, true), + focusedLineno: new SimpleChange(null, 2, true), + }); + await component.ngOnChanges({ + focusedLineno: new SimpleChange(2, 1, false), + }); + + // setValue() shouldn't have been called because there is no change in file + // content. + expect(spies.editorSpy!.setValue).toHaveBeenCalledTimes(0); + expect(spies.editorSpy!.revealLineInCenter).toHaveBeenCalledTimes(2); + // This is the call for the old lineno. + expect(spies.editorSpy!.revealLineInCenter).toHaveBeenCalledWith( + 2, + fakes.fakeMonaco.editor.ScrollType.Smooth + ); + // This is the call for the new lineno. + expect(spies.editorSpy.revealLineInCenter).toHaveBeenCalledWith( + 1, + fakes.fakeMonaco.editor.ScrollType.Smooth + ); + }); + + function sleep(durationMs: number): Promise { + return new Promise((resolve) => { + setInterval(() => { + resolve(); + }, durationMs); + }); + } + + it('calls monaco editor layout() on resize', async () => { + const fixture = TestBed.createComponent(SourceCodeComponent); + const component = fixture.componentInstance; + component.ngOnInit(); + component.lines = lines1; + component.focusedLineno = 3; + await component.ngOnChanges({ + lines: new SimpleChange(null, lines1, true), + focusedLineno: new SimpleChange(null, 3, true), + }); + await loadMonacoShim.loadMonaco(); + component.monaco = windowWithRequireAndMonaco.monaco; + await component.ngOnChanges({ + monaco: new SimpleChange(null, windowWithRequireAndMonaco.monaco, true), + }); + + window.dispatchEvent(new Event('resize')); + await sleep(TEST_ONLY.RESIZE_DEBOUNCE_INTERAVL_MS); + expect(spies.editorSpy!.layout).toHaveBeenCalledTimes(1); + }); +}); + +describe('Source Code Container', () => { + beforeEach(async () => { + setUpMonacoFakes(); + await TestBed.configureTestingModule({ + declarations: [SourceCodeComponent, SourceCodeContainer], + }).compileComponents(); + }); + + afterEach(() => { + tearDownMonacoFakes(); + }); + + it('calls loadMonaco() on ngOnInit()', () => { + const fixture = TestBed.createComponent(SourceCodeContainer); + const component = fixture.componentInstance; + component.ngOnInit(); + expect(spies.loadMonacoSpy!).toHaveBeenCalledTimes(1); + }); +}); diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/source_code_module.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/source_code_module.ts new file mode 100644 index 0000000000..b17a6aaf46 --- /dev/null +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/source_code_module.ts @@ -0,0 +1,27 @@ +/* Copyright 2020 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +==============================================================================*/ + +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; + +import {SourceCodeComponent} from './source_code_component'; +import {SourceCodeContainer} from './source_code_container'; + +@NgModule({ + declarations: [SourceCodeComponent, SourceCodeContainer], + imports: [CommonModule], + exports: [SourceCodeContainer], +}) +export class SourceCodeModule {} diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/testing.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/testing.ts new file mode 100644 index 0000000000..be09133eb7 --- /dev/null +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/testing.ts @@ -0,0 +1,75 @@ +/* Copyright 2020 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +==============================================================================*/ + +/** + * Testing utilities (fakes and spies) for testing monaco-editor-based source + * code components. + */ + +import * as loadMonacoShim from './load_monaco_shim'; + +export class FakeRange { + constructor( + readonly startLineNumber: number, + readonly startColumn: number, + readonly endLineNumber: number, + readonly endColumn: number + ) {} +} + +// TODO(cais): Explore better typing by depending on 3rd-party libraries. +export const spies: { + loadMonacoSpy?: jasmine.Spy; + editorSpy?: jasmine.SpyObj; +} = {}; + +export const fakes: { + fakeMonaco?: any; +} = {}; + +export const windowWithRequireAndMonaco: any = window; + +export function setUpMonacoFakes() { + async function fakeLoadMonaco() { + fakes.fakeMonaco = { + editor: { + create: () => { + spies.editorSpy = jasmine.createSpyObj('editorSpy', [ + 'deltaDecorations', + 'layout', + 'revealLineInCenter', + 'setValue', + ]); + return spies.editorSpy; + }, + ScrollType: { + Immediate: 1, + Smooth: 0, + }, + }, + Range: FakeRange, + }; + windowWithRequireAndMonaco.monaco = fakes.fakeMonaco; + } + spies.loadMonacoSpy = spyOn(loadMonacoShim, 'loadMonaco').and.callFake( + fakeLoadMonaco + ); +} + +export function tearDownMonacoFakes() { + if (windowWithRequireAndMonaco.monaco !== undefined) { + delete windowWithRequireAndMonaco.monaco; + } +} diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/BUILD b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/BUILD new file mode 100644 index 0000000000..2d926f3a89 --- /dev/null +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/BUILD @@ -0,0 +1,59 @@ +package(default_visibility = ["//tensorboard:internal"]) + +load("@npm_angular_bazel//:index.bzl", "ng_module") +load("//tensorboard/defs:defs.bzl", "tf_ts_library") + +licenses(["notice"]) # Apache 2.0 + +ng_module( + name = "source_files", + srcs = [ + "source_files_component.ts", + "source_files_container.ts", + "source_files_module.ts", + ], + assets = [ + "source_files_component.css", + "source_files_component.ng.html", + ], + deps = [ + "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store", + "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store:types", + "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code", + "@npm//@angular/common", + "@npm//@angular/core", + "@npm//@ngrx/store", + "@npm//rxjs", + ], +) + +tf_ts_library( + name = "source_files_container_test_lib", + testonly = True, + srcs = [ + "source_files_container_test.ts", + ], + tsconfig = "//:tsconfig-test", + deps = [ + ":source_files", + "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin:debugger_v2", + "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store", + "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store:types", + "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/testing", + "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/alerts", + "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/execution_data", + "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/inactive", + "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code:load_monaco", + "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code:testing", + "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace", + "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/timeline", + "//tensorboard/webapp/angular:expect_angular_core_testing", + "//tensorboard/webapp/angular:expect_ngrx_store_testing", + "@npm//@angular/common", + "@npm//@angular/compiler", + "@npm//@angular/core", + "@npm//@angular/platform-browser", + "@npm//@ngrx/store", + "@npm//@types/jasmine", + ], +) diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/source_files_component.css b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/source_files_component.css new file mode 100644 index 0000000000..c58b545de9 --- /dev/null +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/source_files_component.css @@ -0,0 +1,57 @@ +/* Copyright 2020 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +==============================================================================*/ + +.header-section { + border-bottom: 1px solid rgba(0, 0, 0, 0.12); + height: 24px; + padding-bottom: 6px; + vertical-align: middle; + white-space: nowrap; + width: 100%; +} + +.file-label { + display: inline-block; + font-weight: normal; + white-space: normal; + padding: 0 20px; +} + +.no-file-selected { + display: inline-block; + color: #666; + padding: 0 20px; +} + +.source-files-container { + display: flex; + flex-direction: column; + font-family: 'Roboto Mono', monospace; + font-size: 10px; + height: 100%; +} + +.title-tag { + display: inline-block; + font-weight: bold; + height: 100%; + padding-left: 6px; + vertical-align: top; +} + +tf-debugger-v2-source-code { + flex-grow: 1; + width: 100%; +} diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/source_files_component.ng.html b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/source_files_component.ng.html new file mode 100644 index 0000000000..ca5db69b0c --- /dev/null +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/source_files_component.ng.html @@ -0,0 +1,47 @@ + + +
+
+
+ Source Code +
+ + +
+ {{ focusedSourceLineSpec.file_path }} +
+ + +
+ (No file selected) +
+
+
+ + + +
diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/source_files_component.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/source_files_component.ts new file mode 100644 index 0000000000..83fa723850 --- /dev/null +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/source_files_component.ts @@ -0,0 +1,38 @@ +/* Copyright 2020 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +==============================================================================*/ +import {Component, Input} from '@angular/core'; + +import {SourceFileContent, SourceLineSpec} from '../../store/debugger_types'; + +/** + * Renders the content of source file(s). + * + * Unlike `SourceCodeComponent`, which displays only the content of a single + * source-code file, `SourceFilesComponent`is aware of the meta-informaton about + * the files being displayed, such their file paths. Such meta-information is + * displayed by this component. + */ +@Component({ + selector: 'source-files-component', + templateUrl: './source_files_component.ng.html', + styleUrls: ['./source_files_component.css'], +}) +export class SourceFilesComponent { + @Input() + focusedSourceFileContent: SourceFileContent | null = null; + + @Input() + focusedSourceLineSpec: SourceLineSpec | null = null; +} diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/source_files_container.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/source_files_container.ts new file mode 100644 index 0000000000..3ff9351226 --- /dev/null +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/source_files_container.ts @@ -0,0 +1,45 @@ +/* Copyright 2020 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +==============================================================================*/ +import {Component} from '@angular/core'; +import {select, Store} from '@ngrx/store'; + +import { + getFocusedSourceFileContent, + getFocusedSourceLineSpec, +} from '../../store'; +import {State} from '../../store/debugger_types'; + +/** @typehack */ import * as _typeHackRxjs from 'rxjs'; + +@Component({ + selector: 'tf-debugger-v2-source-files', + template: ` + + `, +}) +export class SourceFilesContainer { + constructor(private readonly store: Store) {} + + readonly focusedSourceFileContent$ = this.store.pipe( + select(getFocusedSourceFileContent) + ); + + readonly focusedSourceLineSpec$ = this.store.pipe( + select(getFocusedSourceLineSpec) + ); +} diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/source_files_container_test.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/source_files_container_test.ts new file mode 100644 index 0000000000..0d114438c2 --- /dev/null +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/source_files_container_test.ts @@ -0,0 +1,133 @@ +/* Copyright 2020 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +==============================================================================*/ +/** + * Unit tests for the the Source Files component and container. + */ +import {CommonModule} from '@angular/common'; +import {TestBed} from '@angular/core/testing'; +import {By} from '@angular/platform-browser'; + +import {Store} from '@ngrx/store'; +import {provideMockStore, MockStore} from '@ngrx/store/testing'; + +import {DebuggerComponent} from '../../debugger_component'; +import {DebuggerContainer} from '../../debugger_container'; +import {DataLoadState, State} from '../../store/debugger_types'; +import {createDebuggerState, createState} from '../../testing'; +import {AlertsModule} from '../alerts/alerts_module'; +import {ExecutionDataModule} from '../execution_data/execution_data_module'; +import {InactiveModule} from '../inactive/inactive_module'; +import {setUpMonacoFakes, tearDownMonacoFakes} from '../source_code/testing'; +import {StackTraceModule} from '../stack_trace/stack_trace_module'; +import { + getFocusedSourceFileContent, + getFocusedSourceLineSpec, +} from '../../store'; +import {TimelineModule} from '../timeline/timeline_module'; +import {SourceFilesContainer} from './source_files_container'; +import {SourceFilesModule} from './source_files_module'; + +/** @typehack */ import * as _typeHackStore from '@ngrx/store'; + +describe('Source Files Container', () => { + let store: MockStore; + let dispatchSpy: jasmine.Spy; + + beforeEach(async () => { + setUpMonacoFakes(); + await TestBed.configureTestingModule({ + declarations: [DebuggerComponent, DebuggerContainer], + imports: [ + AlertsModule, + CommonModule, + ExecutionDataModule, + InactiveModule, + SourceFilesModule, + StackTraceModule, + TimelineModule, + ], + providers: [provideMockStore(), DebuggerContainer], + }).compileComponents(); + store = TestBed.get(Store); + dispatchSpy = spyOn(store, 'dispatch'); + }); + + afterEach(() => { + tearDownMonacoFakes(); + }); + + it('renders no file selected when no source line is focused on', () => { + const fixture = TestBed.createComponent(SourceFilesContainer); + store.setState(createState(createDebuggerState())); + fixture.detectChanges(); + + const noFileSelectedElement = fixture.debugElement.query( + By.css('.no-file-selected') + ); + expect(noFileSelectedElement.nativeElement.innerText).toBe( + '(No file selected)' + ); + + const sourceCodeElements = fixture.debugElement.queryAll( + By.css('source-code-component') + ); + expect(sourceCodeElements.length).toBe(0); + }); + + it('renders file path and editor when a file is focused on', async () => { + const fixture = TestBed.createComponent(SourceFilesContainer); + 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, + }); + fixture.detectChanges(); + await fixture.whenStable(); + + let fileLabelElement = fixture.debugElement.query(By.css('.file-label')); + expect(fileLabelElement.nativeElement.innerText).toBe('/home/user/main.py'); + + const sourceCodeElements = fixture.debugElement.queryAll( + By.css('source-code-component') + ); + expect(sourceCodeElements.length).toBe(1); + + // Check the behavior when a new file is focused on. + store.overrideSelector(getFocusedSourceFileContent, { + loadState: DataLoadState.LOADED, + lines: [ + 'model = tf.keras.Sequential', + 'model.add(tf.keras.layers.Dense(1))', + ], + }); + store.overrideSelector(getFocusedSourceLineSpec, { + host_name: 'localhost', + file_path: '/home/user/model.py', + lineno: 1, + }); + store.refreshState(); + fixture.detectChanges(); + await fixture.whenStable(); + + fileLabelElement = fixture.debugElement.query(By.css('.file-label')); + expect(fileLabelElement.nativeElement.innerText).toBe( + '/home/user/model.py' + ); + }); +}); diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/source_files_module.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/source_files_module.ts new file mode 100644 index 0000000000..d1dbd8d965 --- /dev/null +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_files/source_files_module.ts @@ -0,0 +1,28 @@ +/* Copyright 2020 The TensorFlow Authors. All Rights Reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +==============================================================================*/ + +import {CommonModule} from '@angular/common'; +import {NgModule} from '@angular/core'; + +import {SourceCodeModule} from '../source_code/source_code_module'; +import {SourceFilesComponent} from './source_files_component'; +import {SourceFilesContainer} from './source_files_container'; + +@NgModule({ + declarations: [SourceFilesComponent, SourceFilesContainer], + imports: [CommonModule, SourceCodeModule], + exports: [SourceFilesContainer], +}) +export class SourceFilesModule {} diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/BUILD b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/BUILD index da3d1c3c05..324b8cd6b7 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/BUILD +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/BUILD @@ -16,8 +16,10 @@ ng_module( "stack_trace_component.ng.html", ], deps = [ + "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/actions", "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store", "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store:types", + "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code", "@npm//@angular/common", "@npm//@angular/core", "@npm//@ngrx/store", diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/stack_trace_component.css b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/stack_trace_component.css index 084219754c..e4214ba1b6 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/stack_trace_component.css +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/stack_trace_component.css @@ -14,7 +14,7 @@ limitations under the License. ==============================================================================*/ .stack-frame-array { - height: 340px; + height: 360px; overflow-x: auto; overflow-y: auto; } @@ -42,17 +42,22 @@ limitations under the License. } .stack-frame-lineno { + cursor: pointer; display: inline-block; max-width: 80px; text-align: left; + text-decoration: underline; width: 80px; } .stack-trace-container { + border-left: 1px solid rgba(0, 0, 0, 0.12); font-size: 10px; font-family: 'Roboto Mono', monospace; height: 360px; + margin-left: 8px; max-height: 360px; + padding-left: 8px; width: 400px; } diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/stack_trace_component.ng.html b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/stack_trace_component.ng.html index 6eaef88e93..424c049e1c 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/stack_trace_component.ng.html +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/stack_trace_component.ng.html @@ -43,9 +43,15 @@ {{ stackFrameForDisplay.concise_file_path }}
- -
- Line {{ stackFrameForDisplay.lineno }}: +
+ Line {{ stackFrameForDisplay.lineno }}
{{ stackFrameForDisplay.function_name }} diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/stack_trace_component.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/stack_trace_component.ts index 745aab2e36..d8c34715a3 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/stack_trace_component.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/stack_trace_component.ts @@ -12,7 +12,7 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ -import {Component, Input} from '@angular/core'; +import {Component, EventEmitter, Input, Output} from '@angular/core'; export interface StackFrameForDisplay { host_name: string; @@ -30,4 +30,11 @@ export interface StackFrameForDisplay { export class StackTraceComponent { @Input() stackFramesForDisplay: StackFrameForDisplay[] | null = null; + + @Output() + onSourceLineClicked = new EventEmitter<{ + host_name: string; + file_path: string; + lineno: number; + }>(); } diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/stack_trace_container.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/stack_trace_container.ts index 8015c5baa5..dca4f01f95 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/stack_trace_container.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/stack_trace_container.ts @@ -17,6 +17,7 @@ import {createSelector, select, Store} from '@ngrx/store'; import {State} from '../../store/debugger_types'; +import {sourceLineFocused} from '../../actions'; import {getFocusedExecutionStackFrames} from '../../store'; import {StackFrameForDisplay} from './stack_trace_component'; @@ -27,6 +28,7 @@ import {StackFrameForDisplay} from './stack_trace_component'; template: ` `, }) @@ -59,4 +61,12 @@ export class StackTraceContainer { ); constructor(private readonly store: Store) {} + + onSourceLineClicked(args: { + host_name: string; + file_path: string; + lineno: number; + }) { + this.store.dispatch(sourceLineFocused({sourceLineSpec: args})); + } } diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/stack_trace_module.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/stack_trace_module.ts index ba4e86ff46..5abd9b304c 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/stack_trace_module.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/stack_trace/stack_trace_module.ts @@ -16,12 +16,13 @@ limitations under the License. import {CommonModule} from '@angular/common'; import {NgModule} from '@angular/core'; +import {SourceCodeModule} from '../source_code/source_code_module'; import {StackTraceComponent} from './stack_trace_component'; import {StackTraceContainer} from './stack_trace_container'; @NgModule({ declarations: [StackTraceComponent, StackTraceContainer], - imports: [CommonModule], + imports: [CommonModule, SourceCodeModule], exports: [StackTraceContainer], }) export class StackTraceModule {} diff --git a/tensorboard/webapp/BUILD b/tensorboard/webapp/BUILD index 408a527078..3d5a54301b 100644 --- a/tensorboard/webapp/BUILD +++ b/tensorboard/webapp/BUILD @@ -123,6 +123,7 @@ tf_web_library( deps = [ ":tb_webapp", "//tensorboard/components:ng_polymer_lib_binary", + "//tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/source_code/monaco:requirejs", ], ) diff --git a/tensorboard/webapp/index.uninlined.html b/tensorboard/webapp/index.uninlined.html index 9dc2663ad4..50321bafb5 100644 --- a/tensorboard/webapp/index.uninlined.html +++ b/tensorboard/webapp/index.uninlined.html @@ -24,6 +24,7 @@ +