UNPKG

chrome-devtools-frontend

Version:
1,068 lines (990 loc) • 64.7 kB
// Copyright 2023 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import type * as Common from '../../../../core/common/common.js'; import type * as Platform from '../../../../core/platform/platform.js'; import * as Trace from '../../../../models/trace/trace.js'; import * as Extensions from '../../../../panels/timeline/extensions/extensions.js'; import {assertScreenshot, raf, renderElementIntoDOM} from '../../../../testing/DOMHelpers.js'; import {describeWithEnvironment} from '../../../../testing/EnvironmentHelpers.js'; import { FakeFlameChartProvider, MockFlameChartDelegate, renderFlameChartIntoDOM, renderFlameChartWithFakeProvider, } from '../../../../testing/TraceHelpers.js'; import {TraceLoader} from '../../../../testing/TraceLoader.js'; import * as PerfUI from './perf_ui.js'; describeWithEnvironment('FlameChart', () => { it('sorts decorations, putting candy striping before warning triangles', async () => { const decorations: PerfUI.FlameChart.FlameChartDecoration[] = [ {type: PerfUI.FlameChart.FlameChartDecorationType.WARNING_TRIANGLE}, {type: PerfUI.FlameChart.FlameChartDecorationType.CANDY, startAtTime: Trace.Types.Timing.Micro(10)}, ]; PerfUI.FlameChart.sortDecorationsForRenderingOrder(decorations); assert.deepEqual(decorations, [ {type: PerfUI.FlameChart.FlameChartDecorationType.CANDY, startAtTime: Trace.Types.Timing.Micro(10)}, {type: PerfUI.FlameChart.FlameChartDecorationType.WARNING_TRIANGLE}, ]); }); let chartInstance: PerfUI.FlameChart.FlameChart|null = null; afterEach(() => { if (chartInstance) { chartInstance.detach(); } }); function renderChart(chart: PerfUI.FlameChart.FlameChart): void { const container = document.createElement('div'); renderElementIntoDOM(container); chart.markAsRoot(); chart.show(container); chart.update(); } const defaultGroupStyle = { height: 17, padding: 4, collapsible: false, color: 'black', backgroundColor: 'grey', nestingLevel: 0, itemsHeight: 17, }; class FakeProvider extends FakeFlameChartProvider { override entryColor(_entryIndex: number): string { return 'red'; } override timelineData(): PerfUI.FlameChart.FlameChartTimelineData|null { return PerfUI.FlameChart.FlameChartTimelineData.create({ entryLevels: [1, 1, 1], entryStartTimes: [5, 60, 80], entryTotalTimes: [50, 10, 10], groups: [{ name: 'Test Group' as Platform.UIString.LocalizedString, startLevel: 1, style: defaultGroupStyle, }], }); } } it('notifies the delegate when the window has changed', async () => { const provider = new FakeProvider(); const delegate = new MockFlameChartDelegate(); const windowChangedSpy = sinon.spy(delegate, 'windowChanged'); chartInstance = new PerfUI.FlameChart.FlameChart(provider, delegate); renderChart(chartInstance); chartInstance.windowChanged(0, 5, false); sinon.assert.calledWith(windowChangedSpy, 0, 5, false); }); it('notifies the delegate when the range selection has changed', async () => { const provider = new FakeProvider(); const delegate = new MockFlameChartDelegate(); const updateRangeSpy = sinon.spy(delegate, 'updateRangeSelection'); chartInstance = new PerfUI.FlameChart.FlameChart(provider, delegate); renderChart(chartInstance); chartInstance.updateRangeSelection(0, 5); sinon.assert.calledWith(updateRangeSpy, 0, 5); }); describe('setSelectedEntry', () => { class SetSelectedEntryTestProvider extends FakeFlameChartProvider { override entryColor(_entryIndex: number): string { return 'red'; } override timelineData(): PerfUI.FlameChart.FlameChartTimelineData|null { return PerfUI.FlameChart.FlameChartTimelineData.create({ entryLevels: [1, 1, 1, 1], entryStartTimes: [5, 60, 80, 300], entryTotalTimes: [50, 10, 10, 500], groups: [{ name: 'Test Group' as Platform.UIString.LocalizedString, startLevel: 1, style: defaultGroupStyle, }], }); } } it('does not change the time window if the selected entry is already revealed', async () => { const provider = new SetSelectedEntryTestProvider(); const delegate = new MockFlameChartDelegate(); const windowChangedSpy = sinon.spy(delegate, 'windowChanged'); chartInstance = new PerfUI.FlameChart.FlameChart(provider, delegate); // Make the window wide so lots is visible chartInstance.setSize(800, 400); chartInstance.setWindowTimes(0, 100); renderChart(chartInstance); // Pick the first event which is only 50ms long and therefore should be in view already. chartInstance.setSelectedEntry(0); sinon.assert.callCount(windowChangedSpy, 0); }); it('will change the window time to reveal the selected entry when the entry is off the right of the screen', async () => { const provider = new SetSelectedEntryTestProvider(); const delegate = new MockFlameChartDelegate(); const windowChangedSpy = sinon.spy(delegate, 'windowChanged'); chartInstance = new PerfUI.FlameChart.FlameChart(provider, delegate); // Make the width narrow so that not everything fits chartInstance.setSize(100, 400); // Ensure the event we want to select is out of the viewport by selecting the first 100ms. chartInstance.setWindowTimes(0, 100); renderChart(chartInstance); chartInstance.setSelectedEntry(3); sinon.assert.calledOnceWithExactly(windowChangedSpy, 300, 400, true); }); it('will change the window time to reveal the selected entry when the entry is off the left of the screen', async () => { const provider = new SetSelectedEntryTestProvider(); const delegate = new MockFlameChartDelegate(); const windowChangedSpy = sinon.spy(delegate, 'windowChanged'); chartInstance = new PerfUI.FlameChart.FlameChart(provider, delegate); // Make the width narrow so that not everything fits chartInstance.setSize(100, 400); // Ensure the event we want to select is out of the viewport by selecting the last 200ms chartInstance.setWindowTimes(250, 600); renderChart(chartInstance); chartInstance.setSelectedEntry(0); sinon.assert.calledOnceWithExactly(windowChangedSpy, 5, 355, true); }); }); describe('highlightEntry', () => { it('updates the chart to highlight the entry and dispatches an event', async () => { const provider = new FakeProvider(); const delegate = new MockFlameChartDelegate(); chartInstance = new PerfUI.FlameChart.FlameChart(provider, delegate); renderChart(chartInstance); const highlightedEventListener = sinon.stub(); chartInstance.addEventListener(PerfUI.FlameChart.Events.ENTRY_HOVERED, highlightedEventListener); // Nothing highlighted, so the highlightElement should be hidden. assert.isTrue(chartInstance.highlightElement.classList.contains('hidden')); const entryIndexToHighlight = 2; chartInstance.highlightEntry(entryIndexToHighlight); // Ensure that the highlighted div is positioned. We cannot assert exact // pixels due to differences in screen sizes and resolution across // machines, but we can ensure that they have all been set. assert.exists(chartInstance.highlightElement.style.height); assert.exists(chartInstance.highlightElement.style.top); assert.exists(chartInstance.highlightElement.style.left); assert.exists(chartInstance.highlightElement.style.width); // And that it is not hidden. assert.isFalse(chartInstance.highlightElement.classList.contains('hidden')); // Ensure that the event listener was called with the right index sinon.assert.callCount(highlightedEventListener, 1); const event = highlightedEventListener.args[0][0] as Common.EventTarget.EventTargetEvent<number>; assert.strictEqual(event.data, entryIndexToHighlight); }); it('does nothing if the entry is already highlighted', async () => { const provider = new FakeProvider(); const delegate = new MockFlameChartDelegate(); chartInstance = new PerfUI.FlameChart.FlameChart(provider, delegate); renderChart(chartInstance); const highlightedEventListener = sinon.stub(); chartInstance.addEventListener(PerfUI.FlameChart.Events.ENTRY_HOVERED, highlightedEventListener); chartInstance.highlightEntry(2); chartInstance.highlightEntry(2); // Ensure that there is only one event listener called, despite the // highlightEntry method being called twice, because it was called with // the same ID. sinon.assert.callCount(highlightedEventListener, 1); }); it('does nothing if the DataProvider entryColor() method returns a falsey value', async () => { class EmptyColorProvider extends FakeProvider { override entryColor(): string { return ''; } } const provider = new EmptyColorProvider(); const delegate = new MockFlameChartDelegate(); chartInstance = new PerfUI.FlameChart.FlameChart(provider, delegate); renderChart(chartInstance); const highlightedEventListener = sinon.stub(); chartInstance.addEventListener(PerfUI.FlameChart.Events.ENTRY_HOVERED, highlightedEventListener); chartInstance.highlightEntry(2); // No calls because entryColor returned a false value. sinon.assert.callCount(highlightedEventListener, 0); }); it('dispatches the highlight event with an ID of -1 when the highlight is hidden', async () => { const provider = new FakeProvider(); const delegate = new MockFlameChartDelegate(); chartInstance = new PerfUI.FlameChart.FlameChart(provider, delegate); renderChart(chartInstance); const highlightedEventListener = sinon.stub(); chartInstance.addEventListener(PerfUI.FlameChart.Events.ENTRY_HOVERED, highlightedEventListener); chartInstance.highlightEntry(2); chartInstance.hideHighlight(); // Ensure the argument to the last event listener call was -1 const event = highlightedEventListener.args[1][0] as Common.EventTarget.EventTargetEvent<number>; assert.strictEqual(event.data, -1); }); }); describe('updateLevelPositions', () => { class UpdateLevelPositionsTestProvider extends FakeFlameChartProvider { static data = PerfUI.FlameChart.FlameChartTimelineData.create({ entryLevels: [0, 1, 2], entryStartTimes: [5, 60, 80], entryTotalTimes: [50, 10, 10], groups: [ { name: 'Test Group 0' as Platform.UIString.LocalizedString, startLevel: 0, style: defaultGroupStyle, }, { name: 'Test Group 1' as Platform.UIString.LocalizedString, startLevel: 1, style: defaultGroupStyle, }, { name: 'Test Group 2' as Platform.UIString.LocalizedString, startLevel: 2, style: {...defaultGroupStyle, collapsible: true, nestingLevel: 1}, }, ], }); override timelineData(): PerfUI.FlameChart.FlameChartTimelineData|null { return UpdateLevelPositionsTestProvider.data; } } it('Calculate the level position correctly', () => { const provider = new UpdateLevelPositionsTestProvider(); const delegate = new MockFlameChartDelegate(); chartInstance = new PerfUI.FlameChart.FlameChart(provider, delegate); renderChart(chartInstance); // For Group 0, it is expanded (not collapsible), // so its offset is 17(RulerHeight + 2) assert.strictEqual(chartInstance.groupIndexToOffsetForTest(0), 17); // For Level 0, it is in Test Group 1, and the group is expanded (not collapsible), // so its offset is 17(Group offset) + 17(group header height) = 34 assert.isTrue(chartInstance.levelIsVisible(0)); assert.strictEqual(chartInstance.levelToOffset(0), 34); // For Group 1, its offset is // 34(level 0 offset) + 17(level 0 height) + 4(style.padding) = 55 assert.strictEqual(chartInstance.groupIndexToOffsetForTest(1), 55); // For Level 1, it is in Group 1, and the group is expanded by default, // so its offset is 55(Group offset) + 17(group header height) = 72 assert.isTrue(chartInstance.levelIsVisible(1)); assert.strictEqual(chartInstance.levelToOffset(1), 72); // For Group 2, it is nested in Group 1, so its offset is // 72(level 1 offset) + 17(level 1 is visible) + 0(no style.padding because nested) = 89 assert.strictEqual(chartInstance.groupIndexToOffsetForTest(2), 89); // For Level 2, it is in Group 2, and the group is not expanded by default (collapsible), // so its offset is 89(Group offset) + 17(group header) = 106 assert.isFalse(chartInstance.levelIsVisible(2)); assert.strictEqual(chartInstance.levelToOffset(2), 106); // For Group 3 and Level 3, they are "fake" group and level, and are used to show then end of the flame chart. // Since Level 2 is invisible (collapsed), so this one has same offset as Level 2. assert.strictEqual(chartInstance.groupIndexToOffsetForTest(3), 106); assert.strictEqual(chartInstance.levelToOffset(3), 106); }); it('Calculate the level position correctly after hide and unhide a group without nested group', () => { const provider = new UpdateLevelPositionsTestProvider(); const delegate = new MockFlameChartDelegate(); chartInstance = new PerfUI.FlameChart.FlameChart(provider, delegate); renderChart(chartInstance); chartInstance.hideGroup(/* groupIndex= */ 0); // For Group 0, it is hidden, so its offset is 17(RulerHeight + 2) assert.strictEqual(chartInstance.groupIndexToOffsetForTest(0), 17); // For Level 0, it is in Test Group 1, and the group is hidden, // so its offset is same as group offset assert.isFalse(chartInstance.levelIsVisible(0)); assert.strictEqual(chartInstance.levelToOffset(0), 17); // For Group 1, its offset is // 17(level 0 offset) + 0(level 0 is hidden) + 4(style.padding) = 21 assert.strictEqual(chartInstance.groupIndexToOffsetForTest(1), 21); // For Level 1, it is in Group 1, and the group is expanded by default, // so its offset is 21(Group offset) + 17(group header height) = 38 assert.isTrue(chartInstance.levelIsVisible(1)); assert.strictEqual(chartInstance.levelToOffset(1), 38); // For Group 2, it is nested in Group 1, so its offset is // 38(level 1 offset) + 17(level 1 is visible) + 0(no style.padding because nested) = 55 assert.strictEqual(chartInstance.groupIndexToOffsetForTest(2), 55); // For Level 2, it is in Group 2, and the group is not expanded by default (collapsible), // so its offset is 55(Group offset) + 17(group header) = 72 assert.isTrue(chartInstance.levelIsVisible(1)); assert.strictEqual(chartInstance.levelToOffset(2), 72); // For Group 3 and Level 3, they are "fake" group and level, and are used to show then end of the flame chart. // Since Level 2 is invisible (collapsed), so this one has same offset as Level 2. assert.strictEqual(chartInstance.groupIndexToOffsetForTest(3), 72); assert.strictEqual(chartInstance.levelToOffset(3), 72); // Unhide Group 0, so the offset should be same as default (see test "Calculate the level position correctly"). chartInstance.showGroup(/* groupIndex= */ 0); assert.strictEqual(chartInstance.groupIndexToOffsetForTest(0), 17); assert.isTrue(chartInstance.levelIsVisible(0)); assert.strictEqual(chartInstance.levelToOffset(0), 34); assert.strictEqual(chartInstance.groupIndexToOffsetForTest(1), 55); assert.isTrue(chartInstance.levelIsVisible(1)); assert.strictEqual(chartInstance.levelToOffset(1), 72); assert.strictEqual(chartInstance.groupIndexToOffsetForTest(2), 89); assert.isFalse(chartInstance.levelIsVisible(2)); assert.strictEqual(chartInstance.levelToOffset(2), 106); assert.strictEqual(chartInstance.groupIndexToOffsetForTest(3), 106); assert.strictEqual(chartInstance.levelToOffset(3), 106); }); describe('hide/unhide nested group', () => { class UpdateLevelPositionsWithNestedGroupTestProvider extends FakeFlameChartProvider { // Define this data statically; otherwise on each call to // timelineData() it is recreated and we lose any state such as the // group hidden / expanded state. This reproduces what we do in // production too; calculating timelineData() is expensive so we want // to do it as infrequently as possible. static data = PerfUI.FlameChart.FlameChartTimelineData.create({ entryLevels: [0, 1, 2], entryStartTimes: [5, 60, 80], entryTotalTimes: [50, 10, 10], groups: [ { name: 'Test Group 0' as Platform.UIString.LocalizedString, startLevel: 0, style: defaultGroupStyle, }, { name: 'Test Group 1' as Platform.UIString.LocalizedString, startLevel: 1, style: defaultGroupStyle, }, // Make the nested group always expanded for better testing the nested case { name: 'Test Group 2' as Platform.UIString.LocalizedString, startLevel: 2, style: {...defaultGroupStyle, nestingLevel: 1}, }, ], }); override timelineData(): PerfUI.FlameChart.FlameChartTimelineData|null { return UpdateLevelPositionsWithNestedGroupTestProvider.data; } } it('Calculate the level position correctly after hide and unhide a group with nested group', () => { const provider = new UpdateLevelPositionsWithNestedGroupTestProvider(); const delegate = new MockFlameChartDelegate(); chartInstance = new PerfUI.FlameChart.FlameChart(provider, delegate); renderChart(chartInstance); chartInstance.hideGroup(/* groupIndex= */ 1); // For Group 0, it is expanded (not collapsible), // so its offset is 17(RulerHeight + 2) assert.strictEqual(chartInstance.groupIndexToOffsetForTest(0), 17); // For Level 0, it is in Test Group 1, and the group is expanded (not collapsible), // so its offset is 17(Group offset) + 17(group header height) = 34 assert.isTrue(chartInstance.levelIsVisible(0)); assert.strictEqual(chartInstance.levelToOffset(0), 34); // For Group 1, it is hidden, so its offset is // 34(level 0 offset) + 17(level 0 height) = 51 assert.strictEqual(chartInstance.groupIndexToOffsetForTest(1), 51); // For Level 1, it is in Group 1, and the group is hidden, // so its offset is 51(Group offset) assert.isFalse(chartInstance.levelIsVisible(1)); assert.strictEqual(chartInstance.levelToOffset(1), 51); // For Group 2, it is nested in Group 1, so it is also hidden, so its offset is // 51(level 1 offset) + 0(level 1 is invisible) = 51 assert.strictEqual(chartInstance.groupIndexToOffsetForTest(2), 51); // For Level 2, it is in Group 2, and the group is hidden, // so its offset is 51(Group offset) assert.isFalse(chartInstance.levelIsVisible(2)); assert.strictEqual(chartInstance.levelToOffset(2), 51); // For Group 3 and Level 3, they are "fake" group and level, and are used to show then end of the flame chart. // Since Level 2 is invisible (hidden), so this one has same offset as Level 2. assert.strictEqual(chartInstance.groupIndexToOffsetForTest(3), 51); assert.strictEqual(chartInstance.levelToOffset(3), 51); // Unhide Group 1, so the offset should be same as default (see test "Calculate the level position correctly"). chartInstance.showGroup(/* groupIndex= */ 1); assert.strictEqual(chartInstance.groupIndexToOffsetForTest(0), 17); assert.isTrue(chartInstance.levelIsVisible(0)); assert.strictEqual(chartInstance.levelToOffset(0), 34); assert.strictEqual(chartInstance.groupIndexToOffsetForTest(1), 55); assert.isTrue(chartInstance.levelIsVisible(1)); assert.strictEqual(chartInstance.levelToOffset(1), 72); assert.strictEqual(chartInstance.groupIndexToOffsetForTest(2), 89); // Slightly different because Group2 is not longer collapsible. // For Level 2, it is in Group 2, and the group is expanded, // so its offset is 89(Group offset) + 17(group header) = 106 assert.isTrue(chartInstance.levelIsVisible(2)); assert.strictEqual(chartInstance.levelToOffset(2), 106); // For Group 3 and Level 3, they are "fake" group and level, and are used to show then end of the flame chart. // Since Level 2 is visible, so its offset is 106(Group offset) + 17(Level 2 height) = 123 assert.strictEqual(chartInstance.groupIndexToOffsetForTest(3), 123); assert.strictEqual(chartInstance.levelToOffset(3), 123); }); it('Calculate the level position correctly after hide and unhide a nested group', () => { const provider = new UpdateLevelPositionsWithNestedGroupTestProvider(); const delegate = new MockFlameChartDelegate(); chartInstance = new PerfUI.FlameChart.FlameChart(provider, delegate); renderChart(chartInstance); chartInstance.hideGroup(/* groupIndex= */ 2); // For Group 0, it is expanded (not collapsible), // so its offset is 17(RulerHeight + 2) assert.strictEqual(chartInstance.groupIndexToOffsetForTest(0), 17); // For Level 0, it is in Test Group 1, and the group is expanded (not collapsible), // so its offset is 17(Group offset) + 17(group header height) = 34 assert.isTrue(chartInstance.levelIsVisible(0)); assert.strictEqual(chartInstance.levelToOffset(0), 34); // For Group 1, it is hidden, so its offset is // 34(level 0 offset) + 17(level 0 height) + 4(style.padding) = 55 assert.strictEqual(chartInstance.groupIndexToOffsetForTest(1), 55); // For Level 1, it is in Group 1, and the group is expanded by default, // so its offset is 55(Group offset) + 17(group header height) = 72 assert.isTrue(chartInstance.levelIsVisible(1)); assert.strictEqual(chartInstance.levelToOffset(1), 72); // For Group 2, it is nested in Group 1, and it is set to hidden, so its offset is // 72(level 1 offset) + 17(level 1 is visible) = 89 assert.strictEqual(chartInstance.groupIndexToOffsetForTest(2), 89); // For Level 2, it is in Group 2, and the group is hidden, // so its offset is 51(Group offset) assert.isFalse(chartInstance.levelIsVisible(2)); assert.strictEqual(chartInstance.levelToOffset(2), 89); // For Group 3 and Level 3, they are "fake" group and level, and are used to show then end of the flame chart. // Since Level 2 is invisible (hidden), so this one has same offset as Level 2. assert.strictEqual(chartInstance.groupIndexToOffsetForTest(3), 89); assert.strictEqual(chartInstance.levelToOffset(3), 89); // Unhide Group 1, so the offset should be same as default (see test "Calculate the level position correctly"). chartInstance.showGroup(/* groupIndex= */ 2); assert.strictEqual(chartInstance.groupIndexToOffsetForTest(0), 17); assert.isTrue(chartInstance.levelIsVisible(0)); assert.strictEqual(chartInstance.levelToOffset(0), 34); assert.strictEqual(chartInstance.groupIndexToOffsetForTest(1), 55); assert.isTrue(chartInstance.levelIsVisible(1)); assert.strictEqual(chartInstance.levelToOffset(1), 72); assert.strictEqual(chartInstance.groupIndexToOffsetForTest(2), 89); // Slightly different because Group2 is not longer collapsible. // For Level 2, it is in Group 2, and the group is expanded, // so its offset is 89(Group offset) + 17(group header) = 106 assert.isTrue(chartInstance.levelIsVisible(2)); assert.strictEqual(chartInstance.levelToOffset(2), 106); // For Group 3 and Level 3, they are "fake" group and level, and are used to show then end of the flame chart. // Since Level 2 is visible, so its offset is 106(Group offset) + 17(Level 2 height) = 123 assert.strictEqual(chartInstance.groupIndexToOffsetForTest(3), 123); assert.strictEqual(chartInstance.levelToOffset(3), 123); }); }); }); describe('Index to/from coordinates coversion', () => { class IndexAndCoordinatesConversionTestProvider extends FakeFlameChartProvider { override entryColor(_entryIndex: number): string { return 'red'; } override maxStackDepth(): number { return 2; } override timelineData(): PerfUI.FlameChart.FlameChartTimelineData|null { return PerfUI.FlameChart.FlameChartTimelineData.create({ entryLevels: [0, 0, 1, 1], entryStartTimes: [5, 60, 80, 300], entryTotalTimes: [50, 10, 10, 500], groups: [ { name: 'Test Group' as Platform.UIString.LocalizedString, startLevel: 0, style: defaultGroupStyle, }, { name: 'Test Group 1' as Platform.UIString.LocalizedString, startLevel: 1, style: defaultGroupStyle, }, ], }); } } describe('entryIndexToCoordinates', () => { it('returns the correct coordinates for a given entry', () => { const provider = new IndexAndCoordinatesConversionTestProvider(); const delegate = new MockFlameChartDelegate(); chartInstance = new PerfUI.FlameChart.FlameChart(provider, delegate); // Make the width narrow so that not everything fits chartInstance.setSize(100, 400); chartInstance.setWindowTimes(0, 100); renderChart(chartInstance); const timelineData = chartInstance.timelineData(); if (!timelineData) { throw new Error('Could not find timeline data'); } const entryIndex = 0; const {x: canvasOffsetX, y: canvasOffsetY} = chartInstance.getCanvasOffset(); // TODO(crbug.com/1440169): We can get all the expected values from // the chart's data and avoid magic numbers const initialXPosition = chartInstance.computePosition(timelineData.entryStartTimes[entryIndex]); assert.deepEqual( chartInstance.entryIndexToCoordinates(entryIndex), // For index 0, it is in level 0, so vertically there are only the ruler(17) and the // header of Group 0 (17) and beyond it. {x: initialXPosition + canvasOffsetX, y: 34 + canvasOffsetY + chartInstance.getScrollOffset()}); }); it('returns the correct coordinates after re-order', () => { const provider = new IndexAndCoordinatesConversionTestProvider(); const delegate = new MockFlameChartDelegate(); chartInstance = new PerfUI.FlameChart.FlameChart(provider, delegate); // Make the width narrow so that not everything fits chartInstance.setSize(100, 400); chartInstance.setWindowTimes(0, 100); renderChart(chartInstance); const timelineData = chartInstance.timelineData(); if (!timelineData) { throw new Error('Could not find timeline data'); } const entryIndex = 0; const {x: canvasOffsetX, y: canvasOffsetY} = chartInstance.getCanvasOffset(); // TODO(crbug.com/1440169): We can get all the expected values from // the chart's data and avoid magic numbers const initialXPosition = chartInstance.computePosition(timelineData.entryStartTimes[entryIndex]); assert.deepEqual( chartInstance.entryIndexToCoordinates(entryIndex), // For index 0, it is in level 0, so vertically there are only the ruler(17) and the // header of Group 0 (17) and beyond it. {x: initialXPosition + canvasOffsetX, y: 34 + canvasOffsetY + chartInstance.getScrollOffset()}); chartInstance.moveGroupDown(0); assert.deepEqual( chartInstance.entryIndexToCoordinates(entryIndex), // Move Group 0 down. So for index 0, it is in level 1, so vertically there are the ruler(17), the header of // Group 1 (17), level 1(inside Group 1, 17), padding of Group 0(4), and header of Group 0 (17)beyond it. {x: initialXPosition + canvasOffsetX, y: 72 + canvasOffsetY + chartInstance.getScrollOffset()}); }); }); describe('coordinatesToEntryIndex', () => { it('returns the correct entry index for given coordinates', () => { const provider = new IndexAndCoordinatesConversionTestProvider(); const delegate = new MockFlameChartDelegate(); chartInstance = new PerfUI.FlameChart.FlameChart(provider, delegate); // Make the width narrow so that not everything fits chartInstance.setSize(100, 400); chartInstance.setWindowTimes(0, 100); renderChart(chartInstance); const timelineData = chartInstance.timelineData(); if (!timelineData) { throw new Error('Could not find timeline data'); } const startXPosition = chartInstance.computePosition(timelineData.entryStartTimes[0]); const beforeStartXPosition = chartInstance.computePosition(timelineData.entryStartTimes[0] - 1); const endXPosition = chartInstance.computePosition(timelineData.entryStartTimes[0] + timelineData.entryTotalTimes[0]); const afterEndXPosition = chartInstance.computePosition(timelineData.entryStartTimes[0] + timelineData.entryTotalTimes[0] + 1); // For index 0, it is in level 0, so vertically there are only the ruler(17) and the // header of Group 0 (17) and beyond it. // And the height of level 0 is 17. // So the index 0 can be mapped from // x: around startXPosition to endXPosition, the reason is x is related to zoom ratio and has some rounds // during calculation. // y: 34(inclusive) to 51(exclusive) assert.strictEqual(chartInstance.coordinatesToEntryIndex(beforeStartXPosition + 1, 34), -1); assert.strictEqual(chartInstance.coordinatesToEntryIndex(startXPosition, 34), 0); assert.strictEqual(chartInstance.coordinatesToEntryIndex(endXPosition, 34), 0); assert.strictEqual(chartInstance.coordinatesToEntryIndex(afterEndXPosition + 3, 34), -1); assert.strictEqual(chartInstance.coordinatesToEntryIndex(startXPosition, 33), -1); assert.strictEqual(chartInstance.coordinatesToEntryIndex(startXPosition, 34), 0); assert.strictEqual(chartInstance.coordinatesToEntryIndex(startXPosition, 50), 0); assert.strictEqual(chartInstance.coordinatesToEntryIndex(startXPosition, 51), -1); }); it('returns the correct entry index for given coordinates after re-order', () => { const provider = new IndexAndCoordinatesConversionTestProvider(); const delegate = new MockFlameChartDelegate(); chartInstance = new PerfUI.FlameChart.FlameChart(provider, delegate); // Make the width narrow so that not everything fits chartInstance.setSize(100, 400); chartInstance.setWindowTimes(0, 100); renderChart(chartInstance); const timelineData = chartInstance.timelineData(); if (!timelineData) { throw new Error('Could not find timeline data'); } const startXPosition = chartInstance.computePosition(timelineData.entryStartTimes[0]); chartInstance.moveGroupDown(0); // Ro-order group will only affect the vertical offsets, so we just need to test |y|. // Move Group 0 down. So for index 0, it is in level 1, so vertically there are the ruler(17), the header of // Group 1 (17), level 1(inside Group 1, 17), padding of Group 0(4), and header of Group 0 (17)beyond it. // And the height of level 0 is 17. // So the entry 0 can be mapped from // y: 72(inclusive) to 89(exclusive) assert.strictEqual(chartInstance.coordinatesToEntryIndex(startXPosition, 71), -1); assert.strictEqual(chartInstance.coordinatesToEntryIndex(startXPosition, 72), 0); assert.strictEqual(chartInstance.coordinatesToEntryIndex(startXPosition, 88), 0); assert.strictEqual(chartInstance.coordinatesToEntryIndex(startXPosition, 89), -1); }); }); describe('coordinatesToGroupIndexAndHoverType', () => { it('returns the correct group index for given coordinates', () => { const provider = new IndexAndCoordinatesConversionTestProvider(); const delegate = new MockFlameChartDelegate(); chartInstance = new PerfUI.FlameChart.FlameChart(provider, delegate); // Make the width narrow so that not everything fits chartInstance.setSize(100, 400); chartInstance.setWindowTimes(0, 100); renderChart(chartInstance); const timelineData = chartInstance.timelineData(); if (!timelineData) { throw new Error('Could not find timeline data'); } // For group 0, vertically there are only the ruler(17) beyond it. So it starts from 17. // For group 1, vertically there are only the ruler(17), header of Group 0 (17), level 0(17), padding of // Group 1(4) and header beyond it. So it starts from 55. // So the group 0 can be mapped from // x: any inside the view // y: 17(inclusive) to 55(exclusive) assert.deepEqual( chartInstance.coordinatesToGroupIndexAndHoverType( /* HEADER_LEFT_PADDING + EDIT_BUTTON_SIZE */ 22, 16), {groupIndex: -1, hoverType: PerfUI.FlameChart.HoverType.OUTSIDE_TRACKS}); assert.deepEqual( chartInstance.coordinatesToGroupIndexAndHoverType( /* HEADER_LEFT_PADDING + EDIT_BUTTON_SIZE */ 22, 17), {groupIndex: 0, hoverType: PerfUI.FlameChart.HoverType.INSIDE_TRACK_HEADER}); assert.deepEqual( chartInstance.coordinatesToGroupIndexAndHoverType( /* HEADER_LEFT_PADDING + EDIT_BUTTON_SIZE */ 22, 50), {groupIndex: 0, hoverType: PerfUI.FlameChart.HoverType.INSIDE_TRACK}); assert.deepEqual( chartInstance.coordinatesToGroupIndexAndHoverType( /* HEADER_LEFT_PADDING + EDIT_BUTTON_SIZE */ 22, 55), {groupIndex: 1, hoverType: PerfUI.FlameChart.HoverType.INSIDE_TRACK_HEADER}); }); it('returns the correct group index for given coordinates after re-order', () => { const provider = new IndexAndCoordinatesConversionTestProvider(); const delegate = new MockFlameChartDelegate(); chartInstance = new PerfUI.FlameChart.FlameChart(provider, delegate); // Make the width narrow so that not everything fits chartInstance.setSize(100, 400); chartInstance.setWindowTimes(0, 100); renderChart(chartInstance); const timelineData = chartInstance.timelineData(); if (!timelineData) { throw new Error('Could not find timeline data'); } chartInstance.moveGroupDown(0); // Ro-order group will only affect the vertical offsets, so we just need to test |y|. // Move Group 0 down. So for group 0, vertically there are only the ruler(17), header of Group 1 (17), // level 1(17), padding of Group 0(4) and header beyond it. So it starts from 55. // And now the Group 0 is the last group, so the end of the Group 0 is 55 + header of Group 0(17) + level 0(17) // = 89 // So the entry 0 can be mapped from // y: 55(inclusive) to 89(exclusive) // Now Group 1 will be before Group 0. so (y)54 will be mapped to Group 1 assert.deepEqual( chartInstance.coordinatesToGroupIndexAndHoverType( /* HEADER_LEFT_PADDING + EDIT_BUTTON_SIZE */ 22, 54), {groupIndex: 1, hoverType: PerfUI.FlameChart.HoverType.INSIDE_TRACK}); assert.deepEqual( chartInstance.coordinatesToGroupIndexAndHoverType( /* HEADER_LEFT_PADDING + EDIT_BUTTON_SIZE */ 22, 55), {groupIndex: 0, hoverType: PerfUI.FlameChart.HoverType.INSIDE_TRACK_HEADER}); assert.deepEqual( chartInstance.coordinatesToGroupIndexAndHoverType( /* HEADER_LEFT_PADDING + EDIT_BUTTON_SIZE */ 22, 88), {groupIndex: 0, hoverType: PerfUI.FlameChart.HoverType.INSIDE_TRACK}); assert.deepEqual( chartInstance.coordinatesToGroupIndexAndHoverType( /* HEADER_LEFT_PADDING + EDIT_BUTTON_SIZE */ 22, 89), {groupIndex: -1, hoverType: PerfUI.FlameChart.HoverType.OUTSIDE_TRACKS}); }); it('returns the correct group index and the icon type for given coordinates', () => { const provider = new IndexAndCoordinatesConversionTestProvider(); const delegate = new MockFlameChartDelegate(); chartInstance = new PerfUI.FlameChart.FlameChart(provider, delegate); // Make the width narrow so that not everything fits chartInstance.setSize(100, 400); chartInstance.setWindowTimes(0, 100); renderChart(chartInstance); const timelineData = chartInstance.timelineData(); if (!timelineData) { throw new Error('Could not find timeline data'); } const context = (chartInstance.getCanvas().getContext('2d') as CanvasRenderingContext2D); const labelWidth = chartInstance.labelWidthForGroup(context, provider.timelineData()?.groups[0]!); // Start of the view assert.deepEqual( chartInstance.coordinatesToGroupIndexAndHoverType(0, 17), {groupIndex: 0, hoverType: PerfUI.FlameChart.HoverType.INSIDE_TRACK_HEADER}); // End of the title label, For title label checking, the end is included. const endOfTitle = /* HEADER_LEFT_PADDING */ 6 + labelWidth; assert.deepEqual( chartInstance.coordinatesToGroupIndexAndHoverType(endOfTitle, 17), {groupIndex: 0, hoverType: PerfUI.FlameChart.HoverType.INSIDE_TRACK_HEADER}); assert.deepEqual( chartInstance.coordinatesToGroupIndexAndHoverType(endOfTitle + 1, 17), {groupIndex: 0, hoverType: PerfUI.FlameChart.HoverType.INSIDE_TRACK}); chartInstance.setEditModeForTest(true); // Start of the view (before the first icon). Will return the track header. assert.deepEqual( chartInstance.coordinatesToGroupIndexAndHoverType(0, 17), {groupIndex: 0, hoverType: PerfUI.FlameChart.HoverType.INSIDE_TRACK_HEADER}); // First icon (Up) assert.deepEqual( chartInstance.coordinatesToGroupIndexAndHoverType(/* HEADER_LEFT_PADDING */ 6, 17), {groupIndex: 0, hoverType: PerfUI.FlameChart.HoverType.TRACK_CONFIG_UP_BUTTON}); // Second icon (Down) assert.deepEqual( chartInstance.coordinatesToGroupIndexAndHoverType( /* HEADER_LEFT_PADDING + EDIT_BUTTON_SIZE */ 25, 17), {groupIndex: 0, hoverType: PerfUI.FlameChart.HoverType.TRACK_CONFIG_DOWN_BUTTON}); // Third icon (Hide) assert.deepEqual( chartInstance.coordinatesToGroupIndexAndHoverType( /* HEADER_LEFT_PADDING + EDIT_BUTTON_SIZE * 2 + GAP_BETWEEN_EDIT_ICONS */ 44, 17), {groupIndex: 0, hoverType: PerfUI.FlameChart.HoverType.TRACK_CONFIG_HIDE_BUTTON}); // This is after the third icon, which is the start of the title label, so should return INSIDE_TRACK_HEADER assert.deepEqual( chartInstance.coordinatesToGroupIndexAndHoverType( /* HEADER_LEFT_PADDING + EDIT_MODE_TOTAL_ICON_WIDTH */ 60, 17), {groupIndex: 0, hoverType: PerfUI.FlameChart.HoverType.INSIDE_TRACK_HEADER}); }); }); }); describe('buildGroupTree', () => { class BuildGroupTreeTestProvider extends FakeFlameChartProvider { override maxStackDepth(): number { return 6; } override timelineData(): PerfUI.FlameChart.FlameChartTimelineData { return PerfUI.FlameChart.FlameChartTimelineData.create({ entryLevels: [], entryStartTimes: [], entryTotalTimes: [], groups: [ { name: 'Test Group 0' as Platform.UIString.LocalizedString, startLevel: 0, style: defaultGroupStyle, }, { name: 'Test Group 1' as Platform.UIString.LocalizedString, startLevel: 1, style: defaultGroupStyle, }, { name: 'Test Group 2' as Platform.UIString.LocalizedString, startLevel: 2, style: {...defaultGroupStyle, collapsible: true, nestingLevel: 1}, }, { name: 'Test Group 3' as Platform.UIString.LocalizedString, startLevel: 3, style: {...defaultGroupStyle, collapsible: true, nestingLevel: 2}, }, { name: 'Test Group 4' as Platform.UIString.LocalizedString, startLevel: 4, style: {...defaultGroupStyle, collapsible: true, nestingLevel: 1}, }, { name: 'Test Group 5' as Platform.UIString.LocalizedString, startLevel: 5, style: {...defaultGroupStyle, collapsible: true, nestingLevel: 0}, }, ], }); } } it('builds the group tree correctly', async () => { const provider = new BuildGroupTreeTestProvider(); const delegate = new MockFlameChartDelegate(); chartInstance = new PerfUI.FlameChart.FlameChart(provider, delegate); const root = chartInstance.buildGroupTree(provider.timelineData().groups); // The built tree should be // Root // / | \ // Group0 Group1 Group5 // / \ // Group2 Group4 // | // Group3 const groupNode5 = { index: 5, nestingLevel: 0, startLevel: 5, // This is the last group, so it will use the end level of the data provider, which is // returned by |dataProvider.maxStackDepth()|, and it is 3. endLevel: 6, children: [], }; const groupNode4 = { index: 4, nestingLevel: 1, startLevel: 4, // The next group is 'Test Group 5', its start level is 5. endLevel: 5, children: [], }; const groupNode3 = { index: 3, nestingLevel: 2, startLevel: 3, // The next group is 'Test Group 4', its start level is 4. endLevel: 4, children: [], }; const groupNode2 = { index: 2, nestingLevel: 1, startLevel: 2, // The next group is 'Test Group 3', its start level is 3. endLevel: 3, children: [groupNode3], }; const groupNode1 = { index: 1, nestingLevel: 0, startLevel: 1, // The next group is 'Test Group 2', its start level is 2. endLevel: 2, children: [groupNode2, groupNode4], }; const groupNode0 = { index: 0, nestingLevel: 0, startLevel: 0, // The next group is 'Test Group 1', its start level is 1. endLevel: 1, children: [], }; const expectedGroupNodeRoot = { index: -1, nestingLevel: -1, startLevel: 0, // The next group is 'Test Group 0', its start level is 0. endLevel: 0, children: [groupNode0, groupNode1, groupNode5], }; assert.deepEqual(root, expectedGroupNodeRoot); }); }); describe('updateGroupTree', () => { class UpdateGroupTreeTestProvider extends FakeFlameChartProvider { override maxStackDepth(): number { return 6; } override timelineData(): PerfUI.FlameChart.FlameChartTimelineData { return PerfUI.FlameChart.FlameChartTimelineData.create({ entryLevels: [], entryStartTimes: [], entryTotalTimes: [], groups: [ { name: 'Test Group 0' as Platform.UIString.LocalizedString, startLevel: 0, style: defaultGroupStyle, }, { name: 'Test Group 1' as Platform.UIString.LocalizedString, startLevel: 1, style: defaultGroupStyle, }, { name: 'Test Group 2' as Platform.UIString.LocalizedString, startLevel: 2, style: {...defaultGroupStyle, collapsible: true, nestingLevel: 1}, }, ], }); } } it('builds the group tree correctly', async () => { const provider = new UpdateGroupTreeTestProvider(); const delegate = new MockFlameChartDelegate(); chartInstance = new PerfUI.FlameChart.FlameChart(provider, delegate); const root = chartInstance.buildGroupTree(provider.timelineData().groups); // The built tree should be // Root // / \ // Group0 Group1 // | // Group2 const groupNode2 = { index: 2, nestingLevel: 1, startLevel: 2, // The next group is 'Test Group 3', its start level is 3. endLevel: 6, children: [], }; const groupNode1 = { index: 1, nestingLevel: 0, startLevel: 1, // The next group is 'Test Group 2', its start level is 2. endLevel: 2, children: [groupNode2], }; const groupNode0 = { index: 0, nestingLevel: 0, startLevel: 0, // The next group is 'Test Group 1', its start level is 1. endLevel: 1, children: [], }; const expectedGroupNodeRoot = { index: -1, nestingLevel: -1, startLevel: 0, // The next group is 'Test Group 0', its start level is 0. endLevel: 0, children: [groupNode0, groupNode1], }; assert.deepEqual(root, expectedGroupNodeRoot); const newGroups = [ { name: 'Test Group 0' as Platform.UIString.LocalizedString, startLevel: 0, style: defaultGroupStyle, }, { name: 'Test Group 1' as Platform.UIString.LocalizedString, startLevel: 2, style: defaultGroupStyle, }, { name: 'Test Group 2' as Platform.UIString.LocalizedString, startLevel: 3, style: {...defaultGroupStyle, collapsible: true, nestingLevel: 1}, }, ]; chartInstance.updateGroupTree(newGroups, root); groupNode0.endLevel = 2; groupNode1.startLevel = 2; groupNode1.endLevel = 3; groupNode2.startLevel = 3; assert.deepEqual(root, expectedGroupNodeRoot); }); }); describe('rendering tracks', () => { it('can render a Node CPU Profile', async function() { // We have to do some work to render this trace, as we take the raw CPU // Profile and wrap it in our code that maps it to a "real" trace. This is what happens for real if a user imports a CPU Profile. const rawCPUProfile = await TraceLoader.rawCPUProfile(this, 'node-fibonacci-website.cpuprofile.gz'); const rawTrace = Trace.Helpers.SamplesIntegrator.SamplesIntegrator.createFakeTraceFromCpuProfile( rawCPUProfile, Trace.Types.Events.ThreadID(1)); const {parsedTrace} = await TraceLoader.executeTraceEngineOnFileContents(rawTrace); await renderFlameChartIntoDOM(this, { dataProvider: 'MAIN', traceFile: parsedTrace, filterTracks(trackName) { return trackName.startsWith('Main'); }, expandTracks() { return true; }, }); await assertScreenshot('timeline/main_thread_node_cpu_profile.png'); }); it('renders the main thread correctly', async function() { await renderFlameChartIntoDOM(this, { dataProvider: 'MAIN', traceFile: 'one-second-interaction.json.gz', filterTracks(trackName) { return trackName.startsWith('Main'); }, expandTracks() { return true; }, }); await assertScreenshot('timeline/render_main_thread.png'); }); it('renders iframe main threads correctly', async function() { await renderFlameChartIntoDOM(this, { dataProvider: 'MAIN', traceFile: 'multiple-navigations-with-iframes.json.gz', filterTracks(trackName) { return trackName.startsWith('Frame'); }, expandTracks() { return true; }, }); await assertScreenshot('timeline/render_iframe_main_thread.png'); })