UNPKG

chrome-devtools-frontend

Version:
639 lines (575 loc) • 24.6 kB
// Copyright 2022 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 {defaultTraceEvent} from '../../../testing/TraceHelpers.js'; import {TraceLoader} from '../../../testing/TraceLoader.js'; import * as Trace from '../trace.js'; describe('MetaHandler', function() { let baseEvents: Trace.Types.Events.Event[]; beforeEach(async function() { let defaultTraceEvents: readonly Trace.Types.Events.Event[]; try { defaultTraceEvents = await TraceLoader.rawEvents(this, 'basic.json.gz'); } catch (error) { assert.fail(error); return; } baseEvents = [ ...defaultTraceEvents, { ...defaultTraceEvent, args: { data: { isLoadingMainFrame: true, isOutermostMainFrame: true, documentLoaderURL: 'test1', navigationId: 'navigation-1', }, frame: '3E1717BE677B75D0536E292E00D6A34A', }, pid: Trace.Types.Events.ProcessID(23456), tid: Trace.Types.Events.ThreadID(775), ts: Trace.Types.Timing.Micro(100), name: 'navigationStart', } as Trace.Types.Events.NavigationStart, { ...defaultTraceEvent, // Should be ignored based on empty documentLoaderURL args: { data: { isLoadingMainFrame: true, isOutermostMainFrame: true, documentLoaderURL: '', navigationId: 'navigation-2', }, }, pid: Trace.Types.Events.ProcessID(23456), tid: Trace.Types.Events.ThreadID(775), ts: Trace.Types.Timing.Micro(800), name: 'navigationStart', // Casting as NavStart, but it is ignored due to the empty documentLoaderURL } as Trace.Types.Events.NavigationStart, { ...defaultTraceEvent, args: { data: { isLoadingMainFrame: true, isOutermostMainFrame: true, documentLoaderURL: 'test3', navigationId: 'navigation-3', }, }, pid: Trace.Types.Events.ProcessID(23456), tid: Trace.Types.Events.ThreadID(775), ts: Trace.Types.Timing.Micro(1000), name: 'navigationStart', } as Trace.Types.Events.NavigationStart, ]; Trace.Handlers.ModelHandlers.Meta.reset(); }); describe('browser process ID', function() { it('obtains the PID if present', async () => { for (const event of baseEvents) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } await Trace.Handlers.ModelHandlers.Meta.finalize(); const data = Trace.Handlers.ModelHandlers.Meta.data(); assert.strictEqual(data.browserProcessId, Trace.Types.Events.ProcessID(8017)); }); }); describe('browser thread ID', function() { it('obtains the TID if present', async () => { for (const event of baseEvents) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } await Trace.Handlers.ModelHandlers.Meta.finalize(); const data = Trace.Handlers.ModelHandlers.Meta.data(); assert.strictEqual(data.browserThreadId, Trace.Types.Events.ThreadID(775)); }); }); describe('renderer process ID', function() { it('obtains the PID if present', async () => { for (const event of baseEvents) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } await Trace.Handlers.ModelHandlers.Meta.finalize(); const data = Trace.Handlers.ModelHandlers.Meta.data(); assert.strictEqual(data.topLevelRendererIds.size, 1); assert.deepEqual([...data.topLevelRendererIds], [Trace.Types.Events.ProcessID(8051)]); }); }); describe('navigations', function() { it('obtains them if present', async () => { for (const event of baseEvents) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } await Trace.Handlers.ModelHandlers.Meta.finalize(); const data = Trace.Handlers.ModelHandlers.Meta.data(); // navigation-2 is discarded because it has no URL. // navigation-3 doesn't have a frame id so it is discarded as well. assert.strictEqual(data.navigationsByFrameId.size, 1); assert.strictEqual(data.navigationsByNavigationId.size, 1); const firstNavigation = data.navigationsByNavigationId.get('navigation-1'); if (!firstNavigation?.args.data) { assert.fail('Navigation data was expected in trace events'); return; } assert.strictEqual(firstNavigation.args.data.documentLoaderURL, 'test1'); }); it('provides a list of main frame only navigations', async function() { const events = await TraceLoader.rawEvents(this, 'multiple-navigations-with-iframes.json.gz'); Trace.Handlers.ModelHandlers.Meta.reset(); for (const event of events) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } await Trace.Handlers.ModelHandlers.Meta.finalize(); const data = Trace.Handlers.ModelHandlers.Meta.data(); const allNavigationsCount = data.navigationsByNavigationId.size; assert.isTrue(data.mainFrameNavigations.length < allNavigationsCount); assert.isTrue(data.mainFrameNavigations.every(event => { return event.args.frame === data.mainFrameId; })); }); }); describe('frames', function() { it('finds the main frame ID', async () => { for (const event of baseEvents) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } await Trace.Handlers.ModelHandlers.Meta.finalize(); const data = Trace.Handlers.ModelHandlers.Meta.data(); assert.strictEqual(data.mainFrameId, '3E1717BE677B75D0536E292E00D6A34A'); }); it('finds the main frame ID for a trace that started with a page reload', async function() { const events = await TraceLoader.rawEvents(this, 'reload-and-trace-page.json.gz'); Trace.Handlers.ModelHandlers.Meta.reset(); for (const event of events) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } await Trace.Handlers.ModelHandlers.Meta.finalize(); const data = Trace.Handlers.ModelHandlers.Meta.data(); assert.strictEqual(data.mainFrameId, '1D148CB660D1F96ED70D78DC6A53267B'); }); it('tracks the frames for found processes', async function() { const events = await TraceLoader.rawEvents(this, 'reload-and-trace-page.json.gz'); Trace.Handlers.ModelHandlers.Meta.reset(); for (const event of events) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } await Trace.Handlers.ModelHandlers.Meta.finalize(); const data = Trace.Handlers.ModelHandlers.Meta.data(); assert.strictEqual(data.frameByProcessId.size, 1); const [[processId, framesInProcess]] = data.frameByProcessId.entries(); assert.strictEqual(processId, 3581385); assert.strictEqual(framesInProcess.size, 1); const [{url}] = framesInProcess.values(); assert.strictEqual(url, 'https://example.com/'); }); }); describe('finding GPU thread and main frame', function() { it('finds the GPU process and GPU Thread', async function() { const events = await TraceLoader.rawEvents(this, 'threejs-gpu.json.gz'); Trace.Handlers.ModelHandlers.Meta.reset(); for (const event of events) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } await Trace.Handlers.ModelHandlers.Meta.finalize(); const {gpuProcessId, gpuThreadId} = Trace.Handlers.ModelHandlers.Meta.data(); assert.strictEqual(gpuProcessId, Trace.Types.Events.ProcessID(3581327)); assert.strictEqual(gpuThreadId, Trace.Types.Events.ThreadID(3581327)); }); it('handles traces that do not have a GPU thread and returns undefined for the thread ID', async function() { const traceEventsWithNoGPUThread = await TraceLoader.rawEvents(this, 'forced-layouts-and-no-gpu.json.gz'); for (const event of traceEventsWithNoGPUThread) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } assert.doesNotThrow(async () => { await Trace.Handlers.ModelHandlers.Meta.finalize(); }); const data = Trace.Handlers.ModelHandlers.Meta.data(); assert.isUndefined(data.gpuThreadId); }); }); it('obtains renderer process IDs when there are no navigations', async function() { let traceEvents: readonly Trace.Types.Events.Event[]; try { traceEvents = await TraceLoader.rawEvents(this, 'threejs-gpu.json.gz'); } catch (error) { assert.fail(error); return; } Trace.Handlers.ModelHandlers.Meta.reset(); for (const event of traceEvents) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } await Trace.Handlers.ModelHandlers.Meta.finalize(); const data = Trace.Handlers.ModelHandlers.Meta.data(); assert.deepEqual([...data.topLevelRendererIds], [3601132]); const rendererProcesses = data.rendererProcessesByFrame.get(data.mainFrameId); if (!rendererProcesses) { assert.fail('No renderer processes found'); return; } assert.deepEqual([...rendererProcesses?.keys()], [3601132]); const windowMinTime = 1143381875846; assert.deepEqual( [...rendererProcesses?.values()], [[{ frame: { frame: '1D148CB660D1F96ED70D78DC6A53267B', name: '', processId: 3601132, url: 'https://threejs.org/examples/', }, window: {min: windowMinTime, max: data.traceBounds.max, range: data.traceBounds.max - windowMinTime}, }]]); }); it('handles multiple renderers from navigations', async function() { let traceEvents: readonly Trace.Types.Events.Event[]; try { traceEvents = await TraceLoader.rawEvents(this, 'multiple-top-level-renderers.json.gz'); } catch (error) { assert.fail(error); return; } Trace.Handlers.ModelHandlers.Meta.reset(); for (const event of traceEvents) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } await Trace.Handlers.ModelHandlers.Meta.finalize(); const data = Trace.Handlers.ModelHandlers.Meta.data(); assert.deepEqual([...data.topLevelRendererIds], [78450, 78473, 79194]); const rendererProcesses = data.rendererProcessesByFrame.get(data.mainFrameId); if (!rendererProcesses) { assert.fail('No renderer processes found'); return; } const windowMinTime = 3550807444741; assert.deepEqual([...rendererProcesses?.keys()], [78450, 78473, 79194]); assert.deepEqual([...rendererProcesses?.values()], [ [{ frame: { frame: 'E70A9327100EBD78F1C03582BBBE8E5F', name: '', processId: 78450, url: 'http://127.0.0.1:8081/', }, window: {min: 3550803491779, max: 3550805534872, range: 2043093}, }], [{ frame: { frame: 'E70A9327100EBD78F1C03582BBBE8E5F', name: '', processId: 78473, url: 'http://localhost:8080/', }, window: {min: 3550805534873, max: 3550807444740, range: 1909867}, }], [{ frame: { frame: 'E70A9327100EBD78F1C03582BBBE8E5F', name: '', processId: 79194, url: 'https://www.google.com/', }, window: {min: windowMinTime, max: data.traceBounds.max, range: data.traceBounds.max - windowMinTime}, }], ]); }); it('handles multiple renderers from navigations where a process handled multiple URLs ', async function() { let traceEvents: readonly Trace.Types.Events.Event[]; try { traceEvents = await TraceLoader.rawEvents(this, 'simple-js-program.json.gz'); } catch (error) { assert.fail(error); return; } Trace.Handlers.ModelHandlers.Meta.reset(); for (const event of traceEvents) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } await Trace.Handlers.ModelHandlers.Meta.finalize(); const data = Trace.Handlers.ModelHandlers.Meta.data(); assert.deepEqual([...data.topLevelRendererIds], [2080]); const rendererProcesses = data.rendererProcessesByFrame.get(data.mainFrameId); if (!rendererProcesses) { assert.fail('No renderer processes found'); return; } assert.deepEqual([...rendererProcesses?.keys()], [2080]); assert.deepEqual([...rendererProcesses?.values()], [ [ { frame: { frame: '1F729458403A23CF1D8D246095129AC4', name: '', processId: 2080, url: 'about:blank', }, window: { min: 251126654355, max: 251126663397, range: 9042, }, }, { frame: { frame: '1F729458403A23CF1D8D246095129AC4', name: '', processId: 2080, url: 'https://www.google.com', }, window: { min: 251126663398, max: 251128073034, range: 1409636, }, }, ], ]); }); it('calculates trace bounds correctly', async function() { let traceEvents: readonly Trace.Types.Events.Event[]; try { traceEvents = await TraceLoader.rawEvents(this, 'basic.json.gz'); } catch (error) { assert.fail(error); return; } Trace.Handlers.ModelHandlers.Meta.reset(); for (const event of traceEvents) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } await Trace.Handlers.ModelHandlers.Meta.finalize(); const data = Trace.Handlers.ModelHandlers.Meta.data(); const { max, min, range, } = data.traceBounds; const expectedMin = 50_442_438_975; const expectedMax = 50_442_438_976; assert.strictEqual(min, expectedMin, 'Min calculated incorrectly'); assert.strictEqual(max, expectedMax, 'Max calculated incorrectly'); assert.strictEqual(range, expectedMax - expectedMin, 'Range calculated incorrectly'); }); it('calculates the min trace bound correctly if no TracingStartedInBrowser event is found', async function() { const baseEvents = await TraceLoader.rawEvents(this, 'basic.json.gz'); // We are about to mutate these events, so copy them to avoid mutating the // cached events from the TraceLoader. const traceEvents = baseEvents.slice().filter(event => { // Delete the tracing started in browser event to force the min bounds to // be calculated based on the event with the smallest timestamp. return event.name !== 'TracingStartedInBrowser'; }); Trace.Handlers.ModelHandlers.Meta.reset(); for (const event of traceEvents) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } await Trace.Handlers.ModelHandlers.Meta.finalize(); const data = Trace.Handlers.ModelHandlers.Meta.data(); const expectedMin = 50_442_438_976; assert.strictEqual(data.traceBounds.min, expectedMin, 'Min calculated incorrectly'); }); it('ignores ::UMA Events', async function() { let traceEvents: readonly Trace.Types.Events.Event[]; try { // This file contains UMA events which need to be ignored. traceEvents = await TraceLoader.rawEvents(this, 'web-dev.json.gz'); } catch (error) { assert.fail(error); return; } Trace.Handlers.ModelHandlers.Meta.reset(); for (const event of traceEvents) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } await Trace.Handlers.ModelHandlers.Meta.finalize(); const data = Trace.Handlers.ModelHandlers.Meta.data(); const { max, min, range, } = data.traceBounds; const expectedMin = 1_020_034_823_047; const expectedMax = 1_020_036_087_961; assert.strictEqual(min, expectedMin, 'Min calculated incorrectly'); assert.strictEqual(max, expectedMax, 'Max calculated incorrectly'); assert.strictEqual(range, expectedMax - expectedMin, 'Range calculated incorrectly'); }); it('collects all thread metadata in all processes', async () => { for (const event of baseEvents) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } await Trace.Handlers.ModelHandlers.Meta.finalize(); const data = Trace.Handlers.ModelHandlers.Meta.data(); const collected = [...data.threadsInProcess.values()].map(threadInProcess => [...threadInProcess.values()]); expect(collected.map(process => process.map(thread => thread.args.name))).to.deep.equal([ [ 'swapper', 'VizCompositorThread', 'ThreadPoolServiceThread', 'ThreadPoolBackgroundWorker', 'GpuWatchdog', 'ThreadPoolForegroundWorker', ], [ 'CrBrowserMain', 'Chrome_IOThread', 'ThreadPoolServiceThread', 'Chrome_DevToolsADBThread', 'ThreadPoolForegroundWorker', ], [ 'CrRendererMain', 'Compositor', 'Chrome_ChildIOThread', 'ThreadPoolServiceThread', 'ThreadPoolForegroundWorker', ], [ 'CrRendererMain', 'Compositor', 'Chrome_ChildIOThread', 'ThreadPoolServiceThread', 'ThreadPoolForegroundWorker', ], ]); }); it('can handle edge cases where there are multiple navigations with the same ID', async function() { // For context to why this test and trace file exist, see crbug.com/1503982 // If an HTML page contains <script>window.location.href = // 'javascript:console.log(1)'</script>, the backend will emit two // navigationStarted events that are identical except for timestamps, and // this caused the trace engine to crash. // To ensure that we handle this case, we have this test which makes sure a // trace that does have two navigations with the same ID does not cause the // MetaHandler to throw an error. const events = await TraceLoader.rawEvents(this, 'multiple-navigations-same-id.json.gz'); assert.doesNotThrow(function() { for (const event of events) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } }); }); it('marks a generic trace as generic', async function() { const events = await TraceLoader.rawEvents(this, 'generic-about-tracing.json.gz'); for (const event of events) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } await Trace.Handlers.ModelHandlers.Meta.finalize(); assert.isTrue(Trace.Handlers.ModelHandlers.Meta.data().traceIsGeneric); }); it('marks a web trace as being not generic', async function() { const events = await TraceLoader.rawEvents(this, 'web-dev-with-commit.json.gz'); for (const event of events) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } await Trace.Handlers.ModelHandlers.Meta.finalize(); assert.isFalse(Trace.Handlers.ModelHandlers.Meta.data().traceIsGeneric); }); it('sets the main frame URL from the TracingStartedInBrowser event', async function() { // This trace has the right URL in TracingStartedInBrowser const events = await TraceLoader.rawEvents(this, 'web-dev-with-commit.json.gz'); for (const event of events) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } await Trace.Handlers.ModelHandlers.Meta.finalize(); const data = Trace.Handlers.ModelHandlers.Meta.data(); assert.strictEqual(data.mainFrameURL, 'https://web.dev/'); }); it('will alter the main frame URL based on the first main frame navigation', async function() { // This trace has the wrong URL in TracingStartedInBrowser - but it will be // corrected by looking at the first main frame navigation. const events = await TraceLoader.rawEvents(this, 'web-dev-initial-url.json.gz'); for (const event of events) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } await Trace.Handlers.ModelHandlers.Meta.finalize(); const data = Trace.Handlers.ModelHandlers.Meta.data(); assert.strictEqual(data.mainFrameURL, 'https://web.dev/articles/inp'); }); it('returns a list of processes and process_name events', async function() { const events = await TraceLoader.rawEvents(this, 'web-dev-initial-url.json.gz'); for (const event of events) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } await Trace.Handlers.ModelHandlers.Meta.finalize(); const data = Trace.Handlers.ModelHandlers.Meta.data(); const pidsToNames = Array.from(data.processNames.entries(), ([pid, event]) => { return [pid, event.args.name]; }); assert.deepEqual(pidsToNames, [ [Trace.Types.Events.ProcessID(37605), 'Browser'], [Trace.Types.Events.ProcessID(48544), 'Renderer'], [Trace.Types.Events.ProcessID(37613), 'GPU Process'], [Trace.Types.Events.ProcessID(48531), 'Renderer'], ]); }); it('does not set a frame as a main frame if it has no URL.', async function() { // This test exists because of a bug report from this trace where we // incorrectly set the main frame ID, causing DevTools to pick an advert in // an iframe as the main thread. This happened because we happily set // mainFrameID to a frame that had no URL, which doesn't make sense. const events = await TraceLoader.rawEvents(this, 'wrong-main-frame-bug.json.gz'); for (const event of events) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } await Trace.Handlers.ModelHandlers.Meta.finalize(); const data = Trace.Handlers.ModelHandlers.Meta.data(); assert.strictEqual(data.mainFrameId, 'D1731088F5DE299149240DF9E6025291'); }); it('will use isOutermostMainFrame to determine the main frame from the TracingStartedInBrowser event if it is present', async function() { const events = await TraceLoader.rawEvents(this, 'web-dev-outermost-frames.json.gz'); for (const event of events) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } await Trace.Handlers.ModelHandlers.Meta.finalize(); const data = Trace.Handlers.ModelHandlers.Meta.data(); assert.strictEqual(data.mainFrameId, '881522AC20B813B0C0E99E27CEBAB951'); }); it('will use isInPrimaryPage along with isOutermostMainFrame to identify the main frame from TracingStartedInBrowser', async function() { // See crbug.com/343873756 for context on this bug report and fix. const events = await TraceLoader.rawEvents(this, 'primary-page-frame.json.gz'); for (const event of events) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } await Trace.Handlers.ModelHandlers.Meta.finalize(); const data = Trace.Handlers.ModelHandlers.Meta.data(); // If you look at the trace, this is the frame that is both: // isInPrimaryPage === true // isOutermostMainFrame == true // // The other frames have isOutermostMainFrame == true (as they are pre-rendered pages) // But they are not in the primary page. assert.strictEqual(data.mainFrameId, '07B7D55F5BE0ADB8AAD6502F2D3859FF'); }); it('will detect the final redirect URL for a main frame navigation', async function() { // See crbug.com/402743677 for context. const events = await TraceLoader.rawEvents(this, 'redirects-http-to-https.json.gz'); for (const event of events) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } await Trace.Handlers.ModelHandlers.Meta.finalize(); const data = Trace.Handlers.ModelHandlers.Meta.data(); assert.strictEqual(data.mainFrameURL, 'https://example.com/'); assert.strictEqual(data.mainFrameNavigations[0].args.data?.documentLoaderURL, 'http://paulirish.com/'); assert.deepEqual([...data.finalDisplayUrlByNavigationId], [ ['832038B69FE671709DE9655B8161EB9A', 'https://www.paulirish.com/'], ]); }); it('will detect history API navigations for the start of the trace even w/o any navigation', async function() { // See crbug.com/402743677 for context. const events = await TraceLoader.rawEvents(this, 'history-api-no-nav.json.gz'); for (const event of events) { Trace.Handlers.ModelHandlers.Meta.handleEvent(event); } await Trace.Handlers.ModelHandlers.Meta.finalize(); const data = Trace.Handlers.ModelHandlers.Meta.data(); assert.strictEqual(data.mainFrameURL, 'http://localhost:10325/test.html'); assert.deepEqual([...data.finalDisplayUrlByNavigationId], [ ['', 'http://localhost:10325/testing/me?0.7574185139653986'], ]); }); });