Repository navigation
fix(signals): restore deferred releases after cleanup throws - #4176
Conversation
🦋 Changeset detectedLatest commit: 035e9b2 The changes in this PR will be included in the next version bump. This PR includes changesets to release 14 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Merging this PR will regress 1 benchmark
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ❌ |
build + consume |
693.9 µs | 776.7 µs | -10.67% |
| ⚡ | memo + sync render effect only (reference) |
33.6 ms | 28.7 ms | +17.19% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing GabbeV:fix/restore-deferred-releases (035e9b2) with next (b0ad1df)
Footnotes
-
5 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
Each passed alone; together they put next 1 B past the 20 B minified allowance (81,159 B against 81,138 recorded). Accepted by the maintainer. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LY7F9czoRqdMEvk2QwPr1
* fix(signals): release lazy nodes collected before a throwing cleanup #4176 restores the release collector when a cleanup throws while an unfinished pass's frame is dismantled, but the throw also skips the drain at the end of the pass, so lazy nodes the frame's children released before the throw were never released. Drain them before rethrowing. Also adds the solid-js changeset for #4183's docs, which ship in the package. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LY7F9czoRqdMEvk2QwPr1 * revert the lazy-release drain; keep only the #4183 changeset The drain costs about 35 B minified in every signals scenario, including the simple-app floor, for a leak that only follows a cleanup that throws. Held for a size decision. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LY7F9czoRqdMEvk2QwPr1 * chore(size): re-record the compiled CSR cap after #4188, #4207 and #4176 Each passed alone; together they put next 1 B past the 20 B minified allowance (81,159 B against 81,138 recorded). Accepted by the maintainer. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018LY7F9czoRqdMEvk2QwPr1 --------- Co-authored-by: Claude <noreply@anthropic.com>
) * fix: a throwing cleanup no longer aborts the rest of the teardown Every cleanup in the tree runs even when one throws: the ones registered before it on the same owner, unvisited siblings, ancestors and effect cleanups. The first error is rethrown once the teardown is over. Client (@solidjs/signals): one guarded call (`runCleanup`) covers both the `_disposal` list and the effect `_cleanup`, collecting the first error. Outside a flush the walk that began the teardown rethrows it; inside a flush (a commit retiring a frame, a pass replacing an uncommitted one, an effect disposing a root) `flush()` rethrows it after the drain completes, so the commit loop and the effect queues are never left half-applied and the pass that tears down a frame still drains the lazy releases it collected (#4176's follow-up case now passes without a dedicated drain). Server (solid-js): `disposeOwner` guards each child walk and each cleanup and rethrows the first error after the walk, so renderToStream's final dispose runs every cleanup and #4206 still reports the error (#4193). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * chore(size): re-record caps for #4213 from CI Every scenario the change grows, at CI's measured sizes. Hello world (app: compiled floor) is 9,928 B brotli, under 10 KB. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * perf(signals): keep the cleanup rethrow off disposeChildren's hot path CodSpeed flagged createOwners at -10%: the end-of-walk rethrow check in disposeChildren slowed every disposal. Root dispose now rethrows after its walk outside a flush; every other teardown leaves it to the flush that runCleanup schedules. Drops the nested parameter too. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * chore(size): re-record #4213's caps from CI after the perf fix Hello world (app: compiled floor) is 9,935 B brotli, under 10 KB. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * fix(signals): collect unobserved-hook throws, drop cleanup errors on halt Review follow-ups for #4213: - A throwing `unobserved` hook is collected like a cleanup, so the teardown around it carries on and a pass's release collector is always removed. - A halt drops a collected cleanup error, so it never surfaces from a later, unrelated dispose(). - Server disposeOwner goes back to the indexed reverse loop. - Tests for the mid-walk release, the halt, and throwing hooks. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 * chore(size): re-record #4213's caps from CI after the review fixes Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015hPxMkC3jg3Uivh65gvaf2 --------- Co-authored-by: Claude <noreply@anthropic.com>
Summary
A throwing reactive
onCleanupduring an unfinished memo's rerun leaves the module-globaldeferredReleasescollector installed. After the caller catches the error, disposing an unrelated lazy memo's last observer appends it to the abandoned collector instead of releasing it, so its cleanup does not run.This regresses behavior before #4164: the public-signals repro runs the unrelated cleanup once on its direct parent,
a9b9f9e4, but zero times on the tested base,a2743fa2. The cleanup error escapes in both versions.Restore the previous collector in
finally. This restores the earlier release behavior while preserving the original exception, interrupted teardown, and successful-pass deferral/draining behavior. Adds throwing/non-throwing public-API regressions and a patch changeset.Minimal JSX playground: click Run to see
Cleanup count: 0; replacing the throw withreturnand rebuilding gives1. The hosted package build ismizulu/solid2@5a4b2e85; the same component separately reproduces against unchanged upstream source ata2743fa2.How did you test this change?
cleanedis 0 instead of 1); the non-throwing control passes. Both pass after the fix and check that the original cleanup error still escapes. Existing rebuilt-reader/lazy-release regressions also pass.node scripts/check-release-invariants.mjspass.