Skip to content

fix(android): release Java-returned wrappers when JavaScript drops them - #127

Open
ammarahm-ed wants to merge 1 commit into
perf/jni-07-wrapper-prototypefrom
perf/jni-08-weak-wrappers
Open

ammarahm-ed wants to merge 1 commit into
perf/jni-07-wrapper-prototypefrom
perf/jni-08-weak-wrappers

Conversation

@ammarahm-ed

@ammarahm-ed ammarahm-ed commented Nov 9, 2026 •

Copy link
Copy Markdown
Collaborator

Every Java object returned to JS stayed alive for the life of the runtime. Its wrapper was held by a strong reference in m_idToObject, and a host-object proxy created for the wrapper pinned it a second time, so returning a lot of objects grew the process until Android killed it. This PR makes the wrapper the only thing that keeps a returned object alive, and ties the Java side to the wrapper's lifetime. It changes the Node-API and the direct runtime the same way.

  • Weak wrappers. A wrapper for a Java-returned object is held by a weak reference. Instances constructed from JS (extend, interface implementations) stay strong, because Java may call back into them at any time.
  • No proxy for plain instances. Only Java arrays keep a host-object proxy, for indexed access. Any other returned object is the wrapper itself, and an object that already has a proxy still returns that proxy so identity is preserved.
  • Native pressure. Each weak wrapper reports about 1 KB to the engine (js_adjust_external_memory / EngineHost::AdjustExternalMemory), so the engine schedules a GC under native pressure instead of only when its own heap grows.
  • Release on collection. When the engine collects a wrapper, its finalizer queues the release to the post-GC drain, which marks the Java instance weak, drops the id entry and returns the memory credit. In the direct runtime the wrapper's native state destructor does the queueing, through an OwnerToken that OnDisposeRuntime clears, because the engine can destroy native state after the manager is gone.
  • Recreated wrappers. Java reuses the id of an instance it has made weak, so EnsureInstanceStrong moves it back to the strong table whenever a wrapper is (re)created. If a new wrapper was linked under an id between the collection and the drain, the drain leaves that id alone.

Results

V8 release build on a Sony SO-01M (Android 11, arm64), AOT-compiled with cmd package compile -m speed. One warm-up launch is discarded, then 5 launches; values are the median ns per call.

Case Node-API before after Δ Direct before after Δ
Return a new object 28,339 12,744 -55% 19,349 12,359 -36%
Call a method on a new object 35,895 19,368 -46% 28,239 19,891 -30%
Return a Date Object 28,710 13,411 -53% 20,413 13,814 -32%
Return this 1,338 1,261 -6% 1,312 1,354 +3%
Return an Int Array 27,547 27,918 +1% 18,232 20,068 +10%
Native heap after run (MB) 268 MB 106 MB -60% 173 MB 97 MB -44%

The heap row is the native heap after one pass of the suite, which returns about 200k objects. Rows this change does not touch moved by up to ±17%. Invoke string callback on the direct runtime is bimodal in this build (7.9k to 11.8k ns across the five runs, against 7.8k to 8.4k before); it does not go through this code for its arguments, and the Node-API runtime is flat on it, so I am treating it as noise, but I have not isolated it.

All cases
Case Node-API before after Δ Direct before after Δ
Void Method on instance 631 662 +5% 584 596 +2%
Field on instance 807 804 -0% 894 835 -7%
Int Field on instance 365 377 +3% 395 386 -2%
Get Static Field 836 813 -3% 856 850 -1%
Void Static Method 309 292 -6% 299 313 +5%
multiply(a, b) 478 486 +2% 514 520 +1%
Pass and return string 1,154 1,218 +6% 1,164 1,202 +3%
Return a String 821 836 +2% 746 760 +2%
Return an Int 330 315 -5% 306 315 +3%
Pass a String 660 700 +6% 654 687 +5%
Return a Boolean 316 313 -1% 296 313 +6%
Return a Double 392 399 +2% 388 395 +2%
Pass an Int 393 415 +6% 407 420 +3%
Pass a Double 389 397 +2% 412 425 +3%
Pass a Boolean 476 480 +1% 432 424 -2%
Return an Int Array 27,547 27,918 +1% 18,232 20,068 +10%
Pass an Int Array 3,051 3,264 +7% 3,983 4,135 +4%
Return a String Array 27,902 29,130 +4% 20,155 21,210 +5%
Pass a String Array 5,354 5,661 +6% 6,641 6,846 +3%
Return a Double Array 26,756 27,120 +1% 19,726 20,490 +4%
Pass a Double Array 3,049 3,134 +3% 3,901 3,970 +2%
Return a Boolean Array 25,451 26,139 +3% 18,996 19,611 +3%
Pass a Boolean Array 2,870 2,980 +4% 3,660 3,812 +4%
Int Array[0] read 348 361 +4% 245 240 -2%
String Array[0] read 777 784 +1% 671 666 -1%
Double Array[0] read 465 426 -8% 336 342 +2%
Boolean Array[0] read 339 350 +3% 234 234 +0%
Int Array[0] write 476 511 +7% 360 364 +1%
String Array[0] write 1,163 1,220 +5% 1,035 1,072 +4%
Double Array[0] write 468 546 +17% 374 373 -0%
Boolean Array[0] write 464 513 +10% 369 367 -1%
Return a Date Object 28,710 13,411 -53% 20,413 13,814 -32%
Pass a Date Object 3,853 3,688 -4% 3,656 3,749 +3%
Invoke void callback 7,647 7,358 -4% 7,214 6,899 -4%
Instance add(a, b) 840 832 -1% 806 796 -1%
Pass and return non-ASCII string 1,161 1,252 +8% 1,189 1,190 +0%
Pass and return 1KB string 5,166 5,040 -2% 5,064 5,114 +1%
Return a Char 351 343 -2% 341 350 +3%
Return this 1,338 1,261 -6% 1,312 1,354 +3%
Return a new object 28,339 12,744 -55% 19,349 12,359 -36%
Call a method on a new object 35,895 19,368 -46% 28,239 19,891 -30%
Pass a 16 Int Array 4,138 4,668 +13% 4,935 5,262 +7%
Pass a 16 Double Array 4,803 5,012 +4% 5,761 5,931 +3%
Invoke int callback 10,298 10,765 +5% 12,617 10,634 -16%
Invoke string callback 8,837 8,908 +1% 8,125 11,360 +40%
Native heap after run (MB) 268 MB 106 MB -60% 173 MB 97 MB -44%

Only V8 was built and run.

Jasmine suite

Run once on a debug build of the top of this stack (this PR, the bulk array reads in the PR above, and the crash-handler PR), on the same device:

main stack top
Node-API 523 specs, 0 failures 522 specs, 0 failures
Direct 451 specs, 1 failure (triggers interval, a timing spec) 450 specs, 0 failures

The spec count is one lower on the stack top because the crash-handler PR removes a disabled spec. This PR was not run on its own.

@coderabbitai

coderabbitai Bot commented Nov 9, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: e52267ab-e946-4c23-8459-6a7d82c111fc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • 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

@ammarahm-ed
ammarahm-ed added this pull request to stack #121 November 9, 2026 06:59

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.

1 participant