diff --git a/test-app/app/src/main/assets/app/tests/benchmarks/jniBenchmarks.js b/test-app/app/src/main/assets/app/tests/benchmarks/jniBenchmarks.js index 0e6c54fe8..4d3d1c4a2 100644 --- a/test-app/app/src/main/assets/app/tests/benchmarks/jniBenchmarks.js +++ b/test-app/app/src/main/assets/app/tests/benchmarks/jniBenchmarks.js @@ -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) { @@ -15,19 +21,23 @@ function median(values) { 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 = []; @@ -41,16 +51,30 @@ 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); } @@ -58,7 +82,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 = []; @@ -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(); @@ -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; @@ -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; @@ -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; @@ -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. @@ -322,18 +461,87 @@ 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(); @@ -341,5 +549,17 @@ exports.run = function () { 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); }; diff --git a/test-app/app/src/main/assets/app/tests/testGC.js b/test-app/app/src/main/assets/app/tests/testGC.js index a4b63da59..43c5b9ae5 100644 --- a/test-app/app/src/main/assets/app/tests/testGC.js +++ b/test-app/app/src/main/assets/app/tests/testGC.js @@ -314,4 +314,107 @@ describe("Tests garbage collection", function () { test.postCallback(); }); + describe("collected wrappers waiting for a flush", function () { + var Holder = com.tns.tests.WrapperHolder; + + it("keep a callback's error when wrappers were collected after its last Java call", function () { + var callable = new java.util.concurrent.Callable({ + call: function () { + for (var i = 0; i < 2000; i++) { + new java.lang.Object(); + } + gc(); + throw new Error("thrown with a batch pending"); + } + }); + var task = new java.util.concurrent.FutureTask(callable); + task.run(); + + expect(task.isDone()).toBe(true); + expect(function () { task.get(); }).toThrow(); + }); + + 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); + }); + }); }); \ No newline at end of file diff --git a/test-app/app/src/main/assets/app/tests/testMetadata.js b/test-app/app/src/main/assets/app/tests/testMetadata.js index 1d7469203..739917953 100644 --- a/test-app/app/src/main/assets/app/tests/testMetadata.js +++ b/test-app/app/src/main/assets/app/tests/testMetadata.js @@ -35,4 +35,113 @@ describe("Tests metadata", function () { var expected = 5; expect(keywordClass.getValue5()).toBe(expected); }); -}); \ No newline at end of file +}); +describe("Java members", function () { + it("are not constructors", function () { + var list = new java.util.ArrayList(); + + expect(function () { return new list.size(); }).toThrowError(TypeError); + expect(list.size.prototype).toBeUndefined(); + }); + + it("keep static methods constructible, so new on a static factory returns its object", function () { + var id = new java.util.UUID.randomUUID(); + + expect(id instanceof java.util.UUID).toBe(true); + }); + + it("leave class constructors constructible", function () { + var MyObject = java.lang.Object.extend({ toString: function () { return "mine"; } }); + + expect(new java.util.ArrayList().size()).toBe(0); + expect(new MyObject().toString()).toBe("mine"); + expect(java.util.UUID.randomUUID() instanceof java.util.UUID).toBe(true); + }); +}); + +describe("Java object wrappers", function () { + it("resolve constructor through the class prototype, with no own properties", function () { + var id = java.util.UUID.randomUUID(); + + expect(Object.getPrototypeOf(id)).toBe(java.util.UUID.prototype); + expect(id.constructor).toBe(java.util.UUID); + expect(Object.prototype.hasOwnProperty.call(id, "constructor")).toBe(false); + expect(Object.keys(id)).toEqual([]); + }); + + it("share a prototype between Java-created and JS-created instances", function () { + var created = new java.lang.StringBuilder("a"); + var returned = new java.lang.StringBuilder().append("b").append(created); + + expect(Object.getPrototypeOf(returned)).toBe(Object.getPrototypeOf(created)); + expect(returned instanceof java.lang.StringBuilder).toBe(true); + expect(String(returned.toString())).toBe("ba"); + }); + + it("read super through an implementation object that is not a Java instance", function () { + var impl = { toString: function () { return "impl"; } }; + java.lang.Object.extend(impl); + + expect(function () { return impl.super; }).not.toThrow(); + }); + + it("leave an extended instance Java creates during a JS construction its own JS object", function () { + var Inner = java.lang.Object.extend({ toString: function () { return "inner"; } }); + var Outer = com.tns.tests.NestedConstruction.extend({ toString: function () { return "outer"; } }); + var outer = new Outer(new Inner().getClass(), null); + + expect(String(outer.toString())).toBe("outer"); + expect(String(outer.getCreated().toString())).toBe("inner"); + expect(Object.prototype.hasOwnProperty.call(outer.getCreated(), "constructor")).toBe(false); + }); + + it("keep a JS construction's instance when a callback constructs another during it", function () { + var Inner = java.lang.Object.extend({ toString: function () { return "inner"; } }); + var Outer = com.tns.tests.NestedConstruction.extend({ toString: function () { return "outer:" + this.tag; } }); + var made = null; + var outer = new Outer(null, new java.lang.Runnable({ + run: function () { + made = new Inner(); + } + })); + outer.tag = "mine"; + + expect(String(outer.toString())).toBe("outer:mine"); + expect(String(made.toString())).toBe("inner"); + }); + + it("give a Java-created extended instance its class's current prototype", function () { + var Created = java.lang.Object.extend({ toString: function () { return "created"; } }); + var createdClass = new Created().getClass(); + com.tns.tests.JavaCreated.newInstance(createdClass); + var replacement = Object.create(Object.getPrototypeOf(Created.prototype)); + Created.prototype = replacement; + + var created = com.tns.tests.JavaCreated.newInstance(createdClass); + + expect(Object.getPrototypeOf(created)).toBe(replacement); + expect(String(created.toString())).toBe("created"); + }); + + it("hand back the same wrapper for the same Java object", function () { + var builder = new java.lang.StringBuilder(); + + expect(builder.append("x")).toBe(builder); + }); + + it("stay linked while thousands of later wrappers are collected", function () { + var list = new java.util.ArrayList(); + var first = java.util.UUID.randomUUID(); + list.add(first); + for (var round = 0; round < 3; round++) { + for (var i = 0; i < 3000; i++) { + java.util.UUID.randomUUID(); + } + gc(); + java.lang.System.gc(); + } + + expect(list.get(0)).toBe(first); + expect(String(list.get(0).toString())).toBe(String(first.toString())); + }); +}); diff --git a/test-app/app/src/main/java/com/tns/tests/ContentionBench.java b/test-app/app/src/main/java/com/tns/tests/ContentionBench.java new file mode 100644 index 000000000..58dd8eb9b --- /dev/null +++ b/test-app/app/src/main/java/com/tns/tests/ContentionBench.java @@ -0,0 +1,76 @@ +package com.tns.tests; + +import java.util.concurrent.CountDownLatch; + +/** + * Several Java threads calling one JS implementation at once, the multithreaded-JS + * case. start() returns immediately so the JS thread can go idle and release the + * isolate; the caller polls result() for the elapsed nanoseconds. + */ +public class ContentionBench { + public interface Sink { + void call0(); + + void call3(Object a, Object b, Object c); + } + + public static final int NO_ARGS = 0; + public static final int EXISTING_OBJECTS = 1; + public static final int NEW_OBJECTS = 2; + public static final int NEW_OBJECTS_MANY_CLASSES = 3; + + private static volatile long resultNanos = -1; + + public static long result() { + return resultNanos; + } + + public static void start(final Sink sink, final int threads, final int callsPerThread, final int mode) { + resultNanos = -1; + final Object[] shared = {new Object(), new Object(), new Object()}; + new Thread(new Runnable() { + @Override + public void run() { + final CountDownLatch ready = new CountDownLatch(threads); + final CountDownLatch go = new CountDownLatch(1); + Thread[] workers = new Thread[threads]; + for (int t = 0; t < threads; t++) { + workers[t] = new Thread(new Runnable() { + @Override + public void run() { + ready.countDown(); + try { + go.await(); + } catch (InterruptedException e) { + return; + } + for (int i = 0; i < callsPerThread; i++) { + if (mode == NO_ARGS) { + sink.call0(); + } else if (mode == EXISTING_OBJECTS) { + sink.call3(shared[0], shared[1], shared[2]); + } else if (mode == NEW_OBJECTS) { + sink.call3(new Object(), new Object(), new Object()); + } else { + sink.call3(JniBench.newOfClass(i), JniBench.newOfClass(i + 5), JniBench.newOfClass(i + 11)); + } + } + } + }); + workers[t].start(); + } + try { + ready.await(); + long start = System.nanoTime(); + go.countDown(); + for (Thread worker : workers) { + worker.join(); + } + resultNanos = System.nanoTime() - start; + } catch (InterruptedException e) { + resultNanos = 0; + } + } + }).start(); + } +} diff --git a/test-app/app/src/main/java/com/tns/tests/FreshArgs.java b/test-app/app/src/main/java/com/tns/tests/FreshArgs.java new file mode 100644 index 000000000..5ca9b899b --- /dev/null +++ b/test-app/app/src/main/java/com/tns/tests/FreshArgs.java @@ -0,0 +1,52 @@ +package com.tns.tests; + +import java.lang.ref.WeakReference; +import java.util.concurrent.CountDownLatch; + +// Java->JS calls with objects JS has never seen, which Java registers before taking the +// isolate lock. +public class FreshArgs { + public interface Target { + void take(Object o); + } + + public static WeakReference callWithNewObject(Target target) { + Object o = new Object(); + WeakReference ref = new WeakReference<>(o); + try { + target.take(o); + } catch (Throwable e) { + // The call is expected to fail before JS sees the argument. + } + return ref; + } + + private static final java.util.concurrent.atomic.AtomicInteger finishedCalls = new java.util.concurrent.atomic.AtomicInteger(); + + public static int finishedCalls() { + return finishedCalls.get(); + } + + // Returns at once: the JS thread must go idle so the workers can take the isolate lock. + public static void startFromThreadsAtOnce(final Target target, int threads) { + finishedCalls.set(0); + final Object shared = new Object(); + final CountDownLatch go = new CountDownLatch(1); + for (int t = 0; t < threads; t++) { + new Thread(new Runnable() { + @Override + public void run() { + try { + go.await(); + target.take(shared); + } catch (InterruptedException e) { + return; + } finally { + finishedCalls.incrementAndGet(); + } + } + }).start(); + } + go.countDown(); + } +} diff --git a/test-app/app/src/main/java/com/tns/tests/JavaCreated.java b/test-app/app/src/main/java/com/tns/tests/JavaCreated.java new file mode 100644 index 000000000..a4ae44fe2 --- /dev/null +++ b/test-app/app/src/main/java/com/tns/tests/JavaCreated.java @@ -0,0 +1,8 @@ +package com.tns.tests; + +/** Instantiates a class from Java, for benchmarks of objects JS did not construct. */ +public class JavaCreated { + public static Object newInstance(Class clazz) throws Exception { + return clazz.getDeclaredConstructor().newInstance(); + } +} diff --git a/test-app/app/src/main/java/com/tns/tests/JniBench.java b/test-app/app/src/main/java/com/tns/tests/JniBench.java index ab3313ebb..0a5a9b5bb 100644 --- a/test-app/app/src/main/java/com/tns/tests/JniBench.java +++ b/test-app/app/src/main/java/com/tns/tests/JniBench.java @@ -45,7 +45,7 @@ public static int getScale() { } } - private static String getProp(String name) { + static String getProp(String name) { try { Process p = new ProcessBuilder("getprop", name).start(); try (BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream()))) { @@ -126,6 +126,101 @@ public static Object newObjectInJava() { return new Object(); } + // Sixteen distinct classes, for call sites that see many runtime classes, as the + // generic ones in an app do (View, Object, Drawable...). + public static final class K0 { + } + + public static final class K1 { + } + + public static final class K2 { + } + + public static final class K3 { + } + + public static final class K4 { + } + + public static final class K5 { + } + + public static final class K6 { + } + + public static final class K7 { + } + + public static final class K8 { + } + + public static final class K9 { + } + + public static final class K10 { + } + + public static final class K11 { + } + + public static final class K12 { + } + + public static final class K13 { + } + + public static final class K14 { + } + + public static final class K15 { + } + + public static Object newOfClass(int i) { + switch (i & 15) { + case 0: + return new K0(); + case 1: + return new K1(); + case 2: + return new K2(); + case 3: + return new K3(); + case 4: + return new K4(); + case 5: + return new K5(); + case 6: + return new K6(); + case 7: + return new K7(); + case 8: + return new K8(); + case 9: + return new K9(); + case 10: + return new K10(); + case 11: + return new K11(); + case 12: + return new K12(); + case 13: + return new K13(); + case 14: + return new K14(); + case 15: + return new K15(); + default: + throw new AssertionError(); + } + } + + private static int nextClass; + + public static Object newObjectOfManyClasses() { + return newOfClass(nextClass++); + } + /** Returns total elapsed nanoseconds for {@code n} Java -> JS calls of the given kind. */ public static long runCallbacks(Callback cb, int kind, int n) { Object arg = new Object(); @@ -146,6 +241,16 @@ public static long runCallbacks(Callback cb, int kind, int n) { case 4: for (int i = 0; i < n; i++) cb.oneObject(arg); break; + case 5: + for (int i = 0; i < n; i++) cb.oneObject(new Object()); + break; + case 6: + for (int i = 0; i < n; i++) cb.oneObject(newOfClass(i)); + break; + case 7: + // The callback is itself a binding: the argument carries its object id. + for (int i = 0; i < n; i++) cb.oneObject(cb); + break; default: throw new IllegalArgumentException("kind " + kind); } diff --git a/test-app/app/src/main/java/com/tns/tests/JniBenchConfig.java b/test-app/app/src/main/java/com/tns/tests/JniBenchConfig.java new file mode 100644 index 000000000..aa563f56a --- /dev/null +++ b/test-app/app/src/main/java/com/tns/tests/JniBenchConfig.java @@ -0,0 +1,21 @@ +package com.tns.tests; + +/** + * Benchmark settings that only the harness reads, kept off JniBench: every static added + * there is another property on the JS constructor its static benchmarks are called through. + */ +public class JniBenchConfig { + /** Timed rounds per benchmark; 0 keeps the default. */ + public static int getRounds() { + try { + return Math.max(0, Integer.parseInt(JniBench.getProp("debug.ns.jnibench.rounds"))); + } catch (NumberFormatException e) { + return 0; + } + } + + /** Comma-separated admission spin budgets to alternate between, round by round, in contention rows. */ + public static String getVariants() { + return JniBench.getProp("debug.ns.jnibench.variants"); + } +} diff --git a/test-app/app/src/main/java/com/tns/tests/NestedConstruction.java b/test-app/app/src/main/java/com/tns/tests/NestedConstruction.java new file mode 100644 index 000000000..db3018249 --- /dev/null +++ b/test-app/app/src/main/java/com/tns/tests/NestedConstruction.java @@ -0,0 +1,17 @@ +package com.tns.tests; + +/** Creates objects while its own constructor runs, for the construction-window specs. */ +public class NestedConstruction { + private final Object created; + + public NestedConstruction(Class toCreate, Runnable during) throws Exception { + created = toCreate == null ? null : toCreate.newInstance(); + if (during != null) { + during.run(); + } + } + + public Object getCreated() { + return created; + } +} diff --git a/test-app/app/src/main/java/com/tns/tests/StaticFieldBench.java b/test-app/app/src/main/java/com/tns/tests/StaticFieldBench.java new file mode 100644 index 000000000..fb6bc8904 --- /dev/null +++ b/test-app/app/src/main/java/com/tns/tests/StaticFieldBench.java @@ -0,0 +1,8 @@ +package com.tns.tests; + +/** A class with few static members, for static field benchmarks; JniBench has too many. */ +public class StaticFieldBench { + public static int counter = 1; + + public static final int CONSTANT = 7; +} diff --git a/test-app/app/src/main/java/com/tns/tests/WrapperHolder.java b/test-app/app/src/main/java/com/tns/tests/WrapperHolder.java new file mode 100644 index 000000000..fc063fccf --- /dev/null +++ b/test-app/app/src/main/java/com/tns/tests/WrapperHolder.java @@ -0,0 +1,7 @@ +package com.tns.tests; + +// Field reads and writes reach Java without a method call, which the finalizer batch tests +// rely on. +public class WrapperHolder { + public static Object held; +} diff --git a/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/impl/FieldsWriterImpl.java b/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/impl/FieldsWriterImpl.java index 379a57b1b..af7928025 100644 --- a/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/impl/FieldsWriterImpl.java +++ b/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/impl/FieldsWriterImpl.java @@ -8,6 +8,7 @@ public class FieldsWriterImpl implements FieldsWriter { private static final String THIZ_KEYWORD = "thiz"; private static final String RUNTIME_TAG_FIELD_DECLARATION = "private volatile int runtimeTag__ns;"; + private static final String OBJECT_ID_FIELD_DECLARATION = "private int objectId__ns;"; private final Writer writer; @@ -19,6 +20,8 @@ public FieldsWriterImpl(final Writer writer) { public void writeRuntimeTagField() { writer.write(TABULATION_LITERAL); writer.writeln(RUNTIME_TAG_FIELD_DECLARATION); + writer.write(TABULATION_LITERAL); + writer.writeln(OBJECT_ID_FIELD_DECLARATION); writer.writeln(); } diff --git a/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/impl/MethodsWriterImpl.java b/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/impl/MethodsWriterImpl.java index 5190f9c6b..1cc504cd3 100644 --- a/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/impl/MethodsWriterImpl.java +++ b/test-app/build-tools/static-binding-generator/src/main/java/org/nativescript/staticbindinggenerator/generating/writing/impl/MethodsWriterImpl.java @@ -51,6 +51,10 @@ public class MethodsWriterImpl implements MethodsWriter { private static final String INTERNAL_RUNTIME_GET_RUNTIME_TAG_RETURN_STATEMENT = "return runtimeTag__ns;"; private static final String INTERNAL_RUNTIME_SET_RUNTIME_TAG_METHOD_SIGNATURE = "public void setRuntimeTag__ns(int tag)"; private static final String INTERNAL_RUNTIME_SET_RUNTIME_TAG_ASSIGNMENT = "runtimeTag__ns = tag;"; + private static final String INTERNAL_RUNTIME_GET_OBJECT_ID_METHOD_SIGNATURE = "public int getObjectId__ns()"; + private static final String INTERNAL_RUNTIME_GET_OBJECT_ID_RETURN_STATEMENT = "return objectId__ns;"; + private static final String INTERNAL_RUNTIME_SET_OBJECT_ID_METHOD_SIGNATURE = "public void setObjectId__ns(int id)"; + private static final String INTERNAL_RUNTIME_SET_OBJECT_ID_ASSIGNMENT = "objectId__ns = id;"; private static final String INTERNAL_SERVICES_ONCREATE_METHOD_SIGNATURE = "public void " + ON_CREATE_METHOD_NAME + "()"; private static final String RETURN_KEYWORD = "return"; @@ -406,6 +410,8 @@ public void writeInternalRuntimeHashCodeMethod() { public void writeInternalRuntimeTagAccessorMethods() { writeInternalRuntimeMethod(INTERNAL_RUNTIME_GET_RUNTIME_TAG_METHOD_SIGNATURE, INTERNAL_RUNTIME_GET_RUNTIME_TAG_RETURN_STATEMENT); writeInternalRuntimeMethod(INTERNAL_RUNTIME_SET_RUNTIME_TAG_METHOD_SIGNATURE, INTERNAL_RUNTIME_SET_RUNTIME_TAG_ASSIGNMENT); + writeInternalRuntimeMethod(INTERNAL_RUNTIME_GET_OBJECT_ID_METHOD_SIGNATURE, INTERNAL_RUNTIME_GET_OBJECT_ID_RETURN_STATEMENT); + writeInternalRuntimeMethod(INTERNAL_RUNTIME_SET_OBJECT_ID_METHOD_SIGNATURE, INTERNAL_RUNTIME_SET_OBJECT_ID_ASSIGNMENT); } private void writeInternalRuntimeMethod(String signature, String returnStatement) { diff --git a/test-app/build-tools/static-binding-generator/src/test/java/com/tns/NativeScriptRuntimeBound.java b/test-app/build-tools/static-binding-generator/src/test/java/com/tns/NativeScriptRuntimeBound.java index 0f1c9d6d2..adb75c29d 100644 --- a/test-app/build-tools/static-binding-generator/src/test/java/com/tns/NativeScriptRuntimeBound.java +++ b/test-app/build-tools/static-binding-generator/src/test/java/com/tns/NativeScriptRuntimeBound.java @@ -4,4 +4,11 @@ public interface NativeScriptRuntimeBound { int getRuntimeTag__ns(); void setRuntimeTag__ns(int tag); + + default int getObjectId__ns() { + return -1; + } + + default void setObjectId__ns(int id) { + } } diff --git a/test-app/build-tools/static-binding-generator/src/test/java/org/nativescript/staticbindinggenerator/test/GeneratorTest.java b/test-app/build-tools/static-binding-generator/src/test/java/org/nativescript/staticbindinggenerator/test/GeneratorTest.java index 4cf5b1e61..4c4cadc33 100644 --- a/test-app/build-tools/static-binding-generator/src/test/java/org/nativescript/staticbindinggenerator/test/GeneratorTest.java +++ b/test-app/build-tools/static-binding-generator/src/test/java/org/nativescript/staticbindinggenerator/test/GeneratorTest.java @@ -53,7 +53,7 @@ public void testCanCompileBinding() throws Exception { Class helloClass = InMemoryJavaCompiler.compile(binding.getClassname(), sourceCode.toString(), options); Assert.assertNotNull(helloClass); - Assert.assertEquals(5, helloClass.getDeclaredMethods().length); + Assert.assertEquals(7, helloClass.getDeclaredMethods().length); } @Test @@ -101,6 +101,9 @@ public void testBindingCarriesARuntimeTag() throws Exception { // read on whichever thread a Java->JS call arrives on Assert.assertTrue(java.lang.reflect.Modifier.isVolatile(boundClass.getDeclaredField("runtimeTag__ns").getModifiers())); + + instance.setObjectId__ns(42); + Assert.assertEquals(42, instance.getObjectId__ns()); } @Test @@ -126,7 +129,7 @@ public void testCanCompileBindingOfInterfaceWithStaticInitializer() throws Excep Class helloClass = InMemoryJavaCompiler.compile("com.tns.gen.com.example.MyInterface", sourceCode.toString(), options); Assert.assertNotNull(helloClass); - Assert.assertEquals(5, helloClass.getDeclaredMethods().length); // 5 methods (includes 'hashCode__super', 'equals__super', 'getRuntimeTag__ns' and 'setRuntimeTag__ns') + Assert.assertEquals(7, helloClass.getDeclaredMethods().length); // 7 methods (includes 'hashCode__super', 'equals__super' and the runtime tag and object id accessors) } @Test @@ -174,7 +177,7 @@ public void testCanCompileBindingClassExtendingAnExtendedClassWithMethodsWithThe Class ComplexClass = InMemoryJavaCompiler.compile(binding.getClassname(), sourceCode.toString(), options); Assert.assertNotNull(ComplexClass); - Assert.assertEquals(6, ComplexClass.getDeclaredMethods().length); // 1 + constructor + (equals + hashcode) + (getRuntimeTag__ns + setRuntimeTag__ns) + Assert.assertEquals(8, ComplexClass.getDeclaredMethods().length); // 1 + constructor + (equals + hashcode) + (runtime tag and object id accessors) } @Test diff --git a/test-app/runtime-binding-generator/src/main/java/com/tns/NativeScriptRuntimeBound.java b/test-app/runtime-binding-generator/src/main/java/com/tns/NativeScriptRuntimeBound.java index c6d5ce412..437777637 100644 --- a/test-app/runtime-binding-generator/src/main/java/com/tns/NativeScriptRuntimeBound.java +++ b/test-app/runtime-binding-generator/src/main/java/com/tns/NativeScriptRuntimeBound.java @@ -16,4 +16,16 @@ public interface NativeScriptRuntimeBound { int getRuntimeTag__ns(); void setRuntimeTag__ns(int tag); + + /** + * The id the owning runtime registered the instance under, written before the tag and + * read only once the tag is set. Lets a Java->JS call skip looking the id up; native + * still checks it. The defaults keep a class compiled without the field working. + */ + default int getObjectId__ns() { + return -1; + } + + default void setObjectId__ns(int id) { + } } diff --git a/test-app/runtime-binding-generator/src/main/java/com/tns/bindings/Dump.java b/test-app/runtime-binding-generator/src/main/java/com/tns/bindings/Dump.java index fbd7f0ed5..d2c253cdf 100644 --- a/test-app/runtime-binding-generator/src/main/java/com/tns/bindings/Dump.java +++ b/test-app/runtime-binding-generator/src/main/java/com/tns/bindings/Dump.java @@ -24,6 +24,7 @@ public class Dump { static final String callJSMethodName = "callJSMethod"; static final String initInstanceMethodName = "initInstance"; static final String RUNTIME_TAG_FIELD_NAME = "runtimeTag__ns"; + static final String OBJECT_ID_FIELD_NAME = "objectId__ns"; static final StringBuffer methodDescriptorBuilder = new StringBuffer(); @@ -494,26 +495,28 @@ private void generateMethods(ClassVisitor cv, ClassDescriptor classTo, MethodDes generateEqualsSuper(cv); generateHashCodeSuper(cv); - generateGetRuntimeTag(cv, tnsClassSignature); - generateSetRuntimeTag(cv, tnsClassSignature); + generateIntGetter(cv, tnsClassSignature, "getRuntimeTag__ns", RUNTIME_TAG_FIELD_NAME); + generateIntSetter(cv, tnsClassSignature, "setRuntimeTag__ns", RUNTIME_TAG_FIELD_NAME); + generateIntGetter(cv, tnsClassSignature, "getObjectId__ns", OBJECT_ID_FIELD_NAME); + generateIntSetter(cv, tnsClassSignature, "setObjectId__ns", OBJECT_ID_FIELD_NAME); } // 2 registers, 1 parameter: 'this' takes the last one, v0 is left as scratch - private void generateGetRuntimeTag(ClassVisitor cv, String tnsClassSignature) { - MethodVisitor mv = cv.visitMethod(org.ow2.asmdex.Opcodes.ACC_PUBLIC, "getRuntimeTag__ns", "I", null, null); + private void generateIntGetter(ClassVisitor cv, String tnsClassSignature, String methodName, String fieldName) { + MethodVisitor mv = cv.visitMethod(org.ow2.asmdex.Opcodes.ACC_PUBLIC, methodName, "I", null, null); mv.visitCode(); mv.visitMaxs(2, 0); - mv.visitFieldInsn(org.ow2.asmdex.Opcodes.INSN_IGET, tnsClassSignature, RUNTIME_TAG_FIELD_NAME, "I", 0, 1); + mv.visitFieldInsn(org.ow2.asmdex.Opcodes.INSN_IGET, tnsClassSignature, fieldName, "I", 0, 1); mv.visitIntInsn(org.ow2.asmdex.Opcodes.INSN_RETURN, 0); mv.visitEnd(); } - // 2 registers, 2 parameters: v0 is 'this' and v1 the id, leaving no scratch - private void generateSetRuntimeTag(ClassVisitor cv, String tnsClassSignature) { - MethodVisitor mv = cv.visitMethod(org.ow2.asmdex.Opcodes.ACC_PUBLIC, "setRuntimeTag__ns", "VI", null, null); + // 2 registers, 2 parameters: v0 is 'this' and v1 the value, leaving no scratch + private void generateIntSetter(ClassVisitor cv, String tnsClassSignature, String methodName, String fieldName) { + MethodVisitor mv = cv.visitMethod(org.ow2.asmdex.Opcodes.ACC_PUBLIC, methodName, "VI", null, null); mv.visitCode(); mv.visitMaxs(2, 0); - mv.visitFieldInsn(org.ow2.asmdex.Opcodes.INSN_IPUT, tnsClassSignature, RUNTIME_TAG_FIELD_NAME, "I", 1, 0); + mv.visitFieldInsn(org.ow2.asmdex.Opcodes.INSN_IPUT, tnsClassSignature, fieldName, "I", 1, 0); mv.visitInsn(org.ow2.asmdex.Opcodes.INSN_RETURN_VOID); mv.visitEnd(); } @@ -831,6 +834,10 @@ private void generateFields(ClassVisitor cv) { // left at its zero default, which NativeScriptRuntimeBound defines as "no runtime yet" fv = cv.visitField(org.ow2.asmdex.Opcodes.ACC_PRIVATE | org.ow2.asmdex.Opcodes.ACC_VOLATILE, RUNTIME_TAG_FIELD_NAME, "I", null, null); fv.visitEnd(); + + // only read once the tag is set, which is written after it + fv = cv.visitField(org.ow2.asmdex.Opcodes.ACC_PRIVATE, OBJECT_ID_FIELD_NAME, "I", null, null); + fv.visitEnd(); } static final String[] classImplentedInterfaces = new String[] { "Lcom/tns/NativeScriptHashCodeProvider;", "Lcom/tns/NativeScriptRuntimeBound;" }; diff --git a/test-app/runtime/CMakeLists.txt b/test-app/runtime/CMakeLists.txt index eb2ca29ac..25644f2a1 100644 --- a/test-app/runtime/CMakeLists.txt +++ b/test-app/runtime/CMakeLists.txt @@ -262,18 +262,23 @@ add_library( set(NATIVES_BLOB_INCLUDE_DIRECTORIES ${PROJECT_SOURCE_DIR}/src/main/libs/${ANDROID_ABI}/include) +# Calls between this library's own exported functions bind directly instead of going +# through the PLT. Exported functions stay callable from plugins; data symbols stay +# interposable so a plugin and the runtime never end up with two copies of one variable. +set(NS_SYMBOLIC_FUNCTIONS "-Wl,-Bsymbolic-functions") + if (OPTIMIZED_BUILD OR OPTIMIZED_WITH_INSPECTOR_BUILD) set_target_properties( NativeScript # The version script keeps V8's public API linkable for plugins while hiding # the ~60k v8::internal:: symbols the static monolith would otherwise export. - PROPERTIES LINK_FLAGS "-Wl,--allow-multiple-definition -Wl,--gc-sections -Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/exported-symbols.map" + PROPERTIES LINK_FLAGS "-Wl,--allow-multiple-definition -Wl,--gc-sections -Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/exported-symbols.map ${NS_SYMBOLIC_FUNCTIONS}" INTERFACE_INCLUDE_DIRECTORIES NATIVES_BLOB_INCLUDE_DIRECTORIES ) else () set_target_properties( NativeScript - PROPERTIES LINK_FLAGS -Wl,--allow-multiple-definition + PROPERTIES LINK_FLAGS "-Wl,--allow-multiple-definition ${NS_SYMBOLIC_FUNCTIONS}" INTERFACE_INCLUDE_DIRECTORIES NATIVES_BLOB_INCLUDE_DIRECTORIES ) endif () diff --git a/test-app/runtime/src/main/cpp/AdmissionGate.h b/test-app/runtime/src/main/cpp/AdmissionGate.h new file mode 100644 index 000000000..bce9173cd --- /dev/null +++ b/test-app/runtime/src/main/cpp/AdmissionGate.h @@ -0,0 +1,118 @@ +#ifndef ADMISSIONGATE_H_ +#define ADMISSIONGATE_H_ + +#include +#include +#include +#include + +#include "v8-locker.h" + +namespace tns { + +/* + * Orders the Java threads calling into one runtime before they take its + * v8::Locker. V8's mutex spins only briefly before sleeping, and waking a + * sleeper takes far longer than a call usually holds the isolate, so with two + * callers every handoff left the isolate idle for a whole wake-up. A waiter + * here spins for a while first, staying on its core to take over at once. + * + * Held for exactly as long as the Locker it precedes. Releasing the isolate + * with a v8::Unlocker while keeping the gate would block every other Java + * caller on an idle isolate. + */ +class AdmissionGate { + public: + void Enter() { + // A woken sleeper spins again rather than getting one try, or under steady + // load the spinners would keep beating it to the gate. + for (;;) { + if (TryTake() || Spin()) { + return; + } + std::unique_lock lock(m_mutex); + m_sleepers.fetch_add(1); + while (m_busy.load()) { + m_wakeup.wait(lock); + } + m_sleepers.fetch_sub(1); + } + } + + void Exit() { + m_busy.store(false); + // Pairs with a sleeper's increment before it checks m_busy: either it sees + // the gate free, or this sees it waiting. + if (m_sleepers.load() > 0) { + std::lock_guard lock(m_mutex); + m_wakeup.notify_one(); + } + } + + // How long a caller spins before sleeping; negative bypasses the gate. + inline static std::atomic s_spinMicros{50}; + + private: + bool Spin() { + int budget = s_spinMicros.load(std::memory_order_relaxed); + if (budget <= 0) { + return false; + } + auto deadline = std::chrono::steady_clock::now() + std::chrono::microseconds(budget); + for (unsigned i = 1;; i++) { + CpuRelax(); + if (TryTake()) { + return true; + } + if ((i & 63) == 0 && std::chrono::steady_clock::now() >= deadline) { + return false; + } + } + } + + bool TryTake() { + bool expected = false; + return !m_busy.load(std::memory_order_relaxed) && + m_busy.compare_exchange_weak(expected, true, std::memory_order_acquire); + } + + static void CpuRelax() { +#if defined(__aarch64__) || defined(__arm__) + asm volatile("yield"); +#elif defined(__x86_64__) || defined(__i386__) + asm volatile("pause"); +#endif + } + + std::atomic m_busy{false}; + std::atomic m_sleepers{0}; + std::mutex m_mutex; + std::condition_variable m_wakeup; +}; + +// Holds the gate for a call into JS from Java, unless this thread already holds +// the isolate (a nested call), where waiting on the gate could deadlock. +class AdmissionScope { + public: + AdmissionScope(AdmissionGate& gate, v8::Isolate* isolate) { + if (AdmissionGate::s_spinMicros.load(std::memory_order_relaxed) >= 0 && + !v8::Locker::IsLocked(isolate)) { + gate.Enter(); + m_gate = &gate; + } + } + ~AdmissionScope() { + if (m_gate != nullptr) { + m_gate->Exit(); + } + } + AdmissionScope(const AdmissionScope&) = delete; + AdmissionScope& operator=(const AdmissionScope&) = delete; + + private: + AdmissionGate* m_gate = nullptr; +}; + +} // namespace tns + +#endif // ADMISSIONGATE_H_ diff --git a/test-app/runtime/src/main/cpp/ArgConverter.cpp b/test-app/runtime/src/main/cpp/ArgConverter.cpp index 2ad647036..20b8bf52a 100644 --- a/test-app/runtime/src/main/cpp/ArgConverter.cpp +++ b/test-app/runtime/src/main/cpp/ArgConverter.cpp @@ -92,38 +92,29 @@ void ArgConverter::NativeScriptLongFunctionCallback(const v8::FunctionCallbackIn } } -int ArgConverter::GetJavaArgsLength(JNIEnv* env, const JavaCallArgs& args) { - return args.types == nullptr ? 0 : env->GetArrayLength(args.types); -} - void ArgConverter::ConvertJavaArgsToJsArgs(Isolate* isolate, JNIEnv* env, const JavaCallArgs& args, int argc, Local* out) { if (argc == 0) { + args.converted = true; return; } constexpr int kInlineArgs = 16; - jint inlineTypes[kInlineArgs]; - jlong inlineValues[kInlineArgs]; - std::vector heapTypes; + jlong inlineValues[JavaCallArgs::PackedLength(kInlineArgs)]; std::vector heapValues; - jint* types = inlineTypes; jlong* values = inlineValues; if (argc > kInlineArgs) { - heapTypes.resize(argc); - heapValues.resize(argc); - types = heapTypes.data(); + heapValues.resize(JavaCallArgs::PackedLength(argc)); values = heapValues.data(); } - env->GetIntArrayRegion(args.types, 0, argc, types); - env->GetLongArrayRegion(args.values, 0, argc, values); + env->GetLongArrayRegion(args.values, 0, JavaCallArgs::PackedLength(argc), values); auto objectManager = Runtime::GetRuntime(isolate)->GetObjectManager(); for (int i = 0; i < argc; i++) { jlong bits = values[i]; Local jsArg; - switch (static_cast(types[i])) { + switch (JavaCallArgs::TypeAt(values, argc, i)) { case Type::Boolean: jsArg = Boolean::New(isolate, bits != 0); break; @@ -155,14 +146,24 @@ void ArgConverter::ConvertJavaArgsToJsArgs(Isolate* isolate, JNIEnv* env, const break; } case Type::JsObject: { - auto javaObjectID = static_cast(bits); - jsArg = objectManager->GetJsObjectByJavaObject(javaObjectID); - + // values[i] packs an id in its low half (exact when FreshArg is set, else a + // hint Java read without the isolate lock, or -1) and the class index above. + auto hintedObjectID = static_cast(bits); + auto classIndex = static_cast((bits >> 32) & 0x7fffffff); + if ((bits & JavaCallArgs::FreshArg) != 0 && + !objectManager->HasJsObject(hintedObjectID)) { + JniLocalRef arg(env->GetObjectArrayElement(args.refs, i)); + jsArg = objectManager->CreateJSWrapper(hintedObjectID, classIndex, arg); + } else if (hintedObjectID != -1) { + jsArg = objectManager->GetLinkedJsObjectByJavaObject(hintedObjectID); + } if (jsArg.IsEmpty()) { - JniLocalRef argJavaClassPath(env->GetObjectArrayElement(args.refs, i)); - string argClassName = jstringToString(ObjectToString(argJavaClassPath)); - argClassName = Util::ConvertFromCanonicalToJniName(argClassName); - jsArg = objectManager->CreateJSWrapper(javaObjectID, argClassName); + JniLocalRef arg(env->GetObjectArrayElement(args.refs, i)); + auto javaObjectID = objectManager->GetOrCreateObjectId(arg); + jsArg = objectManager->GetJsObjectByJavaObject(javaObjectID); + if (jsArg.IsEmpty()) { + jsArg = objectManager->CreateJSWrapper(javaObjectID, classIndex, arg); + } } break; } @@ -173,6 +174,7 @@ void ArgConverter::ConvertJavaArgsToJsArgs(Isolate* isolate, JNIEnv* env, const out[i] = jsArg; } + args.converted = true; } Local ArgConverter::ConvertFromJavaLong(Isolate* isolate, jlong value) { diff --git a/test-app/runtime/src/main/cpp/ArgConverter.h b/test-app/runtime/src/main/cpp/ArgConverter.h index 5e416b6b4..95d4fe294 100644 --- a/test-app/runtime/src/main/cpp/ArgConverter.h +++ b/test-app/runtime/src/main/cpp/ArgConverter.h @@ -10,6 +10,7 @@ #include "v8.h" #include "JEnv.h" +#include "JType.h" #include #include #include @@ -18,18 +19,32 @@ namespace tns { // The argument arrays com.tns.Runtime.packageArgs builds for a Java -> JS call. struct JavaCallArgs { - jintArray types; + jint count; + // count values, then one type id per byte, eight to a slot: one JNI read gets both. jlongArray values; jobjectArray refs; + // Set once every argument has a JS value, so each object Java registered for this + // call (FreshArg) has a wrapper and nothing is left to release if the call fails. + mutable bool converted = false; + + // Set in an object argument's packed value when Java registered the object for this + // call; the low half is then its exact id and the next 31 bits its class index. + static constexpr jlong FreshArg = static_cast(1ULL << 63); + + static constexpr int PackedLength(int count) { + return count + (count + 7) / 8; + } + + static Type TypeAt(const jlong* packed, int count, int i) { + return static_cast((packed[count + i / 8] >> ((i % 8) * 8)) & 0xff); + } }; class ArgConverter { public: static void Init(v8::Local context); - static int GetJavaArgsLength(JNIEnv* env, const JavaCallArgs& args); - - // out must have room for GetJavaArgsLength(args) values. + // out must have room for args.count values. static void ConvertJavaArgsToJsArgs(v8::Isolate* isolate, JNIEnv* env, const JavaCallArgs& args, int argc, v8::Local* out); static v8::Local ConvertFromJavaLong(v8::Isolate* isolate, jlong value); diff --git a/test-app/runtime/src/main/cpp/ArrayHelper.cpp b/test-app/runtime/src/main/cpp/ArrayHelper.cpp index 668993dbd..c8df01fa7 100644 --- a/test-app/runtime/src/main/cpp/ArrayHelper.cpp +++ b/test-app/runtime/src/main/cpp/ArrayHelper.cpp @@ -113,8 +113,7 @@ void ArrayHelper::CreateJavaArray(const v8::FunctionCallbackInfo& inf return; } - jint javaObjectID = objectManager->GetOrCreateObjectId(array); - auto jsWrapper = objectManager->CreateJSWrapper(javaObjectID, "" /* ignored */, array); + auto jsWrapper = objectManager->GetOrCreateJSWrapper(array); info.GetReturnValue().Set(jsWrapper); } diff --git a/test-app/runtime/src/main/cpp/CallbackHandlers.cpp b/test-app/runtime/src/main/cpp/CallbackHandlers.cpp index 803a0805c..9a2b89c97 100644 --- a/test-app/runtime/src/main/cpp/CallbackHandlers.cpp +++ b/test-app/runtime/src/main/cpp/CallbackHandlers.cpp @@ -56,9 +56,12 @@ void CallbackHandlers::Init(Isolate *isolate) { CURRENT_OBJECTID_FIELD_ID = env.GetFieldID(RUNTIME_CLASS, "currentObjectId", "I"); NS_CHECK(CURRENT_OBJECTID_FIELD_ID != nullptr); - MAKE_INSTANCE_STRONG_ID = env.GetMethodID(RUNTIME_CLASS, "makeInstanceStrong", - "(Ljava/lang/Object;I)V"); - NS_CHECK(MAKE_INSTANCE_STRONG_ID != nullptr); + CURRENT_OBJECTCLASS_FIELD_ID = env.GetFieldID(RUNTIME_CLASS, "currentObjectClass", "Ljava/lang/Class;"); + NS_CHECK(CURRENT_OBJECTCLASS_FIELD_ID != nullptr); + + REGISTER_CONSTRUCTED_INSTANCE_ID = env.GetMethodID(RUNTIME_CLASS, "registerConstructedInstance", + "(Ljava/lang/Object;I)J"); + NS_CHECK(REGISTER_CONSTRUCTED_INSTANCE_ID != nullptr); GET_TYPE_METADATA = env.GetStaticMethodID(RUNTIME_CLASS, "getTypeMetadata", "(Ljava/lang/String;I)[Ljava/lang/String;"); @@ -88,6 +91,7 @@ void CallbackHandlers::Init(Isolate *isolate) { namespace { struct JavaCallScope { explicit JavaCallScope(Runtime* runtime) : runtime_(runtime) { + runtime_->GetObjectManager()->FlushFinalizedBatchIfPending(); runtime_->EnterJavaCall(); } ~JavaCallScope() { @@ -97,6 +101,41 @@ struct JavaCallScope { }; } // namespace +// Publishes the instance JS is constructing to Runtime.initInstance for the duration of the +// Java constructor, and restores the enclosing construction afterwards: JS run by a +// callback the constructor makes may construct another instance. Raw JNI, so that the +// destructor never throws. +class CallbackHandlers::ConstructionScope { +public: + ConstructionScope(Runtime* runtime, int javaObjectId, jclass clazz) + : _runtime(runtime), _previous(runtime->CurrentConstruction()) { + Publish({javaObjectId, clazz}); + } + + ~ConstructionScope() { + Publish(_previous); + } + +private: + // The class is written only when it changes: initInstance reads it only while an id is + // published, so a stale one is harmless, and repeated constructions of one class then + // cost a single field write each way. + void Publish(const Runtime::Construction& construction) { + _runtime->CurrentConstruction() = construction; + JNIEnv* env = JEnv(); + jobject javaRuntime = _runtime->GetJavaRuntime(); + jclass& published = _runtime->PublishedConstructionClass(); + if (construction.id != -1 && construction.clazz != published) { + env->SetObjectField(javaRuntime, CURRENT_OBJECTCLASS_FIELD_ID, construction.clazz); + published = construction.clazz; + } + env->SetIntField(javaRuntime, CURRENT_OBJECTID_FIELD_ID, construction.id); + } + + Runtime* _runtime; + Runtime::Construction _previous; +}; + bool CallbackHandlers::RegisterInstance(Isolate *isolate, const Local &jsObject, const std::string &fullClassName, const ArgsWrapper &argWrapper, @@ -125,15 +164,12 @@ bool CallbackHandlers::RegisterInstance(Isolate *isolate, const Local &j objectManager->Link(jsObject, javaObjectID, nullptr); // resolve constructor - auto mi = MethodCache::ResolveConstructorSignature(argWrapper, fullClassName, + const auto &mi = MethodCache::ResolveConstructorSignature(argWrapper, fullClassName, generatedJavaClass, isInterface); - // while the "instance" is being created, if an exception is thrown during the construction - // this scope will guarantee the "javaObjectID" will be set to -1 and won't have an invalid value jobject instance; { - JavaObjectIdScope objIdScope(env, CURRENT_OBJECTID_FIELD_ID, runtime->GetJavaRuntime(), - javaObjectID); + ConstructionScope construction(runtime, javaObjectID, generatedJavaClass); if (argWrapper.type == ArgType::Interface) { instance = env.NewObject(generatedJavaClass, mi.mid); @@ -146,9 +182,10 @@ bool CallbackHandlers::RegisterInstance(Isolate *isolate, const Local &j } } - env.CallVoidMethod(runtime->GetJavaRuntime(), MAKE_INSTANCE_STRONG_ID, instance, javaObjectID); + jlong usedJavaMemory = env.CallLongMethod(runtime->GetJavaRuntime(), REGISTER_CONSTRUCTED_INSTANCE_ID, + instance, javaObjectID); - AdjustAmountOfExternalAllocatedMemory(env, isolate); + AdjustAmountOfExternalAllocatedMemory(isolate, usedJavaMemory); JniLocalRef localInstance(instance); success = !localInstance.IsNull(); @@ -252,10 +289,10 @@ void CallbackHandlers::CallJavaMethod(const Local &caller, const string jclass clazz; jmethodID mid; - string *sig = nullptr; - string *returnType = nullptr; + const string *sig = nullptr; + const string *returnType = nullptr; auto retType = MethodReturnType::Unknown; - MethodCache::CacheMethodInfo mi; + const MethodCache::CacheMethodInfo *mi = nullptr; auto isolate = args.GetIsolate(); @@ -338,8 +375,8 @@ void CallbackHandlers::CallJavaMethod(const Local &caller, const string clazz = env.FindClass(className); if (clazz != nullptr) { - mi = MethodCache::ResolveMethodSignature(className, methodName, args, isStatic); - if (mi.mid == nullptr) { + mi = &MethodCache::ResolveMethodSignature(className, methodName, args, isStatic); + if (mi->mid == nullptr) { DEBUG_WRITE("Cannot resolve class=%s, method=%s, isStatic=%d, isSuper=%d", className.c_str(), methodName.c_str(), isStatic, isSuper); return; @@ -349,8 +386,8 @@ void CallbackHandlers::CallJavaMethod(const Local &caller, const string const string callerClassName = callerNode->GetName(); DEBUG_WRITE("Resolving method on caller class: %s.%s on className %s", callerClassName.c_str(), methodName.c_str(), className.c_str()); - mi = MethodCache::ResolveMethodSignature(callerClassName, methodName, args, isStatic); - if (mi.mid == nullptr) { + mi = &MethodCache::ResolveMethodSignature(callerClassName, methodName, args, isStatic); + if (mi->mid == nullptr) { DEBUG_WRITE( "Cannot resolve class=%s, method=%s, isStatic=%d, isSuper=%d, callerClass=%s", className.c_str(), methodName.c_str(), isStatic, isSuper, @@ -359,11 +396,11 @@ void CallbackHandlers::CallJavaMethod(const Local &caller, const string } } - clazz = mi.clazz; - mid = mi.mid; - sig = &mi.signature; - returnType = &mi.returnType; - retType = mi.retType; + clazz = mi->clazz; + mid = mi->mid; + sig = &mi->signature; + returnType = &mi->returnType; + retType = mi->retType; } if (!isStatic) { @@ -560,13 +597,7 @@ void CallbackHandlers::CallJavaMethod(const Local &caller, const string if (isString) { objectResult = ArgConverter::jstringToV8String(isolate, (jstring) result); } else { - jint javaObjectID = objectManager->GetOrCreateObjectId(result); - objectResult = objectManager->GetJsObjectByJavaObject(javaObjectID); - - if (objectResult.IsEmpty()) { - objectResult = objectManager->CreateJSWrapper(javaObjectID, *returnType, - result); - } + objectResult = objectManager->GetOrCreateJSWrapper(result); } args.GetReturnValue().Set(objectResult); @@ -590,9 +621,9 @@ void CallbackHandlers::CallJavaMethod(const Local &caller, const string // } } -void CallbackHandlers::AdjustAmountOfExternalAllocatedMemory(JEnv &env, Isolate *isolate) { +void CallbackHandlers::AdjustAmountOfExternalAllocatedMemory(Isolate *isolate, int64_t usedJavaMemory) { auto runtime = Runtime::GetRuntime(isolate); - runtime->AdjustAmountOfExternalAllocatedMemory(); + runtime->AdjustAmountOfExternalAllocatedMemory(usedJavaMemory); runtime->TryCallGC(); } @@ -1030,7 +1061,7 @@ Local CallbackHandlers::CallJSMethod(Isolate *isolate, JNIEnv *_env, EscapableHandleScope handleScope(isolate); auto jsMethod = method.As(); - int argc = ArgConverter::GetJavaArgsLength(env, args); + int argc = args.count; constexpr int kInlineArgs = 16; Local inlineArguments[kInlineArgs]; @@ -1079,12 +1110,7 @@ Local CallbackHandlers::FindClass(Isolate *isolate, const string &classN auto runtime = Runtime::GetRuntime(isolate); auto objectManager = runtime->GetObjectManager(); - jint javaObjectID = objectManager->GetOrCreateObjectId(c); - clazz = objectManager->GetJsObjectByJavaObject(javaObjectID); - - if (clazz.IsEmpty()) { - clazz = objectManager->CreateJSWrapper(javaObjectID, "Ljava/lang/Class;", c); - } + clazz = objectManager->GetOrCreateJSWrapper(c); } return clazz; } @@ -1957,8 +1983,9 @@ short CallbackHandlers::MAX_JAVA_STRING_ARRAY_LENGTH = 100; jclass CallbackHandlers::RUNTIME_CLASS = nullptr; jclass CallbackHandlers::JAVA_LANG_STRING = nullptr; jfieldID CallbackHandlers::CURRENT_OBJECTID_FIELD_ID = nullptr; +jfieldID CallbackHandlers::CURRENT_OBJECTCLASS_FIELD_ID = nullptr; jmethodID CallbackHandlers::RESOLVE_CLASS_METHOD_ID = nullptr; -jmethodID CallbackHandlers::MAKE_INSTANCE_STRONG_ID = nullptr; +jmethodID CallbackHandlers::REGISTER_CONSTRUCTED_INSTANCE_ID = nullptr; jmethodID CallbackHandlers::GET_TYPE_METADATA = nullptr; jmethodID CallbackHandlers::ENABLE_VERBOSE_LOGGING_METHOD_ID = nullptr; jmethodID CallbackHandlers::DISABLE_VERBOSE_LOGGING_METHOD_ID = nullptr; diff --git a/test-app/runtime/src/main/cpp/CallbackHandlers.h b/test-app/runtime/src/main/cpp/CallbackHandlers.h index 239522cae..f78380cdd 100644 --- a/test-app/runtime/src/main/cpp/CallbackHandlers.h +++ b/test-app/runtime/src/main/cpp/CallbackHandlers.h @@ -168,7 +168,7 @@ namespace tns { CallbackHandlers() { } - static void AdjustAmountOfExternalAllocatedMemory(JEnv &env, v8::Isolate *isolate); + static void AdjustAmountOfExternalAllocatedMemory(v8::Isolate *isolate, int64_t usedJavaMemory); /* * Helper method that creates a java string array for sending strings over JNI @@ -187,7 +187,11 @@ namespace tns { static jfieldID CURRENT_OBJECTID_FIELD_ID; - static jmethodID MAKE_INSTANCE_STRONG_ID; + static jfieldID CURRENT_OBJECTCLASS_FIELD_ID; + + class ConstructionScope; + + static jmethodID REGISTER_CONSTRUCTED_INSTANCE_ID; static jmethodID GET_TYPE_METADATA; @@ -201,21 +205,6 @@ namespace tns { static FieldAccessor fieldAccessor; - struct JavaObjectIdScope { - JavaObjectIdScope(JEnv &env, jfieldID fieldId, jobject runtime, int javaObjectId) - : _env(env), _fieldID(fieldId), _runtime(runtime) { - _env.SetIntField(_runtime, _fieldID, javaObjectId); - } - - ~JavaObjectIdScope() { - _env.SetIntField(_runtime, _fieldID, -1); - } - - private: - JEnv _env; - jfieldID _fieldID; - jobject _runtime; - }; static std::atomic_int64_t count_; diff --git a/test-app/runtime/src/main/cpp/FieldAccessor.cpp b/test-app/runtime/src/main/cpp/FieldAccessor.cpp index 6837fa214..eacc45bc9 100644 --- a/test-app/runtime/src/main/cpp/FieldAccessor.cpp +++ b/test-app/runtime/src/main/cpp/FieldAccessor.cpp @@ -39,10 +39,10 @@ Local FieldAccessor::GetJavaField(Isolate* isolate, const Local& if (isStatic) { fieldData->clazz = env.FindClass(fieldMetadata.getDeclaringType()); - fieldData->fid = env.GetStaticFieldID(fieldData->clazz, fieldMetadata.name, fieldJniSig); + fieldData->fid = env.GetStaticFieldID(fieldData->clazz, fieldMetadata.getName(), fieldJniSig); } else { fieldData->clazz = env.FindClass(fieldMetadata.getDeclaringType()); - fieldData->fid = env.GetFieldID(fieldData->clazz, fieldMetadata.name, fieldJniSig); + fieldData->fid = env.GetFieldID(fieldData->clazz, fieldMetadata.getName(), fieldJniSig); } } @@ -51,7 +51,7 @@ Local FieldAccessor::GetJavaField(Isolate* isolate, const Local& if (targetJavaObject.IsNull()) { stringstream ss; - ss << "Cannot access property '" << fieldMetadata.name.c_str() << "' because there is no corresponding Java object"; + ss << "Cannot access property '" << fieldMetadata.getName().c_str() << "' because there is no corresponding Java object"; throw NativeScriptException(ss.str()); } } @@ -165,12 +165,7 @@ Local FieldAccessor::GetJavaField(Isolate* isolate, const Local& auto resultV8Value = ArgConverter::jstringToV8String(isolate, (jstring) result); fieldResult = handleScope.Escape(resultV8Value); } else { - int javaObjectID = objectManager->GetOrCreateObjectId(result); - auto objectResult = objectManager->GetJsObjectByJavaObject(javaObjectID); - - if (objectResult.IsEmpty()) { - objectResult = objectManager->CreateJSWrapper(javaObjectID, fieldTypeName, result); - } + auto objectResult = objectManager->GetOrCreateJSWrapper(result); fieldResult = handleScope.Escape(objectResult); } @@ -212,12 +207,12 @@ void FieldAccessor::SetJavaField(Isolate* isolate, const Local& target, if (isStatic) { fieldData->clazz = env.FindClass(fieldMetadata.getDeclaringType()); NS_DCHECK(fieldData->clazz != nullptr); - fieldData->fid = env.GetStaticFieldID(fieldData->clazz, fieldMetadata.name, fieldJniSig); + fieldData->fid = env.GetStaticFieldID(fieldData->clazz, fieldMetadata.getName(), fieldJniSig); NS_DCHECK(fieldData->fid != nullptr); } else { fieldData->clazz = env.FindClass(fieldMetadata.getDeclaringType()); NS_DCHECK(fieldData->clazz != nullptr); - fieldData->fid = env.GetFieldID(fieldData->clazz, fieldMetadata.name, fieldJniSig); + fieldData->fid = env.GetFieldID(fieldData->clazz, fieldMetadata.getName(), fieldJniSig); NS_DCHECK(fieldData->fid != nullptr); } } @@ -227,7 +222,7 @@ void FieldAccessor::SetJavaField(Isolate* isolate, const Local& target, if (targetJavaObject.IsNull()) { stringstream ss; - ss << "Cannot access property '" << fieldMetadata.name.c_str() << "' because there is no corresponding Java object"; + ss << "Cannot access property '" << fieldMetadata.getName().c_str() << "' because there is no corresponding Java object"; throw NativeScriptException(ss.str()); } } @@ -330,7 +325,7 @@ void FieldAccessor::SetJavaField(Isolate* isolate, const Local& target, result = ArgConverter::ConvertToJavaString(value); } else { if (value->IsProxy() && !ObjectManager::UnwrapProxy(value)->IsObject()) { - throw NativeScriptException("Cannot assign a revoked Proxy to field '" + fieldMetadata.name + "'"); + throw NativeScriptException("Cannot assign a revoked Proxy to field '" + fieldMetadata.getName() + "'"); } auto context = isolate->GetCurrentContext(); auto objectWithHiddenID = value->ToObject(context).ToLocalChecked(); diff --git a/test-app/runtime/src/main/cpp/JEnv.cpp b/test-app/runtime/src/main/cpp/JEnv.cpp index 28c29ce2e..f7104c3d8 100644 --- a/test-app/runtime/src/main/cpp/JEnv.cpp +++ b/test-app/runtime/src/main/cpp/JEnv.cpp @@ -33,13 +33,13 @@ JNIEnv* JEnv::GetEnvSlow() { } void JEnv::PinThreadEnv(JNIEnv* env) { - s_threadEnv = env; - s_threadEnvPinned = true; + s_thread.env = env; + s_thread.pinned = true; } void JEnv::UnpinThreadEnv() { - s_threadEnv = nullptr; - s_threadEnvPinned = false; + s_thread.env = nullptr; + s_thread.pinned = false; } JEnv::operator JNIEnv* () const { @@ -892,8 +892,7 @@ void JEnv::CheckForJavaException() { } JavaVM *JEnv::s_jvm = nullptr; -thread_local JNIEnv *JEnv::s_threadEnv = nullptr; -thread_local bool JEnv::s_threadEnvPinned = false; +thread_local JEnv::ThreadEnv JEnv::s_thread; robin_hood::unordered_map JEnv::s_classCache; robin_hood::unordered_map JEnv::s_missingClasses; jclass JEnv::RUNTIME_CLASS = nullptr; diff --git a/test-app/runtime/src/main/cpp/JEnv.h b/test-app/runtime/src/main/cpp/JEnv.h index 20d309a83..a9d8b6a8b 100644 --- a/test-app/runtime/src/main/cpp/JEnv.h +++ b/test-app/runtime/src/main/cpp/JEnv.h @@ -8,7 +8,7 @@ namespace tns { class JEnv { public: - JEnv() : m_env(s_threadEnv != nullptr ? s_threadEnv : GetEnvSlow()) { + JEnv() : m_env(CachedOrSlowEnv()) { } JEnv(JNIEnv* jniEnv) : m_env(jniEnv != nullptr ? jniEnv : JEnv().m_env) { @@ -346,6 +346,16 @@ class JEnv { static void UnpinThreadEnv(); + private: + // env is never filled from GetEnvSlow: its lazy attach is never detached by + // us, so nothing would tell us when the cached env goes stale. One struct + // because without ELF TLS (minSdk < 29) every thread_local costs a lookup. + struct ThreadEnv { + JNIEnv* env = nullptr; + bool pinned = false; + }; + + public: /* * Caches a JNI entry point's env for the duration of the call; leaves a * pinned env alone. The env passed to a native method is valid until @@ -354,15 +364,15 @@ class JEnv { class EntryScope { public: explicit EntryScope(JNIEnv* env) - : m_active(!s_threadEnvPinned), m_previous(s_threadEnv) { + : m_thread(s_thread), m_active(!m_thread.pinned), m_previous(m_thread.env) { if (m_active) { - s_threadEnv = env; + m_thread.env = env; } } ~EntryScope() { - if (m_active && !s_threadEnvPinned) { - s_threadEnv = m_previous; + if (m_active && !m_thread.pinned) { + m_thread.env = m_previous; } } @@ -370,6 +380,7 @@ class JEnv { EntryScope& operator=(const EntryScope&) = delete; private: + ThreadEnv& m_thread; bool m_active; JNIEnv* m_previous; }; @@ -377,14 +388,16 @@ class JEnv { private: static JNIEnv* GetEnvSlow(); + static JNIEnv* CachedOrSlowEnv() { + JNIEnv* env = s_thread.env; + return env != nullptr ? env : GetEnvSlow(); + } + void CheckForJavaException(); JNIEnv* m_env; - // Never filled from GetEnvSlow: its lazy attach is never detached by us, - // so nothing would tell us when the cached env goes stale. - static thread_local JNIEnv* s_threadEnv; - static thread_local bool s_threadEnvPinned; + static thread_local ThreadEnv s_thread; static JavaVM* s_jvm; diff --git a/test-app/runtime/src/main/cpp/MetadataNode.cpp b/test-app/runtime/src/main/cpp/MetadataNode.cpp index b54eea536..5691efcf7 100644 --- a/test-app/runtime/src/main/cpp/MetadataNode.cpp +++ b/test-app/runtime/src/main/cpp/MetadataNode.cpp @@ -27,6 +27,34 @@ using namespace v8; using namespace std; using namespace tns; +// Objects that get more members than this are moved to dictionary mode before the members are +// added: adding properties one by one to a fast-mode object copies its descriptors each time. +static constexpr size_t kManyMembers = 64; + +// V8 has no API for this; deleting a property other than the last one added does it. +static void MoveToDictionaryMode(Isolate* isolate, Local context, Local object) { + auto first = String::NewFromUtf8Literal(isolate, "__ns_dictionary_a"); + auto second = String::NewFromUtf8Literal(isolate, "__ns_dictionary_b"); + object->Set(context, first, v8::True(isolate)).Check(); + object->Set(context, second, v8::True(isolate)).Check(); + object->Delete(context, first).Check(); + object->Delete(context, second).Check(); +} + +// For everything but class constructors and static methods: V8 builds a prototype object and +// an instance map for each constructible API function it instantiates, which a method never uses. +static Local NewNonConstructor(Isolate* isolate, FunctionCallback callback, + Local data = Local()) { + return FunctionTemplate::New(isolate, callback, data, Local(), 0, + ConstructorBehavior::kThrow); +} + +// Property names are internalized by V8 anyway; creating them internalized skips a copy. +static Local NewMemberName(Isolate* isolate, std::string_view name) { + return String::NewFromUtf8(isolate, name.data(), NewStringType::kInternalized, + static_cast(name.size())).ToLocalChecked(); +} + void MetadataNode::Init(Isolate* isolate) { auto key = ArgConverter::ConvertToV8String(isolate, "tns::MetadataKey"); auto cache = GetMetadataNodeCache(isolate); @@ -97,8 +125,13 @@ static Local ResolveImplementationObject(Isolate* isolate, const Local& value, std::string& out) { - auto cache = GetMetadataNodeCache(isolate); - auto node = static_cast(TryReadPrivateExternal(isolate, value, cache->MetadataKey)); + MetadataNode* node; + if (ObjectManager::HasMetadataField(value)) { + node = GetInstanceMetadata(isolate, value); + } else { + auto cache = GetMetadataNodeCache(isolate); + node = static_cast(TryReadPrivateExternal(isolate, value, cache->MetadataKey)); + } if (node == nullptr) { return false; } @@ -161,14 +194,23 @@ Local MetadataNode::CreateExtendedJSWrapper(Isolate* isolate, ObjectMana auto cacheData = GetCachedExtendedClassData(isolate, proxyClassName); if (cacheData.node != nullptr) { - extInstance = objectManager->GetEmptyObject(isolate); - extInstance->SetInternalField(static_cast(ObjectManager::MetadataNodeKeys::CallSuper), True(isolate)); - auto extdCtorFunc = Local::New(isolate, *cacheData.extendedCtorFunction); + // Starts on the extended class's initial map, with its prototype, and without + // running its constructor callback. auto context = Runtime::GetRuntime(isolate)->GetContext(); - extInstance->SetPrototype(context, extdCtorFunc->Get(context, V8StringConstants::GetPrototype(isolate)).ToLocalChecked()); - extInstance->Set(context, ArgConverter::ConvertToV8String(isolate, "constructor"), extdCtorFunc); - - SetInstanceMetadata(isolate, extInstance, cacheData.node); + if (cacheData.instanceTemplate->Get(isolate)->NewInstance(context).ToLocal(&extInstance)) { + // V8 may copy instances from a cached instantiation of the template, which keeps the + // prototype the class had then. This only matters if code reassigns the extended + // class's `prototype` property, which nothing in the runtime or core does; consider + // dropping the check. + auto extdCtorFunc = Local::New(isolate, *cacheData.extendedCtorFunction); + Local prototype; + if (extdCtorFunc->Get(context, V8StringConstants::GetPrototype(isolate)).ToLocal(&prototype) && + !prototype->StrictEquals(extInstance->GetPrototype())) { + extInstance->SetPrototype(context, prototype); + } + extInstance->SetInternalField(static_cast(ObjectManager::MetadataNodeKeys::CallSuper), True(isolate)); + SetInstanceMetadata(isolate, extInstance, cacheData.node); + } } return extInstance; @@ -237,7 +279,7 @@ MetadataTreeNode* MetadataNode::GetOrCreateTreeNodeByName(const string& classNam return s_name2TreeNodeCache.emplace(className, result).first->second; } -string MetadataNode::GetName() { +const string& MetadataNode::GetName() { return m_name; } @@ -284,13 +326,20 @@ Local MetadataNode::CreateJSWrapper(Isolate* isolate, ObjectManager* obj if (m_isArray) { obj = CreateArrayWrapper(isolate); } else { - obj = objectManager->GetEmptyObject(isolate); - if (!obj.IsEmpty()) { - auto ctorFunc = GetConstructorFunction(isolate); - auto context = isolate->GetCurrentContext(); - obj->Set(context, ArgConverter::ConvertToV8String(isolate, "constructor"), ctorFunc); - obj->SetPrototype(context, ctorFunc->Get(context, V8StringConstants::GetPrototype(isolate)).ToLocalChecked()); - SetInstanceMetadata(isolate, obj, this); + auto& data = GetInstanceData(isolate); + if (data.instanceTemplate == nullptr) { + // Throws for a class missing at runtime, which has no template. + GetConstructorFunction(isolate); + auto cache = GetMetadataNodeCache(isolate); + auto ft = Local::New(isolate, *cache->CtorFuncCache.at(m_treeNode).ft); + data.instanceTemplate = new Persistent(isolate, ft->InstanceTemplate()); + } + // Starts on the class's initial map, with the constructor's prototype, and + // without running the constructor callback. + auto context = isolate->GetCurrentContext(); + if (data.instanceTemplate->Get(isolate)->NewInstance(context).ToLocal(&obj)) { + obj->SetInternalField(static_cast(ObjectManager::MetadataNodeKeys::Metadata), + data.metadata->Get(isolate)); } } @@ -339,10 +388,10 @@ Local MetadataNode::CreatePackageObject(Isolate* isolate) { auto ctx = isolate->GetCurrentContext(); auto extData = External::New(isolate, this, v8::kExternalPointerTypeTagDefault); - // Every child is a native data property, so anything that reads a property - // descriptor off a package object - console formatting, most notably - - // would materialize the whole subtree. The marker lets such callers - // recognize a package and stop. + // Every child is a lazy data property (replaced by its value on first read), + // so anything that reads a property descriptor off a package object - + // console formatting, most notably - would materialize the whole subtree. + // The marker lets such callers recognize a package and stop. auto cache = GetMetadataNodeCache(isolate); packageObj->SetPrivate(ctx, Private::ForApi(isolate, Local::New(isolate, *cache->PackageKey)), @@ -351,12 +400,15 @@ Local MetadataNode::CreatePackageObject(Isolate* isolate) { auto ptrChildren = this->m_treeNode->children; if (ptrChildren != nullptr) { const auto& children = *ptrChildren; + if (children.size() > kManyMembers) { + MoveToDictionaryMode(isolate, ctx, packageObj); + } for (auto childNode: children) { - packageObj->SetNativeDataProperty(ctx, - ArgConverter::ConvertToV8String(isolate, childNode->name), - PackageGetterCallback, - nullptr, - extData).ToChecked(); + packageObj->SetLazyDataProperty(ctx, + NewMemberName(isolate, childNode->name), + PackageGetterCallback, + extData, + PropertyAttribute::ReadOnly).ToChecked(); } } @@ -367,7 +419,7 @@ void MetadataNode::SetClassAccessor(Local& ctorFunction) { auto isolate = v8::Isolate::GetCurrent(); auto classFieldName = ArgConverter::ConvertToV8String(isolate, "class"); auto context = isolate->GetCurrentContext(); - auto classGetter = FunctionTemplate::New(isolate, ClassAccessorGetterCallback) + auto classGetter = NewNonConstructor(isolate, ClassAccessorGetterCallback) ->GetFunction(context).ToLocalChecked(); ctorFunction->SetAccessorProperty(classFieldName, classGetter, Local(), PropertyAttribute::DontDelete); @@ -408,7 +460,7 @@ void MetadataNode::NullObjectAccessorGetterCallback(const FunctionCallbackInfo(info.Data().As()->Value(v8::kExternalPointerTypeTagDefault)); V8SetPrivateValue(isolate, thiz, V8StringConstants::GetNullNodePrivate(isolate), External::New(isolate, node, v8::kExternalPointerTypeTagDefault)); - auto funcTemplate = FunctionTemplate::New(isolate, MetadataNode::NullValueOfCallback); + auto funcTemplate = NewNonConstructor(isolate, MetadataNode::NullValueOfCallback); auto context = isolate->GetCurrentContext(); thiz->Delete(context, V8StringConstants::GetValueOf(isolate)); thiz->Set(context, V8StringConstants::GetValueOf(isolate), funcTemplate->GetFunction(context).ToLocalChecked()); @@ -478,6 +530,51 @@ void MetadataNode::FieldAccessorGetterCallback(const FunctionCallbackInfo nsEx.ReThrowToV8(); } } +void MetadataNode::StaticFieldGetterCallback(Local property, const PropertyCallbackInfo& info) { + try { + auto fieldCallbackData = reinterpret_cast(info.Data().As()->Value(v8::kExternalPointerTypeTagDefault)); + if (fieldCallbackData->metadata.getDeclaringType() == "") { + info.GetReturnValue().SetUndefined(); + return; + } + auto isolate = info.GetIsolate(); + info.GetReturnValue().Set(CallbackHandlers::GetJavaField(isolate, info.Holder(), fieldCallbackData)); + } catch (NativeScriptException& e) { + e.ReThrowToV8(); + } catch (std::exception e) { + stringstream ss; + ss << "Error: c++ exception: " << e.what() << endl; + NativeScriptException nsEx(ss.str()); + nsEx.ReThrowToV8(); + } catch (...) { + NativeScriptException nsEx(std::string("Error: c++ exception!")); + nsEx.ReThrowToV8(); + } +} + +void MetadataNode::StaticFieldSetterCallback(Local property, Local value, const PropertyCallbackInfo& info) { + try { + auto fieldCallbackData = reinterpret_cast(info.Data().As()->Value(v8::kExternalPointerTypeTagDefault)); + auto &fieldCallbackMetadata = fieldCallbackData->metadata; + if (fieldCallbackMetadata.getIsFinal()) { + stringstream ss; + ss << "You are trying to set \"" << fieldCallbackMetadata.getName() << "\" which is a final field! Final fields can only be read."; + throw NativeScriptException(ss.str()); + } + CallbackHandlers::SetJavaField(info.GetIsolate(), info.Holder(), value, fieldCallbackData); + } catch (NativeScriptException& e) { + e.ReThrowToV8(); + } catch (std::exception e) { + stringstream ss; + ss << "Error: c++ exception: " << e.what() << endl; + NativeScriptException nsEx(ss.str()); + nsEx.ReThrowToV8(); + } catch (...) { + NativeScriptException nsEx(std::string("Error: c++ exception!")); + nsEx.ReThrowToV8(); + } +} + void MetadataNode::FieldAccessorSetterCallback(const FunctionCallbackInfo& info) { auto value = info[0]; try { @@ -651,9 +748,8 @@ class MetadataNode::PrototypeTemplateFiller { } Local funcData = External::New(isolate, methodInfo, v8::kExternalPointerTypeTagDefault); - Local funcTemplate = FunctionTemplate::New(isolate, MetadataNode::MethodCallback, funcData); - Local nameString = ArgConverter::ConvertToV8String(isolate, name); - m_protoTemplate->Set(nameString, funcTemplate); + Local funcTemplate = NewNonConstructor(isolate, MetadataNode::MethodCallback, funcData); + m_protoTemplate->Set(NewMemberName(isolate, name), funcTemplate); m_addedNames.insert(name); } @@ -665,11 +761,11 @@ class MetadataNode::PrototypeTemplateFiller { } Local fieldData = External::New(isolate, fieldInfo, v8::kExternalPointerTypeTagDefault); - Local nameString = ArgConverter::ConvertToV8String(isolate, name); + Local nameString = NewMemberName(isolate, name); m_protoTemplate->SetAccessorProperty( nameString, - FunctionTemplate::New(isolate, MetadataNode::FieldAccessorGetterCallback, fieldData), - FunctionTemplate::New(isolate, MetadataNode::FieldAccessorSetterCallback, fieldData), + NewNonConstructor(isolate, MetadataNode::FieldAccessorGetterCallback, fieldData), + NewNonConstructor(isolate, MetadataNode::FieldAccessorSetterCallback, fieldData), PropertyAttribute::DontDelete); m_addedNames.insert(name); } @@ -682,11 +778,11 @@ class MetadataNode::PrototypeTemplateFiller { } Local propertyData = External::New(isolate, propertyInfo, v8::kExternalPointerTypeTagDefault); - Local nameString = ArgConverter::ConvertToV8String(isolate, name); + Local nameString = NewMemberName(isolate, name); m_protoTemplate->SetAccessorProperty( nameString, - FunctionTemplate::New(isolate, MetadataNode::PropertyAccessorGetterCallback, propertyData), - FunctionTemplate::New(isolate, MetadataNode::PropertyAccessorSetterCallback, propertyData), + NewNonConstructor(isolate, MetadataNode::PropertyAccessorGetterCallback, propertyData), + NewNonConstructor(isolate, MetadataNode::PropertyAccessorSetterCallback, propertyData), PropertyAttribute::DontDelete); m_addedNames.insert(name); } @@ -745,6 +841,17 @@ vector MetadataNode::SetInstanceMethodsFromS robin_hood::unordered_map collectedExtensionMethodDatas; + // The base class is fully built, so its method names stay put while this class is. + robin_hood::unordered_flat_map baseMethodsByName; + baseMethodsByName.reserve(baseInstanceMethodsCallbackData.size()); + for (auto baseMethod: baseInstanceMethodsCallbackData) { + baseMethodsByName.emplace(baseMethod->candidates.front().name, baseMethod); + } + auto findBaseMethod = [&baseMethodsByName](const std::string &name) -> MethodCallbackData * { + auto itFound = baseMethodsByName.find(std::string_view(name)); + return itFound != baseMethodsByName.end() ? itFound->second : nullptr; + }; + auto extensionFunctionsCount = *reinterpret_cast(curPtr); curPtr += sizeof(uint16_t); for (auto i = 0; i < extensionFunctionsCount; i++) { @@ -792,18 +899,11 @@ vector MetadataNode::SetInstanceMethodsFromS instanceMethodData.push_back(callbackData); instanceMethodsCallbackData.push_back(callbackData); - auto itBegin = baseInstanceMethodsCallbackData.begin(); - auto itEnd = baseInstanceMethodsCallbackData.end(); - auto itFound = find_if(itBegin, itEnd, [&methodName](MethodCallbackData *x) { - return x->candidates.front().name == methodName; - }); - if (itFound != itEnd) { - callbackData->parent = *itFound; - } + callbackData->parent = findBaseMethod(methodName); if (s_profilerEnabled.load(std::memory_order_relaxed)) { Local funcData = External::New(isolate, callbackData, v8::kExternalPointerTypeTagDefault); - Local funcTemplate = FunctionTemplate::New(isolate, MethodCallback, funcData); + Local funcTemplate = NewNonConstructor(isolate, MethodCallback, funcData); auto func = funcTemplate->GetFunction(context).ToLocalChecked(); std::string origin = Constants::APP_ROOT_FOLDER_PATH + GetOrCreateInternal(treeNode)->m_name; Local wrapperFunc = Wrap(isolate, func, methodName, origin, @@ -979,9 +1079,10 @@ void MetadataNode::SetStaticMembers(Isolate* isolate, Local& ctorFunct if (methodName != lastMethodName) { callbackData = cache->Own(this); auto funcData = External::New(isolate, callbackData, v8::kExternalPointerTypeTagDefault); + // Constructible on purpose: `new X.staticFactory()` returns the factory's object. auto funcTemplate = FunctionTemplate::New(isolate, MethodCallback, funcData); auto func = funcTemplate->GetFunction(context).ToLocalChecked(); - auto funcName = ArgConverter::ConvertToV8String(isolate, methodName); + auto funcName = NewMemberName(isolate, methodName); ctorFunction->Set(context, funcName, Wrap(isolate, func, methodName, origin, false /* isCtorFunc */)); lastMethodName = methodName; } @@ -990,20 +1091,41 @@ void MetadataNode::SetStaticMembers(Isolate* isolate, Local& ctorFunct //attach .extend function auto extendFuncName = V8StringConstants::GetExtend(isolate); - auto extendFuncTemplate = FunctionTemplate::New(isolate, ExtendMethodCallback, External::New(isolate, this, v8::kExternalPointerTypeTagDefault)); + auto extendFuncTemplate = NewNonConstructor(isolate, ExtendMethodCallback, External::New(isolate, this, v8::kExternalPointerTypeTagDefault)); ctorFunction->Set(context, extendFuncName, extendFuncTemplate->GetFunction(context).ToLocalChecked()); //get candidates from static fields metadata auto staticFieldCout = *reinterpret_cast(curPtr); curPtr += sizeof(uint16_t); + // A class with many static fields (every android.R inner class) installs all of them + // on first touch. Native accessors skip creating a getter and a setter function per + // field, but adding them one by one to a fast-mode object copies its descriptors each + // time, so such a class is moved to dictionary mode first. Its final fields become + // their value on first read (read-only), since a native accessor on a dictionary-mode + // object is slower to call than reading a plain property. + bool manyFields = staticFieldCout > kManyMembers; + if (manyFields) { + MoveToDictionaryMode(isolate, context, ctorFunction); + } for (auto i = 0; i < staticFieldCout; i++) { auto entry = MetadataReader::ReadStaticFieldEntry(&curPtr); - auto fieldName = ArgConverter::ConvertToV8String(isolate, entry.getName()); + auto fieldName = NewMemberName(isolate, s_metadataReader.ReadNameView(entry.sfi->nameOffset)); auto fieldData = External::New(isolate, cache->Own(entry), v8::kExternalPointerTypeTagDefault); - auto fieldGetter = FunctionTemplate::New(isolate, FieldAccessorGetterCallback, fieldData) + if (manyFields) { + if (entry.getIsFinal()) { + ctorFunction->SetLazyDataProperty(context, fieldName, StaticFieldGetterCallback, fieldData, + static_cast(PropertyAttribute::ReadOnly | PropertyAttribute::DontDelete)).ToChecked(); + } else { + ctorFunction->SetNativeDataProperty(context, fieldName, StaticFieldGetterCallback, + StaticFieldSetterCallback, fieldData, + PropertyAttribute::DontDelete).ToChecked(); + } + continue; + } + auto fieldGetter = NewNonConstructor(isolate, FieldAccessorGetterCallback, fieldData) ->GetFunction(context).ToLocalChecked(); - auto fieldSetter = FunctionTemplate::New(isolate, FieldAccessorSetterCallback, fieldData) + auto fieldSetter = NewNonConstructor(isolate, FieldAccessorSetterCallback, fieldData) ->GetFunction(context).ToLocalChecked(); ctorFunction->SetAccessorProperty(fieldName, fieldGetter, fieldSetter, PropertyAttribute::DontDelete); @@ -1013,7 +1135,7 @@ void MetadataNode::SetStaticMembers(Isolate* isolate, Local& ctorFunct Local nullObjectData = External::New(isolate, this, v8::kExternalPointerTypeTagDefault); auto nullObjectGetter = - FunctionTemplate::New(isolate, NullObjectAccessorGetterCallback, nullObjectData) + NewNonConstructor(isolate, NullObjectAccessorGetterCallback, nullObjectData) ->GetFunction(context).ToLocalChecked(); ctorFunction->SetAccessorProperty(nullObjectName, nullObjectGetter); @@ -1026,12 +1148,13 @@ void MetadataNode::SetInnerTypes(v8::Isolate* isolate, Local& ctorFunc auto context = isolate->GetCurrentContext(); const auto &children = *treeNode->children; for (auto curChild: children) { - bool hasOwnProperty = ctorFunction->HasOwnProperty(context, ArgConverter::ConvertToV8String(isolate, curChild->name)).ToChecked(); + auto childName = NewMemberName(isolate, curChild->name); + bool hasOwnProperty = ctorFunction->HasOwnProperty(context, childName).ToChecked(); // Child is defined as a function already when the inner type is a companion object if (!hasOwnProperty) { ctorFunction->SetNativeDataProperty( context, - v8::String::NewFromUtf8(isolate, curChild->name.c_str()).ToLocalChecked(), + childName, InnerTypeAccessorGetterCallback, nullptr, v8::External::New(isolate, curChild, v8::kExternalPointerTypeTagDefault) ).ToChecked(); @@ -1192,6 +1315,11 @@ Local MetadataNode::GetConstructorFunctionTemplate(Isolate* is } Local MetadataNode::GetConstructorFunction(Isolate* isolate) { + auto cache = GetMetadataNodeCache(isolate); + auto itFound = cache->CtorFunctions.find(this); + if (itFound != cache->CtorFunctions.end()) { + return Local::New(isolate, *itFound->second); + } GetConstructorFunctionTemplate(isolate, m_treeNode); auto ctorFunc = Local::New(isolate, *GetPersistentConstructorFunction(isolate)); @@ -1225,7 +1353,24 @@ void MetadataNode::SetTypeMetadata(Isolate* isolate, Local value, Type V8SetPrivateValue(isolate, value, String::NewFromUtf8(isolate, "typemetadata").ToLocalChecked(), External::New(isolate, data, v8::kExternalPointerTypeTagDefault)); } +MetadataNode::InstanceData& MetadataNode::GetInstanceData(Isolate* isolate) { + auto cache = GetMetadataNodeCache(isolate); + auto& data = cache->Instances[this]; + if (data.metadata == nullptr) { + data.metadata = new Persistent(isolate, External::New(isolate, this, v8::kExternalPointerTypeTagDefault)); + } + return data; +} + MetadataNode* MetadataNode::GetInstanceMetadata(Isolate* isolate, const Local& value) { + if (ObjectManager::HasMetadataField(value)) { + auto field = value->GetInternalField(static_cast(ObjectManager::MetadataNodeKeys::Metadata)).As(); + if (!field->IsExternal()) { + return nullptr; + } + return static_cast(field.As()->Value(v8::kExternalPointerTypeTagDefault)); + } + MetadataNode* node = nullptr; auto cache = GetMetadataNodeCache(isolate); auto key = Local::New(isolate, *cache->MetadataKey); @@ -1240,6 +1385,14 @@ MetadataNode* MetadataNode::GetInstanceMetadata(Isolate* isolate, const Local object, MetadataNode* node) { + if (ObjectManager::HasMetadataField(object)) { + // node is null for `super` read through an object that is not a Java instance. + Local metadata = node == nullptr ? Local(Undefined(isolate)) + : node->GetInstanceData(isolate).metadata->Get(isolate); + object->SetInternalField(static_cast(ObjectManager::MetadataNodeKeys::Metadata), metadata); + return; + } + auto cache = GetMetadataNodeCache(isolate); auto key = Local::New(isolate, *cache->MetadataKey); V8SetPrivateValue(isolate, object, key, External::New(isolate, node, v8::kExternalPointerTypeTagDefault)); @@ -1850,7 +2003,7 @@ void MetadataNode::ExtendMethodCallback(const v8::FunctionCallbackInfoSetPrototype(context, baseClassCtorFunc->Get(context, prototypeName).ToLocalChecked()); auto superGetter = - FunctionTemplate::New(isolate, SuperAccessorGetterCallback, implementationObject) + NewNonConstructor(isolate, SuperAccessorGetterCallback, implementationObject) ->GetFunction(context).ToLocalChecked(); implementationObject->SetAccessorProperty(V8StringConstants::GetSuper(isolate), superGetter); @@ -1868,7 +2021,7 @@ void MetadataNode::ExtendMethodCallback(const v8::FunctionCallbackInfoInstanceTemplate(), fullExtendedName, node); cache->ExtendedCtorFuncCache.emplace(fullExtendedName, cacheData); if (frame.check()) { @@ -2402,7 +2555,7 @@ void MetadataNode::RegisterSymbolHasInstanceCallback(Isolate* isolate, MetadataE } auto extData = External::New(isolate, clazz, v8::kExternalPointerTypeTagDefault); - auto hasInstanceTemplate = FunctionTemplate::New(isolate, MetadataNode::SymbolHasInstanceCallback, extData); + auto hasInstanceTemplate = NewNonConstructor(isolate, MetadataNode::SymbolHasInstanceCallback, extData); auto context = isolate->GetCurrentContext(); auto hasInstanceFunc = hasInstanceTemplate->GetFunction(context).ToLocalChecked(); PropertyDescriptor descriptor(hasInstanceFunc, false); diff --git a/test-app/runtime/src/main/cpp/MetadataNode.h b/test-app/runtime/src/main/cpp/MetadataNode.h index ddb0a1a7d..8380285ef 100644 --- a/test-app/runtime/src/main/cpp/MetadataNode.h +++ b/test-app/runtime/src/main/cpp/MetadataNode.h @@ -41,7 +41,7 @@ class MetadataNode { static void EnableProfiler(bool enableProfiler); - std::string GetName(); + const std::string& GetName(); bool IsNodeTypeInterface(); @@ -139,6 +139,10 @@ class MetadataNode { static MetadataNode* GetInstanceMetadata(v8::Isolate* isolate, const v8::Local& value); + struct InstanceData; + // This runtime's per-instance data for this node, created on first use. + InstanceData& GetInstanceData(v8::Isolate* isolate); + static void SetInstanceMetadata(v8::Isolate* isolate, v8::Local value, MetadataNode* node); static TypeMetadata* GetTypeMetadata(v8::Isolate* isolate, const v8::Local& value); @@ -158,6 +162,10 @@ class MetadataNode { static void FieldAccessorGetterCallback(const v8::FunctionCallbackInfo& info); static void FieldAccessorSetterCallback(const v8::FunctionCallbackInfo& info); + + static void StaticFieldGetterCallback(v8::Local property, const v8::PropertyCallbackInfo& info); + + static void StaticFieldSetterCallback(v8::Local property, v8::Local value, const v8::PropertyCallbackInfo& info); static void PropertyAccessorGetterCallback(const v8::FunctionCallbackInfo& info); static void PropertyAccessorSetterCallback(const v8::FunctionCallbackInfo& info); @@ -265,6 +273,13 @@ class MetadataNode { std::string name; }; + // A node's metadata External is shared by all its instances, so marking an + // instance allocates nothing. instanceTemplate is set once a wrapper was made. + struct InstanceData { + v8::Persistent* metadata = nullptr; + v8::Persistent* instanceTemplate = nullptr; + }; + struct CtorCacheData { CtorCacheData(v8::Persistent* _ft, std::vector _instanceMethodCallbacks) : @@ -278,14 +293,16 @@ class MetadataNode { struct ExtendedClassCacheData { ExtendedClassCacheData() : - extendedCtorFunction(nullptr), node(nullptr) { + extendedCtorFunction(nullptr), instanceTemplate(nullptr), node(nullptr) { } - ExtendedClassCacheData(const v8::Local& extCtorFunc, const std::string& _extendedName, MetadataNode* _node) + ExtendedClassCacheData(const v8::Local& extCtorFunc, const v8::Local& _instanceTemplate, const std::string& _extendedName, MetadataNode* _node) : extendedName(_extendedName), node(_node) { extendedCtorFunction = new v8::Persistent(v8::Isolate::GetCurrent(), extCtorFunc); + instanceTemplate = new v8::Persistent(v8::Isolate::GetCurrent(), _instanceTemplate); } v8::Persistent* extendedCtorFunction; + v8::Persistent* instanceTemplate; std::string extendedName; MetadataNode* node; }; @@ -318,6 +335,8 @@ class MetadataNode { */ robin_hood::unordered_map*> CtorFunctions; + robin_hood::unordered_node_map Instances; + /* * Owns every callback payload this runtime hands to V8 as External * or FunctionTemplate data -- MethodCallbackData, FieldCallbackData, @@ -352,6 +371,10 @@ class MetadataNode { for (auto& entry : CtorFunctions) { delete entry.second; } + for (auto& entry : Instances) { + delete entry.second.metadata; + delete entry.second.instanceTemplate; + } /* * Freed from the maps rather than from CtorCacheData and * ExtendedClassCacheData themselves: both are held by value and @@ -364,6 +387,7 @@ class MetadataNode { } for (auto& entry : ExtendedCtorFuncCache) { delete entry.second.extendedCtorFunction; + delete entry.second.instanceTemplate; } } }; diff --git a/test-app/runtime/src/main/cpp/MetadataReader.h b/test-app/runtime/src/main/cpp/MetadataReader.h index 76ade1e1c..3683f2b55 100644 --- a/test-app/runtime/src/main/cpp/MetadataReader.h +++ b/test-app/runtime/src/main/cpp/MetadataReader.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include "NativeScriptAssert.h" #include "robin_hood.h" @@ -93,6 +94,12 @@ namespace tns { std::string ReadTypeName(MetadataTreeNode *treeNode); + inline std::string_view ReadNameView(uint32_t offset) { + uint16_t length = *reinterpret_cast(m_nameData + offset); + + return {reinterpret_cast(m_nameData + offset + sizeof(uint16_t)), length}; + } + inline std::string ReadName(uint32_t offset) { uint16_t length = *reinterpret_cast(m_nameData + offset); diff --git a/test-app/runtime/src/main/cpp/MethodCache.cpp b/test-app/runtime/src/main/cpp/MethodCache.cpp index 823e27160..2a73205e8 100644 --- a/test-app/runtime/src/main/cpp/MethodCache.cpp +++ b/test-app/runtime/src/main/cpp/MethodCache.cpp @@ -41,17 +41,23 @@ void MethodCache::Init() { NS_CHECK(RESOLVE_CONSTRUCTOR_SIGNATURE_ID != nullptr); } -MethodCache::CacheMethodInfo MethodCache::ResolveMethodSignature(const string& className, const string& methodName, const FunctionCallbackInfo& args, bool isStatic) { +// Reused for every lookup so a cache hit allocates nothing. +static thread_local std::string s_signatureKey; +static const MethodCache::CacheMethodInfo s_unresolved; + +const MethodCache::CacheMethodInfo& MethodCache::ResolveMethodSignature(const string& className, const string& methodName, const FunctionCallbackInfo& args, bool isStatic) { CacheMethodInfo method_info; - auto encoded_method_signature = EncodeSignature(className, methodName, args, isStatic); + EncodeSignature(s_signatureKey, className, methodName, args, isStatic); { std::shared_lock lock(signatureCacheMutex); - auto it = s_mthod_ctor_signature_cache.find(encoded_method_signature); + auto it = s_mthod_ctor_signature_cache.find(s_signatureKey); if (it != s_mthod_ctor_signature_cache.end()) { return it->second; } } + // Resolving calls into Java, which may call back into JS and reuse s_signatureKey. + string encoded_method_signature = s_signatureKey; auto signature = ResolveJavaMethod(args, className, methodName); @@ -72,24 +78,26 @@ MethodCache::CacheMethodInfo MethodCache::ResolveMethodSignature(const string& c env.GetMethodID(clazz, methodName, signature); std::unique_lock lock(signatureCacheMutex); - s_mthod_ctor_signature_cache.emplace(encoded_method_signature, method_info); + return s_mthod_ctor_signature_cache.emplace(encoded_method_signature, method_info).first->second; } - return method_info; + return s_unresolved; } -MethodCache::CacheMethodInfo MethodCache::ResolveConstructorSignature(const ArgsWrapper& argWrapper, const string& fullClassName, jclass javaClass, bool isInterface) { +const MethodCache::CacheMethodInfo& MethodCache::ResolveConstructorSignature(const ArgsWrapper& argWrapper, const string& fullClassName, jclass javaClass, bool isInterface) { CacheMethodInfo constructor_info; auto& args = argWrapper.args; - auto encoded_ctor_signature = EncodeSignature(fullClassName, "", args, false); + static const string kConstructorName = ""; + EncodeSignature(s_signatureKey, fullClassName, kConstructorName, args, false); { std::shared_lock lock(signatureCacheMutex); - auto it = s_mthod_ctor_signature_cache.find(encoded_ctor_signature); + auto it = s_mthod_ctor_signature_cache.find(s_signatureKey); if (it != s_mthod_ctor_signature_cache.end()) { return it->second; } } + string encoded_ctor_signature = s_signatureKey; auto signature = ResolveConstructor(args, javaClass, isInterface); @@ -102,17 +110,16 @@ MethodCache::CacheMethodInfo MethodCache::ResolveConstructorSignature(const Args constructor_info.mid = env.GetMethodID(javaClass, "", signature); std::unique_lock lock(signatureCacheMutex); - s_mthod_ctor_signature_cache.emplace(encoded_ctor_signature, constructor_info); + return s_mthod_ctor_signature_cache.emplace(encoded_ctor_signature, constructor_info).first->second; } - return constructor_info; + return s_unresolved; } // Encoded signature .S/I....<...> -string MethodCache::EncodeSignature(const string& className, const string& methodName, const FunctionCallbackInfo& args, bool isStatic) { +void MethodCache::EncodeSignature(string& sig, const string& className, const string& methodName, const FunctionCallbackInfo& args, bool isStatic) { int len = args.Length(); - string sig; - sig.reserve(className.size() + methodName.size() + 8 + len * 12); + sig.clear(); sig.append(className); sig.append(isStatic ? ".S." : ".I."); sig.append(methodName); @@ -121,14 +128,11 @@ string MethodCache::EncodeSignature(const string& className, const string& metho for (int i = 0; i < len; i++) { sig.append("."); - sig.append(GetType(args.GetIsolate(), args[i])); + AppendType(sig, args.GetIsolate(), args[i]); } - - return sig; } -string MethodCache::GetType(Isolate* isolate, const v8::Local& jsValue) { - string type; +void MethodCache::AppendType(string& type, Isolate* isolate, const v8::Local& jsValue) { // Overloads resolve against what a Proxy wraps. auto value = ObjectManager::UnwrapProxy(jsValue); @@ -141,48 +145,53 @@ string MethodCache::GetType(Isolate* isolate, const v8::Local& jsValu if (!nullNode.IsEmpty()) { auto treeNode = reinterpret_cast(nullNode.As()->Value(v8::kExternalPointerTypeTagDefault)); - type = (treeNode != nullptr) ? treeNode->GetName() : ""; + if (treeNode != nullptr) { + type += treeNode->GetName(); + } else { + type += ""; + } DEBUG_WRITE("Parameter of type %s with NULL value is passed to the method.", type.c_str()); - return type; + return; } } if (value->IsArray()) { - type = "array"; + type += "array"; } else if (value->IsArrayBuffer() || value->IsSharedArrayBuffer() || value->IsInt8Array() || value->IsUint8Array() || value->IsUint8ClampedArray()) { - type = "bytebuffer"; + type += "bytebuffer"; } else if (value->IsInt16Array() || value->IsUint16Array()) { - type = "shortbuffer"; + type += "shortbuffer"; } else if (value->IsInt32Array() || value->IsUint32Array()) { - type = "intbuffer"; + type += "intbuffer"; } else if (value->IsBigUint64Array() || value->IsBigInt64Array()) { - type = "longbuffer"; + type += "longbuffer"; } else if (value->IsFloat32Array()) { - type = "floatbuffer"; + type += "floatbuffer"; } else if (value->IsFloat64Array()) { - type = "doublebuffer"; + type += "doublebuffer"; } else if (value->IsBoolean() || value->IsBooleanObject() || value->IsFalse() || value->IsTrue()) { - type = "bool"; + type += "bool"; } else if (value->IsDataView()) { - type = "view"; + type += "view"; } else if (value->IsDate()) { - type = "date"; + type += "date"; } else if (value->IsFunction()) { - type = "function"; + type += "function"; } else if (value->IsInt32() || value->IsUint32()) { - type = "int"; + type += "int"; } else if (value->IsNull() || value->IsUndefined()) { - type = "null"; + type += "null"; } else if (value->IsString() || value->IsStringObject()) { - type = "string"; + type += "string"; } else if (value->IsNumber() || value->IsNumberObject()) { - auto context = isolate->GetCurrentContext(); - double d = value->NumberValue(context).ToChecked(); + // Not NumberValue: a user valueOf could call into Java and reuse the thread's key. + double d = value->IsNumberObject() ? value.As()->ValueOf() + : value.As()->Value(); int64_t i = (int64_t) d; bool isInteger = d == i; - type = isInteger ? "intnumber" : "doublenumber"; + type += isInteger ? "intnumber" : "doublenumber"; } else if (value->IsObject()) { auto context = isolate->GetCurrentContext(); auto object = value->ToObject(context).ToLocalChecked(); @@ -191,39 +200,42 @@ string MethodCache::GetType(Isolate* isolate, const v8::Local& jsValu switch (castType) { case CastType::Char: - type = "char"; + type += "char"; break; case CastType::Byte: - type = "byte"; + type += "byte"; break; case CastType::Short: - type = "short"; + type += "short"; break; case CastType::Long: - type = "long"; + type += "long"; break; case CastType::Float: - type = "float"; + type += "float"; break; case CastType::Double: - type = "double"; + type += "double"; break; case CastType::None: node = MetadataNode::GetNodeFromHandle(object); - type = (node != nullptr) ? node->GetName() : ""; + if (node != nullptr) { + type += node->GetName(); + } else { + type += ""; + } break; default: throw NativeScriptException("Unsupported cast type"); } } - return type; } string MethodCache::ResolveJavaMethod(const FunctionCallbackInfo& args, const string& className, const string& methodName) { @@ -276,7 +288,7 @@ string MethodCache::ResolveConstructor(const FunctionCallbackInfo& args, return resolvedSignature; } -robin_hood::unordered_map MethodCache::s_mthod_ctor_signature_cache; +robin_hood::unordered_node_map MethodCache::s_mthod_ctor_signature_cache; jclass MethodCache::RUNTIME_CLASS = nullptr; jmethodID MethodCache::RESOLVE_METHOD_OVERLOAD_METHOD_ID = nullptr; jmethodID MethodCache::RESOLVE_CONSTRUCTOR_SIGNATURE_ID = nullptr; diff --git a/test-app/runtime/src/main/cpp/MethodCache.h b/test-app/runtime/src/main/cpp/MethodCache.h index b253d2b37..0aac42904 100644 --- a/test-app/runtime/src/main/cpp/MethodCache.h +++ b/test-app/runtime/src/main/cpp/MethodCache.h @@ -32,17 +32,18 @@ class MethodCache { static void Init(); - static CacheMethodInfo ResolveMethodSignature(const std::string& className, const std::string& methodName, const v8::FunctionCallbackInfo& args, bool isStatic); + // The returned entry lives as long as the process; mid is null when nothing matched. + static const CacheMethodInfo& ResolveMethodSignature(const std::string& className, const std::string& methodName, const v8::FunctionCallbackInfo& args, bool isStatic); - static CacheMethodInfo ResolveConstructorSignature(const ArgsWrapper& argWrapper, const std::string& fullClassName, jclass javaClass, bool isInterface); + static const CacheMethodInfo& ResolveConstructorSignature(const ArgsWrapper& argWrapper, const std::string& fullClassName, jclass javaClass, bool isInterface); private: MethodCache() { } - static std::string EncodeSignature(const std::string& className, const std::string& methodName, const v8::FunctionCallbackInfo& args, bool isStatic); + static void EncodeSignature(std::string& out, const std::string& className, const std::string& methodName, const v8::FunctionCallbackInfo& args, bool isStatic); - static std::string GetType(v8::Isolate* isolate, const v8::Local& value); + static void AppendType(std::string& out, v8::Isolate* isolate, const v8::Local& value); static std::string ResolveJavaMethod(const v8::FunctionCallbackInfo& args, const std::string& className, const std::string& methodName); @@ -58,8 +59,9 @@ class MethodCache { * "s_mthod_ctor_signature_cache" holding all resolved CacheMethodInfo against an encoded_signature string. * Used for caching the resolved constructor or method signature. * The encoded signature has template: .S/I....<...> + * A node map, never erased from: callers keep references to its entries. */ - static robin_hood::unordered_map s_mthod_ctor_signature_cache; + static robin_hood::unordered_node_map s_mthod_ctor_signature_cache; }; } diff --git a/test-app/runtime/src/main/cpp/ObjectIdIndex.h b/test-app/runtime/src/main/cpp/ObjectIdIndex.h new file mode 100644 index 000000000..9966e6944 --- /dev/null +++ b/test-app/runtime/src/main/cpp/ObjectIdIndex.h @@ -0,0 +1,172 @@ +#ifndef OBJECTIDINDEX_H_ +#define OBJECTIDINDEX_H_ + +#include +#include +#include +#include +#include + +namespace tns { + +/* + * Object id -> entry, as a radix tree indexed by id: chunks of leaves of slots. Ids are + * unsigned 32-bit values carried in an int, -1 meaning none; they come from a per-runtime + * monotonic counter and are never reused, so a leaf or chunk that + * empties is freed unless it is the newest; an id linked after its leaf was freed just + * re-creates it. Leaves and chunks are small, so a long-lived entry keeps at most one of + * each alive. Not thread-safe: used under the isolate lock. + */ +template +class ObjectIdIndex { + public: + T* Find(int id) const { + if (id == kNoId) { + return nullptr; + } + size_t chunkIndex = Key(id) >> kChunkShift; + if (chunkIndex >= m_chunks.size()) { + return nullptr; + } + const Chunk* chunk = m_chunks[chunkIndex].get(); + if (chunk == nullptr) { + return nullptr; + } + const Leaf* leaf = chunk->leaves[LeafIndex(id)].get(); + return leaf == nullptr ? nullptr : leaf->slots[Key(id) & kLeafMask]; + } + + // Keeps an existing entry for id, like std::unordered_map::emplace; returns whether value + // was inserted. + bool Insert(int id, T* value) { + if (id == kNoId) { + return false; + } + size_t chunkIndex = Key(id) >> kChunkShift; + if (chunkIndex >= m_chunks.size()) { + m_chunks.resize(chunkIndex + 1); + } + auto& chunk = m_chunks[chunkIndex]; + if (chunk == nullptr) { + chunk = std::make_unique(); + } + auto& leaf = chunk->leaves[LeafIndex(id)]; + if (leaf == nullptr) { + leaf = std::make_unique(); + chunk->liveLeaves++; + } + T*& slot = leaf->slots[Key(id) & kLeafMask]; + size_t leafNumber = Key(id) >> kLeafBits; + if (leafNumber > m_newestLeaf) { + size_t previous = m_newestLeaf; + m_newestLeaf = leafNumber; + // Kept while it was the newest; nothing else frees it once it is empty. + ReleaseIfEmpty(previous); + } + if (slot != nullptr) { + return false; + } + slot = value; + leaf->live++; + return true; + } + + void Erase(int id) { + if (id == kNoId) { + return; + } + size_t chunkIndex = Key(id) >> kChunkShift; + if (chunkIndex >= m_chunks.size() || m_chunks[chunkIndex] == nullptr) { + return; + } + auto& chunk = m_chunks[chunkIndex]; + auto& leaf = chunk->leaves[LeafIndex(id)]; + if (leaf == nullptr) { + return; + } + T*& slot = leaf->slots[Key(id) & kLeafMask]; + if (slot == nullptr) { + return; + } + slot = nullptr; + leaf->live--; + size_t leafNumber = Key(id) >> kLeafBits; + if (leafNumber != m_newestLeaf) { + ReleaseIfEmpty(leafNumber); + } + } + + // Empties the index, then calls f on every entry it held. + template + void DrainAll(F&& f) { + auto chunks = std::move(m_chunks); + m_chunks.clear(); + m_newestLeaf = 0; + for (auto& chunk : chunks) { + if (chunk == nullptr) { + continue; + } + for (auto& leaf : chunk->leaves) { + if (leaf == nullptr) { + continue; + } + for (T* entry : leaf->slots) { + if (entry != nullptr) { + f(entry); + } + } + } + } + } + + private: + static constexpr int kLeafBits = 6; + static constexpr int kLeafMask = (1 << kLeafBits) - 1; + static constexpr int kChunkLeafBits = 6; + static constexpr int kChunkShift = kLeafBits + kChunkLeafBits; + + struct Leaf { + std::array slots{}; + uint32_t live = 0; + }; + + struct Chunk { + std::array, 1 << kChunkLeafBits> leaves; + uint32_t liveLeaves = 0; + }; + + // Frees leaf leafNumber if it is empty, then its chunk if that empties and is not the + // newest. + void ReleaseIfEmpty(size_t leafNumber) { + size_t chunkIndex = leafNumber >> kChunkLeafBits; + if (chunkIndex >= m_chunks.size() || m_chunks[chunkIndex] == nullptr) { + return; + } + auto& chunk = m_chunks[chunkIndex]; + auto& leaf = chunk->leaves[leafNumber & ((1 << kChunkLeafBits) - 1)]; + if (leaf == nullptr || leaf->live != 0) { + return; + } + leaf.reset(); + if (--chunk->liveLeaves == 0 && chunkIndex != (m_newestLeaf >> kChunkLeafBits)) { + chunk.reset(); + } + } + + static constexpr int kNoId = -1; + + static size_t Key(int id) { + return static_cast(id); + } + + static size_t LeafIndex(int id) { + return (Key(id) >> kLeafBits) & ((1 << kChunkLeafBits) - 1); + } + + std::vector> m_chunks; + size_t m_newestLeaf = 0; +}; + +} // namespace tns + +#endif /* OBJECTIDINDEX_H_ */ diff --git a/test-app/runtime/src/main/cpp/ObjectManager.cpp b/test-app/runtime/src/main/cpp/ObjectManager.cpp index b3411d7eb..164401677 100644 --- a/test-app/runtime/src/main/cpp/ObjectManager.cpp +++ b/test-app/runtime/src/main/cpp/ObjectManager.cpp @@ -4,6 +4,7 @@ #include #include "ArgConverter.h" +#include "JType.h" #include "ManualInstrumentation.h" #include "MetadataNode.h" #include "NativeScriptAssert.h" @@ -20,6 +21,7 @@ using namespace tns; ObjectManager::ObjectManager(jobject javaRuntimeObject) : m_javaRuntimeObject(javaRuntimeObject), + m_isolate(nullptr), m_currentObjectId(0), m_cache(NewWeakGlobalRefCallback, DeleteWeakGlobalRefCallback, ValidateWeakGlobalRefCallback, 1000, this) { @@ -53,9 +55,21 @@ void ObjectManager::InitializeJNI() { runtimeClass, "getOrCreateJavaObjectID", "(Ljava/lang/Object;)I"); NS_CHECK(GET_OR_CREATE_JAVA_OBJECT_ID_METHOD_ID != nullptr); - MAKE_INSTANCE_WEAK_AND_CHECK_IF_ALIVE_METHOD_ID = - env.GetMethodID(runtimeClass, "makeInstanceWeakAndCheckIfAlive", "(I)Z"); - NS_CHECK(MAKE_INSTANCE_WEAK_AND_CHECK_IF_ALIVE_METHOD_ID != nullptr); + GET_OR_CREATE_JAVA_OBJECT_ID_AND_CLASS_METHOD_ID = env.GetMethodID( + runtimeClass, "getOrCreateJavaObjectIDAndClass", "(Ljava/lang/Object;)J"); + NS_CHECK(GET_OR_CREATE_JAVA_OBJECT_ID_AND_CLASS_METHOD_ID != nullptr); + + FIND_JAVA_OBJECT_ID_METHOD_ID = env.GetMethodID( + runtimeClass, "findJavaObjectID", "(Ljava/lang/Object;)I"); + NS_CHECK(FIND_JAVA_OBJECT_ID_METHOD_ID != nullptr); + + MAKE_INSTANCE_STRONG_METHOD_ID = env.GetMethodID( + runtimeClass, "makeInstanceStrong", "(Ljava/lang/Object;I)V"); + NS_CHECK(MAKE_INSTANCE_STRONG_METHOD_ID != nullptr); + + PROCESS_FINALIZED_BATCH_METHOD_ID = + env.GetMethodID(runtimeClass, "processFinalizedBatch", "([II[II)V"); + NS_CHECK(PROCESS_FINALIZED_BATCH_METHOD_ID != nullptr); RELEASE_NATIVE_INSTANCE_METHOD_ID = env.GetMethodID(runtimeClass, "releaseNativeCounterpart", "(I)V"); @@ -82,6 +96,7 @@ void ObjectManager::Init(Isolate* isolate) { auto jsWrapperFunc = jsWrapperFuncTemplate->GetFunction(context).ToLocalChecked(); m_poJsWrapperFunc = new Persistent(isolate, jsWrapperFunc); + m_jsWrapperTemplate.Reset(isolate, jsWrapperFuncTemplate->InstanceTemplate()); } JniLocalRef ObjectManager::GetJavaObjectByJsObject( @@ -91,12 +106,12 @@ JniLocalRef ObjectManager::GetJavaObjectByJsObject( if (jsInstanceInfo != nullptr) { jweak existingObject; try { - existingObject = GetJavaObjectByID(jsInstanceInfo->JavaObjectID); + existingObject = GetJavaObjectByID(jsInstanceInfo->javaObjectID); } catch (const NativeScriptException& e) { // this captures usually the error - auto className = GetClassName(jsInstanceInfo->ObjectClazz); + auto className = GetClassName(jsInstanceInfo->objectClazz); throw NativeScriptException("Failed to get Java object by ID. id=" + - std::to_string(jsInstanceInfo->JavaObjectID) + + std::to_string(jsInstanceInfo->javaObjectID) + ", class=" + className + ". " + e.GetErrorMessage()); } @@ -169,9 +184,21 @@ MaybeLocal ObjectManager::ReadArrayThroughProxy(Local context, return scope.Escape(snapshot); } +int ObjectManager::LinkedIdOf(const Local& object) { + auto field = object->GetInternalField(static_cast(MetadataNodeKeys::JsInfo)).As(); + if (!field->IsUint32()) { + return -1; + } + return static_cast(field.As()->Value() - 1u); +} + +void ObjectManager::SetJsInfo(const Local& object, uint32_t javaObjectID) { + object->SetInternalField(static_cast(MetadataNodeKeys::JsInfo), + Integer::NewFromUnsigned(m_isolate, javaObjectID + 1)); +} + Local ObjectManager::GetJSInstanceInfoOwner(const Local& object) { - const int jsInfoIdx = static_cast(MetadataNodeKeys::JsInfo); - if (object->GetInternalField(jsInfoIdx).As()->IsExternal()) { + if (HasJsInfo(object)) { return object; } @@ -181,8 +208,7 @@ Local ObjectManager::GetJSInstanceInfoOwner(const Local& object) if (!prototypeObject.IsEmpty() && prototypeObject->IsObject()) { DEBUG_WRITE("GetJSInstanceInfo: need to check prototype :%d", prototypeObject->GetIdentityHash()); - if (IsJsRuntimeObject(prototypeObject) && - prototypeObject->GetInternalField(jsInfoIdx).As()->IsExternal()) { + if (IsJsRuntimeObject(prototypeObject) && HasJsInfo(prototypeObject)) { return prototypeObject; } } @@ -192,21 +218,31 @@ Local ObjectManager::GetJSInstanceInfoOwner(const Local& object) ObjectManager::JSInstanceInfo* ObjectManager::GetJSInstanceInfoFromRuntimeObject(const Local& object) { - HandleScope handleScope(m_isolate); - - auto owner = GetJSInstanceInfoOwner(object); - if (owner.IsEmpty()) { - return nullptr; + int id = LinkedIdOf(object); + if (id == -1) { + HandleScope handleScope(m_isolate); + auto owner = GetJSInstanceInfoOwner(object); + if (owner.IsEmpty()) { + return nullptr; + } + id = LinkedIdOf(owner); } - auto external = owner->GetInternalField(static_cast(MetadataNodeKeys::JsInfo)).As(); - return static_cast(external->Value(v8::kExternalPointerTypeTagDefault)); + auto state = m_idToObject.Find(id); + return state != nullptr && state->linked ? state : nullptr; } +// Array buffers and their views are linked to Java buffers through V8's own embedder +// fields, which must cover JsInfo and CallSuper; they have no Metadata field. +static_assert(V8_ARRAY_BUFFER_INTERNAL_FIELD_COUNT == + static_cast(ObjectManager::MetadataNodeKeys::Metadata)); +static_assert(V8_ARRAY_BUFFER_VIEW_INTERNAL_FIELD_COUNT == + static_cast(ObjectManager::MetadataNodeKeys::Metadata)); + bool ObjectManager::IsJsRuntimeObject(const v8::Local& object) { int internalFieldCount = object->InternalFieldCount(); - const int count = static_cast(MetadataNodeKeys::END); - return internalFieldCount == count; + return internalFieldCount == static_cast(MetadataNodeKeys::END) || + internalFieldCount == V8_ARRAY_BUFFER_INTERNAL_FIELD_COUNT; } jweak ObjectManager::GetJavaObjectByID(uint32_t javaObjectID) { @@ -217,6 +253,7 @@ jweak ObjectManager::GetJavaObjectByID(uint32_t javaObjectID) { jobject ObjectManager::GetJavaObjectByIDImpl(uint32_t javaObjectID) { JEnv env; + AssertIsolateLocked(); jobject object = env.CallObjectMethod( m_javaRuntimeObject, GET_JAVAOBJECT_BY_ID_METHOD_ID, javaObjectID); return object; @@ -230,37 +267,100 @@ jclass ObjectManager::GetJavaClass(const Local& instance) { DEBUG_WRITE("GetClass called"); JSInstanceInfo* jsInfo = GetJSInstanceInfo(instance); - jclass clazz = jsInfo->ObjectClazz; + jclass clazz = jsInfo->objectClazz; return clazz; } void ObjectManager::SetJavaClass(const Local& instance, jclass clazz) { JSInstanceInfo* jsInfo = GetJSInstanceInfo(instance); - jsInfo->ObjectClazz = clazz; + jsInfo->objectClazz = clazz; } int ObjectManager::GetOrCreateObjectId(jobject object) { JEnv env; + AssertIsolateLocked(); jint javaObjectID = env.CallIntMethod( m_javaRuntimeObject, GET_OR_CREATE_JAVA_OBJECT_ID_METHOD_ID, object); return javaObjectID; } +int ObjectManager::FindObjectId(jobject object) { + JEnv env; + AssertIsolateLocked(); + return env.CallIntMethod(m_javaRuntimeObject, FIND_JAVA_OBJECT_ID_METHOD_ID, + object); +} + +void ObjectManager::RegisterStrong(jobject object, int javaObjectID) { + JEnv env; + AssertIsolateLocked(); + env.CallVoidMethod(m_javaRuntimeObject, MAKE_INSTANCE_STRONG_METHOD_ID, + object, javaObjectID); +} + Local ObjectManager::GetJsObjectByJavaObject(int javaObjectID) { auto isolate = m_isolate; EscapableHandleScope handleScope(isolate); - auto it = m_idToObject.find(javaObjectID); - if (it == m_idToObject.end()) { + auto state = m_idToObject.Find(javaObjectID); + if (state == nullptr) { return handleScope.Escape(Local()); } - Persistent* jsObject = it->second->target; + return handleScope.Escape(JsObjectFor(state)); +} + +Local ObjectManager::JsObjectFor(ObjectWeakCallbackState* state) { + if (state->queued) { + state->reacquired = true; + } + return Local::New(m_isolate, *state->target); +} + +void ObjectManager::ReleaseUnwrappedArgs(const JavaCallArgs& args) { + if (args.converted || args.count == 0) { + return; + } + JEnv env; + JNIEnv* raw = env; + // Runs after the failure was rethrown to Java; keep that exception for the caller. + jthrowable pending = raw->ExceptionOccurred(); + raw->ExceptionClear(); + + jsize argc = args.count; + std::vector values(JavaCallArgs::PackedLength(argc)); + raw->GetLongArrayRegion(args.values, 0, values.size(), values.data()); + AssertIsolateLocked(); + for (jsize i = 0; i < argc; i++) { + if (JavaCallArgs::TypeAt(values.data(), argc, i) != Type::JsObject || + (values[i] & JavaCallArgs::FreshArg) == 0) { + continue; + } + auto javaObjectID = static_cast(values[i]); + if (HasJsObject(javaObjectID)) { + continue; + } + raw->CallVoidMethod(m_javaRuntimeObject, RELEASE_NATIVE_INSTANCE_METHOD_ID, + javaObjectID); + if (raw->ExceptionCheck()) { + raw->ExceptionClear(); + } + } + + if (pending != nullptr) { + raw->Throw(pending); + raw->DeleteLocalRef(pending); + } +} - auto localObject = Local::New(isolate, *jsObject); - return handleScope.Escape(localObject); +Local ObjectManager::GetLinkedJsObjectByJavaObject(int javaObjectID) { + auto state = m_idToObject.Find(javaObjectID); + if (state == nullptr || !state->linked) { + return Local(); + } + return JsObjectFor(state); } Local ObjectManager::CreateJSWrapper(jint javaObjectID, @@ -268,30 +368,86 @@ Local ObjectManager::CreateJSWrapper(jint javaObjectID, return CreateJSWrapperHelper(javaObjectID, typeName, nullptr); } -Local ObjectManager::CreateJSWrapper(jint javaObjectID, - const string& typeName, +Local ObjectManager::GetOrCreateJSWrapper(jobject object) { + JEnv env; + AssertIsolateLocked(); + jlong packed = env.CallLongMethod( + m_javaRuntimeObject, GET_OR_CREATE_JAVA_OBJECT_ID_AND_CLASS_METHOD_ID, + object); + auto javaObjectID = static_cast(packed); + auto wrapper = GetJsObjectByJavaObject(javaObjectID); + if (!wrapper.IsEmpty()) { + return wrapper; + } + return CreateJSWrapper(javaObjectID, static_cast(packed >> 32), object); +} + +Local ObjectManager::CreateJSWrapper(jint javaObjectID, int classIndex, jobject instance) { + jclass clazz; + std::string scratch; + MetadataNode* node = nullptr; + const auto& className = WrapperClassFor(classIndex, instance, clazz, scratch, node); + return CreateJSWrapperHelper(javaObjectID, className, clazz, node); +} + +const std::string& ObjectManager::WrapperClassFor(int classIndex, + jobject instance, + jclass& clazz, + std::string& scratch, + MetadataNode*& node) { + if (classIndex > 0 && + static_cast(classIndex) < m_wrapperClasses.size() && + m_wrapperClasses[classIndex] != nullptr) { + auto& entry = *m_wrapperClasses[classIndex]; + if (entry.node == nullptr) { + entry.node = MetadataNode::GetOrCreate(entry.name); + } + clazz = entry.clazz; + node = entry.node; + return entry.name; + } + JEnv env; - JniLocalRef clazz(env.GetObjectClass(instance)); + JniLocalRef localClass(env.GetObjectClass(instance)); + auto name = GetClassName(static_cast(localClass)); + if (classIndex <= 0) { + scratch = std::move(name); + clazz = nullptr; + return scratch; + } - return CreateJSWrapperHelper(javaObjectID, typeName, clazz); + // The object's own class, not whatever its name resolves to: they differ + // when two class loaders define the same name. + auto entry = std::make_unique(); + entry->name = std::move(name); + entry->clazz = static_cast(env.NewGlobalRef(localClass)); + if (static_cast(classIndex) >= m_wrapperClasses.size()) { + m_wrapperClasses.resize(classIndex + 1); + } + m_wrapperClasses[classIndex] = std::move(entry); + clazz = m_wrapperClasses[classIndex]->clazz; + return m_wrapperClasses[classIndex]->name; } Local ObjectManager::CreateJSWrapperHelper(jint javaObjectID, - const string& typeName, - jclass clazz) { + const string& className, + jclass clazz, + MetadataNode* node) { auto isolate = m_isolate; - auto className = (clazz != nullptr) ? GetClassName(clazz) : typeName; - - auto node = MetadataNode::GetOrCreate(className); + if (node == nullptr) { + node = MetadataNode::GetOrCreate(className); + } auto jsWrapper = node->CreateJSWrapper(isolate, this); if (!jsWrapper.IsEmpty()) { - JEnv env; - auto claz = env.FindClass(className); - Link(jsWrapper, javaObjectID, claz); + if (clazz == nullptr) { + JEnv env; + clazz = env.FindClass(className); + } + Link(jsWrapper, javaObjectID, clazz); } return jsWrapper; } @@ -311,24 +467,19 @@ void ObjectManager::Link(const Local& object, uint32_t javaObjectID, DEBUG_WRITE("Linking js object: %d and java instance id: %d", object->GetIdentityHash(), javaObjectID); - auto jsInstanceInfo = - new JSInstanceInfo(javaObjectID, clazz); - - auto objectHandle = new Persistent(isolate, object); - auto state = new ObjectWeakCallbackState(this, jsInstanceInfo, objectHandle); + auto state = new ObjectWeakCallbackState(this, javaObjectID, clazz, isolate, object); // subscribe for JS GC event - objectHandle->SetWeak(state, JSObjectFinalizerStatic, - WeakCallbackType::kFinalizer); - - auto jsInfoIdx = static_cast(MetadataNodeKeys::JsInfo); - - auto jsInfo = External::New(isolate, jsInstanceInfo, v8::kExternalPointerTypeTagDefault); + state->target->SetWeak(state, JSObjectFinalizerStatic, + WeakCallbackType::kFinalizer); - // link - object->SetInternalField(jsInfoIdx, jsInfo); + SetJsInfo(object, javaObjectID); - m_idToObject.emplace(javaObjectID, state); + bool inserted = m_idToObject.Insert(javaObjectID, state); + // A second link for one id would leave this state's finalizer weakening the first + // wrapper's Java object. + NS_DCHECK(inserted); + (void) inserted; } bool ObjectManager::CloneLink(const Local& src, @@ -384,37 +535,196 @@ void ObjectManager::JSObjectFinalizer(Isolate* isolate, ObjectWeakCallbackState* callbackState) { HandleScope handleScope(m_isolate); Persistent* po = callbackState->target; - auto jsInstanceInfo = GetJSInstanceInfoFromRuntimeObject(po->Get(m_isolate)); - if (jsInstanceInfo == nullptr) { + if (!callbackState->linked) { // The link was already torn down (ReleaseNativeCounterpart); nothing but // the registration is left to drop. - m_idToObject.erase(callbackState->javaObjectID); + m_idToObject.Erase(callbackState->javaObjectID); po->Reset(); - delete po; delete callbackState; return; } - auto javaObjectID = jsInstanceInfo->JavaObjectID; - JEnv env; - jboolean isJavaInstanceAlive = env.CallBooleanMethod( - m_javaRuntimeObject, MAKE_INSTANCE_WEAK_AND_CHECK_IF_ALIVE_METHOD_ID, - javaObjectID); - if (isJavaInstanceAlive) { - // If the Java instance is alive, keep the JavaScript instance alive. - po->SetWeak(callbackState, JSObjectFinalizerStatic, - WeakCallbackType::kFinalizer); + // Kept alive until the batch has asked the Java side whether it may die. + po->SetWeak(callbackState, JSObjectFinalizerStatic, + WeakCallbackType::kFinalizer); + + if (callbackState->queued) { + // Collected again before the flush, so whatever handed it back has let go. + callbackState->reacquired = false; + return; + } + callbackState->queued = true; + auto javaObjectID = static_cast(callbackState->javaObjectID); + (callbackState->javaWeakened ? m_pendingCheck : m_pendingWeaken) + .push_back(javaObjectID); + + // Never flushed from here: that would call into Java and free other + // wrappers' handles in the middle of V8's finalizer pass. The next JS<->Java + // crossing flushes; this task covers a thread that goes idle first, and is + // delayed so that a busy runtime does not wake its event loop per GC. + constexpr double kIdleFlushDelayMs = 4; + ScheduleFinalizedBatchFlush(kIdleFlushDelayMs); +} + +bool ObjectManager::ScheduleFinalizedBatchFlush(double delayMs) { + if (m_flushScheduled) { + return true; + } + auto runtime = Runtime::GetRuntime(m_isolate); + auto eventLoop = runtime->GetEventLoop(); + if (eventLoop == nullptr) { + return false; + } + m_flushScheduled = true; + int runtimeId = runtime->GetId(); + auto flush = [runtimeId]() { + // Runs on the runtime's own thread; skip if the runtime is gone. + auto current = Runtime::GetCurrentRuntime(); + if (current == nullptr || current->GetId() != runtimeId) { + return; + } + auto isolate = current->GetIsolate(); + v8::Locker locker(isolate); + Isolate::Scope isolateScope(isolate); + HandleScope handleScope(isolate); + current->GetObjectManager()->FlushFinalizedBatch(); + }; + if (delayMs > 0) { + eventLoop->PostInternalDelayed(std::move(flush), delayMs); } else { - // If the Java instance is dead, this JavaScript instance can be let die. - delete jsInstanceInfo; - auto jsInfoIdx = static_cast(MetadataNodeKeys::JsInfo); - po->Get(m_isolate)->SetInternalField(jsInfoIdx, Undefined(m_isolate)); - po->Reset(); - m_idToObject.erase(javaObjectID); - delete po; - delete callbackState; + eventLoop->PostInternal(std::move(flush)); + } + return true; +} + +void ObjectManager::FlushFinalizedBatch() { + m_flushScheduled = false; + m_flushBackingOff = false; + if (m_pendingWeaken.empty() && m_pendingCheck.empty()) { + return; + } + // A failed Java->JS call returns with its exception pending: JNI may not be used until it + // is thrown, and the flush must not mistake it for its own failure and clear it. + if (JEnv().ExceptionCheck()) { + ScheduleFinalizedBatchFlush(); + return; + } + HandleScope handleScope(m_isolate); + + auto weaken = std::move(m_pendingWeaken); + auto check = std::move(m_pendingCheck); + m_pendingWeaken.clear(); + m_pendingCheck.clear(); + + // A wrapper handed back to JS since it was queued is reachable again: its + // Java side keeps its current strength until a later collection. + auto keepCollected = [this](std::vector& ids) { + size_t kept = 0; + for (jint id : ids) { + ObjectWeakCallbackState* state = m_idToObject.Find(id); + if (state == nullptr) { + continue; + } + if (state->reacquired) { + state->reacquired = false; + state->queued = false; + continue; + } + ids[kept++] = id; + } + ids.resize(kept); + }; + keepCollected(weaken); + keepCollected(check); + if (weaken.empty() && check.empty()) { + return; } + + JEnv env; + JNIEnv* raw = env; + auto toJava = [raw](const std::vector& ids) -> jintArray { + if (ids.empty()) { + return nullptr; + } + auto count = static_cast(ids.size()); + jintArray array = raw->NewIntArray(count); + if (array != nullptr) { + raw->SetIntArrayRegion(array, 0, count, ids.data()); + } + return array; + }; + JniLocalRef weakenArray(toJava(weaken)); + JniLocalRef checkArray(toJava(check)); + bool failed = (!weaken.empty() && weakenArray.IsNull()) || + (!check.empty() && checkArray.IsNull()); + if (!failed) { + // Java overwrites the id of every object that is already dead with -1. + // Called without JEnv's check, which would throw into whichever JS call + // happens to be crossing. + AssertIsolateLocked(); + raw->CallVoidMethod(m_javaRuntimeObject, PROCESS_FINALIZED_BATCH_METHOD_ID, + (jintArray) weakenArray, + static_cast(weaken.size()), + (jintArray) checkArray, + static_cast(check.size())); + failed = raw->ExceptionCheck(); + } + if (failed) { + // Most likely an OutOfMemoryError. Every Java-side step is idempotent, so + // the whole batch is retried, after a pause so a persistent failure + // cannot spin the event loop. + if (raw->ExceptionCheck()) { + raw->ExceptionDescribe(); + raw->ExceptionClear(); + } + m_pendingWeaken.insert(m_pendingWeaken.end(), weaken.begin(), weaken.end()); + m_pendingCheck.insert(m_pendingCheck.end(), check.begin(), check.end()); + constexpr double kRetryDelayMs = 100; + m_flushBackingOff = ScheduleFinalizedBatchFlush(kRetryDelayMs); + return; + } + + auto settle = [this, raw](const std::vector& ids, jintArray array, + bool weakened) { + if (ids.empty()) { + return; + } + std::vector results(ids.size()); + raw->GetIntArrayRegion(array, 0, static_cast(ids.size()), + results.data()); + for (size_t i = 0; i < ids.size(); i++) { + ObjectWeakCallbackState* state = m_idToObject.Find(ids[i]); + if (state == nullptr) { + continue; + } + if (results[i] < 0) { + ReleaseCollectedWrapper(ids[i]); + continue; + } + state->queued = false; + if (weakened) { + state->javaWeakened = true; + } + } + }; + settle(weaken, (jintArray) weakenArray, true); + settle(check, (jintArray) checkArray, false); +} + +void ObjectManager::ReleaseCollectedWrapper(int javaObjectID) { + ObjectWeakCallbackState* state = m_idToObject.Find(javaObjectID); + if (state == nullptr) { + return; + } + Persistent* po = state->target; + if (state->linked) { + auto object = po->Get(m_isolate); + object->SetInternalField(static_cast(MetadataNodeKeys::JsInfo), Undefined(m_isolate)); + } + m_idToObject.Erase(javaObjectID); + po->Reset(); + delete state; } int ObjectManager::GenerateNewObjectID() { @@ -427,36 +737,33 @@ int ObjectManager::GenerateNewObjectID() { void ObjectManager::ReleaseAllRegistered() { HandleScope handleScope(m_isolate); + m_pendingWeaken.clear(); + m_pendingCheck.clear(); + auto jsInfoIdx = static_cast(MetadataNodeKeys::JsInfo); // Detached first: nothing below should observe a half-emptied map, and the // finalizers these handles were armed with will never run now anyway. - auto survivors = std::move(m_idToObject); - m_idToObject.clear(); - - for (auto& entry : survivors) { - ObjectWeakCallbackState* state = entry.second; + m_idToObject.DrainAll([this, jsInfoIdx](ObjectWeakCallbackState* state) { Persistent* po = state->target; if (!po->IsEmpty()) { auto local = po->Get(m_isolate); if (!local.IsEmpty()) { - // Drop the back-pointer before the JSInstanceInfo goes away. local->SetInternalField(jsInfoIdx, Undefined(m_isolate)); } po->Reset(); } - delete po; - delete state->jsInfo; delete state; - } + }); if (m_poJsWrapperFunc != nullptr) { m_poJsWrapperFunc->Reset(); delete m_poJsWrapperFunc; m_poJsWrapperFunc = nullptr; } + m_jsWrapperTemplate.Reset(); } ObjectManager::~ObjectManager() { @@ -466,6 +773,14 @@ ObjectManager::~ObjectManager() { // objects abandoned the whole cache when it died. ART's weak-global table is // bounded, so enough worker cycles turned that leak into an abort. m_cache.clear(); + + JEnv env; + for (const auto& entry : m_wrapperClasses) { + if (entry != nullptr) { + env.DeleteGlobalRef(entry->clazz); + } + } + m_wrapperClasses.clear(); } jweak ObjectManager::NewWeakGlobalRefCallback(const int& javaObjectID, @@ -494,9 +809,9 @@ bool ObjectManager::ValidateWeakGlobalRefCallback(const int& javaObjectID, } Local ObjectManager::GetEmptyObject(Isolate* isolate) { - auto emptyObjCtorFunc = Local::New(isolate, *m_poJsWrapperFunc); + // The same object `new` on m_poJsWrapperFunc makes, without entering its callback. auto context = Runtime::GetRuntime(isolate)->GetContext(); - auto val = emptyObjCtorFunc->CallAsConstructor(context, 0, nullptr); + auto val = m_jsWrapperTemplate.Get(isolate)->NewInstance(context); if (val.IsEmpty()) { return Local(); } @@ -522,7 +837,7 @@ void ObjectManager::ReleaseNativeCounterpart(v8::Local& object) { } auto instance = source.As(); - // A `super` object shares its owner's JsInfo External; the owner is the one + // A `super` object shares its owner's JsInfo link; the owner is the one // registered, so the release is resolved and recorded there. Local superOwner; V8GetPrivateValue(m_isolate, instance, ArgConverter::ConvertToV8String(m_isolate, "superowner"), superOwner); @@ -539,25 +854,23 @@ void ObjectManager::ReleaseNativeCounterpart(v8::Local& object) { } auto jsInfoIdx = static_cast(MetadataNodeKeys::JsInfo); - auto jsInstanceInfo = static_cast( - owner->GetInternalField(jsInfoIdx).As()->Value(v8::kExternalPointerTypeTagDefault)); + auto javaObjectID = LinkedIdOf(owner); JEnv env; + AssertIsolateLocked(); env.CallVoidMethod(m_javaRuntimeObject, RELEASE_NATIVE_INSTANCE_METHOD_ID, - jsInstanceInfo->JavaObjectID); + javaObjectID); // The registration outlives the link: it is dropped by the finalizer once - // the wrapper is collected. Until then the entry must not point at the - // JSInstanceInfo being freed here. - auto it = m_idToObject.find(jsInstanceInfo->JavaObjectID); - if (it != m_idToObject.end()) { - it->second->jsInfo = nullptr; + // the wrapper is collected. + auto state = m_idToObject.Find(javaObjectID); + if (state != nullptr) { + state->linked = false; } - delete jsInstanceInfo; owner->SetInternalField(jsInfoIdx, Undefined(m_isolate)); - // Every alias of the freed External must be cleared with it. + // Every alias of the link is cleared with it. Local aliasesValue; V8GetPrivateValue(m_isolate, owner, ArgConverter::ConvertToV8String(m_isolate, "superaliases"), aliasesValue); if (!aliasesValue.IsEmpty() && aliasesValue->IsArray()) { @@ -577,6 +890,9 @@ jclass ObjectManager::JAVA_LANG_CLASS = nullptr; jmethodID ObjectManager::GET_NAME_METHOD_ID = nullptr; jmethodID ObjectManager::GET_JAVAOBJECT_BY_ID_METHOD_ID = nullptr; jmethodID ObjectManager::GET_OR_CREATE_JAVA_OBJECT_ID_METHOD_ID = nullptr; -jmethodID ObjectManager::MAKE_INSTANCE_WEAK_AND_CHECK_IF_ALIVE_METHOD_ID = +jmethodID ObjectManager::GET_OR_CREATE_JAVA_OBJECT_ID_AND_CLASS_METHOD_ID = nullptr; +jmethodID ObjectManager::FIND_JAVA_OBJECT_ID_METHOD_ID = nullptr; +jmethodID ObjectManager::MAKE_INSTANCE_STRONG_METHOD_ID = nullptr; +jmethodID ObjectManager::PROCESS_FINALIZED_BATCH_METHOD_ID = nullptr; jmethodID ObjectManager::RELEASE_NATIVE_INSTANCE_METHOD_ID = nullptr; diff --git a/test-app/runtime/src/main/cpp/ObjectManager.h b/test-app/runtime/src/main/cpp/ObjectManager.h index e2a0e6b94..5f94f1385 100644 --- a/test-app/runtime/src/main/cpp/ObjectManager.h +++ b/test-app/runtime/src/main/cpp/ObjectManager.h @@ -2,6 +2,7 @@ #define OBJECTMANAGER_H_ #include +#include #include #include #include @@ -11,9 +12,14 @@ #include "JEnv.h" #include "JniLocalRef.h" #include "LRUCache.h" +#include "NativeScriptAssert.h" +#include "ObjectIdIndex.h" #include "v8.h" namespace tns { +struct JavaCallArgs; +class MetadataNode; + class ObjectManager { public: ObjectManager(jobject javaRuntimeObject); @@ -44,13 +50,44 @@ class ObjectManager { void SetJavaClass(const v8::Local& instance, jclass clazz); int GetOrCreateObjectId(jobject object); + // The id object is registered under, or -1. + int FindObjectId(jobject object); + + void RegisterStrong(jobject object, int javaObjectID); + v8::Local GetJsObjectByJavaObject(int javaObjectID); + // Settles the wrappers collected since the last flush. Called on JS<->Java calls and + // constructions; field and array-element reads that hand JS a Java object skip it and + // rely on the delayed flush task. + void FlushFinalizedBatchIfPending() { + if (!m_flushBackingOff && + (!m_pendingWeaken.empty() || !m_pendingCheck.empty())) { + FlushFinalizedBatch(); + } + } + + // Whether a wrapper was ever created for this id and not yet collected. + bool HasJsObject(int javaObjectID) const { + return m_idToObject.Find(javaObjectID) != nullptr; + } + + // After a failed Java->JS call: drops the Java registration of every object Java + // registered for the call (JavaCallArgs::FreshArg) that never got a wrapper. + void ReleaseUnwrappedArgs(const JavaCallArgs& args); + + // As GetJsObjectByJavaObject, but empty once the wrapper's link to the Java object was + // released. + v8::Local GetLinkedJsObjectByJavaObject(int javaObjectID); + v8::Local CreateJSWrapper(jint javaObjectID, const std::string& typeName); - v8::Local CreateJSWrapper(jint javaObjectID, - const std::string& typeName, + // The wrapper of object, registering it and creating the wrapper if needed. + v8::Local GetOrCreateJSWrapper(jobject object); + + // classIndex is Runtime.classIndexOf(instance's class), or 0 when unknown. + v8::Local CreateJSWrapper(jint javaObjectID, int classIndex, jobject instance); void Link(const v8::Local& object, uint32_t javaObjectID, @@ -64,10 +101,17 @@ class ObjectManager { /* * Whether GetInternalField is safe to call on this exact object. False for * a Proxy even when its target qualifies; unwrap first where a Proxy should - * be accepted. + * be accepted. Any object with this many internal fields is read as a runtime + * object, so embedder code must never store an aligned pointer in field 0 of + * an object with 2 or 3 internal fields. */ static bool IsJsRuntimeObject(const v8::Local& object); + // Whether object is a runtime object with a Metadata field: not an array buffer. + static bool HasMetadataField(const v8::Local& object) { + return object->InternalFieldCount() == static_cast(MetadataNodeKeys::END); + } + /* * The runtime object holding the JsInfo External for `object`: the object * itself, or its prototype when the TypeScript layout keeps the registered @@ -76,6 +120,12 @@ class ObjectManager { */ static v8::Local GetJSInstanceInfoOwner(const v8::Local& object); + // Whether a runtime object's JsInfo field holds a link. The field holds id + 1, so + // the zero V8 stores in a typed array's embedder fields never reads as a link. + static bool HasJsInfo(const v8::Local& object) { + return LinkedIdOf(object) != -1; + } + /* * The innermost target of a Proxy chain; any other value is returned as is. * A revoked Proxy anywhere in the chain yields null. Internal fields and @@ -101,43 +151,81 @@ class ObjectManager { v8::Local GetEmptyObject(v8::Isolate* isolate); - enum class MetadataNodeKeys { JsInfo, CallSuper, END }; + enum class MetadataNodeKeys { JsInfo, CallSuper, Metadata, END }; private: static void InitializeJNI(); - struct JSInstanceInfo { - public: - JSInstanceInfo(uint32_t javaObjectID, jclass claz) - : JavaObjectID(javaObjectID), ObjectClazz(claz) {} - - uint32_t JavaObjectID; - jclass ObjectClazz; - }; + // The registration of one linked Java object id, reached from the wrapper's JsInfo + // field through m_idToObject. struct ObjectWeakCallbackState { - ObjectWeakCallbackState(ObjectManager* _thisPtr, JSInstanceInfo* _jsInfo, - v8::Persistent* _target) + ObjectWeakCallbackState(ObjectManager* _thisPtr, uint32_t _javaObjectID, + jclass clazz, v8::Isolate* isolate, + v8::Local object) : thisPtr(_thisPtr), - jsInfo(_jsInfo), - target(_target), - javaObjectID(_jsInfo->JavaObjectID) {} + handle(isolate, object), + target(&handle), + javaObjectID(_javaObjectID), + objectClazz(clazz) {} ObjectManager* thisPtr; - JSInstanceInfo* jsInfo; + // Persistent, not Global: its destructor never calls into V8, so a state may be + // freed after its isolate is gone. + v8::Persistent handle; v8::Persistent* target; - // Duplicated from jsInfo: the finalizer has to unregister itself even when - // the JsInfo field was already cleared and jsInfo freed. uint32_t javaObjectID; + jclass objectClazz; + // Cleared by ReleaseNativeCounterpart; the registration itself lives until the + // wrapper is collected. + bool linked = true; + // The Java side holds the object weakly; a finalization now only asks + // whether it is still alive. + bool javaWeakened = false; + // Queued in a finalizer batch that has not been flushed yet. + bool queued = false; + // Handed back to JS while queued: the wrapper is reachable again, so the flush must + // leave the Java side alone and let a later collection queue it anew. + bool reacquired = false; }; + using JSInstanceInfo = ObjectWeakCallbackState; + + // The linked state of object, or null. JSInstanceInfo* GetJSInstanceInfo(const v8::Local& object); JSInstanceInfo* GetJSInstanceInfoFromRuntimeObject( const v8::Local& object); + // The id in a runtime object's JsInfo field (an unsigned 32-bit id carried in an int), + // or -1. + static int LinkedIdOf(const v8::Local& object); + + void SetJsInfo(const v8::Local& object, uint32_t javaObjectID); + + // clazz must be a global ref, or null to look className up. v8::Local CreateJSWrapperHelper(jint javaObjectID, - const std::string& typeName, - jclass clazz); + const std::string& className, + jclass clazz, + MetadataNode* node = nullptr); + + struct WrapperClass { + std::string name; + jclass clazz; // global ref + MetadataNode* node = nullptr; // process-wide, never freed + }; + + // The JNI name and a global ref of instance's class, cached by class index. + // node is set only for a cached class. + const std::string& WrapperClassFor(int classIndex, jobject instance, + jclass& clazz, std::string& scratch, + MetadataNode*& node); + + // Indexed by class index. Entries are never replaced, so references to them + // survive a nested wrap growing the vector. + std::vector> m_wrapperClasses; + + // state's wrapper, marking a queued one as handed back to JS. + v8::Local JsObjectFor(ObjectWeakCallbackState* state); static void JSObjectFinalizerStatic( const v8::WeakCallbackInfo& data); @@ -145,6 +233,27 @@ class ObjectManager { void JSObjectFinalizer(v8::Isolate* isolate, ObjectWeakCallbackState* callbackState); + // The Java object table may only be touched by the thread holding the + // isolate lock; every upcall into it checks that in debug builds. + void AssertIsolateLocked() const { + NS_DCHECK(m_isolate == nullptr || v8::Locker::IsLocked(m_isolate)); + } + + void FlushFinalizedBatch(); + + // Whether a flush task is pending afterwards; false on a thread with no event loop. + bool ScheduleFinalizedBatchFlush(double delayMs = 0); + + void ReleaseCollectedWrapper(int javaObjectID); + + // Ids whose JS wrapper was collected, waiting for one JNI call that weakens + // (first collection) or liveness-checks (later ones) their Java objects. + std::vector m_pendingWeaken; + std::vector m_pendingCheck; + bool m_flushScheduled = false; + // A flush failed; crossings leave the retry to the delayed task. + bool m_flushBackingOff = false; + jweak GetJavaObjectByID(uint32_t javaObjectID); jobject GetJavaObjectByIDImpl(uint32_t javaObjectID); @@ -163,7 +272,7 @@ class ObjectManager { v8::Isolate* m_isolate; - std::unordered_map m_idToObject; + ObjectIdIndex m_idToObject; LRUCache m_cache; @@ -179,11 +288,18 @@ class ObjectManager { static jmethodID GET_OR_CREATE_JAVA_OBJECT_ID_METHOD_ID; - static jmethodID MAKE_INSTANCE_WEAK_AND_CHECK_IF_ALIVE_METHOD_ID; + static jmethodID GET_OR_CREATE_JAVA_OBJECT_ID_AND_CLASS_METHOD_ID; + + static jmethodID FIND_JAVA_OBJECT_ID_METHOD_ID; + + static jmethodID MAKE_INSTANCE_STRONG_METHOD_ID; + + static jmethodID PROCESS_FINALIZED_BATCH_METHOD_ID; static jmethodID RELEASE_NATIVE_INSTANCE_METHOD_ID; v8::Persistent* m_poJsWrapperFunc; + v8::Persistent m_jsWrapperTemplate; }; } // namespace tns diff --git a/test-app/runtime/src/main/cpp/Runtime.cpp b/test-app/runtime/src/main/cpp/Runtime.cpp index dd45ea3d4..93e7c9439 100644 --- a/test-app/runtime/src/main/cpp/Runtime.cpp +++ b/test-app/runtime/src/main/cpp/Runtime.cpp @@ -134,7 +134,7 @@ Runtime::Runtime(JNIEnv* env, jobject runtime, int id) m_runGC(false) { // GetRuntime(int) and the thread's pinned JNIEnv assume one runtime per // thread; com.tns.Runtime's constructor enforces the same on the Java side. - NS_CHECK(s_currentRuntime == nullptr); + NS_CHECK(s_current.runtime == nullptr); m_runtime = env->NewGlobalRef(runtime); m_objectManager = new ObjectManager(m_runtime); @@ -143,22 +143,14 @@ Runtime::Runtime(JNIEnv* env, jobject runtime, int id) std::lock_guard lock(s_runtimeCacheMutex); s_id2RuntimeCache.emplace(id, this); } - - if (GET_USED_MEMORY_METHOD_ID == nullptr) { - auto RUNTIME_CLASS = env->FindClass("com/tns/Runtime"); - NS_CHECK(RUNTIME_CLASS != nullptr); - - GET_USED_MEMORY_METHOD_ID = - env->GetMethodID(RUNTIME_CLASS, "getUsedMemory", "()J"); - NS_CHECK(GET_USED_MEMORY_METHOD_ID != nullptr); - } } Runtime* Runtime::GetRuntime(int runtimeId) { // A runtime is only destroyed on its own thread, which is busy in this call, // so the calling thread's own runtime needs neither the lock nor the map. - if (runtimeId == s_currentRuntimeId) { - return s_currentRuntime; + auto& current = s_current; + if (runtimeId == current.id) { + return current.runtime; } Runtime* runtime = nullptr; @@ -298,9 +290,9 @@ void Runtime::Init(JNIEnv* env, jstring filesPath, jstring nativeLibDir, Runtime::~Runtime() { NS_CHECK(std::this_thread::get_id() == m_ownerThread); - if (s_currentRuntime == this) { - s_currentRuntime = nullptr; - s_currentRuntimeId = -1; + if (s_current.runtime == this) { + s_current.runtime = nullptr; + s_current.id = -1; } /* @@ -605,8 +597,34 @@ static bool TryGetPrimitiveReturnBits(Isolate* isolate, } } +namespace { +// Indexed by the id Java's MethodNameIds gives each method name. +struct CallbackMethodNames { + std::vector> byId; +}; + +Local GetCallbackMethodName(Isolate* isolate, JNIEnv* env, + jstring methodName, jint methodNameId) { + auto* names = RuntimeState::For(isolate); + if (names == nullptr || methodNameId < 0) { + return ArgConverter::jstringToInternalizedV8String(isolate, env, methodName); + } + auto index = static_cast(methodNameId); + if (index < names->byId.size() && !names->byId[index].IsEmpty()) { + return names->byId[index].Get(isolate); + } + auto name = ArgConverter::jstringToInternalizedV8String(isolate, env, methodName); + if (index >= names->byId.size()) { + names->byId.resize(index + 1); + } + names->byId[index].Reset(isolate, name); + return name; +} +} // namespace + jobject Runtime::CallJSMethodNative(JNIEnv* _env, jobject obj, - jint javaObjectID, jstring methodName, + jobject javaObject, jint javaObjectID, + jstring methodName, jint methodNameId, jint retType, jboolean isConstructor, const JavaCallArgs& args, jlongArray primitiveResult) { @@ -618,7 +636,22 @@ jobject Runtime::CallJSMethodNative(JNIEnv* _env, jobject obj, DEBUG_WRITE("CallJSMethodNative called javaObjectID=%d", javaObjectID); - auto jsObject = m_objectManager->GetJsObjectByJavaObject(javaObjectID); + m_objectManager->FlushFinalizedBatchIfPending(); + + // javaObjectID may be a hint read without the isolate lock; resolve it exactly when it + // does not name a linked wrapper. + Local jsObject; + if (javaObjectID != -1) { + jsObject = m_objectManager->GetLinkedJsObjectByJavaObject(javaObjectID); + } + if (jsObject.IsEmpty() && javaObject != nullptr) { + javaObjectID = m_objectManager->FindObjectId(javaObject); + if (javaObjectID == -1) { + throw NativeScriptException("Cannot find object id for an instance of " + + m_objectManager->GetClassName(javaObject)); + } + jsObject = m_objectManager->GetJsObjectByJavaObject(javaObjectID); + } if (jsObject.IsEmpty()) { stringstream ss; ss << "JavaScript object for Java ID " << javaObjectID << " not found." @@ -640,7 +673,7 @@ jobject Runtime::CallJSMethodNative(JNIEnv* _env, jobject obj, jsObject->GetIdentityHash()); auto jsMethodName = - ArgConverter::jstringToInternalizedV8String(m_isolate, env, methodName); + GetCallbackMethodName(m_isolate, _env, methodName, methodNameId); auto jsResult = CallbackHandlers::CallJSMethod(m_isolate, env, jsObject, jsMethodName, args); @@ -659,9 +692,9 @@ jobject Runtime::CallJSMethodNative(JNIEnv* _env, jobject obj, } int classReturnType = retType; - jobject javaObject = + jobject javaResult = ConvertJsValueToJavaObject(env, jsResult, classReturnType); - return javaObject; + return javaResult; } void Runtime::CreateJSInstanceNative(JNIEnv* _env, jobject obj, @@ -675,6 +708,9 @@ void Runtime::CreateJSInstanceNative(JNIEnv* _env, jobject obj, JEnv env(_env); + m_objectManager->FlushFinalizedBatchIfPending(); + m_objectManager->RegisterStrong(javaObject, javaObjectID); + string existingClassName = ArgConverter::jstringToString(className); string jniName = Util::ConvertFromCanonicalToJniName(existingClassName); Local jsInstance; @@ -710,9 +746,7 @@ jint Runtime::GenerateNewObjectId(JNIEnv* env, jobject obj) { return objectId; } -void Runtime::AdjustAmountOfExternalAllocatedMemory() { - JEnv env; - int64_t usedMemory = env.CallLongMethod(m_runtime, GET_USED_MEMORY_METHOD_ID); +void Runtime::AdjustAmountOfExternalAllocatedMemory(int64_t usedMemory) { int64_t changeInBytes = usedMemory - m_lastUsedMemory; int64_t externalMemory = 0; @@ -734,7 +768,7 @@ bool Runtime::NotifyGC(JNIEnv* env, jobject obj) { } bool Runtime::TryCallGC() { - auto success = m_gcFunc != nullptr; + auto success = m_gcFunc != nullptr && m_runGC; if (success) { success = __sync_bool_compare_and_swap(&m_runGC, true, false); if (success) { @@ -1208,8 +1242,8 @@ Isolate* Runtime::PrepareV8Runtime(const string& filesPath, this->m_context = new Persistent(isolate, context); this->m_napiEnv = NapiEnv::Create(context, m_eventLoop); - s_currentRuntime = this; - s_currentRuntimeId = m_id; + s_current.runtime = this; + s_current.id = m_id; // The runtime's thread stays attached until DestroyRuntime: the main thread // for the process, a worker thread until after it tears its runtime down. JEnv::PinThreadEnv(JEnv()); @@ -1288,9 +1322,9 @@ void Runtime::DestroyRuntime() { NapiEnv::Destroy(static_cast(m_napiEnv)); m_napiEnv = nullptr; } - if (s_currentRuntime == this) { - s_currentRuntime = nullptr; - s_currentRuntimeId = -1; + if (s_current.runtime == this) { + s_current.runtime = nullptr; + s_current.id = -1; JEnv::UnpinThreadEnv(); } // The events state holds v8::Global handles (backing event target, dispatch @@ -1322,8 +1356,8 @@ void Runtime::DestroyRuntime() { // is alive and its destructors can still touch v8::Global handles. IsolateTracked::SweepAll(m_isolate); - // Same reason: every still-linked Java<->JS wrapper holds a Persistent, a - // JSInstanceInfo and its weak-callback state, and those finalizers will + // Same reason: every still-linked Java<->JS wrapper holds a Persistent in + // its weak-callback state, and those finalizers will // never run now. The JNI half of ObjectManager is released later, in its // destructor, once the isolate is gone. if (m_objectManager != nullptr) { @@ -1374,7 +1408,6 @@ int Runtime::GetId() { return this->m_id; } JavaVM* Runtime::s_jvm = nullptr; -jmethodID Runtime::GET_USED_MEMORY_METHOD_ID = nullptr; robin_hood::unordered_map Runtime::s_id2RuntimeCache; robin_hood::unordered_map Runtime::s_isolate2RuntimesCache; std::mutex Runtime::s_runtimeCacheMutex; @@ -1387,8 +1420,7 @@ v8::Platform* Runtime::platform = nullptr; int Runtime::m_androidVersion = Runtime::GetAndroidVersion(); std::shared_ptr Runtime::s_mainEventLoop; -thread_local Runtime* Runtime::s_currentRuntime = nullptr; -thread_local int Runtime::s_currentRuntimeId = -1; +thread_local Runtime::CurrentRuntime Runtime::s_current; thread_local PendingIsolateSetup Runtime::s_pendingIsolateSetup; void Runtime::SetPendingIsolateSetup(PendingIsolateSetup setup) { diff --git a/test-app/runtime/src/main/cpp/Runtime.h b/test-app/runtime/src/main/cpp/Runtime.h index bc75dc697..77913df69 100644 --- a/test-app/runtime/src/main/cpp/Runtime.h +++ b/test-app/runtime/src/main/cpp/Runtime.h @@ -13,6 +13,7 @@ #include "File.h" #include "Timers.h" #include "EventLoop.h" +#include "AdmissionGate.h" #include #include #include @@ -108,7 +109,7 @@ class Runtime { * thread hosts at most one runtime and only that thread may destroy it. */ static Runtime* GetCurrentRuntime() { - return s_currentRuntime; + return s_current.runtime; } /* @@ -168,14 +169,34 @@ class Runtime { ObjectManager* GetObjectManager() const; + AdmissionGate& GetAdmissionGate() { + return m_admissionGate; + } + + // The instance JS is constructing, as last published to the Java runtime's + // currentObjectId and currentObjectClass. + struct Construction { + int id = -1; + jclass clazz = nullptr; + }; + + Construction& CurrentConstruction() { + return m_construction; + } + + // What the Java runtime's currentObjectClass holds; only native code writes it. + jclass& PublishedConstructionClass() { + return m_publishedConstructionClass; + } + void RunModule(JNIEnv* _env, jobject obj, jstring scriptFile); void RunModule(const char *moduleName); void RunWorker(const std::string& filePath); jobject RunScript(JNIEnv* _env, jobject obj, jstring scriptFile); - jobject CallJSMethodNative(JNIEnv* _env, jobject obj, jint javaObjectID, jstring methodName, jint retType, jboolean isConstructor, const JavaCallArgs& args, jlongArray primitiveResult); + jobject CallJSMethodNative(JNIEnv* _env, jobject obj, jobject javaObject, jint javaObjectID, jstring methodName, jint methodNameId, jint retType, jboolean isConstructor, const JavaCallArgs& args, jlongArray primitiveResult); void CreateJSInstanceNative(JNIEnv* _env, jobject obj, jobject javaObject, jint javaObjectID, jstring className); jint GenerateNewObjectId(JNIEnv* env, jobject obj); - void AdjustAmountOfExternalAllocatedMemory(); + void AdjustAmountOfExternalAllocatedMemory(int64_t usedJavaMemory); bool NotifyGC(JNIEnv* env, jobject obj); bool TryCallGC(); /* @@ -333,6 +354,12 @@ class Runtime { ObjectManager* m_objectManager; + AdmissionGate m_admissionGate; + + Construction m_construction; + + jclass m_publishedConstructionClass = nullptr; + ModuleInternal m_module; ArrayBufferHelper m_arrayBufferHelper; @@ -417,7 +444,6 @@ class Runtime { static JavaVM* s_jvm; - static jmethodID GET_USED_MEMORY_METHOD_ID; /* * The main runtime has finished initializing. Distinct from the @@ -435,8 +461,12 @@ class Runtime { static std::shared_ptr s_mainEventLoop; - static thread_local Runtime* s_currentRuntime; - static thread_local int s_currentRuntimeId; + // One struct because without ELF TLS (minSdk < 29) every thread_local costs a lookup. + struct CurrentRuntime { + Runtime* runtime = nullptr; + int id = -1; + }; + static thread_local CurrentRuntime s_current; static thread_local PendingIsolateSetup s_pendingIsolateSetup; #ifdef APPLICATION_IN_DEBUG diff --git a/test-app/runtime/src/main/cpp/V8GlobalHelpers.cpp b/test-app/runtime/src/main/cpp/V8GlobalHelpers.cpp index 05b795769..a9cd161d5 100644 --- a/test-app/runtime/src/main/cpp/V8GlobalHelpers.cpp +++ b/test-app/runtime/src/main/cpp/V8GlobalHelpers.cpp @@ -19,17 +19,12 @@ static std::string ToDottedName(std::string name) { return name; } -// A JS object linked to a Java counterpart holds a JSInstanceInfo External in -// its first internal field. The field count alone does not identify one: V8 -// hands typed arrays and array buffers the same number of embedder fields. static bool HasJavaCounterpart(Isolate* isolate, const Local& object) { auto objectManager = tns::Runtime::GetObjectManager(isolate); if (objectManager == nullptr || !objectManager->IsJsRuntimeObject(object)) { return false; } - - const int jsInfoIdx = static_cast(tns::ObjectManager::MetadataNodeKeys::JsInfo); - return object->GetInternalField(jsInfoIdx).As()->IsExternal(); + return tns::ObjectManager::HasJsInfo(object); } std::string tns::GetNativeWrapperHint(Isolate* isolate, const Local& value) { diff --git a/test-app/runtime/src/main/cpp/V8StringConstants.cpp b/test-app/runtime/src/main/cpp/V8StringConstants.cpp index 451b503f5..bd1818ea3 100644 --- a/test-app/runtime/src/main/cpp/V8StringConstants.cpp +++ b/test-app/runtime/src/main/cpp/V8StringConstants.cpp @@ -84,6 +84,12 @@ Local V8StringConstants::GetPrototype(Isolate* isolate) { return Local::New(isolate, *consts->PROTOTYPE_PERSISTENT); } +Local V8StringConstants::GetConstructor(Isolate* isolate) { + auto consts = GetConstantsForIsolate(isolate); + + return Local::New(isolate, *consts->CONSTRUCTOR_PERSISTENT); +} + Local V8StringConstants::GetSuper(Isolate* isolate) { auto consts = GetConstantsForIsolate(isolate); @@ -149,6 +155,7 @@ const string V8StringConstants::STACK_TRACE = "stackTrace"; const string V8StringConstants::STACK = "stack"; const string V8StringConstants::LONG_NUMBER = "NativeScriptLongNumber"; const string V8StringConstants::PROTOTYPE = "prototype"; +const string V8StringConstants::CONSTRUCTOR = "constructor"; const string V8StringConstants::SUPER = "super"; const string V8StringConstants::TARGET = "target"; const string V8StringConstants::TO_STRING = "toString"; diff --git a/test-app/runtime/src/main/cpp/V8StringConstants.h b/test-app/runtime/src/main/cpp/V8StringConstants.h index 6d1345a1c..6539d5dd8 100644 --- a/test-app/runtime/src/main/cpp/V8StringConstants.h +++ b/test-app/runtime/src/main/cpp/V8StringConstants.h @@ -29,6 +29,8 @@ class V8StringConstants { static v8::Local GetPrototype(v8::Isolate* isolate); + static v8::Local GetConstructor(v8::Isolate* isolate); + static v8::Local GetSuper(v8::Isolate* isolate); static v8::Local GetTarget(v8::Isolate* isolate); @@ -62,6 +64,7 @@ class V8StringConstants { static const std::string STACK_TRACE; static const std::string LONG_NUMBER; static const std::string PROTOTYPE; + static const std::string CONSTRUCTOR; static const std::string SUPER; static const std::string TARGET; static const std::string TO_STRING; @@ -80,82 +83,85 @@ class V8StringConstants { */ struct PerIsolateV8Constants { PerIsolateV8Constants(v8::Isolate* isolate) { - auto str = String::NewFromUtf8(isolate, CLASS_IMPLEMENTATION_OBJECT.c_str()).ToLocalChecked(); + auto str = String::NewFromUtf8(isolate, CLASS_IMPLEMENTATION_OBJECT.c_str(), NewStringType::kInternalized).ToLocalChecked(); CLASS_IMPLEMENTATION_OBJECT_PERSISTENT = new Persistent(isolate, str); - str = String::NewFromUtf8(isolate, EXTEND.c_str()).ToLocalChecked(); + str = String::NewFromUtf8(isolate, EXTEND.c_str(), NewStringType::kInternalized).ToLocalChecked(); EXTEND_PERSISTENT = new Persistent(isolate, str); - str = String::NewFromUtf8(isolate, NULL_OBJECT.c_str()).ToLocalChecked(); + str = String::NewFromUtf8(isolate, NULL_OBJECT.c_str(), NewStringType::kInternalized).ToLocalChecked(); NULL_OBJECT_PERSISTENT = new Persistent(isolate, str); - str = String::NewFromUtf8(isolate, NULL_NODE_NAME.c_str()).ToLocalChecked(); + str = String::NewFromUtf8(isolate, NULL_NODE_NAME.c_str(), NewStringType::kInternalized).ToLocalChecked(); NULL_NODE_NAME_PERSISTENT = new Persistent(isolate, str); - str = String::NewFromUtf8(isolate, IS_PROTOTYPE_IMPLEMENTATION_OBJECT.c_str()).ToLocalChecked(); + str = String::NewFromUtf8(isolate, IS_PROTOTYPE_IMPLEMENTATION_OBJECT.c_str(), NewStringType::kInternalized).ToLocalChecked(); IS_PROTOTYPE_IMPLEMENTATION_OBJECT_PERSISTENT = new Persistent(isolate, str); - str = String::NewFromUtf8(isolate, NATIVE_EXCEPTION.c_str()).ToLocalChecked(); + str = String::NewFromUtf8(isolate, NATIVE_EXCEPTION.c_str(), NewStringType::kInternalized).ToLocalChecked(); NATIVE_EXCEPTION_PERSISTENT = new Persistent(isolate, str); - str = String::NewFromUtf8(isolate, STACK.c_str()).ToLocalChecked(); + str = String::NewFromUtf8(isolate, STACK.c_str(), NewStringType::kInternalized).ToLocalChecked(); STACK_PERSISTENT = new Persistent(isolate, str); - str = String::NewFromUtf8(isolate, STACK_TRACE.c_str()).ToLocalChecked(); + str = String::NewFromUtf8(isolate, STACK_TRACE.c_str(), NewStringType::kInternalized).ToLocalChecked(); STACK_TRACE_PERSISTENT = new Persistent(isolate, str); - str = String::NewFromUtf8(isolate, LONG_NUMBER.c_str()).ToLocalChecked(); + str = String::NewFromUtf8(isolate, LONG_NUMBER.c_str(), NewStringType::kInternalized).ToLocalChecked(); LONG_NUMBER_PERSISTENT = new Persistent(isolate, str); - str = String::NewFromUtf8(isolate, PROTOTYPE.c_str()).ToLocalChecked(); + str = String::NewFromUtf8(isolate, PROTOTYPE.c_str(), NewStringType::kInternalized).ToLocalChecked(); PROTOTYPE_PERSISTENT = new Persistent(isolate, str); + str = String::NewFromUtf8(isolate, CONSTRUCTOR.c_str(), NewStringType::kInternalized).ToLocalChecked(); + CONSTRUCTOR_PERSISTENT = new Persistent(isolate, str); + - str = String::NewFromUtf8(isolate, SUPER.c_str()).ToLocalChecked(); + str = String::NewFromUtf8(isolate, SUPER.c_str(), NewStringType::kInternalized).ToLocalChecked(); SUPER_PERSISTENT = new Persistent(isolate, str); - str = String::NewFromUtf8(isolate, TARGET.c_str()).ToLocalChecked(); + str = String::NewFromUtf8(isolate, TARGET.c_str(), NewStringType::kInternalized).ToLocalChecked(); TARGET_PERSISTENT = new Persistent(isolate, str); - str = String::NewFromUtf8(isolate, TO_STRING.c_str()).ToLocalChecked(); + str = String::NewFromUtf8(isolate, TO_STRING.c_str(), NewStringType::kInternalized).ToLocalChecked(); TO_STRING_PERSISTENT = new Persistent(isolate, str); - str = String::NewFromUtf8(isolate, JAVA_LONG.c_str()).ToLocalChecked(); + str = String::NewFromUtf8(isolate, JAVA_LONG.c_str(), NewStringType::kInternalized).ToLocalChecked(); JAVA_LONG_PERSISTENT = new Persistent(isolate, str); - str = String::NewFromUtf8(isolate, VALUE.c_str()).ToLocalChecked(); + str = String::NewFromUtf8(isolate, VALUE.c_str(), NewStringType::kInternalized).ToLocalChecked(); VALUE_PERSISTENT = new Persistent(isolate, str); - str = String::NewFromUtf8(isolate, VALUE_OF.c_str()).ToLocalChecked(); + str = String::NewFromUtf8(isolate, VALUE_OF.c_str(), NewStringType::kInternalized).ToLocalChecked(); VALUE_OF_PERSISTENT = new Persistent(isolate, str); - str = String::NewFromUtf8(isolate, UNCAUGHT_ERROR.c_str()).ToLocalChecked(); + str = String::NewFromUtf8(isolate, UNCAUGHT_ERROR.c_str(), NewStringType::kInternalized).ToLocalChecked(); UNCAUGHT_ERROR_PERSISTENT = new Persistent(isolate, str); - str = String::NewFromUtf8(isolate, DISCARDED_ERROR.c_str()).ToLocalChecked(); + str = String::NewFromUtf8(isolate, DISCARDED_ERROR.c_str(), NewStringType::kInternalized).ToLocalChecked(); DISCARDED_ERROR_PERSISTENT = new Persistent(isolate, str); - str = String::NewFromUtf8(isolate, IMPLEMENTATION_OBJECT.c_str()).ToLocalChecked(); + str = String::NewFromUtf8(isolate, IMPLEMENTATION_OBJECT.c_str(), NewStringType::kInternalized).ToLocalChecked(); IMPLEMENTATION_OBJECT_PERSISTENT = new Persistent(isolate, str); // Private::ForApi resolves its name through the isolate's registry // on every call, so the keys read per bridge call are resolved once. NULL_NODE_PRIVATE.Reset(isolate, Private::ForApi(isolate, Local::New(isolate, *NULL_NODE_NAME_PERSISTENT))); - str = String::NewFromUtf8(isolate, CAST_MARKER.c_str()).ToLocalChecked(); + str = String::NewFromUtf8(isolate, CAST_MARKER.c_str(), NewStringType::kInternalized).ToLocalChecked(); CAST_MARKER_PRIVATE.Reset(isolate, Private::ForApi(isolate, str)); }; @@ -178,6 +184,7 @@ class V8StringConstants { ResetAndDelete(NULL_NODE_NAME_PERSISTENT); ResetAndDelete(NULL_OBJECT_PERSISTENT); ResetAndDelete(PROTOTYPE_PERSISTENT); + ResetAndDelete(CONSTRUCTOR_PERSISTENT); ResetAndDelete(STACK_PERSISTENT); ResetAndDelete(STACK_TRACE_PERSISTENT); ResetAndDelete(SUPER_PERSISTENT); @@ -207,6 +214,7 @@ class V8StringConstants { v8::Persistent* STACK_TRACE_PERSISTENT = nullptr; v8::Persistent* LONG_NUMBER_PERSISTENT = nullptr; v8::Persistent* PROTOTYPE_PERSISTENT = nullptr; + v8::Persistent* CONSTRUCTOR_PERSISTENT = nullptr; v8::Persistent* SUPER_PERSISTENT = nullptr; v8::Persistent* TARGET_PERSISTENT = nullptr; v8::Persistent* TO_STRING_PERSISTENT = nullptr; diff --git a/test-app/runtime/src/main/cpp/com_tns_Runtime.cpp b/test-app/runtime/src/main/cpp/com_tns_Runtime.cpp index 82ac48fcf..c2c09ff3d 100644 --- a/test-app/runtime/src/main/cpp/com_tns_Runtime.cpp +++ b/test-app/runtime/src/main/cpp/com_tns_Runtime.cpp @@ -194,7 +194,7 @@ extern "C" JNIEXPORT jobject Java_com_tns_Runtime_runScript(JNIEnv* _env, jobjec return result; } -extern "C" JNIEXPORT jobject Java_com_tns_Runtime_callJSMethodNative(JNIEnv* _env, jobject obj, jint runtimeId, jint javaObjectID, jstring methodName, jint retType, jboolean isConstructor, jintArray argTypes, jlongArray argValues, jobjectArray argRefs, jlongArray primitiveResult) { +extern "C" JNIEXPORT jobject Java_com_tns_Runtime_callJSMethodNative(JNIEnv* _env, jobject obj, jint runtimeId, jobject javaObject, jint javaObjectID, jstring methodName, jint methodNameId, jint retType, jboolean isConstructor, jint argCount, jlongArray argValues, jobjectArray argRefs, jlongArray primitiveResult) { JEnv::EntryScope envScope(_env); jobject result = nullptr; @@ -204,14 +204,16 @@ extern "C" JNIEXPORT jobject Java_com_tns_Runtime_callJSMethodNative(JNIEnv* _en } auto isolate = runtime->GetIsolate(); + AdmissionScope admission(runtime->GetAdmissionGate(), isolate); v8::Locker locker(isolate); v8::Isolate::Scope isolate_scope(isolate); v8::HandleScope handleScope(isolate); auto context = runtime->GetContext(); v8::Context::Scope context_scope(context); + JavaCallArgs args{argCount, argValues, argRefs}; try { - result = runtime->CallJSMethodNative(_env, obj, javaObjectID, methodName, retType, isConstructor, JavaCallArgs{argTypes, argValues, argRefs}, primitiveResult); + result = runtime->CallJSMethodNative(_env, obj, javaObject, javaObjectID, methodName, methodNameId, retType, isConstructor, args, primitiveResult); } catch (NativeScriptException& e) { e.ReThrowToJava(); } catch (std::exception e) { @@ -223,6 +225,11 @@ extern "C" JNIEXPORT jobject Java_com_tns_Runtime_callJSMethodNative(JNIEnv* _en NativeScriptException nsEx(std::string("Error: c++ exception!")); nsEx.ReThrowToJava(); } + auto objectManager = runtime->GetObjectManager(); + // Objects Java registered for a call that never reached them would stay strong. + objectManager->ReleaseUnwrappedArgs(args); + // Wrappers collected during the call, settled while the isolate is still held. + objectManager->FlushFinalizedBatchIfPending(); return result; } @@ -234,6 +241,7 @@ extern "C" JNIEXPORT void Java_com_tns_Runtime_createJSInstanceNative(JNIEnv* _e } auto isolate = runtime->GetIsolate(); + AdmissionScope admission(runtime->GetAdmissionGate(), isolate); v8::Locker locker(isolate); v8::Isolate::Scope isolate_scope(isolate); v8::HandleScope handleScope(isolate); @@ -373,4 +381,8 @@ extern "C" JNIEXPORT void Java_com_tns_Runtime_ResetDateTimeConfigurationCache(J auto isolate = runtime->GetIsolate(); isolate->DateTimeConfigurationChangeNotification(Isolate::TimeZoneDetection::kRedetect); -} \ No newline at end of file +} + +extern "C" JNIEXPORT void Java_com_tns_Runtime_setAdmissionSpinMicros(JNIEnv* _env, jclass clazz, jint micros) { + AdmissionGate::s_spinMicros.store(micros, std::memory_order_relaxed); +} diff --git a/test-app/runtime/src/main/java/com/tns/MethodNameIds.java b/test-app/runtime/src/main/java/com/tns/MethodNameIds.java new file mode 100644 index 000000000..87651091f --- /dev/null +++ b/test-app/runtime/src/main/java/com/tns/MethodNameIds.java @@ -0,0 +1,87 @@ +package com.tns; + +/** + * Process-wide dense ids for the method names Java calls into JS with, so native code can + * keep each name's V8 string instead of copying the characters out of the String on every + * call. An id names the same string in every runtime. + * + * Lookups take no lock. The table is open-addressed on String.hashCode(), which String + * caches, and entries are immutable, so a reader racing an insert either sees a complete + * entry or misses and registers under the lock. + */ +final class MethodNameIds { + private static final class Entry { + final String name; + final int id; + + Entry(String name, int id) { + this.name = name; + this.id = id; + } + } + + private static volatile Entry[] table = new Entry[256]; + private static int count; + + private MethodNameIds() { + } + + static int idOf(String name) { + if (name == null) { + return -1; + } + Entry[] t = table; + int mask = t.length - 1; + for (int i = slot(name, mask); ; i = (i + 1) & mask) { + Entry e = t[i]; + if (e == null) { + return register(name); + } + if (e.name == name || e.name.equals(name)) { + return e.id; + } + } + } + + private static synchronized int register(String name) { + Entry[] t = table; + int mask = t.length - 1; + int i = slot(name, mask); + for (Entry e; (e = t[i]) != null; i = (i + 1) & mask) { + if (e.name.equals(name)) { + return e.id; + } + } + int id = count++; + if (count * 2 > t.length) { + t = grow(t); + mask = t.length - 1; + for (i = slot(name, mask); t[i] != null; i = (i + 1) & mask) { + } + } + t[i] = new Entry(name, id); + table = t; + return id; + } + + private static Entry[] grow(Entry[] old) { + Entry[] t = new Entry[old.length * 2]; + int mask = t.length - 1; + for (Entry e : old) { + if (e == null) { + continue; + } + int i = slot(e.name, mask); + while (t[i] != null) { + i = (i + 1) & mask; + } + t[i] = e; + } + return t; + } + + private static int slot(String name, int mask) { + int h = name.hashCode(); + return (h ^ (h >>> 16)) & mask; + } +} diff --git a/test-app/runtime/src/main/java/com/tns/NativeScriptAbstractMap.java b/test-app/runtime/src/main/java/com/tns/NativeScriptAbstractMap.java deleted file mode 100644 index 3de04826f..000000000 --- a/test-app/runtime/src/main/java/com/tns/NativeScriptAbstractMap.java +++ /dev/null @@ -1,515 +0,0 @@ -package com.tns; - -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import java.io.Serializable; -import java.util.AbstractCollection; -import java.util.AbstractSet; -import java.util.Collection; -import java.util.Iterator; -import java.util.Map; -import java.util.Set; - -/** - * A base class for {@code Map} implementations. - * - *

- * Subclasses that permit new mappings to be added must override {@link #put}. - * - *

- * The default implementations of many methods are inefficient for large maps. For example in the default implementation, each call to {@link #get} performs a linear iteration of the entry set. Subclasses should override such methods to improve their performance. - * - * @since 1.2 - */ -public abstract class NativeScriptAbstractMap implements Map { - // Lazily-initialized key set (for implementing {@link #keySet}). - Set keySet; - - // Lazily-initialized values collection (for implementing {@link #values}). - Collection valuesCollection; - - /** - * An immutable key-value mapping. Despite the name, this class is non-final - * and its subclasses may be mutable. - * - * @since 1.6 - */ - public static class SimpleImmutableEntry implements Map.Entry, Serializable { - private static final long serialVersionUID = 7138329143949025153L; - - private final K key; - private final V value; - - public SimpleImmutableEntry(K theKey, V theValue) { - key = theKey; - value = theValue; - } - - /** - * Constructs an instance with the key and value of {@code copyFrom}. - */ - public SimpleImmutableEntry(Map.Entry copyFrom) { - key = copyFrom.getKey(); - value = copyFrom.getValue(); - } - - public K getKey() { - return key; - } - - public V getValue() { - return value; - } - - /** - * This base implementation throws {@code UnsupportedOperationException} always. - */ - public V setValue(V object) { - throw new UnsupportedOperationException(); - } - - @Override - public boolean equals(Object object) { - if (this == object) { - return true; - } - if (object instanceof Map.Entry) { - Map.Entry entry = (Map.Entry) object; - return (key == null ? entry.getKey() == null : key.equals(entry.getKey())) && (value == null ? entry.getValue() == null : value.equals(entry.getValue())); - } - return false; - } - - @Override - public int hashCode() { - return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode()); - } - - @Override - public String toString() { - return key + "=" + value; - } - } - - /** - * A key-value mapping with mutable values. - * - * @since 1.6 - */ - public static class SimpleEntry implements Map.Entry, Serializable { - private static final long serialVersionUID = -8499721149061103585L; - - private final K key; - private V value; - - public SimpleEntry(K theKey, V theValue) { - key = theKey; - value = theValue; - } - - /** - * Constructs an instance with the key and value of {@code copyFrom}. - */ - public SimpleEntry(Map.Entry copyFrom) { - key = copyFrom.getKey(); - value = copyFrom.getValue(); - } - - public K getKey() { - return key; - } - - public V getValue() { - return value; - } - - public V setValue(V object) { - V result = value; - value = object; - return result; - } - - @Override - public boolean equals(Object object) { - if (this == object) { - return true; - } - if (object instanceof Map.Entry) { - Map.Entry entry = (Map.Entry) object; - return (key == null ? entry.getKey() == null : key.equals(entry.getKey())) && (value == null ? entry.getValue() == null : value.equals(entry.getValue())); - } - return false; - } - - @Override - public int hashCode() { - return (key == null ? 0 : key.hashCode()) ^ (value == null ? 0 : value.hashCode()); - } - - @Override - public String toString() { - return key + "=" + value; - } - } - - protected NativeScriptAbstractMap() { - } - - /** - * {@inheritDoc} - * - *

- * This implementation calls {@code entrySet().clear()}. - */ - public void clear() { - entrySet().clear(); - } - - /** - * {@inheritDoc} - * - *

- * This implementation iterates its key set, looking for a key that {@code key} equals. - */ - public boolean containsKey(Object key) { - Iterator> it = entrySet().iterator(); - if (key != null) { - while (it.hasNext()) { - if (key.equals(it.next().getKey())) { - return true; - } - } - } else { - while (it.hasNext()) { - if (it.next().getKey() == null) { - return true; - } - } - } - return false; - } - - /** - * {@inheritDoc} - * - *

- * This implementation iterates its entry set, looking for an entry with a value that {@code value} equals. - */ - public boolean containsValue(Object value) { - Iterator> it = entrySet().iterator(); - if (value != null) { - while (it.hasNext()) { - if (value.equals(it.next().getValue())) { - return true; - } - } - } else { - while (it.hasNext()) { - if (it.next().getValue() == null) { - return true; - } - } - } - return false; - } - - public abstract Set> entrySet(); - - /** - * {@inheritDoc} - * - *

- * This implementation first checks the structure of {@code object}. If it is not a map or of a different size, this returns false. Otherwise it iterates its own entry set, looking up each entry's key in {@code object} . If any value does not equal the other map's value for the same key, this returns false. Otherwise it returns true. - */ - @Override - public boolean equals(Object object) { - if (this == object) { - return true; - } - if (object instanceof Map) { - Map map = (Map) object; - if (size() != map.size()) { - return false; - } - - try { - for (Entry entry : entrySet()) { - K key = entry.getKey(); - V mine = entry.getValue(); - Object theirs = map.get(key); - if (mine == null) { - if (theirs != null || !map.containsKey(key)) { - return false; - } - } else if (!mine.equals(theirs)) { - return false; - } - } - } catch (NullPointerException ignored) { - return false; - } catch (ClassCastException ignored) { - return false; - } - return true; - } - return false; - } - - /** - * {@inheritDoc} - * - *

- * This implementation iterates its entry set, looking for an entry with a key that {@code key} equals. - */ - public V get(Object key) { - Iterator> it = entrySet().iterator(); - if (key != null) { - while (it.hasNext()) { - Map.Entry entry = it.next(); - if (key.equals(entry.getKey())) { - return entry.getValue(); - } - } - } else { - while (it.hasNext()) { - Map.Entry entry = it.next(); - if (entry.getKey() == null) { - return entry.getValue(); - } - } - } - return null; - } - - /** - * {@inheritDoc} - * - *

- * This implementation iterates its entry set, summing the hashcodes of its entries. - */ - @Override - public int hashCode() { - int result = 0; - Iterator> it = entrySet().iterator(); - while (it.hasNext()) { - result += it.next().hashCode(); - } - return result; - } - - /** - * {@inheritDoc} - * - *

- * This implementation compares {@code size()} to 0. - */ - public boolean isEmpty() { - return size() == 0; - } - - /** - * {@inheritDoc} - * - *

- * This implementation returns a view that calls through this to map. Its iterator transforms this map's entry set iterator to return keys. - */ - public Set keySet() { - if (keySet == null) { - keySet = new AbstractSet() { - @Override - public boolean contains(Object object) { - return containsKey(object); - } - - @Override - public int size() { - return NativeScriptAbstractMap.this.size(); - } - - @Override - public Iterator iterator() { - return new Iterator() { - Iterator> setIterator = entrySet().iterator(); - - public boolean hasNext() { - return setIterator.hasNext(); - } - - public K next() { - return setIterator.next().getKey(); - } - - public void remove() { - setIterator.remove(); - } - }; - } - }; - } - return keySet; - } - - /** - * {@inheritDoc} - * - *

- * This base implementation throws {@code UnsupportedOperationException}. - */ - public V put(K key, V value) { - throw new UnsupportedOperationException(); - } - - /** - * {@inheritDoc} - * - *

- * This implementation iterates through {@code map}'s entry set, calling {@code put()} for each. - */ - public void putAll(Map map) { - for (Map.Entry entry : map.entrySet()) { - put(entry.getKey(), entry.getValue()); - } - } - - /** - * {@inheritDoc} - * - *

- * This implementation iterates its entry set, removing the entry with a key that {@code key} equals. - */ - public V remove(Object key) { - Iterator> it = entrySet().iterator(); - if (key != null) { - while (it.hasNext()) { - Map.Entry entry = it.next(); - if (key.equals(entry.getKey())) { - it.remove(); - return entry.getValue(); - } - } - } else { - while (it.hasNext()) { - Map.Entry entry = it.next(); - if (entry.getKey() == null) { - it.remove(); - return entry.getValue(); - } - } - } - return null; - } - - /** - * {@inheritDoc} - * - *

- * This implementation returns its entry set's size. - */ - public int size() { - return entrySet().size(); - } - - /** - * {@inheritDoc} - * - *

- * This implementation composes a string by iterating its entry set. If this map contains itself as a key or a value, the string "(this Map)" will appear in its place. - */ - @Override - public String toString() { - if (isEmpty()) { - return "{}"; - } - - StringBuilder buffer = new StringBuilder(size() * 28); - buffer.append('{'); - Iterator> it = entrySet().iterator(); - while (it.hasNext()) { - Map.Entry entry = it.next(); - Object key = entry.getKey(); - if (key != this) { - buffer.append(key); - } else { - buffer.append("(this Map)"); - } - buffer.append('='); - Object value = entry.getValue(); - if (value != this) { - buffer.append(value); - } else { - buffer.append("(this Map)"); - } - if (it.hasNext()) { - buffer.append(", "); - } - } - buffer.append('}'); - return buffer.toString(); - } - - /** - * {@inheritDoc} - * - *

- * This implementation returns a view that calls through this to map. Its iterator transforms this map's entry set iterator to return values. - */ - public Collection values() { - if (valuesCollection == null) { - valuesCollection = new AbstractCollection() { - @Override - public int size() { - return NativeScriptAbstractMap.this.size(); - } - - @Override - public boolean contains(Object object) { - return containsValue(object); - } - - @Override - public Iterator iterator() { - return new Iterator() { - Iterator> setIterator = entrySet().iterator(); - - public boolean hasNext() { - return setIterator.hasNext(); - } - - public V next() { - return setIterator.next().getValue(); - } - - public void remove() { - setIterator.remove(); - } - }; - } - }; - } - return valuesCollection; - } - - @SuppressWarnings("unchecked") - @Override - protected Object clone() throws CloneNotSupportedException { - NativeScriptAbstractMap result = (NativeScriptAbstractMap) super.clone(); - result.keySet = null; - result.valuesCollection = null; - return result; - } -} diff --git a/test-app/runtime/src/main/java/com/tns/NativeScriptHashMap.java b/test-app/runtime/src/main/java/com/tns/NativeScriptHashMap.java deleted file mode 100644 index 488eda931..000000000 --- a/test-app/runtime/src/main/java/com/tns/NativeScriptHashMap.java +++ /dev/null @@ -1,1056 +0,0 @@ -package com.tns; - -import java.util.AbstractCollection; -import java.util.AbstractMap; -import java.util.AbstractSet; -import java.util.Arrays; -import java.util.Collection; -import java.util.ConcurrentModificationException; -import java.util.Iterator; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Set; - -import java.io.IOException; -import java.io.InvalidObjectException; -import java.io.ObjectInputStream; -import java.io.ObjectOutputStream; -import java.io.ObjectStreamField; -import java.io.Serializable; - -//import libcore.util.Objects; - -/** - * HashMap is an implementation of {@link Map}. All optional operations are - * supported. - * - *

- * All elements are permitted as keys or values, including null. - * - *

- * Note that the iteration order for HashMap is non-deterministic. If you want deterministic iteration, use {@link LinkedHashMap}. - * - *

- * Note: the implementation of {@code HashMap} is not synchronized. If one thread of several threads accessing an instance modifies the map structurally, access to the map needs to be synchronized. A structural modification is an operation that adds or removes an entry. Changes in the value of an entry are not structural changes. - * - *

- * The {@code Iterator} created by calling the {@code iterator} method may throw a {@code ConcurrentModificationException} if the map is structurally changed while an iterator is used to iterate over the elements. Only the {@code remove} method that is provided by the iterator allows for removal of elements during iteration. It is not possible to guarantee that this mechanism works in all cases of unsynchronized concurrent modification. It should only be used for debugging purposes. - * - * @param the type of keys maintained by this map - * @param the type of mapped values - */ - -public class NativeScriptHashMap extends AbstractMap implements Cloneable, Serializable { - /** - * Min capacity (other than zero) for a HashMap. Must be a power of two - * greater than 1 (and less than 1 << 30). - */ - private static final int MINIMUM_CAPACITY = 4; - - /** - * Max capacity for a HashMap. Must be a power of two >= MINIMUM_CAPACITY. - */ - private static final int MAXIMUM_CAPACITY = 1 << 30; - - /** - * An empty table shared by all zero-capacity maps (typically from default - * constructor). It is never written to, and replaced on first put. Its size - * is set to half the minimum, so that the first resize will create a - * minimum-sized table. - */ - @SuppressWarnings("rawtypes") - private static final Entry[] EMPTY_TABLE = new HashMapEntry[MINIMUM_CAPACITY >>> 1]; - - /** - * The default load factor. Note that this implementation ignores the load - * factor, but cannot do away with it entirely because it's mentioned in the - * API. - * - *

- * Note that this constant has no impact on the behavior of the program, but it is emitted as part of the serialized form. The load factor of .75 is hardwired into the program, which uses cheap shifts in place of expensive division. - */ - static final float DEFAULT_LOAD_FACTOR = .75F; - - /** - * The hash table. If this hash map contains a mapping for null, it is not - * represented this hash table. - */ - transient HashMapEntry[] table; - - /** - * The entry representing the null key, or null if there's no such mapping. - */ - transient HashMapEntry entryForNullKey; - - /** - * The number of mappings in this hash map. - */ - transient int size; - - /** - * Incremented by "structural modifications" to allow (best effort) - * detection of concurrent modification. - */ - transient int modCount; - - /** - * The table is rehashed when its size exceeds this threshold. The value of - * this field is generally .75 * capacity, except when the capacity is zero, - * as described in the EMPTY_TABLE declaration above. - */ - private transient int threshold; - - // Views - lazily initialized - private transient Set keySet; - private transient Set> entrySet; - private transient Collection values; - - /** - * Constructs a new empty {@code HashMap} instance. - */ - @SuppressWarnings("unchecked") - public NativeScriptHashMap() { - table = (HashMapEntry[]) EMPTY_TABLE; - threshold = -1; // Forces first put invocation to replace EMPTY_TABLE - } - - /** - * Constructs a new {@code HashMap} instance with the specified capacity. - * - * @param capacity the initial capacity of this hash map. - * @throws IllegalArgumentException when the capacity is less than zero. - */ - public NativeScriptHashMap(int capacity) { - if (capacity < 0) { - throw new IllegalArgumentException("Capacity: " + capacity); - } - - if (capacity == 0) { - @SuppressWarnings("unchecked") - HashMapEntry[] tab = (HashMapEntry[]) EMPTY_TABLE; - table = tab; - threshold = -1; // Forces first put() to replace EMPTY_TABLE - return; - } - - if (capacity < MINIMUM_CAPACITY) { - capacity = MINIMUM_CAPACITY; - } else if (capacity > MAXIMUM_CAPACITY) { - capacity = MAXIMUM_CAPACITY; - } else { - capacity = roundUpToPowerOfTwo(capacity); - } - makeTable(capacity); - } - - /** - * Constructs a new {@code HashMap} instance with the specified capacity and - * load factor. - * - * @param capacity the initial capacity of this hash map. - * @param loadFactor the initial load factor. - * @throws IllegalArgumentException when the capacity is less than zero or the load factor is - * less or equal to zero or NaN. - */ - public NativeScriptHashMap(int capacity, float loadFactor) { - this(capacity); - - if (loadFactor <= 0 || Float.isNaN(loadFactor)) { - throw new IllegalArgumentException("Load factor: " + loadFactor); - } - - /* - * Note that this implementation ignores loadFactor; it always uses a - * load factor of 3/4. This simplifies the code and generally improves - * performance. - */ - } - - /** - * Constructs a new {@code HashMap} instance containing the mappings from - * the specified map. - * - * @param map the mappings to add. - */ - public NativeScriptHashMap(Map map) { - this(capacityForInitSize(map.size())); - constructorPutAll(map); - } - - /** - * Inserts all of the elements of map into this HashMap in a manner suitable - * for use by constructors and pseudo-constructors (i.e., clone, - * readObject). Also used by LinkedHashMap. - */ - final void constructorPutAll(Map map) { - if (table == EMPTY_TABLE) { - doubleCapacity(); // Don't do unchecked puts to a shared table. - } - for (Entry e : map.entrySet()) { - constructorPut(e.getKey(), e.getValue()); - } - } - - /** - * Returns an appropriate capacity for the specified initial size. Does not - * round the result up to a power of two; the caller must do this! The - * returned value will be between 0 and MAXIMUM_CAPACITY (inclusive). - */ - static int capacityForInitSize(int size) { - int result = (size >> 1) + size; // Multiply by 3/2 to allow for growth - - // boolean expr is equivalent to result >= 0 && result result; - try { - result = (NativeScriptHashMap) super.clone(); - } catch (CloneNotSupportedException e) { - throw new AssertionError(e); - } - - // Restore clone to empty state, retaining our capacity and threshold - result.makeTable(table.length); - result.entryForNullKey = null; - result.size = 0; - result.keySet = null; - result.entrySet = null; - result.values = null; - - result.init(); // Give subclass a chance to initialize itself - result.constructorPutAll(this); // Calls method overridden in subclass!! - return result; - } - - /** - * This method is called from the pseudo-constructors (clone and readObject) - * prior to invoking constructorPut/constructorPutAll, which invoke the - * overridden constructorNewEntry method. Normally it is a VERY bad idea to - * invoke an overridden method from a pseudo-constructor (Effective Java - * Item 17). In this case it is unavoidable, and the init method provides a - * workaround. - */ - void init() { - } - - /** - * Returns whether this map is empty. - * - * @return {@code true} if this map has no elements, {@code false} otherwise. - * @see #size() - */ - @Override - public boolean isEmpty() { - return size == 0; - } - - /** - * Returns the number of elements in this map. - * - * @return the number of elements in this map. - */ - @Override - public int size() { - return size; - } - - /** - * Returns the value of the mapping with the specified key. - * - * @param key the key. - * @return the value of the mapping with the specified key, or {@code null} if no mapping for the specified key is found. - */ - public V get(Object key) { - if (key == null) { - HashMapEntry e = entryForNullKey; - return e == null ? null : e.value; - } - - int hash = secondaryHashForObject(key); - HashMapEntry[] tab = table; - for (HashMapEntry e = tab[hash & (tab.length - 1)]; e != null; e = e.next) { - K eKey = e.key; - if (eKey == key) { - return e.value; - } - } - return null; - } - - /** - * Returns whether this map contains the specified key. - * - * @param key the key to search for. - * @return {@code true} if this map contains the specified key, {@code false} otherwise. - */ - @Override - public boolean containsKey(Object key) { - if (key == null) { - return entryForNullKey != null; - } - - int hash = secondaryHashForObject(key); - HashMapEntry[] tab = table; - for (HashMapEntry e = tab[hash & (tab.length - 1)]; e != null; e = e.next) { - K eKey = e.key; - if (eKey == key) { - return true; - } - } - return false; - } - - /** - * Returns whether this map contains the specified value. - * - * @param value the value to search for. - * @return {@code true} if this map contains the specified value, {@code false} otherwise. - */ - @Override - public boolean containsValue(Object value) { - HashMapEntry[] tab = table; - int len = tab.length; - if (value == null) { - for (int i = 0; i < len; i++) { - for (HashMapEntry e = tab[i]; e != null; e = e.next) { - if (e.value == null) { - return true; - } - } - } - return entryForNullKey != null && entryForNullKey.value == null; - } - - // value is non-null - for (int i = 0; i < len; i++) { - for (HashMapEntry e = tab[i]; e != null; e = e.next) { - if (value == e.value) { - return true; - } - } - } - return entryForNullKey != null && value.equals(entryForNullKey.value); - } - - /** - * Maps the specified key to the specified value. - * - * @param key the key. - * @param value the value. - * @return the value of any previous mapping with the specified key or {@code null} if there was no such mapping. - */ - @Override - public V put(K key, V value) { - if (key == null) { - return putValueForNullKey(value); - } - - int hash = secondaryHashForObject(key); - HashMapEntry[] tab = table; - int index = hash & (tab.length - 1); - for (HashMapEntry e = tab[index]; e != null; e = e.next) { - K eKey = e.key; - if (eKey == key) { - preModify(e); - V oldValue = e.value; - e.value = value; - return oldValue; - } - } - - // No entry for (non-null) key is present; create one - modCount++; - if (size++ > threshold) { - tab = doubleCapacity(); - index = hash & (tab.length - 1); - } - addNewEntry(key, value, hash, index); - return null; - } - - private V putValueForNullKey(V value) { - HashMapEntry entry = entryForNullKey; - if (entry == null) { - addNewEntryForNullKey(value); - size++; - modCount++; - return null; - } else { - preModify(entry); - V oldValue = entry.value; - entry.value = value; - return oldValue; - } - } - - /** - * Give LinkedHashMap a chance to take action when we modify an existing - * entry. - * - * @param e the entry we're about to modify. - */ - void preModify(HashMapEntry e) { - } - - /** - * This method is just like put, except that it doesn't do things that are - * inappropriate or unnecessary for constructors and pseudo-constructors - * (i.e., clone, readObject). In particular, this method does not check to - * ensure that capacity is sufficient, and does not increment modCount. - */ - private void constructorPut(K key, V value) { - if (key == null) { - HashMapEntry entry = entryForNullKey; - if (entry == null) { - entryForNullKey = constructorNewEntry(null, value, 0, null); - size++; - } else { - entry.value = value; - } - return; - } - - int hash = secondaryHashForObject(key); - HashMapEntry[] tab = table; - int index = hash & (tab.length - 1); - HashMapEntry first = tab[index]; - for (HashMapEntry e = first; e != null; e = e.next) { - if (key == e.key) { - e.value = value; - return; - } - } - - // No entry for (non-null) key is present; create one - tab[index] = constructorNewEntry(key, value, hash, first); - size++; - } - - /** - * Creates a new entry for the given key, value, hash, and index and inserts - * it into the hash table. This method is called by put (and indirectly, - * putAll), and overridden by LinkedHashMap. The hash must incorporate the - * secondary hash function. - */ - void addNewEntry(K key, V value, int hash, int index) { - table[index] = new HashMapEntry(key, value, hash, table[index]); - } - - /** - * Creates a new entry for the null key, and the given value and inserts it - * into the hash table. This method is called by put (and indirectly, - * putAll), and overridden by LinkedHashMap. - */ - void addNewEntryForNullKey(V value) { - entryForNullKey = new HashMapEntry(null, value, 0, null); - } - - /** - * Like newEntry, but does not perform any activity that would be - * unnecessary or inappropriate for constructors. In this class, the two - * methods behave identically; in LinkedHashMap, they differ. - */ - HashMapEntry constructorNewEntry(K key, V value, int hash, HashMapEntry first) { - return new HashMapEntry(key, value, hash, first); - } - - /** - * Copies all the mappings in the specified map to this map. These mappings - * will replace all mappings that this map had for any of the keys currently - * in the given map. - * - * @param map the map to copy mappings from. - */ - @Override - public void putAll(Map map) { - ensureCapacity(map.size()); - super.putAll(map); - } - - /** - * Ensures that the hash table has sufficient capacity to store the - * specified number of mappings, with room to grow. If not, it increases the - * capacity as appropriate. Like doubleCapacity, this method moves existing - * entries to new buckets as appropriate. Unlike doubleCapacity, this method - * can grow the table by factors of 2^n for n > 1. Hopefully, a single call - * to this method will be faster than multiple calls to doubleCapacity. - * - *

- * This method is called only by putAll. - */ - private void ensureCapacity(int numMappings) { - int newCapacity = roundUpToPowerOfTwo(capacityForInitSize(numMappings)); - HashMapEntry[] oldTable = table; - int oldCapacity = oldTable.length; - if (newCapacity <= oldCapacity) { - return; - } - if (newCapacity == oldCapacity * 2) { - doubleCapacity(); - return; - } - - // We're growing by at least 4x, rehash in the obvious way - HashMapEntry[] newTable = makeTable(newCapacity); - if (size != 0) { - int newMask = newCapacity - 1; - for (int i = 0; i < oldCapacity; i++) { - for (HashMapEntry e = oldTable[i]; e != null; ) { - HashMapEntry oldNext = e.next; - int newIndex = e.hash & newMask; - HashMapEntry newNext = newTable[newIndex]; - newTable[newIndex] = e; - e.next = newNext; - e = oldNext; - } - } - } - } - - /** - * Allocate a table of the given capacity and set the threshold accordingly. - * - * @param newCapacity must be a power of two - */ - private HashMapEntry[] makeTable(int newCapacity) { - @SuppressWarnings({"unchecked", "rawtypes"}) - HashMapEntry[] newTable = (HashMapEntry[]) new HashMapEntry[newCapacity]; - table = newTable; - threshold = (newCapacity >> 1) + (newCapacity >> 2); // 3/4 capacity - return newTable; - } - - /** - * Doubles the capacity of the hash table. Existing entries are placed in - * the correct bucket on the enlarged table. If the current capacity is, - * MAXIMUM_CAPACITY, this method is a no-op. Returns the table, which will - * be new unless we were already at MAXIMUM_CAPACITY. - */ - private HashMapEntry[] doubleCapacity() { - HashMapEntry[] oldTable = table; - int oldCapacity = oldTable.length; - if (oldCapacity == MAXIMUM_CAPACITY) { - return oldTable; - } - int newCapacity = oldCapacity * 2; - HashMapEntry[] newTable = makeTable(newCapacity); - if (size == 0) { - return newTable; - } - - for (int j = 0; j < oldCapacity; j++) { - /* - * Rehash the bucket using the minimum number of field writes. This - * is the most subtle and delicate code in the class. - */ - HashMapEntry e = oldTable[j]; - if (e == null) { - continue; - } - int highBit = e.hash & oldCapacity; - HashMapEntry broken = null; - newTable[j | highBit] = e; - for (HashMapEntry n = e.next; n != null; e = n, n = n.next) { - int nextHighBit = n.hash & oldCapacity; - if (nextHighBit != highBit) { - if (broken == null) { - newTable[j | nextHighBit] = n; - } else { - broken.next = n; - } - broken = e; - highBit = nextHighBit; - } - } - if (broken != null) { - broken.next = null; - } - } - return newTable; - } - - /** - * Removes the mapping with the specified key from this map. - * - * @param key the key of the mapping to remove. - * @return the value of the removed mapping or {@code null} if no mapping - * for the specified key was found. - */ - @Override - public V remove(Object key) { - if (key == null) { - return removeNullKey(); - } - int hash = secondaryHashForObject(key); - HashMapEntry[] tab = table; - int index = hash & (tab.length - 1); - for (HashMapEntry e = tab[index], prev = null; e != null; prev = e, e = e.next) { - K eKey = e.key; - if (eKey == key) { - if (prev == null) { - tab[index] = e.next; - } else { - prev.next = e.next; - } - modCount++; - size--; - postRemove(e); - return e.value; - } - } - return null; - } - - private V removeNullKey() { - HashMapEntry e = entryForNullKey; - if (e == null) { - return null; - } - entryForNullKey = null; - modCount++; - size--; - postRemove(e); - return e.value; - } - - /** - * Subclass overrides this method to unlink entry. - */ - void postRemove(HashMapEntry e) { - } - - /** - * Removes all mappings from this hash map, leaving it empty. - * - * @see #isEmpty - * @see #size - */ - @Override - public void clear() { - if (size != 0) { - Arrays.fill(table, null); - entryForNullKey = null; - modCount++; - size = 0; - } - } - - /** - * Returns a set of the keys contained in this map. The set is backed by - * this map so changes to one are reflected by the other. The set does not - * support adding. - * - * @return a set of the keys. - */ - @Override - public Set keySet() { - Set ks = keySet; - return (ks != null) ? ks : (keySet = new KeySet()); - } - - /** - * Returns a collection of the values contained in this map. The collection - * is backed by this map so changes to one are reflected by the other. The - * collection supports remove, removeAll, retainAll and clear operations, - * and it does not support add or addAll operations. - *

- * This method returns a collection which is the subclass of AbstractCollection. The iterator method of this subclass returns a "wrapper object" over the iterator of map's entrySet(). The {@code size} method wraps the map's size method and the {@code contains} method wraps the map's containsValue method. - *

- *

- * The collection is created when this method is called for the first time and returned in response to all subsequent calls. This method may return different collections when multiple concurrent calls occur, since no synchronization is performed. - *

- * - * @return a collection of the values contained in this map. - */ - @Override - public Collection values() { - Collection vs = values; - return (vs != null) ? vs : (values = new Values()); - } - - /** - * Returns a set containing all of the mappings in this map. Each mapping is - * an instance of {@link Map.Entry}. As the set is backed by this map, - * changes in one will be reflected in the other. - * - * @return a set of the mappings. - */ - public Set> entrySet() { - Set> es = entrySet; - return (es != null) ? es : (entrySet = new EntrySet()); - } - - static class HashMapEntry implements Entry { - final K key; - V value; - final int hash; - HashMapEntry next; - - HashMapEntry(K key, V value, int hash, HashMapEntry next) { - this.key = key; - this.value = value; - this.hash = hash; - this.next = next; - } - - public final K getKey() { - return key; - } - - public final V getValue() { - return value; - } - - public final V setValue(V value) { - V oldValue = this.value; - this.value = value; - return oldValue; - } - - @Override - public final boolean equals(Object o) { - if (!(o instanceof Entry)) { - return false; - } - Entry e = (Entry) o; - return key == e.getKey() && value == e.getValue(); - } - - @Override - public final int hashCode() { - return hash ^ (value == null ? 0 : value.hashCode()); - } - - @Override - public final String toString() { - return key + "=" + value; - } - } - - private abstract class HashIterator { - int nextIndex; - HashMapEntry nextEntry = entryForNullKey; - HashMapEntry lastEntryReturned; - int expectedModCount = modCount; - - HashIterator() { - if (nextEntry == null) { - HashMapEntry[] tab = table; - HashMapEntry next = null; - while (next == null && nextIndex < tab.length) { - next = tab[nextIndex++]; - } - nextEntry = next; - } - } - - public boolean hasNext() { - return nextEntry != null; - } - - HashMapEntry nextEntry() { - if (modCount != expectedModCount) { - throw new ConcurrentModificationException(); - } - if (nextEntry == null) { - throw new NoSuchElementException(); - } - - HashMapEntry entryToReturn = nextEntry; - HashMapEntry[] tab = table; - HashMapEntry next = entryToReturn.next; - while (next == null && nextIndex < tab.length) { - next = tab[nextIndex++]; - } - nextEntry = next; - return lastEntryReturned = entryToReturn; - } - - public void remove() { - if (lastEntryReturned == null) { - throw new IllegalStateException(); - } - if (modCount != expectedModCount) { - throw new ConcurrentModificationException(); - } - NativeScriptHashMap.this.remove(lastEntryReturned.key); - lastEntryReturned = null; - expectedModCount = modCount; - } - } - - private final class KeyIterator extends HashIterator implements Iterator { - public K next() { - return nextEntry().key; - } - } - - private final class ValueIterator extends HashIterator implements Iterator { - public V next() { - return nextEntry().value; - } - } - - private final class EntryIterator extends HashIterator implements Iterator> { - public Entry next() { - return nextEntry(); - } - } - - /** - * Returns true if this map contains the specified mapping. - */ - private boolean containsMapping(Object key, Object value) { - if (key == null) { - HashMapEntry e = entryForNullKey; - return e != null && Objects.equal(value, e.value); - } - - int hash = secondaryHashForObject(key); - HashMapEntry[] tab = table; - int index = hash & (tab.length - 1); - for (HashMapEntry e = tab[index]; e != null; e = e.next) { - if (key == e.key) { - return Objects.equal(value, e.value); - } - } - return false; // No entry for key - } - - /** - * Removes the mapping from key to value and returns true if this mapping - * exists; otherwise, returns does nothing and returns false. - */ - private boolean removeMapping(Object key, Object value) { - if (key == null) { - HashMapEntry e = entryForNullKey; - if (e == null || !Objects.equal(value, e.value)) { - return false; - } - entryForNullKey = null; - modCount++; - size--; - postRemove(e); - return true; - } - - int hash = secondaryHashForObject(key); - HashMapEntry[] tab = table; - int index = hash & (tab.length - 1); - for (HashMapEntry e = tab[index], prev = null; e != null; prev = e, e = e.next) { - if (key == e.key) { - if (!Objects.equal(value, e.value)) { - return false; // Map has wrong value for key - } - if (prev == null) { - tab[index] = e.next; - } else { - prev.next = e.next; - } - modCount++; - size--; - postRemove(e); - return true; - } - } - return false; // No entry for key - } - - // Subclass (LinkedHashMap) overrides these for correct iteration order - Iterator newKeyIterator() { - return new KeyIterator(); - } - - Iterator newValueIterator() { - return new ValueIterator(); - } - - Iterator> newEntryIterator() { - return new EntryIterator(); - } - - private final class KeySet extends AbstractSet { - public Iterator iterator() { - return newKeyIterator(); - } - - public int size() { - return size; - } - - public boolean isEmpty() { - return size == 0; - } - - public boolean contains(Object o) { - return containsKey(o); - } - - public boolean remove(Object o) { - int oldSize = size; - NativeScriptHashMap.this.remove(o); - return size != oldSize; - } - - public void clear() { - NativeScriptHashMap.this.clear(); - } - } - - private final class Values extends AbstractCollection { - public Iterator iterator() { - return newValueIterator(); - } - - public int size() { - return size; - } - - public boolean isEmpty() { - return size == 0; - } - - public boolean contains(Object o) { - return containsValue(o); - } - - public void clear() { - NativeScriptHashMap.this.clear(); - } - } - - private final class EntrySet extends AbstractSet> { - public Iterator> iterator() { - return newEntryIterator(); - } - - public boolean contains(Object o) { - if (!(o instanceof Entry)) { - return false; - } - Entry e = (Entry) o; - return containsMapping(e.getKey(), e.getValue()); - } - - public boolean remove(Object o) { - if (!(o instanceof Entry)) { - return false; - } - Entry e = (Entry) o; - return removeMapping(e.getKey(), e.getValue()); - } - - public int size() { - return size; - } - - public boolean isEmpty() { - return size == 0; - } - - public void clear() { - NativeScriptHashMap.this.clear(); - } - } - - private static final long serialVersionUID = 362498820763181265L; - - private static final ObjectStreamField[] serialPersistentFields = - {new ObjectStreamField("loadFactor", float.class)}; - - private void writeObject(ObjectOutputStream stream) throws IOException { - // Emulate loadFactor field for other implementations to read - ObjectOutputStream.PutField fields = stream.putFields(); - fields.put("loadFactor", DEFAULT_LOAD_FACTOR); - stream.writeFields(); - - stream.writeInt(table.length); // Capacity - stream.writeInt(size); - for (Entry e : entrySet()) { - stream.writeObject(e.getKey()); - stream.writeObject(e.getValue()); - } - } - - private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { - stream.defaultReadObject(); - int capacity = stream.readInt(); - if (capacity < 0) { - throw new InvalidObjectException("Capacity: " + capacity); - } - if (capacity < MINIMUM_CAPACITY) { - capacity = MINIMUM_CAPACITY; - } else if (capacity > MAXIMUM_CAPACITY) { - capacity = MAXIMUM_CAPACITY; - } else { - capacity = roundUpToPowerOfTwo(capacity); - } - makeTable(capacity); - - int size = stream.readInt(); - if (size < 0) { - throw new InvalidObjectException("Size: " + size); - } - - init(); // Give subclass (LinkedHashMap) a chance to initialize itself - for (int i = 0; i < size; i++) { - @SuppressWarnings("unchecked") - K key = (K) stream.readObject(); - @SuppressWarnings("unchecked") - V val = (V) stream.readObject(); - constructorPut(key, val); - } - } - - // ~~~~~~~~~~~~~~ - - private static int roundUpToPowerOfTwo(int i) { - i--; // If input is a power of two, shift its high-order bit right. - - // "Smear" the high-order bit all the way to the right. - i |= i >>> 1; - i |= i >>> 2; - i |= i >>> 4; - i |= i >>> 8; - i |= i >>> 16; - - return i + 1; - } - - private static int secondaryHashForObject(Object key) { - int intHash; - - if (key instanceof NativeScriptHashCodeProvider) { - NativeScriptHashCodeProvider provider = (NativeScriptHashCodeProvider) key; - intHash = provider.hashCode__super(); - } else { - intHash = System.identityHashCode(key); - } - - return secondaryHashForInt(intHash); - } - - private static int secondaryHashForInt(int h) { - // Spread bits to regularize both segment and index locations, - // using variant of single-word Wang/Jenkins hash. - h += (h << 15) ^ 0xffffcd7d; - h ^= (h >>> 10); - h += (h << 3); - h ^= (h >>> 6); - h += (h << 2) + (h << 14); - return h ^ (h >>> 16); - } -} \ No newline at end of file diff --git a/test-app/runtime/src/main/java/com/tns/NativeScriptWeakHashMap.java b/test-app/runtime/src/main/java/com/tns/NativeScriptWeakHashMap.java deleted file mode 100644 index b66d00fec..000000000 --- a/test-app/runtime/src/main/java/com/tns/NativeScriptWeakHashMap.java +++ /dev/null @@ -1,725 +0,0 @@ -package com.tns; - -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import java.lang.ref.ReferenceQueue; -import java.lang.ref.WeakReference; -import java.util.AbstractCollection; -import java.util.AbstractSet; -import java.util.Arrays; -import java.util.Collection; -import java.util.ConcurrentModificationException; -import java.util.Iterator; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Set; - -/** - * WeakHashMap is an implementation of Map with keys which are WeakReferences. A - * key/value mapping is removed when the key is no longer referenced. All - * optional operations (adding and removing) are supported. Keys and values can - * be any objects. Note that the garbage collector acts similar to a second - * thread on this collection, possibly removing keys. - * - * @since 1.2 - * @see HashMap - * @see WeakReference - */ -public class NativeScriptWeakHashMap extends NativeScriptAbstractMap implements Map { - - private static final int DEFAULT_SIZE = 16; - - private final ReferenceQueue referenceQueue; - - int elementCount; - - Entry[] elementData; - - private final int loadFactor; - - private int threshold; - - volatile int modCount; - - // Simple utility method to isolate unchecked cast for array creation - @SuppressWarnings({"unchecked", "rawtypes"}) - private static Entry[] newEntryArray(int size) { - return new Entry[size]; - } - - private static final class Entry extends WeakReference implements Map.Entry { - final int hash; - - boolean isNull; - - V value; - - Entry next; - - interface Type { - RET get(Map.Entry entry); - } - - Entry(K key, V object, ReferenceQueue queue) { - super(key, queue); - isNull = key == null; - hash = isNull ? 0 : secondaryHashForObject(key); - value = object; - } - - public K getKey() { - return super.get(); - } - - public V getValue() { - return value; - } - - public V setValue(V object) { - V result = value; - value = object; - return result; - } - - @Override - public boolean equals(Object other) { - if (!(other instanceof Map.Entry)) { - return false; - } - Map.Entry entry = (Map.Entry) other; - Object key = super.get(); - return (key == null ? key == entry.getKey() : key == entry.getKey()) && (value == null ? value == entry.getValue() : value == entry.getValue()); - } - - @Override - public int hashCode() { - return hash + (value == null ? 0 : value.hashCode()); - } - - @Override - public String toString() { - return super.get() + "=" + value; - } - } - - class HashIterator implements Iterator { - private int position = 0, expectedModCount; - - private Entry currentEntry, nextEntry; - - private K nextKey; - - final Entry.Type type; - - HashIterator(Entry.Type type) { - this.type = type; - expectedModCount = modCount; - } - - public boolean hasNext() { - if (nextEntry != null && (nextKey != null || nextEntry.isNull)) { - return true; - } - while (true) { - if (nextEntry == null) { - while (position < elementData.length) { - if ((nextEntry = elementData[position++]) != null) { - break; - } - } - if (nextEntry == null) { - return false; - } - } - // ensure key of next entry is not gc'ed - nextKey = nextEntry.get(); - if (nextKey != null || nextEntry.isNull) { - return true; - } - nextEntry = nextEntry.next; - } - } - - public RET next() { - if (expectedModCount == modCount) { - if (hasNext()) { - currentEntry = nextEntry; - nextEntry = currentEntry.next; - RET result = type.get(currentEntry); - // free the key - nextKey = null; - return result; - } - throw new NoSuchElementException(); - } - throw new ConcurrentModificationException(); - } - - public void remove() { - if (expectedModCount == modCount) { - if (currentEntry != null) { - removeEntry(currentEntry); - currentEntry = null; - expectedModCount++; - // cannot poll() as that would change the expectedModCount - } else { - throw new IllegalStateException(); - } - } else { - throw new ConcurrentModificationException(); - } - } - } - - /** - * Constructs a new empty {@code WeakHashMap} instance. - */ - public NativeScriptWeakHashMap() { - this(DEFAULT_SIZE); - } - - /** - * Constructs a new {@code WeakHashMap} instance with the specified - * capacity. - * - * @param capacity - * the initial capacity of this map. - * @throws IllegalArgumentException - * if the capacity is less than zero. - */ - public NativeScriptWeakHashMap(int capacity) { - if (capacity < 0) { - throw new IllegalArgumentException("capacity < 0: " + capacity); - } - elementCount = 0; - elementData = newEntryArray(capacity == 0 ? 1 : capacity); - loadFactor = 7500; // Default load factor of 0.75 - computeMaxSize(); - referenceQueue = new ReferenceQueue(); - } - - /** - * Constructs a new {@code WeakHashMap} instance with the specified capacity - * and load factor. - * - * @param capacity - * the initial capacity of this map. - * @param loadFactor - * the initial load factor. - * @throws IllegalArgumentException - * if the capacity is less than zero or the load factor is less - * or equal to zero. - */ - public NativeScriptWeakHashMap(int capacity, float loadFactor) { - if (capacity < 0) { - throw new IllegalArgumentException("capacity < 0: " + capacity); - } - if (loadFactor <= 0) { - throw new IllegalArgumentException("loadFactor <= 0: " + loadFactor); - } - elementCount = 0; - elementData = newEntryArray(capacity == 0 ? 1 : capacity); - this.loadFactor = (int) (loadFactor * 10000); - computeMaxSize(); - referenceQueue = new ReferenceQueue(); - } - - /** - * Constructs a new {@code WeakHashMap} instance containing the mappings - * from the specified map. - * - * @param map - * the mappings to add. - */ - public NativeScriptWeakHashMap(Map map) { - this(map.size() < 6 ? 11 : map.size() * 2); - putAllImpl(map); - } - - /** - * Removes all mappings from this map, leaving it empty. - * - * @see #isEmpty() - * @see #size() - */ - @Override - public void clear() { - if (elementCount > 0) { - elementCount = 0; - Arrays.fill(elementData, null); - modCount++; - while (referenceQueue.poll() != null) { - // do nothing - } - } - } - - private void computeMaxSize() { - threshold = (int) ((long) elementData.length * loadFactor / 10000); - } - - /** - * Returns whether this map contains the specified key. - * - * @param key - * the key to search for. - * @return {@code true} if this map contains the specified key, {@code false} otherwise. - */ - @Override - public boolean containsKey(Object key) { - return getEntry(key) != null; - } - - /** - * Returns a set containing all of the mappings in this map. Each mapping is - * an instance of {@link Map.Entry}. As the set is backed by this map, - * changes in one will be reflected in the other. It does not support adding - * operations. - * - * @return a set of the mappings. - */ - @Override - public Set> entrySet() { - poll(); - return new AbstractSet>() { - @Override - public int size() { - return NativeScriptWeakHashMap.this.size(); - } - - @Override - public void clear() { - NativeScriptWeakHashMap.this.clear(); - } - - @Override - public boolean remove(Object object) { - if (contains(object)) { - NativeScriptWeakHashMap.this.remove(((Map.Entry) object).getKey()); - return true; - } - return false; - } - - @Override - public boolean contains(Object object) { - if (object instanceof Map.Entry) { - Entry entry = getEntry(((Map.Entry) object).getKey()); - if (entry != null) { - Object key = entry.get(); - if (key != null || entry.isNull) { - return object.equals(entry); - } - } - } - return false; - } - - @Override - public Iterator> iterator() { - return new HashIterator>(new Entry.Type, K, V>() { - public Map.Entry get(Map.Entry entry) { - return entry; - } - }); - } - }; - } - - /** - * Returns a set of the keys contained in this map. The set is backed by - * this map so changes to one are reflected by the other. The set does not - * support adding. - * - * @return a set of the keys. - */ - @Override - public Set keySet() { - poll(); - if (keySet == null) { - keySet = new AbstractSet() { - @Override - public boolean contains(Object object) { - return containsKey(object); - } - - @Override - public int size() { - return NativeScriptWeakHashMap.this.size(); - } - - @Override - public void clear() { - NativeScriptWeakHashMap.this.clear(); - } - - @Override - public boolean remove(Object key) { - if (containsKey(key)) { - NativeScriptWeakHashMap.this.remove(key); - return true; - } - return false; - } - - @Override - public Iterator iterator() { - return new HashIterator(new Entry.Type() { - public K get(Map.Entry entry) { - return entry.getKey(); - } - }); - } - }; - } - return keySet; - } - - /** - * Returns a collection of the values contained in this map. The collection - * is backed by this map so changes to one are reflected by the other. The - * collection supports remove, removeAll, retainAll and clear operations, - * and it does not support add or addAll operations. - *

- * This method returns a collection which is the subclass of AbstractCollection. The iterator method of this subclass returns a "wrapper object" over the iterator of map's entrySet(). The size method wraps the map's size method and the contains method wraps the map's containsValue method. - *

- * The collection is created when this method is called at first time and returned in response to all subsequent calls. This method may return different Collection when multiple calls to this method, since it has no synchronization performed. - * - * @return a collection of the values contained in this map. - */ - @Override - public Collection values() { - poll(); - if (valuesCollection == null) { - valuesCollection = new AbstractCollection() { - @Override - public int size() { - return NativeScriptWeakHashMap.this.size(); - } - - @Override - public void clear() { - NativeScriptWeakHashMap.this.clear(); - } - - @Override - public boolean contains(Object object) { - return containsValue(object); - } - - @Override - public Iterator iterator() { - return new HashIterator(new Entry.Type() { - public V get(Map.Entry entry) { - return entry.getValue(); - } - }); - } - }; - } - return valuesCollection; - } - - /** - * Returns the value of the mapping with the specified key. - * - * @param key - * the key. - * @return the value of the mapping with the specified key, or {@code null} if no mapping for the specified key is found. - */ - @Override - public V get(Object key) { - poll(); - if (key != null) { - int index = (secondaryHashForObject(key) & 0x7FFFFFFF) % elementData.length; - Entry entry = elementData[index]; - while (entry != null) { - if (key == entry.get()) { - return entry.value; - } - entry = entry.next; - } - return null; - } - Entry entry = elementData[0]; - while (entry != null) { - if (entry.isNull) { - return entry.value; - } - entry = entry.next; - } - return null; - } - - Entry getEntry(Object key) { - poll(); - if (key != null) { - int index = (secondaryHashForObject(key) & 0x7FFFFFFF) % elementData.length; - Entry entry = elementData[index]; - while (entry != null) { - if (key == entry.get()) { - return entry; - } - entry = entry.next; - } - return null; - } - Entry entry = elementData[0]; - while (entry != null) { - if (entry.isNull) { - return entry; - } - entry = entry.next; - } - return null; - } - - /** - * Returns whether this map contains the specified value. - * - * @param value - * the value to search for. - * @return {@code true} if this map contains the specified value, {@code false} otherwise. - */ - @Override - public boolean containsValue(Object value) { - poll(); - if (value != null) { - for (int i = elementData.length; --i >= 0;) { - Entry entry = elementData[i]; - while (entry != null) { - K key = entry.get(); - if ((key != null || entry.isNull) && value == entry.value) { - return true; - } - entry = entry.next; - } - } - } else { - for (int i = elementData.length; --i >= 0;) { - Entry entry = elementData[i]; - while (entry != null) { - K key = entry.get(); - if ((key != null || entry.isNull) && entry.value == null) { - return true; - } - entry = entry.next; - } - } - } - return false; - } - - /** - * Returns the number of elements in this map. - * - * @return the number of elements in this map. - */ - @Override - public boolean isEmpty() { - return size() == 0; - } - - @SuppressWarnings("unchecked") - void poll() { - Entry toRemove; - while ((toRemove = (Entry) referenceQueue.poll()) != null) { - removeEntry(toRemove); - } - } - - void removeEntry(Entry toRemove) { - Entry entry, last = null; - int index = (toRemove.hash & 0x7FFFFFFF) % elementData.length; - entry = elementData[index]; - // Ignore queued entries which cannot be found, the user could - // have removed them before they were queued, i.e. using clear() - while (entry != null) { - if (toRemove == entry) { - modCount++; - if (last == null) { - elementData[index] = entry.next; - } else { - last.next = entry.next; - } - elementCount--; - break; - } - last = entry; - entry = entry.next; - } - } - - /** - * Maps the specified key to the specified value. - * - * @param key - * the key. - * @param value - * the value. - * @return the value of any previous mapping with the specified key or {@code null} if there was no mapping. - */ - @Override - public V put(K key, V value) { - poll(); - int index = 0; - Entry entry; - if (key != null) { - index = (secondaryHashForObject(key) & 0x7FFFFFFF) % elementData.length; - entry = elementData[index]; - while (entry != null && key != entry.get()) { - entry = entry.next; - } - } else { - entry = elementData[0]; - while (entry != null && !entry.isNull) { - entry = entry.next; - } - } - if (entry == null) { - modCount++; - if (++elementCount > threshold) { - rehash(); - index = key == null ? 0 : (secondaryHashForObject(key) & 0x7FFFFFFF) % elementData.length; - } - entry = new Entry(key, value, referenceQueue); - entry.next = elementData[index]; - elementData[index] = entry; - return null; - } - V result = entry.value; - entry.value = value; - return result; - } - - private void rehash() { - int length = elementData.length * 2; - if (length == 0) { - length = 1; - } - Entry[] newData = newEntryArray(length); - for (int i = 0; i < elementData.length; i++) { - Entry entry = elementData[i]; - while (entry != null) { - int index = entry.isNull ? 0 : (entry.hash & 0x7FFFFFFF) % length; - Entry next = entry.next; - entry.next = newData[index]; - newData[index] = entry; - entry = next; - } - } - elementData = newData; - computeMaxSize(); - } - - /** - * Copies all the mappings in the given map to this map. These mappings will - * replace all mappings that this map had for any of the keys currently in - * the given map. - * - * @param map - * the map to copy mappings from. - * @throws NullPointerException - * if {@code map} is {@code null}. - */ - @Override - public void putAll(Map map) { - putAllImpl(map); - } - - /** - * Removes the mapping with the specified key from this map. - * - * @param key - * the key of the mapping to remove. - * @return the value of the removed mapping or {@code null} if no mapping - * for the specified key was found. - */ - @Override - public V remove(Object key) { - poll(); - int index = 0; - Entry entry, last = null; - if (key != null) { - index = (secondaryHashForObject(key) & 0x7FFFFFFF) % elementData.length; - entry = elementData[index]; - while (entry != null && key != entry.get()) { - last = entry; - entry = entry.next; - } - } else { - entry = elementData[0]; - while (entry != null && !entry.isNull) { - last = entry; - entry = entry.next; - } - } - if (entry != null) { - modCount++; - if (last == null) { - elementData[index] = entry.next; - } else { - last.next = entry.next; - } - elementCount--; - return entry.value; - } - return null; - } - - /** - * Returns the number of elements in this map. - * - * @return the number of elements in this map. - */ - @Override - public int size() { - poll(); - return elementCount; - } - - private void putAllImpl(Map map) { - if (map.entrySet() != null) { - super.putAll(map); - } - } - - private static int secondaryHashForObject(Object key) { - int intHash; - - if (key instanceof NativeScriptHashCodeProvider) { - NativeScriptHashCodeProvider provider = (NativeScriptHashCodeProvider) key; - intHash = provider.hashCode__super(); - } else { - intHash = System.identityHashCode(key); - } - - return secondaryHashForInt(intHash); - } - - private static int secondaryHashForInt(int h) { - // Spread bits to regularize both segment and index locations, - // using variant of single-word Wang/Jenkins hash. - h += (h << 15) ^ 0xffffcd7d; - h ^= (h >>> 10); - h += (h << 3); - h ^= (h >>> 6); - h += (h << 2) + (h << 14); - return h ^ (h >>> 16); - } -} diff --git a/test-app/runtime/src/main/java/com/tns/ObjectTable.java b/test-app/runtime/src/main/java/com/tns/ObjectTable.java new file mode 100644 index 000000000..4962d3488 --- /dev/null +++ b/test-app/runtime/src/main/java/com/tns/ObjectTable.java @@ -0,0 +1,564 @@ +package com.tns; + +import java.lang.ref.WeakReference; + +/** + * A runtime's registry of the Java objects its JS wrappers refer to, by object id. An entry + * holds its object strongly while the JS wrapper is reachable and through a WeakReference + * after the wrapper was collected (until the object is collected too). + * + * Where StampedLock exists (API 24+) any thread may read and findOrRegister: writers are + * serialized by its write lock and exact reads validate against it. Without it, every + * mutation and every exact read must come from the thread that holds the runtime's + * isolate lock. Neither table allocates per entry. + */ +final class ObjectTable { + static final int NO_ID = -1; + + // Set in findOrRegister's result when that call registered the object. + static final long FRESH = 1L << 32; + + interface IdSource { + int newId(); + } + + // Null where StampedLock is unavailable (API < 24): findIdOptimistic then always misses. + private final Stamps stamps; + + ObjectTable() { + this(false); + } + + ObjectTable(boolean optimisticReads) { + stamps = optimisticReads ? new Stamps() : null; + } + + /** + * Writers are already serialized by the isolate lock; the write stamp only lets an + * optimistic reader detect that a write overlapped it. Kept in its own class so that + * runtimes where StampedLock is missing never load it. + */ + private static final class Stamps { + private final java.util.concurrent.locks.StampedLock lock = new java.util.concurrent.locks.StampedLock(); + + long beginWrite() { + return lock.writeLock(); + } + + void endWrite(long stamp) { + lock.unlockWrite(stamp); + } + + long beginRead() { + return lock.tryOptimisticRead(); + } + + long readLock() { + return lock.readLock(); + } + + void unlockRead(long stamp) { + lock.unlockRead(stamp); + } + + boolean validate(long stamp) { + return lock.validate(stamp); + } + } + + /** + * The id obj is registered under, from any thread, without the isolate lock. A miss + * (NO_ID) is not authoritative: it is also returned when a write overlapped the read. + * A hit was correct at the time of the read; the object may have been released since, + * which the caller must check under the isolate lock. + */ + int findIdOptimistic(Object obj) { + if (stamps == null) { + return NO_ID; + } + long stamp = stamps.beginRead(); + if (stamp == 0) { + return NO_ID; + } + int id; + try { + id = findIdUnlocked(obj); + } catch (RuntimeException e) { + // A torn view of a resize can index past an array; the stamp check below fails then. + id = NO_ID; + } + return stamps.validate(stamp) ? id : NO_ID; + } + + private int findIdUnlocked(Object obj) { + int[] index = identityIndex; + int[] hashes = identityIndexHashes; + int mask = index.length - 1; + int hash = System.identityHashCode(obj); + int i = mix(hash) & mask; + for (int probes = 0; probes <= mask; probes++) { + int entry = index[i]; + if (entry == 0) { + return NO_ID; + } + if (hashes[i] == hash && getUnlocked(entry - 1) == obj) { + return entry - 1; + } + i = (i + 1) & mask; + } + return NO_ID; + } + + // get() without the lock. Safe on a torn view: every array it reads is indexed within + // its own bounds, and a stale one only makes the caller's stamp check fail. + private Object getUnlocked(int id) { + if (id == NO_ID) { + return null; + } + Chunk[] cs = chunks; + int c = id >>> CHUNK_SHIFT; + if (c >= cs.length) { + return null; + } + Chunk chunk = cs[c]; + if (chunk == null) { + return null; + } + Leaf leaf = chunk.leaves[(id >>> LEAF_BITS) & CHUNK_MASK]; + if (leaf == null) { + return null; + } + Object value = leaf.values[id & LEAF_MASK]; + return value == null ? null : deref(value); + } + + private static Object deref(Object value) { + return value instanceof WeakRef ? ((WeakRef) value).get() : value; + } + + /* + * id -> entry, as a radix tree indexed by id: chunks of leaves of slots. Ids are unsigned + * 32-bit values carried in an int (NO_ID is the one excluded value); they come from a + * monotonic counter and are never reused, so a leaf or chunk that empties is dropped + * unless it is the newest; an id registered after its leaf was dropped re-creates it. + * Leaves and chunks are small, so a long-lived object keeps at most one of each alive. + */ + private static final int LEAF_BITS = 6; + private static final int LEAF_SIZE = 1 << LEAF_BITS; + private static final int LEAF_MASK = LEAF_SIZE - 1; + private static final int CHUNK_BITS = 6; + private static final int CHUNK_MASK = (1 << CHUNK_BITS) - 1; + private static final int CHUNK_SHIFT = LEAF_BITS + CHUNK_BITS; + + private static final class Leaf { + // The object, or a WeakRef to it; null for no entry. + final Object[] values = new Object[LEAF_SIZE]; + // The object's identity hash, to find its identity index entry on removal. + final int[] hashes = new int[LEAF_SIZE]; + int live; + } + + private static final class Chunk { + final Leaf[] leaves = new Leaf[1 << CHUNK_BITS]; + int liveLeaves; + } + + private Chunk[] chunks = new Chunk[16]; + // id >>> LEAF_BITS of the highest id registered. + private int newestLeaf = 0; + private int idCount = 0; + + // identity hash -> id + 1, open addressing; the id's entry decides which object it holds. + // identityIndexHashes[i] is the hash of identityIndex[i]'s object, so a probe only looks + // the id up when the hashes match. + private int[] identityIndex = new int[1024]; + private int[] identityIndexHashes = new int[1024]; + private int identityCount = 0; + + /** Registers obj under id, held strongly, replacing whatever id held before. */ + void putStrong(int id, Object obj) { + if (stamps == null) { + putStrongLocked(id, obj); + return; + } + long stamp = stamps.beginWrite(); + try { + putStrongLocked(id, obj); + } finally { + stamps.endWrite(stamp); + } + } + + private void putStrongLocked(int id, Object obj) { + putStrongLocked(id, obj, System.identityHashCode(obj)); + } + + private void putStrongLocked(int id, Object obj, int hash) { + Leaf existingLeaf = leafFor(id); + if (existingLeaf != null) { + Object value = existingLeaf.values[id & LEAF_MASK]; + if (value != null) { + if (deref(value) == obj) { + existingLeaf.values[id & LEAF_MASK] = obj; + return; + } + removeLocked(id); + } + } + + insertEntry(id, obj, hash); + idCount++; + + if ((identityCount + 1) * 2 > identityIndex.length) { + growIdentity(); + } + insertIdentity(hash, id); + identityCount++; + } + + boolean allowsConcurrentRegistration() { + return stamps != null; + } + + /** + * The id obj is registered under, registering it strongly under a new id from ids when + * it has none; FRESH is set in the result when this call registered it. The lookup and + * the insert happen under one write lock, so concurrent calls agree on one id. Only + * when allowsConcurrentRegistration(). + */ + long findOrRegister(Object obj, IdSource ids) { + long stamp = stamps.beginWrite(); + try { + int hash = System.identityHashCode(obj); + int id = findIdLocked(obj, hash); + if (id != NO_ID) { + return id & 0xFFFFFFFFL; + } + id = ids.newId(); + putStrongLocked(id, obj, hash); + return (id & 0xFFFFFFFFL) | FRESH; + } finally { + stamps.endWrite(stamp); + } + } + + /** The id obj is registered under, or NO_ID. */ + int findId(Object obj) { + if (stamps == null) { + return findIdLocked(obj); + } + long stamp = stamps.beginRead(); + if (stamp != 0) { + try { + int id = findIdUnlocked(obj); + if (stamps.validate(stamp)) { + return id; + } + } catch (RuntimeException e) { + // A torn view of a resize; retried under the read lock. + } + } + stamp = stamps.readLock(); + try { + return findIdLocked(obj); + } finally { + stamps.unlockRead(stamp); + } + } + + private int findIdLocked(Object obj) { + return findIdLocked(obj, System.identityHashCode(obj)); + } + + private int findIdLocked(Object obj, int hash) { + int mask = identityIndex.length - 1; + int i = mix(hash) & mask; + while (true) { + int entry = identityIndex[i]; + if (entry == 0) { + return NO_ID; + } + int id = entry - 1; + if (identityIndexHashes[i] == hash && getLocked(id) == obj) { + return id; + } + i = (i + 1) & mask; + } + } + + /** The object registered under id; null if there is none or it was collected. */ + Object get(int id) { + if (stamps == null) { + return getLocked(id); + } + long stamp = stamps.beginRead(); + if (stamp != 0) { + try { + Object value = getUnlocked(id); + if (stamps.validate(stamp)) { + return value; + } + } catch (RuntimeException e) { + // A torn view of a resize; retried under the read lock. + } + } + stamp = stamps.readLock(); + try { + return getLocked(id); + } finally { + stamps.unlockRead(stamp); + } + } + + private Object getLocked(int id) { + Object value = rawLocked(id); + return value == null ? null : deref(value); + } + + boolean contains(int id) { + if (stamps == null) { + return rawLocked(id) != null; + } + long stamp = stamps.readLock(); + try { + return rawLocked(id) != null; + } finally { + stamps.unlockRead(stamp); + } + } + + /** + * The JS wrapper for id was collected: hold the object weakly from now on. Returns whether + * the object is still alive; an entry whose object was already collected is removed. + */ + boolean weakenAndCheckAlive(int id) { + if (stamps == null) { + return weakenAndCheckAliveLocked(id); + } + long stamp = stamps.beginWrite(); + try { + return weakenAndCheckAliveLocked(id); + } finally { + stamps.endWrite(stamp); + } + } + + /** + * weakenAndCheckAlive for ids[0..count), under one write lock; overwrites the id of + * every object no longer alive with NO_ID. + */ + void weakenAndCheckAlive(int[] ids, int count) { + if (count == 0) { + return; + } + long stamp = stamps == null ? 0 : stamps.beginWrite(); + try { + for (int i = 0; i < count; i++) { + if (!weakenAndCheckAliveLocked(ids[i])) { + ids[i] = NO_ID; + } + } + } finally { + if (stamps != null) { + stamps.endWrite(stamp); + } + } + } + + private boolean weakenAndCheckAliveLocked(int id) { + Leaf leaf = leafFor(id); + Object value = leaf == null ? null : leaf.values[id & LEAF_MASK]; + if (value == null) { + return false; + } + if (value instanceof WeakRef) { + if (((WeakRef) value).get() != null) { + return true; + } + removeLocked(id); + return false; + } + leaf.values[id & LEAF_MASK] = new WeakRef(value); + return true; + } + + void remove(int id) { + if (stamps == null) { + removeLocked(id); + return; + } + long stamp = stamps.beginWrite(); + try { + removeLocked(id); + } finally { + stamps.endWrite(stamp); + } + } + + private void removeLocked(int id) { + Leaf leaf = leafFor(id); + int i = id & LEAF_MASK; + if (leaf == null || leaf.values[i] == null) { + return; + } + int hash = leaf.hashes[i]; + leaf.values[i] = null; + leaf.hashes[i] = 0; + leaf.live--; + int leafNumber = id >>> LEAF_BITS; + if (leafNumber != newestLeaf) { + releaseIfEmpty(leafNumber); + } + idCount--; + removeIdentity(hash, id); + identityCount--; + } + + int size() { + return idCount; + } + + // Distinguishes the table's own weak entries from a registered object that happens to be + // a WeakReference. + private static final class WeakRef extends WeakReference { + WeakRef(Object referent) { + super(referent); + } + } + + private Leaf leafFor(int id) { + if (id == NO_ID) { + return null; + } + int c = id >>> CHUNK_SHIFT; + Chunk chunk = c < chunks.length ? chunks[c] : null; + return chunk == null ? null : chunk.leaves[(id >>> LEAF_BITS) & CHUNK_MASK]; + } + + // The entry for id as stored (the object or a WeakRef to it), or null. + private Object rawLocked(int id) { + Leaf leaf = leafFor(id); + return leaf == null ? null : leaf.values[id & LEAF_MASK]; + } + + // Adds an entry for an id that has none. + private void insertEntry(int id, Object value, int hash) { + int c = id >>> CHUNK_SHIFT; + if (c >= chunks.length) { + int length = chunks.length; + while (length <= c) { + length *= 2; + } + Chunk[] grown = new Chunk[length]; + System.arraycopy(chunks, 0, grown, 0, chunks.length); + chunks = grown; + } + Chunk chunk = chunks[c]; + if (chunk == null) { + chunk = new Chunk(); + chunks[c] = chunk; + } + int l = (id >>> LEAF_BITS) & CHUNK_MASK; + Leaf leaf = chunk.leaves[l]; + if (leaf == null) { + leaf = new Leaf(); + chunk.leaves[l] = leaf; + chunk.liveLeaves++; + } + int i = id & LEAF_MASK; + leaf.values[i] = value; + leaf.hashes[i] = hash; + leaf.live++; + int leafNumber = id >>> LEAF_BITS; + if (leafNumber > newestLeaf) { + int previous = newestLeaf; + newestLeaf = leafNumber; + // Kept while it was the newest; nothing else drops it once it is empty. + releaseIfEmpty(previous); + } + } + + // Drops leaf leafNumber if it is empty, then its chunk if that empties and is not the + // newest. + private void releaseIfEmpty(int leafNumber) { + int c = leafNumber >>> CHUNK_BITS; + Chunk chunk = c < chunks.length ? chunks[c] : null; + if (chunk == null) { + return; + } + int l = leafNumber & CHUNK_MASK; + Leaf leaf = chunk.leaves[l]; + if (leaf == null || leaf.live != 0) { + return; + } + chunk.leaves[l] = null; + if (--chunk.liveLeaves == 0 && c != (newestLeaf >>> CHUNK_BITS)) { + chunks[c] = null; + } + } + + private void insertIdentity(int hash, int id) { + insertIdentity(identityIndex, identityIndexHashes, hash, id); + } + + private static void insertIdentity(int[] index, int[] hashes, int hash, int id) { + int mask = index.length - 1; + int i = mix(hash) & mask; + while (index[i] != 0) { + i = (i + 1) & mask; + } + hashes[i] = hash; + index[i] = id + 1; + } + + private void removeIdentity(int hash, int id) { + int mask = identityIndex.length - 1; + int i = mix(hash) & mask; + while (identityIndex[i] != id + 1) { + i = (i + 1) & mask; + } + int hole = i; + int j = i; + while (true) { + j = (j + 1) & mask; + int entry = identityIndex[j]; + if (entry == 0) { + break; + } + int home = mix(identityIndexHashes[j]) & mask; + if (!isBetween(hole, home, j)) { + identityIndex[hole] = entry; + identityIndexHashes[hole] = identityIndexHashes[j]; + hole = j; + } + } + identityIndex[hole] = 0; + identityIndexHashes[hole] = 0; + } + + // Whether home lies cyclically in (hole, j], i.e. the entry at j must stay put. + private static boolean isBetween(int hole, int home, int j) { + return hole <= j ? (hole < home && home <= j) : (hole < home || home <= j); + } + + private void growIdentity() { + int[] oldIndex = identityIndex; + int[] oldHashes = identityIndexHashes; + int[] index = new int[oldIndex.length * 2]; + int[] hashes = new int[index.length]; + for (int i = 0; i < oldIndex.length; i++) { + if (oldIndex[i] != 0) { + insertIdentity(index, hashes, oldHashes[i], oldIndex[i] - 1); + } + } + identityIndexHashes = hashes; + identityIndex = index; + } + + private static int mix(int h) { + h ^= (h >>> 16); + h *= 0x85ebca6b; + h ^= (h >>> 13); + return h; + } +} diff --git a/test-app/runtime/src/main/java/com/tns/Runtime.java b/test-app/runtime/src/main/java/com/tns/Runtime.java index 3aa1709e4..e2bf3189d 100644 --- a/test-app/runtime/src/main/java/com/tns/Runtime.java +++ b/test-app/runtime/src/main/java/com/tns/Runtime.java @@ -11,7 +11,6 @@ import java.io.File; import java.io.IOException; -import java.lang.ref.WeakReference; import java.lang.reflect.Array; import java.lang.reflect.Constructor; import java.lang.reflect.Field; @@ -43,7 +42,7 @@ private native void initNativeScript(int runtimeId, String filesPath, String nat private native Object runScript(int runtimeId, String filePath) throws NativeScriptException; - private native Object callJSMethodNative(int runtimeId, int javaObjectID, String methodName, int retType, boolean isConstructor, int[] argTypes, long[] argValues, Object[] argRefs, long[] primitiveResult) throws NativeScriptException; + private native Object callJSMethodNative(int runtimeId, Object javaObject, int javaObjectID, String methodName, int methodNameId, int retType, boolean isConstructor, int argCount, long[] argValues, Object[] argRefs, long[] primitiveResult) throws NativeScriptException; private native void createJSInstanceNative(int runtimeId, Object javaObject, int javaObjectID, String canonicalName); @@ -58,6 +57,12 @@ private native void initNativeScript(int runtimeId, String filesPath, String nat private static native int generateNewObjectIdCritical(int runtimeId); private static native int generateNewObjectIdLegacy(int runtimeId); + /** + * How long a Java thread calling into JS spins for the isolate before sleeping, in + * microseconds; negative lets callers go straight to V8's lock. + */ + public static native void setAdmissionSpinMicros(int micros); + private static int generateNewObjectId(int runtimeId) { return SUPPORTS_OPTIMIZED_NATIVE ? generateNewObjectIdCritical(runtimeId) @@ -176,20 +181,33 @@ public void run() { "Primitive types need to be manually wrapped in their respective Object wrappers.\n" + "If you are creating an instance of an inner class, make sure to always provide reference to the outer `this` as the first argument."; - private Map strongInstances = new HashMap<>(); + // Any thread may read it optimistically and register new objects through findOrRegister + // (see ObjectTable); every other mutation and every exact read comes from the thread + // holding this runtime's isolate lock. + private final ObjectTable objectTable; - private Map> weakInstances = new HashMap<>(); - - private Map strongJavaObjectToID = new NativeScriptHashMap(); + private final ObjectTable.IdSource objectIds = new ObjectTable.IdSource() { + @Override + public int newId() { + return generateNewObjectId(getRuntimeId()); + } + }; - private Map weakJavaObjectToID = new NativeScriptWeakHashMap(); + // Set in a packed argument when packageArgs registered the object itself, before the + // isolate lock; native then wraps it without asking Java again. + private static final long FRESH_ARG = 1L << 63; private Map, JavaScriptImplementation> loadedJavaScriptExtends = new HashMap, JavaScriptImplementation>(); private final java.lang.Runtime dalvikRuntime = java.lang.Runtime.getRuntime(); private final Object keyNotFoundObject = new Object(); + // The id and class of the instance JS is constructing, set by the native side around + // the Java constructor call. Only an instance of exactly that class may take the id: any + // other binding instance created while that constructor runs was created by Java. The + // class is only meaningful while the id is set, and only native code writes it. private int currentObjectId = -1; + private Class currentObjectClass; private ExtractPolicy extractPolicy; @@ -226,6 +244,8 @@ public int compare(Method lhs, Method rhs) { }; private final StaticConfiguration config; + private final boolean discardUncaughtJsExceptions; + private final boolean multithreadedJavascript; private static StaticConfiguration staticConfiguration; private final DynamicConfiguration dynamicConfig; @@ -247,24 +267,74 @@ public int compare(Method lhs, Method rhs) { */ private static volatile Runtime mainRuntime; private final static Map runtimeCache = new ConcurrentHashMap<>(); + // runtimeCache by id for the Java->JS call path, which would otherwise box the id and + // probe the map on every call; ids past its length fall back to the map. + private static volatile Runtime[] runtimesById = new Runtime[8]; + private static final int MAX_INDEXED_RUNTIME_ID = 1024; + + private static synchronized void indexRuntime(int runtimeId, Runtime runtime) { + if (runtimeId < 0 || runtimeId >= MAX_INDEXED_RUNTIME_ID) { + return; + } + Runtime[] index = runtimesById; + index = Arrays.copyOf(index, Math.max(index.length, Integer.highestOneBit(runtimeId) * 2)); + index[runtimeId] = runtime; + runtimesById = index; + } + + private static Runtime runtimeForId(int runtimeId) { + Runtime[] index = runtimesById; + if (runtimeId >= 0 && runtimeId < index.length) { + return index[runtimeId]; + } + return runtimeCache.get(runtimeId); + } public static boolean nativeLibraryLoaded; private static final ClassStorageService classStorageService = new ClassStorageServiceImpl(ClassCacheImpl.INSTANCE, ClassLoadersCollectionImpl.INSTANCE); - public Runtime(ClassResolver classResolver, GcListener gcListener, StaticConfiguration config, DynamicConfiguration dynamicConfig, int runtimeId, int workerId, HashMap strongInstances, HashMap> weakInstances, NativeScriptHashMap strongJavaObjectToId, NativeScriptWeakHashMap weakJavaObjectToId) { + // A dense index per runtime class, shared by every runtime, so native code can cache a + // class's name and global ref by index instead of asking Java for them per object. 0 is + // never assigned; native reads it as "no index". + private static final ConcurrentHashMap, Integer> classIndices = new ConcurrentHashMap<>(); + private static int nextClassIndex = 1; + + static int classIndexOf(Class clazz) { + Integer index = classIndices.get(clazz); + return index != null ? index : registerClass(clazz); + } + + private static int registerClass(Class clazz) { + synchronized (classIndices) { + Integer index = classIndices.get(clazz); + if (index == null) { + classStorageService.storeClass(clazz.getName(), clazz); + index = nextClassIndex++; + classIndices.put(clazz, index); + } + return index; + } + } + + // An object id in the low half, the object's class index in the high half. + static long packIdAndClass(int objectId, Object obj) { + return ((long) classIndexOf(obj.getClass()) << 32) | (objectId & 0xFFFFFFFFL); + } + + public Runtime(ClassResolver classResolver, GcListener gcListener, StaticConfiguration config, DynamicConfiguration dynamicConfig, int runtimeId, int workerId, ObjectTable objectTable) { this.classResolver = classResolver; this.gcListener = gcListener; this.config = config; + this.discardUncaughtJsExceptions = config != null && config.appConfig != null && config.appConfig.getDiscardUncaughtJsExceptions(); + this.multithreadedJavascript = config != null && config.appConfig != null && config.appConfig.getEnableMultithreadedJavascript(); this.dynamicConfig = dynamicConfig; this.runtimeId = runtimeId; this.workerId = workerId; - this.strongInstances = strongInstances; - this.weakInstances = weakInstances; - this.strongJavaObjectToID = strongJavaObjectToId; - this.weakJavaObjectToID = weakJavaObjectToId; + this.objectTable = objectTable; } public Runtime(StaticConfiguration config, DynamicConfiguration dynamicConfiguration) { + this.objectTable = new ObjectTable(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N); synchronized (Runtime.currentRuntime) { ManualInstrumentation.Frame frame = ManualInstrumentation.start("new Runtime"); try { @@ -275,18 +345,16 @@ public Runtime(StaticConfiguration config, DynamicConfiguration dynamicConfigura this.runtimeId = nextRuntimeId.getAndIncrement(); this.config = config; + this.discardUncaughtJsExceptions = config.appConfig.getDiscardUncaughtJsExceptions(); + this.multithreadedJavascript = config.appConfig.getEnableMultithreadedJavascript(); this.dynamicConfig = dynamicConfiguration; this.threadScheduler = dynamicConfiguration.myThreadScheduler; this.workerId = dynamicConfiguration.workerId; // if multithreadedJS, make all maps concurrent or synchronized: if (config.appConfig.getEnableMultithreadedJavascript()) { - this.strongInstances = new ConcurrentHashMap<>(); - this.weakInstances = new ConcurrentHashMap<>(); // TODO: can't use a ConcurrentHashMap for loadedJavaScriptExtends because it loads null objects, which aren't supported // either leave it like this or create a separate set for null caches this.loadedJavaScriptExtends = Collections.synchronizedMap(new HashMap<>()); - this.strongJavaObjectToID = Collections.synchronizedMap(new NativeScriptHashMap<>()); - this.weakJavaObjectToID = Collections.synchronizedMap(new NativeScriptWeakHashMap<>()); } classResolver = new ClassResolver(classStorageService); @@ -299,6 +367,7 @@ public Runtime(StaticConfiguration config, DynamicConfiguration dynamicConfigura // the thread-local or the cache currentRuntime.set(this); runtimeCache.put(this.runtimeId, this); + indexRuntime(this.runtimeId, this); } finally { frame.close(); } @@ -444,10 +513,12 @@ public static String[] getRemoteModuleAllowlist() { * The tag is the runtime id plus one, so that the field's zero default means * "no owner yet": runtime id 0 is the main runtime's. */ - private static void recordOwningRuntime(Object instance, int runtimeId) { + private static void recordOwningRuntime(Object instance, int runtimeId, int objectId) { if (instance instanceof NativeScriptRuntimeBound) { NativeScriptRuntimeBound bound = (NativeScriptRuntimeBound) instance; if (bound.getRuntimeTag__ns() == 0) { + // Before the tag: readers take the id only once they see a tag. + bound.setObjectId__ns(objectId); bound.setRuntimeTag__ns(runtimeId + 1); } } @@ -554,18 +625,10 @@ public void ResetDateTimeConfigurationCache() { } } + // Only native code may call this, from the thread holding the isolate lock. + @RuntimeCallable public void releaseNativeCounterpart(int nativeObjectId) { - Object strongRef = strongInstances.get(nativeObjectId); - if (strongRef != null) { - strongInstances.remove(nativeObjectId); - strongJavaObjectToID.remove(strongRef); - } - - WeakReference weakRef = weakInstances.get(nativeObjectId); - if (weakRef != null) { - weakInstances.remove(nativeObjectId); - weakJavaObjectToID.remove(weakRef); - } + objectTable.remove(nativeObjectId); } /* @@ -627,6 +690,7 @@ public static void runWorkerLoop() { @RuntimeCallable public static void detachWorkerRuntime(int runtimeId) { Runtime runtime = runtimeCache.remove(runtimeId); + indexRuntime(runtimeId, null); if (runtime != null) { GcListener.unsubscribe(runtime); @@ -651,6 +715,7 @@ private static Runtime initRuntime(DynamicConfiguration dynamicConfiguration) { // failed bootstrap doesn't leave a stale, half-initialized runtime // reachable through the caches runtimeCache.remove(runtime.getRuntimeId()); + indexRuntime(runtime.getRuntimeId(), null); currentRuntime.remove(); GcListener.unsubscribe(runtime); throw t; @@ -809,7 +874,6 @@ private Class resolveClass(String baseClassName, String fullClassName, String return javaClass; } - @RuntimeCallable private long getUsedMemory() { long usedMemory = dalvikRuntime.totalMemory() - dalvikRuntime.freeMemory(); return usedMemory; @@ -871,23 +935,23 @@ public static void initInstance(Object instance) { int objectId = runtime.currentObjectId; - if (objectId != -1) { + if (objectId != -1 && instance.getClass() == runtime.currentObjectClass) { + runtime.currentObjectId = -1; runtime.makeInstanceStrong(instance, objectId); } else { - runtime.createJSInstance(instance); + objectId = runtime.createJSInstance(instance); } - recordOwningRuntime(instance, runtime.getRuntimeId()); + recordOwningRuntime(instance, runtime.getRuntimeId(), objectId); } finally { frame.close(); } } - private void createJSInstance(Object instance) { + private int createJSInstance(Object instance) { + // Registered by createJSInstanceNative, which holds the isolate lock. int javaObjectID = generateNewObjectId(getRuntimeId()); - makeInstanceStrong(instance, javaObjectID); - Class clazz = instance.getClass(); if (!loadedJavaScriptExtends.containsKey(clazz)) { @@ -908,6 +972,8 @@ private void createJSInstance(Object instance) { if (logger.isEnabled()) { logger.write("JSInstance for " + instance.getClass().toString() + " created with overrides"); } + + return javaObjectID; } @RuntimeCallable @@ -1023,7 +1089,12 @@ private static String getTypeMetadata(Class clazz) { return ret; } - private Class lastStoredClass; + // One upcall for what native needs after constructing an object from JS. + @RuntimeCallable + private long registerConstructedInstance(Object instance, int objectId) { + makeInstanceStrong(instance, objectId); + return getUsedMemory(); + } @RuntimeCallable private void makeInstanceStrong(Object instance, int objectId) { @@ -1032,51 +1103,27 @@ private void makeInstanceStrong(Object instance, int objectId) { } int key = objectId; - strongInstances.put(key, instance); - strongJavaObjectToID.put(instance, key); + objectTable.putStrong(key, instance); - Class clazz = instance.getClass(); - // Instances of one class are usually registered back to back, and storeClass takes - // two locks. A racing read only costs a redundant store. - if (clazz != lastStoredClass) { - classStorageService.storeClass(clazz.getName(), clazz); - lastStoredClass = clazz; - } + // Stores the class by name on its first registration. + classIndexOf(instance.getClass()); if (logger != null && logger.isEnabled()) { logger.write("MakeInstanceStrong (" + key + ", " + instance.getClass().toString() + ")"); } } + /* + * One call per finalizer batch: the JS wrappers of these ids were collected. weaken holds + * first collections (held strongly until now), check later ones. Each entry whose Java + * object is already gone is overwritten with -1 so its wrapper can be released too. + */ @RuntimeCallable - private boolean makeInstanceWeakAndCheckIfAlive(int javaObjectID) { + private void processFinalizedBatch(int[] weaken, int weakenCount, int[] check, int checkCount) { + objectTable.weakenAndCheckAlive(weaken, weakenCount); + objectTable.weakenAndCheckAlive(check, checkCount); if (logger.isEnabled()) { - logger.write("makeInstanceWeakAndCheckIfAlive instance " + javaObjectID); - } - Object instance = strongInstances.get(javaObjectID); - if (instance == null) { - WeakReference ref = weakInstances.get(javaObjectID); - if (ref == null) { - return false; - } else { - instance = ref.get(); - if (instance == null) { - // The Java was moved from strong to weak, and then the Java instance was collected. - weakInstances.remove(javaObjectID); - weakJavaObjectToID.remove(ref); - return false; - } else { - return true; - } - } - } else { - strongInstances.remove(javaObjectID); - strongJavaObjectToID.remove(instance); - - weakJavaObjectToID.put(instance, javaObjectID); - weakInstances.put(javaObjectID, new WeakReference(instance)); - - return true; + logger.write("processFinalizedBatch weakened=" + weakenCount + " checked=" + checkCount); } } @@ -1086,51 +1133,62 @@ private Object getJavaObjectByID(int javaObjectID) throws Exception { logger.write("Platform.getJavaObjectByID:" + javaObjectID); } - Object instance = strongInstances.get(javaObjectID); - + Object instance = objectTable.get(javaObjectID); if (instance == null) { - instance = keyNotFoundObject; + throw new NativeScriptException(objectTable.contains(javaObjectID) + ? "Attempt to use cleared object reference id=" + javaObjectID + : "No weak reference found. Attempt to use cleared object reference id=" + javaObjectID); } - if (instance == keyNotFoundObject) { - WeakReference wr = weakInstances.get(javaObjectID); - if (wr == null) { - throw new NativeScriptException("No weak reference found. Attempt to use cleared object reference id=" + javaObjectID); - } - - instance = wr.get(); - if (instance == null) { - throw new NativeScriptException("Attempt to use cleared object reference id=" + javaObjectID); - } - } - - // Log.d(DEFAULT_LOG_TAG, - // "Platform.getJavaObjectByID found strong object with id:" + - // javaObjectID); return instance; } private Integer getJavaObjectID(Object obj) { - Integer id = strongJavaObjectToID.get(obj); - if (id == null) { - id = weakJavaObjectToID.get(obj); - } - - return id; + int id = objectTable.findId(obj); + return id == ObjectTable.NO_ID ? null : id; } @RuntimeCallable - private int getOrCreateJavaObjectID(Object obj) { - Integer result = getJavaObjectID(obj); + private int findJavaObjectID(Object obj) { + return objectTable.findId(obj); + } - if (result == null) { - int objectId = generateNewObjectId(getRuntimeId()); - makeInstanceStrong(obj, objectId); + // The class index is only needed to create a wrapper, and an object that already has + // an id almost always has one; native asks for the class itself when it does not. + @RuntimeCallable + private long getOrCreateJavaObjectIDAndClass(Object obj) { + long found = getOrCreateObjectId(obj); + if ((found & ObjectTable.FRESH) == 0) { + return found; + } + return packIdAndClass((int) found, obj); + } - result = objectId; + @RuntimeCallable + private int getOrCreateJavaObjectID(Object obj) { + long found = getOrCreateObjectId(obj); + if ((found & ObjectTable.FRESH) != 0) { + // Stores the class by name on its first registration. + classIndexOf(obj.getClass()); } + return (int) found; + } - return result; + // obj's id (low 32 bits), registering it strongly under a new id if it has none, with + // ObjectTable.FRESH set then. Threads registering call arguments (packageArgs) do not + // hold the isolate lock, so where they may, the lookup and insert are one atomic step. + private long getOrCreateObjectId(Object obj) { + // Exact: only the isolate-lock holder (this thread) removes entries. + int id = objectTable.findId(obj); + if (id != ObjectTable.NO_ID) { + return id & 0xFFFFFFFFL; + } + if (objectTable.allowsConcurrentRegistration()) { + return objectTable.findOrRegister(obj, objectIds); + } + id = generateNewObjectId(getRuntimeId()); + makeInstanceStrong(obj, id); + return (id & 0xFFFFFFFFL) | ObjectTable.FRESH; } public static Object callJSMethodFromPossibleNonMainThread(Object javaObject, String methodName, Class retType, Object... args) throws NativeScriptException { @@ -1188,11 +1246,20 @@ public static Object callJSMethodWithDelay(Object javaObject, String methodName, } public static Object callJSMethod(Object javaObject, String methodName, Class retType, boolean isConstructor, long delay, Object... args) throws NativeScriptException { - int owningRuntimeId = getOwningRuntimeId(javaObject); + int owningRuntimeId = NO_OWNING_RUNTIME; + int objectIdHint = -1; + if (javaObject instanceof NativeScriptRuntimeBound) { + NativeScriptRuntimeBound bound = (NativeScriptRuntimeBound) javaObject; + int tag = bound.getRuntimeTag__ns(); + if (tag != 0) { + owningRuntimeId = tag - 1; + objectIdHint = bound.getObjectId__ns(); + } + } Runtime runtime; if (owningRuntimeId != NO_OWNING_RUNTIME) { - runtime = runtimeCache.get(owningRuntimeId); + runtime = runtimeForId(owningRuntimeId); if (runtime == null) { return discardCall(methodName, retType, describeInstance(javaObject) @@ -1210,7 +1277,7 @@ public static Object callJSMethod(Object javaObject, String methodName, Class } } - return runtime.dispatchCallJSMethodNative(javaObject, methodName, isConstructor, delay, retType, args); + return runtime.dispatchCallJSMethodNative(javaObject, objectIdHint, methodName, isConstructor, delay, retType, args); } /* @@ -1223,26 +1290,26 @@ private static String describeInstance(Object javaObject) { } /* - * Arguments cross to native as three parallel arrays so the native side - * reads every primitive with one bulk region copy instead of unboxing each - * through JNI. argValues holds primitives as raw bits (float/double via - * floatToRawIntBits/doubleToRawLongBits) and object IDs; argRefs holds - * strings and, for objects, the class name a missing JS wrapper needs. - * Type IDs are those of TypeIDs.GetObjectTypeId(Object). + * Arguments cross to native in one long[] so the native side reads them with + * a single region copy instead of unboxing each through JNI: count values + * (primitives as raw bits, float/double via floatToRawIntBits/ + * doubleToRawLongBits; objects as a packed id and class index), then the + * TypeIDs.GetObjectTypeId(Object) of each, one per byte. refs holds strings + * and objects. Must match JavaCallArgs in ArgConverter.h. */ private static final class PackagedArgs { - final int[] types; + final int count; final long[] values; final Object[] refs; - PackagedArgs(int[] types, long[] values, Object[] refs) { - this.types = types; + PackagedArgs(int count, long[] values, Object[] refs) { + this.count = count; this.values = values; this.refs = refs; } } - private static final PackagedArgs NO_ARGS = new PackagedArgs(new int[0], new long[0], null); + private static final PackagedArgs NO_ARGS = new PackagedArgs(0, null, null); // If a javaObject has no javaObjectID, meaning no JavaScript object exists // for it yet, it is assigned one. @@ -1252,8 +1319,7 @@ private PackagedArgs packageArgs(Object... args) { return NO_ARGS; } - int[] types = new int[len]; - long[] values = new long[len]; + long[] values = new long[len + (len + 7) / 8]; Object[] refs = null; for (int i = 0; i < len; i++) { @@ -1297,14 +1363,38 @@ private PackagedArgs packageArgs(Object... args) { if (refs == null) { refs = new Object[len]; } - refs[i] = value.getClass().getName(); - values[i] = getOrCreateJavaObjectID(value); + refs[i] = value; + int id = ObjectTable.NO_ID; + if (value instanceof NativeScriptRuntimeBound) { + // A binding of this runtime carries its id (written before the tag, never + // reused); native resolves it like an identity-index hint. + NativeScriptRuntimeBound bound = (NativeScriptRuntimeBound) value; + if (bound.getRuntimeTag__ns() == getRuntimeId() + 1) { + id = bound.getObjectId__ns(); + } + } + if (id == ObjectTable.NO_ID) { + id = objectTable.findIdOptimistic(value); + } + long fresh = 0; + if (id == ObjectTable.NO_ID && objectTable.allowsConcurrentRegistration()) { + // Registered here rather than under the isolate lock, so threads calling + // in with new objects only serialize on creating their wrappers. + long found = objectTable.findOrRegister(value, objectIds); + id = (int) found; + if ((found & ObjectTable.FRESH) != 0) { + fresh = FRESH_ARG; + } + } + // Otherwise the id is a hint, resolved or registered natively under the + // isolate lock. + values[i] = packIdAndClass(id, value) | fresh; } - types[i] = typeId; + values[len + (i >> 3)] |= (long) typeId << ((i & 7) * 8); } - return new PackagedArgs(types, values, refs); + return new PackagedArgs(len, values, refs); } static Class getClassForName(String className) { @@ -1396,22 +1486,24 @@ private int resolveJavaObjectID(Object javaObject, String methodName) throws Nat return javaObjectID; } - private Object dispatchCallJSMethodNative(final Object javaObject, final String methodName, boolean isConstructor, long delay, Class retType, final Object[] args) throws NativeScriptException { + private Object dispatchCallJSMethodNative(final Object javaObject, int objectIdHint, final String methodName, boolean isConstructor, long delay, Class retType, final Object[] args) throws NativeScriptException { final int returnType = TypeIDs.GetObjectTypeId(retType); Object ret = null; boolean isWorkThread = threadScheduler.getThread().equals(Thread.currentThread()); final Object[] tmpArgs = extendConstructorArgs(methodName, isConstructor, args); - final boolean discardUncaughtJsExceptions = this.config.appConfig.getDiscardUncaughtJsExceptions(); - boolean enableMultithreadedJavascript = this.config.appConfig.getEnableMultithreadedJavascript(); + final boolean discardUncaughtJsExceptions = this.discardUncaughtJsExceptions; + boolean enableMultithreadedJavascript = this.multithreadedJavascript; if (enableMultithreadedJavascript || isWorkThread) { - int javaObjectID = resolveJavaObjectID(javaObject, methodName); PackagedArgs packagedArgs = packageArgs(tmpArgs); long[] primitiveResult = newPrimitiveResult(returnType); try { - ret = callJSMethodNative(getRuntimeId(), javaObjectID, methodName, returnType, isConstructor, packagedArgs.types, packagedArgs.values, packagedArgs.refs, primitiveResult); + // A hint: other threads may be writing the table, so native re-checks it under the + // isolate lock and resolves the id itself on a miss. + ret = callJSMethodNative(getRuntimeId(), javaObject, + objectIdHint != ObjectTable.NO_ID ? objectIdHint : objectTable.findIdOptimistic(javaObject), methodName, MethodNameIds.idOf(methodName), returnType, isConstructor, packagedArgs.count, packagedArgs.values, packagedArgs.refs, primitiveResult); ret = takePrimitiveResult(ret, returnType, primitiveResult); } catch (NativeScriptException e) { // With multithreaded JS the owner can be torn down between the @@ -1452,7 +1544,7 @@ public void run() { } final PackagedArgs packagedArgs = packageArgs(tmpArgs); final long[] primitiveResult = newPrimitiveResult(returnType); - arr[0] = takePrimitiveResult(callJSMethodNative(getRuntimeId(), javaObjectID, methodName, returnType, isCtor, packagedArgs.types, packagedArgs.values, packagedArgs.refs, primitiveResult), returnType, primitiveResult); + arr[0] = takePrimitiveResult(callJSMethodNative(getRuntimeId(), javaObject, javaObjectID, methodName, MethodNameIds.idOf(methodName), returnType, isCtor, packagedArgs.count, packagedArgs.values, packagedArgs.refs, primitiveResult), returnType, primitiveResult); } catch (NativeScriptException e) { if (discardUncaughtJsExceptions && !e.isEscapedFromJs()) { String errorMessage = "Error on \"" + Thread.currentThread().getName() + "\" thread for callJSMethodNative\n"; diff --git a/test-app/runtime/src/test/java/com/tns/MethodNameIdsTest.java b/test-app/runtime/src/test/java/com/tns/MethodNameIdsTest.java new file mode 100644 index 000000000..ef832fa23 --- /dev/null +++ b/test-app/runtime/src/test/java/com/tns/MethodNameIdsTest.java @@ -0,0 +1,77 @@ +package com.tns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; + +public class MethodNameIdsTest { + @Test + public void givesEqualNamesOneIdAndDistinctNamesDistinctIds() { + int a = MethodNameIds.idOf("testName_a"); + int b = MethodNameIds.idOf("testName_b"); + + assertNotEquals(a, b); + assertEquals(a, MethodNameIds.idOf("testName_a")); + assertEquals(a, MethodNameIds.idOf(new String("testName_a"))); + assertEquals(-1, MethodNameIds.idOf(null)); + } + + @Test + public void keepsIdsAcrossGrowth() { + List ids = new ArrayList<>(); + for (int i = 0; i < 2000; i++) { + ids.add(MethodNameIds.idOf("testGrowth_" + i)); + } + Set distinct = new HashSet<>(ids); + assertEquals(2000, distinct.size()); + for (int i = 0; i < 2000; i++) { + assertEquals((int) ids.get(i), MethodNameIds.idOf("testGrowth_" + i)); + } + } + + @Test + public void agreesAcrossThreadsRegisteringTheSameNames() throws Exception { + final int threads = 8; + final int names = 500; + final ConcurrentHashMap seen = new ConcurrentHashMap<>(); + final List failures = new ArrayList<>(); + final CountDownLatch start = new CountDownLatch(1); + List workers = new ArrayList<>(); + for (int t = 0; t < threads; t++) { + Thread worker = new Thread(() -> { + try { + start.await(); + for (int i = 0; i < names; i++) { + String name = "testRace_" + i; + int id = MethodNameIds.idOf(name); + Integer previous = seen.putIfAbsent(name, id); + if (previous != null && previous != id) { + throw new AssertionError(name + " got " + id + " and " + previous); + } + } + } catch (Throwable e) { + synchronized (failures) { + failures.add(e); + } + } + }); + workers.add(worker); + worker.start(); + } + start.countDown(); + for (Thread worker : workers) { + worker.join(); + } + assertTrue(failures.toString(), failures.isEmpty()); + assertEquals(names, new HashSet<>(seen.values()).size()); + } +} diff --git a/test-app/runtime/src/test/java/com/tns/ObjectTableTest.java b/test-app/runtime/src/test/java/com/tns/ObjectTableTest.java new file mode 100644 index 000000000..f2d88ae41 --- /dev/null +++ b/test-app/runtime/src/test/java/com/tns/ObjectTableTest.java @@ -0,0 +1,387 @@ +package com.tns; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +public class ObjectTableTest { + @Test + public void registersLooksUpAndRemoves() { + ObjectTable table = new ObjectTable(); + Object a = new Object(); + Object b = new Object(); + table.putStrong(7, a); + table.putStrong(9, b); + + assertEquals(7, table.findId(a)); + assertEquals(9, table.findId(b)); + assertSame(a, table.get(7)); + assertEquals(ObjectTable.NO_ID, table.findId(new Object())); + + table.remove(7); + assertEquals(ObjectTable.NO_ID, table.findId(a)); + assertNull(table.get(7)); + assertEquals(9, table.findId(b)); + assertEquals(1, table.size()); + } + + @Test + public void weakEntriesStayFindableWhileTheObjectLives() { + ObjectTable table = new ObjectTable(); + Object a = new Object(); + table.putStrong(1, a); + + assertTrue(table.weakenAndCheckAlive(1)); + assertSame(a, table.get(1)); + assertEquals(1, table.findId(a)); + assertTrue(table.weakenAndCheckAlive(1)); + assertFalse(table.weakenAndCheckAlive(2)); + } + + @Test + public void registeringAnIdAgainReplacesItsEntry() { + ObjectTable table = new ObjectTable(); + Object a = new Object(); + Object b = new Object(); + table.putStrong(5, a); + table.weakenAndCheckAlive(5); + table.putStrong(5, a); + assertEquals(1, table.size()); + assertSame(a, table.get(5)); + + table.putStrong(5, b); + assertEquals(1, table.size()); + assertSame(b, table.get(5)); + assertEquals(5, table.findId(b)); + assertEquals(ObjectTable.NO_ID, table.findId(a)); + } + + @Test + public void aRegisteredWeakReferenceIsNotMistakenForAWeakEntry() { + ObjectTable table = new ObjectTable(); + java.lang.ref.WeakReference ref = new java.lang.ref.WeakReference<>(new Object()); + table.putStrong(3, ref); + + assertSame(ref, table.get(3)); + assertEquals(3, table.findId(ref)); + } + + @Test + public void optimisticReadsNeverReturnAWrongIdWhileAnotherThreadWrites() throws Exception { + final ObjectTable table = new ObjectTable(true); + final int stableCount = 64; + final Object[] stable = new Object[stableCount]; + for (int i = 0; i < stableCount; i++) { + stable[i] = new Object(); + table.putStrong(i, stable[i]); + } + + final java.util.concurrent.atomic.AtomicBoolean done = new java.util.concurrent.atomic.AtomicBoolean(); + final java.util.concurrent.atomic.AtomicReference failure = new java.util.concurrent.atomic.AtomicReference<>(); + final java.util.concurrent.atomic.AtomicLong hits = new java.util.concurrent.atomic.AtomicLong(); + + Thread writer = new Thread(new Runnable() { + @Override + public void run() { + Random random = new Random(7); + List churn = new ArrayList<>(); + int nextId = stableCount; + while (!done.get()) { + // Grow well past the initial capacity so readers overlap resizes and + // backward-shift deletions, then shrink again. + if (churn.size() < 20_000 && random.nextInt(3) != 0) { + int id = nextId++; + table.putStrong(id, new Object()); + churn.add(id); + } else if (!churn.isEmpty()) { + int id = churn.remove(churn.size() - 1); + if (random.nextBoolean()) { + table.weakenAndCheckAlive(id); + } + table.remove(id); + } + } + } + }); + + Thread[] readers = new Thread[3]; + for (int r = 0; r < readers.length; r++) { + final int seed = r; + readers[r] = new Thread(new Runnable() { + @Override + public void run() { + Random random = new Random(seed); + while (!done.get()) { + int expected = random.nextInt(stableCount); + int id = table.findIdOptimistic(stable[expected]); + if (id == expected) { + hits.incrementAndGet(); + } else if (id != ObjectTable.NO_ID) { + failure.compareAndSet(null, "expected " + expected + " or NO_ID, got " + id); + } + } + } + }); + } + + writer.start(); + for (Thread reader : readers) { + reader.start(); + } + Thread.sleep(2000); + done.set(true); + writer.join(); + for (Thread reader : readers) { + reader.join(); + } + + assertNull(failure.get(), failure.get()); + assertTrue("optimistic reads should mostly hit", hits.get() > 1000); + } + + @Test + public void concurrentRegistrationsAgreeOnOneIdPerObject() throws Exception { + final ObjectTable table = new ObjectTable(true); + final java.util.concurrent.atomic.AtomicInteger nextId = new java.util.concurrent.atomic.AtomicInteger(); + final ObjectTable.IdSource ids = new ObjectTable.IdSource() { + @Override + public int newId() { + return nextId.getAndIncrement(); + } + }; + final int objectCount = 5000; + final Object[] objects = new Object[objectCount]; + for (int i = 0; i < objectCount; i++) { + objects[i] = new Object(); + } + final int threadCount = 6; + final int[][] seen = new int[threadCount][objectCount]; + final int[] freshCounts = new int[threadCount]; + final java.util.concurrent.CountDownLatch go = new java.util.concurrent.CountDownLatch(1); + Thread[] threads = new Thread[threadCount]; + for (int t = 0; t < threadCount; t++) { + final int index = t; + threads[t] = new Thread(new Runnable() { + @Override + public void run() { + Random random = new Random(index); + try { + go.await(); + } catch (InterruptedException e) { + return; + } + for (int n = 0; n < objectCount; n++) { + int i = (n * 7 + index * 131 + random.nextInt(3)) % objectCount; + long found = table.findOrRegister(objects[i], ids); + if ((found & ObjectTable.FRESH) != 0) { + freshCounts[index]++; + } + seen[index][i] = (int) found + 1; + } + } + }); + threads[t].start(); + } + go.countDown(); + for (Thread thread : threads) { + thread.join(); + } + + int fresh = 0; + for (int count : freshCounts) { + fresh += count; + } + assertEquals(table.size(), fresh); + for (int i = 0; i < objectCount; i++) { + int id = table.findId(objects[i]); + for (int t = 0; t < threadCount; t++) { + if (seen[t][i] != 0) { + assertEquals(id, seen[t][i] - 1); + } + } + } + } + + @Test + public void exactReadsStayExactWhileAnotherThreadRegisters() throws Exception { + final ObjectTable table = new ObjectTable(true); + final java.util.concurrent.atomic.AtomicInteger nextId = new java.util.concurrent.atomic.AtomicInteger(1000); + final ObjectTable.IdSource ids = new ObjectTable.IdSource() { + @Override + public int newId() { + return nextId.getAndIncrement(); + } + }; + final int stableCount = 64; + final Object[] stable = new Object[stableCount]; + for (int i = 0; i < stableCount; i++) { + stable[i] = new Object(); + table.putStrong(i, stable[i]); + } + final java.util.concurrent.atomic.AtomicBoolean done = new java.util.concurrent.atomic.AtomicBoolean(); + final java.util.concurrent.atomic.AtomicReference failure = new java.util.concurrent.atomic.AtomicReference<>(); + + Thread writer = new Thread(new Runnable() { + @Override + public void run() { + List churn = new ArrayList<>(); + Random random = new Random(3); + while (!done.get()) { + // Grow well past the initial capacity, then shrink, so reads overlap resizes. + if (churn.size() < 20_000 && random.nextInt(3) != 0) { + churn.add((int) table.findOrRegister(new Object(), ids)); + } else if (!churn.isEmpty()) { + table.remove(churn.remove(churn.size() - 1)); + } + } + } + }); + Thread reader = new Thread(new Runnable() { + @Override + public void run() { + Random random = new Random(5); + while (!done.get()) { + int i = random.nextInt(stableCount); + int id = table.findId(stable[i]); + if (id != i) { + failure.compareAndSet(null, "findId: expected " + i + ", got " + id); + } + if (table.get(i) != stable[i]) { + failure.compareAndSet(null, "get(" + i + ") returned another object"); + } + if (!table.contains(i)) { + failure.compareAndSet(null, "contains(" + i + ") was false"); + } + } + } + }); + writer.start(); + reader.start(); + Thread.sleep(2000); + done.set(true); + writer.join(); + reader.join(); + + assertNull(failure.get(), failure.get()); + } + + @Test + public void keepsSurvivorsOfReleasedRangesAndOldIdsRegisteredLate() { + ObjectTable table = new ObjectTable(); + Object[] objects = new Object[3000]; + for (int id = 0; id < objects.length; id++) { + objects[id] = new Object(); + table.putStrong(id, objects[id]); + } + for (int id = 0; id < 2048; id++) { + if (id != 5 && id != 1500) { + table.remove(id); + } + } + + assertTrue(table.weakenAndCheckAlive(5)); + assertSame(objects[5], table.get(5)); + assertEquals(5, table.findId(objects[5])); + assertEquals(1500, table.findId(objects[1500])); + + Object late = new Object(); + table.putStrong(700, late); + assertSame(late, table.get(700)); + assertEquals(700, table.findId(late)); + + table.remove(5); + assertNull(table.get(5)); + assertFalse(table.contains(5)); + assertEquals(ObjectTable.NO_ID, table.findId(objects[5])); + assertEquals(954, table.size()); + } + + @Test + public void idsPastIntMaxAreUnsigned() { + ObjectTable table = new ObjectTable(true); + Object a = new Object(); + Object b = new Object(); + table.putStrong(Integer.MIN_VALUE + 3, a); + + assertSame(a, table.get(Integer.MIN_VALUE + 3)); + assertEquals(Integer.MIN_VALUE + 3, table.findId(a)); + assertTrue(table.weakenAndCheckAlive(Integer.MIN_VALUE + 3)); + + long found = table.findOrRegister(b, new ObjectTable.IdSource() { + @Override + public int newId() { + return -7; + } + }); + assertTrue((found & ObjectTable.FRESH) != 0); + assertEquals(-7, (int) found); + assertEquals(-7, table.findId(b)); + assertEquals(-7L & 0xFFFFFFFFL, table.findOrRegister(b, null)); + + table.remove(-7); + assertNull(table.get(-7)); + assertEquals(ObjectTable.NO_ID, table.findId(b)); + assertEquals(1, table.size()); + } + + @Test + public void optimisticReadsMissWhenDisabled() { + ObjectTable table = new ObjectTable(false); + Object a = new Object(); + table.putStrong(1, a); + assertEquals(ObjectTable.NO_ID, table.findIdOptimistic(a)); + } + + @Test + public void matchesReferenceMapsUnderRandomOperations() { + Random random = new Random(42); + ObjectTable table = new ObjectTable(); + Map byId = new HashMap<>(); + Map byObject = new IdentityHashMap<>(); + List live = new ArrayList<>(); + int nextId = 0; + + for (int step = 0; step < 200_000; step++) { + int op = random.nextInt(10); + if (op < 5 || live.isEmpty()) { + Object obj = new Object(); + int id = nextId++; + table.putStrong(id, obj); + byId.put(id, obj); + byObject.put(obj, id); + live.add(id); + } else if (op < 8) { + int id = live.get(random.nextInt(live.size())); + Object obj = byId.get(id); + assertEquals(id, table.findId(obj)); + assertSame(obj, table.get(id)); + } else { + int index = random.nextInt(live.size()); + int id = live.get(index); + live.set(index, live.get(live.size() - 1)); + live.remove(live.size() - 1); + Object obj = byId.remove(id); + byObject.remove(obj); + table.remove(id); + assertEquals(ObjectTable.NO_ID, table.findId(obj)); + assertNull(table.get(id)); + } + } + + assertEquals(byId.size(), table.size()); + for (Map.Entry entry : byObject.entrySet()) { + assertEquals((int) entry.getValue(), table.findId(entry.getKey())); + } + } +} diff --git a/test-app/runtime/src/test/java/com/tns/RuntimeTest.java b/test-app/runtime/src/test/java/com/tns/RuntimeTest.java index 265062ced..3249cecb1 100644 --- a/test-app/runtime/src/test/java/com/tns/RuntimeTest.java +++ b/test-app/runtime/src/test/java/com/tns/RuntimeTest.java @@ -1,22 +1,11 @@ package com.tns; -import org.junit.Assert; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.mockito.Mockito.mock; + import org.junit.Before; import org.junit.Test; -import org.mockito.Mock; - -import org.mockito.MockitoAnnotations; -import org.mockito.Spy; - -import java.lang.ref.WeakReference; -import java.util.HashMap; -import java.util.Map; - -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.mockingDetails; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; public class RuntimeTest { @@ -24,67 +13,39 @@ public class RuntimeTest { private static final Object TESTING_NATIVE_OBJECT = new Object(); private Runtime runtime; - - @Mock - private HashMap strongInstances; - - @Mock - private HashMap> weakInstances; - - @Mock - private NativeScriptHashMap strongJavaObjectToId; - - @Mock - private NativeScriptWeakHashMap weakJavaObjectToId; - - @Mock - private WeakReference testingObjectWeakRefMock; + private ObjectTable objectTable; @Before public void setUp() { - MockitoAnnotations.initMocks(this); + objectTable = new ObjectTable(); runtime = new Runtime(mock(ClassResolver.class), mock(GcListener.class), mock(StaticConfiguration.class), mock(DynamicConfiguration.class), 0, 0, - strongInstances, - weakInstances, - strongJavaObjectToId, - weakJavaObjectToId); + objectTable); } @Test public void testStrongInstanceIsSuccessfullyRemoved() { - when(strongInstances.get(TESTING_NATIVE_OBJECT_ID)).thenReturn(TESTING_NATIVE_OBJECT); - - runtime.releaseNativeCounterpart(TESTING_NATIVE_OBJECT_ID); - - verify(strongInstances).remove(TESTING_NATIVE_OBJECT_ID); - verify(strongJavaObjectToId).remove(TESTING_NATIVE_OBJECT); - } - - @Test - public void testWeakInstanceIsSuccessfullyRemoved(){ - when(weakInstances.get(TESTING_NATIVE_OBJECT_ID)).thenReturn(testingObjectWeakRefMock); + objectTable.putStrong(TESTING_NATIVE_OBJECT_ID, TESTING_NATIVE_OBJECT); runtime.releaseNativeCounterpart(TESTING_NATIVE_OBJECT_ID); - verify(weakInstances).remove(TESTING_NATIVE_OBJECT_ID); - verify(weakJavaObjectToId).remove(testingObjectWeakRefMock); + assertNull(objectTable.get(TESTING_NATIVE_OBJECT_ID)); + assertEquals(ObjectTable.NO_ID, objectTable.findId(TESTING_NATIVE_OBJECT)); } @Test - public void testStrongAndWeakInstancesAreSuccessfullyRemoved(){ - when(strongInstances.get(TESTING_NATIVE_OBJECT_ID)).thenReturn(TESTING_NATIVE_OBJECT); - when(weakInstances.get(TESTING_NATIVE_OBJECT_ID)).thenReturn(testingObjectWeakRefMock); + public void testWeakInstanceIsSuccessfullyRemoved() { + objectTable.putStrong(TESTING_NATIVE_OBJECT_ID, TESTING_NATIVE_OBJECT); + objectTable.weakenAndCheckAlive(TESTING_NATIVE_OBJECT_ID); runtime.releaseNativeCounterpart(TESTING_NATIVE_OBJECT_ID); - verify(strongInstances).remove(TESTING_NATIVE_OBJECT_ID); - verify(strongJavaObjectToId).remove(TESTING_NATIVE_OBJECT); - verify(weakInstances).remove(TESTING_NATIVE_OBJECT_ID); - verify(weakJavaObjectToId).remove(testingObjectWeakRefMock); + assertNull(objectTable.get(TESTING_NATIVE_OBJECT_ID)); + assertEquals(ObjectTable.NO_ID, objectTable.findId(TESTING_NATIVE_OBJECT)); + assertEquals(0, objectTable.size()); } }