garrytan / garrytan/gstack

browse: headless GPU process pegs ~800% CPU on macOS 26 / M5; --disable-gpu alone insufficient

Open
#2,709 0 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.