UNPKG

chrome-devtools-frontend

Version:
897 lines (782 loc) 43.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 * as Platform from '../../core/platform/platform.js'; import * as Bindings from '../../models/bindings/bindings.js'; import * as Trace from '../../models/trace/trace.js'; import {describeWithEnvironment} from '../../testing/EnvironmentHelpers.js'; import {setupIgnoreListManagerEnvironment} from '../../testing/TraceHelpers.js'; import {TraceLoader} from '../../testing/TraceLoader.js'; import * as PerfUI from '../../ui/legacy/components/perf_ui/perf_ui.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as Timeline from './timeline.js'; import * as Utils from './utils/utils.js'; const {urlString} = Platform.DevToolsPath; class MockViewDelegate implements Timeline.TimelinePanel.TimelineModeViewDelegate { selection: Timeline.TimelineSelection.TimelineSelection|null = null; select(selection: Timeline.TimelineSelection.TimelineSelection|null): void { this.selection = selection; } set3PCheckboxDisabled(_disabled: boolean): void { } selectEntryAtTime(_events: Trace.Types.Events.Event[]|null, _time: number): void { } highlightEvent(_event: Trace.Types.Events.Event|null): void { } element = document.createElement('div'); } describeWithEnvironment('TimelineFlameChartView', function() { beforeEach(() => { setupIgnoreListManagerEnvironment(); }); describe('groupForLevel', () => { const {groupForLevel} = Timeline.TimelineFlameChartView; it('finds the right group for the given level', async () => { const groups: PerfUI.FlameChart.Group[] = [ { name: 'group-1' as Common.UIString.LocalizedString, startLevel: 0, style: {} as PerfUI.FlameChart.GroupStyle, }, { name: 'group-2' as Common.UIString.LocalizedString, startLevel: 10, style: {} as PerfUI.FlameChart.GroupStyle, }, { name: 'group-3' as Common.UIString.LocalizedString, startLevel: 12, style: {} as PerfUI.FlameChart.GroupStyle, }, ]; assert.strictEqual(groupForLevel(groups, 1), groups[0]); assert.strictEqual(groupForLevel(groups, 10), groups[1]); assert.strictEqual(groupForLevel(groups, 11), groups[1]); assert.strictEqual(groupForLevel(groups, 12), groups[2]); assert.strictEqual(groupForLevel(groups, 999), groups[2]); }); }); it('Can search for events by name in the timeline', async function() { const {parsedTrace, metadata} = await TraceLoader.traceEngine(this, 'lcp-images.json.gz'); // The timeline flamechart view will invoke the `select` method // of this delegate every time an event has matched on a search. const mockViewDelegate = new MockViewDelegate(); const flameChartView = new Timeline.TimelineFlameChartView.TimelineFlameChartView(mockViewDelegate); const searchableView = new UI.SearchableView.SearchableView(flameChartView, null); flameChartView.setSearchableView(searchableView); flameChartView.setModel(parsedTrace, metadata); const searchQuery = 'Paint'; const searchConfig = new UI.SearchableView.SearchConfig(/* query */ searchQuery, /* caseSensitive */ false, /* isRegex */ false); flameChartView.performSearch(searchConfig, true); assert.strictEqual(flameChartView.getSearchResults()?.length, 14); assertSelectionName('PrePaint'); flameChartView.jumpToNextSearchResult(); assertSelectionName('Paint'); flameChartView.jumpToNextSearchResult(); assertSelectionName('Paint'); flameChartView.jumpToPreviousSearchResult(); assertSelectionName('Paint'); flameChartView.jumpToPreviousSearchResult(); assertSelectionName('PrePaint'); function assertSelectionName(name: string) { const selection = mockViewDelegate.selection; if (!selection || !Timeline.TimelineSelection.selectionIsEvent(selection)) { throw new Error('Selection is not present or not a Trace Event'); } assert.strictEqual(selection.event.name, name); } }); it('can search across both flame charts for events', async function() { const {parsedTrace, metadata} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz'); // The timeline flamechart view will invoke the `select` method // of this delegate every time an event has matched on a search. const mockViewDelegate = new MockViewDelegate(); const flameChartView = new Timeline.TimelineFlameChartView.TimelineFlameChartView(mockViewDelegate); const searchableView = new UI.SearchableView.SearchableView(flameChartView, null); flameChartView.setSearchableView(searchableView); flameChartView.setModel(parsedTrace, metadata); const searchQuery = 'app.js'; const searchConfig = new UI.SearchableView.SearchConfig(/* query */ searchQuery, /* caseSensitive */ false, /* isRegex */ false); flameChartView.performSearch(searchConfig, true); const results = flameChartView.getSearchResults(); assert.isOk(results); assert.lengthOf(results, 6); // We should have 5 results from the main provider, and 1 from the network assert.lengthOf(results.filter(r => r.provider === 'main'), 5); assert.lengthOf(results.filter(r => r.provider === 'network'), 1); }); // This test is still failing after bumping up the timeout to 20 seconds. So // skip it while we work on a fix for the trace load speed. it.skip('[crbug.com/1492405] Shows the network track correctly', async function() { const {parsedTrace, metadata} = await TraceLoader.traceEngine(this, 'load-simple.json.gz'); // The timeline flamechart view will invoke the `select` method // of this delegate every time an event has matched on a search. const mockViewDelegate = new MockViewDelegate(); const flameChartView = new Timeline.TimelineFlameChartView.TimelineFlameChartView(mockViewDelegate); flameChartView.setModel(parsedTrace, metadata); assert.isTrue(flameChartView.isNetworkTrackShownForTests()); }); it('Does not show the network track when there is no network request', async function() { const {parsedTrace, metadata} = await TraceLoader.traceEngine(this, 'basic.json.gz'); // The timeline flamechart view will invoke the `select` method // of this delegate every time an event has matched on a search. const mockViewDelegate = new MockViewDelegate(); const flameChartView = new Timeline.TimelineFlameChartView.TimelineFlameChartView(mockViewDelegate); flameChartView.setModel(parsedTrace, metadata); assert.isFalse(flameChartView.isNetworkTrackShownForTests()); }); it('Adds Hidden Descendants Arrow as a decoration when a Context Menu action is applied on a node', async function() { const {parsedTrace, metadata} = await TraceLoader.traceEngine(this, 'load-simple.json.gz'); const mockViewDelegate = new MockViewDelegate(); const flameChartView = new Timeline.TimelineFlameChartView.TimelineFlameChartView(mockViewDelegate); flameChartView.setModel(parsedTrace, metadata); // Find the main track to later collapse entries of const mainTrack = flameChartView.getMainFlameChart().timelineData()?.groups.find(group => { return group.name === 'Main — http://localhost:8080/'; }); if (!mainTrack) { throw new Error('Could not find main track'); } // Find the first node that has children to collapse and is visible in the timeline const nodeOfGroup = flameChartView.getMainDataProvider().groupTreeEvents(mainTrack); const firstNodeWithChildren = nodeOfGroup?.find(node => { const childrenAmount = parsedTrace.Renderer.entryToNode.get(node as Trace.Types.Events.Event)?.children.length; if (!childrenAmount) { return false; } return childrenAmount > 0 && node.cat === 'devtools.timeline'; }); const node = parsedTrace.Renderer.entryToNode.get(firstNodeWithChildren as Trace.Types.Events.Event); if (!node) { throw new Error('Could not find a visible node with children'); } // Apply COLLAPSE_FUNCTION action to the node. This action will hide all the children of the passed node and add HIDDEN_DESCENDANTS_ARROW decoration to it. flameChartView.getMainFlameChart().modifyTree(PerfUI.FlameChart.FilterAction.COLLAPSE_FUNCTION, node?.id); const decorationsForEntry = flameChartView.getMainFlameChart().timelineData()?.entryDecorations[node?.id]; assert.deepEqual(decorationsForEntry, [ { type: PerfUI.FlameChart.FlameChartDecorationType.HIDDEN_DESCENDANTS_ARROW, }, ]); }); it('Adds Hidden Descendants Arrow as a decoration when a Context Menu action is applied on a selected node with a key shortcut event', async function() { const {parsedTrace, metadata} = await TraceLoader.traceEngine(this, 'load-simple.json.gz'); const mockViewDelegate = new MockViewDelegate(); const flameChartView = new Timeline.TimelineFlameChartView.TimelineFlameChartView(mockViewDelegate); flameChartView.setModel(parsedTrace, metadata); // Find the main track to later collapse entries of const mainTrack = flameChartView.getMainFlameChart().timelineData()?.groups.find(group => { return group.name === 'Main — http://localhost:8080/'; }); if (!mainTrack) { throw new Error('Could not find main track'); } // Find the first node that has children to collapse and is visible in the timeline const groupTreeEvents = flameChartView.getMainDataProvider().groupTreeEvents(mainTrack); const firstEventWithChildren = groupTreeEvents?.find(node => { const childrenAmount = parsedTrace.Renderer.entryToNode.get(node as Trace.Types.Events.Event)?.children.length; if (!childrenAmount) { return false; } return childrenAmount > 0 && node.cat === 'devtools.timeline'; }); assert.exists(firstEventWithChildren); const nodeId = flameChartView.getMainDataProvider().indexForEvent(firstEventWithChildren); assert.exists(nodeId); flameChartView.getMainFlameChart().setSelectedEntry(nodeId); // Dispatch a shortcut keydown event that applies 'Hide Children' Context menu action const event = new KeyboardEvent('keydown', {code: 'KeyC'}); flameChartView.getMainFlameChart().getCanvas().dispatchEvent(event); const decorationsForEntry = flameChartView.getMainFlameChart().timelineData()?.entryDecorations[nodeId]; assert.deepEqual(decorationsForEntry, [ { type: PerfUI.FlameChart.FlameChartDecorationType.HIDDEN_DESCENDANTS_ARROW, }, ]); }); it('Removes Hidden Descendants Arrow as a decoration when Reset Children action is applied on a node', async function() { const {parsedTrace, metadata} = await TraceLoader.traceEngine(this, 'load-simple.json.gz'); const mockViewDelegate = new MockViewDelegate(); const flameChartView = new Timeline.TimelineFlameChartView.TimelineFlameChartView(mockViewDelegate); flameChartView.setModel(parsedTrace, metadata); Timeline.ModificationsManager.ModificationsManager.activeManager(); // Find the main track to later collapse entries of let mainTrack = flameChartView.getMainFlameChart().timelineData()?.groups.find(group => { return group.name === 'Main — http://localhost:8080/'; }); if (!mainTrack) { throw new Error('Could not find main track'); } // Find the first node that has children to collapse and is visible in the timeline const nodeOfGroup = flameChartView.getMainDataProvider().groupTreeEvents(mainTrack); const firstNodeWithChildren = nodeOfGroup?.find(node => { const childrenAmount = parsedTrace.Renderer.entryToNode.get(node as Trace.Types.Events.Event)?.children.length; if (!childrenAmount) { return false; } return childrenAmount > 0 && node.cat === 'devtools.timeline'; }); const node = parsedTrace.Renderer.entryToNode.get(firstNodeWithChildren as Trace.Types.Events.Event); if (!node) { throw new Error('Could not find a visible node with children'); } // Apply COLLAPSE_FUNCTION Context Menu action to the node. // This action will hide all the children of the passed node and add HIDDEN_DESCENDANTS_ARROW decoration to it. flameChartView.getMainFlameChart().modifyTree(PerfUI.FlameChart.FilterAction.COLLAPSE_FUNCTION, node?.id); let decorationsForEntry = flameChartView.getMainFlameChart().timelineData()?.entryDecorations[node?.id]; assert.deepEqual(decorationsForEntry, [ { type: PerfUI.FlameChart.FlameChartDecorationType.HIDDEN_DESCENDANTS_ARROW, }, ]); mainTrack = flameChartView.getMainFlameChart().timelineData()?.groups.find(group => { return group.name === 'Main — http://localhost:8080/'; }); if (!mainTrack) { throw new Error('Could not find main track'); } // Apply a RESET_CHILDREN action that will reveal all of the hidden children of the passed node and remove HIDDEN_DESCENDANTS_ARROW decoration from it. flameChartView.getMainFlameChart().modifyTree(PerfUI.FlameChart.FilterAction.RESET_CHILDREN, node?.id); // No decorations should exist on the node decorationsForEntry = flameChartView.getMainFlameChart().timelineData()?.entryDecorations[node?.id]; assert.isUndefined(decorationsForEntry); }); it('renders metrics as marker overlays w/ tooltips', async function() { const {parsedTrace, metadata, insights} = await TraceLoader.traceEngine(this, 'crux.json.gz'); const mockViewDelegate = new MockViewDelegate(); const flameChartView = new Timeline.TimelineFlameChartView.TimelineFlameChartView(mockViewDelegate); flameChartView.setInsights(insights, new Map()); flameChartView.setModel(parsedTrace, metadata); const tooltips = [...flameChartView.element.querySelectorAll('.overlay-type-TIMINGS_MARKER .marker-title')].map(el => { el?.dispatchEvent(new MouseEvent('mousemove', { clientX: 0, clientY: 0, })); return flameChartView.element.querySelector('.timeline-entry-tooltip-element') ?.textContent?.replaceAll('\xa0', ' '); }); assert.deepEqual(tooltips, [ '0 μsNav', '43.98 msL', '75.90 msFCP - Local939.00 msFCP - Field (URL)', '75.90 msLCP - Local936.00 msLCP - Field (URL)', '43.75 msDCL', ]); }); describe('Context Menu', function() { let flameChartView: Timeline.TimelineFlameChartView.TimelineFlameChartView; let parsedTrace: Trace.Handlers.Types.ParsedTrace; let metadata: Trace.Types.File.MetaData|null; this.beforeEach(async () => { ({parsedTrace, metadata} = await TraceLoader.traceEngine(this, 'recursive-blocking-js.json.gz')); const mockViewDelegate = new MockViewDelegate(); flameChartView = new Timeline.TimelineFlameChartView.TimelineFlameChartView(mockViewDelegate); flameChartView.setModel(parsedTrace, metadata); Timeline.ModificationsManager.ModificationsManager.activeManager(); }); it('Does not create customized Context Menu for network track', async function() { // The mouse event passed to the Context Menu is used to indicate where the menu should appear. Since we don't // need it to actually appear for this test, pass an event with coordinates that is not in the track header. flameChartView.getNetworkFlameChart().onContextMenu(new MouseEvent('contextmenu', {clientX: 100, clientY: 100})); assert.isUndefined(flameChartView.getNetworkFlameChart().getContextMenu()); }); it('Does not create Context Menu for Network track header', async function() { // So for the first track header, its x will start from beginning. // And its y will start after the ruler (ruler's height is 17). flameChartView.getNetworkFlameChart().onContextMenu(new MouseEvent('contextmenu', {clientX: 0, clientY: 17})); assert.isUndefined(flameChartView.getNetworkFlameChart().getContextMenu()); }); it('Create correct Context Menu for track headers in main flame chart', async function() { // So for the first track header, its x will start from beginning. // And its y will start after the ruler (ruler's height is 17). flameChartView.getMainFlameChart().onContextMenu(new MouseEvent('contextmenu', {clientX: 0, clientY: 17})); assert.strictEqual(flameChartView.getMainFlameChart().getContextMenu()?.defaultSection().items.length, 1); assert.strictEqual( flameChartView.getMainFlameChart().getContextMenu()?.defaultSection().items.at(0)?.buildDescriptor().label, 'Configure tracks'); }); describe('Context Menu Actions For Thread tracks', function() { this.beforeEach(async () => { // Find the Main track to later collapse entries of const mainTrack = flameChartView.getMainFlameChart().timelineData()?.groups.find(group => { return group.name === 'Main — http://127.0.0.1:8080/'; }); if (!mainTrack) { throw new Error('Could not find main track'); } }); function getMainThread(data: Trace.Handlers.ModelHandlers.Renderer.RendererHandlerData): Trace.Handlers.ModelHandlers.Renderer.RendererThread { let mainThread: Trace.Handlers.ModelHandlers.Renderer.RendererThread|null = null; for (const [, process] of data.processes) { for (const [, thread] of process.threads) { if (thread.name === 'CrRendererMain') { mainThread = thread; break; } } } if (!mainThread) { throw new Error('Could not find main thread.'); } return mainThread; } function findFirstEntry( allEntries: readonly Trace.Types.Events.Event[], predicate: (entry: Trace.Types.Events.Event) => boolean): Trace.Types.Events.Event { const entry = allEntries.find(entry => predicate(entry)); if (!entry) { throw new Error('Could not find expected entry.'); } return entry; } function generateContextMenuForNodeId(nodeId: number): void { // Highlight the node to make the Context Menu dispatch on this node flameChartView.getMainFlameChart().highlightEntry(nodeId); const eventCoordinates = flameChartView.getMainFlameChart().entryIndexToCoordinates(nodeId); if (!eventCoordinates) { throw new Error('Coordinates were not found'); } // The mouse event passed to the Context Menu is used to indicate where the menu should appear. So just simply // use the pixels of top left corner of the event. flameChartView.getMainFlameChart().onContextMenu( new MouseEvent('contextmenu', {clientX: eventCoordinates.x, clientY: eventCoordinates.y})); } function generateContextMenuForNode(node: Trace.Types.Events.Event): void { const nodeId = flameChartView.getMainDataProvider().indexForEvent(node); assert.isNotNull(nodeId); generateContextMenuForNodeId(nodeId); } it('When an entry has no children, correctly make only Hide Entry enabled in the Context Menu action', async function() { /** Part of this stack looks roughly like so (with some events omitted): * =============== foo =============== * =============== foo =============== * =============== foo =============== * =============== foo =============== * =============== foo =============== * =============== foo =============== * =============== foo =============== * == now == == updateCounters == <-- ID=245 * * In this test we want to test that the Context Menu option available * for an entry with no children and a parent is to hide given entry only. * Since there are no children to hide, we don't want to show 'hide children' option. * * To achieve that, we will dispatch the context menu on the 'updateCounters' function that does not have * children. * The ID of 'updateCounters' is 245. **/ const nodeIdWithNoChildren = 245; generateContextMenuForNodeId(nodeIdWithNoChildren); assert.strictEqual(flameChartView.getMainFlameChart().getContextMenu()?.defaultSection().items.length, 5); // Hide function enabled assert.isTrue(flameChartView.getMainFlameChart() .getContextMenu() ?.defaultSection() .items.at(0) ?.buildDescriptor() .enabled); // Rest of the actions disabled assert.isFalse(flameChartView.getMainFlameChart() .getContextMenu() ?.defaultSection() .items.at(1) ?.buildDescriptor() .enabled); assert.isFalse(flameChartView.getMainFlameChart() .getContextMenu() ?.defaultSection() .items.at(2) ?.buildDescriptor() .enabled); assert.isFalse(flameChartView.getMainFlameChart() .getContextMenu() ?.defaultSection() .items.at(3) ?.buildDescriptor() .enabled); assert.isFalse(flameChartView.getMainFlameChart() .getContextMenu() ?.defaultSection() .items.at(4) ?.buildDescriptor() .enabled); }); it('When an entry has children, correctly make only Hide Entry and Hide Children enabled in the Context Menu action', async function() { /** Part of this stack looks roughly like so (with some events omitted): * =============== foo =============== * =============== foo =============== * =============== foo =============== * =============== foo =============== * =============== foo =============== * =============== foo =============== * =============== foo =============== * ===== wait ===== ===== wait ===== * = now = = now = = now = = now = * * In this test we want to test that the Context Menu option available * for an entry with children and a parent is to hide given entry, and hide children only. * Since there are no repeating children to hide, we don't want to show 'hide repeating children' option. * * To achieve that, we will dispatch the context menu on the 'wait' function that has only non-repeating * children. **/ const mainThread = getMainThread(parsedTrace.Renderer); const entry = findFirstEntry(mainThread.entries, entry => { return Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === 'wait'; }); const nodeIdWithNoChildren = flameChartView.getMainDataProvider().indexForEvent(entry); assert.exists(nodeIdWithNoChildren); generateContextMenuForNodeId(nodeIdWithNoChildren); // This entry has URL, so there are 5 always-shown actions, and one to add script to ignore list. assert.strictEqual(flameChartView.getMainFlameChart().getContextMenu()?.defaultSection().items.length, 6); // Hide function enabled assert.isTrue(flameChartView.getMainFlameChart() .getContextMenu() ?.defaultSection() .items.at(0) ?.buildDescriptor() .enabled); // Hide children enabled assert.isTrue(flameChartView.getMainFlameChart() .getContextMenu() ?.defaultSection() .items.at(1) ?.buildDescriptor() .enabled); // Rest of the actions disabled assert.isFalse(flameChartView.getMainFlameChart() .getContextMenu() ?.defaultSection() .items.at(2) ?.buildDescriptor() .enabled); assert.isFalse(flameChartView.getMainFlameChart() .getContextMenu() ?.defaultSection() .items.at(3) ?.buildDescriptor() .enabled); assert.isFalse(flameChartView.getMainFlameChart() .getContextMenu() ?.defaultSection() .items.at(4) ?.buildDescriptor() .enabled); }); it('When an entry has repeating children, correctly make only Hide Entry, Hide Children and Hide repeating children enabled in the Context Menu action', async function() { /** Part of this stack looks roughly like so (with some events omitted): * =============== foo =============== * =============== foo =============== * =============== foo =============== * =============== foo =============== * =============== foo =============== * =============== foo =============== * =============== foo =============== * ===== wait ===== ===== wait ===== * = now = = now = = now = = now = * * In this test we want to test that the Context Menu option available * for an entry with children repeating children and a parent is to hide given entry, hide children and hide * repeating children. * * To achieve that, we will dispatch the context menu on the 'foo' function that has child 'foo' calls. **/ const mainThread = getMainThread(parsedTrace.Renderer); const entry = findFirstEntry(mainThread.entries, entry => { return Trace.Types.Events.isProfileCall(entry) && entry.callFrame.functionName === 'foo'; }); const nodeId = flameChartView.getMainDataProvider().indexForEvent(entry); assert.exists(nodeId); generateContextMenuForNodeId(nodeId); // This entry has URL, so there are 5 always-shown actions, and one to add script to ignore list. assert.strictEqual(flameChartView.getMainFlameChart().getContextMenu()?.defaultSection().items.length, 6); // Hide function enabled assert.isTrue(flameChartView.getMainFlameChart() .getContextMenu() ?.defaultSection() .items.at(0) ?.buildDescriptor() .enabled); // Hide children enabled assert.isTrue(flameChartView.getMainFlameChart() .getContextMenu() ?.defaultSection() .items.at(1) ?.buildDescriptor() .enabled); // Hide repeating children enabled assert.isTrue(flameChartView.getMainFlameChart() .getContextMenu() ?.defaultSection() .items.at(2) ?.buildDescriptor() .enabled); // Rest of the actions disabled assert.isFalse(flameChartView.getMainFlameChart() .getContextMenu() ?.defaultSection() .items.at(3) ?.buildDescriptor() .enabled); assert.isFalse(flameChartView.getMainFlameChart() .getContextMenu() ?.defaultSection() .items.at(4) ?.buildDescriptor() .enabled); }); it('When an entry has no parent and has children, correctly make only Hide Children enabled in the Context Menu action', async function() { /** Part of this stack looks roughly like so (with some events omitted): * =============== Task ============== * =============== foo =============== * =============== foo =============== * =============== foo =============== * =============== foo =============== * =============== foo =============== * =============== foo =============== * ===== wait ===== ===== wait ===== * = now = = now = = now = = now = * * In this test we want to test that the Context Menu option available for an entry with no parent is only to * hide children. * If an entry has no parent, we don't want to show an option to hide the entry since when an entry is hidden, * it is indicated by adding a decoration to the parent and if there is no parent, there is no way to show it * is hidden. * * To achieve that, we will dispatch the context menu on the 'Task' function that is on the top of the stack * and has no parent. **/ const mainThread = getMainThread(parsedTrace.Renderer); const entry = findFirstEntry(mainThread.entries, entry => { const childrenAmount = parsedTrace.Renderer.entryToNode.get(entry as Trace.Types.Events.Event)?.children.length; if (!childrenAmount) { return false; } return Trace.Types.Events.isRunTask(entry); }); const nodeId = flameChartView.getMainDataProvider().indexForEvent(entry); assert.exists(nodeId); generateContextMenuForNodeId(nodeId); // Hide function disabled assert.isFalse(flameChartView.getMainFlameChart() .getContextMenu() ?.defaultSection() .items.at(0) ?.buildDescriptor() .enabled); // Hide children enabled assert.isTrue(flameChartView.getMainFlameChart() .getContextMenu() ?.defaultSection() .items.at(1) ?.buildDescriptor() .enabled); // Rest of the actions disabled assert.isFalse(flameChartView.getMainFlameChart() .getContextMenu() ?.defaultSection() .items.at(2) ?.buildDescriptor() .enabled); assert.isFalse(flameChartView.getMainFlameChart() .getContextMenu() ?.defaultSection() .items.at(3) ?.buildDescriptor() .enabled); assert.isFalse(flameChartView.getMainFlameChart() .getContextMenu() ?.defaultSection() .items.at(4) ?.buildDescriptor() .enabled); }); it('Reset Trace Context Menu action is disabled before some action has been applied', async function() { /** Part of this stack looks roughly like so (with some events omitted): * =============== Task ============== * =============== foo =============== * =============== foo =============== * =============== foo =============== * =============== foo =============== * =============== foo =============== * =============== foo =============== * ===== wait ===== ===== wait ===== * = now = = now = = now = = now = * * In this test we want to test that the Reset Trace Context Menu option is disabled by default and enabled after * some action has been applied. * * To achieve that, we will first check if Reset Trace is disabled and then dispatch a Context Menu action on * "Task" entry and then check if Reset Trace is enabled. **/ const mainThread = getMainThread(parsedTrace.Renderer); const entry = findFirstEntry(mainThread.entries, entry => Trace.Types.Events.isRunTask(entry)); const nodeId = flameChartView.getMainDataProvider().indexForEvent(entry); assert.exists(nodeId); generateContextMenuForNodeId(nodeId); assert.strictEqual(flameChartView.getMainFlameChart().getContextMenu()?.defaultSection().items.length, 5); assert.strictEqual( flameChartView.getMainFlameChart().getContextMenu()?.defaultSection().items.at(4)?.buildDescriptor().label, 'Reset trace'); // Check that Reset Trace is disabled assert.isFalse(flameChartView.getMainFlameChart() .getContextMenu() ?.defaultSection() .items.at(4) ?.buildDescriptor() .enabled); flameChartView.getMainFlameChart().modifyTree(PerfUI.FlameChart.FilterAction.MERGE_FUNCTION, nodeId); generateContextMenuForNodeId(nodeId); // Check that Reset Trace is enabled assert.isTrue(flameChartView.getMainFlameChart() .getContextMenu() ?.defaultSection() .items.at(4) ?.buildDescriptor() .enabled); }); it('When an entry has URL and is not ignored, correctly show the Add script to ignore list in the Context Menu action', async function() { const mainThread = getMainThread(parsedTrace.Renderer); const entryWithUrl = findFirstEntry(mainThread.entries, entry => { // Let's find the first entry with URL. return Trace.Types.Events.isProfileCall(entry) && Boolean(entry.callFrame.url); }); generateContextMenuForNode(entryWithUrl); assert.strictEqual(flameChartView.getMainFlameChart().getContextMenu()?.defaultSection().items.length, 6); assert.strictEqual( flameChartView.getMainFlameChart() .getContextMenu() ?.defaultSection() .items.at(5) ?.buildDescriptor() .label, 'Add script to ignore list'); }); it('When an entry has URL and is ignored, correctly show the Remove script from ignore list in the Context Menu action', async function() { const mainThread = getMainThread(parsedTrace.Renderer); const entryWithIgnoredUrl = findFirstEntry(mainThread.entries, entry => { // Let's find the first entry with URL. return Trace.Types.Events.isProfileCall(entry) && Boolean(entry.callFrame.url); }); Bindings.IgnoreListManager.IgnoreListManager.instance().ignoreListURL( urlString`${(entryWithIgnoredUrl as Trace.Types.Events.SyntheticProfileCall).callFrame.url}`); generateContextMenuForNode(entryWithIgnoredUrl); assert.strictEqual(flameChartView.getMainFlameChart().getContextMenu()?.defaultSection().items.length, 6); assert.strictEqual( flameChartView.getMainFlameChart() .getContextMenu() ?.defaultSection() .items.at(5) ?.buildDescriptor() .label, 'Remove script from ignore list'); }); }); }); describe('updating the active AI call tree', () => { it('updates the UI Context with the active AI Call tree for the selected event', async function() { const {parsedTrace, metadata} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz'); const mockViewDelegate = new MockViewDelegate(); const flameChartView = new Timeline.TimelineFlameChartView.TimelineFlameChartView(mockViewDelegate); flameChartView.setModel(parsedTrace, metadata); // Find some task in the main thread that we can build an AI Call Tree from const task = parsedTrace.Renderer.allTraceEntries.find(event => { return Trace.Types.Events.isRunTask(event) && event.dur > 5_000 && Utils.AICallTree.AICallTree.from(event, parsedTrace) !== null; }); assert.isOk(task); UI.Context.Context.instance().setFlavor(Utils.AICallTree.AICallTree, null); const selection = Timeline.TimelineSelection.selectionFromEvent(task); flameChartView.setSelectionAndReveal(selection); const flavor = UI.Context.Context.instance().flavor(Utils.AICallTree.AICallTree); assert.instanceOf(flavor, Utils.AICallTree.AICallTree); }); }); describe('Link between entries annotation in progress', function() { let flameChartView: Timeline.TimelineFlameChartView.TimelineFlameChartView; let parsedTrace: Trace.Handlers.Types.ParsedTrace; let metadata: Trace.Types.File.MetaData|null; this.beforeEach(async () => { ({parsedTrace, metadata} = await TraceLoader.traceEngine(this, 'recursive-blocking-js.json.gz')); const mockViewDelegate = new MockViewDelegate(); flameChartView = new Timeline.TimelineFlameChartView.TimelineFlameChartView(mockViewDelegate); flameChartView.setModel(parsedTrace, metadata); Timeline.ModificationsManager.ModificationsManager.activeManager(); }); it('Creates a `link between entries Annotation in progress` tracking object', async function() { // Make sure the link annotation in the progress of creation does not exist assert.isNull(flameChartView.getLinkSelectionAnnotation()); // Start creating a link between entries from an entry with ID 204 flameChartView.onEntriesLinkAnnotationCreate(flameChartView.getMainDataProvider(), 204); // Make sure the link is started and only has 'from' entry set assert.isNotNull(flameChartView.getLinkSelectionAnnotation()); assert.isNotNull(flameChartView.getLinkSelectionAnnotation()?.entryFrom); assert.isUndefined(flameChartView.getLinkSelectionAnnotation()?.entryTo); // Make sure the annotation exists in the ModificationsManager const annotations = Timeline.ModificationsManager.ModificationsManager.activeManager()?.getAnnotations(); assert.exists(annotations); assert.strictEqual(annotations?.length, 1); assert.strictEqual(annotations[0].type, 'ENTRIES_LINK'); }); it('Sets the link between entries annotation in progress to null when the second entry is selected', async function() { // Make sure the link annotation in the progress of creation does not exist assert.isNull(flameChartView.getLinkSelectionAnnotation()); // Start creating a link between entries from an entry with ID 204 flameChartView.onEntriesLinkAnnotationCreate(flameChartView.getMainDataProvider(), 204); const entryFrom = flameChartView.getMainDataProvider().eventByIndex(204); // Hover on another entry to complete the link flameChartView.updateLinkSelectionAnnotationWithToEntry(flameChartView.getMainDataProvider(), 245); const entryTo = flameChartView.getMainDataProvider().eventByIndex(245); // Make sure the entry 'to' is set assert.exists(flameChartView.getLinkSelectionAnnotation()?.entryTo); // Select the other entry to complete the link and set the one in progress to null flameChartView.handleToEntryOfLinkBetweenEntriesSelection(245); // Make sure the link annotation in progress is set to null assert.isNull(flameChartView.getLinkSelectionAnnotation()); // Make sure the annotation exists in the ModificationsManager const annotations = Timeline.ModificationsManager.ModificationsManager.activeManager()?.getAnnotations(); assert.exists(annotations); assert.strictEqual(annotations?.length, 1); assert.strictEqual(annotations[0].type, 'ENTRIES_LINK'); const entriesLink = annotations[0] as Trace.Types.File.EntriesLinkAnnotation; assert.strictEqual(entriesLink.entryFrom, entryFrom); assert.strictEqual(entriesLink.entryTo, entryTo); }); it('Reverses entries in the link if `to` entry timestamp is earlier than `from` entry timestamo', async function() { // Make sure the link annotation in the progress of creation does not exist assert.isNull(flameChartView.getLinkSelectionAnnotation()); // Start creating a link between entries from an entry with ID 245 flameChartView.onEntriesLinkAnnotationCreate(flameChartView.getMainDataProvider(), 245); const entryFrom = flameChartView.getMainDataProvider().eventByIndex(245); // Hover on another entry that starts before the entry that the link is being created from flameChartView.updateLinkSelectionAnnotationWithToEntry(flameChartView.getMainDataProvider(), 204); const entryTo = flameChartView.getMainDataProvider().eventByIndex(204); // Select the other entry to complete the link and set the one in progress to null flameChartView.handleToEntryOfLinkBetweenEntriesSelection(204); // Make sure the annotation exists in the ModificationsManager const annotations = Timeline.ModificationsManager.ModificationsManager.activeManager()?.getAnnotations(); assert.exists(annotations); assert.strictEqual(annotations?.length, 1); assert.strictEqual(annotations[0].type, 'ENTRIES_LINK'); const entriesLink = annotations[0] as Trace.Types.File.EntriesLinkAnnotation; // Make 'entryFrom' has an earlier timestamp and the entries `to` and `from` got switched up assert.strictEqual(entriesLink.entryFrom, entryTo); assert.strictEqual(entriesLink.entryTo, entryFrom); }); }); });