Skip to content

Commit dfca663

Browse files
jackfranklinDevtools-frontend LUCI CQ
authored andcommitted
RPP: support new format screenshot events
Once crrev.com/c/6197645 lands the screenshot trace event will be completely changed in terms of shape. This is to enable us to accurately relate screenshots to animation frames. This CL does not do that work, but instead does the work to update the trace engine types and the code to be able to handle both screenshot events which are of different shapes. Bug: 390127829 Change-Id: Iad9e563977b772ed185ea98554ce5c72392f61ea Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6216264 Reviewed-by: Andres Olivares <[email protected]> Auto-Submit: Jack Franklin <[email protected]> Commit-Queue: Andres Olivares <[email protected]> Commit-Queue: Jack Franklin <[email protected]>
1 parent bcaf542 commit dfca663

22 files changed

+276
-123
lines changed

front_end/models/trace/extras/FilmStrip.test.ts

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,55 @@ import {TraceLoader} from '../../../testing/TraceLoader.js';
77
import * as Trace from '../trace.js';
88

99
describeWithEnvironment('FilmStrip', function() {
10-
it('identifies the frames from a trace', async function() {
11-
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
12-
const filmStrip = Trace.Extras.FilmStrip.fromParsedTrace(parsedTrace);
13-
assert.lengthOf(filmStrip.frames, 5);
10+
/**
11+
* See `models/trace/types/TraceEvents.ts` for the type of `LegacyScreenshot`
12+
* and the comment explaining the differences in the "old" and "new" format.
13+
*/
14+
describe('using the old screenshot format', () => {
15+
it('identifies the frames from a trace', async function() {
16+
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
17+
const filmStrip = Trace.Extras.FilmStrip.fromParsedTrace(parsedTrace);
18+
assert.lengthOf(filmStrip.frames, 5);
19+
});
20+
21+
it('exposes the snapshot string for each frame', async function() {
22+
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
23+
const filmStrip = Trace.Extras.FilmStrip.fromParsedTrace(parsedTrace);
24+
assert.isTrue(filmStrip.frames.every(frame => {
25+
return Trace.Types.Events.isLegacySyntheticScreenshot(frame.screenshotEvent) &&
26+
typeof frame.screenshotEvent.args.dataUri === 'string' && frame.screenshotEvent.args.dataUri.length > 0;
27+
}));
28+
});
29+
30+
it('can use a custom zero time to filter out screenshots', async function() {
31+
const {screenshotImageDataUri} = Trace.Handlers.ModelHandlers.Screenshots;
32+
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
33+
const filmStrip = Trace.Extras.FilmStrip.fromParsedTrace(parsedTrace);
34+
// Set a custom zero time after the first screenshot and ensure that we now only have four events.
35+
const newCustomZeroTime = Trace.Types.Timing.Micro(filmStrip.frames[0].screenshotEvent.ts + 1000);
36+
const newFilmStrip = Trace.Extras.FilmStrip.fromParsedTrace(parsedTrace, newCustomZeroTime);
37+
// Check that the new film strip is all the frames other than the first, now we have set a custom time.
38+
assert.deepEqual(newFilmStrip.frames.map(f => screenshotImageDataUri(f.screenshotEvent)), [
39+
screenshotImageDataUri(filmStrip.frames[1].screenshotEvent),
40+
screenshotImageDataUri(filmStrip.frames[2].screenshotEvent),
41+
screenshotImageDataUri(filmStrip.frames[3].screenshotEvent),
42+
screenshotImageDataUri(filmStrip.frames[4].screenshotEvent),
43+
]);
44+
});
45+
});
46+
47+
describe('using the new screenshot format', () => {
48+
it('identifies the frames from a trace', async function() {
49+
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev-screenshot-source-ids.json.gz');
50+
const filmStrip = Trace.Extras.FilmStrip.fromParsedTrace(parsedTrace);
51+
assert.lengthOf(filmStrip.frames, 20);
52+
});
53+
54+
it('has an array of new screenshot events', async function() {
55+
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev-screenshot-source-ids.json.gz');
56+
const filmStrip = Trace.Extras.FilmStrip.fromParsedTrace(parsedTrace);
57+
assert.isTrue(filmStrip.frames.every(f => Trace.Types.Events.isScreenshot(f.screenshotEvent)));
58+
});
1459
});
1560

1661
it('caches the film strip based on the trace data and the zero time', async function() {
@@ -26,29 +71,6 @@ describeWithEnvironment('FilmStrip', function() {
2671
assert.notStrictEqual(filmStrip3, filmStrip4);
2772
});
2873

29-
it('exposes the snapshot string for each frame', async function() {
30-
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
31-
const filmStrip = Trace.Extras.FilmStrip.fromParsedTrace(parsedTrace);
32-
assert.isTrue(filmStrip.frames.every(frame => {
33-
return typeof frame.screenshotEvent.args.dataUri === 'string' && frame.screenshotEvent.args.dataUri.length > 0;
34-
}));
35-
});
36-
37-
it('can use a custom zero time to filter out screenshots', async function() {
38-
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
39-
const filmStrip = Trace.Extras.FilmStrip.fromParsedTrace(parsedTrace);
40-
// Set a custom zero time after the first screenshot and ensure that we now only have four events.
41-
const newCustomZeroTime = Trace.Types.Timing.Micro(filmStrip.frames[0].screenshotEvent.ts + 1000);
42-
const newFilmStrip = Trace.Extras.FilmStrip.fromParsedTrace(parsedTrace, newCustomZeroTime);
43-
// Check that the new film strip is all the frames other than the first, now we have set a custom time.
44-
assert.deepEqual(newFilmStrip.frames.map(f => f.screenshotEvent.args.dataUri), [
45-
filmStrip.frames[1].screenshotEvent.args.dataUri,
46-
filmStrip.frames[2].screenshotEvent.args.dataUri,
47-
filmStrip.frames[3].screenshotEvent.args.dataUri,
48-
filmStrip.frames[4].screenshotEvent.args.dataUri,
49-
]);
50-
});
51-
5274
it('can return the frame closest to a given timestamp', async function() {
5375
const {parsedTrace} = await TraceLoader.traceEngine(this, 'web-dev.json.gz');
5476
const filmStrip = Trace.Extras.FilmStrip.fromParsedTrace(parsedTrace);

front_end/models/trace/extras/FilmStrip.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ export interface Data {
1313
}
1414

1515
export interface Frame {
16-
screenshotEvent: Types.Events.SyntheticScreenshot;
16+
screenshotEvent: Types.Events.LegacySyntheticScreenshot|Types.Events.Screenshot;
1717
index: number;
1818
}
1919

@@ -42,7 +42,9 @@ export function fromParsedTrace(parsedTrace: HandlerDataWithScreenshots, customZ
4242
return fromCache;
4343
}
4444

45-
for (const screenshotEvent of parsedTrace.Screenshots.all) {
45+
const screenshots = parsedTrace.Screenshots.screenshots ?? parsedTrace.Screenshots.legacySyntheticScreenshots ?? [];
46+
47+
for (const screenshotEvent of screenshots) {
4648
if (screenshotEvent.ts < zeroTime) {
4749
continue;
4850
}

front_end/models/trace/handlers/LayoutShiftsHandler.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -183,10 +183,19 @@ function updateTraceWindowMax(traceWindow: Types.Timing.TraceWindowMicro, newMax
183183
}
184184

185185
function findScreenshots(timestamp: Types.Timing.Micro): Types.Events.LayoutShiftParsedData['screenshots'] {
186-
const screenshots = screenshotsHandlerData().all;
187-
const before = Helpers.Trace.findPreviousEventBeforeTimestamp(screenshots, timestamp);
188-
const after = before ? screenshots[screenshots.indexOf(before) + 1] : null;
189-
return {before, after};
186+
const data = screenshotsHandlerData();
187+
if (data.screenshots) {
188+
const before = Helpers.Trace.findPreviousEventBeforeTimestamp(data.screenshots, timestamp);
189+
const after = before ? data.screenshots[data.screenshots.indexOf(before) + 1] : null;
190+
return {before, after};
191+
}
192+
if (data.legacySyntheticScreenshots) {
193+
const before = Helpers.Trace.findPreviousEventBeforeTimestamp(data.legacySyntheticScreenshots, timestamp);
194+
const after = before ? data.legacySyntheticScreenshots[data.legacySyntheticScreenshots.indexOf(before) + 1] : null;
195+
return {before, after};
196+
}
197+
// No screenshots
198+
return {before: null, after: null};
190199
}
191200

192201
function buildScoreRecords(): void {

front_end/models/trace/handlers/ScreenshotsHandler.test.ts

Lines changed: 31 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,66 +2,53 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import {defaultTraceEvent} from '../../../testing/TraceHelpers.js';
65
import {TraceLoader} from '../../../testing/TraceLoader.js';
76
import * as Trace from '../trace.js';
87

98
describe('ScreenshotsHandler', function() {
10-
const baseEvent = {
11-
...defaultTraceEvent,
12-
name: 'Screenshot',
13-
// Ensure that the screenshots are held against the pid & tid values
14-
// that match the Browser process and CrBrowserMain in defaultTraceEvents.
15-
pid: Trace.Types.Events.ProcessID(8017),
16-
tid: Trace.Types.Events.ThreadID(775),
17-
ts: Trace.Types.Timing.Micro(0),
18-
args: {},
19-
cat: 'test',
20-
ph: Trace.Types.Events.Phase.OBJECT_SNAPSHOT,
21-
};
22-
23-
let baseEvents: readonly Trace.Types.Events.Event[];
24-
259
beforeEach(async function() {
26-
// The screenshot handler requires the meta handler because it needs
27-
// to know the browser process and thread IDs. Here, then, we reset
28-
// and later we will pass events to the meta handler, otherwise the
29-
// screenshots handler will fail.
3010
Trace.Handlers.ModelHandlers.Meta.reset();
31-
3211
Trace.Handlers.ModelHandlers.Screenshots.reset();
3312
});
13+
describe('supporting old and new screenshot formats', () => {
14+
async function runHandler(events: readonly Trace.Types.Events.Event[]) {
15+
Trace.Helpers.SyntheticEvents.SyntheticEventsManager.createAndActivate(events);
3416

35-
describe('frames', () => {
36-
it('obtains them if present', async function() {
37-
const defaultTraceEvents = await TraceLoader.rawEvents(this, 'basic.json.gz');
38-
39-
baseEvents = [
40-
...defaultTraceEvents,
41-
{...baseEvent, ts: Trace.Types.Timing.Micro(100)},
42-
{...baseEvent, ts: Trace.Types.Timing.Micro(200)},
43-
];
44-
Trace.Helpers.SyntheticEvents.SyntheticEventsManager.createAndActivate(baseEvents);
45-
46-
for (const event of baseEvents) {
17+
for (const event of events) {
4718
Trace.Handlers.ModelHandlers.Meta.handleEvent(event);
4819
Trace.Handlers.ModelHandlers.Screenshots.handleEvent(event);
4920
}
5021

5122
await Trace.Handlers.ModelHandlers.Meta.finalize();
5223
await Trace.Handlers.ModelHandlers.Screenshots.finalize();
24+
}
25+
26+
it('finds the screenshots in traces using the OBJECT_SNAPSHOT screenshot format', async function() {
27+
const events = await TraceLoader.rawEvents(this, 'web-dev-with-commit.json.gz');
28+
await runHandler(events);
29+
const data = Trace.Handlers.ModelHandlers.Screenshots.data();
30+
assert.isOk(data.legacySyntheticScreenshots);
31+
assert.isNull(data.screenshots);
32+
assert.lengthOf(data.legacySyntheticScreenshots, 18);
33+
});
5334

54-
const data = Trace.Handlers.ModelHandlers.Screenshots.data().all;
55-
assert.lengthOf(data, 2);
35+
// TODO: leave explainer comment here
36+
it('finds the screenshots in traces using the new instant event screenshot format', async function() {
37+
const events = await TraceLoader.rawEvents(this, 'web-dev-screenshot-source-ids.json.gz');
38+
await runHandler(events);
39+
const data = Trace.Handlers.ModelHandlers.Screenshots.data();
40+
assert.isOk(data.screenshots);
41+
assert.isNull(data.legacySyntheticScreenshots);
42+
assert.lengthOf(data.screenshots, 20);
5643
});
5744
});
5845

5946
describe('presentation timestamps', () => {
6047
function getMsDifferences(
61-
syntheticScreenshots: Trace.Types.Events.SyntheticScreenshot[],
62-
originalScreenshotEvents: Trace.Types.Events.Screenshot[]): number[] {
48+
syntheticScreenshots: Trace.Types.Events.LegacySyntheticScreenshot[],
49+
originalScreenshotEvents: Trace.Types.Events.LegacyScreenshot[]): number[] {
6350
return syntheticScreenshots.map((synEvent, i) => {
64-
const origEvent = originalScreenshotEvents.at(i) as Trace.Types.Events.Screenshot;
51+
const origEvent = originalScreenshotEvents.at(i) as Trace.Types.Events.LegacyScreenshot;
6552
const msDifference = (synEvent.ts - origEvent.ts) / 1000;
6653
return msDifference;
6754
});
@@ -77,8 +64,9 @@ describe('ScreenshotsHandler', function() {
7764
await Trace.Handlers.ModelHandlers.Meta.finalize();
7865
await Trace.Handlers.ModelHandlers.Screenshots.finalize();
7966

80-
const syntheticScreenshots = Trace.Handlers.ModelHandlers.Screenshots.data().all;
81-
const originalScreenshotEvents = events.filter(Trace.Types.Events.isScreenshot);
67+
const syntheticScreenshots = Trace.Handlers.ModelHandlers.Screenshots.data().legacySyntheticScreenshots;
68+
assert.isOk(syntheticScreenshots);
69+
const originalScreenshotEvents = events.filter(Trace.Types.Events.isLegacyScreenshot);
8270
assert.strictEqual(syntheticScreenshots.length, originalScreenshotEvents.length);
8371

8472
for (const oEvent of originalScreenshotEvents) {
@@ -103,8 +91,9 @@ describe('ScreenshotsHandler', function() {
10391
await Trace.Handlers.ModelHandlers.Meta.finalize();
10492
await Trace.Handlers.ModelHandlers.Screenshots.finalize();
10593

106-
const syntheticScreenshots = Trace.Handlers.ModelHandlers.Screenshots.data().all;
107-
const originalScreenshotEvents = events.filter(Trace.Types.Events.isScreenshot);
94+
const syntheticScreenshots = Trace.Handlers.ModelHandlers.Screenshots.data().legacySyntheticScreenshots;
95+
const originalScreenshotEvents = events.filter(Trace.Types.Events.isLegacyScreenshot);
96+
assert.isOk(syntheticScreenshots);
10897
assert.strictEqual(syntheticScreenshots.length, originalScreenshotEvents.length);
10998

11099
for (const oEvent of originalScreenshotEvents) {

front_end/models/trace/handlers/ScreenshotsHandler.ts

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,24 @@ import type {HandlerName} from './types.js';
1111
// used to store the event in the correct process thread entry below.
1212
const unpairedAsyncEvents: Types.Events.PipelineReporter[] = [];
1313

14-
const snapshotEvents: Types.Events.Screenshot[] = [];
15-
const syntheticScreenshots: Types.Events.SyntheticScreenshot[] = [];
14+
const legacyScreenshotEvents: Types.Events.LegacyScreenshot[] = [];
15+
const modernScreenshotEvents: Types.Events.Screenshot[] = [];
16+
const syntheticScreenshots: Types.Events.LegacySyntheticScreenshot[] = [];
1617
let frameSequenceToTs: Record<string, Types.Timing.Micro> = {};
1718

1819
export function reset(): void {
1920
unpairedAsyncEvents.length = 0;
20-
snapshotEvents.length = 0;
21+
legacyScreenshotEvents.length = 0;
2122
syntheticScreenshots.length = 0;
23+
modernScreenshotEvents.length = 0;
2224
frameSequenceToTs = {};
2325
}
2426

2527
export function handleEvent(event: Types.Events.Event): void {
26-
if (Types.Events.isScreenshot(event)) {
27-
snapshotEvents.push(event);
28+
if (Types.Events.isLegacyScreenshot(event)) {
29+
legacyScreenshotEvents.push(event);
30+
} else if (Types.Events.isScreenshot(event)) {
31+
modernScreenshotEvents.push(event);
2832
} else if (Types.Events.isPipelineReporter(event)) {
2933
unpairedAsyncEvents.push(event);
3034
}
@@ -39,10 +43,10 @@ export async function finalize(): Promise<void> {
3943
return [frameSequenceId, presentationTs];
4044
}));
4145

42-
for (const snapshotEvent of snapshotEvents) {
46+
for (const snapshotEvent of legacyScreenshotEvents) {
4347
const {cat, name, ph, pid, tid} = snapshotEvent;
4448
const syntheticEvent = Helpers.SyntheticEvents.SyntheticEventsManager.registerSyntheticEvent<
45-
Types.Events.SyntheticScreenshot>({
49+
Types.Events.LegacySyntheticScreenshot>({
4650
rawSourceEvent: snapshotEvent,
4751
cat,
4852
name,
@@ -60,14 +64,21 @@ export async function finalize(): Promise<void> {
6064
}
6165
}
6266

67+
export function screenshotImageDataUri(event: Types.Events.LegacySyntheticScreenshot|Types.Events.Screenshot): string {
68+
if (Types.Events.isLegacySyntheticScreenshot(event)) {
69+
return event.args.dataUri;
70+
}
71+
return `data:image/jpg;base64,${event.args.snapshot}`;
72+
}
73+
6374
/**
6475
* Correct the screenshot timestamps
6576
* The screenshot 'snapshot object' trace event has the "frame sequence number" attached as an ID.
6677
* We match that up with the "PipelineReporter" trace events as they terminate at presentation.
6778
* Presentation == when the pixels hit the screen. AKA Swap on the GPU
6879
*/
6980
// eslint-disable-next-line @typescript-eslint/no-unused-vars
70-
function getPresentationTimestamp(screenshotEvent: Types.Events.Screenshot): Types.Timing.Micro {
81+
function getPresentationTimestamp(screenshotEvent: Types.Events.LegacyScreenshot): Types.Timing.Micro {
7182
const frameSequence = parseInt(screenshotEvent.id, 16);
7283
// If it's 1, then it's an old trace (before https://crrev.com/c/4957973) and cannot be corrected.
7384
if (frameSequence === 1) {
@@ -85,9 +96,23 @@ function getPresentationTimestamp(screenshotEvent: Types.Events.Screenshot): Typ
8596
return updatedTs ?? screenshotEvent.ts;
8697
}
8798

99+
export interface Data {
100+
// These are nullable because in January 2025 a CL in Chromium
101+
// crrev.com/c/6197645 landed which changed the format of screenshots. For a
102+
// given trace, it can have either "legacy" screenshot events, or "modern"
103+
// screenshot events, but no trace can ever contain both.
104+
// So, if either of these arrays are empty, we instead return `null`. This forces consumers to check the presence of the array.
105+
// Traces can have no screenshots if the trace category is not enabled, so it
106+
// is possible for a trace to return null for both of these arrays.
107+
legacySyntheticScreenshots: Types.Events.LegacySyntheticScreenshot[]|null;
108+
screenshots: Types.Events.Screenshot[]|null;
109+
}
88110
// TODO(crbug/41484172): should be readonly
89-
export function data(): ({all: Types.Events.SyntheticScreenshot[]}) {
90-
return {all: syntheticScreenshots};
111+
export function data(): Data {
112+
return {
113+
legacySyntheticScreenshots: syntheticScreenshots.length ? syntheticScreenshots : null,
114+
screenshots: modernScreenshotEvents.length ? modernScreenshotEvents : null,
115+
};
91116
}
92117

93118
export function deps(): HandlerName[] {

front_end/models/trace/helpers/SyntheticEvents.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ describe('SyntheticEvents', function() {
4545
const allSyntheticEvents = [
4646
...parsedTrace.Animations.animations,
4747
...parsedTrace.NetworkRequests.byTime,
48-
...parsedTrace.Screenshots.all,
48+
...parsedTrace.Screenshots.legacySyntheticScreenshots ?? [],
4949
];
5050
const syntheticEventsManager = Trace.Helpers.SyntheticEvents.SyntheticEventsManager.getActiveManager();
5151
for (const syntheticEvent of allSyntheticEvents) {

front_end/models/trace/helpers/Trace.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -732,7 +732,7 @@ describeWithEnvironment('Trace helpers', function() {
732732
describe('findNextEventAfterTimestamp', () => {
733733
it('gets the first screenshot after a trace', async function() {
734734
const {parsedTrace} = await TraceLoader.traceEngine(this, 'cls-multiple-frames.json.gz');
735-
const screenshots = parsedTrace.Screenshots.all;
735+
const screenshots = parsedTrace.Screenshots.legacySyntheticScreenshots ?? [];
736736
const {clusters} = parsedTrace.LayoutShifts;
737737
const shifts = clusters.flatMap(cluster => cluster.events);
738738
assert.isAtLeast(shifts.length, 10);

0 commit comments

Comments
 (0)