UNPKG

chrome-devtools-frontend

Version:
1,222 lines (1,128 loc) 74.2 kB
// Copyright 2021 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 * as Common from '../../core/common/common.js'; import * as Platform from '../../core/platform/platform.js'; import * as SDK from '../../core/sdk/sdk.js'; import type * as Protocol from '../../generated/protocol.js'; import * as Bindings from '../../models/bindings/bindings.js'; import * as Trace from '../../models/trace/trace.js'; import * as Workspace from '../../models/workspace/workspace.js'; import { dispatchClickEvent, doubleRaf, raf, renderElementIntoDOM, } from '../../testing/DOMHelpers.js'; import {createTarget, deinitializeGlobalVars, initializeGlobalVars} from '../../testing/EnvironmentHelpers.js'; import { clearMockConnectionResponseHandler, describeWithMockConnection, setMockConnectionResponseHandler, } from '../../testing/MockConnection.js'; import { loadBasicSourceMapExample, setupPageResourceLoaderForSourceMap, } from '../../testing/SourceMapHelpers.js'; import { getBaseTraceParseModelData, getEventOfType, getMainThread, makeCompleteEvent, makeInstantEvent, makeMockRendererHandlerData, makeMockSamplesHandlerData, makeProfileCall, } from '../../testing/TraceHelpers.js'; import {TraceLoader} from '../../testing/TraceLoader.js'; import * as Components from '../../ui/legacy/components/utils/utils.js'; import * as ThemeSupport from '../../ui/legacy/theme_support/theme_support.js'; import * as Timeline from './timeline.js'; import * as Utils from './utils/utils.js'; const {urlString} = Platform.DevToolsPath; describeWithMockConnection('TimelineUIUtils', function() { before(async () => { await initializeGlobalVars(); }); after(async () => await deinitializeGlobalVars()); let target: SDK.Target.Target; // Trace events contain script ids as strings. However, the linkifier // utilities assume it is a number because that's how it's defined at // the protocol level. For practicality, we declare these two // variables so that we can satisfy type checking throughout the tests. const SCRIPT_ID_NUMBER = 1; const SCRIPT_ID_STRING = String(SCRIPT_ID_NUMBER) as Protocol.Runtime.ScriptId; beforeEach(() => { target = createTarget(); const workspace = Workspace.Workspace.WorkspaceImpl.instance(); const targetManager = SDK.TargetManager.TargetManager.instance(); const resourceMapping = new Bindings.ResourceMapping.ResourceMapping(targetManager, workspace); const debuggerWorkspaceBinding = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance({ forceNew: true, resourceMapping, targetManager, }); Bindings.IgnoreListManager.IgnoreListManager.instance({forceNew: true, debuggerWorkspaceBinding}); }); afterEach(() => { clearMockConnectionResponseHandler('DOM.pushNodesByBackendIdsToFrontend'); }); it('creates top frame location text for function calls', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'one-second-interaction.json.gz'); const functionCallEvent = parsedTrace.Renderer.allTraceEntries.find(Trace.Types.Events.isFunctionCall); assert.isOk(functionCallEvent); assert.strictEqual( 'chrome-extension://blijaeebfebmkmekmdnehcmmcjnblkeo/lib/utils.js:11:43', await Timeline.TimelineUIUtils.TimelineUIUtils.buildDetailsTextForTraceEvent(functionCallEvent, parsedTrace)); }); it('creates top frame location text as a fallback', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz'); const timerInstallEvent = parsedTrace.Renderer.allTraceEntries.find(Trace.Types.Events.isTimerInstall); assert.isOk(timerInstallEvent); assert.strictEqual( 'https://web.dev/js/index-7b6f3de4.js:96:533', await Timeline.TimelineUIUtils.TimelineUIUtils.buildDetailsTextForTraceEvent(timerInstallEvent, parsedTrace)); }); describe('script location as an URL', function() { it('makes the script location of a call frame a full URL when the inspected target is not the same the call frame was taken from (e.g. a loaded file)', async function() { // The actual trace doesn't matter here, just need one so we can pass // it into buildDetailsNodeForTraceEvent const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz'); const fakeFunctionCall: Trace.Types.Events.FunctionCall = { name: Trace.Types.Events.Name.FUNCTION_CALL, ph: Trace.Types.Events.Phase.COMPLETE, cat: 'devtools-timeline', dur: Trace.Types.Timing.Micro(100), ts: Trace.Types.Timing.Micro(100), pid: Trace.Types.Events.ProcessID(1), tid: Trace.Types.Events.ThreadID(1), args: { data: { functionName: 'test', url: 'https://google.com/test.js', scriptId: Number(SCRIPT_ID_STRING), lineNumber: 1, columnNumber: 1, }, }, }; target.setInspectedURL(urlString`https://not-google.com`); const node = await Timeline.TimelineUIUtils.TimelineUIUtils.buildDetailsNodeForTraceEvent( fakeFunctionCall, target, new Components.Linkifier.Linkifier(), false, parsedTrace); if (!node) { throw new Error('Node was unexpectedly null'); } // URL path assert.strictEqual(node.textContent, 'test @ /test.js:1:1'); }); it('makes the script location of a call frame a script name when the inspected target is the one the call frame was taken from', async function() { // The actual trace doesn't matter here, just need one so we can pass // it into buildDetailsNodeForTraceEvent const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz'); const fakeFunctionCall: Trace.Types.Events.FunctionCall = { name: Trace.Types.Events.Name.FUNCTION_CALL, ph: Trace.Types.Events.Phase.COMPLETE, cat: 'devtools-timeline', dur: Trace.Types.Timing.Micro(100), ts: Trace.Types.Timing.Micro(100), pid: Trace.Types.Events.ProcessID(1), tid: Trace.Types.Events.ThreadID(1), args: { data: { functionName: 'test', url: 'https://google.com/test.js', scriptId: Number(SCRIPT_ID_STRING), lineNumber: 1, columnNumber: 1, }, }, }; target.setInspectedURL(urlString`https://google.com`); const node = await Timeline.TimelineUIUtils.TimelineUIUtils.buildDetailsNodeForTraceEvent( fakeFunctionCall, target, new Components.Linkifier.Linkifier(), false, parsedTrace); if (!node) { throw new Error('Node was unexpectedly null'); } assert.strictEqual(node.textContent, 'test @ /test.js:1:1'); }); }); describe('mapping to authored script when recording is fresh', function() { beforeEach(async () => { // Register mock script and source map. const sourceMapContent = JSON.stringify({ version: 3, names: ['unminified', 'par1', 'par2', 'console', 'log'], sources: [ '/original-script.ts', ], file: '/test.js', sourcesContent: ['function unminified(par1, par2) {\n console.log(par1, par2);\n}\n'], mappings: 'AAAA,SAASA,EAAWC,EAAMC,GACxBC,QAAQC,IAAIH,EAAMC', }); setupPageResourceLoaderForSourceMap(sourceMapContent); target.setInspectedURL(urlString`https://google.com`); const scriptUrl = urlString`https://google.com/script.js`; const sourceMapUrl = urlString`script.js.map`; const debuggerModel = target.model(SDK.DebuggerModel.DebuggerModel); assert.isNotNull(debuggerModel); if (debuggerModel === null) { return; } const sourceMapManager = debuggerModel.sourceMapManager(); const script = debuggerModel.parsedScriptSource( SCRIPT_ID_STRING, scriptUrl, 0, 0, 0, 0, 0, '', undefined, false, sourceMapUrl, true, false, length, false, null, null, null, null, null, null); await sourceMapManager.sourceMapForClientPromise(script); }); it('maps to the authored script when a call frame is provided', async function() { const linkifier = new Components.Linkifier.Linkifier(); const node = Timeline.TimelineUIUtils.TimelineUIUtils.linkifyLocation({ scriptId: SCRIPT_ID_STRING, url: 'https://google.com/test.js', lineNumber: 0, columnNumber: 0, isFreshRecording: true, target, linkifier, }); if (!node) { throw new Error('Node was unexpectedly null'); } // Wait for the location to be resolved using the registered source map. await Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().pendingLiveLocationChangesPromise(); assert.strictEqual(node.textContent, 'original-script.ts:1:1'); }); it('maps to the authored script when a trace event from the new engine with a stack trace is provided', async function() { const functionCallEvent = makeCompleteEvent('FunctionCall', 10, 100); functionCallEvent.args = ({ data: { stackTrace: [{ functionName: 'test', url: 'https://google.com/test.js', scriptId: SCRIPT_ID_NUMBER, lineNumber: 0, columnNumber: 0, }], }, }); const linkifier = new Components.Linkifier.Linkifier(); const node = Timeline.TimelineUIUtils.TimelineUIUtils.linkifyTopCallFrame(functionCallEvent, target, linkifier, true); if (!node) { throw new Error('Node was unexpectedly null'); } // Wait for the location to be resolved using the registered source map. await Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance() .pendingLiveLocationChangesPromise(); assert.strictEqual(node.textContent, 'original-script.ts:1:1'); }); }); describe('mapping to authored function name when recording is fresh', function() { it('maps to the authored name and script of a profile call', async function() { const {script} = await loadBasicSourceMapExample(target); // Ideally we would get a column number we can use from the source // map however the current status of the source map helpers makes // it difficult to do so. const columnNumber = 51; const profileCall = makeProfileCall('function', 10, 100, Trace.Types.Events.ProcessID(1), Trace.Types.Events.ThreadID(1)); profileCall.callFrame = { columnNumber, functionName: 'minified', lineNumber: 0, scriptId: script.scriptId, url: 'file://gen.js', }; const workersData: Trace.Handlers.ModelHandlers.Workers.WorkersData = { workerSessionIdEvents: [], workerIdByThread: new Map(), workerURLById: new Map(), }; // This only includes data used in the SourceMapsResolver and // TimelineUIUtils const parsedTrace = getBaseTraceParseModelData({ Samples: makeMockSamplesHandlerData([profileCall]), Workers: workersData, Renderer: makeMockRendererHandlerData([profileCall]), }); const resolver = new Timeline.Utils.SourceMapsResolver.SourceMapsResolver(parsedTrace); await resolver.install(); const details = await Timeline.TimelineUIUtils.TimelineUIUtils.buildTraceEventDetails( parsedTrace, profileCall, new Components.Linkifier.Linkifier(), false, null); const stackTraceData = getStackTraceForDetailsElement(details); assert.exists(stackTraceData); assert.isTrue(stackTraceData[0].startsWith('someFunction @')); }); it('maps to the authored name and script of a function call', async function() { const {script} = await loadBasicSourceMapExample(target); const [lineNumber, columnNumber, ts, dur, pid, tid] = [0, 51, 10, 100, Trace.Types.Events.ProcessID(1), Trace.Types.Events.ThreadID(1)]; const profileCall = makeProfileCall('function', ts, dur, pid, tid); profileCall.callFrame = { columnNumber, functionName: 'minified', lineNumber: 0, scriptId: script.scriptId, url: 'file://gen.js', }; const functionCall = makeCompleteEvent(Trace.Types.Events.Name.FUNCTION_CALL, ts, dur, '', pid, tid) as Trace.Types.Events.FunctionCall; functionCall.args = { data: { // line and column number of function calls have an offset // from CPU profile values. columnNumber: columnNumber + 1, lineNumber: lineNumber + 1, functionName: 'minified', scriptId: script.scriptId, url: 'file://gen.js', }, }; const workersData: Trace.Handlers.ModelHandlers.Workers.WorkersData = { workerSessionIdEvents: [], workerIdByThread: new Map(), workerURLById: new Map(), }; // This only includes data used in the SourceMapsResolver and // TimelineUIUtils const parsedTrace = getBaseTraceParseModelData({ Samples: makeMockSamplesHandlerData([profileCall]), Workers: workersData, Renderer: makeMockRendererHandlerData([functionCall]), }); const resolver = new Timeline.Utils.SourceMapsResolver.SourceMapsResolver(parsedTrace); await resolver.install(); const details = await Timeline.TimelineUIUtils.TimelineUIUtils.buildTraceEventDetails( parsedTrace, functionCall, new Components.Linkifier.Linkifier(), false, null, ); const detailsData = getRowDataForDetailsElement(details).find(row => row.title?.startsWith('Function')); assert.exists(detailsData); assert.deepEqual(detailsData, {title: 'Function', value: 'someFunction @ gen.js:1:52'}); }); }); describe('adjusting timestamps for events and navigations', function() { it('adjusts the time for a DCL event after a navigation', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz'); const mainFrameID = parsedTrace.Meta.mainFrameId; const dclEvent = parsedTrace.PageLoadMetrics.allMarkerEvents.find(event => { return Trace.Types.Events.isMarkDOMContent(event) && event.args.data?.frame === mainFrameID; }); if (!dclEvent) { throw new Error('Could not find DCL event'); } const traceMinBound = parsedTrace.Meta.traceBounds.min; // Round the time to 2DP to avoid needlessly long expectation numbers! const unadjustedStartTimeMilliseconds = Trace.Helpers.Timing .microToMilli( Trace.Types.Timing.Micro(dclEvent.ts - traceMinBound), ) .toFixed(2); assert.strictEqual(unadjustedStartTimeMilliseconds, String(190.79)); const adjustedTime = Timeline.TimelineUIUtils.timeStampForEventAdjustedForClosestNavigationIfPossible(dclEvent, parsedTrace); assert.strictEqual(adjustedTime.toFixed(2), String(178.92)); }); it('can adjust the times for events that are not PageLoad markers', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'user-timings.json.gz'); // Use a performance.mark event. Exact event is unimportant except that // it should not be a Page Load event as those are covered by the tests // above. const userMark = parsedTrace.UserTimings.performanceMarks.find(event => event.name === 'mark1'); if (!userMark) { throw new Error('Could not find user mark'); } const adjustedMarkTime = Timeline.TimelineUIUtils.timeStampForEventAdjustedForClosestNavigationIfPossible(userMark, parsedTrace); assert.strictEqual(adjustedMarkTime.toFixed(2), String(79.88)); }); }); function getInnerTextAcrossShadowRoots(root: Node|null): string { // Don't recurse into elements that are not displayed if (!root || (root instanceof HTMLElement && !root.checkVisibility())) { return ''; } if (root.nodeType === Node.TEXT_NODE) { return root.nodeValue || ''; } if (root instanceof HTMLElement && root.shadowRoot) { return getInnerTextAcrossShadowRoots(root.shadowRoot); } return Array.from(root.childNodes).map(getInnerTextAcrossShadowRoots).join(''); } function getRowDataForDetailsElement(details: DocumentFragment) { const container = document.createElement('div'); renderElementIntoDOM(container); container.appendChild(details); return Array.from(container.querySelectorAll<HTMLDivElement>('.timeline-details-view-row')).map(row => { const title = row.querySelector<HTMLDivElement>('.timeline-details-view-row-title')?.innerText; const valueEl = row.querySelector<HTMLDivElement>('.timeline-details-view-row-value') ?? row.querySelector<HTMLElement>('div,span'); let value = valueEl?.innerText || ''; if (!value && valueEl) { // Stack traces and renderEventJson have the contents within a shadowRoot. value = getInnerTextAcrossShadowRoots(valueEl).trim(); } return {title, value}; }); } function getStackTraceForDetailsElement(details: DocumentFragment): string[]|null { const stackTraceContainer = details .querySelector<HTMLDivElement>( '.timeline-details-view-row.timeline-details-stack-values .stack-preview-container') ?.shadowRoot; if (!stackTraceContainer) { return null; } return Array.from(stackTraceContainer.querySelectorAll<HTMLTableRowElement>('tbody tr')).map(row => { const functionName = row.querySelector<HTMLElement>('.function-name')?.innerText; const url = row.querySelector<HTMLElement>('.link')?.innerText; return `${functionName || ''} @ ${url || ''}`; }); } function getPieChartDataForDetailsElement(details: DocumentFragment) { const pieChartComp = details.querySelector<HTMLDivElement>('devtools-perf-piechart'); if (!pieChartComp?.shadowRoot) { return []; } return Array.from(pieChartComp.shadowRoot.querySelectorAll<HTMLElement>('.pie-chart-legend-row')).map(row => { const title = row.querySelector<HTMLDivElement>('.pie-chart-name')?.innerText; const value = row.querySelector<HTMLDivElement>('.pie-chart-size')?.innerText; return {title, value}; }); } describe('colors', function() { before(() => { // Rather than use the real colours here and burden the test with having to // inject loads of CSS, we fake out the colours. this is fine for our tests as // the exact value of the colours is not important; we just make sure that it // parses them out correctly. Each variable is given a different rgb() value to // ensure we know the code is working and using the right one. const styleElement = document.createElement('style'); styleElement.id = 'fake-perf-panel-colors'; styleElement.textContent = ` :root { --app-color-loading: rgb(0 0 0); --app-color-loading-children: rgb(1 1 1); --app-color-scripting: rgb(2 2 2); --app-color-scripting-children: rgb(3 3 3); --app-color-rendering: rgb(4 4 4); --app-color-rendering-children: rgb(5 5 5); --app-color-painting: rgb(6 6 6); --app-color-painting-children: rgb(7 7 7); --app-color-task: rgb(8 8 8); --app-color-task-children: rgb(9 9 9); --app-color-system: rgb(10 10 10); --app-color-system-children: rgb(11 11 11); --app-color-idle: rgb(12 12 12); --app-color-idle-children: rgb(13 13 13); --app-color-async: rgb(14 14 14); --app-color-async-children: rgb(15 15 15); --app-color-other: rgb(16 16 16); } `; document.documentElement.appendChild(styleElement); ThemeSupport.ThemeSupport.clearThemeCache(); }); after(() => { const styleElementToRemove = document.documentElement.querySelector('#fake-perf-panel-colors'); if (styleElementToRemove) { document.documentElement.removeChild(styleElementToRemove); } ThemeSupport.ThemeSupport.clearThemeCache(); }); it('should return the correct rgb value for a corresponding CSS variable', function() { const parsedColor = Utils.EntryStyles.getCategoryStyles().scripting.getComputedColorValue(); assert.strictEqual('rgb(2 2 2)', parsedColor); }); it('should return the color as a CSS variable', function() { const cssVariable = Utils.EntryStyles.getCategoryStyles().scripting.getCSSValue(); assert.strictEqual('var(--app-color-scripting)', cssVariable); }); it('treats the v8.parseOnBackgroundWaiting as scripting even though it would usually be idle', function() { const event = makeCompleteEvent( Trace.Types.Events.Name.STREAMING_COMPILE_SCRIPT_WAITING, 1, 1, 'v8,devtools.timeline,disabled-by-default-v8.compile', ); assert.strictEqual('rgb(2 2 2)', Timeline.TimelineUIUtils.TimelineUIUtils.eventColor(event)); }); it('assigns the correct color to the swatch of an event\'s title', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'lcp-web-font.json.gz'); const events = parsedTrace.Renderer.allTraceEntries; const task = events.find(event => { return event.name.includes('RunTask'); }); if (!task) { throw new Error('Could not find expected event.'); } const details = await Timeline.TimelineUIUtils.TimelineUIUtils.buildTraceEventDetails( parsedTrace, task, new Components.Linkifier.Linkifier(), false, null, ); const titleSwatch: HTMLElement|null = details.querySelector('.timeline-details-chip-title div'); assert.strictEqual(titleSwatch?.style.backgroundColor, 'rgb(10, 10, 10)'); }); }); describe('testContentMatching', () => { it('matches call frame events based on a regular expression and the contents of the event', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'react-hello-world.json.gz'); // Find an event from the trace that represents some work that React did. This // event is not chosen for any particular reason other than it was the example // used in the bug report: crbug.com/1484504 const mainThread = getMainThread(parsedTrace.Renderer); const performConcurrentWorkEvent = mainThread.entries.find(entry => { if (Trace.Types.Events.isProfileCall(entry)) { return entry.callFrame.functionName === 'performConcurrentWorkOnRoot'; } return false; }); if (!performConcurrentWorkEvent) { throw new Error('Could not find expected event'); } assert.isTrue(Timeline.TimelineUIUtils.TimelineUIUtils.testContentMatching( performConcurrentWorkEvent, /perfo/, parsedTrace)); }); }); describe('traceEventDetails', function() { it('shows the interaction ID and INP breakdown metrics for a given interaction', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'one-second-interaction.json.gz'); const interactionEvent = parsedTrace.UserInteractions.interactionEventsWithNoNesting.find(entry => { return entry.dur === 979974 && entry.type === 'click'; }); if (!interactionEvent) { throw new Error('Could not find expected event'); } const details = await Timeline.TimelineUIUtils.TimelineUIUtils.buildTraceEventDetails( parsedTrace, interactionEvent, new Components.Linkifier.Linkifier(), false, null, ); const rowData = getRowDataForDetailsElement(details); assert.deepEqual(rowData, [ { title: 'Warning', value: 'Long interaction is indicating poor page responsiveness.', }, {title: 'Duration', value: '979.97\xA0ms'}, { title: 'ID', value: '4122', }, { title: 'Input delay', value: '1\xA0ms', }, { title: 'Processing duration', value: '977\xA0ms', }, { title: 'Presentation delay', value: '2\xA0ms', }, ]); }); it('renders all event data for a generic trace', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'generic-about-tracing.json.gz'); const event = parsedTrace.Renderer.allTraceEntries.find(entry => { return entry.name === 'ThreadControllerImpl::RunTask'; }); if (!event) { throw new Error('Could not find event.'); } const details = await Timeline.TimelineUIUtils.TimelineUIUtils.buildTraceEventDetails( parsedTrace, event, new Components.Linkifier.Linkifier(), false, null, ); const rowData = getRowDataForDetailsElement(details); assert.deepEqual(rowData, [ {title: 'Duration', value: '0.22\xA0ms (self 0.20\xA0ms)'}, { title: '', // Generic traces get their events rendered as JSON value: '{ "args": {\n "chrome_task_annotator": {\n "delay_policy": "PRECISE",\n "task_delay_us": 7159\n },\n "src_file": "cc/scheduler/scheduler.cc",\n "src_func": "ScheduleBeginImplFrameDeadline"\n },\n "cat": "toplevel",\n "dur": 222,\n "name": "ThreadControllerImpl::RunTask",\n "ph": "X",\n "pid": 1214129,\n "tdur": 163,\n "tid": 7,\n "ts": 1670373249790,\n "tts": 5752392\n}', }, ]); }); it('renders invalidations correctly', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'style-invalidation-change-attribute.json.gz'); TraceLoader.initTraceBoundsManager(parsedTrace); // Set up a fake DOM so that we can request nodes by backend Ids (even // though we return none, we need to mock these calls else the frontend // will not work.) const documentNode = {nodeId: 1 as Protocol.DOM.BackendNodeId}; setMockConnectionResponseHandler('DOM.getDocument', () => ({root: documentNode})); setMockConnectionResponseHandler('DOM.pushNodesByBackendIdsToFrontend', () => { return { nodeIds: [], }; }); const updateLayoutTreeEvent = parsedTrace.Renderer.allTraceEntries.find(event => { return Trace.Types.Events.isUpdateLayoutTree(event) && event.args.beginData?.stackTrace?.[0].functionName === 'testFuncs.changeAttributeAndDisplay'; }); if (!updateLayoutTreeEvent) { throw new Error('Could not find update layout tree event'); } const details = await Timeline.TimelineUIUtils.TimelineUIUtils.buildTraceEventDetails( parsedTrace, updateLayoutTreeEvent, new Components.Linkifier.Linkifier(), false, null, ); const rowData = getRowDataForDetailsElement(details); assert.deepEqual(rowData, [ {title: 'Duration', value: '0.19\xA0ms'}, { title: 'Elements affected', value: '3', }, { title: 'Selector stats', value: 'Select "" to collect detailed CSS selector matching statistics.', }, { // The "Recalculation forced" Stack trace title: undefined, value: 'testFuncs.changeAttributeAndDisplay @ chromedevtools.github.io/performance-stories/style-invalidations/app.js:47:40\n(anonymous) @ chromedevtools.github.io/performance-stories/style-invalidations/app.js:64:36', }, { title: 'Initiated by', value: 'Schedule style recalculation', }, { title: 'Pending for', value: '7.1 ms', }, { title: 'PseudoClass:active', value: 'BUTTON id=\'changeAttributeAndDisplay\'', }, { title: 'Attribute (dir)', value: 'DIV id=\'testElementFour\' at chromedevtools.github.io/performance-stories/style-invalidations/app.js:46', }, { title: 'Attribute (dir)', value: 'DIV id=\'testElementFive\' at chromedevtools.github.io/performance-stories/style-invalidations/app.js:47', }, { title: 'Element has pending invalidation list', value: 'DIV id=\'testElementFour\'', }, { title: 'Element has pending invalidation list', value: 'DIV id=\'testElementFive\'', }, ]); }); it('renders details for performance.mark', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'user-timings-details.json.gz'); const mark = parsedTrace.UserTimings.performanceMarks[0]; if (!mark) { throw new Error('Could not find expected event'); } const details = await Timeline.TimelineUIUtils.TimelineUIUtils.buildTraceEventDetails( parsedTrace, mark, new Components.Linkifier.Linkifier(), false, null, ); const rowData = getRowDataForDetailsElement(details); assert.deepEqual(rowData, [ { title: 'Timestamp', value: '1,058.3\xA0ms', }, {title: 'Details', value: '{ "hello": "world"\n}'}, {title: undefined, value: '(anonymous) @ localhost:8787/perf-details/app.js:1:12'} ]); }); it('renders details for performance.measure', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'user-timings-details.json.gz'); const measure = parsedTrace.UserTimings.performanceMeasures[0]; if (!measure) { throw new Error('Could not find expected event'); } const details = await Timeline.TimelineUIUtils.TimelineUIUtils.buildTraceEventDetails( parsedTrace, measure, new Components.Linkifier.Linkifier(), false, null, ); const rowData = getRowDataForDetailsElement(details); assert.deepEqual(rowData, [ { title: 'Timestamp', value: '1,005.5\xA0ms', }, {title: 'Duration', value: '500.00\xA0ms'}, { title: 'Details', value: '{ "devtools": {\n "metadata": {\n "extensionName": "hello",\n "dataType": "track-entry"\n },\n "color": "error",\n "track": "An extension track"\n }\n}', }, ]); }); it('renders details for a v8.compile ("Compile Script") event', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'user-timings.json.gz'); const compileEvent = parsedTrace.Renderer.allTraceEntries.find(Trace.Types.Events.isV8Compile); if (!compileEvent) { throw new Error('Could not find expected event'); } const details = await Timeline.TimelineUIUtils.TimelineUIUtils.buildTraceEventDetails( parsedTrace, compileEvent, new Components.Linkifier.Linkifier(), false, null, ); const rowData = getRowDataForDetailsElement(details); assert.deepEqual(rowData, [ {title: 'Duration', value: '0.98\xA0ms (self 34\xA0μs)'}, { title: 'Script', // URL path plus line/col number value: '/lib/utils.js:1:1', }, { title: 'Streamed', value: 'false: inline script', }, {title: 'Compilation cache status', value: 'script not eligible'}, ]); }); it('renders the details for an extension entry properly', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'extension-tracks-and-marks.json.gz'); const extensionEntry = parsedTrace.ExtensionTraceData.extensionTrackData[1].entriesByTrack['An Extension Track'][0]; if (!extensionEntry) { throw new Error('Could not find extension entry.'); } const details = await Timeline.TimelineUIUtils.TimelineUIUtils.buildTraceEventDetails( parsedTrace, extensionEntry, new Components.Linkifier.Linkifier(), false, null, ); const rowData = getRowDataForDetailsElement(details).slice(0, 3); assert.deepEqual( rowData, [ {title: 'Duration', value: '1.00\xA0s (self 400.50\xA0ms)'}, { title: 'Description', value: 'This is a child task', }, {title: 'Tip', value: 'Do something about it'}, ], ); }); it('can handle an extension entry having a `null` value', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'extension-tracks-and-marks.json.gz'); const extensionEntry = parsedTrace.ExtensionTraceData.extensionTrackData[1].entriesByTrack['An Extension Track'][0]; if (!extensionEntry) { throw new Error('Could not find extension entry.'); } const mutableEntry: Trace.Types.Extensions.SyntheticExtensionEntry = { ...extensionEntry, args: { ...extensionEntry.args, // Note: we do not support this, but bad values can come in via mistakes in user code. properties: [['key', null]] } }; const details = await Timeline.TimelineUIUtils.TimelineUIUtils.buildTraceEventDetails( parsedTrace, mutableEntry, new Components.Linkifier.Linkifier(), false, null, ); const rowData = getRowDataForDetailsElement(details).slice(0, 3); assert.deepEqual( rowData, [ {title: 'Duration', value: '1.00\xA0s'}, {title: 'key', value: 'null'}, ], ); }); it('renders the details for an extension marker properly', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'extension-tracks-and-marks.json.gz'); const extensionMark = parsedTrace.ExtensionTraceData.extensionMarkers[0]; if (!extensionMark) { throw new Error('Could not find extension mark.'); } const details = await Timeline.TimelineUIUtils.TimelineUIUtils.buildTraceEventDetails( parsedTrace, extensionMark, new Components.Linkifier.Linkifier(), false, null, ); const rowData = getRowDataForDetailsElement(details)[0]; assert.deepEqual( rowData, { title: 'Description', value: 'This marks the start of a task', }, ); }); it('renders the details for a profile call properly', async function() { Common.Linkifier.registerLinkifier({ contextTypes() { return [Timeline.CLSLinkifier.CLSRect]; }, async loadLinkifier() { return Timeline.CLSLinkifier.Linkifier.instance(); }, }); const {parsedTrace} = await TraceLoader.traceEngine(this, 'simple-js-program.json.gz'); const [process] = parsedTrace.Renderer.processes.values(); const [thread] = process.threads.values(); const profileCalls = thread.entries.filter(entry => Trace.Types.Events.isProfileCall(entry)); if (!profileCalls) { throw new Error('Could not find renderer events'); } const details = await Timeline.TimelineUIUtils.TimelineUIUtils.buildTraceEventDetails( parsedTrace, profileCalls[0], new Components.Linkifier.Linkifier(), false, null, ); const stackTraceData = getStackTraceForDetailsElement(details); assert.exists(stackTraceData); assert.strictEqual(stackTraceData[0], '(anonymous) @ www.google.com:21:17'); }); it('renders the stack trace of a ScheduleStyleRecalculation properly', async function() { Common.Linkifier.registerLinkifier({ contextTypes() { return [Timeline.CLSLinkifier.CLSRect]; }, async loadLinkifier() { return Timeline.CLSLinkifier.Linkifier.instance(); }, }); const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz'); TraceLoader.initTraceBoundsManager(parsedTrace); const [process] = parsedTrace.Renderer.processes.values(); const [thread] = process.threads.values(); const scheduleStyleRecalcs = thread.entries.filter(entry => Trace.Types.Events.isScheduleStyleRecalculation(entry)); const details = await Timeline.TimelineUIUtils.TimelineUIUtils.buildTraceEventDetails( parsedTrace, scheduleStyleRecalcs[1], new Components.Linkifier.Linkifier(), false, null, ); const stackTraceData = getStackTraceForDetailsElement(details); assert.deepEqual( stackTraceData, ['(anonymous) @ web.dev/js/app.js?v=1423cda3:1:183'], ); const rowData = getRowDataForDetailsElement(details)[0]; assert.deepEqual( rowData, { title: 'Details', value: 'web.dev/js/app.js?v=1423cda3:1:183', }, ); }); it('renders the stack trace of a RecalculateStyles properly', async function() { Common.Linkifier.registerLinkifier({ contextTypes() { return [Timeline.CLSLinkifier.CLSRect]; }, async loadLinkifier() { return Timeline.CLSLinkifier.Linkifier.instance(); }, }); const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz'); TraceLoader.initTraceBoundsManager(parsedTrace); const [process] = parsedTrace.Renderer.processes.values(); const [thread] = process.threads.values(); const stylesRecalc = thread.entries.filter(entry => entry.name === Trace.Types.Events.Name.UPDATE_LAYOUT_TREE); const details = await Timeline.TimelineUIUtils.TimelineUIUtils.buildTraceEventDetails( parsedTrace, stylesRecalc[3], new Components.Linkifier.Linkifier(), false, null, ); const stackTraceData = getStackTraceForDetailsElement(details); assert.deepEqual( stackTraceData, ['(anonymous) @ web.dev/js/app.js?v=1423cda3:1:183'], ); }); async function basicStackTraceParsedTrace(): Promise<Readonly<Trace.Handlers.Types.EnabledHandlerDataWithMeta<typeof Trace.Handlers.ModelHandlers>>> { const pid = 0; const traceId = 0; const tid = 0; Common.Linkifier.registerLinkifier({ contextTypes() { return [Timeline.CLSLinkifier.CLSRect]; }, async loadLinkifier() { return Timeline.CLSLinkifier.Linkifier.instance(); }, }); // Build the following hierarchy // |-----------------v8.run--------------------| // |--V8.ParseFuntion--||---------f1-------| // |---f2---||---f3---| // |measure| |mark| const evaluateScript = makeCompleteEvent(Trace.Types.Events.Name.EVALUATE_SCRIPT, 0, 500, '', pid, tid); const v8Run = makeCompleteEvent('v8.run', 10, 490, '', pid, tid); const parseFunction = makeCompleteEvent('V8.ParseFunction', 12, 1, '', pid, tid); const function1 = makeProfileCall('function 1', 300, 130, pid, tid); const function2 = makeProfileCall('function 2', 300, 50, pid, tid); const function3 = makeProfileCall('function 3', 351, 20, pid, tid); const measure = makeCompleteEvent(Trace.Types.Events.Name.USER_TIMING, 300, 50, 'blink.user_timing', pid, tid) as unknown as Trace.Types.Events.PerformanceMeasureBegin; const measureTrace = makeCompleteEvent(Trace.Types.Events.Name.USER_TIMING_MEASURE, 300, 50, 'cat', pid, tid) as Trace.Types.Events.UserTimingMeasure; const mark = makeInstantEvent('Mark', 352, 'blink.user_timing', pid, tid); const rendererHandlerData = makeMockRendererHandlerData( [evaluateScript, v8Run, parseFunction, function1, function2, measure, measureTrace, function3, mark]); measureTrace.args.traceId = traceId; measure.args.traceId = traceId; Trace.Handlers.ModelHandlers.UserTimings.handleEvent(measureTrace); await Trace.Handlers.ModelHandlers.UserTimings.finalize(); const timingsData = Trace.Handlers.ModelHandlers.UserTimings.data(); const traceData = getBaseTraceParseModelData({UserTimings: timingsData, Renderer: rendererHandlerData}); TraceLoader.initTraceBoundsManager(traceData); return traceData; } it('renders the stack trace of extension entries properly', async function() { const traceData = await basicStackTraceParsedTrace(); const [function1, function2, function3] = traceData.Renderer.allTraceEntries.filter(Trace.Types.Events.isProfileCall); const mark = traceData.Renderer.allTraceEntries.find(event => event.name === 'Mark'); const measure = traceData.Renderer.allTraceEntries.find(event => event.name === Trace.Types.Events.Name.USER_TIMING) as Trace.Types.Events.UserTimingMeasure; assert.exists(mark); assert.exists(measure); const markerExtensionEntry = { cat: 'devtools.extension', ts: function3.ts, pid: function3.pid, tid: function3.tid, args: {}, rawSourceEvent: mark, } as unknown as Trace.Types.Extensions.SyntheticExtensionEntry; const markerDetails = await Timeline.TimelineUIUtils.TimelineUIUtils.buildTraceEventDetails( traceData, markerExtensionEntry, new Components.Linkifier.Linkifier(), false, null, ); const markerStackTraceData = getStackTraceForDetailsElement(markerDetails); assert.exists(markerStackTraceData); assert.deepEqual( markerStackTraceData, [ `${function3.callFrame.functionName} @ `, `${function1.callFrame.functionName} @ `, ], ); const mockExtensionTrackEntry = { cat: 'devtools.extension', ts: function2.ts, pid: function2.pid, tid: function2.tid, args: {}, rawSourceEvent: { cat: 'blink.user_timing', args: {traceId: measure.args.traceId}, ph: Trace.Types.Events.Phase.ASYNC_NESTABLE_START, }, } as Trace.Types.Extensions.SyntheticExtensionEntry; const trackEntryDetails = await Timeline.TimelineUIUtils.TimelineUIUtils.buildTraceEventDetails( traceData, mockExtensionTrackEntry, new Components.Linkifier.Linkifier(), false, null, ); const trackEntryStackTraceData = getStackTraceForDetailsElement(trackEntryDetails); assert.exists(trackEntryStackTraceData); assert.deepEqual(trackEntryStackTraceData, [ `${function2.callFrame.functionName} @ `, `${function1.callFrame.functionName} @ `, ]); }); it('renders the stack trace of user timings properly', async function() { const traceData = await basicStackTraceParsedTrace(); const [function1, function2, function3] = traceData.Renderer.allTraceEntries.filter(Trace.Types.Events.isProfileCall); const mark = traceData.Renderer.allTraceEntries.find(event => event.name === 'Mark'); const measure = traceData.Renderer.allTraceEntries.find(event => event.name === Trace.Types.Events.Name.USER_TIMING); assert.exists(mark); assert.exists(measure); const markerDetails = await Timeline.TimelineUIUtils.TimelineUIUtils.buildTraceEventDetails( traceData, mark, new Components.Linkifier.Linkifier(), false, null, ); const markerStackTraceData = getStackTraceForDetailsElement(markerDetails); assert.exists(markerStackTraceData); assert.deepEqual( markerStackTraceData, [ `${function3.callFrame.functionName} @ `, `${function1.callFrame.functionName} @ `, ], ); const trackEntryDetails = await Timeline.TimelineUIUtils.TimelineUIUtils.buildTraceEventDetails( traceData, measure, new Components.Linkifier.Linkifier(), false, null, ); const trackEntryStackTraceData = getStackTraceForDetailsElement(trackEntryDetails); assert.exists(trackEntryStackTraceData); assert.deepEqual(trackEntryStackTraceData, [ `${function2.callFrame.functionName} @ `, `${function1.callFrame.functionName} @ `, ]); }); it('renders the warning for a trace event in its details', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'simple-js-program.json.gz'); const events = parsedTrace.Renderer.allTraceEntries; const longTask = events.find(e => (e.dur || 0) > 1_000_000); if (!longTask) { throw new Error('Could not find Long Task event.'); } const details = await Timeline.TimelineUIUtils.TimelineUIUtils.buildTraceEventDetails( parsedTrace, longTask, new Components.Linkifier.Linkifier(), false, null, ); const rowData = getRowDataForDetailsElement(details); assert.deepEqual( rowData, [ { title: 'Warning', value: 'Long task took 1.30\u00A0s.', }, {title: 'Duration', value: '1.30\xA0s (self 47\xA0μs)'}, ], ); }); it('shows information for the WebSocketCreate initiator when viewing a WebSocketSendHandshakeRequest event', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-sockets.json.gz'); TraceLoader.initTraceBoundsManager(parsedTrace); const sendHandshake = parsedTrace.Renderer.allTraceEntries.find(Trace.Types.Events.isWebSocketSendHandshakeRequest); if (!sendHandshake) { throw new Error('Could not find handshake event.'); } const details = await Timeline.TimelineUIUtils.TimelineUIUtils.buildTraceEventDetails( parsedTrace, sendHandshake, new Components.Linkifier.Linkifier(), false, null, ); const rowData = getRowDataForDetailsElement(details); const expectedRowData = [ {title: 'URL', value: 'wss://socketsbay.com/wss/v2/1/demo/'}, // The 'First Invalidated' Stack trace {title: undefined, value: 'connect @ socketsbay.com/test-websockets:314:25'}, {title: 'Initiated by', value: 'Create WebSocket'}, {title: 'Pending for', value: '72.0 ms'}, ]; assert.deepEqual( rowData, expectedRowData, ); }); it('shows information for the events initiated by WebSocketCreate when viewing a WebSocketCreate event', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-sockets.json.gz'); TraceLoader.initTraceBoundsManager(parsedTrace); const sendHandshake = parsedTrace.Renderer.allTraceEntries.find(Trace.Types.Events.isWebSocketCreate); if (!sendHandshake) { throw new Error('Could not find handshake event.'); } const details = await Timeline.TimelineUIUtils.TimelineUIUtils.buildTraceEventDetails( parsedTrace, sendHandshake, new Components.Linkifier.Linkifier(), false, null, ); const rowData = getRowDataForDetailsElement(details); const expectedRowData = [ {title: 'URL', value: 'wss://socketsbay.com/wss/v2/1/demo/'}, // The initiator stack trace { title: undefined, value: 'connect @ socketsbay.com/test-websockets:314:25\n(anonymous) @ socketsbay.com/test-websockets:130:129' }, // The 2 entries under "Initiator for" are displayed as seperate links and in the UI it is obvious they are seperate {title: 'Initiator for', value: 'Send WebSocket handshake Receive WebSocket handshake'}, ]; assert.deepEqual( rowData, expectedRowData, ); }); it('shows the aggregated time information for an event', async function() { const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz'); const event = parsedTrace.Renderer.allTraceEntries.find(e => e.ts === 1020034919877 && e.name === 'RunTask'); if (!event) { throw new Error('Could not find renderer events'); } const details = await Timeline.TimelineUIUtils.TimelineUIUtils.buildTraceEventDetails( parsedTrace, event, new Components.Linkifier.Linkifier(), true, null, ); const pieChartData = getPieChartDataF