Skip to content

[Homebrew/Test] Isolate Brew tap memory across browsers - #1168

Closed
brandonpayton wants to merge 28 commits into
mainfrom
test/executable-module-cache-public-chromium-qk044
Closed

[Homebrew/Test] Isolate Brew tap memory across browsers#1168
brandonpayton wants to merge 28 commits into
mainfrom
test/executable-module-cache-public-chromium-qk044

Conversation

@brandonpayton

@brandonpayton brandonpayton commented Jul 31, 2026

Copy link
Copy Markdown
Member

Why

Fresh Chromium core and canary runs both approached the hosted
runner's 16 GB memory ceiling while stock Brew tapped a repository.
The renderer then crashed.

PR #1167 found that Kandelo recompiles the same WebAssembly executable
for every short-lived process. It adds a bounded compiled-module cache.
Focused tests cannot show whether that change controls a real Brew
workload in Chromium.

This diagnostic branch combines the public Chromium lifecycle with
#1167. It exists only to run the exact immutable core and canary inputs
on fresh hosted runners.

Hosted Chromium result

Run 30635830180 disproved cache thrashing as the complete cause.
Both independent runners reached the same cache state:

  • 160 executable requests;
  • 149 hits and 11 misses;
  • 3 evictions;
  • 8 retained entries representing 34,963,976 source bytes.

The core runner still peaked at 13,915,262,976 cgroup bytes. Its main
renderer later held about 6.2 GiB. The canary runner peaked at
14,345,535,488 cgroup bytes and later held about 9.4 GiB in its main
renderer. Both renderers crashed with zero cgroup OOM and OOM-kill
events.

Both failures happened during the common first-party brew tap phase,
before canary-specific work. Neither run reported a guest trap, a fork
continuation error, a Ruby exception, or an incorrect child status.

Matched cross-browser result

The same sealed core fixture was then run with Playwright's Chromium,
Firefox, and WebKit engines on one Mac. The fixture and its 38 inputs
were verified by digest before each run.

Engine Result Peak engine-tree RSS
Chromium 149 Renderer crashed during brew tap 7,021 MiB
Firefox 151 Tap/install/execute passed 4,036 MiB
WebKit 26.5 Earlier Wasm call-stack overflow 4,771 MiB

RSS means resident set size: the physical-memory pages attributed to
the browser process tree at that sample.

Firefox completed in 5.7 minutes, and its RSS fell between allocation
waves. Chromium reproduced the hosted failure in 61 seconds on the same
machine. Its main renderer alone reached about 6.43 GiB.

WebKit did not reproduce the Chromium memory failure. It failed earlier
inside instrumented Ruby with Maximum call stack size exceeded. The
stack was Ruby Wasm compilation and require work, after the module had
already instantiated. That matches the known WebKit stack limit for an
overly broad wasm-fork-instrument closure.

These results rule against a simple cross-engine Kandelo memory leak.
They do reveal an architectural workload amplifier: the older Ruby
creates hundreds of process workers and fresh Wasm memories. Chromium
retains or collects that churn differently enough for its renderer to
crash. Kandelo should remove avoidable churn rather than rely on one
engine's garbage-collection timing.

Current interpretation

The immutable fixture uses the older fork-heavy Ruby bottle, not
#1166's direct-posix_spawn Ruby. The Chromium trace contains 712
process-worker script requests but only 160 executable-cache requests.
The extra worker generations therefore reuse executables while still
creating fresh process memories.

The old Ruby imports a minimum of 205 Wasm pages, or 12.8125 MiB.
Multiplying that minimum by roughly 552 extra worker generations gives
about 7 GiB of cumulative backing churn. That is close to Chromium's
observed peak, but it is an inference; it does not prove that all those
memories were live at once.

There is no observed semantic wasm-fork-instrument failure in the
Chromium trace. Such a failure would normally produce a continuation
trap, duplicate effects, an incorrect child status, or another guest
error. WebKit's separate stack failure still requires focused
instrumenter follow-up.

Diagnostic boundary

Changing host/src also changes three unrelated generated image
identities: Lamp, Nginx/PHP, and WordPress. The final product must
regenerate those images before #1147 can build and prove the complete
main site.

This do-not-merge measurement builds only the focused Homebrew fixture.
It still uses the exact public shell, bottle mirror, core tap, and
independent canary tap. Its failure is diagnostic evidence; it is not
the final main-product proof required to merge #1147.

Next discriminator

Keep the fixture, host, cache, kernel, rootfs, and tap revisions fixed.
Replace only Ruby with #1166 revision 13, then record fork, spawn, exec,
exit, worker-launch, and memory counts in all three engines.

If fork counts and memory collapse, the old Ruby process path caused
the remaining pressure. If forks are nearly absent but memory remains,
the host launch and executable-snapshot path is responsible. A
low-memory residual fork that traps or returns a wrong status would
instead implicate fork instrumentation.

Do not merge this pull request. Land product corrections through their
separate reviewed pull requests, then close this diagnostic branch.

Scope

The product inputs are unchanged from #1147. The runtime difference is
#1167's bounded compiled-executable cache. The diagnostic workflow also
removes unrelated demo images from this measurement only.

This does not change the kernel, guest ABI, process-memory ownership,
packages, bottles, or VFS format.

Every push to main starts a Pages workflow. Cancellation is only a
resource optimization; it does not decide which run may publish.
Document that the deployment gate admits only the newest run triggered
for main, so a delayed older run cannot replace a newer site.
The fail-closed development shell drops ambient workflow variables. The
Pages acceptance step set its product identities only on the outer step,
so the browser could silently receive defaults instead of the admitted
shell contract.

Pass every selected identity through the explicit dev-shell boundary.
Extend the deployment checker and its mutation suite so deleting any
required handoff fails before Pages can publish.
The shipping gate now proves first- and third-party installs in two
fresh Node processes while Chromium retains the complete lifecycle.
State that exact contract in the Pages cutover docs.
Vite imports a content-addressed shell image under dist/assets. The
un-hashed public copy can be stale while the browser loads different
bytes.

Make Pages validate the asset Chromium will use, and lock that choice
into the deployment contract with positive and negative checks.
Run first-party and independent-tap shipping proofs as separate
browser scopes. Keep the comprehensive reboot lifecycle as a separate
soak.

Fail immediately when Chromium loses its renderer, stream only bounded
guest progress, and collect bounded cgroup and per-Chromium RSS
telemetry. This turns an early page crash into useful evidence instead
of an eighteen-minute timeout.
Bind the immutable mirror release and exact plan in the recovery lock.
Let public fixtures omit local bottle payloads because Chromium verifies
the plan-owned URLs, sizes, and hashes as it downloads each lazy layer.
Capture each fresh runner cgroup OOM baseline and require it to remain
unchanged. Preserve the counters beside browser and process telemetry so
a renderer loss cannot be mistaken for a successful bottle install.
Build the sealed browser once, then give the core and canary scopes
separate fresh Chromium runners. Fetch immutable inputs anonymously,
bind the public mirror plan, fail on page loss or cgroup OOM, and retain
bounded process telemetry.

Keep product and runtime-source authorities distinct. Static mutation
tests prevent proof consumers from regaining Nix, Vite, resolver, or
publication work.
A consume-only browser proof must not repeat the product build on the
memory-constrained runner or confuse test-code provenance with the
published product authority.

Create one deterministic handoff containing the sealed browser dist, the
exact public fixture, and only the Playwright source needed to run it.
Record the immutable product commit separately from the runtime source
commit. Reject traversal, symlinks, special nodes, inventory drift,
changed bytes or modes, and any relaxed resource bound.

Use a minimal pinned Playwright package and a fixed static server so the
consumer installs npm and Chromium but does not run Nix, Vite build,
kernel fetch, or publication.

Validation:
- browser runtime handoff contract suite
- shell syntax and ShellCheck
- Node syntax for the static server
- minimal npm install and Playwright shipping-test discovery

The live public Chromium lifecycle was not run by this commit.
Remove GitHub Actions credentials before Playwright starts Chromium and
the sealed static server. This keeps the public bottle proof anonymous
even if later transport code begins reading its parent environment.
Run the browser runtime handoff mutation suite from the aggregate
Homebrew publisher contract so inventory, authority, mode, and resource
bounds cannot drift outside focused development.
Require the recovery lock to retain the exact lifecycle release asset
sizes and SHA-256 values. A syntactically valid replacement digest must
not silently redefine the immutable product being proved.
A future edit to the proxy server, fixture contract, or handoff verifier must not bypass the live public Chromium proof merely because the workflow path filter did not name that executable input.
The consume-only Chromium proof serves sealed product bytes without
Vite. The current browser still sends public bottle, OCI, and Git
traffic through its same-origin CORS proxy route. Omitting that route
makes the sealed proof fail before it reaches Kandelo.

Add a temporary, bounded transport seam to the sealed server. Permit
only GitHub and GHCR public hosts, pin public DNS results, stream
bounded responses, and support the GET, HEAD, and POST behavior
required by Homebrew and Git smart HTTP. Relay only guest-created
protocol headers, remove authorization on redirects, and preserve safe
range selectors across GitHub CDN redirects.

Document service-worker ownership as the intended architecture. Cover
GHCR, encoded Git POSTs, redirects, credential stripping, range
headers, null-body statuses, SSRF rejection, size limits, timeouts, and
upstream failures.

Validation:

- canonical browser runtime handoff contract suite
- ShellCheck and Node syntax checks
- minimal npm install and Playwright shipping-test discovery
- live public release GET and Range reads through production policy
- live public Git smart-HTTP advertisement through production policy

The live public Chromium lifecycle was not run by this commit.
Keep the public Chromium recovery beside the product cutover it admits.
This avoids a second main merge and gives the live proof the exact
combined PR merge as its runtime authority.
The sealed-handoff verifier executes Node, but the producer called it
through the runner ambient PATH after other tool setup. That PATH no
longer selected Node 24 in the live proof.

Run handoff creation inside the repository dev shell, lock that boundary
in the workflow checker, and report the observed major on future version
mismatches.
The Chromium handoff must seal an exact, clean source checkout. The
normal musl build applies Kandelo overlays inside its submodule, so an
in-tree build makes a valid handoff look contaminated.

Archive the exact synthetic-merge tree and musl gitlink into runner
temporary storage, build both browser sysroots there, and copy only the
generated outputs back. Keep the full clean-tree check and include a
bounded path preview when it fails.

Validation:
- handoff and workflow mutation tests
- real isolated wasm32 and wasm64 musl builds
- Ruby syntax, shellcheck, actionlint, and git diff checks
The sealed browser manifest records every file, including the Vite
Doom asset .gitignore. Artifact upload excludes dotfiles by default, so
both fresh consumers received 103 of 104 signed members and rejected the
incomplete inventory before Chromium could start.

Enable hidden-member transport only for the bounded handoff upload.
Require that setting in the canonical workflow checker and its mutation
test. The manifest verifier still authenticates every permitted path,
size, digest, and mode, so unexpected files fail closed.

Validation:
- handoff and workflow mutation tests
- Ruby syntax, shellcheck, actionlint, and git diff checks
- downloaded run 30618124505 artifact inventory comparison
Repeated spawn and exec of the same VFS executable compiled another
WebAssembly.Module every time. Short-lived Homebrew commands could
therefore leave many compiled-code allocations awaiting engine
collection after their process memory had retired.

Key one bounded, cross-host LRU by the complete prepared byte
snapshot length and SHA-256. This lets aliases share compilation while
a write or replacement always selects new code. Coalesce concurrent
misses, compile oversized modules uncached, and preserve the existing
malformed-initial-image trap path.

Cover content aliases, same-length replacement, concurrent reuse,
eviction, failed compilation, and a real Node VFS replacement.
Precompiling initial executables in the kernel Worker must not change
how a malformed initial module fails. A CompileError still belongs to
the process-Worker loader path, while exec and posix_spawn reject bad
modules during preflight.

Launch a header-valid but truncated module through the real Node host.
Verify that it creates the process, exits through the loader-error path,
and reports the existing host diagnostic.
Keep the new cache and its focused unit tests consistent with the host
TypeScript formatting contract.
The Lamp, Nginx/PHP, and WordPress image builders boot Kandelo while
generating or prewarming data. Their declared host/src input changed
with executable-module caching, so regenerate the authoritative program
projection instead of admitting stale cache keys.
Build only the focused Homebrew fixture for this do-not-merge
measurement. The complete product build now needs three unrelated image
regenerations because host/src is one of their declared inputs.

Keep the exact immutable shell, mirror, core, and canary inputs. Final
#1147 must still prove the complete main product after those images are
rebuilt.
The diagnostic build deliberately excludes unrelated generated images, but Vite still imports the current packaged kernel. Resolve only that package through the declared binary cache and bind the same cache to the build.

Keep the workflow checker exact and add a mutation test that rejects substituting another package.
Even the focused Vite graph imports the kernel and generic rootfs.

Resolve only those two packaged inputs through one isolated cache.

Keep unrelated generated browser images outside this diagnostic.

Extend the exact workflow checker and its mutation test accordingly.
Record bounded cache hit, miss, and eviction counters before a renderer
crash. Preserve Playwright browser-process diagnostics so the next
throwaway proof can distinguish cache thrash from a Chromium failure.
Regenerate the three host-backed image identities after adding
bounded Chromium cache telemetry. This keeps focused binary
resolution fail-closed without building unrelated demo images.
@brandonpayton
brandonpayton force-pushed the test/executable-module-cache-public-chromium-qk044 branch from 8824329 to 2c2b900 Compare July 31, 2026 13:46
@github-actions

Copy link
Copy Markdown
Contributor

Phase B-1 matrix build status — pr-1168-staging

ABI v42. 0 built, 3 failed, 3 total.

Package Arch Status Sha
lamp wasm32 failed
nginx-php-vfs wasm32 failed
wordpress wasm32 failed

Auto-generated; replaced on each push. Raw data in the publish-status workflow artifact.

@brandonpayton brandonpayton changed the title [Host/Test] Measure compiled-module reuse in Chromium [Homebrew/Test] Isolate Brew tap memory across browsers Jul 31, 2026
@brandonpayton

Copy link
Copy Markdown
Member Author

Closing this do-not-merge diagnostic after the bounded Pages cutover.\n\nThe module-cache experiment did not prevent the Chromium renderer\ncrash. Product cache work remains in #1167; the failed fresh-runner\nproof stays here as diagnostic evidence.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant