Skip to content

Commit dd96ef2

Browse files
jackfranklinDevtools-frontend LUCI CQ
authored andcommitted
AutoRun: cache downloaded trace files
This updates the TraceDownloader to only download a trace file if the one on disk is older than 1 hour. This means if you run the tool with `--times=10` you only download the file at most 1 time, or none if you ran it recently. I also took the opportunity to tidy up and move some JSDoc types into TS, and extract TraceDownloader into its own file. R=ergunsh@chromium.org Bug: none Change-Id: I604250cebcd18ce151978ff80653b706b2840d40 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6520601 Auto-Submit: Jack Franklin <jacktfranklin@chromium.org> Commit-Queue: Jack Franklin <jacktfranklin@chromium.org> Commit-Queue: Ergün Erdoğmuş <ergunsh@chromium.org> Reviewed-by: Ergün Erdoğmuş <ergunsh@chromium.org>
1 parent 7607c60 commit dd96ef2

File tree

3 files changed

+127
-113
lines changed

3 files changed

+127
-113
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ npm-debug.log
2828
/scripts/ai_assistance/data
2929
/scripts/ai_assistance/auto-run/data
3030
/scripts/ai_assistance/performance-trace-downloads
31+
/scripts/ai_assistance/auto-run/performance-trace-downloads
3132

3233
/build
3334
/buildtools

scripts/ai_assistance/auto-run/auto-run.ts

Lines changed: 21 additions & 113 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717
ExampleMetadata, ExecutedExample, IndividualPromptRequestResponse, Logs, PatchTest, RunResult} from '../types';
1818

1919
import {parseComment, parseFollowUps} from './auto-run-helpers.ts';
20+
import {TraceDownloader} from './trace-downloader.ts';
2021

2122
// eslint-disable-next-line @typescript-eslint/naming-convention
2223
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
@@ -92,71 +93,6 @@ for (const exampleUrl of userArgs.exampleUrls) {
9293
}
9394
}
9495

95-
/**
96-
* Performance examples have a trace file so that this script does not have to
97-
* trigger a trace recording.
98-
*/
99-
class TraceDownloader {
100-
static location = path.join(__dirname, 'performance-trace-downloads');
101-
static ensureLocationExists() {
102-
try {
103-
fs.mkdirSync(TraceDownloader.location);
104-
} catch {
105-
}
106-
}
107-
108-
/**
109-
* @param {string} filename - the filename to look for
110-
* @param {number} attempts - the number of attempts we have had to find the file
111-
*/
112-
static async ensureDownloadExists(filename: string, attempts = 0) {
113-
if (attempts === 5) {
114-
return false;
115-
}
116-
117-
if (fs.existsSync(path.join(TraceDownloader.location, filename))) {
118-
return true;
119-
}
120-
121-
return await new Promise(r => {
122-
setTimeout(() => {
123-
return r(TraceDownloader.ensureDownloadExists(filename, attempts + 1));
124-
}, 200);
125-
});
126-
}
127-
/**
128-
* Downloads a trace file for a given example.
129-
* @param {Example} example - the example that is being run
130-
* @param {puppeteer.Page} page - the page instance associated with this example
131-
* @returns {Promise<string>} - the file name that was downloaded.
132-
*/
133-
static async run(example: Example, page: Page) {
134-
const url = new URL(example.url());
135-
const idForUrl = path.basename(path.dirname(url.pathname));
136-
const cdp = await page.createCDPSession();
137-
await cdp.send('Browser.setDownloadBehavior', {
138-
behavior: 'allow',
139-
downloadPath: TraceDownloader.location,
140-
});
141-
const fileName = `${idForUrl}.json.gz`;
142-
const traceUrl = example.url().replace('index.html', fileName);
143-
// Using page.goto(traceUrl) does download the file, but it also causes a
144-
// net::ERR_ABORTED error to be thrown. Doing it this way does not. See
145-
// https://github.com/puppeteer/puppeteer/issues/6728#issuecomment-986082241.
146-
await page.evaluate(traceUrl => {
147-
location.href = traceUrl;
148-
}, traceUrl);
149-
const foundFile = await TraceDownloader.ensureDownloadExists(fileName);
150-
if (!foundFile) {
151-
console.error(
152-
`Could not find '${fileName}' in download location (${TraceDownloader.location}). Aborting.`,
153-
);
154-
}
155-
example.log(`Downloaded performance trace: ${fileName}`);
156-
return fileName;
157-
}
158-
}
159-
16096
class Logger {
16197
#logs: Logs = {};
16298
#updateElapsedTimeInterval: NodeJS.Timeout|null = null;
@@ -184,12 +120,7 @@ class Logger {
184120
}
185121
}
186122

187-
/**
188-
* Formats an error object for display.
189-
* @param {Error} err The error object to format.
190-
* @return {string} The formatted error string.
191-
*/
192-
formatError(err: Error) {
123+
formatError(err: Error): string {
193124
const stack = typeof err.cause === 'object' && err.cause && 'stack' in err.cause ? err.cause.stack : '';
194125
return `${err.stack}${err.cause ? `\n${stack}` : ''}`;
195126
}
@@ -220,7 +151,7 @@ class Logger {
220151
}
221152
}
222153

223-
class Example {
154+
export class Example {
224155
#url: string;
225156
#browser: Browser;
226157
#ready = false;
@@ -260,12 +191,7 @@ class Example {
260191
await this.#waitForElementToHaveHeight(canvas, 200);
261192
}
262193

263-
/**
264-
* @param {puppeteer.ElementHandle<HTMLElement>} elem
265-
* @param {number} height
266-
* @param {number} tries
267-
*/
268-
async #waitForElementToHaveHeight(elem: ElementHandle<HTMLElement>, height: number, tries = 0) {
194+
async #waitForElementToHaveHeight(elem: ElementHandle<HTMLElement>, height: number, tries = 0): Promise<boolean> {
269195
const h = await elem.evaluate(e => e.clientHeight);
270196
if (h > height) {
271197
return true;
@@ -278,19 +204,10 @@ class Example {
278204
});
279205
}
280206

281-
/**
282-
* Parses out comments from the page.
283-
* @param {puppeteer.Page} page
284-
* @returns {Promise<string[]>} - the comments on the page.
285-
*/
286-
async #getCommentStringsFromPage(page: Page) {
287-
/** @type {Array<string>} */
207+
async #getCommentStringsFromPage(page: Page): Promise<string[]> {
288208
const commentStringsFromPage = await page.evaluate(() => {
289-
/**
290-
* @param {Node|ShadowRoot} root
291-
* @return {Array<{comment: string, commentElement: Comment, targetElement: Element|null}>}
292-
*/
293-
function collectComments(root: Node|ShadowRoot) {
209+
function collectComments(root: Node|ShadowRoot):
210+
Array<{comment: string, commentElement: Comment, targetElement: Element | null}> {
294211
const walker = document.createTreeWalker(root, NodeFilter.SHOW_COMMENT);
295212
const results = [];
296213
while (walker.nextNode()) {
@@ -325,11 +242,7 @@ class Example {
325242
return commentStringsFromPage;
326243
}
327244

328-
/**
329-
* @param {puppeteer.Page} devtoolsPage
330-
* @returns {Promise<puppeteer.ElementHandle<HTMLElement>>} - the prompt from the annotation, if it exists, or undefined if one was not found.
331-
*/
332-
async #lookForAnnotatedPerformanceEvent(devtoolsPage: Page) {
245+
async #lookForAnnotatedPerformanceEvent(devtoolsPage: Page): Promise<ElementHandle<HTMLElement>> {
333246
const elem = await devtoolsPage.$(
334247
'devtools-entry-label-overlay >>> [aria-label="Entry label"]',
335248
);
@@ -339,22 +252,22 @@ class Example {
339252
);
340253
}
341254

342-
const overlay = /** @type{puppeteer.ElementHandle<HTMLElement>} */ (elem);
343-
return overlay;
255+
return elem as ElementHandle<HTMLElement>;
344256
}
345257

346258
/**
347259
* Generates the metadata for a given example.
348260
* If we are testing performance, we look for an annotation to give us our target event that we need to select.
349261
* We then also parse the HTML to find the comments which give us our prompt
350262
* and explanation.
351-
* @param {puppeteer.Page} page - the target page
352-
* @param {puppeteer.Page} devtoolsPage - the DevTools page
353-
* @returns {Promise<{idx: number, queries: string[], explanation: string, performanceAnnotation?: puppeteer.ElementHandle<HTMLElement>, rawComment: Record<string, string>}>}
354263
*/
355-
async #generateMetadata(page: Page, devtoolsPage: Page) {
356-
/** @type {puppeteer.ElementHandle<HTMLElement>|undefined} */
357-
let annotation = undefined;
264+
async #generateMetadata(page: Page, devtoolsPage: Page): Promise<{
265+
idx: number,
266+
queries: string[],
267+
explanation: string,
268+
performanceAnnotation?: ElementHandle<HTMLElement>, rawComment: Record<string, string>,
269+
}> {
270+
let annotation: ElementHandle<HTMLElement>|undefined = undefined;
358271
if (userArgs.testTarget === 'performance-main-thread') {
359272
annotation = await this.#lookForAnnotatedPerformanceEvent(devtoolsPage);
360273
}
@@ -384,14 +297,11 @@ class Example {
384297
};
385298
}
386299

387-
/**
388-
* @return {string} the URL of the example
389-
*/
390-
url() {
300+
url(): string {
391301
return this.#url;
392302
}
393303

394-
id() {
304+
id(): string {
395305
return this.#url.split('/').pop()?.replace('.html', '') ?? 'unknown-id';
396306
}
397307

@@ -425,7 +335,7 @@ class Example {
425335
await new Promise(resolve => setTimeout(resolve, 2000));
426336
} else {
427337
if (userArgs.testTarget === 'performance-main-thread' || userArgs.testTarget === 'performance-insights') {
428-
const fileName = await TraceDownloader.run(this, page);
338+
const fileName = await TraceDownloader.download(this, page);
429339
await this.#loadPerformanceTrace(devtoolsPage, fileName);
430340
}
431341

@@ -798,10 +708,8 @@ async function main() {
798708

799709
logger.head('Preparing examples...');
800710
const examples = exampleUrls.map(exampleUrl => new Example(exampleUrl, browser));
801-
/** @type {import('./types').IndividualPromptRequestResponse[]} */
802-
let allExampleResults = [];
803-
/** @type {import('./types').ExampleMetadata[]} */
804-
let metadata = [];
711+
let allExampleResults: IndividualPromptRequestResponse[] = [];
712+
let metadata: ExampleMetadata[] = [];
805713

806714
if (userArgs.parallel) {
807715
({allExampleResults, metadata} = await runInParallel(examples));
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright 2025 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import * as fs from 'node:fs';
6+
import * as path from 'node:path';
7+
import * as url from 'node:url';
8+
import type {Page} from 'puppeteer-core';
9+
10+
import type {Example} from './auto-run';
11+
12+
// eslint-disable-next-line @typescript-eslint/naming-convention
13+
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
14+
15+
/**
16+
* Performance examples have a trace file so that this script does not have to
17+
* trigger a trace recording.
18+
*/
19+
export class TraceDownloader {
20+
static location = path.join(__dirname, 'performance-trace-downloads');
21+
22+
static ensureLocationExists() {
23+
try {
24+
fs.mkdirSync(TraceDownloader.location);
25+
} catch {
26+
}
27+
}
28+
29+
static async ensureDownloadExists(filename: string, attempts = 0): Promise<boolean> {
30+
if (attempts === 5) {
31+
return false;
32+
}
33+
34+
if (fs.existsSync(path.join(TraceDownloader.location, filename))) {
35+
return true;
36+
}
37+
38+
return await new Promise(r => {
39+
setTimeout(() => {
40+
return r(TraceDownloader.ensureDownloadExists(filename, attempts + 1));
41+
}, 200);
42+
});
43+
}
44+
45+
/**
46+
* Determines if a trace file needs to be downloaded.
47+
*
48+
* A file needs to be downloaded if:
49+
* 1. The file does not exist at the expected location.
50+
* 2. The file exists, but its last modification time was more than an hour ago.
51+
*
52+
* @param filename The name of the file to check.
53+
* @returns True if the file should be downloaded, false otherwise.
54+
*/
55+
static shouldDownloadFile(filename: string): boolean {
56+
const filePath = path.join(TraceDownloader.location, filename);
57+
58+
if (!fs.existsSync(filePath)) {
59+
return true;
60+
}
61+
62+
const stats = fs.statSync(filePath);
63+
const oneHourInMilliseconds = 60 * 60 * 1000;
64+
const fileAge = Date.now() - stats.mtimeMs;
65+
66+
if (fileAge > oneHourInMilliseconds) {
67+
return true;
68+
}
69+
70+
return false;
71+
}
72+
73+
static async download(example: Example, page: Page): Promise<string> {
74+
const url = new URL(example.url());
75+
const idForUrl = path.basename(path.dirname(url.pathname));
76+
const fileName = `${idForUrl}.json.gz`;
77+
if (!TraceDownloader.shouldDownloadFile(fileName)) {
78+
console.log(
79+
`Warning: not downloading ${fileName} because it was last downloaded <1hour ago. Delete this file from ${
80+
TraceDownloader.location} to force a re-download.`);
81+
return fileName;
82+
}
83+
84+
const cdp = await page.createCDPSession();
85+
await cdp.send('Browser.setDownloadBehavior', {
86+
behavior: 'allow',
87+
downloadPath: TraceDownloader.location,
88+
});
89+
const traceUrl = example.url().replace('index.html', fileName);
90+
// Using page.goto(traceUrl) does download the file, but it also causes a
91+
// net::ERR_ABORTED error to be thrown. Doing it this way does not. See
92+
// https://github.com/puppeteer/puppeteer/issues/6728#issuecomment-986082241.
93+
await page.evaluate(traceUrl => {
94+
location.href = traceUrl;
95+
}, traceUrl);
96+
const foundFile = await TraceDownloader.ensureDownloadExists(fileName);
97+
if (!foundFile) {
98+
console.error(
99+
`Could not find '${fileName}' in download location (${TraceDownloader.location}). Aborting.`,
100+
);
101+
}
102+
example.log(`Downloaded performance trace: ${fileName}`);
103+
return fileName;
104+
}
105+
}

0 commit comments

Comments
 (0)