Skip to content

Commit e8fb30c

Browse files
authored
fix: rework performance parsing & error handling (#75)
This PR tidies up the code around performance parsing and what we respond with from our tools. It introduces the ability to have no Insights from a trace (relatively rare, but can happen), and also adds more information to the output in the event that something went wrong. Previously we just logged errors, but if we respond with them here that will also help users report issues and increase the chances that we can debug them.
1 parent d6bab91 commit e8fb30c

File tree

5 files changed

+145
-28
lines changed

5 files changed

+145
-28
lines changed

src/tools/performance.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
getTraceSummary,
1212
InsightName,
1313
parseRawTraceBuffer,
14+
traceResultIsSuccess,
1415
} from '../trace-processing/parse.js';
1516
import {logger} from '../logger.js';
1617
import {Page} from 'puppeteer-core';
@@ -160,22 +161,26 @@ async function stopTracingAndAppendOutput(
160161
const traceEventsBuffer = await page.tracing.stop();
161162
const result = await parseRawTraceBuffer(traceEventsBuffer);
162163
response.appendResponseLine('The performance trace has been stopped.');
163-
if (result) {
164+
if (traceResultIsSuccess(result)) {
164165
context.storeTraceRecording(result);
165-
const insightText = getTraceSummary(result);
166-
if (insightText) {
167-
response.appendResponseLine('Insights with performance opportunities:');
168-
response.appendResponseLine(insightText);
169-
} else {
170-
response.appendResponseLine(
171-
'No insights have been found. The performance looks good!',
172-
);
173-
}
166+
response.appendResponseLine(
167+
'Here is a high level summary of the trace and the Insights that were found:',
168+
);
169+
const traceSummaryText = getTraceSummary(result);
170+
response.appendResponseLine(traceSummaryText);
171+
} else {
172+
response.appendResponseLine(
173+
'There was an unexpected error parsing the trace:',
174+
);
175+
response.appendResponseLine(result.error);
174176
}
175177
} catch (e) {
176-
logger(
177-
`Error stopping performance trace: ${e instanceof Error ? e.message : JSON.stringify(e)}`,
178+
const errorText = e instanceof Error ? e.message : JSON.stringify(e);
179+
logger(`Error stopping performance trace: ${errorText}`);
180+
response.appendResponseLine(
181+
'An error occured generating the response for this trace:',
178182
);
183+
response.appendResponseLine(errorText);
179184
} finally {
180185
context.setIsRunningPerformanceTrace(false);
181186
}

src/trace-processing/parse.ts

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,33 @@ const engine = TraceEngine.TraceModel.Model.createWithAllHandlers();
1414

1515
export interface TraceResult {
1616
parsedTrace: TraceEngine.TraceModel.ParsedTrace;
17-
insights: TraceEngine.Insights.Types.TraceInsightSets;
17+
insights: TraceEngine.Insights.Types.TraceInsightSets | null;
18+
}
19+
20+
export function traceResultIsSuccess(
21+
x: TraceResult | TraceParseError,
22+
): x is TraceResult {
23+
return 'parsedTrace' in x;
24+
}
25+
26+
export interface TraceParseError {
27+
error: string;
1828
}
1929

2030
export async function parseRawTraceBuffer(
2131
buffer: Uint8Array<ArrayBufferLike> | undefined,
22-
): Promise<TraceResult | null> {
32+
): Promise<TraceResult | TraceParseError> {
2333
engine.resetProcessor();
2434
if (!buffer) {
25-
return null;
35+
return {
36+
error: 'No buffer was provided.',
37+
};
2638
}
2739
const asString = new TextDecoder().decode(buffer);
2840
if (!asString) {
29-
return null;
41+
return {
42+
error: 'Decoding the trace buffer returned an empty string.',
43+
};
3044
}
3145
try {
3246
const data = JSON.parse(asString) as
@@ -39,25 +53,23 @@ export async function parseRawTraceBuffer(
3953
await engine.parse(events);
4054
const parsedTrace = engine.parsedTrace();
4155
if (!parsedTrace) {
42-
return null;
56+
return {
57+
error: 'No parsed trace was returned from the trace engine.',
58+
};
4359
}
4460

45-
const insights = parsedTrace?.insights;
46-
if (!insights) {
47-
return null;
48-
}
61+
const insights = parsedTrace?.insights ?? null;
4962

5063
return {
5164
parsedTrace,
5265
insights,
5366
};
5467
} catch (e) {
55-
if (e instanceof Error) {
56-
logger(`Error parsing trace: ${e.message}`);
57-
} else {
58-
logger(`Error parsing trace: ${JSON.stringify(e)}`);
59-
}
60-
return null;
68+
const errorText = e instanceof Error ? e.message : JSON.stringify(e);
69+
logger(`Unexpeced error parsing trace: ${errorText}`);
70+
return {
71+
error: errorText,
72+
};
6173
}
6274
}
6375

@@ -76,6 +88,12 @@ export function getInsightOutput(
7688
result: TraceResult,
7789
insightName: InsightName,
7890
): InsightOutput {
91+
if (!result.insights) {
92+
return {
93+
error: 'No Performance insights are available for this trace.',
94+
};
95+
}
96+
7997
// Currently, we do not support inspecting traces with multiple navigations. We either:
8098
// 1. Find Insights from the first navigation (common case: user records a trace with a page reload to test load performance)
8199
// 2. Fall back to finding Insights not associated with a navigation (common case: user tests an interaction without a page load).

tests/tools/performance.test.js.snapshot

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,56 @@ We can break this time down into the 4 phases that combine to make the LCP time:
4040
- https://web.dev/articles/lcp
4141
- https://web.dev/articles/optimize-lcp
4242
`;
43+
44+
exports[`performance > performance_stop_trace > returns an error message if parsing the trace buffer fails 1`] = `
45+
The performance trace has been stopped.
46+
There was an unexpected error parsing the trace:
47+
No buffer was provided.
48+
`;
49+
50+
exports[`performance > performance_stop_trace > returns the high level summary of the performance trace 1`] = `
51+
The performance trace has been stopped.
52+
Here is a high level summary of the trace and the Insights that were found:
53+
URL: https://web.dev/
54+
Bounds: {min: 122410994891, max: 122416385853}
55+
CPU throttling: none
56+
Network throttling: none
57+
Metrics (lab / observed):
58+
- LCP: 129 ms, event: (eventKey: r-6063, ts: 122411126100)
59+
- LCP breakdown:
60+
- TTFB: 8 ms, bounds: {min: 122410996889, max: 122411004828}
61+
- Load delay: 33 ms, bounds: {min: 122411004828, max: 122411037986}
62+
- Load duration: 15 ms, bounds: {min: 122411037986, max: 122411052690}
63+
- Render delay: 73 ms, bounds: {min: 122411052690, max: 122411126100}
64+
- CLS: 0.00
65+
Metrics (field / real users): n/a – no data for this page in CrUX
66+
Available insights:
67+
- insight name: LCPBreakdown
68+
description: Each [subpart has specific improvement strategies](https://web.dev/articles/optimize-lcp#lcp-breakdown). Ideally, most of the LCP time should be spent on loading the resources, not within delays.
69+
relevant trace bounds: {min: 122410996889, max: 122411126100}
70+
example question: Help me optimize my LCP score
71+
example question: Which LCP phase was most problematic?
72+
example question: What can I do to reduce the LCP time for this page load?
73+
- insight name: LCPDiscovery
74+
description: Optimize LCP by making the LCP image [discoverable](https://web.dev/articles/optimize-lcp#1_eliminate_resource_load_delay) from the HTML immediately, and [avoiding lazy-loading](https://web.dev/articles/lcp-lazy-loading)
75+
relevant trace bounds: {min: 122411004828, max: 122411055039}
76+
example question: Suggest fixes to reduce my LCP
77+
example question: What can I do to reduce my LCP discovery time?
78+
example question: Why is LCP discovery time important?
79+
- insight name: RenderBlocking
80+
description: Requests are blocking the page's initial render, which may delay LCP. [Deferring or inlining](https://web.dev/learn/performance/understanding-the-critical-path#render-blocking_resources) can move these network requests out of the critical path.
81+
relevant trace bounds: {min: 122411037528, max: 122411053852}
82+
example question: Show me the most impactful render blocking requests that I should focus on
83+
example question: How can I reduce the number of render blocking requests?
84+
- insight name: DocumentLatency
85+
description: Your first network request is the most important. Reduce its latency by avoiding redirects, ensuring a fast server response, and enabling text compression.
86+
relevant trace bounds: {min: 122410998910, max: 122411043781}
87+
estimated metric savings: FCP 0 ms, LCP 0 ms
88+
estimated wasted bytes: 77.1 kB
89+
example question: How do I decrease the initial loading time of my page?
90+
example question: Did anything slow down the request for this document?
91+
- insight name: ThirdParties
92+
description: 3rd party code can significantly impact load performance. [Reduce and defer loading of 3rd party code](https://web.dev/articles/optimizing-content-efficiency-loading-third-party-javascript/) to prioritize your page's content.
93+
relevant trace bounds: {min: 122411037881, max: 122416229595}
94+
example question: Which third parties are having the largest impact on my page performance?
95+
`;

tests/tools/performance.test.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {loadTraceAsBuffer} from '../trace-processing/fixtures/load.js';
1717
import {
1818
parseRawTraceBuffer,
1919
TraceResult,
20+
traceResultIsSuccess,
2021
} from '../../src/trace-processing/parse.js';
2122

2223
describe('performance', () => {
@@ -141,7 +142,9 @@ describe('performance', () => {
141142
async function parseTrace(fileName: string): Promise<TraceResult> {
142143
const rawData = loadTraceAsBuffer(fileName);
143144
const result = await parseRawTraceBuffer(rawData);
144-
assert.ok(result);
145+
if (!traceResultIsSuccess(result)) {
146+
assert.fail(`Unexpected trace parse error: ${result.error}`);
147+
}
145148
return result;
146149
}
147150

@@ -238,5 +241,30 @@ describe('performance', () => {
238241
sinon.assert.calledOnce(stopTracingStub);
239242
});
240243
});
244+
245+
it('returns an error message if parsing the trace buffer fails', async t => {
246+
await withBrowser(async (response, context) => {
247+
context.setIsRunningPerformanceTrace(true);
248+
const selectedPage = context.getSelectedPage();
249+
sinon
250+
.stub(selectedPage.tracing, 'stop')
251+
.returns(Promise.resolve(undefined));
252+
await stopTrace.handler({params: {}}, response, context);
253+
t.assert.snapshot(response.responseLines.join('\n'));
254+
});
255+
});
256+
257+
it('returns the high level summary of the performance trace', async t => {
258+
const rawData = loadTraceAsBuffer('web-dev-with-commit.json.gz');
259+
await withBrowser(async (response, context) => {
260+
context.setIsRunningPerformanceTrace(true);
261+
const selectedPage = context.getSelectedPage();
262+
sinon.stub(selectedPage.tracing, 'stop').callsFake(async () => {
263+
return rawData;
264+
});
265+
await stopTrace.handler({params: {}}, response, context);
266+
t.assert.snapshot(response.responseLines.join('\n'));
267+
});
268+
});
241269
});
242270
});

tests/trace-processing/parse.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,30 @@ describe('Trace parsing', async () => {
1515
it('can parse a Uint8Array from Tracing.stop())', async () => {
1616
const rawData = loadTraceAsBuffer('basic-trace.json.gz');
1717
const result = await parseRawTraceBuffer(rawData);
18+
if ('error' in result) {
19+
assert.fail(`Unexpected parse failure: ${result.error}`);
20+
}
1821
assert.ok(result?.parsedTrace);
1922
assert.ok(result?.insights);
2023
});
2124

2225
it('can format results of a trace', async t => {
2326
const rawData = loadTraceAsBuffer('web-dev-with-commit.json.gz');
2427
const result = await parseRawTraceBuffer(rawData);
28+
if ('error' in result) {
29+
assert.fail(`Unexpected parse failure: ${result.error}`);
30+
}
2531
assert.ok(result?.parsedTrace);
2632
assert.ok(result?.insights);
2733

2834
const output = getTraceSummary(result);
2935
t.assert.snapshot(output);
3036
});
37+
38+
it('will return a message if there is an error', async () => {
39+
const result = await parseRawTraceBuffer(undefined);
40+
assert.deepEqual(result, {
41+
error: 'No buffer was provided.',
42+
});
43+
});
3144
});

0 commit comments

Comments
 (0)