chrome-devtools-frontend
Version:
Chrome DevTools UI
229 lines (212 loc) • 11.1 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 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';
describeWithEnvironment('CompatibilityTracksAppender', function() {
let parsedTrace: Trace.Handlers.Types.ParsedTrace;
let tracksAppender: Timeline.CompatibilityTracksAppender.CompatibilityTracksAppender;
let entryData: Trace.Types.Events.Event[] = [];
let flameChartData = PerfUI.FlameChart.FlameChartTimelineData.createEmpty();
let entryTypeByLevel: Timeline.TimelineFlameChartDataProvider.EntryType[] = [];
async function initTrackAppender(
context: Mocha.Suite|Mocha.Context, fixture = 'timings-track.json.gz'): Promise<void> {
entryData = [];
flameChartData = PerfUI.FlameChart.FlameChartTimelineData.createEmpty();
entryTypeByLevel = [];
({parsedTrace} = await TraceLoader.traceEngine(context, fixture));
const entityMapper = new Timeline.Utils.EntityMapper.EntityMapper(parsedTrace);
tracksAppender = new Timeline.CompatibilityTracksAppender.CompatibilityTracksAppender(
flameChartData, parsedTrace, entryData, entryTypeByLevel, entityMapper);
const timingsTrack = tracksAppender.timingsTrackAppender();
const gpuTrack = tracksAppender.gpuTrackAppender();
const threadAppenders = tracksAppender.threadAppenders();
let currentLevel = timingsTrack.appendTrackAtLevel(0);
currentLevel = gpuTrack.appendTrackAtLevel(currentLevel);
for (const threadAppender of threadAppenders) {
currentLevel = threadAppender.appendTrackAtLevel(currentLevel);
}
}
beforeEach(async () => {
setupIgnoreListManagerEnvironment();
await initTrackAppender(this);
});
describe('Tree view data', () => {
describe('eventsInTrack', () => {
it('returns all the events appended by a track with multiple levels', () => {
const timingsTrack = tracksAppender.timingsTrackAppender();
const timingsTrackEvents = tracksAppender.eventsInTrack(timingsTrack);
const allTimingEvents = [
...parsedTrace.UserTimings.consoleTimings,
...parsedTrace.UserTimings.timestampEvents,
...parsedTrace.UserTimings.performanceMarks,
...parsedTrace.UserTimings.performanceMeasures,
].sort((a, b) => a.ts - b.ts);
assert.deepEqual(timingsTrackEvents, allTimingEvents);
});
it('returns all the events appended by a track with one level', () => {
const gpuTrack = tracksAppender.gpuTrackAppender();
const gpuTrackEvents = tracksAppender.eventsInTrack(gpuTrack) as readonly Trace.Types.Events.Event[];
assert.deepEqual(gpuTrackEvents, parsedTrace.GPU.mainGPUThreadTasks);
});
});
describe('eventsForTreeView', () => {
it('returns only sync events if using async events means a tree cannot be built', () => {
const timingsTrack = tracksAppender.timingsTrackAppender();
const timingsEvents = tracksAppender.eventsInTrack(timingsTrack);
assert.isFalse(Trace.Helpers.TreeHelpers.canBuildTreesFromEvents(timingsEvents));
const treeEvents = tracksAppender.eventsForTreeView(timingsTrack);
const allEventsAreSync = treeEvents.every(event => !Trace.Types.Events.isPhaseAsync(event.ph));
assert.isTrue(allEventsAreSync);
});
it('returns both sync and async events if a tree can be built with them', async () => {
// This file contains events in the timings track that can be assembled as a tree
await initTrackAppender(this, 'sync-like-timings.json.gz');
const timingsTrack = tracksAppender.timingsTrackAppender();
const timingsEvents = tracksAppender.eventsInTrack(timingsTrack);
assert.isTrue(Trace.Helpers.TreeHelpers.canBuildTreesFromEvents(timingsEvents));
const treeEvents = tracksAppender.eventsForTreeView(timingsTrack);
assert.deepEqual(treeEvents, timingsEvents);
});
it('returns events for tree view for nested tracks', async () => {
// This file contains two rasterizer threads which should be
// nested inside the same header.
await initTrackAppender(this, 'lcp-images-rasterizer.json.gz');
const rasterTracks = tracksAppender.threadAppenders().filter(
threadAppender => threadAppender.threadType === Trace.Handlers.Threads.ThreadType.RASTERIZER);
assert.lengthOf(rasterTracks, 2);
const raster1Events = tracksAppender.eventsInTrack(rasterTracks[0]);
assert.lengthOf(raster1Events, 6);
assert.isTrue(Trace.Helpers.TreeHelpers.canBuildTreesFromEvents(raster1Events));
const raster1TreeEvents = tracksAppender.eventsForTreeView(rasterTracks[0]);
assert.deepEqual(raster1TreeEvents, raster1Events);
const raster2Events = tracksAppender.eventsInTrack(rasterTracks[1]);
assert.lengthOf(raster2Events, 1);
assert.isTrue(Trace.Helpers.TreeHelpers.canBuildTreesFromEvents(raster2Events));
const raster2TreeEvents = tracksAppender.eventsForTreeView(rasterTracks[1]);
assert.deepEqual(raster2TreeEvents, raster2Events);
});
});
describe('groupEventsForTreeView', () => {
it('returns all the events of a flame chart group with multiple levels', async () => {
// This file contains events in the timings track that can be assembled as a tree
await initTrackAppender(this, 'sync-like-timings.json.gz');
const timingsGroupEvents = tracksAppender.groupEventsForTreeView(flameChartData.groups[0]);
if (!timingsGroupEvents) {
assert.fail('Could not find events for group');
}
const allTimingEvents = [
...parsedTrace.UserTimings.consoleTimings,
...parsedTrace.UserTimings.timestampEvents,
...parsedTrace.UserTimings.performanceMarks,
...parsedTrace.UserTimings.performanceMeasures,
].sort((a, b) => a.ts - b.ts);
assert.deepEqual(timingsGroupEvents, allTimingEvents);
});
it('returns all the events of a flame chart group with one level', () => {
const gpuGroupEvents =
tracksAppender.groupEventsForTreeView(flameChartData.groups[1]) as readonly Trace.Types.Events.Event[];
if (!gpuGroupEvents) {
assert.fail('Could not find events for group');
}
assert.deepEqual(gpuGroupEvents, parsedTrace.GPU.mainGPUThreadTasks);
});
});
});
describe('popoverInfo', () => {
it('shows the correct warning for a long task when hovered', async function() {
await initTrackAppender(this, 'simple-js-program.json.gz');
const events = parsedTrace.Renderer?.allTraceEntries;
if (!events) {
throw new Error('Could not find renderer events');
}
const longTask = events.find(e => (e.dur || 0) > 1_000_000);
if (!longTask) {
throw new Error('Could not find long task');
}
const info = tracksAppender.popoverInfo(longTask, 2);
assert.strictEqual(info.warningElements?.length, 1);
const warning = info.warningElements?.[0];
if (!(warning instanceof HTMLSpanElement)) {
throw new Error('Found unexpected warning');
}
assert.strictEqual(warning?.innerText, 'Long task took 1.30\u00A0s.');
});
it('shows the correct warning for a forced recalc styles when hovered', async function() {
await initTrackAppender(this, 'large-layout-small-recalc.json.gz');
const events = parsedTrace.Warnings.perWarning.get('FORCED_REFLOW') || [];
if (!events) {
throw new Error('Could not find forced reflows events');
}
const recalcStyles = events[0];
if (!recalcStyles) {
throw new Error('Could not find recalc styles');
}
const info = tracksAppender.popoverInfo(recalcStyles, 2);
assert.strictEqual(info.warningElements?.length, 1);
const warning = info.warningElements?.[0];
if (!(warning instanceof HTMLSpanElement)) {
throw new Error('Found unexpected warning');
}
assert.strictEqual(warning?.innerText, 'Forced reflow is a likely performance bottleneck.');
});
it('shows the correct warning for a forced layout when hovered', async function() {
await initTrackAppender(this, 'large-layout-small-recalc.json.gz');
const events = parsedTrace.Warnings.perWarning.get('FORCED_REFLOW') || [];
if (!events) {
throw new Error('Could not find forced reflows events');
}
const layout = events[1];
if (!layout) {
throw new Error('Could not find layout');
}
const info = tracksAppender.popoverInfo(layout, 2);
assert.strictEqual(info.warningElements?.length, 1);
const warning = info.warningElements?.[0];
if (!(warning instanceof HTMLSpanElement)) {
throw new Error('Found unexpected warning');
}
assert.strictEqual(warning?.innerText, 'Forced reflow is a likely performance bottleneck.');
});
it('shows the correct warning for slow idle callbacks', async function() {
await initTrackAppender(this, 'idle-callback.json.gz');
const events = parsedTrace.Renderer?.allTraceEntries;
if (!events) {
throw new Error('Could not find renderer events');
}
const idleCallback = events.find(event => {
const {duration} = Trace.Helpers.Timing.eventTimingsMilliSeconds(event);
if (!Trace.Types.Events.isFireIdleCallback(event)) {
return false;
}
if (duration <= event.args.data.allottedMilliseconds) {
false;
}
return true;
});
if (!idleCallback) {
throw new Error('Could not find idle callback');
}
const info = tracksAppender.popoverInfo(idleCallback, 2);
assert.strictEqual(info.warningElements?.length, 1);
const warning = info.warningElements?.[0];
if (!(warning instanceof HTMLSpanElement)) {
throw new Error('Found unexpected warning');
}
assert.strictEqual(warning?.innerText, 'Idle callback execution extended beyond deadline by 79.56\u00A0ms');
});
});
it('can return the group for a given level', async () => {
await initTrackAppender(this, 'web-dev-with-commit.json.gz');
// The order of these groups might seem odd, but it's based on the setup in
// the initTrackAppender function which does GPU and then threads.
const groupForLevel0 = tracksAppender.groupForLevel(0);
assert.strictEqual(groupForLevel0?.name, 'GPU');
const groupForLevel1 = tracksAppender.groupForLevel(1);
assert.strictEqual(groupForLevel1?.name, 'Main — https://web.dev/');
});
});