From ef7d1c416118b53529999b6fb302a7faa3c42a03 Mon Sep 17 00:00:00 2001 From: Jack Franklin Date: Mon, 15 Sep 2025 09:18:23 +0100 Subject: [PATCH 1/2] feat: Use PerformanceTraceFormatter to give a summary of the trace. Note: this will error until a chrome-devtools-frontend with this change https://chromiumdash.appspot.com/commit/62e1652b2bf044ec53a2ec212cf53b1a62bc357e is released on npm. Bug: 441265851 --- package-lock.json | 8 +-- package.json | 3 +- scripts/post-build.ts | 50 +++++++++++++++---- src/devtools.d.ts | 4 ++ src/trace-processing/parse.ts | 42 +++------------- tests/trace-processing/parse.test.js.snapshot | 43 ++++++++++++++++ tests/trace-processing/parse.test.ts | 27 +++++----- tsconfig.json | 11 ++++ 8 files changed, 126 insertions(+), 62 deletions(-) create mode 100644 tests/trace-processing/parse.test.js.snapshot diff --git a/package-lock.json b/package-lock.json index d9d8c961..cf2e088c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "@types/yargs": "^17.0.33", "@typescript-eslint/eslint-plugin": "^8.43.0", "@typescript-eslint/parser": "^8.43.0", - "chrome-devtools-frontend": "1.0.1514545", + "chrome-devtools-frontend": "1.0.1515446", "eslint": "^9.35.0", "globals": "^16.4.0", "prettier": "^3.6.2", @@ -1186,9 +1186,9 @@ } }, "node_modules/chrome-devtools-frontend": { - "version": "1.0.1514545", - "resolved": "https://registry.npmjs.org/chrome-devtools-frontend/-/chrome-devtools-frontend-1.0.1514545.tgz", - "integrity": "sha512-w6LytCch0+dXpAAOpZQLrPV8+K51BIs3igllYWyB90T7FhZCuEkJRXPbVVkgxrexJ4LHUvh9MEc/zqYbT+UaeQ==", + "version": "1.0.1515446", + "resolved": "https://registry.npmjs.org/chrome-devtools-frontend/-/chrome-devtools-frontend-1.0.1515446.tgz", + "integrity": "sha512-Y1GUTZARj0Jhe0h99C4WdhxmCiI1PNC8e6hqb1bOS9DBosXQ3ju8FiTVFxKZvTlsWDWU1EXPun/Yj2tnwrMQgA==", "dev": true, "license": "BSD-3-Clause" }, diff --git a/package.json b/package.json index a25da39f..08cb48f3 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "test": "npm run build && node --test-reporter spec --test-force-exit --test 'build/tests/**/*.test.js'", "test:only": "npm run build && node --test-reporter spec --test-force-exit --test --test-only 'build/tests/**/*.test.js'", "test:only:no-build": "node --test-reporter spec --test-force-exit --test --test-only 'build/tests/**/*.test.js'", + "test:update-snapshots": "npm run build && node --test-force-exit --test --test-update-snapshots 'build/tests/**/*.test.js'", "prepare": "node --experimental-strip-types scripts/prepare.ts" }, "files": [ @@ -47,7 +48,7 @@ "@types/yargs": "^17.0.33", "@typescript-eslint/eslint-plugin": "^8.43.0", "@typescript-eslint/parser": "^8.43.0", - "chrome-devtools-frontend": "1.0.1514545", + "chrome-devtools-frontend": "1.0.1515446", "eslint": "^9.35.0", "globals": "^16.4.0", "prettier": "^3.6.2", diff --git a/scripts/post-build.ts b/scripts/post-build.ts index 041cbce7..21a15a07 100644 --- a/scripts/post-build.ts +++ b/scripts/post-build.ts @@ -68,15 +68,42 @@ function main(): void { const i18nDir = path.join(BUILD_DIR, devtoolsFrontEndCorePath, 'i18n'); fs.mkdirSync(i18nDir, {recursive: true}); const i18nFile = path.join(i18nDir, 'i18n.js'); - const i18nContent = `export const i18n = { - registerUIStrings: () => {}, - getLocalizedString: (_, str) => { - // So that the string passed in gets output verbatim. - return str; - }, - lockedLazyString: () => {}, - getLazilyComputedLocalizedString: () => {} - };`; + const i18nContent = ` +export const i18n = { + registerUIStrings: () => {}, + getLocalizedString: (_, str) => { + // So that the string passed in gets output verbatim. + return str; + }, + lockedLazyString: () => {}, + getLazilyComputedLocalizedString: () => {}, +}; + +// TODO(jacktfranklin): once the DocumentLatency insight does not depend on +// this method, we can remove this stub. +export const TimeUtilities = { + millisToString(x) { + const separator = '\xA0'; + const formatter = new Intl.NumberFormat('en-US', { + style: 'unit', + unitDisplay: 'narrow', + minimumFractionDigits: 0, + maximumFractionDigits: 1, + unit: 'millisecond', + }); + + const parts = formatter.formatToParts(x); + for (const part of parts) { + if (part.type === 'literal') { + if (part.value === ' ') { + part.value = separator; + } + } + } + + return parts.map(part => part.value).join(''); + } +};`; writeFile(i18nFile, i18nContent); // Create codemirror.next mock. @@ -94,7 +121,10 @@ function main(): void { const rootDir = path.join(BUILD_DIR, devtoolsFrontEndCorePath, 'root'); fs.mkdirSync(rootDir, {recursive: true}); const runtimeFile = path.join(rootDir, 'Runtime.js'); - const runtimeContent = 'export default {};'; + const runtimeContent = ` +export function getChromeVersion() { return ''; }; +export const hostConfig = {}; + `; writeFile(runtimeFile, runtimeContent); // Update protocol_client to remove: diff --git a/src/devtools.d.ts b/src/devtools.d.ts index 6e0345e4..fbd64030 100644 --- a/src/devtools.d.ts +++ b/src/devtools.d.ts @@ -5,3 +5,7 @@ */ type CSSInJS = string & {_tag: 'CSS-in-JS'}; +declare module '*.css.js' { + const styles: CSSInJS; + export default styles; +} diff --git a/src/trace-processing/parse.ts b/src/trace-processing/parse.ts index e34f51e7..c6a018a7 100644 --- a/src/trace-processing/parse.ts +++ b/src/trace-processing/parse.ts @@ -4,9 +4,10 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {PerformanceInsightFormatter} from '../../node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.js'; +import {PerformanceTraceFormatter} from '../../node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.js'; import * as TraceEngine from '../../node_modules/chrome-devtools-frontend/front_end/models/trace/trace.js'; import {logger} from '../logger.js'; +import {AgentFocus} from '../../node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance/AIContext.js'; const engine = TraceEngine.TraceModel.Model.createWithAllHandlers(); @@ -62,38 +63,9 @@ export async function parseRawTraceBuffer( // TODO(jactkfranklin): move the formatters from DevTools to use here. // This is a very temporary helper to output some text from the tool call to aid development. export function insightOutput(result: TraceResult): string { - const mainNavigationId = - result.parsedTrace.data.Meta.mainFrameNavigations.at(0)?.args.data - ?.navigationId; - if (!mainNavigationId) { - return ''; - } - - let text = ''; - const insightsForNav = result.insights.get(mainNavigationId); - if (!insightsForNav) { - text += 'No Performance insights were found for this trace.'; - return text; - } - - const failingInsightKeys = Object.keys(insightsForNav.model).filter( - insightKey => { - const key = insightKey as keyof TraceEngine.Insights.Types.InsightModels; - const data = insightsForNav.model[key] ?? null; - return data?.state === 'fail'; - }, - ) as Array; - logger(`Found failing Insight keys: ${failingInsightKeys.join(', ')}`); - - for (const failingKey of failingInsightKeys) { - const modelData = insightsForNav.model[failingKey]; - const formatter = new PerformanceInsightFormatter( - result.parsedTrace, - modelData, - ); - - const output = formatter.formatInsight(); - text += `${output}\n`; - } - return text; + const focus = AgentFocus.full(result.parsedTrace); + const serializer = new TraceEngine.EventsSerializer.EventsSerializer(); + const formatter = new PerformanceTraceFormatter(focus, serializer); + const output = formatter.formatTraceSummary(); + return output; } diff --git a/tests/trace-processing/parse.test.js.snapshot b/tests/trace-processing/parse.test.js.snapshot new file mode 100644 index 00000000..2e10c664 --- /dev/null +++ b/tests/trace-processing/parse.test.js.snapshot @@ -0,0 +1,43 @@ +exports[`Trace parsing > can format results of a trace 1`] = ` +URL: https://web.dev/ +Bounds: {min: 122410994891, max: 122416385853} +CPU throttling: none +Network throttling: none +Metrics (lab / observed): + - LCP: 129 ms, event: (eventKey: r-6063, ts: 122411126100) + - LCP breakdown: + - TTFB: 7.9 ms, bounds: {min: 122410996889, max: 122411004828} + - Load delay: 33.2 ms, bounds: {min: 122411004828, max: 122411037986} + - Load duration: 14.7 ms, bounds: {min: 122411037986, max: 122411052690} + - Render delay: 73.4 ms, bounds: {min: 122411052690, max: 122411126100} + - CLS: 0.00 +Available insights: + - insight name: LCPBreakdown + 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. + relevant trace bounds: {min: 122410996889, max: 122411126100} + example question: Help me optimize my LCP score + example question: Which LCP phase was most problematic? + example question: What can I do to reduce the LCP time for this page load? + - insight name: LCPDiscovery + 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) + relevant trace bounds: {min: 122411004828, max: 122411055039} + example question: Suggest fixes to reduce my LCP + example question: What can I do to reduce my LCP discovery time? + example question: Why is LCP discovery time important? + - insight name: RenderBlocking + 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. + relevant trace bounds: {min: 122411037528, max: 122411053852} + example question: Show me the most impactful render blocking requests that I should focus on + example question: How can I reduce the number of render blocking requests? + - insight name: DocumentLatency + description: Your first network request is the most important. Reduce its latency by avoiding redirects, ensuring a fast server response, and enabling text compression. + relevant trace bounds: {min: 122410998910, max: 122411043781} + estimated metric savings: FCP 0 ms, LCP 0 ms + estimated wasted bytes: 77.1 kB + example question: How do I decrease the initial loading time of my page? + example question: Did anything slow down the request for this document? + - insight name: ThirdParties + 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. + relevant trace bounds: {min: 122411037881, max: 122416229595} + example question: Which third parties are having the largest impact on my page performance? +`; diff --git a/tests/trace-processing/parse.test.ts b/tests/trace-processing/parse.test.ts index 057e67d7..30ec41c1 100644 --- a/tests/trace-processing/parse.test.ts +++ b/tests/trace-processing/parse.test.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ import {describe, it} from 'node:test'; -import assert from 'assert'; +import assert from 'node:assert'; import { insightOutput, parseRawTraceBuffer, @@ -12,6 +12,18 @@ import { import {loadTraceAsBuffer} from './fixtures/load.js'; describe('Trace parsing', async () => { + it.snapshot.setResolveSnapshotPath(testPath => { + // By default the snapshots go into the build directory, but we want them + // in the tests/ directory. + const correctPath = testPath?.replace('/build/tests', '/tests'); + return correctPath + '.snapshot'; + }); + + // The default serializer is JSON.stringify which outputs a very hard to read + // snapshot. So we override it to one that shows new lines literally rather + // than via `\n`. + it.snapshot.setDefaultSnapshotSerializers([String]); + it('can parse a Uint8Array from Tracing.stop())', async () => { const rawData = loadTraceAsBuffer('basic-trace.json.gz'); const result = await parseRawTraceBuffer(rawData); @@ -19,22 +31,13 @@ describe('Trace parsing', async () => { assert.ok(result?.insights); }); - it('can format results of a trace', async () => { + it('can format results of a trace', async t => { const rawData = loadTraceAsBuffer('web-dev-with-commit.json.gz'); const result = await parseRawTraceBuffer(rawData); assert.ok(result?.parsedTrace); assert.ok(result?.insights); const output = insightOutput(result); - assert.strictEqual( - output.includes( - 'The Largest Contentful Paint (LCP) time for this navigation was 129.2 ms.', - ), - true, - ); - assert.strictEqual( - output.includes('- fetchpriority=high should be applied: FAILED'), - true, - ); + t.assert.snapshot(output); }); }); diff --git a/tsconfig.json b/tsconfig.json index de537118..653eac15 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -24,9 +24,20 @@ "node_modules/chrome-devtools-frontend/front_end/models/logs", "node_modules/chrome-devtools-frontend/front_end/models/text_utils", "node_modules/chrome-devtools-frontend/front_end/models/network_time_calculator", + "node_modules/chrome-devtools-frontend/front_end/models/crux-manager", "node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceInsightFormatter.ts", + "node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/PerformanceTraceFormatter.ts", "node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/NetworkRequestFormatter.ts", "node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/data_formatters/UnitFormatters.ts", + "node_modules/chrome-devtools-frontend/front_end/models/ai_assistance/performance", + "node_modules/chrome-devtools-frontend/front_end/models/trace_source_maps_resolver", + "node_modules/chrome-devtools-frontend/front_end/models/emulation", + "node_modules/chrome-devtools-frontend/front_end/models/stack_trace", + "node_modules/chrome-devtools-frontend/front_end/models/bindings", + "node_modules/chrome-devtools-frontend/front_end/models/formatter", + "node_modules/chrome-devtools-frontend/front_end/models/geometry", + "node_modules/chrome-devtools-frontend/front_end/models/source_map_scopes", + "node_modules/chrome-devtools-frontend/front_end/models/workspace", "node_modules/chrome-devtools-frontend/front_end/core/common", "node_modules/chrome-devtools-frontend/front_end/core/sdk", "node_modules/chrome-devtools-frontend/front_end/core/protocol_client", From 7a0a5758de7192be6469c05a00d4260c38cc1141 Mon Sep 17 00:00:00 2001 From: Jack Franklin Date: Tue, 16 Sep 2025 18:03:48 +0100 Subject: [PATCH 2/2] roll latest devtools frontend --- package-lock.json | 8 ++++---- package.json | 2 +- tests/trace-processing/parse.test.js.snapshot | 1 + 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index cf2e088c..8704e6fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "@types/yargs": "^17.0.33", "@typescript-eslint/eslint-plugin": "^8.43.0", "@typescript-eslint/parser": "^8.43.0", - "chrome-devtools-frontend": "1.0.1515446", + "chrome-devtools-frontend": "1.0.1515796", "eslint": "^9.35.0", "globals": "^16.4.0", "prettier": "^3.6.2", @@ -1186,9 +1186,9 @@ } }, "node_modules/chrome-devtools-frontend": { - "version": "1.0.1515446", - "resolved": "https://registry.npmjs.org/chrome-devtools-frontend/-/chrome-devtools-frontend-1.0.1515446.tgz", - "integrity": "sha512-Y1GUTZARj0Jhe0h99C4WdhxmCiI1PNC8e6hqb1bOS9DBosXQ3ju8FiTVFxKZvTlsWDWU1EXPun/Yj2tnwrMQgA==", + "version": "1.0.1515796", + "resolved": "https://registry.npmjs.org/chrome-devtools-frontend/-/chrome-devtools-frontend-1.0.1515796.tgz", + "integrity": "sha512-u5hyaSwxkioXHOSZSbk4oLROkP12sWw+6XJ5xw2qRfPvgjnosp6il1RhkpAITcfRvZrM6f8gBREcQ4b1iCUhkA==", "dev": true, "license": "BSD-3-Clause" }, diff --git a/package.json b/package.json index 08cb48f3..39943c38 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "@types/yargs": "^17.0.33", "@typescript-eslint/eslint-plugin": "^8.43.0", "@typescript-eslint/parser": "^8.43.0", - "chrome-devtools-frontend": "1.0.1515446", + "chrome-devtools-frontend": "1.0.1515796", "eslint": "^9.35.0", "globals": "^16.4.0", "prettier": "^3.6.2", diff --git a/tests/trace-processing/parse.test.js.snapshot b/tests/trace-processing/parse.test.js.snapshot index 2e10c664..2f09ca53 100644 --- a/tests/trace-processing/parse.test.js.snapshot +++ b/tests/trace-processing/parse.test.js.snapshot @@ -11,6 +11,7 @@ Metrics (lab / observed): - Load duration: 14.7 ms, bounds: {min: 122411037986, max: 122411052690} - Render delay: 73.4 ms, bounds: {min: 122411052690, max: 122411126100} - CLS: 0.00 +Metrics (field / real users): n/a – no data for this page in CrUX Available insights: - insight name: LCPBreakdown 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.