Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 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
8da7000
perf: keep each callback method name's V8 string instead of rebuildin…
edusperoni Nov 10, 2026
32c17d1
perf: let a binding carry its JS object id, and look runtimes up by i…
edusperoni Nov 10, 2026
1de228f
perf: keep each identity-index entry's hash so a lookup miss stays in…
edusperoni Nov 10, 2026
78f344d
perf: cache a wrapper class's MetadataNode and allocate a link's hand…
edusperoni Nov 10, 2026
b63cc51
perf: resolve a callback's receiver wrapper with one map lookup
edusperoni Nov 10, 2026
69efd2c
test: time a reference callback alongside every callback benchmark round
edusperoni Nov 10, 2026
f1f3669
perf: keep a linked object's state in one allocation, indexed by id
edusperoni Nov 10, 2026
926f3d1
perf: build Java object wrappers from the class's instance template
edusperoni Nov 10, 2026
6feaa50
perf: index ObjectTable's entries directly by id
edusperoni Nov 10, 2026
350ddd5
build: bind calls between the runtime's own functions directly
edusperoni Nov 10, 2026
cde9b0f
fix: address review findings on the object lifecycle changes
edusperoni Nov 10, 2026
a5c973b
fix: let only the instance JS is constructing take its object id
edusperoni Nov 10, 2026
a85fa88
perf: pass a binding argument's own object id to a callback
edusperoni Nov 10, 2026
c118b09
perf: build wrappers of Java-created extended instances from their te…
edusperoni Nov 10, 2026
a97a4e9
fix: keep a Java-created extended instance on its class's current pro…
edusperoni Nov 11, 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
238 changes: 229 additions & 9 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,28 +6,38 @@
// 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) {
var sorted = values.slice().sort(function (a, b) { return a - b; });
return sorted[Math.floor(sorted.length / 2)];
}

function report(name, nsPerOp) {
function report(name, nsPerOp, refNsPerOp) {
var result = {
name: name,
median: Math.round(median(nsPerOp) * 10) / 10,
min: Math.round(Math.min.apply(null, nsPerOp) * 10) / 10
};
if (refNsPerOp) {
result.ref = Math.round(median(refNsPerOp) * 10) / 10;
result.refMin = Math.round(Math.min.apply(null, refNsPerOp) * 10) / 10;
}
console.log("JNIBENCH " + JSON.stringify(result));
return result;
}

// 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 @@ -41,24 +51,38 @@ function measureJs(name, iterations, body) {
return report(name, samples);
}

// Callback rows drift with the device's performance state independently of the other rows,
// so each round also times a fixed reference callback (twoInts on its own object). Compare
// launches by median / ref rather than by median alone.
var REF_CALLBACK_KIND = 1;
var REF_CALLBACK_ITERATIONS = 20000;
var refCallback = null;

function measureCallback(name, cb, kind, iterations) {
if (FILTER && name.indexOf(FILTER) < 0) return null;
if (skipped(name)) return null;
iterations *= SCALE;
var refIterations = REF_CALLBACK_ITERATIONS * SCALE;
if (refCallback === null) {
refCallback = new JniBench.Callback({ twoInts: function (a, b) { } });
}
JniBench.runCallbacks(refCallback, REF_CALLBACK_KIND, refIterations >> 2);
JniBench.runCallbacks(cb, kind, Math.max(1000, iterations >> 2));
var samples = [];
var ref = [];
for (var r = 0; r < ROUNDS; r++) {
ref.push(Number(JniBench.runCallbacks(refCallback, REF_CALLBACK_KIND, refIterations)) / refIterations);
var nanos = JniBench.runCallbacks(cb, kind, iterations);
samples.push(Number(nanos) / iterations);
}
return report(name, samples);
return report(name, samples, ref);
}


// Splits an object's lifecycle into allocation (objects kept alive), release through a
// 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 +112,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 +222,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 +321,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 +349,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 @@ -289,13 +417,24 @@ function buildBenchmarks() {
return new Extended();
});
},
function () {
if (skipped("static.newInstance(extended class) [Java-created]")) return null;
var Created = java.lang.Object.extend({ toString: function () { return "created"; } });
var createdClass = new Created().getClass();
var JavaCreated = com.tns.tests.JavaCreated;
return measureJs("static.newInstance(extended class) [Java-created]", 10000, function (n) {
var o;
for (var i = 0; i < n; i++) o = JavaCreated.newInstance(createdClass);
return o;
});
},
function () {
return measureLifecycle("lifecycle JniBench.newObjectInJava()", 20000, function () {
return JniBench.newObjectInJava();
});
},
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 +461,105 @@ 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 () {
return measureCallback("callback.oneObject(binding)", callback, 7, 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);
};
Loading
Loading