Skip to content

refactor(runtime): a Runtime owns one isolate and publishes it from Init - #513

Open
edusperoni wants to merge 1 commit into
mainfrom
fix/memory-pressure-registry
Open

edusperoni wants to merge 1 commit into
mainfrom
fix/memory-pressure-registry

Conversation

@edusperoni

@edusperoni edusperoni commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Supersedes #512, which found the race this fixes. Thanks @adrian-niculescu.

Problem

Setting up a runtime was assembled by hand at each call site:

  • Main (NativeScript.mm): new Runtime(), CreateIsolate(), Locker, Init(isolate).
  • Worker (Worker.mm): the same, plus WatchHeapLimit before Init(isolate, true) and SetWorkerId after it.

That split caused several problems:

  • Startup race. CreateIsolate added the isolate to the live registry (isolates_) before Init wrote its Caches slot. Runtime::NotifyMemoryPressure (feat(runtime): report native memory held by wrappers to V8, forward memory pressure #507) runs on a dispatch queue and reads that slot for every registered isolate, so a memory-pressure event during startup could read it while it was being written. On a miss it also built a stand-in Caches off-thread. fix(runtime): publish an isolate's Caches under the registry lock #512 fixed this by writing the slot under the registry lock; this PR removes the window instead.
  • Wrong worker flag. SetWorkerId ran after Init, so Runtime::IsWorker() returned false during a worker's Init. That includes worker-scoped class naming in ClassBuilder and the main-isolate-only checks in ns:runtime.
  • Leaked isolates. CreateIsolate could be called more than once on the same Runtime. And a Runtime deleted before Init left its isolate registered and never disposed, because isolate_ was only set at the end of Init.

Change

  • Runtime(const RuntimeOptions&) creates the isolate on the calling thread, which becomes its home thread. The isolate limits and the worker id are passed in up front, so IsWorker() is correct from the start.
  • The caller still takes the Locker and can work with the isolate before it's ready; the worker arms WatchHeapLimit there. It then calls Init().
  • Init() adds the isolate to the registry only after writing its Caches slot. Teardown already removes it from the registry before clearing the slots. So any thread that finds an isolate in the registry may read its slots while holding the registry lock. This rule is documented on isolates_, and the memory-pressure handler relies on it unchanged.
  • A Runtime deleted before Init disposes its isolate, which no other thread has seen yet.
  • CreateIsolate and SetWorkerId are removed. Runtime.h is a project header, not part of the public framework headers.

Not covered

A reader that finds the isolate in the registry, releases the lock, and only then reads the slot can still race teardown's Caches::Remove. The pre-Locker IsolateWrapper::IsValid() in ArgConverter::MethodCallback does this. That's a separate fix: read the slot only under the isolate's Locker.

Tests

No new test: the startup overlap depends on timing (see #512 for a sleep-based repro). The full suite under ASan on a dedicated simulator gives 1835 specs, 0 failures, and no sanitizer reports.

Summary by CodeRabbit

  • Refactor
    • Runtime setup now applies resource limits and worker identification from the outset. Initialization and cleanup follow a consistent lifecycle for both standard and worker runtimes.

@coderabbitai

coderabbitai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 62eb19cd-6ee9-40c3-b0d7-d687688c2f83
📥 Commits

Reviewing files that changed from the base of the PR and between 66ac3ce and ca7dda6.

📒 Files selected for processing: 4
  • NativeScript/NativeScript.mm
  • NativeScript/runtime/Runtime.h
  • NativeScript/runtime/Runtime.mm
  • NativeScript/runtime/Worker.mm

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 0 remain after this review.

📝 Walkthrough

Runtime now creates and configures isolates in the constructor using RuntimeOptions. Init() initializes caches and registers the isolate. Application and worker setup retrieve the isolate from Runtime and call Init() without arguments.

Changes: Runtime Isolate Lifecycle

Layer / File(s) Summary
Configure and create the isolate
NativeScript/runtime/Runtime.h, NativeScript/runtime/Runtime.mm
RuntimeOptions carries isolate limits and a worker ID. The constructor creates and configures the isolate. The destructor directly disposes of the isolate if initialization did not complete.
Initialize and publish the isolate
NativeScript/runtime/Runtime.h, NativeScript/runtime/Runtime.mm, NativeScript/NativeScript.mm, NativeScript/runtime/Worker.mm
Init() initializes caches, registers the isolate, and marks the runtime initialized. Application and worker setup retrieve the isolate from Runtime and call Init() without arguments. Worker setup passes its resource limits and ID to the constructor.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Bug fix

Suggested reviewers: nathanwalker

Merge Risk: ⚪ Minimal · up to ca7dd

The inspected lifecycle ordering addresses the memory-pressure startup race. No issue remains that should block merging after normal checks.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 1 files. (3 skipped: 3 … Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly describes the main runtime change: Runtime creates and owns one isolate, then publishes it during Init.
Full details: Docstring Coverage

Explanation

Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 3 functions across 1 files. (3 skipped: 3 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

A rabbit checks the runtime’s start,
With limits tucked beside its heart.
The isolate wakes, then joins the line,
Init() sets the caches in time.
Worker IDs arrive in tow,
And off the little runtimes go.

Comment @coderabbitai help to get the list of available commands.

Setting up a runtime took CreateIsolate, Init and, for workers, SetWorkerId,
assembled by hand at each call site. The isolate joined the live registry in
CreateIsolate, before Init wrote its Caches slot, so a thread that found it
there (the memory-pressure handler, #512) could read that slot while it was
being written. CreateIsolate could also be called twice on one Runtime, and a
Runtime deleted before Init left its isolate registered and never disposed.

- Runtime(const RuntimeOptions&) creates the isolate, homed on the calling
  thread, and takes the limits and worker id up front, so IsWorker() is
  correct during a worker's Init. The caller still locks the isolate and can
  set it up (WatchHeapLimit) before calling Init().
- Init() publishes the isolate to the registry only after writing its Caches
  slot. With the teardown already unregistering before clearing it, a thread
  that finds an isolate in the registry may read its slots under the lock.
- A Runtime deleted before Init disposes its unpublished isolate.
- CreateIsolate and SetWorkerId are gone.

Reported in #512.
@edusperoni
edusperoni force-pushed the fix/memory-pressure-registry branch from 66ac3ce to ca7dda6 Compare October 13, 2026 20:53
@edusperoni edusperoni changed the title fix(runtime): keep isolate ids in the runtime registry refactor(runtime): a Runtime owns one isolate and publishes it from Init Oct 13, 2026
@adrian-niculescu

Copy link
Copy Markdown
Contributor

Glad to help

This branch has not been deployed

No deployments
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