chrome-devtools-frontend
Version:
Chrome DevTools UI
639 lines (575 loc) • 24.6 kB
text/typescript
// 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'],
]);
});
});