Repository navigation
fix(sharedobjects): cap the JNI global refs shared objects hold - #40
Merged
Merged
Conversation
|
| Base | This PR | Change | |
|---|---|---|---|
| File size | 347,576 B (339.4 KiB) | 351,464 B (343.2 KiB) | +3,888 B (+1.12%) |
| Exported symbols | 20 | 20 | 0 |
Sections (7 changed)
| Section | Base | This PR | Change |
|---|---|---|---|
.text |
248,540 | 251,420 | +2,880 (+1.16%) |
.eh_frame |
34,168 | 34,724 | +556 (+1.63%) |
.rodata |
19,283 | 19,171 | -112 (-0.58%) |
.gcc_except_table |
16,444 | 16,920 | +476 (+2.89%) |
.eh_frame_hdr |
5,076 | 5,148 | +72 (+1.42%) |
.dynstr |
5,106 | 5,106 | 0 |
.data.rel.ro |
4,328 | 4,328 | 0 |
.dynsym |
3,480 | 3,480 | 0 |
(headers and padding) |
2,318 | 2,333 | +15 (+0.65%) |
.rela.dyn |
2,328 | 2,329 | +1 (+0.04%) |
.rela.plt |
2,088 | 2,088 | 0 |
.plt |
1,424 | 1,424 | 0 |
.got.plt |
720 | 720 | 0 |
.dynamic |
464 | 464 | 0 |
.data |
328 | 328 | 0 |
.gnu.version |
290 | 290 | 0 |
.got |
280 | 280 | 0 |
.shstrtab |
275 | 275 | 0 |
.comment |
204 | 204 | 0 |
.note.android.ident |
152 | 152 | 0 |
.gnu.hash |
148 | 148 | 0 |
.gnu.version_r |
64 | 64 | 0 |
.note.gnu.build-id |
36 | 36 | 0 |
.fini_array |
16 | 16 | 0 |
.init_array |
16 | 16 | 0 |
Kotlin code size (:react, dexed by D8, not shrunk)
No change
| Base | This PR | Change | |
|---|---|---|---|
| Dex size | 284,264 B (277.6 KiB) | 284,264 B (277.6 KiB) | 0 |
| Class files | 724,773 B (707.8 KiB) | 724,773 B (707.8 KiB) | 0 |
| Classes | 196 | 196 | 0 |
| Methods | 1,781 | 1,781 | 0 |
Dex sections (0 changed)
| Section | Base | This PR | Change |
|---|---|---|---|
string_data |
117,858 | 117,858 | 0 |
code |
70,722 | 70,722 | 0 |
annotations |
23,380 | 23,380 | 0 |
method_ids |
14,248 | 14,248 | 0 |
class_data |
9,975 | 9,975 | 0 |
string_ids |
9,852 | 9,852 | 0 |
proto_ids |
6,960 | 6,960 | 0 |
class_defs |
6,848 | 6,848 | 0 |
debug_info |
6,078 | 6,078 | 0 |
annotations_directories |
5,656 | 5,656 | 0 |
annotation_sets |
4,688 | 4,688 | 0 |
type_lists |
3,036 | 3,036 | 0 |
field_ids |
3,008 | 3,008 | 0 |
type_ids |
1,532 | 1,532 | 0 |
map_list |
208 | 208 | 0 |
header |
112 | 112 | 0 |
encoded_arrays |
103 | 103 | 0 |
Class files by package (0 changed)
| Package | Base | This PR | Change | Classes |
|---|---|---|---|---|
io.github.expo.modules.v2.converters |
213,544 | 213,544 | 0 | 41 |
io.github.expo.modules.v2.types |
123,662 | 123,662 | 0 | 43 |
io.github.expo.modules.v2.async |
58,113 | 58,113 | 0 | 16 |
io.github.expo.modules.v2.modules |
40,479 | 40,479 | 0 | 11 |
io.github.expo.modules.v2.jsi |
37,357 | 37,357 | 0 | 9 |
io.github.expo.modules.v2.records |
36,048 | 36,048 | 0 | 10 |
io.github.expo.modules.v2.args |
34,500 | 34,500 | 0 | 8 |
io.github.expo.modules.v2 |
31,576 | 31,576 | 0 | 19 |
io.github.expo.modules.v2.binary |
25,758 | 25,758 | 0 | 4 |
io.github.expo.modules.v2.sharedobjects |
22,116 | 22,116 | 0 | 4 |
io.github.expo.modules.v2.events |
21,878 | 21,878 | 0 | 4 |
io.github.expo.modules.v2.react |
18,981 | 18,981 | 0 | 6 |
io.github.expo.modules.v2.cache |
16,655 | 16,655 | 0 | 6 |
io.github.expo.modules.v2.records.writers |
16,026 | 16,026 | 0 | 3 |
io.github.expo.modules.v2.records.readers |
15,795 | 15,795 | 0 | 3 |
io.github.expo.modules.v2.logging |
4,635 | 4,635 | 0 | 4 |
io.github.expo.modules.v2.loader |
2,535 | 2,535 | 0 | 1 |
io.github.expo.modules.v2.errors |
2,101 | 2,101 | 0 | 1 |
io.github.expo.modules.v2.jni |
1,563 | 1,563 | 0 | 1 |
io.github.expo.modules.v2.core |
1,451 | 1,451 | 0 | 2 |
Base: main at 83ca7fa. This PR: bfe5bec, merged into the base.
lukmccall
force-pushed
the
perf/shared-object-global-ref-budget
branch
from
October 28, 2026 12:29
7de329b to
4788f8e
Compare
lukmccall
force-pushed
the
perf/shared-object-global-ref-budget
branch
from
October 28, 2026 13:27
c409b91 to
bfe5bec
Compare
This was referenced Oct 28, 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.
Why
Every shared object pinned its Kotlin instance with a JNI global ref of its own. A benchmark that runs
new SharedPoint(1.5, 2.5)in a loop aborted on Android withJNI ERROR (app bug): global reference table overflow (max=51200): 50,362 of the refs heldSharedPointV2. The limit covers the whole process, and Hermes finalizes facades late, so JavaScript can hold or leave behind far more shared objects than there is room for.What
New
objects/InstanceRefreplaces thekolibri::GlobalRefinObjectState:config::kSharedObjectGlobalRefs(1024, 2% of Android's limit) are held, a shared object gets a global ref of its own, exactly as before. A released or collected object gives its ref back, so the next object created gets one again.Object[]chunk. Each chunk holdsconfig::kInstanceSlotsPerChunk(1024) instances and costs one global ref. A call reads the instance out as a local ref throughScopedInstanceand deletes it after the call.InstanceRefpoints straight at its own chunk, so reading the instance looks nothing up. There is no table of chunks and no cap on how many objects can sit in slots.InstanceRef::globaland never count against the budget.The path for objects within the budget is unchanged:
invokeFunction,getPropertyandsetPropertyborrow the global ref and tail-call the invoker just as before. Reading a slot lives in out-of-line*InSlotfunctions. The compiled setter differs frommainby one predicted null check and one register save and restore.ObjectState::instance()is replaced byholdsInstance()andinstanceRef(). The callers outside the hot path (event emitter, listener table,JniEncoder,JavaScriptObject::nativeInstance) take a local ref throughnewLocal/ScopedInstanceinstead of using the raw global.The new settings sit in
Config.hnext to the other tuning constants, with astatic_asserton the chunk size.Benchmarks
Host JMH (HotSpot, host Hermes),
SharedObjectBenchmark, 3 forks × 5 iterations,mainand this branch alternated twice; ns/op:¹ Measured after rebasing onto
mainwith #38, which made creating an object cost about 250 ns more on both sides; the other rows were measured before the rebase.main.property-writeandpass-argcame out 2–4 ns higher in these runs, more than the extra null check explains, so it is probably code-layout noise.GetObjectArrayElementandDeleteLocalRef.constructrunsnew P(1.5, 2.5)in a loop, like the benchmark that crashed.*-slottedcases hold 2,048 objects first, so the measured receiver sits in a slot.None of this has been measured on a device or on ART.
Test plan
SharedObjectTestcase: JavaScript holds 40,000Spots.release()frees its slot for reuse.ExpoTestSupport.__instanceRefCounts()(globals, slots, chunks) and__holdsGlobalRef(obj)../gradlew :test-app:test: 226 tests, 0 failures./gradlew :api:test: 156 tests, 0 failures./gradlew ':react:buildCMakeDebug[arm64-v8a][expo-modules-v2]'SharedPointbenchmark on a device: it should no longer abort🤖 Generated with Claude Code