UNPKG

chrome-devtools-frontend

Version:
896 lines (852 loc) • 33.1 kB
// Copyright 2024 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 Trace from '../trace.js'; let idCounter = 0; export interface PerformanceAPIExtensionTestData { detail: {devtools?: Trace.Types.Extensions.ExtensionDataPayload}; name: string; ts: number; dur?: number; } export interface ConsoleAPIExtensionTestData { name: string; start?: string|number; end?: string|number; track?: string; trackGroup?: string; color?: string; ts: number; } function makeTimingEventWithPerformanceExtensionData( {name, ts: tsMicro, detail, dur: durMicro}: PerformanceAPIExtensionTestData): Trace.Types.Events.Event[] { const isMark = durMicro === undefined; const currentId = idCounter++; const traceEventBase = { cat: 'blink.user_timing', pid: Trace.Types.Events.ProcessID(2017), tid: Trace.Types.Events.ThreadID(259), id2: {local: `${currentId}`}, }; const stringDetail = JSON.stringify(detail); const args = isMark ? {data: {detail: stringDetail}} : {detail: stringDetail}; const firstEvent = { args, name, ph: isMark ? Trace.Types.Events.Phase.INSTANT : Trace.Types.Events.Phase.ASYNC_NESTABLE_START, ts: Trace.Types.Timing.Micro(tsMicro), ...traceEventBase, } as Trace.Types.Events.Event; if (isMark) { return [firstEvent]; } return [ firstEvent, { name, ...traceEventBase, ts: Trace.Types.Timing.Micro(tsMicro + (durMicro || 0)), ph: Trace.Types.Events.Phase.ASYNC_NESTABLE_END, }, ]; } function makeTimingEventWithConsoleExtensionData({name, ts, start, end, track, trackGroup, color}: ConsoleAPIExtensionTestData): Trace.Types.Events.ConsoleTimeStamp { return { cat: 'devtools.timeline', pid: Trace.Types.Events.ProcessID(2017), tid: Trace.Types.Events.ThreadID(259), name: Trace.Types.Events.Name.TIME_STAMP, args: { data: { message: name, start, end, track, trackGroup, color, } }, ts: Trace.Types.Timing.Micro(ts), ph: Trace.Types.Events.Phase.INSTANT, }; } export async function createTraceExtensionDataFromPerformanceAPITestInput( extensionData: PerformanceAPIExtensionTestData[]): Promise<Trace.Handlers.ModelHandlers.ExtensionTraceData.ExtensionTraceData> { const events = extensionData.flatMap(makeTimingEventWithPerformanceExtensionData).sort((e1, e2) => e1.ts - e2.ts); return await createTraceExtensionDataFromEvents(events); } async function createTraceExtensionDataFromConsoleAPITestInput(extensionData: ConsoleAPIExtensionTestData[]): Promise<Trace.Handlers.ModelHandlers.ExtensionTraceData.ExtensionTraceData> { const events = extensionData.flatMap(makeTimingEventWithConsoleExtensionData).sort((e1, e2) => e1.ts - e2.ts); return await createTraceExtensionDataFromEvents(events); } async function createTraceExtensionDataFromEvents(events: Trace.Types.Events.Event[]): Promise<Trace.Handlers.ModelHandlers.ExtensionTraceData.ExtensionTraceData> { Trace.Helpers.SyntheticEvents.SyntheticEventsManager.createAndActivate(events); Trace.Handlers.ModelHandlers.UserTimings.reset(); for (const event of events) { Trace.Handlers.ModelHandlers.UserTimings.handleEvent(event); } await Trace.Handlers.ModelHandlers.UserTimings.finalize(); Trace.Handlers.ModelHandlers.ExtensionTraceData.reset(); // ExtensionTraceData handler doesn't need to handle events since // it only consumes the output of the user timings handler. await Trace.Handlers.ModelHandlers.ExtensionTraceData.finalize(); return Trace.Handlers.ModelHandlers.ExtensionTraceData.data(); } describe('ExtensionTraceDataHandler', function() { describe('parsing extension data added via the performance.measure/mark API', function() { let extensionHandlerOutput: Trace.Handlers.ModelHandlers.ExtensionTraceData.ExtensionTraceData; beforeEach(async function() { extensionHandlerOutput = await createTraceExtensionDataExample(); }); after(() => { Trace.Handlers.ModelHandlers.ExtensionTraceData.reset(); Trace.Handlers.ModelHandlers.UserTimings.reset(); }); function createTraceExtensionDataExample(): Promise<Trace.Handlers.ModelHandlers.ExtensionTraceData.ExtensionTraceData> { const extensionData = [ { detail: { devtools: { color: 'error', dataType: 'marker', properties: [['Description', 'This marks the start of a task']], tooltipText: 'A mark', }, }, name: 'A custom mark', ts: 100, }, // Marker with invalid dataType { detail: {devtools: {color: 'error', dataType: 'invalid-marker'}}, name: 'A custom mark', ts: 100, }, { detail: { devtools: {dataType: 'track-entry', track: 'An Extension Track', properties: [['Description', 'Something']]}, }, name: 'An extension measurement', ts: 100, dur: 100, }, // Track entry with no explicit dataType (should be accepted) { detail: {devtools: {track: 'An Extension Track', color: 'tertiary'}}, name: 'An extension measurement', ts: 105, dur: 50, }, // Track entry with no explicit color (should be accepted) { detail: { devtools: { dataType: 'track-entry', track: 'Another Extension Track', properties: [['Description', 'Something'], ['Tip', 'A tip to improve this']], tooltipText: 'A hint if needed', }, }, name: 'An extension measurement', ts: 100, dur: 100, }, // Track entry with invalid data type (should be ignored). { detail: { devtools: { dataType: 'invalid-type' as Trace.Types.Extensions.ExtensionDataPayload['dataType'], track: 'Another Extension Track', }, }, name: 'An extension measurement', ts: 105, dur: 50, }, // Track entry with no track value (should be ignored). { detail: {devtools: {dataType: 'track-entry'}}, name: 'An extension measurement', ts: 105, dur: 50, }, ] as PerformanceAPIExtensionTestData[]; return createTraceExtensionDataFromPerformanceAPITestInput(extensionData); } describe('track data parsing from user timings that use the extension API', function() { it('creates tracks', async () => { assert.lengthOf(extensionHandlerOutput.extensionTrackData, 2); }); it('parses track data correctly', async () => { assert.lengthOf(extensionHandlerOutput.extensionTrackData[1].entriesByTrack['An Extension Track'], 2); assert.strictEqual(extensionHandlerOutput.extensionTrackData[1].name, 'An Extension Track'); assert.lengthOf(extensionHandlerOutput.extensionTrackData[0].entriesByTrack['Another Extension Track'], 1); assert.strictEqual(extensionHandlerOutput.extensionTrackData[0].name, 'Another Extension Track'); }); it('gets data from individual entries', async () => { const {tooltipText, track, properties} = extensionHandlerOutput.extensionTrackData[0].entriesByTrack['Another Extension Track'][0].args; assert.strictEqual(tooltipText, 'A hint if needed'); assert.strictEqual(track, 'Another Extension Track'); assert.strictEqual(JSON.stringify(properties), '[["Description","Something"],["Tip","A tip to improve this"]]'); }); it('discards track data without a corresponding track field', async () => { // The test example contains a track entry without a track field. // Ensure it is discarded. const allTrackEntries = extensionHandlerOutput.extensionTrackData.map(track => Object.values(track.entriesByTrack)).flat(2); const validTrackEntries = allTrackEntries.filter(entry => entry.args.track); assert.lengthOf(validTrackEntries, allTrackEntries.length); }); it('discards track data without a valid dataType field', async () => { // The test example contains extension data with an invalid dataType // value. // Ensure it is discarded. const allTrackEntries = extensionHandlerOutput.extensionTrackData.map(track => Object.values(track.entriesByTrack)).flat(2); const validTrackEntries = allTrackEntries.filter(entry => entry.args.dataType === 'track-entry' || entry.args.dataType === undefined); assert.lengthOf(validTrackEntries, allTrackEntries.length); }); }); describe('Timeline markers from user timings that use the extension API', function() { before(async function() { extensionHandlerOutput = await createTraceExtensionDataExample(); }); it('parses marker data correctly', async () => { assert.lengthOf(extensionHandlerOutput.extensionMarkers, 1); assert.strictEqual(extensionHandlerOutput.extensionMarkers[0].name, 'A custom mark'); const {tooltipText, properties} = extensionHandlerOutput.extensionMarkers[0].args; assert.strictEqual(tooltipText, 'A mark'); assert.strictEqual(JSON.stringify(properties), '[["Description","This marks the start of a task"]]'); }); it('discards markers whose details are not valid stringified JSON', async () => { const performanceMarkEvent: Trace.Types.Events.PerformanceMark = { args: { data: { detail: 'this-is-not-json', }, }, name: 'test-perf-mark', cat: 'blink.user_timing', ph: Trace.Types.Events.Phase.INSTANT, pid: Trace.Types.Events.ProcessID(1), tid: Trace.Types.Events.ThreadID(1), ts: Trace.Types.Timing.Micro(100), }; assert.isNull( Trace.Handlers.ModelHandlers.ExtensionTraceData.extensionDataInPerformanceTiming(performanceMarkEvent), ); }); it('discards markers without a valid dataType field', async () => { // The test example contains extension data with an invalid dataType // value. // Ensure it is discarded. const allMarkers = extensionHandlerOutput.extensionMarkers; const validTrackEntries = allMarkers.filter(entry => entry.args.dataType === 'marker'); assert.lengthOf(validTrackEntries, allMarkers.length); }); }); describe('Data filtering', () => { it('extracts the extension data from a timing\'s detail when present', async function() { const extensionData: PerformanceAPIExtensionTestData[] = [ { detail: { devtools: { color: 'error', dataType: 'marker', properties: [['Description', 'This marks the start of a task']], tooltipText: 'A mark', }, }, name: 'A custom mark', ts: 100, }, ]; const extensionHandlerOutput = await createTraceExtensionDataFromPerformanceAPITestInput(extensionData); assert.lengthOf(extensionHandlerOutput.extensionMarkers, 1); }); it('ignores a timing if its detail does not contain a devtools object', async function() { const extensionData = [ { detail: {}, name: 'A custom mark', ts: 100, }, ] as PerformanceAPIExtensionTestData[]; const extensionHandlerOutput = await createTraceExtensionDataFromPerformanceAPITestInput(extensionData); assert.lengthOf(extensionHandlerOutput.extensionMarkers, 0); }); it('ignores a timing if its detail contains a devtools object w/o valid extension data', async function() { const extensionData = [ { // Invalid data type detail: { devtools: { color: 'error', dataType: 'invalid' as Trace.Types.Extensions.ExtensionDataPayload['dataType'], }, }, name: 'A custom mark', ts: 100, }, { detail: { devtools: { // Defaulted to track-entry but no trackName provided color: 'error', }, }, name: 'A measurement', ts: 100, }, { detail: { devtools: { // track-entry w/o trackName provided dataType: 'track-entry', }, }, name: 'A measurement', ts: 100, }, ] as PerformanceAPIExtensionTestData[]; const extensionHandlerOutput = await createTraceExtensionDataFromPerformanceAPITestInput(extensionData); assert.lengthOf(extensionHandlerOutput.extensionMarkers, 0); }); it('ignores a timing if its detail contains a devtools with a track group but no track name', async function() { const extensionData = [ { // Invalid data type detail: { devtools: { trackGroup: 'Track group', }, }, name: 'A measurement', ts: 100, dur: 100, }, ] as PerformanceAPIExtensionTestData[]; const extensionHandlerOutput = await createTraceExtensionDataFromPerformanceAPITestInput(extensionData); assert.lengthOf(extensionHandlerOutput.extensionMarkers, 0); }); }); describe('Track groups', () => { it('builds extension track data for grouped tracks correctly', async function() { const extensionDevToolsObjects: Array<PerformanceAPIExtensionTestData['detail']['devtools']> = [ // Track group 1 { dataType: 'track-entry', trackGroup: 'Group 1', track: 'Track 1', }, { dataType: 'track-entry', trackGroup: 'Group 1', track: 'Track 1', }, { dataType: 'track-entry', trackGroup: 'Group 1', track: 'Track 2', }, { dataType: 'track-entry', trackGroup: 'Group 1', track: 'Track 3', }, // Track group 2 { dataType: 'track-entry', trackGroup: 'Group 2', track: 'Track 1', }, // Un grouped tracks { dataType: 'track-entry', track: 'Ungrouped Track 1', }, { dataType: 'track-entry', track: 'Ungrouped Track 2', }, { dataType: 'track-entry', track: 'Ungrouped Track 2', }, ]; const extensionHandlerOutput = await createTraceExtensionDataFromPerformanceAPITestInput( extensionDevToolsObjects.map((devtools, i) => ({ detail: {devtools}, name: 'A measurement', // Use different timestamps // to prevent event switching // due to equal start and end. ts: 100 + i, dur: 100, }))); assert.lengthOf(extensionHandlerOutput.extensionTrackData, 4); const firstTrackData = extensionHandlerOutput.extensionTrackData[0]; assert.strictEqual(firstTrackData.name, 'Group 1'); assert.isTrue(firstTrackData.isTrackGroup); assert.deepEqual(Object.keys(firstTrackData.entriesByTrack), ['Track 1', 'Track 2', 'Track 3']); assert.deepEqual(Object.values(firstTrackData.entriesByTrack).map(entries => entries.length), [2, 1, 1]); const secondTrackData = extensionHandlerOutput.extensionTrackData[1]; assert.strictEqual(secondTrackData.name, 'Group 2'); assert.isTrue(secondTrackData.isTrackGroup); assert.deepEqual(Object.keys(secondTrackData.entriesByTrack), ['Track 1']); assert.deepEqual(Object.values(secondTrackData.entriesByTrack).map(entries => entries.length), [1]); const thirdTrackData = extensionHandlerOutput.extensionTrackData[2]; assert.strictEqual(thirdTrackData.name, 'Ungrouped Track 1'); assert.deepEqual(Object.keys(thirdTrackData.entriesByTrack), ['Ungrouped Track 1']); assert.deepEqual(Object.values(thirdTrackData.entriesByTrack).map(entries => entries.length), [1]); const fourthTrackData = extensionHandlerOutput.extensionTrackData[3]; assert.strictEqual(fourthTrackData.name, 'Ungrouped Track 2'); assert.deepEqual(Object.keys(fourthTrackData.entriesByTrack), ['Ungrouped Track 2']); assert.deepEqual(Object.values(fourthTrackData.entriesByTrack).map(entries => entries.length), [2]); }); it('calculates self time sub track by sub track for events added with the performance API', async function() { const extensionDevToolsObjects: PerformanceAPIExtensionTestData[] = [ // Track group 1 { detail: { devtools: { dataType: 'track-entry', trackGroup: 'Group 1', track: 'Track 1', }, }, name: 'Measurement 1', ts: 0, dur: 100, }, { detail: { devtools: { dataType: 'track-entry', trackGroup: 'Group 1', track: 'Track 1', }, }, name: 'Measurement 2', ts: 0, dur: 20, }, { detail: { devtools: { dataType: 'track-entry', trackGroup: 'Group 1', track: 'Track 1', }, }, name: 'Measurement 3', ts: 60, dur: 40, }, { detail: { devtools: { dataType: 'track-entry', trackGroup: 'Group 1', track: 'Track 1', }, }, name: 'Measurement 4', ts: 70, dur: 10, }, { detail: { devtools: { dataType: 'track-entry', trackGroup: 'Group 1', track: 'Track 2', }, }, name: 'Measurement 5', ts: 0, dur: 200, }, // Standalone track { detail: { devtools: { dataType: 'track-entry', track: 'Ungrouped Track', }, }, name: 'Measurement 6', ts: 0, dur: 100, }, { detail: { devtools: { dataType: 'track-entry', track: 'Ungrouped Track', }, }, name: 'Measurement 7', ts: 50, dur: 50, }, ]; const extensionHandlerOutput = await createTraceExtensionDataFromPerformanceAPITestInput(extensionDevToolsObjects); assert.lengthOf(extensionHandlerOutput.extensionTrackData, 2); const trackGroupData = extensionHandlerOutput.extensionTrackData[0]; const testDataTrack1 = trackGroupData.entriesByTrack['Track 1'].map(entry => { const selfTime = extensionHandlerOutput.entryToNode.get(entry)?.selfTime as number; return {name: entry.name, selfTime}; }); assert.deepEqual(testDataTrack1, [ {name: 'Measurement 1', selfTime: 40}, {name: 'Measurement 2', selfTime: 20}, {name: 'Measurement 3', selfTime: 30}, {name: 'Measurement 4', selfTime: 10}, ]); const testDataTrack2 = trackGroupData.entriesByTrack['Track 2'].map(entry => { const selfTime = extensionHandlerOutput.entryToNode.get(entry)?.selfTime as number; return {name: entry.name, selfTime}; }); assert.deepEqual(testDataTrack2, [{name: 'Measurement 5', selfTime: 200}]); const ungroupedTrackData = extensionHandlerOutput.extensionTrackData[1].entriesByTrack['Ungrouped Track'].map(entry => { const selfTime = extensionHandlerOutput.entryToNode.get(entry)?.selfTime as number; return {name: entry.name, selfTime}; }); assert.deepEqual( ungroupedTrackData, [{name: 'Measurement 6', selfTime: 50}, {name: 'Measurement 7', selfTime: 50}]); }); }); }); describe('parsing extension data added via the console.timeStamp API', function() { let extensionHandlerOutput: Trace.Handlers.ModelHandlers.ExtensionTraceData.ExtensionTraceData; beforeEach(async function() { extensionHandlerOutput = await createTraceExtensionDataExample(); }); after(() => { Trace.Handlers.ModelHandlers.ExtensionTraceData.reset(); Trace.Handlers.ModelHandlers.UserTimings.reset(); }); function createTraceExtensionDataExample(): Promise<Trace.Handlers.ModelHandlers.ExtensionTraceData.ExtensionTraceData> { const extensionData: ConsoleAPIExtensionTestData[] = [ // Custom track 1 { name: 'Mark 1', ts: 100, }, { name: 'Measure 1', start: 'Mark 1', track: 'Custom track 1', ts: 200, }, // Custom track 2 { name: 'Mark 2', ts: 100, }, { name: 'Mark 3', ts: 200, }, { track: 'Custom track 2', name: 'Measure 2', start: 'Mark 2', end: 'Mark 3', ts: 300, }, // Custom track 3 { track: 'Custom track 3', name: 'Measure 3', start: 'Mark 1', end: 'Mark 4', ts: 300, }, { track: 'Custom track 3', name: 'Measure 4', start: 'Mark 2', end: 'Mark 3', ts: 300, }, // No track { name: 'Measure 5', start: 'Mark 1', end: 'Mark 4', ts: 300, }, // numeric start and end { track: 'Custom track 1', name: 'Measure 6', start: 300, end: 400, ts: 300, }, { track: 'Custom track 1', name: 'Measure 7', start: 350, ts: 400, } ]; return createTraceExtensionDataFromConsoleAPITestInput(extensionData); } describe('track data parsing', function() { it('creates tracks', async () => { assert.lengthOf(extensionHandlerOutput.extensionTrackData, 3); }); it('parses track data correctly', async () => { assert.lengthOf(extensionHandlerOutput.extensionTrackData, 3); const expectedData: Record<string, Array<{name: string, ts: number, dur: number}>> = { 'Custom track 3': [{name: 'Measure 3', ts: 100, dur: 200}, {name: 'Measure 4', ts: 100, dur: 100}], 'Custom track 1': [ {name: 'Measure 1', ts: 100, dur: 100}, {name: 'Measure 6', ts: 300, dur: 100}, {name: 'Measure 7', ts: 350, dur: 50} ], 'Custom track 2': [{name: 'Measure 2', ts: 100, dur: 100}] }; for (let i = 0; i < extensionHandlerOutput.extensionTrackData.length; i++) { const track = extensionHandlerOutput.extensionTrackData[i]; assert.strictEqual(track.name, Object.keys(expectedData)[i]); const actualTrackData = track.entriesByTrack[track.name]; const expectedTrackData = expectedData[track.name]; for (let j = 0; j < actualTrackData.length; j++) { const {name, ts, dur} = actualTrackData[j]; assert.strictEqual(name, expectedTrackData[j].name); assert.strictEqual(ts, expectedTrackData[j].ts); assert.strictEqual(dur, expectedTrackData[j].dur); } } }); it('parses synthetic console timings for the timings track', async () => { assert.lengthOf(extensionHandlerOutput.syntheticConsoleEntriesForTimingsTrack, 1); assert.strictEqual(extensionHandlerOutput.syntheticConsoleEntriesForTimingsTrack[0].name, 'Measure 5'); }); it('discards track data without a corresponding track field', async () => { // The test example contains a track entry without a track field. // Ensure it is discarded. const allTrackEntries = extensionHandlerOutput.extensionTrackData.map(track => Object.values(track.entriesByTrack)).flat(2); const validTrackEntries = allTrackEntries.filter(entry => entry.args.track); assert.lengthOf(validTrackEntries, allTrackEntries.length); }); }); describe('Track groups', () => { it('builds extension track data for grouped tracks correctly', async function() { const mockData = [ // Track group 1 { trackGroup: 'Group 1', track: 'Track 1', }, { trackGroup: 'Group 1', track: 'Track 1', }, { trackGroup: 'Group 1', track: 'Track 2', }, { trackGroup: 'Group 1', track: 'Track 3', }, // Track group 2 { trackGroup: 'Group 2', track: 'Track 1', }, // Un grouped tracks { track: 'Ungrouped Track 1', }, { track: 'Ungrouped Track 2', }, { track: 'Ungrouped Track 2', }, ]; const extensionHandlerOutput = await createTraceExtensionDataFromConsoleAPITestInput(mockData.map(({track, trackGroup}, i) => ({ track, trackGroup, name: 'A measurement', // Use different timestamps // to prevent event switching // due to equal start and end. ts: 100 + i, dur: 100, }))); assert.lengthOf(extensionHandlerOutput.extensionTrackData, 4); const firstTrackData = extensionHandlerOutput.extensionTrackData[0]; assert.strictEqual(firstTrackData.name, 'Group 1'); assert.isTrue(firstTrackData.isTrackGroup); assert.deepEqual(Object.keys(firstTrackData.entriesByTrack), ['Track 1', 'Track 2', 'Track 3']); assert.deepEqual(Object.values(firstTrackData.entriesByTrack).map(entries => entries.length), [2, 1, 1]); const secondTrackData = extensionHandlerOutput.extensionTrackData[1]; assert.strictEqual(secondTrackData.name, 'Group 2'); assert.isTrue(secondTrackData.isTrackGroup); assert.deepEqual(Object.keys(secondTrackData.entriesByTrack), ['Track 1']); assert.deepEqual(Object.values(secondTrackData.entriesByTrack).map(entries => entries.length), [1]); const thirdTrackData = extensionHandlerOutput.extensionTrackData[2]; assert.strictEqual(thirdTrackData.name, 'Ungrouped Track 1'); assert.deepEqual(Object.keys(thirdTrackData.entriesByTrack), ['Ungrouped Track 1']); assert.deepEqual(Object.values(thirdTrackData.entriesByTrack).map(entries => entries.length), [1]); const fourthTrackData = extensionHandlerOutput.extensionTrackData[3]; assert.strictEqual(fourthTrackData.name, 'Ungrouped Track 2'); assert.deepEqual(Object.keys(fourthTrackData.entriesByTrack), ['Ungrouped Track 2']); assert.deepEqual(Object.values(fourthTrackData.entriesByTrack).map(entries => entries.length), [2]); }); it('calculates self time sub track by sub track for events added with the console API', async function() { const extensionDevToolsObjects: ConsoleAPIExtensionTestData[] = [ // Track group 1 { name: 'A', ts: 0, }, { name: 'B', ts: 100, }, { trackGroup: 'Group 1', track: 'Track 1', name: 'Measurement 1', ts: 100, start: 'A', end: 'B', }, { name: 'C', ts: 0, }, { name: 'D', ts: 20, }, { trackGroup: 'Group 1', track: 'Track 1', name: 'Measurement 2', ts: 20, start: 'C', end: 'D', }, { name: 'E', ts: 60, }, { trackGroup: 'Group 1', track: 'Track 1', start: 'E', name: 'Measurement 3', ts: 100, }, { name: 'F', ts: 70, }, { trackGroup: 'Group 1', track: 'Track 1', name: 'Measurement 4', start: 'F', ts: 80, }, { trackGroup: 'Group 1', track: 'Track 1', name: 'Measurement 5', start: 75, ts: 80, }, { name: 'G', ts: 0, }, { name: 'H', ts: 200, }, { trackGroup: 'Group 1', track: 'Track 2', name: 'Measurement 5', start: 'G', end: 'H', ts: 200, }, // Standalone track { name: 'I', ts: 0, }, { track: 'Ungrouped Track', name: 'Measurement 6', ts: 100, start: 'I', }, { name: 'J', ts: 50, }, { name: 'K', ts: 100, }, { track: 'Ungrouped Track', name: 'Measurement 7', ts: 150, start: 'J', end: 'K', }, ]; const extensionHandlerOutput = await createTraceExtensionDataFromConsoleAPITestInput(extensionDevToolsObjects); assert.lengthOf(extensionHandlerOutput.extensionTrackData, 2); const trackGroupData = extensionHandlerOutput.extensionTrackData[0]; const testDataTrack1 = trackGroupData.entriesByTrack['Track 1'].map(entry => { const selfTime = extensionHandlerOutput.entryToNode.get(entry)?.selfTime as number; return {name: entry.name, selfTime}; }); assert.deepEqual(testDataTrack1, [ {name: 'Measurement 1', selfTime: 40}, {name: 'Measurement 2', selfTime: 20}, {name: 'Measurement 3', selfTime: 30}, {name: 'Measurement 4', selfTime: 5}, {name: 'Measurement 5', selfTime: 5}, ]); const testDataTrack2 = trackGroupData.entriesByTrack['Track 2'].map(entry => { const selfTime = extensionHandlerOutput.entryToNode.get(entry)?.selfTime as number; return {name: entry.name, selfTime}; }); assert.deepEqual(testDataTrack2, [{name: 'Measurement 5', selfTime: 200}]); const ungroupedTrackData = extensionHandlerOutput.extensionTrackData[1].entriesByTrack['Ungrouped Track'].map(entry => { const selfTime = extensionHandlerOutput.entryToNode.get(entry)?.selfTime as number; return {name: entry.name, selfTime}; }); assert.deepEqual( ungroupedTrackData, [{name: 'Measurement 6', selfTime: 50}, {name: 'Measurement 7', selfTime: 50}]); }); }); }); });