browse: headless GPU process pegs ~800% CPU on macOS 26 / M5; --disable-gpu alone insufficient
- Dominant language
- TypeScript
- Stars
- 133k
- Forks
- 19.9k
- Avg merge
- 18h 46m
- Merged PRs (30d)
- 26
Description
## Summary
On macOS 26 / Apple M5, the headless Chromium **GPU process** spawned by `browse` pegs ~8 of 10 cores (740–830% CPU) and never settles. It survives `browse stop` as an orphan and has to be killed by hand. Reproduced 4/4 over two days, including across a reboot.
`--disable-gpu` alone does **not** stop it. A four-flag set does.
Related but distinct: #294 (orphaned **renderer** processes, ~120% each, accumulating on CDP disconnect). Here it is a single **gpu-process** at ~800%, and the orphan appears even without a CDP disconnect loop.
## Environment
```
macOS 26.5.1 (25F80)
CPU Apple M5, 10 cores
Chromium 151.0.7922.34 (chromium_headless_shell-1234)
gstack 1.68.3.0
```
## Observed
```
$ ps aux | grep chrome-headless-shell
PID 53536 808% --type=gpu-process
PID 53535 0% (main)
...all other children 0%
```
- Tab list at the time: `about:blank` (the browser had done real work earlier — navigations + screenshots — then gone idle).
- `browse stop` reports the daemon stopped, but the process keeps running at ~800% until `pkill`.
- Elapsed times observed: 3 min, 5 min, 32 min — it does not settle on its own.
## What I ruled out
Both of these were launched **directly** (no Playwright, no gstack), `about:blank`, measured after 20 s:
| Binary | GPU process CPU |
|---|---|
| `chromium_headless_shell-1223` (Chrome 148) | 0.0% |
| `chromium_headless_shell-1234` (Chrome 151) | 0.0% |
| `chromium_headless_shell-1234` + `--enable-features=CDPScreenshotNewSurface` | 0.0% |
So it is **not** the Chromium version, and **not** idle launch. It only appears through the `browse` path after real page work.
I did **not** isolate whether the trigger is Playwright's launch/CDP usage or the rendering/screenshot workload itself — reproducing that means letting it spin again, which I stopped doing once a workaround held.
Note `STEALTH_LAUNCH_ARGS` is only `--disable-blink-features=AutomationControlled`, so gstack's own args look unrelated to GPU.
## Workaround that holds
Adding these to the launch args in `browse/src/stealth.ts` → `buildGStackLaunchArgs()` drops it to 0.0% while screenshots keep working (verified against a real page):
```ts
const plat = (globalThis as any).process?.platform;
if (plat === 'darwin' && env.GSTACK_DISABLE_GPU !== 'off') {
args.push(
'--disable-gpu',
'--disable-software-rasterizer',
'--disable-gpu-compositing',
'--disable-gpu-watchdog',
);
}
```
`--disable-gpu` **on its own is not enough** — it disables hardware GPU but the software-compositing GPU process still spawns and still spins (confirmed: the flag was present on the 808% run).
## Suggestion
Either ship the flag set on darwin by default for headless, or expose an escape hatch for extra launch args — `buildGStackLaunchArgs()` currently only reads specific `GSTACK_*` vars, so there is no way to add a flag without editing gstack source (which an upgrade then overwrites).
Contributor guide
Research direction
Start in browse/src/stealth.ts at buildGStackLaunchArgs() and reproduce the macOS headless browse flow with real navigation and screenshots. Compare the documented four-flag workaround with the current launch arguments, then verify that the GPU process stays near 0% and browse stop does not leave it orphaned while screenshots continue working.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- cli, devtools
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 72/100