Repository navigation
perf: cut JNI bridge per-call overhead #2092
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
3a4392b
test: add JNI bridge micro-benchmarks to the test app
edusperoni 6d9ffb7
test: add filter/scale knobs and more JNI benchmarks
edusperoni 638defb
perf: cache MetadataNode's interface flag instead of re-reading it pe…
edusperoni 1f8a038
perf: cache the thread's JNIEnv instead of calling GetEnv per JEnv
edusperoni cce0e70
perf: read memoized metadata node types without the reader's lock
edusperoni 037451c
perf: resolve the cast-marker and null-node private keys once per iso…
edusperoni 4632c04
perf: pass Java -> JS call arguments as parallel primitive arrays
edusperoni 3ad624a
perf: convert char returns directly and skip the String check where i…
edusperoni a953720
perf: trim per-callback work on the Java -> JS path
edusperoni aedccc7
perf: convert JS string arguments through a stack buffer
edusperoni 0582f28
perf: read the cached private keys with one GetPrivate
edusperoni e325aae
perf: resolve the calling thread's own runtime without the global lock
edusperoni File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
281 changes: 281 additions & 0 deletions
281
test-app/app/src/main/assets/app/tests/benchmarks/jniBenchmarks.js
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,281 @@ | ||
| // JNI bridge micro-benchmarks. Not part of the Jasmine suite: boot.js runs this instead of the | ||
| // suite when `adb shell setprop debug.ns.jnibench 1` is set, and every result is logged as a | ||
| // `JNIBENCH {json}` line (read with `adb logcat -s JS:I | grep JNIBENCH`). | ||
| // | ||
| // Each benchmark warms up (so V8 tiers the caller up), then runs ROUNDS timed rounds and reports | ||
| // 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 FILTER = String(JniBench.getFilter()); | ||
| 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) { | ||
| var result = { | ||
| name: name, | ||
| median: Math.round(median(nsPerOp) * 10) / 10, | ||
| min: Math.round(Math.min.apply(null, nsPerOp) * 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; | ||
| iterations *= SCALE; | ||
| var sink = body(Math.max(1000, iterations >> 2)); | ||
| var samples = []; | ||
| for (var r = 0; r < ROUNDS; r++) { | ||
| var start = performance.now(); | ||
| sink = body(iterations); | ||
| var elapsed = performance.now() - start; | ||
| samples.push((elapsed * 1e6) / iterations); | ||
| } | ||
| globalThis.__jniBenchSink = sink; | ||
| return report(name, samples); | ||
| } | ||
|
|
||
| function measureCallback(name, cb, kind, iterations) { | ||
| if (FILTER && name.indexOf(FILTER) < 0) return null; | ||
| iterations *= SCALE; | ||
| JniBench.runCallbacks(cb, kind, Math.max(1000, iterations >> 2)); | ||
| var samples = []; | ||
| for (var r = 0; r < ROUNDS; r++) { | ||
| var nanos = JniBench.runCallbacks(cb, kind, iterations); | ||
| samples.push(Number(nanos) / iterations); | ||
| } | ||
| return report(name, samples); | ||
| } | ||
|
|
||
| function buildBenchmarks() { | ||
| var bench = new JniBench(); | ||
| var other = bench.getOther(); | ||
| var jsString = "hello world"; | ||
| var longString = new Array(129).join("abcdefgh"); | ||
| var Extended = java.lang.Object.extend({ | ||
| toString: function () { return "extended"; } | ||
| }); | ||
| var buffer = new ArrayBuffer(64); | ||
| var intArray = [1, 2, 3, 4, 5, 6, 7, 8]; | ||
| var callback = new JniBench.Callback({ | ||
| noArgs: function () { }, | ||
| twoInts: function (a, b) { }, | ||
| oneString: function (s) { }, | ||
| intReturn: function (a) { return a + 1; }, | ||
| oneObject: function (o) { } | ||
| }); | ||
|
|
||
| return [ | ||
| function () { | ||
| return measureJs("js.emptyFunctionCall", 200000, function (n) { | ||
| var f = function (x) { return x + 1; }; | ||
| var s = 0; | ||
| for (var i = 0; i < n; i++) s = f(s); | ||
| return s; | ||
| }); | ||
| }, | ||
| function () { | ||
| return measureJs("static.noop()", 50000, function (n) { | ||
| for (var i = 0; i < n; i++) JniBench.noop(); | ||
| return n; | ||
| }); | ||
| }, | ||
| function () { | ||
| return measureJs("static.add(int,int)", 50000, function (n) { | ||
| var s = 0; | ||
| for (var i = 0; i < n; i++) s = JniBench.add(i, 1); | ||
| return s; | ||
| }); | ||
| }, | ||
| function () { | ||
| return measureJs("static.addDouble(double,double)", 50000, function (n) { | ||
| var s = 0; | ||
| for (var i = 0; i < n; i++) s = JniBench.addDouble(i, 0.5); | ||
| return s; | ||
| }); | ||
| }, | ||
| function () { | ||
| return measureJs("static.addLong(long,long)", 50000, function (n) { | ||
| var s = 0; | ||
| for (var i = 0; i < n; i++) s = JniBench.addLong(i, 1); | ||
| return s; | ||
| }); | ||
| }, | ||
| function () { | ||
| return measureJs("instance.noop()", 50000, function (n) { | ||
| for (var i = 0; i < n; i++) bench.instanceNoop(); | ||
| return n; | ||
| }); | ||
| }, | ||
| function () { | ||
| return measureJs("instance.getInt()", 50000, function (n) { | ||
| var s = 0; | ||
| for (var i = 0; i < n; i++) s = bench.getInt(); | ||
| return s; | ||
| }); | ||
| }, | ||
| function () { | ||
| return measureJs("instance.getBool()", 50000, function (n) { | ||
| var s = false; | ||
| for (var i = 0; i < n; i++) s = bench.getBool(); | ||
| return s; | ||
| }); | ||
| }, | ||
| function () { | ||
| return measureJs("instance.getChar()", 50000, function (n) { | ||
| var s; | ||
| for (var i = 0; i < n; i++) s = bench.getChar(); | ||
| return s; | ||
| }); | ||
| }, | ||
| function () { | ||
| return measureJs("instance.echo(String)", 50000, function (n) { | ||
| var s; | ||
| for (var i = 0; i < n; i++) s = bench.echo(jsString); | ||
| return s; | ||
| }); | ||
| }, | ||
| function () { | ||
| return measureJs("instance.echo(String 1KB)", 20000, function (n) { | ||
| var s; | ||
| for (var i = 0; i < n; i++) s = bench.echo(longString); | ||
| return s; | ||
| }); | ||
| }, | ||
| function () { | ||
| return measureJs("instance.getString()", 50000, function (n) { | ||
| var s; | ||
| for (var i = 0; i < n; i++) s = bench.getString(); | ||
| return s; | ||
| }); | ||
| }, | ||
| function () { | ||
| return measureJs("instance.self() [wrapped return]", 50000, function (n) { | ||
| var s; | ||
| for (var i = 0; i < n; i++) s = bench.self(); | ||
| return s; | ||
| }); | ||
| }, | ||
| function () { | ||
| return measureJs("instance.getOther() [wrapped return]", 50000, function (n) { | ||
| var s; | ||
| for (var i = 0; i < n; i++) s = bench.getOther(); | ||
| return s; | ||
| }); | ||
| }, | ||
| function () { | ||
| return measureJs("instance.takeObject(Object)", 50000, function (n) { | ||
| for (var i = 0; i < n; i++) bench.takeObject(other); | ||
| return n; | ||
| }); | ||
| }, | ||
| function () { | ||
| return measureJs("instance.takeObjects(Object x3)", 50000, function (n) { | ||
| var s = 0; | ||
| for (var i = 0; i < n; i++) s = bench.takeObjects(other, bench, other); | ||
| return s; | ||
| }); | ||
| }, | ||
| function () { | ||
| return measureJs("static.bufferCapacity(ArrayBuffer)", 20000, function (n) { | ||
| var s = 0; | ||
| for (var i = 0; i < n; i++) s = JniBench.bufferCapacity(buffer); | ||
| return s; | ||
| }); | ||
| }, | ||
| function () { | ||
| return measureJs("static.sumInts(int[8] from JS array)", 20000, function (n) { | ||
| var s = 0; | ||
| for (var i = 0; i < n; i++) s = JniBench.sumInts(intArray); | ||
| return s; | ||
| }); | ||
| }, | ||
| function () { | ||
| return measureJs("static.newObjectInJava() [new wrapper]", 20000, function (n) { | ||
| var s; | ||
| for (var i = 0; i < n; i++) s = JniBench.newObjectInJava(); | ||
| return s; | ||
| }); | ||
| }, | ||
| function () { | ||
| return measureJs("new java.lang.Object()", 20000, function (n) { | ||
| var s; | ||
| for (var i = 0; i < n; i++) s = new java.lang.Object(); | ||
| return s; | ||
| }); | ||
| }, | ||
| function () { | ||
| return measureJs("new Object.extend({})()", 20000, function (n) { | ||
| var s; | ||
| for (var i = 0; i < n; i++) s = new Extended(); | ||
| return s; | ||
| }); | ||
| }, | ||
| function () { | ||
| return measureJs("field.get int", 50000, function (n) { | ||
| var s = 0; | ||
| for (var i = 0; i < n; i++) s = bench.intField; | ||
| return s; | ||
| }); | ||
| }, | ||
| function () { | ||
| return measureJs("field.set int", 50000, function (n) { | ||
| for (var i = 0; i < n; i++) bench.intField = i; | ||
| return n; | ||
| }); | ||
| }, | ||
| function () { | ||
| return measureJs("field.get Object", 50000, function (n) { | ||
| var s; | ||
| for (var i = 0; i < n; i++) s = bench.objField; | ||
| return s; | ||
| }); | ||
| }, | ||
| function () { | ||
| return measureJs("field.get String", 50000, function (n) { | ||
| var s; | ||
| for (var i = 0; i < n; i++) s = bench.strField; | ||
| return s; | ||
| }); | ||
| }, | ||
| function () { | ||
| return measureCallback("callback.noArgs()", callback, 0, 20000); | ||
| }, | ||
| function () { | ||
| return measureCallback("callback.twoInts(int,int)", callback, 1, 20000); | ||
| }, | ||
| function () { | ||
| return measureCallback("callback.oneString(String)", callback, 2, 20000); | ||
| }, | ||
| function () { | ||
| return measureCallback("callback.intReturn(int) -> int", callback, 3, 20000); | ||
| }, | ||
| function () { | ||
| return measureCallback("callback.oneObject(Object)", callback, 4, 20000); | ||
| } | ||
| ]; | ||
| } | ||
|
|
||
| exports.run = function () { | ||
| 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)); | ||
| return; | ||
| } | ||
| gc(); | ||
| var result = benchmarks[index++](); | ||
| if (result) results.push(result); | ||
| setTimeout(next, 50); | ||
| } | ||
| setTimeout(next, 500); | ||
| }; | ||
Oops, something went wrong.
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard the
gc()call.gc()exists only if--expose_gcreaches V8. The runtime cachesgconly when it is a function. If the flag is missing,gc()throws aReferenceErrorinside thesetTimeoutcallback. The benchmark then stops withoutJNIBENCH_DONE, and no benchmark runs.Check for the function before the call.
Proposed fix
Committable suggestion