| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
Worker threads run a bare CFRunLoopRun with no autorelease pool management, so autoreleased ObjC objects created while executing JS on a worker (marshalled returns, framework temporaries, call-scoped block copies) accumulated in the thread's bottom pool and only drained when the worker died - a leak proportional to worker lifetime and native-call volume. The main thread does not have this problem because UIKit drains a pool once per run-loop pass. Scope pools to the runtime's own callouts instead of run-loop passes: EventLoop::RunGuarded wraps each non-bare work unit (both scheduler lanes, where all worker JS executes; bare entries may @throw on purpose and stay unwrapped), the worker's message-drain source wraps DrainPendingTasks, and explicit pools cover the boot phase (runs before the loop) and teardown (otherwise drains only when the backing NSOperation ends). On the main thread this only tightens drain latency; the lifetime contract - alive at least to the end of the current callout - is unchanged. A UIKit-style run-loop observer (tried in both single-pool and pool-per-nesting-level forms) aborts with AutoreleasePoolPage::badPop under the HTTP-ESM loader tests: nested CFRunLoopRunInMode pumps interleave foreign pool lifetimes across callouts, cutting observer-owned tokens. A pool pushed and popped inside a single callout cannot be interleaved with, and matches the codebase's existing entry-point pattern. The new spec autoreleases a TNSAllocLog on the worker in one timer callout and reports TNSGetOutput() from the next; the dealloc entry can only be present in between if the pool drained per callout.
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. ❤️ ShareComment @coderabbitai help to get the list of available commands. |
Sorry, something went wrong.
| Back | FazBrowse Home | New Git URL |
Problem
Worker threads run a bare CFRunLoopRun() with no autorelease pool management. Autoreleased ObjC objects created while executing JS on a worker (marshalled method returns, framework-internal temporaries, call-scoped block copies) accumulate in the thread's implicit bottom pool and only drain when the worker dies — the backing NSOperation's pool is the only drain point. That is a memory leak proportional to worker lifetime and native-call volume. The main thread does not have this problem because UIKit installs run-loop observers that drain a pool once per loop pass.
Fix
Scope pools to the runtime's own callouts instead of to run-loop passes:
Why not a UIKit-style run-loop observer?
Two observer designs (single pool with drain-per-pass; one pool per run-loop nesting level, UIKit's scheme) were tried first and both abort with AutoreleasePoolPage::badPop under the HTTP-ESM loader tests: worker module loading pumps nested CFRunLoopRunInMode from inside callouts whose async machinery interleaves its own pool lifetimes across callouts, so an observer-owned token can be cut out from under the observer by a pool it does not control. A pool that is pushed and popped strictly inside a single callout cannot be interleaved with by construction — and per-callout @autoreleasepool at entry points is already this codebase's established pattern (ModuleInternalCallbacks, InteropTypes, AnimationFrame, HttpLoader).
Known limitation: callouts the runtime does not own (e.g. a notification block delivered directly to the worker loop) still autorelease into the bottom pool. All JS execution and marshalling paths are covered.
Test
TestRunner/app/tests/WorkerAutoreleasePoolTests.js + autoreleasePoolDrainWorker.js: the worker autoreleases a TNSAllocLog instance (new +autoreleaseInstance fixture method — CFAutorelease(CFBridgingRetain(...)), so the pool holds the only reference and no JS wrapper retain is involved) inside one timer callout and reports TNSGetOutput() from the next. The dealloc log can only be present in between if the pool drained between the callouts; without the fix it appears only at worker death, after the report is sent.
Full suite: green (Debug, simulator).