GeezerVison is showing github.com — feat(runtime): report native memory held by wrappers to V8, forward memory pressure by edusperoni · Pull Request #507 · NativeScript/ios
Conversation
edusperoni
commented
Oct 10, 2026
•
edited by coderabbitai
Bot
Loading
Uh oh!
There was an error while loading. Please reload this page.
What
The runtime never told V8 about native memory kept alive by JS wrappers. V8 only weighs its own heap (plus ArrayBuffer backing stores, which it counts itself) when scheduling GCs. So a wrapper holding a multi-MB UIImage looks like ~50 bytes and can sit uncollected until iOS jetsams the app. This PR adds reporting at three levels of confidence and forwards OS memory pressure.
Accounting primitive
ExternalMemoryCharge (wrapping v8::ExternalMemoryAccounter, the non-deprecated replacement for AdjustAmountOfExternalAllocatedMemory) hangs off BaseDataWrapper. Whichever path deletes the wrapper (finalizer, __releaseNativeCounterpart, teardown, an adapter's -dealloc on another thread) returns the charge.
- Release is thread-safe: V8's decrease is an atomic counter update that V8 itself calls from GC worker threads. The isolate's gate is pinned for the call. Once the gate is closed the decrease is skipped, because disposal drops the isolate's total anyway.
Tier 1: exact sizes the runtime allocates
interop.alloc(n) charges n bytes to the adopted pointer.
Tier 2: estimated sizes for objects JS likely owns
- Estimators:
NSData → length; UIImage / CGImage → bytesPerRow * height; CVPixelBuffer → CVPixelBufferGetDataSize. They are resolved once per class and cached in Caches.
- CoreGraphics and CoreVideo functions are resolved with
dlsym, so the runtime gains no link dependencies.
NSDataAdapter is excluded, since V8 already counts its JS buffer.
- Symbol images are skipped, since
-CGImage rasterizes them.
- Applied only when JS most likely holds the last reference:
new X(...)
- initializers
- +1 returns (
copy / new / Create)
- class factory methods
- Instance getters (
view.image) are not charged, because the receiver usually keeps the object alive and the GCs triggered by charging it could not free anything.
Tier 3: library-declared sizes
interop.setExternalSize(obj, bytes) replaces the charge on a native object, pointer, reference, block or function reference; 0 clears it. interop.getExternalSize(obj) reads it back.
- Class, protocol and type objects throw
TypeError, because they live as long as the isolate and the charge could never be returned.
- Struct objects throw
TypeError too. Since perf(runtime): build struct objects from per-type templates with GC-owned storage #499 their wrapper is destroyed by the storage's backing-store deleter, which may run off-thread and must not touch V8, and V8 already counts that storage.
- Byte counts must be in
[0, 2^34], otherwise RangeError. V8 aborts the process, rather than throwing, on a single adjustment above its sanity limit.
- This is the hook for
@nativescript/core ImageSource, canvas and plugins, which know their real footprint.
Memory pressure
- A process-wide
DISPATCH_SOURCE_TYPE_MEMORYPRESSURE source maps normal/warn/critical to MemoryPressureLevel::kNone/kModerate/kCritical and calls Isolate::MemoryPressureNotification on every live isolate.
- The notification runs outside the isolate registry lock, with each isolate's gate pinned. Off-thread, V8 requests a GC interrupt and posts a task to the isolate's runner.
Known limitations
- Estimates are taken once, at wrap time; later
NSMutableData growth is not tracked.
- A decoded-bitmap estimate for an image loaded from a file overstates its memory until it is drawn.
interop.bufferFromData on a charged NSData counts the bytes twice: once for the NSData wrapper, once for the ArrayBuffer.
interop.setExternalSize / getExternalSize typings belong in @nativescript/types and are a follow-up.
Tests
TestRunner/app/tests/ExternalMemoryTests.js (12 specs) covers:
- setting, replacing and clearing charges
- rejecting invalid targets and byte counts
- release through GC and explicit release
interop.alloc
- the factory, constructor, +1 and getter paths
- CGImage/UIImage bitmap estimates
Rebased on #499. Full suite under ASan on a dedicated simulator: 1835 specs, 0 failures, no sanitizer reports. The memory-pressure path has no automated test because the simulator has no reliable way to raise a memory-pressure event.
Summary by CodeRabbit
-
New Features
- Added external-memory accounting for native objects and allocations, including estimated sizes for supported data and image objects.
- Added APIs to set, retrieve, and clear memory charges for supported native wrappers.
- Added system memory-pressure monitoring and notifications across runtime isolates.
-
Tests
- Added coverage for memory charges, allocation accounting, estimated object sizes, invalid inputs, and charge release.
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
coderabbitai
Bot
commented
Oct 10, 2026
•
edited
Loading
Uh oh!
There was an error while loading. Please reload this page.
|

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:
21395424-5bda-426e-8475-01a9df11f1ea
📥 Commits
-
Reviewing files that changed from the base of the PR and between caaee30 and 061eae8.
📒 Files selected for processing (8)
-
NativeScript/runtime/ArgConverter.mm
NativeScript/runtime/Caches.h
NativeScript/runtime/Interop.h
NativeScript/runtime/Interop.mm
NativeScript/runtime/InteropTypes.mm
NativeScript/runtime/Runtime.mm
TestRunner/app/tests/ExternalMemoryTests.js
TestRunner/app/tests/index.js
Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.
📝 Walkthrough
-
Walkthrough
The runtime adds external-memory accounting for native wrappers, APIs to set and read wrapper charges, estimates for supported native objects, and memory-pressure notification forwarding to live isolates.
Changes
External-Memory Accounting
| Layer / File(s) |
Summary |
Charge model and native size estimators NativeScript/runtime/DataWrapper.h, NativeScript/runtime/Caches.h, NativeScript/runtime/ExternalMemory.h, NativeScript/runtime/ExternalMemory.mm, v8ios.xcodeproj/project.pbxproj |
Wrappers can own external-memory charges. The runtime adjusts charges against V8 accounting and caches estimators for supported Objective-C and CF objects. The Xcode project includes the new implementation and header. |
Interop APIs and automatic charges NativeScript/runtime/ArgConverter.mm, NativeScript/runtime/Interop.h, NativeScript/runtime/Interop.mm, NativeScript/runtime/InteropTypes.mm, TestRunner/app/tests/ExternalMemoryTests.js, TestRunner/app/tests/index.js |
Interop adds APIs to set and read wrapper charges. Allocations and selected native-object return paths record sizes. Tests cover charge validation, allocation charges, object estimates, and image estimates. |
Memory-pressure notifications NativeScript/runtime/ExternalMemory.mm, NativeScript/runtime/Runtime.h, NativeScript/runtime/Runtime.mm |
Runtime initialization starts memory-pressure monitoring. Notifications map system statuses to V8 pressure levels and are forwarded to live isolates whose gates can be pinned. |
Priority: ➖ Normal
Estimated code review effort: 3 (Moderate) | ~25 minutes
Change: Feature
Sequence Diagram(s)
sequenceDiagram
participant Monitor as Dispatch memory-pressure monitor
participant Runtime as Runtime::NotifyMemoryPressure
participant Isolates as Live V8 isolates
Monitor->>Runtime: forward mapped pressure level
Runtime->>Isolates: notify isolates with pinnable gates
Loading
Suggested reviewers: nathanwalker
Merge Risk: ⚪ Minimal · up to 061ea
This change adds native-memory accounting for wrappers and forwards memory-pressure events to running isolates. No concrete defect was confirmed. A possible edge case remains unconfirmed: negative or failed interop.alloc sizes may be reported to the memory accounting. The owner may want to add input validation there as a follow-up.
Pre-merge checks · 4 · 1
-
Failed checks (1 warning)
| Check name |
Status |
Explanation |
Resolution |
| Docstring Coverage |
 |
Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 14 functions across 7 files. (4 skipped: 4… |
Write docstrings for the functions missing them to satisfy the coverage threshold. |
Passed checks (4 passed)
-
| Check name |
Status |
Explanation |
| Linked Issues check |
 |
Check skipped because no linked issues were found for this pull request. |
| Out of Scope Changes check |
 |
Check skipped because no linked issues were found for this pull request. |
| Description Check |
 |
Check skipped - CodeRabbit’s high-level summary is enabled. |
| Title check |
 |
The title clearly summarizes the main changes: reporting native memory held by wrappers to V8 and forwarding memory-pressure notifications. |
Full details: Docstring Coverage
-
Explanation
Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 14 functions across 7 files. (4 skipped: 4 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 counts the bytes with care, Native wrappers join the tally there. A pointer gets its measured share, V8 updates what it can bear. When pressure calls, the isolates hear, And hops the rabbit, pleased and clear.
Comment @coderabbitai help to get the list of available commands.
|
Sorry, something went wrong.
Uh oh!
There was an error while loading. Please reload this page.
…emory pressure
V8 only weighs its own heap (plus ArrayBuffer backing stores) when scheduling
collections, so a JS wrapper holding megabytes of native memory looks like a
few dozen bytes and can sit uncollected until the process is jetsammed.
- Wrappers can carry an ExternalMemoryCharge (a v8::ExternalMemoryAccounter)
that is returned whenever the wrapper is deleted, from any thread; once the
isolate's gate is closed the decrease is skipped since disposal drops it.
- interop.alloc(n) charges n bytes to the returned pointer.
- Objects JS likely owns (JS constructors, initializers, +1 returns, class
factory methods) get an estimated size: NSData length, UIImage/CGImage
bitmap size, CVPixelBuffer data size. CG/CV are resolved at runtime, so no
new link dependencies. NSDataAdapter is excluded (V8 already counts it).
- interop.setExternalSize(obj, bytes) / interop.getExternalSize(obj) let
libraries declare the native footprint of what they wrap.
- A process-wide dispatch memory-pressure source forwards normal/warn/critical
to every live isolate via Isolate::MemoryPressureNotification.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Assignees
No one assigned
Milestone
No milestone
Development
Successfully merging this pull request may close these issues.
Uh oh!
There was an error while loading. Please reload this page.
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.
|