UNPKG

chrome-devtools-frontend

Version:
762 lines (698 loc) • 29.2 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 {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); }); }); }); });