Skip to content

Fix runtime thread pool leak caused by arc reference cycles#151

Merged
ruccho merged 1 commit into
mainfrom
fix/unienc-runtime-leak
Jul 3, 2026
Merged

Fix runtime thread pool leak caused by arc reference cycles#151
ruccho merged 1 commit into
mainfrom
fix/unienc-runtime-leak

Conversation

@ruccho

@ruccho ruccho commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

The unienc_c Runtime had Arc reference cycles, so calling unienc_drop_runtime never freed the thread pool — a permanent leak. Worker threads and memory accumulate on every Unity domain reload or session re-creation.

Investigation found two cycles:

  1. ThreadPool → after_start closure → Arc<Mutex<Option<Runtime>>> → Runtime → Arc<ThreadPool>
  2. after_start stores a strong Runtime reference in each worker thread's TLS CURRENT, so worker thread TLS → Runtime → Arc<ThreadPool> also holds. A ThreadPool only sends Close to workers once its handle is dropped, but as long as a worker lives its TLS holds the Arc, so fixing only # 1 still never frees it.

Fix

Weakened both cycles.

  • CURRENT: RefCell<Option<Runtime>>RefCell<Option<WeakRuntime>>
  • The lazy_runtime cell: Arc<Mutex<Option<Runtime>>>Arc<Mutex<Option<WeakRuntime>>> (Runtime::new stores runtime.weak())
  • after_start copies the WeakRuntime into TLS as-is (does not set CURRENT if the cell is uninitialized)
  • Runtime::spawn upgrades the Weak in CURRENT before use
  • The synchronization structure between worker startup and Runtime::new is preserved

After the fix, the only long-lived strong reference to the ThreadPool is the C#-owned Box<Runtime> (unienc_new_runtime), so unienc_drop_runtime → strong count 0 → ThreadPool drop → Close → before_stop → worker exit holds. As a side effect, the lifetime check via WeakRuntime::upgrade now correctly fails after disposal.

Verification (macOS)

After submodule init, all three configurations cargo check successfully (0 errors):

  • cargo check (default = multi-thread)
  • cargo check --no-default-features (single-thread path)
  • cargo check --features unity (includes unity.rs)

Grepped the whole crate to confirm no long-lived object stores a strong Runtime reference.

🤖 Generated with Claude Code

the after_start closure stored in the thread pool captured a strong
Runtime via the lazy init cell, and worker thread-local CURRENT also
held strong Runtime clones. both formed cycles keeping the ThreadPool
alive after unienc_drop_runtime, leaking worker threads and memory on
every runtime recreation. store WeakRuntime in the lazy cell and in
CURRENT, upgrading on spawn, so dropping the last external Runtime
handle shuts the pool down.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ruccho ruccho marked this pull request as ready for review July 3, 2026 05:01
@ruccho ruccho requested a review from hkmt-mmy July 3, 2026 05:01
@ruccho ruccho merged commit da8c172 into main Jul 3, 2026
2 checks passed
@ruccho ruccho deleted the fix/unienc-runtime-leak branch July 3, 2026 08:25
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