Skip to content

fix(runtime): reclaim native channel rows and unread buffered values - #1532

Merged
Lorenzobattistela merged 4 commits into
bendlang:mainfrom
oxura:fix/issue-1268-channel-lifetime-20261012
Oct 13, 2026
Merged

Lorenzobattistela merged 4 commits into
bendlang:mainfrom
oxura:fix/issue-1268-channel-lifetime-20261012

Conversation

@oxura

@oxura oxura commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #1268. Native Chan is now an owned heap row with an owned FIFO of two-Term nodes, using existing ARR reference counting and iterative term_drop. The global row table, generation handles and deferred row recycling are removed. Each effect consumes its handle; parked operations retain it until wake/deadline. Close still wakes waiters and preserves buffered FIFO contents for copied handles. Receiving transfers a value out, and a full-channel dequeue reuses its node when waking a queued sender. Capacity is logical: creating a channel no longer reserves/touches a capacity-sized ring. No new runtime tag, finalizer framework or garbage collector.

Actual Linux/clang 22.1.8 verification:

  • 500,000 room-one buffered send/close iterations, --threads 4, exit 0 and done: baseline peak RSS 40,728 KiB; treatment 2,232 KiB. Empty-close control: 2,268 -> 2,140 KiB. Exact commands use /usr/bin/time -f 'RSS_KIB=%M elapsed=%e'. Single-run elapsed samples are not a general performance claim.
  • 26 actual native consumer runs at 1/4 threads pass: copied closed-buffer FIFO, boxed values, final parked handle/deadline, rendezvous, try-send/receive, wake/timer cancellation, fork/join and channel-only deadlock termination.
  • Earlier clang AddressSanitizer runs at 4 threads reported no diagnostics for ordinary allocations such as waiter timers. ASan cannot see Bend heap row/node bounds or prove their reclamation. This x86 runtime pins registers; those builds use -fomit-frame-pointer -mllvm -x86-use-base-pointer=false with -fsanitize=address.
  • Both new consumer fixtures also pass the actual Bun CLI's unchanged JS backend.
  • Repository gate: PASS 54 / 54. Protected files and gate limits unchanged.

There is deliberately no permanent RSS threshold: standalone before/after/control measurements prove reclamation; deterministic regressions cover ownership transitions without depending on platform RSS or sanitizer overhead. Native Chan's opaque foreign representation changes, documented in EFFECTS; foreign effects already have no ABI promise. Lifetime follows the existing native reference-counting model, not tracing collection of cycles. No Metal/CUDA run is claimed.

Review follow-up (a781bf7)

Preserved maintainer d95603c: Chan handles are born RFC-wrapped so sharing a Data container does not fail-stop. The retained chan_shared_node fixture prints 78end on actual native 1/4-thread runs, interpreter and emitted JS. Rebuilt 26 existing native channel/fiber consumer runs pass their success/error contracts. Repo gate passes 54/54.

Added chan_reclaim_limit.bend: 16,777,217 discarded, never-explicitly-closed handles. Original main fails with the old lifetime-limit diagnostic; after-fix native and JS print done. Single Linux samples: native 1.23s / 2,228 KiB peak RSS, emitted JS 3.10s. Neither timing nor RSS is a permanent assertion.

Tradeoff: maintainer measurements show channel-only loops 6–13% slower due to node allocation and handle dropping; there is no channel perf bench covering this cost. This is a memory-lifetime correction, not a blanket speedup. No fresh GPU, Mac, mini-cluster or ASan run is claimed for this follow-up.

@Lorenzobattistela Lorenzobattistela self-assigned this Oct 13, 2026
@Lorenzobattistela

Copy link
Copy Markdown
Collaborator

Thanks, this fixes #1268: 500k buffered send/close goes from 34.5 MB to 1.6 MB here. The 17M-iteration loop no longer stops at "more than 16777216 channels", and never-closed channels are reclaimed too.

One bug: a Chan is Data, but chan_open returns a plain, non-shared ARR block. When a node holding the handle is shared and then matched, for example a +xs : List<&2, Chan(U32)> used twice, span_fade finds a non-rfc field and the program dies with runtime fail-stop. The same program works on main. Wrapping it at birth fixes it:

return rfc_wrap(e, term_blk(true, cls, loc), 1);

Please add a test for that case (a channel in a shared list cell). Also, both new tests pass on main, so neither would catch a revert of this fix.

Smaller things:

  • The three one-line ifs should use braces like the rest of the file.
  • The header comment still says "the rows live here".
  • ASan can't see the Bend heap, where the row and nodes now live, so the "ASan clean" claim covers only the timers.
  • Channel-only loops are about 6-13% slower (node alloc + handle drop per op), and no perf bench covers channels. Please mention that in the description.

@oxura

oxura commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

Published a781bf7. Your d95603c arrived while I was pushing the same RFC fix; I merged and preserved it rather than overwriting the branch. I retained your tests/io/chan_shared_node.bend and removed my overlapping shared-list fixture. Your retained fixture prints 78end on rebuilt native binaries at 1/4 threads, the interpreter and emitted JS. My separate before-fix shared-list probe had reproduced runtime fail-stop, so the regression was observed, not inferred from green gates.

Added a reclamation regression that fails on original main rather than only covering old behavior: tests/io/chan_reclaim_limit.bend creates and discards 16,777,217 never-explicitly-closed handles. Original main exits 1 with “more than 16777216 channels at once” (788,152 KiB peak RSS); the patched native run prints done (2,228 KiB; 1.23s) and emitted JS prints done (3.10s). These are single Linux samples, not timing/RSS assertions in the test. The ordinary interpreter run also prints done. This checks the removed process-wide lifetime limit without a platform-dependent RSS threshold.

Rebuilt and exercised all 26 existing native channel/fiber consumer runs at 1/4 threads, including the expected closed-join and deadlock errors. Repo gate: 54/54. Added braces to the three one-line ifs and corrected the header/foreign-representation documentation.

Agreed on the remaining limits: the earlier ASan runs cover ordinary allocations such as waiter timers, not Bend heap bounds or row/node reclamation. Your channel-only 6–13% slowdown is a maintainer measurement, not a general speedup claim; I am adding both that cost and the absence of a channel perf bench to the PR description. No GPU, Mac or mini-cluster run claimed.

@Lorenzobattistela
Lorenzobattistela merged commit b0be246 into bendlang:main Oct 13, 2026
1 check passed
@Lorenzobattistela Lorenzobattistela mentioned this pull request Oct 20, 2026
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.

Native C retains every buffered channel closed without a later receiver

2 participants