Fix runtime thread pool leak caused by arc reference cycles#151
Merged
Conversation
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>
hkmt-mmy
approved these changes
Jul 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The
unienc_cRuntime had Arc reference cycles, so callingunienc_drop_runtimenever 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:
ThreadPool → after_start closure → Arc<Mutex<Option<Runtime>>> → Runtime → Arc<ThreadPool>after_startstores a strong Runtime reference in each worker thread's TLSCURRENT, soworker 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>>lazy_runtimecell:Arc<Mutex<Option<Runtime>>>→Arc<Mutex<Option<WeakRuntime>>>(Runtime::newstoresruntime.weak())after_startcopies the WeakRuntime into TLS as-is (does not set CURRENT if the cell is uninitialized)Runtime::spawnupgrades the Weak in CURRENT before useRuntime::newis preservedAfter the fix, the only long-lived strong reference to the ThreadPool is the C#-owned
Box<Runtime>(unienc_new_runtime), sounienc_drop_runtime→ strong count 0 → ThreadPool drop → Close →before_stop→ worker exit holds. As a side effect, the lifetime check viaWeakRuntime::upgradenow correctly fails after disposal.Verification (macOS)
After submodule init, all three configurations
cargo checksuccessfully (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