From ae470b77f4d49ed2e0ce4dd0ea65458d61ba9d00 Mon Sep 17 00:00:00 2001 From: Dara Adedeji Date: Thu, 30 Jul 2026 00:06:00 -0700 Subject: [PATCH 01/21] Pin the macOS memory-reduction native work and the audit briefs Native SDK branch macos-memory-shared-renderer-prep (600d6cf6) carries the autorelease-pool, analytic-rounded-clip, and Metal tiled-image memory work; the tiled-image change still needs live verification. The briefs scope the two follow-up passes: the error-propagation seams (partially landed via #43) and the receipt sweep over every numeric limit. The shared-renderer experiment plan lives in the handoff doc accompanying this work. --- docs/error-propagation-brief.md | 150 ++++++++++++++++++++++++++++++++ docs/receipt-sweep-brief.md | 149 +++++++++++++++++++++++++++++++ runtime/native-sdk | 2 +- 3 files changed, 300 insertions(+), 1 deletion(-) create mode 100644 docs/error-propagation-brief.md create mode 100644 docs/receipt-sweep-brief.md diff --git a/docs/error-propagation-brief.md b/docs/error-propagation-brief.md new file mode 100644 index 0000000..8fd7fcc --- /dev/null +++ b/docs/error-propagation-brief.md @@ -0,0 +1,150 @@ +# Error-propagation pass — the seams + +Brief for a detailed pass over every place an error can be born, swallowed, or +presented. The governing rule is the one this repo already committed to +(#39, "Name every silent failure where the developer looks"): a failure must +surface **where the developer is looking** — `weaver check` output, the dev CLI +stream, the per-widget log, or the widget window itself — and it must name the +budget/cause, not just exist as a bare error name. + +Everything below was reproduced live on 2026-07-29 against noro-shell unless +marked speculative. The single worst end-to-end demo: add ~6 retained nodes to +`examples/noro-shell/widget.tsx` and the widget window renders a flat field of +**uninitialized GPU memory** (a different random color every launch), with +`weaver check` passing and zero error lines in any log. + +## Seam 1 — the SDK render path has no error boundary (highest leverage) + +- `sdk/src/reconciler.ts:835` `scheduleRender()` runs `renderRoot()` inside + `void Promise.resolve().then(...)`. Any throw inside a re-render (including + every budget error the Zig bridge deliberately throws) becomes an unhandled + promise rejection. +- No `JS_SetHostPromiseRejectionTracker` is installed anywhere + (`rg PromiseRejection runtime/` is empty), so QuickJS drops the rejection on + the floor. Confirmed: over-budget fresh start logs *nothing*, hot swap logs + only a bare `error: CallbackFailed`. +- `renderRoot()` (`sdk/src/reconciler.ts:403`) has `try/finally` around the + batch but no catch: a throw mid-reconcile leaves a **half-built tree already + committed** via `native.endBatch()`. There is no rollback and no "don't + present a tree whose build threw." + +Wanted: a render error boundary that (a) catches, (b) logs the message + stack +through a bridge call so it lands in the per-widget log, (c) puts the widget +into a visible error state (even a solid color + name is fine), and (d) never +commits a partially-built generation. Same treatment for effect callbacks, +`useInterval` callbacks, and `onFrame` canvas callbacks. + +## Seam 2 — platform callback failures lose their name + +- `runtime/native-sdk/src/platform/macos/root.zig:763` intends to log + `platform callback failed: (event )`, but what actually reaches + the widget log is a bare `error: CallbackFailed` (observed three times + today). Find where that line is emitted (likely the runtime's top-level exit + path in `runtime/src/main.zig`) and make the *named* line the one that lands + in the per-widget log before the process dies. +- After the runtime process dies, the host keeps the widget window alive + showing whatever memory the surface had. That's both a UX bug and arguably an + info leak (stale GPU memory). The host should clear the surface and/or show a + tombstone when the runtime for a window is gone. + +## Seam 3 — budget errors: born loud, dying silent + +The bridge does the right thing at the throw site — `failFmt` +(`runtime/src/bridge.zig:142`) even documents that budget errors must name the +budget, the limit, and the ask. But: + +- `runtime/src/bridge.zig:166` `createNode` → "node capacity exhausted" names + neither `max_nodes` nor 128 nor the node count. Same for the generic + "appendChild failed" / "insertBefore failed" (`bridge.zig:185,195`) which is + how `max_children = 24` surfaces. Bring these up to the `failFmt` standard. +- All of them then die in Seam 1 anyway. Both halves need fixing. +- `runtime/src/tree.zig:4-21` budgets (`max_nodes 128`, `max_children 24`, + `max_text_bytes 192`, `max_canvases 8`) are *statically checkable* for the + initial tree — `weaver check` should count nodes/children of the authored + JSX and fail with headroom numbers instead of letting the runtime discover + it. (Separately: 128 is probably just too small — native SDK uses 1024/view + and documents why it abandoned 128/256 — but that's a sizing decision, not + this pass.) + +## Seam 4 — image failures are log-only, screen-silent + +- `runtime/src/main.zig:119` and `:1012` log `ImageTooLarge` etc. to the + per-widget log, then render proceeds with a black hole where the image was. + Nothing on screen, nothing in the dev CLI stream, `weaver check` passes. +- The 256 KiB decoded-RGBA cap + (`runtime/native-sdk/src/runtime/canvas_limits.zig:118`, widget profile) is + hit by *any* real album art; the bundled `cover.jpg` is 256×256 = exactly at + the cap. `weaver check` can decode bundled assets and fail with the exact + dimensions/byte math at check time. Host-fed `media.artPath` art needs either + host-side downscale-to-fit or a runtime log + on-widget placeholder that says + why. + +## Seam 5 — dev loop failure modes + +- `cli/src/index.ts:431-460`: rebuild failures print once via `printFailure`, + but the runtime keeps hot-swapping/serving the **stale bundle** with no + banner that what's on screen no longer matches the file. Persist an "out of + date since