Skip to content

perf: guard ObjectTable with a monitor instead of StampedLock - #2103

Merged
edusperoni merged 2 commits into
mainfrom
perf/objecttable-synchronized
Nov 11, 2026
Merged

edusperoni merged 2 commits into
mainfrom
perf/objecttable-synchronized

Conversation

@edusperoni

@edusperoni edusperoni commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

Description

ObjectTable (added in #2099) guarded its tables with java.util.concurrent.locks.StampedLock: write locks for mutations, optimistic reads elsewhere. On device, StampedLock runs interpreted. It lives in the boot classpath (core-oj) but outside the boot image, and an app's baseline profile can't compile boot-classpath methods. A profile of NativeScript core's apps/automated suite showed StampedLock.tryWriteLock/casState/writeLock/unlockWriteInternal running in the interpreter on the main thread.

This replaces it with synchronized (this). An uncontended monitor enter/exit is compiled into every caller. Reads now take the lock too, so the torn-read handling the optimistic path needed (findIdUnlocked, getUnlocked) is gone. A second commit makes the table concurrent on every API level. It was limited to API 24+ only because StampedLock needs it. On API 21–23, argument objects were resolved natively under the isolate lock and new ids took a separate path; now every level registers through findOrRegister. findIdOptimistic is merged into findId, and getOrCreateObjectId takes one lock instead of a lookup followed by a registration.

JNI microbenchmarks (debug.ns.jnibench, optimized release, SM-G780G; main and this branch interleaved, 2 passes × 3 runs; median ns):

Benchmark main this PR
new java.lang.Object() 2763 1528 −45% (6/6 runs faster)
lifecycle new java.lang.Object() [alloc, kept alive] 2807 1520 −46%
lifecycle JniBench.newObjectInJava() [alloc, kept alive] 2015 1094 −46%
static.newObjectOfManyClasses() [16 classes] 1683 1097 −35%
static.newObjectInJava() [new wrapper] 1276 954 −25%
static.newInstance(extended class) [Java-created] 8974 7419 −17%
new Object.extend({})() 3975 3740 −6%
contention, 1 thread, no args 3608 2153 −40%
callback.noArgs() 479 436 −9%
contention, 2 threads, 3 new objects 6101 6305 +3% (1/6)
contention, 2 threads, 3 new objects of 16 classes 6295 6487 +3% (0/6)
contention, 4 threads, 3 existing objects 3343 3546 +6% (0/6)

The other rows are within ±2%. Even after the JIT has compiled it, StampedLock's write path was costly.

Known trade-off: when 2–4 Java threads call into JS at the same time, their object-id lookups used to be lock-free optimistic reads and now contend on the monitor: +3..+6% in those benchmarks. Apps run JS on one thread in the common case, where this is a clear win.

Real apps: no measurable difference on the template app's cold start (497/491 vs 503/494 ms) or on a 1000-tile flex-wrap mount. The phone was thermally throttling during that comparison, so those numbers only rule out a large effect.

Does your commit message include the wording below to reference a specific issue in this repo?

No issue.

Related Pull Requests

Does your pull request have unit tests?

Yes. The ObjectTable JVM test for concurrent readers and a writer now requires every read of a stable entry to hit; it used to allow optimistic misses. The test for disabled optimistic reads is gone with the flag. JVM tests: 29/29.

Full device suite:

  • API 36 emulator: 1551 specs, 0 failures.
  • API 23 emulator (arm64): 1551 specs, 3 failures, the same 3 that main fails on that emulator:
    • a proxy test that matches ByteBuffer.toString()'s newer cap= format;
    • two HTTP module-import tests that time out.

On API 23 the harness also reports "App has crashed - ErrorReportActivity" for both main and this PR. run-as ... ls exits 0 there even when the file is missing, so the harness's checks misfire; the results were read from the XML directly.

Summary by CodeRabbit

  • Bug Fixes
    • Improved the reliability of object lookups when multiple threads access the runtime, helping ensure stable objects return the correct identifiers.
    • Object registration, lookup, retrieval, and removal now use consistent synchronization, reducing the risk of inconsistent results during concurrent access.
    • Updated object argument handling to register objects without an existing identifier and correctly mark newly registered objects.

StampedLock lives in the boot classpath but outside the boot image, so on
device it runs interpreted, and an app's baseline profile can't compile it.
A monitor's uncontended enter/exit is compiled into every caller. Reads now
take the lock too, which also removes the torn-read handling the optimistic
path needed.
@coderabbitai

coderabbitai Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

Explain Change →

No actionable comments were generated in the recent review.

Walkthrough

ObjectTable removes StampedLock and optimistic reads. It synchronizes table reads, registration, and mutations on the table monitor. Runtime uses the synchronized lookup and registration methods, and concurrency tests require stable lookup results.

Changes: ObjectTable synchronization

Layer / File(s) Summary
Monitor-synchronized table access
test-app/runtime/src/main/java/com/tns/ObjectTable.java
ObjectTable removes StampedLock and optimistic lookup paths. It synchronizes lookup, registration, retrieval, containment, weakening, and removal on the table monitor. Batch weakening holds the monitor across the batch.
Runtime lookup and registration
test-app/runtime/src/main/java/com/tns/Runtime.java
Runtime uses findOrRegister for object ID lookup and registration, and uses findId for native dispatch. Argument packaging marks newly registered objects with FRESH_ARG.
Concurrent access tests
test-app/runtime/src/test/java/com/tns/ObjectTableTest.java
The concurrent-read test uses findId and requires the expected ID. Tests no longer configure optimistic reads or assert that optimistic reads return NO_ID.

Estimated code review effort: 2 (Simple) | ~15 minutes

Change: Refactor

Possibly related PRs

  • NativeScript/android#2099: Introduced ObjectTable and replaced the runtime's object maps; this PR changes ObjectTable's synchronization model.
Merge Risk: Minimal · up to fee28

The change replaces the StampedLock in ObjectTable with monitor synchronization to improve performance. No merge-blocking risk was found.

Pre-merge checks: Passed 4

Check name Status Explanation
Title check Passed The title clearly and concisely identifies the main change: replacing ObjectTable's StampedLock with a monitor.
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.
Finishing Touches

Generate docstrings

  • Commit to this branch
  • Create a new PR
Generate unit tests (beta)

  • 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

Recent review info

Run configuration

Commits

Reviewing files that changed from the base of the PR and between 7f7ad3a and fee28fb.

Files selected for processing: 3

  • test-app/runtime/src/main/java/com/tns/ObjectTable.java
  • test-app/runtime/src/main/java/com/tns/Runtime.java
  • test-app/runtime/src/test/java/com/tns/ObjectTableTest.java

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

ObjectTable was only concurrent where StampedLock exists (API 24+). With a
monitor there is no reason to keep API 21-23 on the slower paths: argument
objects resolved natively under the isolate lock, and new ids registered
through a separate path. findIdOptimistic is now findId, and
getOrCreateObjectId and packageArgs register through findOrRegister alone.
@edusperoni
edusperoni merged commit 66c5344 into main Nov 11, 2026
8 of 20 checks passed
@edusperoni
edusperoni deleted the perf/objecttable-synchronized branch November 11, 2026 20:15
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