Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
c471ec7
test: benchmark Java threads calling into JS concurrently
edusperoni Nov 8, 2026
8694f15
perf: keep Java object ids in one table touched only under the isolat…
edusperoni Nov 8, 2026
8e830f8
perf: resolve known objects before taking the isolate lock
edusperoni Nov 8, 2026
f5060ce
perf: settle collected JS wrappers in batches instead of one JNI call…
edusperoni Nov 8, 2026
1301088
perf: settle collected wrappers at the next JS/Java crossing
edusperoni Nov 8, 2026
718783b
test: benchmark new objects of one class against many classes
edusperoni Nov 9, 2026
5e166e9
perf: look up a wrapped object's class by index instead of by name
edusperoni Nov 9, 2026
5eb7b3c
perf: register new argument objects before taking the isolate lock
edusperoni Nov 9, 2026
82ad1bf
test: let the benchmark filter name several rows
edusperoni Nov 9, 2026
71702e0
perf: settle a finalizer batch under one write lock, and after each call
edusperoni Nov 9, 2026
0579b0b
perf: keep the next Java caller awake while another holds the isolate
edusperoni Nov 9, 2026
3dc8cc4
test: alternate admission spin variants within one benchmark launch
edusperoni Nov 9, 2026
bbc2e8b
perf: skip the unconverted-argument cleanup for calls without arguments
edusperoni Nov 9, 2026
e1d81c3
perf: read a callback's argument types and values in one JNI call
edusperoni Nov 9, 2026
bd08716
perf: register a constructed object and read Java heap usage in one u…
edusperoni Nov 9, 2026
478e213
perf: replace a package child's getter with its value on first read
edusperoni Nov 9, 2026
f4cbece
perf: look up resolved signatures without allocating
edusperoni Nov 9, 2026
2a07cb7
perf: internalize the runtime's cached property-name strings
edusperoni Nov 9, 2026
61fcbe2
perf: create a Java object's JS wrapper without a constructor call or…
edusperoni Nov 9, 2026
079ae43
perf: read each thread's cached env and runtime with one TLS lookup
edusperoni Nov 9, 2026
7908de0
test: benchmark the first access of large framework classes
edusperoni Nov 9, 2026
9eba5c9
test: benchmark static field reads and writes on a small and a huge c…
edusperoni Nov 9, 2026
706c012
perf: install a large class's static fields without a getter and sett…
edusperoni Nov 9, 2026
cb7022a
perf: build a large package's object in dictionary mode
edusperoni Nov 9, 2026
7c79224
perf: create member names internalized, straight from the metadata
edusperoni Nov 9, 2026
85a82dc
perf: find a method's base-class counterpart by hash, not by a scan
edusperoni Nov 9, 2026
7a16959
perf: create Java methods and accessors as non-constructors
edusperoni Nov 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 195 additions & 7 deletions test-app/app/src/main/assets/app/tests/benchmarks/jniBenchmarks.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,14 @@
// the median and minimum ns/op. Java -> JS benchmarks are timed in Java with System.nanoTime.

var JniBench = com.tns.tests.JniBench;
var ROUNDS = 7;
var ROUNDS = com.tns.tests.JniBenchConfig.getRounds() || 7;
var FILTER = String(JniBench.getFilter());
var VARIANTS = String(com.tns.tests.JniBenchConfig.getVariants()).split(",").filter(function (v) { return v !== ""; }).map(Number);
// Several names may be given, separated by "|"; a benchmark runs when its name contains any.
var FILTERS = FILTER ? FILTER.split("|") : [];
function skipped(name) {
return FILTERS.length > 0 && !FILTERS.some(function (f) { return name.indexOf(f) >= 0; });
}
var SCALE = JniBench.getScale();

function median(values) {
Expand All @@ -27,7 +33,7 @@ function report(name, nsPerOp) {

// body(n) runs the operation n times; returning a value keeps V8 from dropping the loop.
function measureJs(name, iterations, body) {
if (FILTER && name.indexOf(FILTER) < 0) return null;
if (skipped(name)) return null;
iterations *= SCALE;
var sink = body(Math.max(1000, iterations >> 2));
var samples = [];
Expand All @@ -42,7 +48,7 @@ function measureJs(name, iterations, body) {
}

function measureCallback(name, cb, kind, iterations) {
if (FILTER && name.indexOf(FILTER) < 0) return null;
if (skipped(name)) return null;
iterations *= SCALE;
JniBench.runCallbacks(cb, kind, Math.max(1000, iterations >> 2));
var samples = [];
Expand All @@ -58,7 +64,7 @@ function measureCallback(name, cb, kind, iterations) {
// V8 GC (wrapper finalization and the Java-side strong -> weak bookkeeping), and the
// Java GC that follows. Each phase is reported per object.
function measureLifecycle(name, count, create) {
if (FILTER && name.indexOf(FILTER) < 0) return null;
if (skipped(name)) return null;
count *= SCALE;
var javaRuntime = java.lang.Runtime.getRuntime();
var alloc = [], release = [], javaGc = [];
Expand Down Expand Up @@ -88,6 +94,56 @@ function measureLifecycle(name, count, create) {
}



// Aggregate ns per call while `threads` Java threads call into JS at once. Runs
// asynchronously: the JS thread must go idle and release the isolate for the
// callers to get in, so it polls for the result.
function measureContention(name, sink, threads, calls, mode, done) {
if (skipped(name)) {
done(null);
return;
}
var Bench = com.tns.tests.ContentionBench;
calls *= SCALE;
// Variants alternate round by round inside one launch, so a change in the device's
// performance state between launches cannot favour one of them.
var variants = VARIANTS.length > 0 ? VARIANTS : [null];
var samples = variants.map(function () { return []; });
var round = -variants.length;
var total = ROUNDS * variants.length;
function variantOf(r) {
return ((r % variants.length) + variants.length) % variants.length;
}
function startRound() {
var spin = variants[variantOf(round)];
if (spin !== null) {
com.tns.Runtime.setAdmissionSpinMicros(spin);
}
Bench.start(sink, threads, calls, mode);
setTimeout(poll, 5);
}
function poll() {
var nanos = Number(Bench.result());
if (nanos < 0) {
setTimeout(poll, 5);
return;
}
if (round >= 0) {
samples[variantOf(round)].push(nanos / (threads * calls));
}
round++;
if (round < total) {
gc();
setTimeout(startRound, 20);
} else {
done(variants.map(function (spin, i) {
return report(spin === null ? name : name + " [spin=" + spin + "]", samples[i]);
}));
}
}
startRound();
}

function buildBenchmarks() {
var bench = new JniBench();
var other = bench.getOther();
Expand Down Expand Up @@ -148,6 +204,22 @@ function buildBenchmarks() {
return n;
});
},
function () {
if (skipped("instance.getAlpha() [android.view.View]")) return null;
var view = new android.view.View(com.tns.NativeScriptApplication.getInstance());
return measureJs("instance.getAlpha() [android.view.View]", 50000, function (n) {
var s = 0;
for (var i = 0; i < n; i++) s = view.getAlpha();
return s;
});
},
function () {
return measureJs("package member read android.view.View", 200000, function (n) {
var s = null;
for (var i = 0; i < n; i++) s = android.view.View;
return s;
});
},
function () {
return measureJs("instance.getInt()", 50000, function (n) {
var s = 0;
Expand Down Expand Up @@ -231,6 +303,13 @@ function buildBenchmarks() {
return s;
});
},
function () {
return measureJs("static.newObjectOfManyClasses() [16 classes]", 20000, function (n) {
var s;
for (var i = 0; i < n; i++) s = JniBench.newObjectOfManyClasses();
return s;
});
},
function () {
return measureJs("static.newObjectInJava() [new wrapper]", 20000, function (n) {
var s;
Expand All @@ -252,6 +331,37 @@ function buildBenchmarks() {
return s;
});
},
function () {
return measureJs("static field get int [small class]", 50000, function (n) {
var Fields = com.tns.tests.StaticFieldBench;
var s = 0;
for (var i = 0; i < n; i++) s = Fields.counter;
return s;
});
},
function () {
return measureJs("static field get final int [small class]", 50000, function (n) {
var Fields = com.tns.tests.StaticFieldBench;
var s = 0;
for (var i = 0; i < n; i++) s = Fields.CONSTANT;
return s;
});
},
function () {
return measureJs("static field set int [small class]", 50000, function (n) {
var Fields = com.tns.tests.StaticFieldBench;
for (var i = 0; i < n; i++) Fields.counter = i;
return n;
});
},
function () {
return measureJs("static field get final int [android.R.id]", 50000, function (n) {
var ids = android.R.id;
var s = 0;
for (var i = 0; i < n; i++) s = ids.content;
return s;
});
},
function () {
return measureJs("field.get int", 50000, function (n) {
var s = 0;
Expand Down Expand Up @@ -295,7 +405,7 @@ function buildBenchmarks() {
});
},
function () {
if (FILTER && "instance.self() [50k live wrappers]".indexOf(FILTER) < 0) return null;
if (skipped("instance.self() [50k live wrappers]")) return null;
var live = new Array(50000);
for (var i = 0; i < live.length; i++) live[i] = new java.lang.Object();
// Pinned from a global: an optimized frame may drop a local after its last use.
Expand All @@ -322,24 +432,102 @@ function buildBenchmarks() {
},
function () {
return measureCallback("callback.oneObject(Object)", callback, 4, 20000);
},
function () {
return measureCallback("callback.oneObject(new Object)", callback, 5, 20000);
},
function () {
return measureCallback("callback.oneObject(new, 16 classes)", callback, 6, 20000);
}
];
}

function buildContentionBenchmarks() {
var Bench = com.tns.tests.ContentionBench;
var sink = new Bench.Sink({
call0: function () { },
call3: function (a, b, c) { }
});
var list = [];
[1, 2, 4].forEach(function (threads) {
[["no args", Bench.NO_ARGS], ["3 existing objects", Bench.EXISTING_OBJECTS], ["3 new objects", Bench.NEW_OBJECTS], ["3 new objects of 16 classes", Bench.NEW_OBJECTS_MANY_CLASSES]].forEach(function (mode) {
list.push(function (done) {
measureContention("contention " + threads + " thread(s), " + mode[0], sink, threads, 20000, mode[1], done);
});
});
});
return list;
}

// The first touch of a class builds its JS constructor and installs every static member, once
// per runtime, so each row is a single sample and must run before anything else touches the class.
function measureFirstAccess() {
var rows = [
["first access android.R", function () { return android.R; }],
["first access android.R.attr", function () { return android.R.attr; }],
["first access android.R.id", function () { return android.R.id; }],
["first access android.R.style", function () { return android.R.style; }],
["first access android.R.drawable", function () { return android.R.drawable; }],
["first access android.R.string", function () { return android.R.string; }],
["first read android.R.id.content", function () { return android.R.id.content; }],
["first read android.R.id.text1", function () { return android.R.id.text1; }],
["first read android.R.attr.textColor", function () { return android.R.attr.textColor; }],
["first read android.R.style.Theme", function () { return android.R.style.Theme; }],
["first access android.view [package]", function () { return android.view; }],
["first access android.view.View", function () { return android.view.View; }],
["first access android.widget [package]", function () { return android.widget; }],
["first access android.widget.TextView", function () { return android.widget.TextView; }],
["first access android.content.Context", function () { return android.content.Context; }],
["first access android.app.Activity", function () { return android.app.Activity; }]
];
var results = [];
rows.forEach(function (row) {
if (skipped(row[0])) return;
gc();
var start = performance.now();
globalThis.__jniBenchSink = row[1]();
results.push(report(row[0], [(performance.now() - start) * 1e6]));
});
var name = "first calls of 20 android.view.View methods";
if (!skipped(name)) {
var view = new android.view.View(com.tns.NativeScriptApplication.getInstance());
gc();
var start = performance.now();
globalThis.__jniBenchSink = [view.getAlpha(), view.getRotation(), view.getX(), view.getY(), view.getId(),
view.getWidth(), view.getHeight(), view.getLeft(), view.getTop(), view.getRight(),
view.getBottom(), view.getPaddingLeft(), view.getPaddingTop(), view.getScaleX(), view.getScaleY(),
view.getTranslationX(), view.getTranslationY(), view.getVisibility(), view.isEnabled(), view.isFocused()];
results.push(report(name, [(performance.now() - start) * 1e6]));
}
return results;
}

exports.run = function () {
var results = measureFirstAccess();
var benchmarks = buildBenchmarks();
var results = [];
var index = 0;
// Yield to the looper between benchmarks so GC and queued work run outside timed rounds.
function next() {
if (index >= benchmarks.length) {
console.log("JNIBENCH_DONE " + JSON.stringify(results));
runContention();
return;
}
gc();
var result = benchmarks[index++]();
if (result) results.push(result);
setTimeout(next, 50);
}
var contention = buildContentionBenchmarks();
var contentionIndex = 0;
function runContention() {
if (contentionIndex >= contention.length) {
console.log("JNIBENCH_DONE " + JSON.stringify(results));
return;
}
contention[contentionIndex++](function (result) {
if (result) results.push.apply(results, [].concat(result));
setTimeout(runContention, 50);
});
}
setTimeout(next, 500);
};
86 changes: 86 additions & 0 deletions test-app/app/src/main/assets/app/tests/testGC.js
Original file line number Diff line number Diff line change
Expand Up @@ -314,4 +314,90 @@ describe("Tests garbage collection", function () {

test.postCallback();
});
describe("collected wrappers waiting for a flush", function () {
var Holder = com.tns.tests.WrapperHolder;

function collectJava() {
for (var i = 0; i < 3; i++) {
java.lang.Runtime.getRuntime().gc();
java.lang.System.runFinalization();
}
}

afterEach(function () {
Holder.held = null;
});

it("keep the Java object of a wrapper handed back to JS before the flush", function () {
(function () {
Holder.held = new java.lang.StringBuilder("reacquired");
})();
gc();
var again = Holder.held;
Holder.held = null;
collectJava();

expect(again.toString()).toBe("reacquired");
});

it("keep a wrapper handed back to JS while its liveness check is queued", function () {
(function () {
Holder.held = new java.lang.StringBuilder("checked");
})();
gc();
collectJava();
gc();
var again = Holder.held;
collectJava();

expect(again.toString()).toBe("checked");
again = null;
gc();
collectJava();
});

it("release an object Java registered for a call that failed before reaching it", function () {
// Not a function: the call fails before its argument is converted.
var target = new com.tns.tests.FreshArgs.Target({ take: 42 });
var ref = com.tns.tests.FreshArgs.callWithNewObject(target);
collectJava();

expect(ref.get()).toBe(null);
});

it("give threads passing the same new object at once one wrapper", function (done) {
var FreshArgs = com.tns.tests.FreshArgs;
var received = [];
var target = new FreshArgs.Target({
take: function (o) {
received.push(o);
}
});
FreshArgs.startFromThreadsAtOnce(target, 4);
(function poll() {
if (FreshArgs.finishedCalls() < 4) {
setTimeout(poll, 10);
return;
}
expect(received.length).toBe(4);
for (var i = 1; i < received.length; i++) {
expect(received[i] === received[0]).toBe(true);
}
done();
})();
});

it("let Java reclaim objects during a synchronous loop that never yields", function () {
// Far more than the Java heap: every buffer must become collectable while the loop runs.
var total = 0;
for (var i = 0; i < 160; i++) {
var stream = new java.io.ByteArrayOutputStream(4 * 1024 * 1024);
total += stream.size() + 1;
if (i % 8 === 7) {
gc();
}
}
expect(total).toBe(160);
});
});
});
Loading
Loading