Skip to content

webasm: intermittent renderer process crash during a long run (1 of 3 attempts, not reproduced since) #870

Closed

Description

@fuzzie360

Follows #866. Filing this deliberately thin, because it happened once and I have not reproduced it — but a renderer crash seemed worth recording even without a repro, in case it matches something known.

What happened

Driving the gpu.rocks benchmark suite (30 workloads, mode: 'webasm' plus a plain-JS baseline, all inside one Web Worker) headless on Chrome 150, Apple M1 Max, branch build of #866 (11b4c0e).

Three consecutive attempts at the identical pass:

attempt outcome
1 completed all 30 rows
2 renderer died around row 6–7 (~90 s in)
3 completed all 30 rows

On attempt 2 the driver's next page.evaluate threw Attempted to use detached Frame, which is puppeteer's symptom for the renderer process going away. Attempt 2 was the same build, same rows, same order as the two that passed.

What I can rule out

After it happened I instrumented the driver to sample performance.memory.usedJSHeapSize alongside the status on every poll. Across the full successful pass afterwards the main-thread heap stayed flat at 11–14 MB from row 1 to row 30, so it is not a monotonic main-thread leak. That measurement is from a passing run, though — I have no heap trace from the crash itself.

The work runs in a Worker with OffscreenCanvas, and the wasm backend under mode: 'webasm' (not the async/threaded contract), so no SharedArrayBuffer or worker pool is involved on this path as far as I can tell.

What I cannot say

  • Whether it is attributable to this backend at all. It is suggestive that two prior full passes of the same suite on published 2.21.0, with every other backend and no webasm column, completed without incident — but that is three data points against two, not evidence.
  • Which workload was executing. My driver only knew the last polled status, which is why I added crash-time status capture afterwards; if it recurs I will have the workload name and can update this.

If the shape of this rings a bell — a wasm instance or memory being freed while a call is in flight, say, or a rebuild after destroy() racing something — that would be more use than anything further I can measure from outside. Otherwise I will add a note here if it recurs with the workload named.

Activity

  1. fuzzie360 commented on Aug 3, 2026

    @fuzzie360
    MemberAuthor

    The shape does ring a bell, and the flat heap trace is the tell rather than the alibi: performance.memory.usedJSHeapSize cannot see WebAssembly memories. Each webasm kernel caches one WebAssembly.Memory per size signature ([args | constants | output], so tens of MB per kernel at these workload sizes), and in Chrome each Memory also pins a multi-GB virtual guard reservation for bounds-check elision. The bench destroys per workload, but destruction only drops JS references — the wrapper objects are tiny, so a pressure-blind GC has no urgency, and dead memories from a 30-workload sweep can transiently accumulate until the renderer hits its commit or reservation limit. Intermittency by GC timing fits the 1-of-3 pattern; "around row 6–7" fits the first rows being the large ones.

    Hardened on feature/webasm-backend (b79dfb9), on the reasoning that even an unattributed crash deserves the one memory-shaped defect the audit found fixed:

    • the per-kernel module cache is LRU-bounded (moduleCacheLimit, default 8) — previously a size-sweeping kernel (dynamicOutput) grew it without bound;
    • evicted and destroyed entries are scrubbed: instance, module, memory, views and bytecode all dropped, so a kernel object retained anywhere (the run-shortcut closure, user code) no longer keeps every cached memory alive with it;
    • for threaded entries the worker pool now releases the worker-side instantiations, which were holding the shared buffer forever; deferred past the threaded tail so an in-flight dispatch keeps what it captured.

    Sync and threaded eviction-then-revisit are pinned by test/features/webasm/module-cache.js.

    What cannot be claimed: proof this was the crash. If it recurs on a build with these changes, the useful captures are the workload name (now instrumented), performance.measureUserAgentSpecificMemory() (sees wasm, unlike performance.memory), and the exit reason from chrome://crashes — an OOM kill there would confirm the mechanism even without a repro.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions