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..ac5caf436 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) { @@ -27,7 +33,7 @@ function report(name, nsPerOp) { // body(n) runs the operation n times; returning a value keeps V8 from dropping the loop. function measureJs(name, iterations, body) { - if (FILTER && name.indexOf(FILTER) < 0) return null; + if (skipped(name)) return null; iterations *= SCALE; var sink = body(Math.max(1000, iterations >> 2)); var samples = []; @@ -42,7 +48,7 @@ function measureJs(name, iterations, body) { } function measureCallback(name, cb, kind, iterations) { - if (FILTER && name.indexOf(FILTER) < 0) return null; + if (skipped(name)) return null; iterations *= SCALE; JniBench.runCallbacks(cb, kind, Math.max(1000, iterations >> 2)); var samples = []; @@ -58,7 +64,7 @@ function measureCallback(name, cb, kind, iterations) { // V8 GC (wrapper finalization and the Java-side strong -> weak bookkeeping), and the // Java GC that follows. Each phase is reported per object. function measureLifecycle(name, count, create) { - if (FILTER && name.indexOf(FILTER) < 0) return null; + if (skipped(name)) return null; count *= SCALE; var javaRuntime = java.lang.Runtime.getRuntime(); var alloc = [], release = [], javaGc = []; @@ -88,6 +94,56 @@ function measureLifecycle(name, count, create) { } + +// Aggregate ns per call while `threads` Java threads call into JS at once. Runs +// asynchronously: the JS thread must go idle and release the isolate for the +// callers to get in, so it polls for the result. +function measureContention(name, sink, threads, calls, mode, done) { + if (skipped(name)) { + done(null); + return; + } + var Bench = com.tns.tests.ContentionBench; + calls *= SCALE; + // Variants alternate round by round inside one launch, so a change in the device's + // performance state between launches cannot favour one of them. + var variants = VARIANTS.length > 0 ? VARIANTS : [null]; + var samples = variants.map(function () { return []; }); + var round = -variants.length; + var total = ROUNDS * variants.length; + function variantOf(r) { + return ((r % variants.length) + variants.length) % variants.length; + } + function startRound() { + var spin = variants[variantOf(round)]; + if (spin !== null) { + com.tns.Runtime.setAdmissionSpinMicros(spin); + } + Bench.start(sink, threads, calls, mode); + setTimeout(poll, 5); + } + function poll() { + var nanos = Number(Bench.result()); + if (nanos < 0) { + setTimeout(poll, 5); + return; + } + if (round >= 0) { + samples[variantOf(round)].push(nanos / (threads * calls)); + } + round++; + if (round < total) { + gc(); + setTimeout(startRound, 20); + } else { + done(variants.map(function (spin, i) { + return report(spin === null ? name : name + " [spin=" + spin + "]", samples[i]); + })); + } + } + startRound(); +} + function buildBenchmarks() { var bench = new JniBench(); var other = bench.getOther(); @@ -148,6 +204,22 @@ function buildBenchmarks() { return n; }); }, + function () { + if (skipped("instance.getAlpha() [android.view.View]")) return null; + var view = new android.view.View(com.tns.NativeScriptApplication.getInstance()); + return measureJs("instance.getAlpha() [android.view.View]", 50000, function (n) { + var s = 0; + for (var i = 0; i < n; i++) s = view.getAlpha(); + return s; + }); + }, + function () { + return measureJs("package member read android.view.View", 200000, function (n) { + var s = null; + for (var i = 0; i < n; i++) s = android.view.View; + return s; + }); + }, function () { return measureJs("instance.getInt()", 50000, function (n) { var s = 0; @@ -231,6 +303,13 @@ function buildBenchmarks() { return s; }); }, + function () { + return measureJs("static.newObjectOfManyClasses() [16 classes]", 20000, function (n) { + var s; + for (var i = 0; i < n; i++) s = JniBench.newObjectOfManyClasses(); + return s; + }); + }, function () { return measureJs("static.newObjectInJava() [new wrapper]", 20000, function (n) { var s; @@ -252,6 +331,37 @@ function buildBenchmarks() { return s; }); }, + function () { + return measureJs("static field get int [small class]", 50000, function (n) { + var Fields = com.tns.tests.StaticFieldBench; + var s = 0; + for (var i = 0; i < n; i++) s = Fields.counter; + return s; + }); + }, + function () { + return measureJs("static field get final int [small class]", 50000, function (n) { + var Fields = com.tns.tests.StaticFieldBench; + var s = 0; + for (var i = 0; i < n; i++) s = Fields.CONSTANT; + return s; + }); + }, + function () { + return measureJs("static field set int [small class]", 50000, function (n) { + var Fields = com.tns.tests.StaticFieldBench; + for (var i = 0; i < n; i++) Fields.counter = i; + return n; + }); + }, + function () { + return measureJs("static field get final int [android.R.id]", 50000, function (n) { + var ids = android.R.id; + var s = 0; + for (var i = 0; i < n; i++) s = ids.content; + return s; + }); + }, function () { return measureJs("field.get int", 50000, function (n) { var s = 0; @@ -295,7 +405,7 @@ function buildBenchmarks() { }); }, function () { - if (FILTER && "instance.self() [50k live wrappers]".indexOf(FILTER) < 0) return null; + if (skipped("instance.self() [50k live wrappers]")) return null; var live = new Array(50000); for (var i = 0; i < live.length; i++) live[i] = new java.lang.Object(); // Pinned from a global: an optimized frame may drop a local after its last use. @@ -322,18 +432,84 @@ function buildBenchmarks() { }, function () { return measureCallback("callback.oneObject(Object)", callback, 4, 20000); + }, + function () { + return measureCallback("callback.oneObject(new Object)", callback, 5, 20000); + }, + function () { + return measureCallback("callback.oneObject(new, 16 classes)", callback, 6, 20000); } ]; } +function buildContentionBenchmarks() { + var Bench = com.tns.tests.ContentionBench; + var sink = new Bench.Sink({ + call0: function () { }, + call3: function (a, b, c) { } + }); + var list = []; + [1, 2, 4].forEach(function (threads) { + [["no args", Bench.NO_ARGS], ["3 existing objects", Bench.EXISTING_OBJECTS], ["3 new objects", Bench.NEW_OBJECTS], ["3 new objects of 16 classes", Bench.NEW_OBJECTS_MANY_CLASSES]].forEach(function (mode) { + list.push(function (done) { + measureContention("contention " + threads + " thread(s), " + mode[0], sink, threads, 20000, mode[1], done); + }); + }); + }); + return list; +} + +// The first touch of a class builds its JS constructor and installs every static member, once +// per runtime, so each row is a single sample and must run before anything else touches the class. +function measureFirstAccess() { + var rows = [ + ["first access android.R", function () { return android.R; }], + ["first access android.R.attr", function () { return android.R.attr; }], + ["first access android.R.id", function () { return android.R.id; }], + ["first access android.R.style", function () { return android.R.style; }], + ["first access android.R.drawable", function () { return android.R.drawable; }], + ["first access android.R.string", function () { return android.R.string; }], + ["first read android.R.id.content", function () { return android.R.id.content; }], + ["first read android.R.id.text1", function () { return android.R.id.text1; }], + ["first read android.R.attr.textColor", function () { return android.R.attr.textColor; }], + ["first read android.R.style.Theme", function () { return android.R.style.Theme; }], + ["first access android.view [package]", function () { return android.view; }], + ["first access android.view.View", function () { return android.view.View; }], + ["first access android.widget [package]", function () { return android.widget; }], + ["first access android.widget.TextView", function () { return android.widget.TextView; }], + ["first access android.content.Context", function () { return android.content.Context; }], + ["first access android.app.Activity", function () { return android.app.Activity; }] + ]; + var results = []; + rows.forEach(function (row) { + if (skipped(row[0])) return; + gc(); + var start = performance.now(); + globalThis.__jniBenchSink = row[1](); + results.push(report(row[0], [(performance.now() - start) * 1e6])); + }); + var name = "first calls of 20 android.view.View methods"; + if (!skipped(name)) { + var view = new android.view.View(com.tns.NativeScriptApplication.getInstance()); + gc(); + var start = performance.now(); + globalThis.__jniBenchSink = [view.getAlpha(), view.getRotation(), view.getX(), view.getY(), view.getId(), + view.getWidth(), view.getHeight(), view.getLeft(), view.getTop(), view.getRight(), + view.getBottom(), view.getPaddingLeft(), view.getPaddingTop(), view.getScaleX(), view.getScaleY(), + view.getTranslationX(), view.getTranslationY(), view.getVisibility(), view.isEnabled(), view.isFocused()]; + results.push(report(name, [(performance.now() - start) * 1e6])); + } + return results; +} + exports.run = function () { + var results = measureFirstAccess(); var benchmarks = buildBenchmarks(); - var results = []; var index = 0; // Yield to the looper between benchmarks so GC and queued work run outside timed rounds. function next() { if (index >= benchmarks.length) { - console.log("JNIBENCH_DONE " + JSON.stringify(results)); + runContention(); return; } gc(); @@ -341,5 +517,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..4c4d58a75 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,90 @@ describe("Tests garbage collection", function () { test.postCallback(); }); + describe("collected wrappers waiting for a flush", function () { + var Holder = com.tns.tests.WrapperHolder; + + function collectJava() { + for (var i = 0; i < 3; i++) { + java.lang.Runtime.getRuntime().gc(); + java.lang.System.runFinalization(); + } + } + + afterEach(function () { + Holder.held = null; + }); + + it("keep the Java object of a wrapper handed back to JS before the flush", function () { + (function () { + Holder.held = new java.lang.StringBuilder("reacquired"); + })(); + gc(); + var again = Holder.held; + Holder.held = null; + collectJava(); + + expect(again.toString()).toBe("reacquired"); + }); + + it("keep a wrapper handed back to JS while its liveness check is queued", function () { + (function () { + Holder.held = new java.lang.StringBuilder("checked"); + })(); + gc(); + collectJava(); + gc(); + var again = Holder.held; + collectJava(); + + expect(again.toString()).toBe("checked"); + again = null; + gc(); + collectJava(); + }); + + it("release an object Java registered for a call that failed before reaching it", function () { + // Not a function: the call fails before its argument is converted. + var target = new com.tns.tests.FreshArgs.Target({ take: 42 }); + var ref = com.tns.tests.FreshArgs.callWithNewObject(target); + collectJava(); + + expect(ref.get()).toBe(null); + }); + + it("give threads passing the same new object at once one wrapper", function (done) { + var FreshArgs = com.tns.tests.FreshArgs; + var received = []; + var target = new FreshArgs.Target({ + take: function (o) { + received.push(o); + } + }); + FreshArgs.startFromThreadsAtOnce(target, 4); + (function poll() { + if (FreshArgs.finishedCalls() < 4) { + setTimeout(poll, 10); + return; + } + expect(received.length).toBe(4); + for (var i = 1; i < received.length; i++) { + expect(received[i] === received[0]).toBe(true); + } + done(); + })(); + }); + + it("let Java reclaim objects during a synchronous loop that never yields", function () { + // Far more than the Java heap: every buffer must become collectable while the loop runs. + var total = 0; + for (var i = 0; i < 160; i++) { + var stream = new java.io.ByteArrayOutputStream(4 * 1024 * 1024); + total += stream.size() + 1; + if (i % 8 === 7) { + gc(); + } + } + expect(total).toBe(160); + }); + }); }); \ 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..a9d2df4cc 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,26 @@ 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); + }); +}); 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/JniBench.java b/test-app/app/src/main/java/com/tns/tests/JniBench.java index ab3313ebb..c14b1605f 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,12 @@ 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; 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/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/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..24bd2dc93 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 >= 0) { + 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..aa35e1378 100644 --- a/test-app/runtime/src/main/cpp/CallbackHandlers.cpp +++ b/test-app/runtime/src/main/cpp/CallbackHandlers.cpp @@ -56,9 +56,9 @@ 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); + 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 +88,7 @@ void CallbackHandlers::Init(Isolate *isolate) { namespace { struct JavaCallScope { explicit JavaCallScope(Runtime* runtime) : runtime_(runtime) { + runtime_->GetObjectManager()->FlushFinalizedBatchIfPending(); runtime_->EnterJavaCall(); } ~JavaCallScope() { @@ -125,7 +126,7 @@ 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 @@ -146,9 +147,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 +254,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 +340,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 +351,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 +361,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 +562,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 +586,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 +1026,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 +1075,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; } @@ -1958,7 +1949,7 @@ jclass CallbackHandlers::RUNTIME_CLASS = nullptr; jclass CallbackHandlers::JAVA_LANG_STRING = nullptr; jfieldID CallbackHandlers::CURRENT_OBJECTID_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..2daee1714 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,7 @@ namespace tns { static jfieldID CURRENT_OBJECTID_FIELD_ID; - static jmethodID MAKE_INSTANCE_STRONG_ID; + static jmethodID REGISTER_CONSTRUCTED_INSTANCE_ID; static jmethodID GET_TYPE_METADATA; 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..dadd3c55a 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); @@ -237,7 +265,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; } @@ -288,7 +316,7 @@ Local MetadataNode::CreateJSWrapper(Isolate* isolate, ObjectManager* obj if (!obj.IsEmpty()) { auto ctorFunc = GetConstructorFunction(isolate); auto context = isolate->GetCurrentContext(); - obj->Set(context, ArgConverter::ConvertToV8String(isolate, "constructor"), ctorFunc); + obj->Set(context, V8StringConstants::GetConstructor(isolate), ctorFunc); obj->SetPrototype(context, ctorFunc->Get(context, V8StringConstants::GetPrototype(isolate)).ToLocalChecked()); SetInstanceMetadata(isolate, obj, this); } @@ -339,10 +367,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 +379,14 @@ 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).ToChecked(); } } @@ -367,7 +397,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 +438,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 +508,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 +726,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 +739,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 +756,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 +819,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 +877,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 +1057,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 +1069,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 +1113,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 +1126,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 +1293,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)); @@ -1850,7 +1956,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); @@ -2402,7 +2508,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..8e22eadbb 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(); @@ -158,6 +158,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); 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..be7a57951 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,52 @@ 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(); 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 +199,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 +287,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/ObjectManager.cpp b/test-app/runtime/src/main/cpp/ObjectManager.cpp index b3411d7eb..18d474006 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" @@ -53,9 +54,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 +95,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( @@ -217,6 +231,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; @@ -242,12 +257,27 @@ void ObjectManager::SetJavaClass(const Local& instance, jclass 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); @@ -257,41 +287,134 @@ Local ObjectManager::GetJsObjectByJavaObject(int javaObjectID) { return handleScope.Escape(Local()); } - Persistent* jsObject = it->second->target; + ObjectWeakCallbackState* state = it->second; + if (state->queued) { + state->reacquired = true; + } - auto localObject = Local::New(isolate, *jsObject); + auto localObject = Local::New(isolate, *state->target); return handleScope.Escape(localObject); } +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); + } +} + +Local ObjectManager::GetLinkedJsObjectByJavaObject(int javaObjectID) { + auto it = m_idToObject.find(javaObjectID); + if (it == m_idToObject.end() || it->second->jsInfo == nullptr) { + return Local(); + } + return GetJsObjectByJavaObject(javaObjectID); +} + Local ObjectManager::CreateJSWrapper(jint javaObjectID, const string& typeName) { 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; + const auto& className = WrapperClassFor(classIndex, instance, clazz, scratch); + return CreateJSWrapperHelper(javaObjectID, className, clazz); +} + +const std::string& ObjectManager::WrapperClassFor(int classIndex, + jobject instance, + jclass& clazz, + std::string& scratch) { + if (classIndex > 0 && + static_cast(classIndex) < m_wrapperClasses.size() && + m_wrapperClasses[classIndex] != nullptr) { + clazz = m_wrapperClasses[classIndex]->clazz; + return m_wrapperClasses[classIndex]->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, + const string& className, jclass clazz) { auto isolate = m_isolate; - auto className = (clazz != nullptr) ? GetClassName(clazz) : typeName; - auto 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; } @@ -396,25 +519,186 @@ void ObjectManager::JSObjectFinalizer(Isolate* isolate, 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(jsInstanceInfo->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. + eventLoop->PostInternal(std::move(flush)); + } + return true; +} + +void ObjectManager::FlushFinalizedBatch() { + m_flushScheduled = false; + m_flushBackingOff = false; + if (m_pendingWeaken.empty() && m_pendingCheck.empty()) { + 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) { + auto it = m_idToObject.find(id); + if (it == m_idToObject.end()) { + continue; + } + ObjectWeakCallbackState* state = it->second; + 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++) { + auto it = m_idToObject.find(ids[i]); + if (it == m_idToObject.end()) { + continue; + } + if (results[i] < 0) { + ReleaseCollectedWrapper(ids[i]); + continue; + } + it->second->queued = false; + if (weakened) { + it->second->javaWeakened = true; + } + } + }; + settle(weaken, (jintArray) weakenArray, true); + settle(check, (jintArray) checkArray, false); +} + +void ObjectManager::ReleaseCollectedWrapper(int javaObjectID) { + auto it = m_idToObject.find(javaObjectID); + if (it == m_idToObject.end()) { + return; + } + ObjectWeakCallbackState* state = it->second; + Persistent* po = state->target; + auto object = po->Get(m_isolate); + auto jsInstanceInfo = GetJSInstanceInfoFromRuntimeObject(object); + if (jsInstanceInfo != nullptr) { 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; + object->SetInternalField(jsInfoIdx, Undefined(m_isolate)); } + m_idToObject.erase(it); + po->Reset(); + delete po; + delete state; } int ObjectManager::GenerateNewObjectID() { @@ -427,6 +711,9 @@ 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 @@ -457,6 +744,7 @@ void ObjectManager::ReleaseAllRegistered() { delete m_poJsWrapperFunc; m_poJsWrapperFunc = nullptr; } + m_jsWrapperTemplate.Reset(); } ObjectManager::~ObjectManager() { @@ -466,6 +754,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 +790,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(); } @@ -543,6 +839,7 @@ void ObjectManager::ReleaseNativeCounterpart(v8::Local& object) { owner->GetInternalField(jsInfoIdx).As()->Value(v8::kExternalPointerTypeTagDefault)); JEnv env; + AssertIsolateLocked(); env.CallVoidMethod(m_javaRuntimeObject, RELEASE_NATIVE_INSTANCE_METHOD_ID, jsInstanceInfo->JavaObjectID); @@ -577,6 +874,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..e6cfefdb7 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,12 @@ #include "JEnv.h" #include "JniLocalRef.h" #include "LRUCache.h" +#include "NativeScriptAssert.h" #include "v8.h" namespace tns { +struct JavaCallArgs; + class ObjectManager { public: ObjectManager(jobject javaRuntimeObject); @@ -44,13 +48,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 every JS<->Java + // crossing: JS cannot create or receive a Java object without one, so the Java objects + // still held strongly for collected wrappers cannot grow between a GC and the flush. + 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) != m_idToObject.end(); + } + + // 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, @@ -128,6 +163,14 @@ class ObjectManager { // Duplicated from jsInfo: the finalizer has to unregister itself even when // the JsInfo field was already cleared and jsInfo freed. uint32_t javaObjectID; + // 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; }; JSInstanceInfo* GetJSInstanceInfo(const v8::Local& object); @@ -135,16 +178,51 @@ class ObjectManager { JSInstanceInfo* GetJSInstanceInfoFromRuntimeObject( const v8::Local& object); + // clazz must be a global ref, or null to look className up. v8::Local CreateJSWrapperHelper(jint javaObjectID, - const std::string& typeName, + const std::string& className, jclass clazz); + struct WrapperClass { + std::string name; + jclass clazz; // global ref + }; + + // The JNI name and a global ref of instance's class, cached by class index. + const std::string& WrapperClassFor(int classIndex, jobject instance, + jclass& clazz, std::string& scratch); + + // Indexed by class index. Entries are never replaced, so references to them + // survive a nested wrap growing the vector. + std::vector> m_wrapperClasses; + static void JSObjectFinalizerStatic( const v8::WeakCallbackInfo& data); 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); @@ -179,11 +257,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..50209f01a 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; } /* @@ -606,7 +598,8 @@ static bool TryGetPrimitiveReturnBits(Isolate* isolate, } jobject Runtime::CallJSMethodNative(JNIEnv* _env, jobject obj, - jint javaObjectID, jstring methodName, + jobject javaObject, jint javaObjectID, + jstring methodName, jint retType, jboolean isConstructor, const JavaCallArgs& args, jlongArray primitiveResult) { @@ -618,7 +611,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 >= 0) { + jsObject = m_objectManager->GetLinkedJsObjectByJavaObject(javaObjectID); + } + if (jsObject.IsEmpty() && javaObject != nullptr) { + javaObjectID = m_objectManager->FindObjectId(javaObject); + if (javaObjectID < 0) { + 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." @@ -659,9 +667,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 +683,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 +721,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 +743,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 +1217,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 +1297,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 @@ -1374,7 +1383,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 +1395,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..0adbc9364 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,18 @@ class Runtime { ObjectManager* GetObjectManager() const; + AdmissionGate& GetAdmissionGate() { + return m_admissionGate; + } + 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 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 +338,8 @@ class Runtime { ObjectManager* m_objectManager; + AdmissionGate m_admissionGate; + ModuleInternal m_module; ArrayBufferHelper m_arrayBufferHelper; @@ -417,7 +424,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 +441,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/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..5bbd22291 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 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, 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/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..73fb75839 --- /dev/null +++ b/test-app/runtime/src/main/java/com/tns/ObjectTable.java @@ -0,0 +1,502 @@ +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 mask = index.length - 1; + int i = mix(System.identityHashCode(obj)) & mask; + for (int probes = 0; probes <= mask; probes++) { + int entry = index[i]; + if (entry == 0) { + return NO_ID; + } + if (getUnlocked(entry - 1) == obj) { + return entry - 1; + } + i = (i + 1) & mask; + } + return NO_ID; + } + + // get() with every probe bounded: a torn view need not contain the empty slot that + // ends a probe. + private Object getUnlocked(int id) { + int[] keys = idKeys; + Object[] vals = values; + int mask = keys.length - 1; + int i = mix(id) & mask; + int key = id + 1; + for (int probes = 0; probes <= mask; probes++) { + int k = keys[i]; + if (k == key) { + Object value = vals[i]; + return value instanceof WeakRef ? ((WeakRef) value).get() : value; + } + if (k == 0) { + return null; + } + i = (i + 1) & mask; + } + return null; + } + + // id -> entry, open addressing on the id. Keys are id + 1 so 0 marks an empty slot. + private int[] idKeys = new int[1024]; + private Object[] values = new Object[1024]; + private int[] identityHashes = new int[1024]; + private int idCount = 0; + + // identity hash -> id + 1, open addressing; values[] decides which object an id holds. + private int[] identityIndex = 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) { + int existing = findIdSlot(id); + if (existing >= 0) { + Object value = values[existing]; + if ((value instanceof WeakRef ? ((WeakRef) value).get() : value) == obj) { + values[existing] = obj; + return; + } + removeLocked(id); + } + + int hash = System.identityHashCode(obj); + if ((idCount + 1) * 2 > idKeys.length) { + growIds(); + } + insertId(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 id = findIdLocked(obj); + if (id != NO_ID) { + return id; + } + id = ids.newId(); + putStrongLocked(id, obj); + return id | 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) { + int mask = identityIndex.length - 1; + int i = mix(System.identityHashCode(obj)) & mask; + while (true) { + int entry = identityIndex[i]; + if (entry == 0) { + return NO_ID; + } + int id = entry - 1; + if (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) { + int slot = findIdSlot(id); + if (slot < 0) { + return null; + } + Object value = values[slot]; + return value instanceof WeakRef ? ((WeakRef) value).get() : value; + } + + boolean contains(int id) { + if (stamps == null) { + return findIdSlot(id) >= 0; + } + long stamp = stamps.readLock(); + try { + return findIdSlot(id) >= 0; + } 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) { + int slot = findIdSlot(id); + if (slot < 0) { + return false; + } + Object value = values[slot]; + if (value instanceof WeakRef) { + if (((WeakRef) value).get() != null) { + return true; + } + removeLocked(id); + return false; + } + values[slot] = 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) { + int slot = findIdSlot(id); + if (slot < 0) { + return; + } + int hash = identityHashes[slot]; + removeIdSlot(slot); + 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 int findIdSlot(int id) { + int mask = idKeys.length - 1; + int i = mix(id) & mask; + int key = id + 1; + while (true) { + int k = idKeys[i]; + if (k == key) { + return i; + } + if (k == 0) { + return -1; + } + i = (i + 1) & mask; + } + } + + private void insertId(int id, Object value, int hash) { + int mask = idKeys.length - 1; + int i = mix(id) & mask; + while (idKeys[i] != 0) { + i = (i + 1) & mask; + } + idKeys[i] = id + 1; + values[i] = value; + identityHashes[i] = hash; + } + + // Backward-shift deletion keeps linear probing tombstone-free. + private void removeIdSlot(int slot) { + int mask = idKeys.length - 1; + int hole = slot; + int j = slot; + while (true) { + j = (j + 1) & mask; + int key = idKeys[j]; + if (key == 0) { + break; + } + int home = mix(key - 1) & mask; + if (!isBetween(hole, home, j)) { + idKeys[hole] = key; + values[hole] = values[j]; + identityHashes[hole] = identityHashes[j]; + hole = j; + } + } + idKeys[hole] = 0; + values[hole] = null; + identityHashes[hole] = 0; + } + + private void insertIdentity(int hash, int id) { + int mask = identityIndex.length - 1; + int i = mix(hash) & mask; + while (identityIndex[i] != 0) { + i = (i + 1) & mask; + } + identityIndex[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(identityHashes[findIdSlot(entry - 1)]) & mask; + if (!isBetween(hole, home, j)) { + identityIndex[hole] = entry; + hole = j; + } + } + identityIndex[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 growIds() { + int[] oldKeys = idKeys; + Object[] oldValues = values; + int[] oldHashes = identityHashes; + int capacity = oldKeys.length * 2; + idKeys = new int[capacity]; + values = new Object[capacity]; + identityHashes = new int[capacity]; + for (int i = 0; i < oldKeys.length; i++) { + if (oldKeys[i] != 0) { + insertId(oldKeys[i] - 1, oldValues[i], oldHashes[i]); + } + } + } + + private void growIdentity() { + int[] old = identityIndex; + identityIndex = new int[old.length * 2]; + for (int entry : old) { + if (entry != 0) { + insertIdentity(identityHashes[findIdSlot(entry - 1)], entry - 1); + } + } + } + + 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..81637a389 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 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,13 +181,21 @@ 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<>(); - - private Map> weakInstances = new HashMap<>(); + // Mutated and read exactly only by the thread holding this runtime's isolate lock (the + // native side resolves and registers objects after taking it); other threads may only + // findIdOptimistic, whose result the native side re-checks. + private final ObjectTable objectTable; - 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>(); @@ -251,20 +264,46 @@ public int compare(Method lhs, Method rhs) { 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.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 { @@ -280,13 +319,9 @@ public Runtime(StaticConfiguration config, DynamicConfiguration dynamicConfigura 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); @@ -554,18 +589,10 @@ public void ResetDateTimeConfigurationCache() { } } - 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); - } + // Only native code may call this, from the thread holding the isolate lock. + @RuntimeCallable + void releaseNativeCounterpart(int nativeObjectId) { + objectTable.remove(nativeObjectId); } /* @@ -809,7 +836,6 @@ private Class resolveClass(String baseClassName, String fullClassName, String return javaClass; } - @RuntimeCallable private long getUsedMemory() { long usedMemory = dalvikRuntime.totalMemory() - dalvikRuntime.freeMemory(); return usedMemory; @@ -884,10 +910,9 @@ public static void initInstance(Object instance) { } private void 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)) { @@ -1023,7 +1048,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 +1062,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,37 +1092,37 @@ 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; - } - - 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); - } + 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); } - // 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); - } + int id = objectTable.findId(obj); + return id == ObjectTable.NO_ID ? null : id; + } - return id; + @RuntimeCallable + private int findJavaObjectID(Object obj) { + return objectTable.findId(obj); + } + + // 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) { + int id = objectTable.findId(obj); + if (id != ObjectTable.NO_ID) { + return id & 0xFFFFFFFFL; + } + id = generateNewObjectId(getRuntimeId()); + makeInstanceStrong(obj, id); + return packIdAndClass(id, obj); } @RuntimeCallable @@ -1223,26 +1229,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 +1258,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 +1302,27 @@ 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.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) { @@ -1407,11 +1425,12 @@ private Object dispatchCallJSMethodNative(final Object javaObject, final String boolean enableMultithreadedJavascript = this.config.appConfig.getEnableMultithreadedJavascript(); 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, objectTable.findIdOptimistic(javaObject), 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 +1471,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, 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/ObjectTableTest.java b/test-app/runtime/src/test/java/com/tns/ObjectTableTest.java new file mode 100644 index 000000000..9b80a577a --- /dev/null +++ b/test-app/runtime/src/test/java/com/tns/ObjectTableTest.java @@ -0,0 +1,328 @@ +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 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()); } }