From 10258b998b076947db2f33e9971c8409d556d9cb Mon Sep 17 00:00:00 2001 From: Peter Hedenskog Date: Mon, 26 Oct 2026 09:01:56 +0100 Subject: [PATCH] Remove Chrome's temporary files after each run Each Chrome or Edge run left about 2.5 MB in the system temp folder: the temporary browser profile, plus small folders that Chrome never removes. Chromedriver deletes the profile a few seconds after the browser closes, but Selenium stops Chromedriver before that happens. When you run many tests on the same machine or in the same container, the disk fills up. One user ran out of disk after a month. Chromedriver and the browser now get their own temp folder for each run, and Browsertime deletes it when the browser stops. Fixes sitespeedio/sitespeed.io#4567 Assisted-by: Claude Opus 5.5 noreply@anthropic.com --- lib/chrome/webdriver/builder.js | 10 +++++++++- lib/core/seleniumRunner.js | 22 ++++++++++++++++++++-- lib/core/webdriver/index.js | 6 +++--- lib/edge/webdriver/builder.js | 10 +++++++++- 4 files changed, 41 insertions(+), 7 deletions(-) diff --git a/lib/chrome/webdriver/builder.js b/lib/chrome/webdriver/builder.js index 655440cfe0..36d4f4a699 100644 --- a/lib/chrome/webdriver/builder.js +++ b/lib/chrome/webdriver/builder.js @@ -9,7 +9,7 @@ import { pick, isEmpty, getProperty } from '../../support/util.js'; * @param builder * @param {Object} options the options for a web driver. */ -export async function configureBuilder(builder, baseDir, options) { +export async function configureBuilder(builder, baseDir, options, tmpDir) { const chromeConfig = options.chrome || {}; let chromedriverPath = getProperty(chromeConfig, 'chromedriverPath'); @@ -35,6 +35,14 @@ export async function configureBuilder(builder, baseDir, options) { if (options.verbose >= 3 || chromeConfig.enableVerboseChromeDriverLog) serviceBuilder.enableVerboseLogging(); } + if (tmpDir) { + serviceBuilder.setEnvironment({ + ...process.env, + TMPDIR: tmpDir, + TMP: tmpDir, + TEMP: tmpDir + }); + } builder.setChromeService(serviceBuilder); const proxyPacSettings = pick(options.proxy, ['pac']); diff --git a/lib/core/seleniumRunner.js b/lib/core/seleniumRunner.js index 0b6d00fe6b..b2bb106d50 100644 --- a/lib/core/seleniumRunner.js +++ b/lib/core/seleniumRunner.js @@ -1,3 +1,6 @@ +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; import { getLogger } from '@sitespeed.io/log'; import { Condition, error as webdriverError } from 'selenium-webdriver'; import { isAndroidConfigured } from '../android/index.js'; @@ -77,11 +80,18 @@ export class SeleniumRunner { * @throws {BrowserError} if the browser can't start */ async start() { + // Chromedriver deletes its temporary profile a few seconds after the + // browser closes, but Selenium stops Chromedriver before that. Chrome + // also leaves files in the temp folder. Give them their own folder and + // remove it in stop(). + if (this.options.browser === 'chrome' || this.options.browser === 'edge') { + this.tmpDir = await mkdtemp(path.join(tmpdir(), 'browsertime-')); + } const tries = this.browserRestartTries; for (let index = 0; index < tries; ++index) { try { this.driver = await timeout( - createWebDriver(this.baseDir, this.options), + createWebDriver(this.baseDir, this.options, this.tmpDir), this.options.timeouts.browserStart, `Failed to start ${this.options.browser} in ${ this.options.timeouts.browserStart / 1000 @@ -97,6 +107,7 @@ export class SeleniumRunner { } } if (!this.driver) { + await this.removeTmpDir(); throw new BrowserError( `Could not start ${this.options.browser} with ${tries} tries` ); @@ -527,7 +538,7 @@ export class SeleniumRunner { this.driver.quit(), 120_000, 'Could not close the browser using driver.quit' - ); + ).finally(() => this.removeTmpDir()); } catch (error) { throw new BrowserError(error.message, { cause: error @@ -536,6 +547,13 @@ export class SeleniumRunner { } } + async removeTmpDir() { + if (this.tmpDir) { + await rm(this.tmpDir, { recursive: true, force: true, maxRetries: 3 }); + this.tmpDir = undefined; + } + } + /** * * Scripts should be valid statements or IIFEs '(function() {...})()' that can run diff --git a/lib/core/webdriver/index.js b/lib/core/webdriver/index.js index a0c026df9f..13838f8423 100644 --- a/lib/core/webdriver/index.js +++ b/lib/core/webdriver/index.js @@ -11,7 +11,7 @@ import { isEmpty } from '../../support/util.js'; * @returns {!Promise} a promise that resolves to the webdriver, * or rejects if the current configuration is invalid. */ -export async function createWebDriver(baseDir, options) { +export async function createWebDriver(baseDir, options, tmpDir) { const browser = options.browser || 'chrome'; const seleniumUrl = options.selenium ? options.selenium.url : undefined; const capabilities = options.selenium @@ -31,7 +31,7 @@ export async function createWebDriver(baseDir, options) { switch (browser) { case 'chrome': { - await configureBuilderChrome(builder, baseDir, options); + await configureBuilderChrome(builder, baseDir, options, tmpDir); break; } @@ -46,7 +46,7 @@ export async function createWebDriver(baseDir, options) { } case 'edge': { - await configureBuilderEdge(builder, baseDir, options); + await configureBuilderEdge(builder, baseDir, options, tmpDir); break; } diff --git a/lib/edge/webdriver/builder.js b/lib/edge/webdriver/builder.js index e996039158..f0920a487c 100644 --- a/lib/edge/webdriver/builder.js +++ b/lib/edge/webdriver/builder.js @@ -6,7 +6,7 @@ import { pac, manual } from 'selenium-webdriver/proxy.js'; import { pick, isEmpty, getProperty } from '../../support/util.js'; const log = getLogger('browsertime.edge'); -export async function configureBuilder(builder, baseDir, options) { +export async function configureBuilder(builder, baseDir, options, tmpDir) { const edgeConfig = options.edge || {}; const chromeConfig = options.chrome || {}; @@ -37,6 +37,14 @@ export async function configureBuilder(builder, baseDir, options) { if (options.verbose >= 3 || chromeConfig.enableVerboseChromeDriverLog) serviceBuilder.enableVerboseLogging(); } + if (tmpDir) { + serviceBuilder.setEnvironment({ + ...process.env, + TMPDIR: tmpDir, + TMP: tmpDir, + TEMP: tmpDir + }); + } builder.setEdgeService(serviceBuilder); log.info(