Skip to content

Demo stability (lockup + leak) and continuous Pages deployment (#20) - #25

Merged
lann merged 6 commits into
mainfrom
demo-stability-and-pages-cd
Aug 18, 2026
Merged

Demo stability (lockup + leak) and continuous Pages deployment (#20)#25
lann merged 6 commits into
mainfrom
demo-stability-and-pages-cd

Conversation

@lannbot

@lannbot lannbot commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Rebased onto main after #24 merged — these five commits were pushed to that branch minutes after it landed, so they never made it in. Content is unchanged apart from the rebase.

Demo stability: the reported lockup/crash

Reported as "memory leak; app locks up and crashes after several interactions and a couple of minutes". Two real causes, both fixed:

  • Backpressure, not (only) a leak. Every periodic driver appended to an unbounded promise chain unconditionally — app poll (400 ms x3), reconciliation pulls (2.5 s), auto bucket-sync (4 s), stats (4 s) — while the work behind them routinely outlives the period (storage runs 1-3 s/op on consumer APIs). Fixed-rate scheduling plus slower-than-period work diverges: the queue becomes the leak, and user input lands behind hundreds of pending jobs. Periodic work now skips a tick whose predecessor is still running. Measured against a 1.5 s/op delay proxy: 180 ticks skipped in 3 minutes (all of which the old code would have queued), queue depth bounded at 3-4, and a UI task add still completing in 3 ms while storage churns. __demo.health() exposes depth and per-timer skip counts.
  • A genuine unbounded table in the engine: sync-start inserted a result into syncs for every sync and nothing removed it (sync-status only read it) — unbounded at ~48 syncs/minute. Statuses are one-shot by contract, so the entry is removed as it is read; stats() now publishes guest table sizes, because a growth bug inside a component is invisible from outside it.

The rest was a measurement artifact, and my earlier claim was wrong. I had blamed the deltic browser ports in the previous PR's README; two independent checks cleared them: host/table-probe.ts runs 400 pulls headless (every guest table flat, RSS plateaus ~300 MB), and host/cdp-heap.ts drives the identical page in real headless Chromium with a forced GC — over 300 s the heap sawtooths 8-40 MB and returns to 7.5 MB, net -3.1 MB. The unbounded growth was the queue divergence plus paseo's instrumented webview retaining objects. All three probes are committed.

Also in here: transport errors now name their request (PUT host/path: transport failed after 3 attempts) and retry transient failures 3x (statuses are never retried); storage setup announces each of its ~20 steps so a failure says which one died; a duplicate "Save & connect" is refused synchronously; and unversioned assets no longer serve returning visitors a cached bundle (build-stamped root, artifacts inherit it).

Continuous Pages deployment

.github/workflows/pages.yml builds the site from source on every push and deploys docs/ from main; PRs build but do not deploy, so a broken demo fails the PR rather than the site. scripts/setup.sh is the shared entry point — the same script developers run — pinning the sibling deltic ports by commit, the toolchain via rust-toolchain.toml, and wasm-tools/wac/just by version; it delegates to polymorph-iroh's own setup for its vendored deps. Verified from a cold clone: siblings pinned, endpoint component built, engine composite composed against it.

Cutover is deliberately staged (order in the workflow header): land the workflow and confirm a green build on main → switch the Pages source from branch: main /docs to GitHub Actions and confirm the deploy → then delete docs/spike-demo/ from git and gitignore it. I did the deletion first, then reverted it: removing the artifacts that currently serve the site, in the same change that introduces an unproven deploy path, would take the demo offline for the length of the gap.

Note: no PR-triggered run appeared for the previous branch, and this repo has never run a user-defined workflow — I could not determine why from the API (org Actions policy is not readable by this account). If this PR shows no Pages / build check, that is the thing to look at.

Gates

  • spikes/tasks-engine: just check clean; full MinIO+relay scenario SPIKE PASSED.
  • spikes/demo: deno check clean; just site/just pages build; demo beats re-driven in-browser (both providers, revocation on each).
  • scripts/setup.sh: cold-clone run verified.

Refs #20, #19.

lann added 6 commits August 18, 2026 07:53
… stuck (#19, #20)

A live Dropbox run failed with 'fetch: send: NetworkError' after ~20 s
of a single 'configuring storage…' line — a message that names neither
the operation nor the host, following a window with no progress at all.
Three fixes, all reproduced and verified in the browser:

- every provider request names itself in transport errors (PUT
  host/path: transport failed after 3 attempts: …), and transport
  failures retry up to 3x. Statuses are never retried — 429/5xx belong
  to the caller — and every provider call in the engine is idempotent by
  construction, so the retry is safe.
- storage setup announces each of its ~20 sequential steps, so a failure
  says which one died and the advice is actionable.
- a duplicate 'Save & connect' no longer re-runs the whole setup
  underneath the first one. The guard is claimed synchronously at call
  time: the background chain serializes work, so a flag checked inside
  the job always finds the previous run finished.

Also: unversioned assets were serving returning visitors a cached bundle
against fresh components (it cost an hour of chasing an already-deployed
fix). The build now stamps a mutable root and artifacts inherit it —
NOTES §Release integrity's bootloader shape in miniature, and what makes
a Pages republish take effect. docs/spike-demo is rebuilt on it.

Verified after the changes: the tasks-engine MinIO+relay scenario still
passes; in-browser, the S3 leg revokes with the cooperative note and
bob's pull goes dark (kp missing 404), and the Dropbox leg reports
expired tokens legibly (401 expired_access_token) instead of a bare
network error.
)

The lockup-then-crash was backpressure, not (only) a leak: every
periodic driver appended to an unbounded promise chain unconditionally
— app poll (400ms x3), reconciliation pulls (2.5s), auto bucket-sync
(4s), stats (4s) — while the work behind them routinely outlives the
period (storage runs 1-3s/op on consumer APIs). Fixed-rate scheduling
plus slower-than-period work diverges: the queue itself becomes the
leak, and user input ends up behind hundreds of pending jobs.

Periodic work now skips a tick whose predecessor is still running, which
is the correct semantics anyway — a reconciliation pull is a refresh,
not a transaction. Measured against a 1.5s/op delay proxy in front of
MinIO: 180 ticks skipped in 3 minutes (all of which the old code would
have queued), background depth bounded at 3-4, and a UI-path add still
completing in 3ms while storage churns. __demo.health() exposes queue
depth and per-timer skip counts.

A residual browser-only leak remains and is now bounded away from our
code: 500 driver/tasks calls leak nothing, 10 sync pulls leak nothing,
boot without a wire is flat, and host/leak-probe.ts — the same engine
composite under Deno with the same live subscriptions, idling 90s — is
flat (-1.8MB). Growth needs a live wire in a browser, so it belongs to
the deltic browser ports or the embedder's browser glue, not to the
engine. Recorded with the isolation steps for a follow-up profile.
…real-browser measurement (#20)

Two outcomes from chasing the reported lockup/crash.

A real leak, fixed: driver.sync-start inserted a result into the syncs
map for every sync and nothing ever removed it — sync-status only read
it — so the table grew without bound at the demo's ~48 syncs/minute.
Statuses are one-shot by contract, so the entry is now removed as it is
read. stats() additionally publishes the guest's table sizes, because a
growth bug inside the component is otherwise invisible from outside it.

The rest was not a leak, and the previous README claim (blaming the
deltic browser ports) was wrong. Bisected: driver/tasks calls flat, app
polls flat, reconciliation pulls appeared to grow — then two independent
checks cleared the stack. table-probe.ts runs 400 pulls headless: every
guest table flat, RSS plateaus at ~300MB. cdp-heap.ts drives the
identical page in a real headless Chromium and forces a GC at the end:
over 300s the heap sawtooths 8-40MB and returns to 7.5MB, net -3.1MB.

So the unbounded growth was the queue divergence fixed in the previous
commit (one retained closure per queued job) plus paseo's instrumented
webview retaining objects. Memory gets measured in a real browser from
now on; the probes are committed so the next person starts from a
reproduction.
The site was built on a laptop and committed: every rebuild added an
~11 MB engine composite to git history (four of them already landed),
and 'keep Pages updated' was a manual step that could silently drift
from the source it claims to demo.

.github/workflows/pages.yml builds the site from source on every push
and deploys docs/ from main; PRs build it too but do not deploy, so a
broken demo fails the PR rather than the site. scripts/setup.sh is the
shared entry point — the same script developers run — pinning the
sibling deltic ports by commit (they carry embedder conventions that
have broken this demo before), the toolchain via rust-toolchain.toml,
and wasm-tools/wac/just by version. It delegates to polymorph-iroh's own
setup for its vendored deps, verified from a cold clone.

docs/spike-demo/ is now generated rather than committed, so the wasm
artifacts stop accruing in history.

NOTE: the Pages source must be switched from 'branch: main /docs' to
'GitHub Actions' for this to take effect; until then the last committed
build keeps serving. Doing that before this lands would take the demo
offline, so it is a deliberate post-merge step.
Restores docs/spike-demo/ and drops the .gitignore entry from the
previous commit. The workflow still builds the site from source, but
deleting the artifacts that currently SERVE the site in the same change
that introduces an unproven deploy path would take the demo offline for
the length of the gap — and this repo has never run a user-defined
workflow, so the path is unproven by definition.

The cutover order now lives in the workflow header: green build on main,
switch the Pages source to GitHub Actions and confirm the deploy, then
delete the committed artifacts.
@lann
lann merged commit 4fe91d1 into main Aug 18, 2026
2 checks passed
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.

2 participants