Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 42 additions & 3 deletions cli/run.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import UrlUtils from '../core/lib/url-utils.js';

const _RUNTIME_ERROR_CODE = 1;
const _PROTOCOL_TIMEOUT_EXIT_CODE = 67;
const _SHM_SIZE_THRESHOLD_BYTES = 64 * 1024 * 1024;

/**
* exported for testing
Expand Down Expand Up @@ -64,13 +65,39 @@ function parseChromeFlags(flags = '') {
});
}

/**
* Logs a warning on Linux when `/dev/shm` is small (e.g. default 64MB in Docker/CI containers)
* and `--disable-dev-shm-usage` was not passed, as Chrome may crash or hang (`PROTOCOL_TIMEOUT`).
* @param {Array<string>} chromeFlags
* @param {NodeJS.Platform} [platform]
*/
function checkDevShmUsage(chromeFlags, platform = process.platform) {
if (platform !== 'linux') return;
if (chromeFlags.includes('--disable-dev-shm-usage')) return;

try {
const {bsize, blocks} = fs.statfsSync('/dev/shm');
const totalBytes = bsize * blocks;
if (totalBytes > 0 && totalBytes <= _SHM_SIZE_THRESHOLD_BYTES) {
const sizeMb = Math.round(totalBytes / (1024 * 1024));
log.warn(
'CLI',
`Detected small /dev/shm (${sizeMb}MB). Pass --chrome-flags="--disable-dev-shm-usage" ` +
'or increase shared memory size to avoid Chrome crashes and PROTOCOL_TIMEOUT errors.'
);
}
} catch {
// Ignore if /dev/shm does not exist or cannot be inspected.
}
}

/**
* Attempts to connect to an instance of Chrome with an open remote-debugging
* port. If none is found, launches a debuggable instance.
* @param {LH.CliFlags} flags
* @return {Promise<ChromeLauncher.LaunchedChrome>}
*/
function getDebuggableChrome(flags) {
async function getDebuggableChrome(flags) {
if (process.platform === 'darwin' && process.arch === 'x64') {
const cpus = os.cpus();
if (cpus[0].model.includes('Apple')) {
Expand All @@ -83,12 +110,23 @@ function getDebuggableChrome(flags) {
}
}

return ChromeLauncher.launch({
const chromeFlags = parseChromeFlags(flags.chromeFlags);
if (!flags.port) {
checkDevShmUsage(chromeFlags);
}

const launchedChrome = await ChromeLauncher.launch({
port: flags.port,
ignoreDefaultFlags: flags.chromeIgnoreDefaultFlags,
chromeFlags: parseChromeFlags(flags.chromeFlags),
chromeFlags,
logLevel: flags.logLevel,
});

if (flags.port && launchedChrome.pid) {
checkDevShmUsage(chromeFlags);
}

return launchedChrome;
}

/** @return {never} */
Expand Down Expand Up @@ -288,6 +326,7 @@ async function runLighthouse(urlOrUrls, flags, config) {
}

export {
checkDevShmUsage,
parseChromeFlags,
saveResults,
runLighthouse,
Expand Down
66 changes: 65 additions & 1 deletion cli/test/cli/run-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ import fs from 'fs';

import * as td from 'testdouble';
import * as jestMock from 'jest-mock';
import log from 'lighthouse-logger';

import * as run from '../../run.js';
import {parseChromeFlags} from '../../run.js';
import {checkDevShmUsage, parseChromeFlags} from '../../run.js';
import {getFlags} from '../../cli-flags.js';
import {LH_ROOT} from '../../../shared/root.js';
import {fnAny, readJson} from '../../../core/test/test-utils.js';
Expand Down Expand Up @@ -387,3 +388,66 @@ describe('Parsing --chrome-flags', () => {
);
});
});

describe('checkDevShmUsage', () => {
/** @type {ReturnType<typeof jestMock.spyOn>} */
let statfsSpy;
/** @type {ReturnType<typeof jestMock.spyOn>} */
let logWarnSpy;

beforeEach(() => {
statfsSpy = jestMock.spyOn(fs, 'statfsSync');
logWarnSpy = jestMock.spyOn(log, 'warn');
});

afterEach(() => {
statfsSpy.mockRestore();
logWarnSpy.mockRestore();
});

it('warns on Linux when /dev/shm is <= 64MB and --disable-dev-shm-usage is missing', () => {
statfsSpy.mockReturnValue({bsize: 4096, blocks: 16384}); // 64MB

checkDevShmUsage(['--headless'], 'linux');

expect(statfsSpy).toHaveBeenCalledWith('/dev/shm');
expect(logWarnSpy).toHaveBeenCalledTimes(1);
expect(logWarnSpy).toHaveBeenCalledWith(
'CLI',
expect.stringContaining('Detected small /dev/shm (64MB)')
);
});

it('does not warn when /dev/shm is larger than 64MB', () => {
statfsSpy.mockReturnValue({bsize: 4096, blocks: 524288}); // 2GB

checkDevShmUsage(['--headless'], 'linux');

expect(logWarnSpy).not.toHaveBeenCalled();
});

it('does not warn when --disable-dev-shm-usage is passed', () => {
checkDevShmUsage(['--headless', '--disable-dev-shm-usage'], 'linux');

expect(statfsSpy).not.toHaveBeenCalled();
expect(logWarnSpy).not.toHaveBeenCalled();
});

it('does not warn on non-Linux platforms', () => {
checkDevShmUsage(['--headless'], 'darwin');
checkDevShmUsage(['--headless'], 'win32');

expect(statfsSpy).not.toHaveBeenCalled();
expect(logWarnSpy).not.toHaveBeenCalled();
});

it('ignores errors when /dev/shm cannot be inspected', () => {
statfsSpy.mockImplementation(() => {
throw new Error('ENOENT');
});

checkDevShmUsage(['--headless'], 'linux');

expect(logWarnSpy).not.toHaveBeenCalled();
});
});
16 changes: 16 additions & 0 deletions docs/headless-chrome.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,22 @@ lighthouse --chrome-flags="--headless" https://github.com
There is also the new `--headless=new` option, which includes functionality that
was explicitly omitted from the original headless browser.

## Running in Docker / CI containers

Containerized Linux environments (such as Docker, Kubernetes, and many CI runners) default `/dev/shm` shared memory to `64MB`. On modern pages, Chrome can exhaust this shared memory mid-audit, causing the renderer tab to crash (`TARGET_CRASHED`) or stop responding to DevTools protocol commands (`PROTOCOL_TIMEOUT`, see [#6512](https://github.com/GoogleChrome/lighthouse/issues/6512)).

To prevent shared-memory crashes in containers, either pass `--disable-dev-shm-usage` so Chrome writes shared memory files to `/tmp` instead of `/dev/shm`:

```sh
lighthouse --chrome-flags="--headless --no-sandbox --disable-dev-shm-usage" https://github.com
```

Or mount a larger `/dev/shm` volume when starting the container (recommended if `/tmp` is backed by slow disk I/O):

```sh
docker run --shm-size=1g ...
```

## CLI (xvfb)

Alternatively, you can run full Chrome + xvfb instead of headless mode. These steps worked on Debian Jessie:
Expand Down
2 changes: 2 additions & 0 deletions docs/puppeteer.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,5 @@ console.log(`Lighthouse scores: ${Object.values(lhr.categories).map(c => c.score
await browser.disconnect();
chrome.kill();
```

> **Note:** Avoid running concurrent Puppeteer/Playwright automation commands or leaving active CDP listeners manipulating the audited tab while Lighthouse is gathering (especially when sharing a browser via `--port`). Concurrent CDP control over the same target can cause `PROTOCOL_TIMEOUT` errors.
2 changes: 1 addition & 1 deletion docs/running-at-scale.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ The [Lighthouse CLI](https://github.com/GoogleChrome/lighthouse#using-the-node-c

Many teams have wrapped around the Lighthouse CLI with bash, python, or node scripts. The npm modules [multihouse](https://github.com/samdutton/multihouse) and [lighthouse-batch](https://www.npmjs.com/package/lighthouse-batch) both leverage this pattern.

You'll be running Lighthouse CLI on your own machines, and we have guidance on the [specs of machines suitable](./variability.md#run-on-adequate-hardware) for running Lighthouse without skewing performance results. The environment must also be able to run either headful Chrome or headless Chrome.
You'll be running Lighthouse CLI on your own machines, and we have guidance on the [specs of machines suitable](./variability.md#run-on-adequate-hardware) for running Lighthouse without skewing performance results. The environment must also be able to run either headful Chrome or headless Chrome. If running inside Linux containers (Docker, Kubernetes, etc.) where `/dev/shm` defaults to `64MB`, increase shared memory (e.g. `docker run --shm-size=1g`) or pass `--chrome-flags="--disable-dev-shm-usage"` to avoid Chrome tab crashes and `PROTOCOL_TIMEOUT` errors.

* PRO: Ultimate configurability
* CON: Must create and maintain testing environment
Expand Down
2 changes: 1 addition & 1 deletion docs/variability.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ Loading modern webpages on a modern browser is not an easy task. Using appropria

- Minimum 2 dedicated cores (4 recommended)
- Minimum 2GB RAM (4-8GB recommended)
- Avoid non-standard Chromium flags (`--single-process` is not supported, `--no-sandbox` and `--headless` should be OK, though educate yourself about [sandbox tradeoffs](https://github.com/GoogleChrome/lighthouse-ci/tree/fbb540507c031100ee13bf7eb1a4b61c79c5e1e6/docs/recipes/docker-client#--no-sandbox-issues-explained))
- Avoid non-standard Chromium flags (`--single-process` is not supported, `--no-sandbox` and `--headless` should be OK, though educate yourself about [sandbox tradeoffs](https://github.com/GoogleChrome/lighthouse-ci/tree/fbb540507c031100ee13bf7eb1a4b61c79c5e1e6/docs/recipes/docker-client#--no-sandbox-issues-explained); in Docker/Kubernetes containers with a `64MB` `/dev/shm`, increase `/dev/shm` size or pass `--disable-dev-shm-usage` to avoid crashes and `PROTOCOL_TIMEOUT` errors)
- Avoid function-as-a-service infrastructure (Lambda, GCF, etc)
- Avoid "burstable" or "shared-core" instance types (AWS `t` instances, GCP shared-core N1 and E2 instances, etc)

Expand Down
Loading