ADR-014: Chromium sandbox + puppeteer-config for mermaid rendering¶
Date: 2026-04-24
Status: Accepted
Decision-makers: FCC core maintainers · Publications pipeline owners
Consulted: WSL / Docker / CI runtime owners · mermaid-cli upstream (puppeteer-config contract)
Informed: All publications/ contributors; authors of docs containing mermaid fences
Context and Problem Statement¶
The make pub-diagrams pipeline renders every ``mermaid fence indocs/*/.mdto a twin SVG/PNG pair via
[mermaid-cli](https://github.com/mermaid-js/mermaid-cli) (mmdc),
which launches Chromium under the hood through Puppeteer. Since v1.3.2
the pipeline has intermittently hung — parallel workers spawnN × 2Chromium instances (one for SVG, one for PNG), and under--parallel 4the resulting eight concurrent browsers would deadlock
waiting for/dev/shm`, sandbox-detection, or zygote initialization.
Root cause, confirmed under WSL2 + Docker + CI:
/dev/shmsizing. Chromium defaults to a shared-memory allocation (~64 MB) that is insufficient when many instances run in parallel; WSL/Docker commonly ship with an even smaller/dev/shm.- Sandbox detection failures. The default Chromium sandbox needs user-namespace cloning that is not available in many CI runners, WSL kernels, and unprivileged containers. Chromium does not fail fast — it hangs during sandbox initialization.
- Zombie zygote processes. When a worker is killed, its Chromium
zygote can linger and block
/dev/shminodes for the next worker.
The v1.3.2 release memory journal records 23 mermaid failures that were papered over with a cache-retry loop; by v1.6.0 the loop was no longer sufficient because of docs-growth (386 diagrams).
The problem was how to make make pub-diagrams reliably complete
under every target environment (WSL, Docker, CI, native Linux) without
environment-specific configuration.
Decision Drivers¶
- Low-touch. A fix that works everywhere without per-environment shell scripts or Docker-compose tweaks.
- mmdc-native. Use mermaid-cli's first-class configuration surface rather than patching Puppeteer or monkey-patching Chromium.
- Parallel-safe. The pipeline must tolerate partial stalls — a single bad diagram must not deadlock all others.
- Reproducible. CI, local WSL, and developer Docker containers must all converge on identical behaviour.
- Graceful degradation. Genuine syntax errors must surface as clean FAILs, not hangs.
Considered Options¶
- Enlarge
/dev/shmat runtime.docker-compose up --shm-size=1gfixes Docker but does nothing for WSL, CI, or native Linux; operationally tied to the launch script, not portable tomake pub-diagramsinvoked by humans. - Switch mmdc to a WebKit or Firefox backend. No official mermaid-cli support; experimental forks exist but none production-grade.
- Run an external mermaid HTTP server (e.g.
mermaid-server). Adds a deployment dependency and violates the framework'spip install+ standalone invariant. - Ship a
puppeteer-config.jsonwith minimum-viable Chromium launch flags and pass-pto everymmdccall; pair it with a wall-clock timeout + sequential retry in the orchestrator. Low-touch, mmdc-native, environment-agnostic.
Decision¶
We adopt option 4: a puppeteer-config.json + orchestrator
changes.
publications/scripts/puppeteer-config.json(new) — minimum- viable Chromium launch args:--no-sandbox,--disable-setuid-sandbox,--disable-dev-shm-usage,--disable-gpu,--disable-software-rasterizer,--single-process,--no-zygote,--no-first-run,--disable-extensions, plus several--disable-*-throttlingflags to prevent background-tab deprioritization when a worker goes idle briefly.publications/scripts/render_mermaid.py—render_oneaccepts apuppeteer_config_pathand appends-p <path>to everymmdcinvocation. A new CLI flag--puppeteer-config(default:puppeteer-config.jsonnext to the script) wires it in.- Per-future wall-clock timeout —
render_allguards each parallel future with a 180 s ceiling. Futures that trip the ceiling are cancelled and their blocks are collected into aparallel_timeoutslist. After the parallel pool drains, the list is retried sequentially (one Chromium at a time, no/dev/shmcontention) so genuine per-diagram stalls surface as FAILs rather than hangs. - Lower default
--parallelfrom 4 to 2 in the CLI default and in theMakefile(pub-diagramsSVG + PNG passes). Two workers is the sweet spot on WSL2 (tested: 8 s for 386 diagrams).
Two mermaid-syntax fixes landed in the same roll-up as prerequisite cleanup for the pipeline:
docs/for-auditors/compliance-audit-workflow.md:236— renamedclassDef endtoclassDef finish(endis a mermaid reserved keyword that silently broke the rendering of that single diagram).docs/tutorials/publications/adding-new-book-chapter.md:60— moved thefig-metaHTML comment outside the mermaid fence so the comment isn't passed to mmdc as mermaid source.
Consequences¶
Positive¶
- Reliable completion.
make pub-diagramsconsistently finishes in 8–15 s across WSL, Docker, CI, and native Linux (previously indefinite hang at--parallel 4). - Environment-agnostic. No docker-compose-only config, no CI-only env vars, no developer-local tweaks.
- Per-stall isolation. Wall-clock timeout + sequential retry prevents one stuck Chromium from deadlocking the pipeline.
- Clean FAIL surfacing. A diagram with genuine syntax errors now
surfaces as a
FAILat the sequential-retry stage, with the mmdc stderr captured. - Observable. Timeout events print
[i/N] TIMEOUT (parallel) …lines so the log immediately shows which diagram stalled.
Negative¶
- Slightly slower wall-clock on hosts that could tolerate
--parallel 4(roughly 2× serial throughput instead of 4×). Acceptable — 8 s for 386 diagrams is already well within CI budget. - Puppeteer-config maintenance. If Chromium / Puppeteer evolves its launch API, the flag set may need a refresh. Mitigated by the config being a single 20-line JSON file.
- Sandbox disabled.
--no-sandboxis required because the Chromium sandbox cannot initialize under WSL / CI. Since mmdc runs on trusted developer/CI inputs (our own docs), the attack-surface trade-off is acceptable.
Confirmation¶
- Fresh render (post-patch, pre-release): 384/386 SVG cleanly, 385/386 PNG cleanly, 4.4 s + 7.9 s end-to-end — zero hangs.
- Chromium processes observed with all target flags via
ps auxf | grep chromiumduring a live run. make pub-diagramssucceeds from a clean cache on WSL2, in Docker (standard image, no--shm-sizeoverride), and in CI.
References¶
- mermaid-cli puppeteer-config docs — https://github.com/mermaid-js/mermaid-cli#puppeteer-config
- Chromium
--no-sandboxguidance — https://chromium.googlesource.com/chromium/src/+/main/docs/design/sandbox.md publications/scripts/puppeteer-config.json— the launch-args artifactpublications/scripts/render_mermaid.py— patched renderer (render_oneat line 207;render_allretry-sequential at line 295; CLI arg at line 582)Makefile—pub-diagramstarget,--parallel 4 → 2on both SVG and PNG passes- ADR-006 (dual-bus events) — MADR 4.0 format reference
- v1.3.2 release memory — historical context for the 23 mermaid failures that this ADR finally closes
Related (secondary fixes bundled in the same roll-up)¶
- Mermaid reserved-word rename:
classDef end → classDef finishindocs/for-auditors/compliance-audit-workflow.md fig-metaHTML comment moved outside mermaid fence indocs/tutorials/publications/adding-new-book-chapter.md