chrome-devtools-frontend
Version:
Chrome DevTools UI
762 lines (698 loc) • 29.2 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 {describeWithEnvironment} from '../../../testing/EnvironmentHelpers.js';
import {
getMainThread,
makeAsyncEndEvent,
makeAsyncStartEvent,
makeCompleteEvent,
makeInstantEvent,
} from '../../../testing/TraceHelpers.js';
import {TraceLoader} from '../../../testing/TraceLoader.js';
import * as Trace from '../trace.js';
describeWithEnvironment('Trace helpers', function() {
describe('extractOriginFromTrace', () => {
it('extracts the origin of a parsed trace correctly', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const origin = Trace.Helpers.Trace.extractOriginFromTrace(parsedTrace.Meta.mainFrameURL);
assert.strictEqual(origin, 'web.dev');
});
it('will remove the `www` if it is present', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'multiple-navigations.json.gz');
const origin = Trace.Helpers.Trace.extractOriginFromTrace(parsedTrace.Meta.mainFrameURL);
assert.strictEqual(origin, 'google.com');
});
it('returns null when no origin is found', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'missing-url.json.gz');
const origin = Trace.Helpers.Trace.extractOriginFromTrace(parsedTrace.Meta.mainFrameURL);
assert.isNull(origin);
});
});
describe('addEventToProcessThread', () => {
function makeTraceEvent(
pid: Trace.Types.Events.ProcessID, tid: Trace.Types.Events.ThreadID): Trace.Types.Events.Event {
return {
name: 'process_name',
tid,
pid,
ts: Trace.Types.Timing.Micro(0),
cat: 'test',
ph: Trace.Types.Events.Phase.METADATA,
};
}
function pid(x: number): Trace.Types.Events.ProcessID {
return Trace.Types.Events.ProcessID(x);
}
function tid(x: number): Trace.Types.Events.ThreadID {
return Trace.Types.Events.ThreadID(x);
}
const eventMap =
new Map<Trace.Types.Events.ProcessID, Map<Trace.Types.Events.ThreadID, Trace.Types.Events.Event[]>>();
beforeEach(() => {
eventMap.clear();
});
it('will create a process and thread if it does not exist yet', async () => {
const event = makeTraceEvent(pid(1), tid(1));
Trace.Helpers.Trace.addEventToProcessThread(event, eventMap);
assert.strictEqual(eventMap.get(pid(1))?.size, 1);
const threadEvents = eventMap.get(pid(1))?.get(tid(1));
assert.strictEqual(threadEvents?.length, 1);
});
it('adds new events to existing threads correctly', async () => {
const event = makeTraceEvent(pid(1), tid(1));
Trace.Helpers.Trace.addEventToProcessThread(event, eventMap);
const newEvent = makeTraceEvent(pid(1), tid(1));
Trace.Helpers.Trace.addEventToProcessThread(newEvent, eventMap);
assert.deepEqual(eventMap.get(pid(1))?.get(tid(1)), [event, newEvent]);
});
});
describe('sortTraceEventsInPlace', () => {
function makeFakeEvent(ts: number, dur: number): Trace.Types.Events.Event {
return {
ts: Trace.Types.Timing.Micro(ts),
dur: Trace.Types.Timing.Micro(dur),
} as unknown as Trace.Types.Events.Event;
}
it('sorts by start time in ASC order', () => {
const event1 = makeFakeEvent(1, 1);
const event2 = makeFakeEvent(2, 1);
const event3 = makeFakeEvent(3, 1);
const events = [event3, event1, event2];
Trace.Helpers.Trace.sortTraceEventsInPlace(events);
assert.deepEqual(events, [event1, event2, event3]);
});
it('sorts by longest duration if the timestamps are the same', () => {
const event1 = makeFakeEvent(1, 1);
const event2 = makeFakeEvent(1, 2);
const event3 = makeFakeEvent(1, 3);
const events = [event1, event2, event3];
Trace.Helpers.Trace.sortTraceEventsInPlace(events);
assert.deepEqual(events, [event3, event2, event1]);
});
});
describe('getNavigationForTraceEvent', () => {
it('returns the correct navigation for a request', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'multiple-navigations.json.gz');
const {NetworkRequests, Meta} = parsedTrace;
const request1 = NetworkRequests.byTime[0];
const navigationForFirstRequest =
Trace.Helpers.Trace.getNavigationForTraceEvent(request1, request1.args.data.frame, Meta.navigationsByFrameId);
assert.isUndefined(navigationForFirstRequest?.ts);
const request2 = NetworkRequests.byTime[1];
const navigationForSecondRequest =
Trace.Helpers.Trace.getNavigationForTraceEvent(request2, request2.args.data.frame, Meta.navigationsByFrameId);
assert.strictEqual(navigationForSecondRequest?.ts, Trace.Types.Timing.Micro(636471400029));
});
it('returns the correct navigation for a page load event', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'multiple-navigations.json.gz');
const {Meta, PageLoadMetrics} = parsedTrace;
const firstNavigationId = Meta.navigationsByNavigationId.keys().next().value!;
const fcp = PageLoadMetrics.metricScoresByFrameId.get(Meta.mainFrameId)
?.get(firstNavigationId)
?.get(Trace.Handlers.ModelHandlers.PageLoadMetrics.MetricName.FCP);
if (!fcp?.event) {
assert.fail('FCP not found');
}
const navigationForFirstRequest =
Trace.Helpers.Trace.getNavigationForTraceEvent(fcp.event, Meta.mainFrameId, Meta.navigationsByFrameId);
assert.strictEqual(navigationForFirstRequest?.args.data?.navigationId, firstNavigationId);
});
});
describe('extractId', () => {
it('returns the correct id for an event', async () => {
const fakeEventWithId = {id: 'id'} as unknown as Trace.Types.Events.PairableAsync;
const id = Trace.Helpers.Trace.extractId(fakeEventWithId);
assert.strictEqual(id, fakeEventWithId.id);
const fakeEventWithGlobalId2 = {id2: {global: 'globalId2'}} as unknown as Trace.Types.Events.PairableAsync;
const globalId2 = Trace.Helpers.Trace.extractId(fakeEventWithGlobalId2);
assert.strictEqual(globalId2, fakeEventWithGlobalId2.id2?.global);
const fakeEventWithLocalId2 = {id2: {local: 'localId2'}} as unknown as Trace.Types.Events.PairableAsync;
const localId2 = Trace.Helpers.Trace.extractId(fakeEventWithLocalId2);
assert.strictEqual(localId2, fakeEventWithLocalId2.id2?.local);
});
});
describe('mergeEventsInOrder', () => {
it('merges two ordered arrays of trace events with no duration', async () => {
const array1 = [
{
name: 'a',
ts: 0,
},
{
name: 'b',
ts: 2,
},
{
name: 'c',
ts: 4,
},
{
name: 'd',
ts: 6,
},
{
name: 'e',
ts: 8,
},
] as Trace.Types.Events.Event[];
const array2 = [
{
name: 'a',
ts: 1,
},
{
name: 'b',
ts: 3,
},
{
name: 'c',
ts: 5,
},
{
name: 'd',
ts: 7,
},
{
name: 'e',
ts: 9,
},
] as Trace.Types.Events.Event[];
const ordered = Trace.Helpers.Trace.mergeEventsInOrder(array1, array2);
for (let i = 1; i < ordered.length; i++) {
assert.isAbove(ordered[i].ts, ordered[i - 1].ts);
}
});
it('merges two ordered arrays of trace events with duration', async () => {
const array1 = [
{
name: 'a',
ts: 0,
dur: 10,
},
{
name: 'b',
ts: 2,
dur: 12,
},
{
name: 'c',
ts: 4,
dur: 2,
},
{
name: 'd',
ts: 6,
dur: 9,
},
{
name: 'e',
ts: 8,
dur: 100,
},
] as Trace.Types.Events.Event[];
const array2 = [
{
name: 'a',
ts: 1,
dur: 2,
},
{
name: 'b',
ts: 3,
dur: 1,
},
{
name: 'c',
ts: 5,
dur: 99,
},
{
name: 'd',
ts: 7,
},
{
name: 'e',
ts: 9,
dur: 0,
},
] as Trace.Types.Events.Event[];
const ordered = Trace.Helpers.Trace.mergeEventsInOrder(array1, array2);
for (let i = 1; i < ordered.length; i++) {
assert.isAbove(ordered[i].ts, ordered[i - 1].ts);
}
});
it('merges two ordered arrays of trace events when timestamps collide', async () => {
const array1 = [
{
name: 'a',
ts: 0,
dur: 10,
},
{
name: 'b',
ts: 2,
dur: 12,
},
{
name: 'c',
ts: 4,
dur: 2,
},
{
name: 'd',
ts: 6,
dur: 9,
},
{
name: 'e',
ts: 8,
dur: 100,
},
] as Trace.Types.Events.Event[];
const array2 = [
{
name: 'a',
ts: 0,
dur: 2,
},
{
name: 'b',
ts: 2,
dur: 1,
},
{
name: 'c',
ts: 4,
dur: 99,
},
{
name: 'd',
ts: 7,
},
{
name: 'e',
ts: 9,
dur: 0,
},
] as Trace.Types.Events.Event[];
const ordered = Trace.Helpers.Trace.mergeEventsInOrder(array1, array2);
for (let i = 1; i < ordered.length; i++) {
const dur = ordered[i].dur;
const durPrev = ordered[i - 1].dur;
const eventsHaveDuration = dur !== undefined && durPrev !== undefined;
const correctOrderForSharedTimestamp =
eventsHaveDuration && ordered[i].ts === ordered[i - 1].ts && dur <= durPrev;
assert.isTrue(ordered[i].ts > ordered[i - 1].ts || correctOrderForSharedTimestamp);
}
});
it('merges two ordered arrays of trace events when timestamps and durations collide', async () => {
const array1 = [
{
name: 'a',
ts: 0,
dur: 10,
},
{
name: 'b',
ts: 2,
dur: 10,
},
{
name: 'c',
ts: 4,
dur: 10,
},
{
name: 'd',
ts: 6,
dur: 10,
},
{
name: 'e',
ts: 8,
dur: 10,
},
] as Trace.Types.Events.Event[];
const array2 = [...array1];
const ordered = Trace.Helpers.Trace.mergeEventsInOrder(array1, array2);
for (let i = 1; i < ordered.length; i++) {
const dur = ordered[i].dur;
const durPrev = ordered[i - 1].dur;
const eventsHaveDuration = dur !== undefined && durPrev !== undefined;
const correctOrderForSharedTimestamp =
eventsHaveDuration && ordered[i].ts === ordered[i - 1].ts && dur <= durPrev;
assert.isTrue(ordered[i].ts > ordered[i - 1].ts || correctOrderForSharedTimestamp);
}
});
});
describe('activeURLForFrameAtTime', () => {
it('extracts the active url for a frame at a given time', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'simple-js-program.json.gz');
const frameId = '1F729458403A23CF1D8D246095129AC4';
const firstURL = Trace.Helpers.Trace.activeURLForFrameAtTime(
frameId, Trace.Types.Timing.Micro(251126654355), parsedTrace.Meta.rendererProcessesByFrame);
assert.strictEqual(firstURL, 'about:blank');
const secondURL = Trace.Helpers.Trace.activeURLForFrameAtTime(
frameId, Trace.Types.Timing.Micro(251126663398), parsedTrace.Meta.rendererProcessesByFrame);
assert.strictEqual(secondURL, 'https://www.google.com');
});
});
describe('createMatchedSortedSyntheticEvents', () => {
it('matches up arbitrary async events', async function() {
const events = await TraceLoader.rawEvents(this, 'user-timings.json.gz');
const asyncEvents =
events.filter(event => Trace.Types.Events.isPhaseAsync(event.ph)) as Trace.Types.Events.PairableAsync[];
const synthEvents = Trace.Helpers.Trace.createMatchedSortedSyntheticEvents(asyncEvents);
// There's a lot of events, let's only assert one event per name
const seen = new Set();
// Make a readable output of each event to assert
const eventSummary = (e: Trace.Types.Events.SyntheticEventPair) =>
`@ ${(e.ts / 1000 - 1003e5).toFixed(3).padEnd(9)} for ${(e.dur / 1000).toFixed(3).padStart(8)}: ${e.name}`;
const eventsSummary = synthEvents
.filter(e => {
const alreadySeen = seen.has(e.name);
seen.add(e.name);
return !alreadySeen;
})
.map(eventSummary);
assert.deepEqual(eventsSummary, [
'@ 22336.946 for 16.959: PipelineReporter',
'@ 22350.590 for 3.315: BeginImplFrameToSendBeginMainFrame',
'@ 40732.328 for 0.834: SendBeginMainFrameToCommit',
'@ 40733.162 for 0.307: Commit',
'@ 40733.469 for 0.097: EndCommitToActivation',
'@ 40733.566 for 0.019: Activation',
'@ 40733.585 for 1.775: EndActivateToSubmitCompositorFrame',
'@ 40735.360 for 58.412: SubmitCompositorFrameToPresentationCompositorFrame',
'@ 40735.360 for 0.148: SubmitToReceiveCompositorFrame',
'@ 40735.508 for 3.667: ReceiveCompositorFrameToStartDraw',
'@ 40739.175 for 54.136: StartDrawToSwapStart',
'@ 40793.311 for 0.461: Swap',
'@ 40810.809 for 205.424: first measure',
'@ 40810.809 for 606.224: second measure',
'@ 40825.971 for 11.802: InputLatency::MouseMove',
'@ 41818.833 for 2005.601: third measure',
]);
assert.lengthOf(synthEvents, 237);
});
describe('createSortedSyntheticEvents()', () => {
it('correctly creates synthetic events when instant animation events are present', async function() {
const events = await TraceLoader.rawEvents(this, 'instant-animation-events.json.gz');
const animationEvents = events.filter(event => Trace.Types.Events.isAnimation(event));
const animationSynthEvents = Trace.Helpers.Trace.createMatchedSortedSyntheticEvents(animationEvents);
const wantPairs = new Map<string, {compositeFailed: number, unsupportedProperties?: string[]}>([
[
'blink.animations,devtools.timeline,benchmark,rail:0x11d00230380:Animation',
{compositeFailed: 8224, unsupportedProperties: ['width']},
],
['blink.animations,devtools.timeline,benchmark,rail:0x11d00234738:Animation', {compositeFailed: 0}],
[
'blink.animations,devtools.timeline,benchmark,rail:0x11d00234b08:Animation',
{compositeFailed: 8224, unsupportedProperties: ['height']},
],
[
'blink.animations,devtools.timeline,benchmark,rail:0x11d00234ed8:Animation',
{compositeFailed: 8224, unsupportedProperties: ['font-size']},
],
]);
// Ensure we have the correct number of synthetic events created.
assert.deepEqual(wantPairs.size, animationSynthEvents.length);
animationSynthEvents.forEach(event => {
const id = event.id;
assert.exists(id);
assert.exists(wantPairs.get(id));
const beginEvent = event.args.data.beginEvent;
const endEvent = event.args.data.endEvent;
const instantEvents = event.args.data.instantEvents;
assert.exists(beginEvent);
// Check that the individual event ids match the synthetic id.
assert.isTrue(beginEvent.id2?.local && id.includes(beginEvent.id2?.local));
if (endEvent) {
assert.isTrue(endEvent.id2?.local && id?.includes(endEvent.id2?.local));
}
assert.isTrue(instantEvents?.every(event => event.id2?.local && id.includes(event.id2?.local)));
assert.lengthOf(instantEvents, 2);
// Check that the non-composited data matches the expected.
const nonCompositedEvents = instantEvents.filter(event => event.args.data.compositeFailed);
nonCompositedEvents.forEach(event => {
assert.strictEqual(event.args.data.compositeFailed, wantPairs.get(id)?.compositeFailed);
assert.deepEqual(event.args.data.unsupportedProperties, wantPairs.get(id)?.unsupportedProperties);
});
});
});
});
});
describe('getZeroIndexedLineAndColumnNumbersForEvent', () => {
it('subtracts one from the line number of a function call', async () => {
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(123),
lineNumber: 1,
columnNumber: 1,
},
},
};
assert.deepEqual(Trace.Helpers.Trace.getZeroIndexedLineAndColumnForEvent(fakeFunctionCall), {
lineNumber: 0,
columnNumber: 0,
});
});
});
describe('frameIDForEvent', () => {
it('returns the frame ID from beginData if the event has it', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz');
const parseHTMLEvent = parsedTrace.Renderer.allTraceEntries.find(Trace.Types.Events.isParseHTML);
assert.isOk(parseHTMLEvent);
const frameId = Trace.Helpers.Trace.frameIDForEvent(parseHTMLEvent);
assert.isNotNull(frameId);
assert.strictEqual(frameId, parseHTMLEvent.args.beginData.frame);
});
it('returns the frame ID from args.data if the event has it', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz');
const invalidateLayoutEvent = parsedTrace.Renderer.allTraceEntries.find(Trace.Types.Events.isInvalidateLayout);
assert.isOk(invalidateLayoutEvent);
const frameId = Trace.Helpers.Trace.frameIDForEvent(invalidateLayoutEvent);
assert.isNotNull(frameId);
assert.strictEqual(frameId, invalidateLayoutEvent.args.data.frame);
});
it('returns null if the event does not have a frame', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz');
const v8CompileEvent = parsedTrace.Renderer.allTraceEntries.find(Trace.Types.Events.isV8Compile);
assert.isOk(v8CompileEvent);
const frameId = Trace.Helpers.Trace.frameIDForEvent(v8CompileEvent);
assert.isNull(frameId);
});
});
describe('findUpdateLayoutTreeEvents', () => {
it('returns the set of UpdateLayoutTree events within the right time range', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'selector-stats.json.gz');
const mainThread = getMainThread(parsedTrace.Renderer);
const foundEvents = Trace.Helpers.Trace.findUpdateLayoutTreeEvents(
mainThread.entries,
parsedTrace.Meta.traceBounds.min,
);
assert.lengthOf(foundEvents, 11);
const lastEvent = foundEvents.at(-1);
assert.isOk(lastEvent);
// Check we can filter by endTime by making the endTime less than the start
// time of the last event:
const filteredByEndTimeEvents = Trace.Helpers.Trace.findUpdateLayoutTreeEvents(
mainThread.entries,
parsedTrace.Meta.traceBounds.min,
Trace.Types.Timing.Micro(lastEvent.ts - 1_000),
);
assert.lengthOf(filteredByEndTimeEvents, 10);
});
});
describe('forEachEvent', () => {
const pid = Trace.Types.Events.ProcessID(1);
const tid = Trace.Types.Events.ThreadID(1);
it('iterates through the events in the expected tree-like order', async () => {
// |------------- RunTask -------------||-- RunTask --|
// |-- RunMicrotasks --||-- Layout --|
// |- FunctionCall -|
const traceEvents = [
makeCompleteEvent('RunTask', 0, 10, '*', pid, tid), // 0..10
makeCompleteEvent('RunMicrotasks', 1, 3, '*', pid, tid), // 1..4
makeCompleteEvent('FunctionCall', 2, 1, '*', pid, tid), // 2..3
makeCompleteEvent('Layout', 5, 3, '*', pid, tid), // 5..8
makeCompleteEvent('RunTask', 11, 3, '*', pid, tid), // 11..14
];
const onStartEvent = sinon.spy<(e: Trace.Types.Events.Event) => void>(_event => {});
const onEndEvent = sinon.spy<(e: Trace.Types.Events.Event) => void>(_event => {});
Trace.Helpers.Trace.forEachEvent(traceEvents, {
onEndEvent,
onStartEvent,
});
const eventsFromStartEventCalls = onStartEvent.getCalls().map(a => a.args[0]);
const eventsFromEndEventCalls = onEndEvent.getCalls().map(a => a.args[0]);
assert.deepEqual(
eventsFromStartEventCalls.map(e => e.name),
['RunTask', 'RunMicrotasks', 'FunctionCall', 'Layout', 'RunTask'],
);
assert.deepEqual(
eventsFromEndEventCalls.map(e => e.name),
['FunctionCall', 'RunMicrotasks', 'Layout', 'RunTask', 'RunTask'],
);
});
it('allows for a custom start and end time', async () => {
// |------------- RunTask -------------||-- RunTask --|
// |-- RunMicrotasks --||-- Layout --|
// |- FunctionCall -|
const traceEvents = [
makeCompleteEvent('RunTask', 0, 10, '*', pid, tid), // 0..10
makeCompleteEvent('RunMicrotasks', 1, 3, '*', pid, tid), // 1..4
makeCompleteEvent('FunctionCall', 2, 1, '*', pid, tid), // 2..3
makeCompleteEvent('Layout', 5, 3, '*', pid, tid), // 5..8
];
const onStartEvent = sinon.spy<(e: Trace.Types.Events.Event) => void>(_event => {});
const onEndEvent = sinon.spy<(e: Trace.Types.Events.Event) => void>(_event => {});
Trace.Helpers.Trace.forEachEvent(traceEvents, {
onEndEvent,
onStartEvent,
startTime: Trace.Types.Timing.Micro(5),
endTime: Trace.Types.Timing.Micro(9),
});
const eventsFromStartEventCalls = onStartEvent.getCalls().map(a => a.args[0]);
const eventsFromEndEventCalls = onEndEvent.getCalls().map(a => a.args[0]);
// We expect the RunTask event (0-10) and the Layout event (5-8) as
// those fit in the 5-9 custom time range.
assert.deepEqual(
eventsFromStartEventCalls.map(e => e.name),
['RunTask', 'Layout'],
);
assert.deepEqual(
eventsFromEndEventCalls.map(e => e.name),
['Layout', 'RunTask'],
);
});
it('lets the user filter out events with a custom filter', async () => {
// |------------- RunTask -------------||-- RunTask --|
// |-- RunMicrotasks --||-- Layout --|
// |- FunctionCall -|
const traceEvents = [
makeCompleteEvent('RunTask', 0, 10, '*', pid, tid), // 0..10
makeCompleteEvent('RunMicrotasks', 1, 3, '*', pid, tid), // 1..4
makeCompleteEvent('FunctionCall', 2, 1, '*', pid, tid), // 2..3
makeCompleteEvent('Layout', 5, 3, '*', pid, tid), // 5..8
makeCompleteEvent('RunTask', 11, 3, '*', pid, tid), // 11..14
];
const onStartEvent = sinon.spy<(e: Trace.Types.Events.Event) => void>(_event => {});
const onEndEvent = sinon.spy<(e: Trace.Types.Events.Event) => void>(_event => {});
Trace.Helpers.Trace.forEachEvent(traceEvents, {
onEndEvent,
onStartEvent,
eventFilter(event) {
return event.name !== 'RunTask';
},
});
const eventsFromStartEventCalls = onStartEvent.getCalls().map(a => a.args[0]);
const eventsFromEndEventCalls = onEndEvent.getCalls().map(a => a.args[0]);
assert.deepEqual(
eventsFromStartEventCalls.map(e => e.name),
['RunMicrotasks', 'FunctionCall', 'Layout'],
);
assert.deepEqual(
eventsFromEndEventCalls.map(e => e.name),
['FunctionCall', 'RunMicrotasks', 'Layout'],
);
});
it('calls the onInstantEvent callback when it finds an event with 0 duration', async () => {
const traceEvents = [
makeInstantEvent('FakeInstantEvent', 0, '*', pid, tid),
];
const onStartEvent = sinon.spy<(e: Trace.Types.Events.Event) => void>(_event => {});
const onEndEvent = sinon.spy<(e: Trace.Types.Events.Event) => void>(_event => {});
const onInstantEvent = sinon.spy<(e: Trace.Types.Events.Event) => void>(_event => {});
Trace.Helpers.Trace.forEachEvent(traceEvents, {
onEndEvent,
onStartEvent,
onInstantEvent,
});
sinon.assert.callCount(onStartEvent, 0);
sinon.assert.callCount(onEndEvent, 0);
sinon.assert.callCount(onInstantEvent, 1);
});
it('skips async events by default', async () => {
const traceEvents = [
makeAsyncStartEvent('FakeAsync', 0, pid, tid),
makeAsyncEndEvent('FakeAsync', 0, pid, tid),
];
const onStartEvent = sinon.spy<(e: Trace.Types.Events.Event) => void>(_event => {});
const onEndEvent = sinon.spy<(e: Trace.Types.Events.Event) => void>(_event => {});
const onInstantEvent = sinon.spy<(e: Trace.Types.Events.Event) => void>(_event => {});
Trace.Helpers.Trace.forEachEvent(traceEvents, {
onEndEvent,
onStartEvent,
onInstantEvent,
});
sinon.assert.callCount(onStartEvent, 0);
sinon.assert.callCount(onEndEvent, 0);
sinon.assert.callCount(onInstantEvent, 0);
});
it('can be configured to include async events', async () => {
const traceEvents = [
makeAsyncStartEvent('FakeAsync', 0, pid, tid),
makeAsyncEndEvent('FakeAsync', 0, pid, tid),
];
const onStartEvent = sinon.spy<(e: Trace.Types.Events.Event) => void>(_event => {});
const onEndEvent = sinon.spy<(e: Trace.Types.Events.Event) => void>(_event => {});
const onInstantEvent = sinon.spy<(e: Trace.Types.Events.Event) => void>(_event => {});
Trace.Helpers.Trace.forEachEvent(traceEvents, {
onEndEvent,
onStartEvent,
onInstantEvent,
ignoreAsyncEvents: false,
});
sinon.assert.callCount(onStartEvent, 0);
sinon.assert.callCount(onEndEvent, 0);
sinon.assert.callCount(onInstantEvent, 2);
});
});
describe('isTopLevelEvent', () => {
it('is true for a RunTask event', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
const runTask = parsedTrace.Renderer.allTraceEntries.find(Trace.Types.Events.isRunTask);
assert.isOk(runTask);
assert.isTrue(Trace.Helpers.Trace.isTopLevelEvent(runTask));
});
});
describe('findNextEventAfterTimestamp', () => {
it('gets the first screenshot after a trace', async function() {
const {parsedTrace} = await TraceLoader.traceEngine(this, 'cls-multiple-frames.json.gz');
const screenshots = parsedTrace.Screenshots.legacySyntheticScreenshots ?? [];
const {clusters} = parsedTrace.LayoutShifts;
const shifts = clusters.flatMap(cluster => cluster.events);
assert.isAtLeast(shifts.length, 10);
shifts.forEach((shift, i) => {
const prevScreenshot = Trace.Helpers.Trace.findPreviousEventBeforeTimestamp(screenshots, shift.ts);
const nextScreenshot = Trace.Helpers.Trace.findNextEventAfterTimestamp(screenshots, shift.ts);
// There may be a screenshot after the last shift. but very possible there isn't.
if (i === shifts.length - 1) {
return;
}
assert.isNotNull(nextScreenshot);
assert.isNotNull(prevScreenshot);
// Make sure the screenshot came after the shift.
assert.isAbove(nextScreenshot.ts, shift.ts);
// Make sure the previous screenshot came before the shift
assert.isBelow(prevScreenshot.ts, shift.ts);
// Bonus, we expect the result of calling prevEvent* to be the same as `items[nextIndex - 1]`
const nextIndex = screenshots.indexOf(nextScreenshot);
const alsoPrevious = screenshots[nextIndex - 1];
assert.strictEqual(prevScreenshot, alsoPrevious);
});
});
});
});