chrome-devtools-frontend
Version:
Chrome DevTools UI
270 lines (234 loc) • 12.7 kB
text/typescript
// Copyright 2023 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 Platform from '../../core/platform/platform.js';
import * as Trace from '../../models/trace/trace.js';
import {describeWithEnvironment} from '../../testing/EnvironmentHelpers.js';
import {setupIgnoreListManagerEnvironment} from '../../testing/TraceHelpers.js';
import {TraceLoader} from '../../testing/TraceLoader.js';
import * as PerfUi from '../../ui/legacy/components/perf_ui/perf_ui.js';
import * as Timeline from './timeline.js';
const {urlString} = Platform.DevToolsPath;
describeWithEnvironment('TimelineFlameChartDataProvider', function() {
describe('groupTreeEvents', function() {
it('returns the correct events for tree views given a flame chart group', async function() {
const dataProvider = new Timeline.TimelineFlameChartDataProvider.TimelineFlameChartDataProvider();
const {parsedTrace} = await TraceLoader.traceEngine(this, 'sync-like-timings.json.gz');
dataProvider.setModel(parsedTrace);
const timingsTrackGroup = dataProvider.timelineData().groups.find(g => g.name === 'Timings');
if (!timingsTrackGroup) {
assert.fail('Could not find Timings track flame chart group');
}
const groupTreeEvents = dataProvider.groupTreeEvents(timingsTrackGroup);
const allTimingEvents = [
...parsedTrace.UserTimings.consoleTimings,
...parsedTrace.UserTimings.timestampEvents,
...parsedTrace.UserTimings.performanceMarks,
...parsedTrace.UserTimings.performanceMeasures,
].sort((a, b) => a.ts - b.ts);
assert.deepEqual(groupTreeEvents, allTimingEvents);
});
it('filters out async events if they cannot be added to the tree', async function() {
const dataProvider = new Timeline.TimelineFlameChartDataProvider.TimelineFlameChartDataProvider();
const {parsedTrace} = await TraceLoader.traceEngine(this, 'timings-track.json.gz');
dataProvider.setModel(parsedTrace);
const timingsTrackGroup = dataProvider.timelineData().groups.find(g => g.name === 'Timings');
if (!timingsTrackGroup) {
assert.fail('Could not find Timings track flame chart group');
}
const groupTreeEvents = dataProvider.groupTreeEvents(timingsTrackGroup);
assert.strictEqual(groupTreeEvents?.length, 6);
const allEventsAreSync = groupTreeEvents?.every(event => !Trace.Types.Events.isPhaseAsync(event.ph));
assert.isTrue(allEventsAreSync);
});
});
it('can provide the index for an event and the event for a given index', async function() {
setupIgnoreListManagerEnvironment();
const dataProvider = new Timeline.TimelineFlameChartDataProvider.TimelineFlameChartDataProvider();
const {parsedTrace} = await TraceLoader.traceEngine(this, 'one-second-interaction.json.gz');
dataProvider.setModel(parsedTrace);
// Need to use an index that is not a frame, so jump past the frames.
const event = dataProvider.eventByIndex(100);
assert.isOk(event);
assert.strictEqual(dataProvider.indexForEvent(event), 100);
});
it('renders track in the correct order by default', async function() {
setupIgnoreListManagerEnvironment();
const dataProvider = new Timeline.TimelineFlameChartDataProvider.TimelineFlameChartDataProvider();
const {parsedTrace} = await TraceLoader.traceEngine(this, 'extension-tracks-and-marks.json.gz');
dataProvider.setModel(parsedTrace);
const groupNames = dataProvider.timelineData().groups.map(g => g.name);
assert.deepEqual(
groupNames,
[
'Frames',
'Timings',
'Interactions',
'A track group — Custom track',
'Another Extension Track',
'An Extension Track — Custom track',
'Main — http://localhost:3000/',
'Thread pool',
'Thread pool worker 1',
'Thread pool worker 2',
'Thread pool worker 3',
'StackSamplingProfiler',
'GPU',
],
);
});
it('can return the FlameChart group for a given event', async function() {
setupIgnoreListManagerEnvironment();
const dataProvider = new Timeline.TimelineFlameChartDataProvider.TimelineFlameChartDataProvider();
const {parsedTrace} = await TraceLoader.traceEngine(this, 'one-second-interaction.json.gz');
dataProvider.setModel(parsedTrace);
// Force the track appenders to run and populate the chart data.
dataProvider.timelineData();
const longest = parsedTrace.UserInteractions.longestInteractionEvent;
assert.isOk(longest);
const index = dataProvider.indexForEvent(longest);
assert.isNotNull(index);
const group = dataProvider.groupForEvent(index);
assert.strictEqual(group?.name, 'Interactions');
});
it('adds candy stripe and triangle decorations to long tasks in the main thread', async function() {
setupIgnoreListManagerEnvironment();
const dataProvider = new Timeline.TimelineFlameChartDataProvider.TimelineFlameChartDataProvider();
const {parsedTrace} = await TraceLoader.traceEngine(this, 'one-second-interaction.json.gz');
dataProvider.setModel(parsedTrace);
dataProvider.timelineData();
const {entryDecorations} = dataProvider.timelineData();
const stripingTitles: string[] = [];
const triangleTitles: string[] = [];
Object.entries(entryDecorations).forEach(([index, decorationsForEvent]) => {
const entryTitle = dataProvider.entryTitle(parseInt(index, 10)) ?? '';
for (const decoration of decorationsForEvent) {
if (decoration.type === PerfUi.FlameChart.FlameChartDecorationType.CANDY) {
stripingTitles.push(entryTitle);
}
if (decoration.type === PerfUi.FlameChart.FlameChartDecorationType.WARNING_TRIANGLE) {
triangleTitles.push(entryTitle);
}
}
});
assert.deepEqual(stripingTitles, [
'Pointer', // The interaction event in the Interactions track for the pointer event.
'Task', // The same long task as above, but rendered by the new engine.
]);
assert.deepEqual(triangleTitles, [
'Pointer', // The interaction event in the Interactions track for the pointer event.
'Task', // The same long task as above, but rendered by the new engine.
]);
});
it('populates the frames track with frames and screenshots', async function() {
const dataProvider = new Timeline.TimelineFlameChartDataProvider.TimelineFlameChartDataProvider();
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
dataProvider.setModel(parsedTrace);
const framesTrack = dataProvider.timelineData().groups.find(g => {
return g.name.includes('Frames');
});
if (!framesTrack) {
throw new Error('Could not find expected Frames track');
}
const framesLevel = framesTrack.startLevel;
const screenshotsLevel = framesLevel + 1;
// The frames track first shows the frames, and then shows screenhots just below it.
assert.strictEqual(
dataProvider.getEntryTypeForLevel(framesLevel), Timeline.TimelineFlameChartDataProvider.EntryType.FRAME);
assert.strictEqual(
dataProvider.getEntryTypeForLevel(screenshotsLevel),
Timeline.TimelineFlameChartDataProvider.EntryType.SCREENSHOT);
// There are 5 screenshots in this trace, so we expect there to be 5 events on the screenshots track level.
const eventsOnScreenshotsLevel = dataProvider.timelineData().entryLevels.filter(e => e === screenshotsLevel);
assert.lengthOf(eventsOnScreenshotsLevel, 5);
});
describe('ignoring frames', function() {
it('removes entries from the data that match the ignored URL', async function() {
const {ignoreListManager} = setupIgnoreListManagerEnvironment();
const dataProvider = new Timeline.TimelineFlameChartDataProvider.TimelineFlameChartDataProvider();
const {parsedTrace} = await TraceLoader.traceEngine(this, 'react-hello-world.json.gz');
dataProvider.setModel(parsedTrace);
const eventCountBeforeIgnoreList = dataProvider.timelineData().entryStartTimes.length;
const SCRIPT_TO_IGNORE = urlString`https://unpkg.com/react@18.2.0/umd/react.development.js`;
// Clear the data provider cache and add the React script to the ignore list.
dataProvider.reset();
dataProvider.setModel(parsedTrace);
ignoreListManager.ignoreListURL(SCRIPT_TO_IGNORE);
const eventCountAfterIgnoreList = dataProvider.timelineData().entryStartTimes.length;
// Ensure that the amount of events we show on the flame chart is less
// than before, now we have added the React URL to the ignore list.
assert.isBelow(eventCountAfterIgnoreList, eventCountBeforeIgnoreList);
// Clear the data provider cache and unignore the script again
dataProvider.reset();
dataProvider.setModel(parsedTrace);
ignoreListManager.unIgnoreListURL(SCRIPT_TO_IGNORE);
// Ensure that now we have un-ignored the URL that we get the full set of events again.
assert.strictEqual(dataProvider.timelineData().entryStartTimes.length, eventCountBeforeIgnoreList);
});
});
it('filters navigations to only return those that happen on the main frame', async function() {
const dataProvider = new Timeline.TimelineFlameChartDataProvider.TimelineFlameChartDataProvider();
const {parsedTrace} = await TraceLoader.traceEngine(this, 'multiple-navigations-with-iframes.json.gz');
dataProvider.setModel(parsedTrace);
const mainFrameID = parsedTrace.Meta.mainFrameId;
const navigationEvents = dataProvider.mainFrameNavigationStartEvents();
// Ensure that every navigation event that we return is for the main frame.
assert.isTrue(navigationEvents.every(navEvent => {
return navEvent.args.frame === mainFrameID;
}));
});
it('can search for entries within a given time-range', async function() {
const dataProvider = new Timeline.TimelineFlameChartDataProvider.TimelineFlameChartDataProvider();
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz');
dataProvider.setModel(parsedTrace);
const bounds = parsedTrace.Meta.traceBounds;
const filter = new Timeline.TimelineFilters.TimelineRegExp(/Evaluate script/);
const results = dataProvider.search(bounds, filter);
assert.lengthOf(results, 12);
assert.deepEqual(results[0], {index: 147, startTimeMilli: 122411041.395, provider: 'main'});
});
it('delete annotations associated with an event', async function() {
const dataProvider = new Timeline.TimelineFlameChartDataProvider.TimelineFlameChartDataProvider();
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz');
dataProvider.setModel(parsedTrace);
const entryIndex = 0;
const eventToFindAssociatedEntriesFor = dataProvider.eventByIndex(entryIndex);
const event = dataProvider.eventByIndex(1);
assert.exists(eventToFindAssociatedEntriesFor);
assert.exists(event);
// This label annotation should be deleted
Timeline.ModificationsManager.ModificationsManager.activeManager()?.createAnnotation({
type: 'ENTRY_LABEL',
entry: eventToFindAssociatedEntriesFor,
label: 'label',
});
Timeline.ModificationsManager.ModificationsManager.activeManager()?.createAnnotation({
type: 'ENTRY_LABEL',
entry: event,
label: 'label',
});
dataProvider.deleteAnnotationsForEntry(entryIndex);
// Make sure one of the annotations was deleted
assert.deepEqual(Timeline.ModificationsManager.ModificationsManager.activeManager()?.getAnnotations().length, 1);
});
it('correctly identifies if an event has annotations', async function() {
const dataProvider = new Timeline.TimelineFlameChartDataProvider.TimelineFlameChartDataProvider();
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev-with-commit.json.gz');
dataProvider.setModel(parsedTrace);
const eventIndex = 0;
const event = dataProvider.eventByIndex(eventIndex);
assert.exists(event);
// Create a label for an event
Timeline.ModificationsManager.ModificationsManager.activeManager()?.createAnnotation({
type: 'ENTRY_LABEL',
entry: event,
label: 'label',
});
// Made sure the event has annotations
assert.isTrue(dataProvider.entryHasAnnotations(eventIndex));
// Delete annotations for the event
dataProvider.deleteAnnotationsForEntry(eventIndex);
// Made sure the event does not have annotations
assert.isFalse(dataProvider.entryHasAnnotations(eventIndex));
});
});