diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/actions/debugger_actions.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/actions/debugger_actions.ts index 0e04b595cc..aa1082f4b7 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/actions/debugger_actions.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/actions/debugger_actions.ts @@ -27,6 +27,7 @@ import { import { ExecutionDigestsResponse, ExecutionDataResponse, + GraphExecutionDataResponse, SourceFileResponse, } from '../data_source/tfdbg2_data_source'; @@ -144,6 +145,24 @@ export const numGraphExecutionsLoaded = createAction( props<{numGraphExecutions: number}>() ); +export const graphExecutionDataRequested = createAction( + '[Debugger] Intra-Graph Execution Data Requested', + props<{pageIndex: number}>() +); + +export const graphExecutionDataLoaded = createAction( + '[Debugger] Intra-Graph Execution Data Loaded', + props() +); + +export const graphExecutionScrollToIndex = createAction( + '[Debugger] Scroll Intra-Graph Execution List to Given Index', + props<{index: number}>() +); + +/** + * Actions related to source files and stack traces. + */ export const sourceFileListRequested = createAction( '[Debugger] Source File List Requested.' ); 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 294d74fa4c..83ee7b2b90 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 @@ -15,13 +15,18 @@ limitations under the License. .bottom-section { border-top: 1px solid rgba(0, 0, 0, 0.12); - height: 353px; + height: 34%; padding-top: 6px; width: 100%; } +.debugger-container { + background-color: #fff; + height: 100%; +} + .top-section { - height: 360px; + height: 66%; padding: 6px 0; width: 100%; } 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 922caabc46..761c52137c 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 @@ -15,7 +15,7 @@ limitations under the License. --> -
+
diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/effects/debugger_effects.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/effects/debugger_effects.ts index d6447a575d..bd55be4e9d 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/effects/debugger_effects.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/effects/debugger_effects.ts @@ -17,6 +17,7 @@ import {Store} from '@ngrx/store'; import {Actions, createEffect, ofType} from '@ngrx/effects'; import {merge, Observable} from 'rxjs'; import { + debounceTime, filter, map, mergeMap, @@ -37,6 +38,9 @@ import { executionScrollLeft, executionScrollRight, executionScrollToIndex, + graphExecutionDataLoaded, + graphExecutionDataRequested, + graphExecutionScrollToIndex, numAlertsAndBreakdownLoaded, numAlertsAndBreakdownRequested, numExecutionsLoaded, @@ -61,6 +65,11 @@ import { getExecutionPageSize, getExecutionScrollBeginIndex, getFocusedSourceFileContent, + getGraphExecutionDataPageLoadedSizes, + getGraphExecutionDisplayCount, + getGraphExecutionPageSize, + getGraphExecutionScrollBeginIndex, + getGraphExecutionDataLoadingPages, getNumExecutions, getNumExecutionsLoaded, getLoadedAlertsOfFocusedType, @@ -541,6 +550,109 @@ export class DebuggerEffects { ); } + /** + * Emits when scrolling event leads to need to load new intra-graph execution + * data. + * + * The returned observable contains the + * - runId: active runId, + * - missingPage: indices of missing `GraphExecution` pages that need to be + * loaded by a downstream pipe. + * - pageSize: GraphExecution data page size. + * - numGraphExecutions: Current total number of `GraphExecution`s. + */ + private onGraphExecutionScroll(): Observable<{ + runId: string; + missingPages: number[]; + pageSize: number; + numGraphExecutions: number; + }> { + return this.actions$.pipe( + ofType(graphExecutionScrollToIndex), + debounceTime(100), + withLatestFrom( + this.store.select(getActiveRunId), + this.store.select(getNumGraphExecutions), + this.store.select(getGraphExecutionScrollBeginIndex) + ), + filter(([, runId, numGraphExecutions]) => { + return runId !== null && numGraphExecutions > 0; + }), + map(([, runId, numGraphExecutions, scrollBeginIndex]) => ({ + runId, + numGraphExecutions, + scrollBeginIndex, + })), + withLatestFrom( + this.store.select(getGraphExecutionPageSize), + this.store.select(getGraphExecutionDisplayCount), + this.store.select(getGraphExecutionDataLoadingPages), + this.store.select(getGraphExecutionDataPageLoadedSizes) + ), + map( + ([ + {runId, numGraphExecutions, scrollBeginIndex}, + pageSize, + displayCount, + loadingPages, + pageLoadedSizes, + ]) => { + let missingPages: number[] = getMissingPages( + scrollBeginIndex, + Math.min(scrollBeginIndex + displayCount, numGraphExecutions), + pageSize, + numGraphExecutions, + pageLoadedSizes + ); + // Omit pages that are already loading. + missingPages = missingPages.filter( + (page) => loadingPages.indexOf(page) === -1 + ); + return { + runId: runId!, + missingPages, + pageSize, + numGraphExecutions, + }; + } + ) + ); + } + + private loadGraphExecutionPages( + prevStream$: Observable<{ + runId: string; + missingPages: number[]; + pageSize: number; + numGraphExecutions: number; + }> + ): Observable { + return prevStream$.pipe( + filter(({missingPages}) => missingPages.length > 0), + tap(({missingPages}) => { + missingPages.forEach((pageIndex) => { + this.store.dispatch(graphExecutionDataRequested({pageIndex})); + }); + }), + mergeMap(({runId, missingPages, pageSize, numGraphExecutions}) => { + const begin = missingPages[0] * pageSize; + const end = Math.min( + (missingPages[missingPages.length - 1] + 1) * pageSize, + numGraphExecutions + ); + return this.dataSource.fetchGraphExecutionData(runId!, begin, end).pipe( + tap((graphExecutionDataResponse) => { + this.store.dispatch( + graphExecutionDataLoaded(graphExecutionDataResponse) + ); + }), + map(() => void null) + ); + // TODO(cais): Add catchError() to pipe. + }) + ); + } + /** * Emits when user focuses on an alert type. * @@ -766,6 +878,8 @@ export class DebuggerEffects { * * on source file requested ---> fetch source file * + * on graph-execution scroll --> fetch graph-execution data + * **/ this.loadData$ = createEffect( () => { @@ -827,6 +941,10 @@ export class DebuggerEffects { const onSourceFileFocused$ = this.onSourceFileFocused(); + const onGraphExecutionScroll$ = this.loadGraphExecutionPages( + this.onGraphExecutionScroll() + ); + // ExecutionDigest and ExecutionData can be loaded in parallel. return merge( onNumAlertsLoaded$, @@ -834,7 +952,8 @@ export class DebuggerEffects { onExecutionDataLoaded$, onNumGraphExecutionLoaded$, loadSourceFileList$, - onSourceFileFocused$ + onSourceFileFocused$, + onGraphExecutionScroll$ ).pipe( // createEffect expects an Observable that emits {}. map(() => ({})) diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/effects/debugger_effects_test.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/effects/debugger_effects_test.ts index 18c8470ea4..40a373419a 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/effects/debugger_effects_test.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/effects/debugger_effects_test.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 {TestBed} from '@angular/core/testing'; +import {fakeAsync, TestBed, tick} from '@angular/core/testing'; import {provideMockActions} from '@ngrx/effects/testing'; import {Action, Store} from '@ngrx/store'; import {MockStore, provideMockStore} from '@ngrx/store/testing'; @@ -30,6 +30,9 @@ import { executionScrollLeft, executionScrollRight, executionScrollToIndex, + graphExecutionDataRequested, + graphExecutionDataLoaded, + graphExecutionScrollToIndex, numAlertsAndBreakdownRequested, numAlertsAndBreakdownLoaded, numExecutionsLoaded, @@ -47,6 +50,7 @@ import { AlertsResponse, ExecutionDataResponse, ExecutionDigestsResponse, + GraphExecutionDataResponse, GraphExecutionDigestsResponse, SourceFileListResponse, SourceFileResponse, @@ -62,10 +66,16 @@ import { getNumAlertsOfFocusedType, getNumExecutionsLoaded, getNumExecutions, + getNumGraphExecutions, getDisplayCount, getExecutionDigestsLoaded, getExecutionPageSize, getExecutionScrollBeginIndex, + getGraphExecutionDisplayCount, + getGraphExecutionDataLoadingPages, + getGraphExecutionDataPageLoadedSizes, + getGraphExecutionPageSize, + getGraphExecutionScrollBeginIndex, getLoadedAlertsOfFocusedType, getLoadedExecutionData, getLoadedStackFrames, @@ -78,6 +88,7 @@ import { DebuggerRunListing, Execution, ExecutionDigest, + GraphExecution, State, SourceFileSpec, SourceFileContent, @@ -86,9 +97,10 @@ import { createDebuggerState, createState, createTestExecutionData, - createTestStackFrame, - createTestInfNanAlert, createTestExecutionDigest, + createTestGraphExecution, + createTestInfNanAlert, + createTestStackFrame, } from '../testing'; import {TBHttpClientTestingModule} from '../../../../webapp/webapp_data_source/tb_http_client_testing'; @@ -322,6 +334,20 @@ describe('Debugger effects', () => { .and.returnValue(of(graphExcutionDigestsResponse)); } + function createFetchGraphExecutionDataSpy( + runId: string, + begin: number, + end: number, + graphExcutionDataResponse: GraphExecutionDataResponse + ) { + return spyOn( + TestBed.get(Tfdbg2HttpServerDataSource), + 'fetchGraphExecutionData' + ) + .withArgs(runId, begin, end) + .and.returnValue(of(graphExcutionDataResponse)); + } + describe('loadData', () => { const runListingForTest: DebuggerRunListing = { __default_debugger_run__: { @@ -881,6 +907,90 @@ describe('Debugger effects', () => { } }); + describe('graphExecutionScrollToIndex', () => { + beforeEach(() => { + debuggerEffects.loadData$.subscribe(); + }); + + for (const {dataExists, page3Size, loadingPages} of [ + {dataExists: false, page3Size: 0, loadingPages: [3]}, + {dataExists: false, page3Size: 0, loadingPages: []}, + {dataExists: true, page3Size: 2, loadingPages: []}, + ]) { + it( + `triggers GraphExecution loading: dataExists=${dataExists}, ` + + `loadingPages=${JSON.stringify(loadingPages)}`, + fakeAsync(() => { + const runId = '__default_debugger_run__'; + const originalScrollBeginIndex = 50; + const newScrollBeginIndex = originalScrollBeginIndex + 2; + const numGraphExecutions = 100; + const pageSize = 20; + const displayCount = 10; + store.overrideSelector(getActiveRunId, runId); + store.overrideSelector(getNumGraphExecutions, numGraphExecutions); + store.overrideSelector( + getGraphExecutionScrollBeginIndex, + newScrollBeginIndex + ); + store.overrideSelector(getGraphExecutionPageSize, pageSize); + store.overrideSelector(getGraphExecutionDisplayCount, displayCount); + store.overrideSelector(getExecutionPageSize, pageSize); + store.overrideSelector( + getGraphExecutionDataLoadingPages, + loadingPages + ); + const pageLoadedSizes: {[pageIndex: number]: number} = { + 0: 20, + 1: 20, + 2: 20, + }; + pageLoadedSizes[3] = page3Size; + store.overrideSelector( + getGraphExecutionDataPageLoadedSizes, + pageLoadedSizes + ); + store.refreshState(); + + const graphExecutions = new Array(pageSize).fill( + createTestGraphExecution() + ); + const graphExecutionDataResponse: GraphExecutionDataResponse = { + begin: 60, + end: 60 + pageSize, + graph_executions: graphExecutions, + }; + const fetchExecutionData = createFetchGraphExecutionDataSpy( + runId, + 60, + 60 + pageSize, + graphExecutionDataResponse + ); + + action.next( + graphExecutionScrollToIndex({index: newScrollBeginIndex}) + ); + tick(100); + + if (dataExists || loadingPages.length > 0) { + expect(fetchExecutionData).not.toHaveBeenCalled(); + expect(dispatchedActions).toEqual([]); + } else { + expect(fetchExecutionData).toHaveBeenCalledTimes(1); + expect(dispatchedActions).toEqual([ + graphExecutionDataRequested({pageIndex: 3}), + graphExecutionDataLoaded({ + begin: 60, + end: 60 + pageSize, + graph_executions: graphExecutions, + }), + ]); + } + }) + ); + } + }); + describe('load alerts of given type', () => { const runId = '__default_debugger_run__'; const alert0 = createTestInfNanAlert({ diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store/debugger_reducers.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store/debugger_reducers.ts index 913c9eabe4..4d9e68981b 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store/debugger_reducers.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store/debugger_reducers.ts @@ -18,6 +18,7 @@ import * as actions from '../actions'; import { ExecutionDataResponse, ExecutionDigestsResponse, + GraphExecutionDataResponse, GraphExecutionDigestsResponse, SourceFileResponse, } from '../data_source/tfdbg2_data_source'; @@ -39,7 +40,7 @@ import { /** @typehack */ import * as _typeHackStore from '@ngrx/store/store'; const DEFAULT_EXECUTION_PAGE_SIZE = 100; -const DEFAULT_GRAPH_EXECUTION_PAGE_SIZE = 100; +const DEFAULT_GRAPH_EXECUTION_PAGE_SIZE = 200; export function createInitialExecutionsState(): Executions { return { @@ -78,11 +79,13 @@ export function createInitialGraphExecutionsState(): GraphExecutions { }, // TODO(cais) Remove the hardcoding of this, which is coupled with css width // properties. - displayCount: 50, + displayCount: 100, pageSize: DEFAULT_GRAPH_EXECUTION_PAGE_SIZE, scrollBeginIndex: 0, focusIndex: null, graphExecutionDigests: {}, + graphExecutionDataLoadingPages: [], + graphExecutionDataPageLoadedSizes: {}, graphExecutionData: {}, }; } @@ -564,6 +567,82 @@ const reducer = createReducer( return newState; } ), + on( + actions.graphExecutionDataRequested, + (state: DebuggerState, {pageIndex}): DebuggerState => { + if (state.activeRunId === null) { + return state; + } + const graphExecutionDataLoadingPages = state.graphExecutions.graphExecutionDataLoadingPages.slice(); + if (graphExecutionDataLoadingPages.indexOf(pageIndex) === -1) { + graphExecutionDataLoadingPages.push(pageIndex); + } + return { + ...state, + graphExecutions: { + ...state.graphExecutions, + graphExecutionDataLoadingPages, + }, + }; + } + ), + on( + actions.graphExecutionDataLoaded, + (state: DebuggerState, data: GraphExecutionDataResponse): DebuggerState => { + if (state.activeRunId === null) { + return state; + } + const {pageSize} = state.graphExecutions; + const graphExecutionDataLoadingPages = state.graphExecutions.graphExecutionDataLoadingPages.slice(); + const graphExecutionDataPageLoadedSizes = { + ...state.graphExecutions.graphExecutionDataPageLoadedSizes, + }; + const graphExecutionData = {...state.graphExecutions.graphExecutionData}; + for (let i = data.begin; i < data.end; ++i) { + const pageIndex = Math.floor(i / pageSize); + if (graphExecutionDataLoadingPages.indexOf(pageIndex) !== -1) { + graphExecutionDataLoadingPages.splice( + graphExecutionDataLoadingPages.indexOf(pageIndex), + 1 + ); + } + if (graphExecutionDataPageLoadedSizes[pageIndex] === undefined) { + graphExecutionDataPageLoadedSizes[pageIndex] = 0; + } + if (graphExecutionData[i] === undefined) { + graphExecutionDataPageLoadedSizes[pageIndex]++; + } + graphExecutionData[i] = data.graph_executions[i - data.begin]; + } + return { + ...state, + graphExecutions: { + ...state.graphExecutions, + graphExecutionDataLoadingPages, + graphExecutionDataPageLoadedSizes, + graphExecutionData, + }, + }; + } + ), + on( + actions.graphExecutionScrollToIndex, + (state: DebuggerState, action: {index: number}): DebuggerState => { + if (action.index < 0 || !Number.isInteger(action.index)) { + throw new Error( + `Attempt to scroll to negative or non-integer graph-execution ` + + `index (${action.index})` + ); + } + return { + ...state, + graphExecutions: { + ...state.graphExecutions, + scrollBeginIndex: action.index, + }, + }; + } + ), //////////////////////////////////////////////////////// // Reducers related to source files and stack traces. // //////////////////////////////////////////////////////// diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store/debugger_reducers_test.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store/debugger_reducers_test.ts index 476cd196ab..7b1163992f 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store/debugger_reducers_test.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store/debugger_reducers_test.ts @@ -31,6 +31,7 @@ import { createDebuggerStateWithLoadedExecutionDigests, createDigestsStateWhileLoadingExecutionDigests, createTestExecutionData, + createTestGraphExecution, createTestStackFrame, createTestInfNanAlert, } from '../testing'; @@ -1364,4 +1365,133 @@ describe('Debugger reducers', () => { ).toEqual(12345); }); }); + + describe('graphExecutionDataRequested', () => { + it('updates loading pages by adding a new one', () => { + const state = createDebuggerState({ + activeRunId: '__default_debugger_run__', + graphExecutions: createDebuggerGraphExecutionsState({ + graphExecutionDataLoadingPages: [2222, 7777], + }), + }); + const nextState = reducers( + state, + actions.graphExecutionDataRequested({pageIndex: 4321}) + ); + expect(nextState.graphExecutions.graphExecutionDataLoadingPages).toEqual([ + 2222, + 7777, + 4321, + ]); + }); + }); + + describe('graphExecutionDataLoaded', () => { + it('with new data, updates loading pages, loaded pages and data', () => { + const state = createDebuggerState({ + activeRunId: '__default_debugger_run__', + graphExecutions: createDebuggerGraphExecutionsState({ + pageSize: 2, + graphExecutionDataLoadingPages: [1, 2], + graphExecutionDataPageLoadedSizes: {0: 2}, + graphExecutionData: { + 0: createTestGraphExecution({op_name: 'TestOp_0'}), + 1: createTestGraphExecution({op_name: 'TestOp_1'}), + }, + }), + }); + const nextState = reducers( + state, + actions.graphExecutionDataLoaded({ + begin: 2, + end: 4, + graph_executions: [ + createTestGraphExecution({op_name: 'TestOp_2'}), + createTestGraphExecution({op_name: 'TestOp_3'}), + ], + }) + ); + expect(nextState.graphExecutions.graphExecutionDataLoadingPages).toEqual([ + 2, + ]); + expect( + nextState.graphExecutions.graphExecutionDataPageLoadedSizes + ).toEqual({ + 0: 2, + 1: 2, + }); + expect(nextState.graphExecutions.graphExecutionData).toEqual({ + 0: createTestGraphExecution({op_name: 'TestOp_0'}), + 1: createTestGraphExecution({op_name: 'TestOp_1'}), + 2: createTestGraphExecution({op_name: 'TestOp_2'}), + 3: createTestGraphExecution({op_name: 'TestOp_3'}), + }); + }); + + it('with partly new data, correctly updates pages and data', () => { + const state = createDebuggerState({ + activeRunId: '__default_debugger_run__', + graphExecutions: createDebuggerGraphExecutionsState({ + pageSize: 2, + graphExecutionDataLoadingPages: [1], + graphExecutionDataPageLoadedSizes: {0: 2, 1: 1}, + graphExecutionData: { + 0: createTestGraphExecution({op_name: 'TestOp_0'}), + 1: createTestGraphExecution({op_name: 'TestOp_1'}), + 2: createTestGraphExecution({op_name: 'TestOp_2'}), + }, + }), + }); + const nextState = reducers( + state, + actions.graphExecutionDataLoaded({ + begin: 2, + end: 4, + graph_executions: [ + createTestGraphExecution({op_name: 'TestOp_2_overwrite'}), + createTestGraphExecution({op_name: 'TestOp_3_overwrite'}), + ], + }) + ); + expect(nextState.graphExecutions.graphExecutionDataLoadingPages).toEqual( + [] + ); + expect( + nextState.graphExecutions.graphExecutionDataPageLoadedSizes + ).toEqual({ + 0: 2, + 1: 2, + }); + expect(nextState.graphExecutions.graphExecutionData).toEqual({ + 0: createTestGraphExecution({op_name: 'TestOp_0'}), + 1: createTestGraphExecution({op_name: 'TestOp_1'}), + 2: createTestGraphExecution({op_name: 'TestOp_2_overwrite'}), + 3: createTestGraphExecution({op_name: 'TestOp_3_overwrite'}), + }); + }); + }); + + describe('graphExecutionScrollToIndex', () => { + it('updates graph-execution scrollBeginIndex', () => { + const state = createDebuggerState({ + graphExecutions: createDebuggerGraphExecutionsState({ + scrollBeginIndex: 0, + }), + }); + const nextState = reducers( + state, + actions.graphExecutionScrollToIndex({index: 1337}) + ); + expect(nextState.graphExecutions.scrollBeginIndex).toBe(1337); + }); + + for (const index of [-1, 8.8, Infinity, NaN]) { + it(`throws error for invalid scroll index: ${index}`, () => { + const state = createDebuggerState(); + expect(() => + reducers(state, actions.graphExecutionScrollToIndex({index})) + ).toThrowError(/.*scroll.*negative or non-integer/); + }); + } + }); }); 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 669f59656b..7ff7849cbc 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 @@ -25,6 +25,7 @@ import { Execution, ExecutionDigest, ExecutionDigestLoadState, + GraphExecution, LoadState, SourceFileContent, SourceFileSpec, @@ -208,6 +209,48 @@ export const getNumGraphExecutions = createSelector( } ); +export const getGraphExecutionScrollBeginIndex = createSelector( + selectDebuggerState, + (state: DebuggerState): number => { + return state.graphExecutions.scrollBeginIndex; + } +); + +export const getGraphExecutionDisplayCount = createSelector( + selectDebuggerState, + (state: DebuggerState): number => { + return state.graphExecutions.displayCount; + } +); + +export const getGraphExecutionPageSize = createSelector( + selectDebuggerState, + (state: DebuggerState): number => { + return state.graphExecutions.pageSize; + } +); + +export const getGraphExecutionDataLoadingPages = createSelector( + selectDebuggerState, + (state: DebuggerState): number[] => { + return state.graphExecutions.graphExecutionDataLoadingPages; + } +); + +export const getGraphExecutionDataPageLoadedSizes = createSelector( + selectDebuggerState, + (state: DebuggerState): {[page: number]: number} => { + return state.graphExecutions.graphExecutionDataPageLoadedSizes; + } +); + +export const getGraphExecutionData = createSelector( + selectDebuggerState, + (state: DebuggerState): {[index: number]: GraphExecution} => { + return state.graphExecutions.graphExecutionData; + } +); + /** * Get the focused alert types (if any) of the execution digests current being * displayed. For each displayed execution digest, there are two possibilities: diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store/debugger_selectors_test.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store/debugger_selectors_test.ts index 5d373bce0f..5bbb24797d 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store/debugger_selectors_test.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store/debugger_selectors_test.ts @@ -22,6 +22,12 @@ import { getFocusedExecutionStackFrames, getFocusedSourceFileContent, getFocusedSourceFileIndex, + getGraphExecutionData, + getGraphExecutionDataLoadingPages, + getGraphExecutionDataPageLoadedSizes, + getGraphExecutionDisplayCount, + getGraphExecutionPageSize, + getGraphExecutionScrollBeginIndex, getLoadedAlertsOfFocusedType, getNumAlerts, getNumAlertsOfFocusedType, @@ -45,6 +51,7 @@ import { createState, createTestExecutionData, createTestExecutionDigest, + createTestGraphExecution, createTestInfNanAlert, } from '../testing'; @@ -766,4 +773,121 @@ describe('debugger selectors', () => { expect(getNumGraphExecutions(state)).toBe(10); }); }); + + describe('getGraphExecutionScrollBeginIndex', () => { + it('returns correct initial zero state', () => { + const state = createState(createDebuggerState()); + expect(getGraphExecutionScrollBeginIndex(state)).toBe(0); + }); + + it('returns correct non-zero state', () => { + const state = createState( + createDebuggerState({ + graphExecutions: createDebuggerGraphExecutionsState({ + scrollBeginIndex: 1234567, + }), + }) + ); + expect(getGraphExecutionScrollBeginIndex(state)).toBe(1234567); + }); + }); + + describe('getGraphExecutionDisplayCount', () => { + it('returns correct value', () => { + const state = createState( + createDebuggerState({ + graphExecutions: createDebuggerGraphExecutionsState({ + displayCount: 240, + }), + }) + ); + expect(getGraphExecutionDisplayCount(state)).toBe(240); + }); + }); + + describe('getGraphExecutionPageSize', () => { + it('returns correct value', () => { + const state = createState( + createDebuggerState({ + graphExecutions: createDebuggerGraphExecutionsState({ + pageSize: 126, + }), + }) + ); + expect(getGraphExecutionPageSize(state)).toBe(126); + }); + }); + + describe('getGraphExecutionDataLoadingPages', () => { + it('returns correct empty value', () => { + const state = createState( + createDebuggerState({ + graphExecutions: createDebuggerGraphExecutionsState({ + graphExecutionDataLoadingPages: [], + }), + }) + ); + expect(getGraphExecutionDataLoadingPages(state)).toEqual([]); + }); + + it('returns correct non-empty value', () => { + const state = createState( + createDebuggerState({ + graphExecutions: createDebuggerGraphExecutionsState({ + graphExecutionDataLoadingPages: [1, 2, 100], + }), + }) + ); + expect(getGraphExecutionDataLoadingPages(state)).toEqual([1, 2, 100]); + }); + }); + + describe('getGraphExecutionDataLoadingPages', () => { + it('returns correct empty value', () => { + const state = createState( + createDebuggerState({ + graphExecutions: createDebuggerGraphExecutionsState({ + graphExecutionDataPageLoadedSizes: {}, + }), + }) + ); + expect(getGraphExecutionDataPageLoadedSizes(state)).toEqual({}); + }); + + it('returns correct non-empty value', () => { + const state = createState( + createDebuggerState({ + graphExecutions: createDebuggerGraphExecutionsState({ + graphExecutionDataPageLoadedSizes: {0: 10, 2: 40}, + }), + }) + ); + expect(getGraphExecutionDataPageLoadedSizes(state)).toEqual({ + 0: 10, + 2: 40, + }); + }); + }); + + describe('getGraphExecutionData', () => { + it('returns correct initial empty state', () => { + const state = createState(createDebuggerState()); + expect(getGraphExecutionData(state)).toEqual({}); + }); + + it('returns correct non-empty value', () => { + const state = createState( + createDebuggerState({ + graphExecutions: createDebuggerGraphExecutionsState({ + graphExecutionData: { + 10: createTestGraphExecution(), + }, + }), + }) + ); + expect(getGraphExecutionData(state)).toEqual({ + 10: createTestGraphExecution(), + }); + }); + }); }); diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store/debugger_types.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store/debugger_types.ts index 5e80e34702..09ca45e2f8 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store/debugger_types.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/store/debugger_types.ts @@ -256,8 +256,14 @@ export interface GraphExecutions extends PagedExecutions { // Intra-graph execution digests the frontend has loaded so far. graphExecutionDigests: {[index: number]: GraphExecutionDigest}; + // Indices to GraphExecution pages currently being loaded. + graphExecutionDataLoadingPages: number[]; + + // Number of items in each `GraphExecution` page that have been loaded. + graphExecutionDataPageLoadedSizes: {[page: number]: number}; + // Detailed data objects about intra-graph execution. - graphExecutionData: {[index: number]: Execution}; + graphExecutionData: {[index: number]: GraphExecution}; } // The state of a loaded DebuggerV2 run. diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/testing/index.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/testing/index.ts index 4465d64b48..f1c00a5c12 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/testing/index.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/testing/index.ts @@ -24,6 +24,7 @@ import { DebuggerState, Execution, Executions, + GraphExecution, ExecutionDigest, GraphExecutions, InfNanAlert, @@ -89,6 +90,22 @@ export function createTestExecutionDigest( }; } +export function createTestGraphExecution( + override?: Partial +): GraphExecution { + return { + op_name: 'test_namescope/TestOp', + op_type: 'TestOp', + output_slot: 0, + graph_id: 'g1', + graph_ids: ['g0', 'g1,'], + device_name: '/GPU:0', + tensor_debug_mode: 2, + debug_tensor_value: [0, 1], + ...override, + }; +} + export function createDebuggerState( override?: Partial ): DebuggerState { diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/BUILD b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/BUILD index ae9a3e0b2b..aa77f493bc 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/BUILD +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/BUILD @@ -21,6 +21,7 @@ ng_module( "//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/webapp/angular:expect_angular_cdk_scrolling", "@npm//@angular/common", "@npm//@angular/core", "@npm//@ngrx/store", diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_component.css b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_component.css index a3d0e697c8..b297ad3e6d 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_component.css +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_component.css @@ -21,3 +21,72 @@ limitations under the License. margin-left: 8px; padding-left: 10px; } + +.graph-execution-index { + color: #757575; + display: inline-block; + width: 40px; + padding-right: 4px; +} + +.graph-executions-viewport { + flex-grow: 1; + font-size: 12px; + width: 100%; + overflow-x: hidden; +} + +.loading-spinner { + display: inline-block; +} + +.op-type { + background-color: #e3e5e8; + border: 1px solid #c0c0c0; + border-radius: 4px; + direction: rtl; + font-family: 'Roboto Mono', monospace; + font-size: 10px; + display: block; + height: 14px; + line-height: 14px; + padding: 1px 3px; + width: max-content; +} + +.tensor-container { + width: 100%; +} + +.tensor-item { + border-bottom: 1px solid rgba(0, 0, 0, 0.12); + display: flex; + flex-wrap: nowrap; + height: 36px; + line-height: 36px; + text-align: left; + vertical-align: top; + white-space: nowrap; + width: 100%; +} + +.tensor-name { + direction: rtl; + display: block; + height: 16px; + line-height: 16px; + overflow: hidden; + padding: 0 2px; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; +} + +.tensor-name-and-op-type { + direction: rtl; + display: inline-block; + overflow: hidden; + padding-right: 8px; + text-align: right; + width: 240px; +} diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_component.ng.html b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_component.ng.html index 6b78f3fe71..68db3b26c3 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_component.ng.html +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_component.ng.html @@ -20,5 +20,39 @@ Graph Executions ({{ numGraphExecutions }})
- + +
+
+
+ {{ i }} +
+
+
+
+ {{ graphExecutionData[i].op_name }}:{{ + graphExecutionData[i].output_slot }} +
+
+ {{ graphExecutionData[i].op_type }} +
+
+
+ + +
+ Loading... + +
+
+
+
+
diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_component.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_component.ts index 7cd3f43230..145960e693 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_component.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_component.ts @@ -13,14 +13,32 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ -import {Component, Input} from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, +} from '@angular/core'; + +import {GraphExecution} from '../../store/debugger_types'; @Component({ selector: 'graph-executions-component', templateUrl: './graph_executions_component.ng.html', styleUrls: ['./graph_executions_component.css'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class GraphExecutionsComponent { @Input() - numGraphExecutions: number | null = null; + numGraphExecutions!: number; + + @Input() + graphExecutionData!: {[index: number]: GraphExecution}; + + @Input() + graphExecutionIndices!: number[]; + + @Output() + onScrolledIndexChange = new EventEmitter(); } diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_container.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_container.ts index 9af09cf15c..cc609ee27c 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_container.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_container.ts @@ -13,9 +13,10 @@ 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 {createSelector, select, Store} from '@ngrx/store'; -import {getNumGraphExecutions} from '../../store'; +import {graphExecutionScrollToIndex} from '../../actions'; +import {getGraphExecutionData, getNumGraphExecutions} from '../../store'; import {State} from '../../store/debugger_types'; /** @typehack */ import * as _typeHackRxjs from 'rxjs'; @@ -25,11 +26,34 @@ import {State} from '../../store/debugger_types'; template: ` `, }) export class GraphExecutionsContainer { readonly numGraphExecutions$ = this.store.pipe(select(getNumGraphExecutions)); + readonly graphExecutionData$ = this.store.pipe(select(getGraphExecutionData)); + + readonly graphExecutionIndices$ = this.store.pipe( + select( + createSelector( + getNumGraphExecutions, + (numGraphExecution: number): number[] | null => { + if (numGraphExecution === 0) { + return null; + } + return Array.from({length: numGraphExecution}).map((_, i) => i); + } + ) + ) + ); + + onScrolledIndexChange(scrolledIndex: number) { + this.store.dispatch(graphExecutionScrollToIndex({index: scrolledIndex})); + } + constructor(private readonly store: Store) {} } diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_container_test.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_container_test.ts index b01ff8e7fd..6e6749f057 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_container_test.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_container_test.ts @@ -16,16 +16,21 @@ limitations under the License. * Unit tests for the the intra-graph execution component and container. */ import {CommonModule} from '@angular/common'; -import {TestBed} from '@angular/core/testing'; +import {fakeAsync, TestBed, tick} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {Store} from '@ngrx/store'; -import {provideMockStore, MockStore} from '@ngrx/store/testing'; +import {MockStore, provideMockStore} from '@ngrx/store/testing'; import {DebuggerComponent} from '../../debugger_component'; import {DebuggerContainer} from '../../debugger_container'; -import {State} from '../../store/debugger_types'; -import {createDebuggerState, createState} from '../../testing'; +import {State, GraphExecution} from '../../store/debugger_types'; +import {getNumGraphExecutions, getGraphExecutionData} from '../../store'; +import { + createDebuggerState, + createState, + createTestGraphExecution, +} from '../../testing'; import {AlertsModule} from '../alerts/alerts_module'; import {ExecutionDataModule} from '../execution_data/execution_data_module'; import {InactiveModule} from '../inactive/inactive_module'; @@ -36,7 +41,6 @@ import {GraphExecutionsContainer} from './graph_executions_container'; import {GraphExecutionsModule} from './graph_executions_module'; /** @typehack */ import * as _typeHackStore from '@ngrx/store'; -import {getNumGraphExecutions} from '../../store'; describe('Graph Executions Container', () => { let store: MockStore; @@ -64,14 +68,93 @@ describe('Graph Executions Container', () => { store = TestBed.get(Store); }); - it('renders number of graph executions', () => { + it('does not render execs viewport if # execs = 0', fakeAsync(() => { + const fixture = TestBed.createComponent(GraphExecutionsContainer); + store.overrideSelector(getNumGraphExecutions, 0); + fixture.autoDetectChanges(); + tick(); + + const titleElement = fixture.debugElement.query( + By.css('.graph-executions-title') + ); + expect(titleElement.nativeElement.innerText).toBe('Graph Executions (0)'); + const viewPort = fixture.debugElement.query( + By.css('.graph-executions-viewport') + ); + expect(viewPort).toBeNull(); + })); + + it('renders # execs and execs viewport if # execs > 0; fully loaded', fakeAsync(() => { const fixture = TestBed.createComponent(GraphExecutionsContainer); store.overrideSelector(getNumGraphExecutions, 120); - fixture.detectChanges(); + const graphExecutionData: {[index: number]: GraphExecution} = {}; + for (let i = 0; i < 120; ++i) { + graphExecutionData[i] = createTestGraphExecution({ + op_name: `TestOp_${i}`, + op_type: `OpType_${i}`, + }); + } + store.overrideSelector(getGraphExecutionData, graphExecutionData); + fixture.autoDetectChanges(); + tick(); const titleElement = fixture.debugElement.query( By.css('.graph-executions-title') ); expect(titleElement.nativeElement.innerText).toBe('Graph Executions (120)'); - }); + const viewPort = fixture.debugElement.query( + By.css('.graph-executions-viewport') + ); + expect(viewPort).not.toBeNull(); + const tensorContainers = fixture.debugElement.queryAll( + By.css('.tensor-container') + ); + expect(tensorContainers.length).toBeGreaterThan(0); + const graphExecutionIndices = fixture.debugElement.queryAll( + By.css('.graph-execution-index') + ); + const tensorNames = fixture.debugElement.queryAll(By.css('.tensor-name')); + const opTypes = fixture.debugElement.queryAll(By.css('.op-type')); + expect(graphExecutionIndices.length).toBe(tensorContainers.length); + expect(tensorNames.length).toBe(tensorContainers.length); + expect(opTypes.length).toBe(tensorContainers.length); + for (let i = 0; i < tensorNames.length; ++i) { + expect(graphExecutionIndices[i].nativeElement.innerText).toBe(`${i}`); + expect(tensorNames[i].nativeElement.innerText).toBe(`TestOp_${i}:0`); + expect(opTypes[i].nativeElement.innerText).toBe(`OpType_${i}`); + } + })); + + it('renders # execs and execs viewport if # execs > 0; not loaded', fakeAsync(() => { + const fixture = TestBed.createComponent(GraphExecutionsContainer); + store.overrideSelector(getNumGraphExecutions, 120); + store.overrideSelector(getGraphExecutionData, {}); + fixture.autoDetectChanges(); + tick(); + + const titleElement = fixture.debugElement.query( + By.css('.graph-executions-title') + ); + expect(titleElement.nativeElement.innerText).toBe('Graph Executions (120)'); + const viewPort = fixture.debugElement.query( + By.css('.graph-executions-viewport') + ); + expect(viewPort).not.toBeNull(); + const tensorContainers = fixture.debugElement.queryAll( + By.css('.tensor-container') + ); + expect(tensorContainers.length).toBeGreaterThan(0); + const graphExecutionIndices = fixture.debugElement.queryAll( + By.css('.graph-execution-index') + ); + const loadingElements = fixture.debugElement.queryAll( + By.css('.loading-spinner') + ); + const tensorNames = fixture.debugElement.queryAll(By.css('.tensor-name')); + const opTypes = fixture.debugElement.queryAll(By.css('.op-type')); + expect(graphExecutionIndices.length).toBe(tensorContainers.length); + expect(loadingElements.length).toBe(tensorContainers.length); + expect(tensorNames.length).toBe(0); + expect(opTypes.length).toBe(0); + })); }); diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_module.ts b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_module.ts index 864a258ae4..5e43f6e91c 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_module.ts +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/graph_executions/graph_executions_module.ts @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. ==============================================================================*/ +import {ScrollingModule} from '@angular/cdk/scrolling'; import {CommonModule} from '@angular/common'; import {NgModule} from '@angular/core'; @@ -21,7 +22,7 @@ import {GraphExecutionsContainer} from './graph_executions_container'; @NgModule({ declarations: [GraphExecutionsComponent, GraphExecutionsContainer], - imports: [CommonModule], + imports: [CommonModule, ScrollingModule], exports: [GraphExecutionsContainer], }) export class GraphExecutionsModule {} 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 681f9bb652..1a4fce8e9d 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 @@ -54,7 +54,7 @@ limitations under the License. border-left: 1px solid rgba(0, 0, 0, 0.12); font-size: 10px; font-family: 'Roboto Mono', monospace; - height: 360px; + height: 100%; margin-left: 8px; max-height: 360px; overflow-x: hidden; diff --git a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/timeline/timeline_component.ng.html b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/timeline/timeline_component.ng.html index a836b8891b..7e2e44c338 100644 --- a/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/timeline/timeline_component.ng.html +++ b/tensorboard/plugins/debugger_v2/tf_debugger_v2_plugin/views/timeline/timeline_component.ng.html @@ -16,7 +16,7 @@ -->
-
Execution Timeline
+
Eager Execution Timeline
diff --git a/tensorboard/webapp/angular/BUILD b/tensorboard/webapp/angular/BUILD index 659df158b6..2a5ff6399a 100644 --- a/tensorboard/webapp/angular/BUILD +++ b/tensorboard/webapp/angular/BUILD @@ -145,6 +145,15 @@ tf_ts_library( ], ) +# This is a dummy rule used as a @angular/cdk/scrolling dependency. +tf_ts_library( + name = "expect_angular_cdk_scrolling", + srcs = [], + deps = [ + "@npm//@angular/cdk", + ], +) + # This is a dummy rule used as a @ngrx/store/testing dependency. # This is not a replacement for @ngrx/store dependency. tf_ts_library(