diff --git a/test-app/app/src/main/assets/app/boot.js b/test-app/app/src/main/assets/app/boot.js index ee7f6cf26..8d38bfad7 100644 --- a/test-app/app/src/main/assets/app/boot.js +++ b/test-app/app/src/main/assets/app/boot.js @@ -29,7 +29,10 @@ global.__JUnitSaveResults = function (unitTestResults) { } }; -require('./Infrastructure/Jasmine/jasmine-2.0.1/boot'); //runs jasmine, attaches the junitOutputter +if (com.tns.tests.JniBench.isEnabled()) { + require("./tests/benchmarks/jniBenchmarks").run(); +} else { + require('./Infrastructure/Jasmine/jasmine-2.0.1/boot'); //runs jasmine, attaches the junitOutputter - -require("./mainpage"); \ No newline at end of file + require("./mainpage"); +} \ No newline at end of file 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 new file mode 100644 index 000000000..d0fbb5725 --- /dev/null +++ b/test-app/app/src/main/assets/app/tests/benchmarks/jniBenchmarks.js @@ -0,0 +1,281 @@ +// JNI bridge micro-benchmarks. Not part of the Jasmine suite: boot.js runs this instead of the +// suite when `adb shell setprop debug.ns.jnibench 1` is set, and every result is logged as a +// `JNIBENCH {json}` line (read with `adb logcat -s JS:I | grep JNIBENCH`). +// +// Each benchmark warms up (so V8 tiers the caller up), then runs ROUNDS timed rounds and reports +// the median and minimum ns/op. Java -> JS benchmarks are timed in Java with System.nanoTime. + +var JniBench = com.tns.tests.JniBench; +var ROUNDS = 7; +var FILTER = String(JniBench.getFilter()); +var SCALE = JniBench.getScale(); + +function median(values) { + var sorted = values.slice().sort(function (a, b) { return a - b; }); + return sorted[Math.floor(sorted.length / 2)]; +} + +function report(name, nsPerOp) { + var result = { + name: name, + median: Math.round(median(nsPerOp) * 10) / 10, + min: Math.round(Math.min.apply(null, nsPerOp) * 10) / 10 + }; + console.log("JNIBENCH " + JSON.stringify(result)); + return result; +} + +// body(n) runs the operation n times; returning a value keeps V8 from dropping the loop. +function measureJs(name, iterations, body) { + if (FILTER && name.indexOf(FILTER) < 0) return null; + iterations *= SCALE; + var sink = body(Math.max(1000, iterations >> 2)); + var samples = []; + for (var r = 0; r < ROUNDS; r++) { + var start = performance.now(); + sink = body(iterations); + var elapsed = performance.now() - start; + samples.push((elapsed * 1e6) / iterations); + } + globalThis.__jniBenchSink = sink; + return report(name, samples); +} + +function measureCallback(name, cb, kind, iterations) { + if (FILTER && name.indexOf(FILTER) < 0) return null; + iterations *= SCALE; + JniBench.runCallbacks(cb, kind, Math.max(1000, iterations >> 2)); + var samples = []; + for (var r = 0; r < ROUNDS; r++) { + var nanos = JniBench.runCallbacks(cb, kind, iterations); + samples.push(Number(nanos) / iterations); + } + return report(name, samples); +} + +function buildBenchmarks() { + var bench = new JniBench(); + var other = bench.getOther(); + var jsString = "hello world"; + var longString = new Array(129).join("abcdefgh"); + var Extended = java.lang.Object.extend({ + toString: function () { return "extended"; } + }); + var buffer = new ArrayBuffer(64); + var intArray = [1, 2, 3, 4, 5, 6, 7, 8]; + var callback = new JniBench.Callback({ + noArgs: function () { }, + twoInts: function (a, b) { }, + oneString: function (s) { }, + intReturn: function (a) { return a + 1; }, + oneObject: function (o) { } + }); + + return [ + function () { + return measureJs("js.emptyFunctionCall", 200000, function (n) { + var f = function (x) { return x + 1; }; + var s = 0; + for (var i = 0; i < n; i++) s = f(s); + return s; + }); + }, + function () { + return measureJs("static.noop()", 50000, function (n) { + for (var i = 0; i < n; i++) JniBench.noop(); + return n; + }); + }, + function () { + return measureJs("static.add(int,int)", 50000, function (n) { + var s = 0; + for (var i = 0; i < n; i++) s = JniBench.add(i, 1); + return s; + }); + }, + function () { + return measureJs("static.addDouble(double,double)", 50000, function (n) { + var s = 0; + for (var i = 0; i < n; i++) s = JniBench.addDouble(i, 0.5); + return s; + }); + }, + function () { + return measureJs("static.addLong(long,long)", 50000, function (n) { + var s = 0; + for (var i = 0; i < n; i++) s = JniBench.addLong(i, 1); + return s; + }); + }, + function () { + return measureJs("instance.noop()", 50000, function (n) { + for (var i = 0; i < n; i++) bench.instanceNoop(); + return n; + }); + }, + function () { + return measureJs("instance.getInt()", 50000, function (n) { + var s = 0; + for (var i = 0; i < n; i++) s = bench.getInt(); + return s; + }); + }, + function () { + return measureJs("instance.getBool()", 50000, function (n) { + var s = false; + for (var i = 0; i < n; i++) s = bench.getBool(); + return s; + }); + }, + function () { + return measureJs("instance.getChar()", 50000, function (n) { + var s; + for (var i = 0; i < n; i++) s = bench.getChar(); + return s; + }); + }, + function () { + return measureJs("instance.echo(String)", 50000, function (n) { + var s; + for (var i = 0; i < n; i++) s = bench.echo(jsString); + return s; + }); + }, + function () { + return measureJs("instance.echo(String 1KB)", 20000, function (n) { + var s; + for (var i = 0; i < n; i++) s = bench.echo(longString); + return s; + }); + }, + function () { + return measureJs("instance.getString()", 50000, function (n) { + var s; + for (var i = 0; i < n; i++) s = bench.getString(); + return s; + }); + }, + function () { + return measureJs("instance.self() [wrapped return]", 50000, function (n) { + var s; + for (var i = 0; i < n; i++) s = bench.self(); + return s; + }); + }, + function () { + return measureJs("instance.getOther() [wrapped return]", 50000, function (n) { + var s; + for (var i = 0; i < n; i++) s = bench.getOther(); + return s; + }); + }, + function () { + return measureJs("instance.takeObject(Object)", 50000, function (n) { + for (var i = 0; i < n; i++) bench.takeObject(other); + return n; + }); + }, + function () { + return measureJs("instance.takeObjects(Object x3)", 50000, function (n) { + var s = 0; + for (var i = 0; i < n; i++) s = bench.takeObjects(other, bench, other); + return s; + }); + }, + function () { + return measureJs("static.bufferCapacity(ArrayBuffer)", 20000, function (n) { + var s = 0; + for (var i = 0; i < n; i++) s = JniBench.bufferCapacity(buffer); + return s; + }); + }, + function () { + return measureJs("static.sumInts(int[8] from JS array)", 20000, function (n) { + var s = 0; + for (var i = 0; i < n; i++) s = JniBench.sumInts(intArray); + return s; + }); + }, + function () { + return measureJs("static.newObjectInJava() [new wrapper]", 20000, function (n) { + var s; + for (var i = 0; i < n; i++) s = JniBench.newObjectInJava(); + return s; + }); + }, + function () { + return measureJs("new java.lang.Object()", 20000, function (n) { + var s; + for (var i = 0; i < n; i++) s = new java.lang.Object(); + return s; + }); + }, + function () { + return measureJs("new Object.extend({})()", 20000, function (n) { + var s; + for (var i = 0; i < n; i++) s = new Extended(); + return s; + }); + }, + function () { + return measureJs("field.get int", 50000, function (n) { + var s = 0; + for (var i = 0; i < n; i++) s = bench.intField; + return s; + }); + }, + function () { + return measureJs("field.set int", 50000, function (n) { + for (var i = 0; i < n; i++) bench.intField = i; + return n; + }); + }, + function () { + return measureJs("field.get Object", 50000, function (n) { + var s; + for (var i = 0; i < n; i++) s = bench.objField; + return s; + }); + }, + function () { + return measureJs("field.get String", 50000, function (n) { + var s; + for (var i = 0; i < n; i++) s = bench.strField; + return s; + }); + }, + function () { + return measureCallback("callback.noArgs()", callback, 0, 20000); + }, + function () { + return measureCallback("callback.twoInts(int,int)", callback, 1, 20000); + }, + function () { + return measureCallback("callback.oneString(String)", callback, 2, 20000); + }, + function () { + return measureCallback("callback.intReturn(int) -> int", callback, 3, 20000); + }, + function () { + return measureCallback("callback.oneObject(Object)", callback, 4, 20000); + } + ]; +} + +exports.run = function () { + var benchmarks = buildBenchmarks(); + var results = []; + var index = 0; + // Yield to the looper between benchmarks so GC and queued work run outside timed rounds. + function next() { + if (index >= benchmarks.length) { + console.log("JNIBENCH_DONE " + JSON.stringify(results)); + return; + } + gc(); + var result = benchmarks[index++](); + if (result) results.push(result); + setTimeout(next, 50); + } + setTimeout(next, 500); +}; 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 new file mode 100644 index 000000000..ab3313ebb --- /dev/null +++ b/test-app/app/src/main/java/com/tns/tests/JniBench.java @@ -0,0 +1,154 @@ +package com.tns.tests; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.nio.ByteBuffer; + +/** + * Targets for the JNI micro-benchmarks in assets/app/tests/benchmarks/jniBenchmarks.js. + * Every member is deliberately trivial so a measurement is dominated by the bridge, not the work. + */ +public class JniBench { + public interface Callback { + void noArgs(); + + void twoInts(int a, int b); + + void oneString(String s); + + int intReturn(int a); + + void oneObject(Object o); + } + + public int intField = 7; + public Object objField = new Object(); + public String strField = "hello"; + + private final Object other = new Object(); + + public static boolean isEnabled() { + return getProp("debug.ns.jnibench").equals("1"); + } + + /** Substring a benchmark name must contain to run; empty runs all. */ + public static String getFilter() { + return getProp("debug.ns.jnibench.filter"); + } + + /** Iteration multiplier, e.g. to keep one benchmark running long enough to profile. */ + public static int getScale() { + try { + return Math.max(1, Integer.parseInt(getProp("debug.ns.jnibench.scale"))); + } catch (NumberFormatException e) { + return 1; + } + } + + private static String getProp(String name) { + try { + Process p = new ProcessBuilder("getprop", name).start(); + try (BufferedReader r = new BufferedReader(new InputStreamReader(p.getInputStream()))) { + String line = r.readLine(); + return line == null ? "" : line.trim(); + } + } catch (Exception e) { + return ""; + } + } + + public static void noop() { + } + + public static int add(int a, int b) { + return a + b; + } + + public static long addLong(long a, long b) { + return a + b; + } + + public static double addDouble(double a, double b) { + return a + b; + } + + public void instanceNoop() { + } + + public int getInt() { + return intField; + } + + public boolean getBool() { + return true; + } + + public char getChar() { + return 'x'; + } + + public String echo(String s) { + return s; + } + + public String getString() { + return strField; + } + + public Object self() { + return this; + } + + public Object getOther() { + return other; + } + + public void takeObject(Object o) { + } + + public int takeObjects(Object a, Object b, Object c) { + return 3; + } + + public static int bufferCapacity(ByteBuffer b) { + return b.capacity(); + } + + public static int sumInts(int[] values) { + int sum = 0; + for (int v : values) { + sum += v; + } + return sum; + } + + public static Object newObjectInJava() { + return new Object(); + } + + /** 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(); + long start = System.nanoTime(); + switch (kind) { + case 0: + for (int i = 0; i < n; i++) cb.noArgs(); + break; + case 1: + for (int i = 0; i < n; i++) cb.twoInts(i, 2); + break; + case 2: + for (int i = 0; i < n; i++) cb.oneString("hello world"); + break; + case 3: + for (int i = 0; i < n; i++) cb.intReturn(i); + break; + case 4: + for (int i = 0; i < n; i++) cb.oneObject(arg); + break; + default: + throw new IllegalArgumentException("kind " + kind); + } + return System.nanoTime() - start; + } +} diff --git a/test-app/runtime/src/main/cpp/ArgConverter.cpp b/test-app/runtime/src/main/cpp/ArgConverter.cpp index 93cdf54d9..2ad647036 100644 --- a/test-app/runtime/src/main/cpp/ArgConverter.cpp +++ b/test-app/runtime/src/main/cpp/ArgConverter.cpp @@ -1,4 +1,5 @@ #include "ArgConverter.h" +#include "JniLocalRef.h" #include "ObjectManager.h" #include "Util.h" #include "V8StringConstants.h" @@ -8,7 +9,9 @@ #include "V8GlobalHelpers.h" #include "NativeScriptAssert.h" #include "RuntimeState.h" +#include #include +#include using namespace v8; using namespace std; @@ -89,58 +92,74 @@ void ArgConverter::NativeScriptLongFunctionCallback(const v8::FunctionCallbackIn } } -Local ArgConverter::ConvertJavaArgsToJsArgs(Local context, jobjectArray args) { - JEnv env; +int ArgConverter::GetJavaArgsLength(JNIEnv* env, const JavaCallArgs& args) { + return args.types == nullptr ? 0 : env->GetArrayLength(args.types); +} - int argc = env.GetArrayLength(args) / 3; - auto isolate = v8::Isolate::GetCurrent(); - Local arr(Array::New(isolate, argc)); +void ArgConverter::ConvertJavaArgsToJsArgs(Isolate* isolate, JNIEnv* env, const JavaCallArgs& args, int argc, Local* out) { + if (argc == 0) { + return; + } - auto runtime = Runtime::GetRuntime(isolate); - auto objectManager = runtime->GetObjectManager(); + constexpr int kInlineArgs = 16; + jint inlineTypes[kInlineArgs]; + jlong inlineValues[kInlineArgs]; + std::vector heapTypes; + std::vector heapValues; + jint* types = inlineTypes; + jlong* values = inlineValues; + if (argc > kInlineArgs) { + heapTypes.resize(argc); + heapValues.resize(argc); + types = heapTypes.data(); + values = heapValues.data(); + } - int jArrayIndex = 0; - for (int i = 0; i < argc; i++) { - JniLocalRef argTypeIDObj(env.GetObjectArrayElement(args, jArrayIndex++)); - JniLocalRef arg(env.GetObjectArrayElement(args, jArrayIndex++)); - JniLocalRef argJavaClassPath(env.GetObjectArrayElement(args, jArrayIndex++)); + env->GetIntArrayRegion(args.types, 0, argc, types); + env->GetLongArrayRegion(args.values, 0, argc, values); - Type argTypeID = (Type) JType::IntValue(env, argTypeIDObj); + auto objectManager = Runtime::GetRuntime(isolate)->GetObjectManager(); + for (int i = 0; i < argc; i++) { + jlong bits = values[i]; Local jsArg; - switch (argTypeID) { + switch (static_cast(types[i])) { case Type::Boolean: - jsArg = Boolean::New(isolate, JType::BooleanValue(env, arg)); + jsArg = Boolean::New(isolate, bits != 0); break; case Type::Char: - jsArg = jcharToV8String(isolate, JType::CharValue(env, arg)); + jsArg = jcharToV8String(isolate, static_cast(bits)); break; case Type::Byte: - jsArg = Number::New(isolate, JType::ByteValue(env, arg)); - break; case Type::Short: - jsArg = Number::New(isolate, JType::ShortValue(env, arg)); - break; case Type::Int: - jsArg = Number::New(isolate, JType::IntValue(env, arg)); - break; case Type::Long: - jsArg = Number::New(isolate, JType::LongValue(env, arg)); + jsArg = Number::New(isolate, static_cast(bits)); break; - case Type::Float: - jsArg = Number::New(isolate, JType::FloatValue(env, arg)); + case Type::Float: { + auto raw = static_cast(bits); + float value; + memcpy(&value, &raw, sizeof(value)); + jsArg = Number::New(isolate, value); break; - case Type::Double: - jsArg = Number::New(isolate, JType::DoubleValue(env, arg)); + } + case Type::Double: { + double value; + memcpy(&value, &bits, sizeof(value)); + jsArg = Number::New(isolate, value); break; - case Type::String: - jsArg = jstringToV8String(isolate, (jstring) arg); + } + case Type::String: { + JniLocalRef str(env->GetObjectArrayElement(args.refs, i)); + jsArg = jstringToV8String(isolate, (jstring) str); break; + } case Type::JsObject: { - jint javaObjectID = JType::IntValue(env, arg); + auto javaObjectID = static_cast(bits); jsArg = objectManager->GetJsObjectByJavaObject(javaObjectID); if (jsArg.IsEmpty()) { + JniLocalRef argJavaClassPath(env->GetObjectArrayElement(args.refs, i)); string argClassName = jstringToString(ObjectToString(argJavaClassPath)); argClassName = Util::ConvertFromCanonicalToJniName(argClassName); jsArg = objectManager->CreateJSWrapper(javaObjectID, argClassName); @@ -152,10 +171,8 @@ Local ArgConverter::ConvertJavaArgsToJsArgs(Local context, jobje break; } - arr->Set(context, i, jsArg); + out[i] = jsArg; } - - return arr; } 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 e8d941592..5e416b6b4 100644 --- a/test-app/runtime/src/main/cpp/ArgConverter.h +++ b/test-app/runtime/src/main/cpp/ArgConverter.h @@ -12,14 +12,25 @@ #include "JEnv.h" #include #include +#include namespace tns { +// The argument arrays com.tns.Runtime.packageArgs builds for a Java -> JS call. +struct JavaCallArgs { + jintArray types; + jlongArray values; + jobjectArray refs; +}; + class ArgConverter { public: static void Init(v8::Local context); - static v8::Local ConvertJavaArgsToJsArgs(v8::Local context, jobjectArray args); + static int GetJavaArgsLength(JNIEnv* env, const JavaCallArgs& args); + + // out must have room for GetJavaArgsLength(args) 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); @@ -39,6 +50,31 @@ class ArgConverter { return v8String; } + inline static v8::Local jcharToV8String(v8::Isolate* isolate, jchar value) { + auto v8String = ConvertToV8String(isolate, &value, 1); + return v8String; + } + + // For property keys: an internalized key skips the string-table insert a + // fresh key costs on every property lookup. + static v8::Local jstringToInternalizedV8String(v8::Isolate* isolate, JNIEnv* env, jstring value) { + if (value == nullptr) { + return v8::String::Empty(isolate); + } + + constexpr jsize kInlineUnits = 64; + jsize length = env->GetStringLength(value); + jchar inlineUnits[kInlineUnits]; + std::vector heapUnits; + jchar* units = inlineUnits; + if (length > kInlineUnits) { + heapUnits.resize(length); + units = heapUnits.data(); + } + env->GetStringRegion(value, 0, length, units); + return v8::String::NewFromTwoByte(isolate, reinterpret_cast(units), v8::NewStringType::kInternalized, length).ToLocalChecked(); + } + static std::string jstringToString(jstring value) { if (value == nullptr) { return {}; @@ -95,8 +131,27 @@ class ArgConverter { inline static jstring ConvertToJavaString(const v8::Local& jsValue) { JEnv env; auto isolate = v8::Isolate::GetCurrent(); - v8::String::Value stringValue(isolate, jsValue); - return env.NewString((const jchar*) *stringValue, stringValue.length()); + v8::Local str; + if (jsValue->IsString()) { + str = jsValue.As(); + } else if (!jsValue->ToString(isolate->GetCurrentContext()).ToLocal(&str)) { + // ToString threw and left a V8 exception pending; running it + // again would re-enter user code. + static const jchar kNoUnits[1] = {0}; + return env.NewString(kNoUnits, 0); + } + + constexpr uint32_t kInlineUnits = 256; + uint32_t length = str->Length(); + uint16_t inlineUnits[kInlineUnits]; + std::vector heapUnits; + uint16_t* units = inlineUnits; + if (length > kInlineUnits) { + heapUnits.resize(length); + units = heapUnits.data(); + } + str->WriteV2(isolate, 0, length, units); + return env.NewString(reinterpret_cast(units), static_cast(length)); } inline static v8::Local ConvertToV8String(v8::Isolate* isolate, const jchar* data, int length) { @@ -142,11 +197,6 @@ class ArgConverter { return (jstring) object; } - inline static v8::Local jcharToV8String(v8::Isolate* isolate, jchar value) { - auto v8String = ConvertToV8String(isolate, &value, 1); - return v8String; - } - static void NativeScriptLongFunctionCallback(const v8::FunctionCallbackInfo& args); static void NativeScriptLongValueOfFunctionCallback(const v8::FunctionCallbackInfo& args); diff --git a/test-app/runtime/src/main/cpp/CallbackHandlers.cpp b/test-app/runtime/src/main/cpp/CallbackHandlers.cpp index 4a1789456..0e82bc58b 100644 --- a/test-app/runtime/src/main/cpp/CallbackHandlers.cpp +++ b/test-app/runtime/src/main/cpp/CallbackHandlers.cpp @@ -230,6 +230,17 @@ void CallbackHandlers::SetJavaField(Isolate *isolate, const Local &targe fieldAccessor.SetJavaField(isolate, target, value, fieldData); } +// java.lang.String is final, so an object returned through any other declared +// type can only be a String if that type is one of String's supertypes. +static bool CanBeJavaString(const std::string &jniReturnType) { + return jniReturnType == "Ljava/lang/Object;" || + jniReturnType == "Ljava/lang/CharSequence;" || + jniReturnType == "Ljava/lang/Comparable;" || + jniReturnType == "Ljava/io/Serializable;" || + jniReturnType == "Ljava/lang/constant/Constable;" || + jniReturnType == "Ljava/lang/constant/ConstantDesc;"; +} + void CallbackHandlers::CallJavaMethod(const Local &caller, const string &className, const string &methodName, MetadataEntry *entry, bool isFromInterface, bool isStatic, @@ -441,11 +452,7 @@ void CallbackHandlers::CallJavaMethod(const Local &caller, const string result = env.CallCharMethodA(callerJavaObject, mid, javaArgs); } - JniLocalRef str(env.NewString(&result, 1)); - jboolean bol = true; - const char *resP = env.GetStringUTFChars(str, &bol); - args.GetReturnValue().Set(ArgConverter::ConvertToV8String(isolate, resP, 1)); - env.ReleaseStringUTFChars(str, resP); + args.GetReturnValue().Set(ArgConverter::jcharToV8String(isolate, result)); break; } case MethodReturnType::Short: { @@ -546,7 +553,8 @@ void CallbackHandlers::CallJavaMethod(const Local &caller, const string } if (result != nullptr) { - auto isString = env.IsInstanceOf(result, JAVA_LANG_STRING); + auto isString = (returnType == nullptr || CanBeJavaString(*returnType)) + && env.IsInstanceOf(result, JAVA_LANG_STRING); Local objectResult; if (isString) { @@ -1000,42 +1008,45 @@ vector CallbackHandlers::GetTypeMetadata(const string &name, int index) } Local CallbackHandlers::CallJSMethod(Isolate *isolate, JNIEnv *_env, - const Local &jsObject, const string &methodName, - jobjectArray args) { + const Local &jsObject, const Local &methodName, + const JavaCallArgs &args) { SET_PROFILER_FRAME(); JEnv env(_env); Local result; auto context = Runtime::GetRuntime(isolate)->GetContext(); - auto method = jsObject->Get(context, ArgConverter::ConvertToV8String(isolate, - methodName)).ToLocalChecked(); + auto method = jsObject->Get(context, methodName).ToLocalChecked(); if (method.IsEmpty() || method->IsUndefined()) { stringstream ss; - ss << "Cannot find method '" << methodName << "' implementation"; + ss << "Cannot find method '" << ArgConverter::ConvertToString(methodName) << "' implementation"; throw NativeScriptException(ss.str()); } else if (!method->IsFunction()) { stringstream ss; - ss << "Property '" << methodName << "' is not a function"; + ss << "Property '" << ArgConverter::ConvertToString(methodName) << "' is not a function"; throw NativeScriptException(ss.str()); } else { EscapableHandleScope handleScope(isolate); auto jsMethod = method.As(); - auto jsArgs = ArgConverter::ConvertJavaArgsToJsArgs(context, args); - int argc = jsArgs->Length(); + int argc = ArgConverter::GetJavaArgsLength(env, args); - std::vector> arguments(argc); - for (int i = 0; i < argc; i++) { - arguments[i] = jsArgs->Get(context, i).ToLocalChecked(); + constexpr int kInlineArgs = 16; + Local inlineArguments[kInlineArgs]; + std::vector> heapArguments; + Local* arguments = inlineArguments; + if (argc > kInlineArgs) { + heapArguments.resize(argc); + arguments = heapArguments.data(); } + ArgConverter::ConvertJavaArgsToJsArgs(isolate, env, args, argc, arguments); TryCatch tc(isolate); Local jsResult; { SET_PROFILER_FRAME(); - jsMethod->Call(context, jsObject, argc, argc == 0 ? nullptr : arguments.data()).ToLocal( + jsMethod->Call(context, jsObject, argc, argc == 0 ? nullptr : arguments).ToLocal( &jsResult); } @@ -1048,7 +1059,7 @@ Local CallbackHandlers::CallJSMethod(Isolate *isolate, JNIEnv *_env, jsResult = v8::Undefined(isolate); } else { stringstream ss; - ss << "Calling js method " << methodName << " failed"; + ss << "Calling js method " << ArgConverter::ConvertToString(methodName) << " failed"; throw NativeScriptException(tc, ss.str()); } } diff --git a/test-app/runtime/src/main/cpp/CallbackHandlers.h b/test-app/runtime/src/main/cpp/CallbackHandlers.h index 86dba64a5..239522cae 100644 --- a/test-app/runtime/src/main/cpp/CallbackHandlers.h +++ b/test-app/runtime/src/main/cpp/CallbackHandlers.h @@ -62,7 +62,7 @@ namespace tns { static v8::Local CallJSMethod(v8::Isolate *isolate, JNIEnv *_env, const v8::Local &jsObject, - const std::string &methodName, jobjectArray args); + const v8::Local &methodName, const JavaCallArgs &args); static v8::Local GetJavaField(v8::Isolate *isolate, const v8::Local &caller, diff --git a/test-app/runtime/src/main/cpp/FieldAccessor.cpp b/test-app/runtime/src/main/cpp/FieldAccessor.cpp index 4801568e6..6837fa214 100644 --- a/test-app/runtime/src/main/cpp/FieldAccessor.cpp +++ b/test-app/runtime/src/main/cpp/FieldAccessor.cpp @@ -89,11 +89,7 @@ Local FieldAccessor::GetJavaField(Isolate* isolate, const Local& result = env.GetCharField(targetJavaObject, fieldId); } - JniLocalRef str(env.NewString(&result, 1)); - jboolean bol = true; - const char* resP = env.GetStringUTFChars(str, &bol); - fieldResult = handleScope.Escape(ArgConverter::ConvertToV8String(isolate, resP, 1)); - env.ReleaseStringUTFChars(str, resP); + fieldResult = handleScope.Escape(ArgConverter::jcharToV8String(isolate, result)); break; } case 'S': { //short diff --git a/test-app/runtime/src/main/cpp/JEnv.cpp b/test-app/runtime/src/main/cpp/JEnv.cpp index 7cef05520..28c29ce2e 100644 --- a/test-app/runtime/src/main/cpp/JEnv.cpp +++ b/test-app/runtime/src/main/cpp/JEnv.cpp @@ -19,8 +19,7 @@ using namespace tns; static std::shared_mutex classCacheMutex; using namespace std; -JEnv::JEnv() - : m_env(nullptr) { +JNIEnv* JEnv::GetEnvSlow() { JNIEnv *env = nullptr; jint ret = s_jvm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6); @@ -30,22 +29,17 @@ JEnv::JEnv() NS_CHECK(env != nullptr); } - m_env = env; + return env; } -JEnv::JEnv(JNIEnv *jniEnv) { - jint ret = s_jvm->GetEnv(reinterpret_cast(&jniEnv), JNI_VERSION_1_6); - - if ((ret != JNI_OK) || (jniEnv == nullptr)) { - ret = s_jvm->AttachCurrentThread(&jniEnv, nullptr); - NS_CHECK(ret == JNI_OK); - NS_CHECK(jniEnv != nullptr); - } - - m_env = jniEnv; +void JEnv::PinThreadEnv(JNIEnv* env) { + s_threadEnv = env; + s_threadEnvPinned = true; } -JEnv::~JEnv() { +void JEnv::UnpinThreadEnv() { + s_threadEnv = nullptr; + s_threadEnvPinned = false; } JEnv::operator JNIEnv* () const { @@ -898,6 +892,8 @@ void JEnv::CheckForJavaException() { } JavaVM *JEnv::s_jvm = nullptr; +thread_local JNIEnv *JEnv::s_threadEnv = nullptr; +thread_local bool JEnv::s_threadEnvPinned = false; 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 fa33bc5d0..20d309a83 100644 --- a/test-app/runtime/src/main/cpp/JEnv.h +++ b/test-app/runtime/src/main/cpp/JEnv.h @@ -8,11 +8,13 @@ namespace tns { class JEnv { public: - JEnv(); + JEnv() : m_env(s_threadEnv != nullptr ? s_threadEnv : GetEnvSlow()) { + } - JEnv(JNIEnv* jniEnv); + JEnv(JNIEnv* jniEnv) : m_env(jniEnv != nullptr ? jniEnv : JEnv().m_env) { + } - ~JEnv(); + ~JEnv() = default; operator JNIEnv* () const; @@ -335,11 +337,55 @@ class JEnv { static void Init(JavaVM* jvm); + /* + * Caches env as this thread's JNIEnv until UnpinThreadEnv. Only for a + * thread that stays attached for the whole pinned span: a JNIEnv dies + * with DetachCurrentThread and a re-attach may hand out another one. + */ + static void PinThreadEnv(JNIEnv* env); + + static void UnpinThreadEnv(); + + /* + * 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 + * it returns, whatever thread it runs on. + */ + class EntryScope { + public: + explicit EntryScope(JNIEnv* env) + : m_active(!s_threadEnvPinned), m_previous(s_threadEnv) { + if (m_active) { + s_threadEnv = env; + } + } + + ~EntryScope() { + if (m_active && !s_threadEnvPinned) { + s_threadEnv = m_previous; + } + } + + EntryScope(const EntryScope&) = delete; + EntryScope& operator=(const EntryScope&) = delete; + + private: + bool m_active; + JNIEnv* m_previous; + }; + private: + static JNIEnv* 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 JavaVM* s_jvm; static jclass RUNTIME_CLASS; diff --git a/test-app/runtime/src/main/cpp/JsArgConverter.cpp b/test-app/runtime/src/main/cpp/JsArgConverter.cpp index 38fb50328..3f563cbf2 100644 --- a/test-app/runtime/src/main/cpp/JsArgConverter.cpp +++ b/test-app/runtime/src/main/cpp/JsArgConverter.cpp @@ -17,7 +17,7 @@ using namespace tns; JsArgConverter::JsArgConverter(const Local &caller, const v8::FunctionCallbackInfo &args, const string &methodSignature, MetadataEntry *entry) - : m_isolate(args.GetIsolate()), m_methodSignature(methodSignature), m_isValid(true), + : m_isolate(args.GetIsolate()), m_isValid(true), m_error(Error()) { int v8ProvidedArgumentsLength = args.Length(); m_argsLen = 1 + v8ProvidedArgumentsLength; @@ -25,12 +25,12 @@ JsArgConverter::JsArgConverter(const Local &caller, if (m_argsLen > 0) { if ((entry != nullptr) && (entry->getIsResolved())) { if (entry->parsedSig.empty()) { - JniSignatureParser parser(m_methodSignature); + JniSignatureParser parser(methodSignature); entry->parsedSig = parser.Parse(); } m_tokens = &entry->parsedSig; } else { - JniSignatureParser parser(m_methodSignature); + JniSignatureParser parser(methodSignature); m_ownedTokens = parser.Parse(); } @@ -53,19 +53,19 @@ JsArgConverter::JsArgConverter(const Local &caller, JsArgConverter::JsArgConverter(const v8::FunctionCallbackInfo &args, bool hasImplementationObject, const string &methodSignature, MetadataEntry *entry) - : m_isolate(args.GetIsolate()), m_methodSignature(methodSignature), m_isValid(true), + : m_isolate(args.GetIsolate()), m_isValid(true), m_error(Error()) { m_argsLen = !hasImplementationObject ? args.Length() : args.Length() - 1; if (m_argsLen > 0) { if ((entry != nullptr) && (entry->getIsResolved())) { if (entry->parsedSig.empty()) { - JniSignatureParser parser(m_methodSignature); + JniSignatureParser parser(methodSignature); entry->parsedSig = parser.Parse(); } m_tokens = &entry->parsedSig; } else { - JniSignatureParser parser(m_methodSignature); + JniSignatureParser parser(methodSignature); m_ownedTokens = parser.Parse(); } @@ -81,11 +81,11 @@ JsArgConverter::JsArgConverter(const v8::FunctionCallbackInfo &args, JsArgConverter::JsArgConverter(const v8::FunctionCallbackInfo &args, const string &methodSignature) - : m_isolate(args.GetIsolate()), m_methodSignature(methodSignature), m_isValid(true), + : m_isolate(args.GetIsolate()), m_isValid(true), m_error(Error()) { m_argsLen = args.Length(); - JniSignatureParser parser(m_methodSignature); + JniSignatureParser parser(methodSignature); m_ownedTokens = parser.Parse(); for (int i = 0; i < m_argsLen; i++) { @@ -366,8 +366,7 @@ bool JsArgConverter::ConvertArg(const Local &jsArg, int index) { obj = objectManager->GetJavaObjectByJsObject(jsObject); } - V8GetPrivateValue(m_isolate, jsObject, ArgConverter::ConvertToV8String(m_isolate, - V8StringConstants::NULL_NODE_NAME), + V8GetPrivateValue(m_isolate, jsObject, V8StringConstants::GetNullNodePrivate(m_isolate), castValue); if (!castValue.IsEmpty()) { diff --git a/test-app/runtime/src/main/cpp/JsArgConverter.h b/test-app/runtime/src/main/cpp/JsArgConverter.h index 3e7ea92b5..d3954a018 100644 --- a/test-app/runtime/src/main/cpp/JsArgConverter.h +++ b/test-app/runtime/src/main/cpp/JsArgConverter.h @@ -81,8 +81,6 @@ namespace tns { int m_args_refs[255]; int m_args_refs_size = 0; - std::string m_methodSignature; - std::vector m_ownedTokens; // May point at m_ownedTokens, which is why the converter is not copyable. diff --git a/test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp b/test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp index 3e24d986a..8f6800259 100644 --- a/test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp +++ b/test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp @@ -60,7 +60,7 @@ JsArgToArrayConverter::JsArgToArrayConverter(const v8::FunctionCallbackInfo context, const Local &jsArg, int index) { bool success = false; - stringstream s; + std::string error; JEnv env; @@ -71,10 +71,10 @@ bool JsArgToArrayConverter::ConvertArg(Local context, const LocalIsProxy() && !arg->IsObject()) { - s << "Cannot marshal a revoked Proxy at index " << index << " to Java type."; + error = "Cannot marshal a revoked Proxy at index " + std::to_string(index) + " to Java type."; success = false; } else if (arg->IsInt32() && (returnType == Type::Int || returnType == Type::Null)) { jint value = arg->Int32Value(context).ToChecked(); @@ -350,14 +350,14 @@ bool JsArgToArrayConverter::ConvertArg(Local context, const Local(castValue.As()->Value(v8::kExternalPointerTypeTagDefault)); if (node == nullptr) { - s << "Cannot get type of the null argument at index " << index; + error = "Cannot get type of the null argument at index " + std::to_string(index); success = false; break; } @@ -386,9 +386,9 @@ bool JsArgToArrayConverter::ConvertArg(Local context, const Local") << " at index " << index - << " to Java type."; + error = std::string("Cannot marshal JavaScript argument ") + + (*jsObjStr ? *jsObjStr : "") + " at index " + + std::to_string(index) + " to Java type."; } break; @@ -399,13 +399,13 @@ bool JsArgToArrayConverter::ConvertArg(Local context, const Local& value) { @@ -405,10 +404,10 @@ void MetadataNode::NullObjectAccessorGetterCallback(const FunctionCallbackInfo hiddenVal; - V8GetPrivateValue(isolate, thiz, V8StringConstants::GetNullNodeName(isolate), hiddenVal); + V8GetPrivateValue(isolate, thiz, V8StringConstants::GetNullNodePrivate(isolate), hiddenVal); if (hiddenVal.IsEmpty()) { auto node = reinterpret_cast(info.Data().As()->Value(v8::kExternalPointerTypeTagDefault)); - V8SetPrivateValue(isolate, thiz, V8StringConstants::GetNullNodeName(isolate), External::New(isolate, node, v8::kExternalPointerTypeTagDefault)); + V8SetPrivateValue(isolate, thiz, V8StringConstants::GetNullNodePrivate(isolate), External::New(isolate, node, v8::kExternalPointerTypeTagDefault)); auto funcTemplate = FunctionTemplate::New(isolate, MetadataNode::NullValueOfCallback); auto context = isolate->GetCurrentContext(); thiz->Delete(context, V8StringConstants::GetValueOf(isolate)); diff --git a/test-app/runtime/src/main/cpp/MetadataNode.h b/test-app/runtime/src/main/cpp/MetadataNode.h index 67ddb32cc..ddb0a1a7d 100644 --- a/test-app/runtime/src/main/cpp/MetadataNode.h +++ b/test-app/runtime/src/main/cpp/MetadataNode.h @@ -191,6 +191,8 @@ class MetadataNode { std::string m_name; std::string m_implType; bool m_isArray; + // Read on every Java method call; GetNodeType takes the reader's state lock. + bool m_isInterface; static std::string TNS_PREFIX; static MetadataReader s_metadataReader; diff --git a/test-app/runtime/src/main/cpp/MetadataReader.cpp b/test-app/runtime/src/main/cpp/MetadataReader.cpp index 3dc214260..fc0756411 100644 --- a/test-app/runtime/src/main/cpp/MetadataReader.cpp +++ b/test-app/runtime/src/main/cpp/MetadataReader.cpp @@ -245,9 +245,14 @@ MetadataTreeNode *MetadataReader::GetRoot() const { } uint8_t MetadataReader::GetNodeType(MetadataTreeNode *treeNode) { + uint8_t memoized = treeNode->type.load(std::memory_order_relaxed); + if (memoized != MetadataTreeNode::INVALID_TYPE) { + return memoized; + } + StateLock lock(m_stateMutex); - if (treeNode->type == MetadataTreeNode::INVALID_TYPE) { + if (treeNode->type.load(std::memory_order_relaxed) == MetadataTreeNode::INVALID_TYPE) { uint8_t nodeType; uint32_t offsetValue = treeNode->offsetValue; @@ -267,10 +272,10 @@ uint8_t MetadataReader::GetNodeType(MetadataTreeNode *treeNode) { nodeType = *(m_valueData + arrElemNode->offsetValue); } - treeNode->type = nodeType; + treeNode->type.store(nodeType, std::memory_order_relaxed); } - return treeNode->type; + return treeNode->type.load(std::memory_order_relaxed); } MetadataTreeNode *MetadataReader::GetOrCreateTreeNodeByName(const string &className) { @@ -396,11 +401,12 @@ MetadataTreeNode *MetadataReader::GetOrCreateTreeNodeByName(const string &classN if ((cKind == 'C') || (cKind == 'I')) { child->metadata = new string(part); - child->type = (cKind == 'C') ? MetadataTreeNode::CLASS - : MetadataTreeNode::INTERFACE; + uint8_t childType = (cKind == 'C') ? MetadataTreeNode::CLASS + : MetadataTreeNode::INTERFACE; if (name == "S") { - child->type |= MetadataTreeNode::STATIC; + childType |= MetadataTreeNode::STATIC; } + child->type.store(childType, std::memory_order_relaxed); getline(s, line); stringstream baseClassLine(line); @@ -412,11 +418,11 @@ MetadataTreeNode *MetadataReader::GetOrCreateTreeNodeByName(const string &classN auto baseClassNodeId = GetNodeId(baseClassTreeNode); child->offsetValue = m_valueLength; - m_valueData[m_valueLength++] = child->type; + m_valueData[m_valueLength++] = childType; *reinterpret_cast(m_valueData + m_valueLength) = baseClassNodeId; m_valueLength += sizeof(uint16_t); } else { - child->type = MetadataTreeNode::PACKAGE; + child->type.store(MetadataTreeNode::PACKAGE, std::memory_order_relaxed); } m_v.push_back(child); diff --git a/test-app/runtime/src/main/cpp/MetadataTreeNode.h b/test-app/runtime/src/main/cpp/MetadataTreeNode.h index e30cfb1ea..0baa73373 100644 --- a/test-app/runtime/src/main/cpp/MetadataTreeNode.h +++ b/test-app/runtime/src/main/cpp/MetadataTreeNode.h @@ -1,6 +1,8 @@ #ifndef TREENODE_H_ #define TREENODE_H_ +#include +#include #include #include @@ -16,7 +18,10 @@ struct MetadataTreeNode { std::vector* children; // std::string* metadata; - uint8_t type; + // Write-once memo: INVALID_TYPE until GetNodeType derives it from the + // immutable value data, so a reader that sees any other value can use it + // without the reader's state lock. + std::atomic type; static const uint8_t PACKAGE = 0; static const uint8_t CLASS = 1 << 0; diff --git a/test-app/runtime/src/main/cpp/MethodCache.cpp b/test-app/runtime/src/main/cpp/MethodCache.cpp index 97531ce2e..076e18c6f 100644 --- a/test-app/runtime/src/main/cpp/MethodCache.cpp +++ b/test-app/runtime/src/main/cpp/MethodCache.cpp @@ -142,7 +142,7 @@ string MethodCache::GetType(Isolate* isolate, const v8::Local& jsValu auto context = isolate->GetCurrentContext(); auto objVal = value->ToObject(context).ToLocalChecked(); Local nullNode; //out - V8GetPrivateValue(isolate, objVal, V8StringConstants::GetNullNodeName(isolate), nullNode); + V8GetPrivateValue(isolate, objVal, V8StringConstants::GetNullNodePrivate(isolate), nullNode); if (!nullNode.IsEmpty()) { auto treeNode = reinterpret_cast(nullNode.As()->Value(v8::kExternalPointerTypeTagDefault)); diff --git a/test-app/runtime/src/main/cpp/NumericCasts.cpp b/test-app/runtime/src/main/cpp/NumericCasts.cpp index ba1806292..40533fcda 100644 --- a/test-app/runtime/src/main/cpp/NumericCasts.cpp +++ b/test-app/runtime/src/main/cpp/NumericCasts.cpp @@ -24,7 +24,7 @@ void NumericCasts::CreateGlobalCastFunctions(Isolate* isolate, const Local& object) { auto ret = CastType::None; - auto key = ArgConverter::ConvertToV8String(isolate, s_castMarker); + auto key = V8StringConstants::GetCastMarkerPrivate(isolate); Local hidden; V8GetPrivateValue(isolate, object, key, hidden); if (!hidden.IsEmpty()) { @@ -361,7 +361,7 @@ void NumericCasts::MarkAsDoubleCallback(const v8::FunctionCallbackInfo& a } void NumericCasts::MarkJsObject(Isolate* isolate, const Local& object, CastType castType, const Local& value) { - auto key = ArgConverter::ConvertToV8String(isolate, s_castMarker); + auto key = V8StringConstants::GetCastMarkerPrivate(isolate); auto type = Integer::New(isolate, static_cast(castType)); V8SetPrivateValue(isolate, object, key, type); auto context = GetCreationContextOrCurrent(isolate, object); @@ -370,4 +370,3 @@ void NumericCasts::MarkJsObject(Isolate* isolate, const Local& object, C DEBUG_WRITE("MarkJsObject: Marking js object: %d with cast type: %d", object->GetIdentityHash(), castType); } -std::string NumericCasts::s_castMarker = "t::cast"; diff --git a/test-app/runtime/src/main/cpp/NumericCasts.h b/test-app/runtime/src/main/cpp/NumericCasts.h index d66ee69c5..a55687fed 100644 --- a/test-app/runtime/src/main/cpp/NumericCasts.h +++ b/test-app/runtime/src/main/cpp/NumericCasts.h @@ -54,7 +54,6 @@ class NumericCasts { static NumericCasts* GetThis(const v8::FunctionCallbackInfo& args); - static std::string s_castMarker; }; } diff --git a/test-app/runtime/src/main/cpp/Runtime.cpp b/test-app/runtime/src/main/cpp/Runtime.cpp index e8eb2a87a..db0173ba2 100644 --- a/test-app/runtime/src/main/cpp/Runtime.cpp +++ b/test-app/runtime/src/main/cpp/Runtime.cpp @@ -121,10 +121,15 @@ int Runtime::GetAndroidVersion() { Runtime::Runtime(JNIEnv* env, jobject runtime, int id) : m_id(id), + m_ownerThread(std::this_thread::get_id()), m_isolate(nullptr), m_lastUsedMemory(0), m_gcFunc(nullptr), 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); + m_runtime = env->NewGlobalRef(runtime); m_objectManager = new ObjectManager(m_runtime); m_state = std::make_unique(); @@ -144,6 +149,12 @@ Runtime::Runtime(JNIEnv* env, jobject runtime, int id) } 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; + } + Runtime* runtime = nullptr; { std::lock_guard lock(s_runtimeCacheMutex); @@ -302,6 +313,12 @@ 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; + } + /* * Idempotent backstop for the erase DestroyRuntime does: a bootstrap that * failed before PrepareV8Runtime ran never reached it. The isolate entry is @@ -505,7 +522,7 @@ jobject Runtime::RunScript(JNIEnv* _env, jobject obj, jstring scriptFile) { jobject Runtime::CallJSMethodNative(JNIEnv* _env, jobject obj, jint javaObjectID, jstring methodName, jint retType, jboolean isConstructor, - jobjectArray packagedArgs) { + const JavaCallArgs& args) { SET_PROFILER_FRAME(); auto isolate = m_isolate; @@ -535,9 +552,15 @@ jobject Runtime::CallJSMethodNative(JNIEnv* _env, jobject obj, DEBUG_WRITE("CallJSMethodNative called jsObject=%d", jsObject->GetIdentityHash()); - string method_name = ArgConverter::jstringToString(methodName); + auto jsMethodName = + ArgConverter::jstringToInternalizedV8String(m_isolate, env, methodName); auto jsResult = CallbackHandlers::CallJSMethod(m_isolate, env, jsObject, - method_name, packagedArgs); + jsMethodName, args); + + // Type::Null is the id of void.class: the Java caller discards the result. + if (retType == static_cast(Type::Null)) { + return nullptr; + } int classReturnType = retType; jobject javaObject = @@ -1093,6 +1116,10 @@ Isolate* Runtime::PrepareV8Runtime(const string& filesPath, this->m_napiEnv = NapiEnv::Create(context, m_eventLoop); s_currentRuntime = this; + s_currentRuntimeId = 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()); if (m_isMainThread) { // Releases any runtime waiting in ElectMainRuntime: the metadata tree and @@ -1131,6 +1158,7 @@ double Runtime::PerformanceNowMillis() { } void Runtime::DestroyRuntime() { + NS_CHECK(std::this_thread::get_id() == m_ownerThread); CrashBreadcrumbs::UnregisterRuntime(m_id); { std::lock_guard lock(s_runtimeCacheMutex); @@ -1169,6 +1197,8 @@ void Runtime::DestroyRuntime() { } if (s_currentRuntime == this) { s_currentRuntime = nullptr; + s_currentRuntimeId = -1; + JEnv::UnpinThreadEnv(); } // The events state holds v8::Global handles (backing event target, dispatch // closures and tracked promise rejections) - reset them while the isolate @@ -1265,6 +1295,7 @@ 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 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 22f0436fd..05e169578 100644 --- a/test-app/runtime/src/main/cpp/Runtime.h +++ b/test-app/runtime/src/main/cpp/Runtime.h @@ -2,6 +2,7 @@ #define RUNTIME_H_ #include "v8.h" +#include #include "JniLocalRef.h" #include "ObjectManager.h" #include "SimpleAllocator.h" @@ -25,6 +26,7 @@ typedef struct napi_env__* napi_env; namespace tns { +struct JavaCallArgs; class PromiseRejectionTracker; class RuntimeState; @@ -102,9 +104,8 @@ class Runtime { /* * The runtime whose home thread is the calling thread, or null. Set at - * the end of PrepareV8Runtime; may be stale after a Runtime destroyed - * on another thread, so consumers must validate through - * GetNapiEnvIfAlive rather than dereference it. + * the end of PrepareV8Runtime and cleared when it is torn down; a + * thread hosts at most one runtime and only that thread may destroy it. */ static Runtime* GetCurrentRuntime() { return s_currentRuntime; @@ -162,7 +163,7 @@ class Runtime { 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, jobjectArray packagedArgs); + jobject CallJSMethodNative(JNIEnv* _env, jobject obj, jint javaObjectID, jstring methodName, jint retType, jboolean isConstructor, const JavaCallArgs& args); void CreateJSInstanceNative(JNIEnv* _env, jobject obj, jobject javaObject, jint javaObjectID, jstring className); jint GenerateNewObjectId(JNIEnv* env, jobject obj); void AdjustAmountOfExternalAllocatedMemory(); @@ -316,6 +317,8 @@ class Runtime { Runtime(JNIEnv* env, jobject runtime, int id); int m_id; + + std::thread::id m_ownerThread; jobject m_runtime; v8::Isolate* m_isolate; @@ -424,6 +427,7 @@ class Runtime { static std::shared_ptr s_mainEventLoop; static thread_local Runtime* s_currentRuntime; + static thread_local int s_currentRuntimeId; static thread_local PendingIsolateSetup s_pendingIsolateSetup; #ifdef APPLICATION_IN_DEBUG diff --git a/test-app/runtime/src/main/cpp/V8GlobalHelpers.cpp b/test-app/runtime/src/main/cpp/V8GlobalHelpers.cpp index ba467dee0..05b795769 100644 --- a/test-app/runtime/src/main/cpp/V8GlobalHelpers.cpp +++ b/test-app/runtime/src/main/cpp/V8GlobalHelpers.cpp @@ -112,3 +112,31 @@ bool tns::V8SetPrivateValue(Isolate* isolate, const Local& obj, const Lo return res.FromMaybe(false); } + +// An absent private reads as undefined, so one GetPrivate replaces the +// HasPrivate + GetPrivate pair; callers never store undefined under these keys. +bool tns::V8GetPrivateValue(Isolate* isolate, const Local& obj, const Local& privateKey, Local& out) { + auto context = GetCreationContextOrCurrent(isolate, obj); + Local value; + if (!obj->GetPrivate(context, privateKey).ToLocal(&value)) { + throw tns::NativeScriptException("Failed to Get Private Value"); + } + + if (value->IsUndefined()) { + return false; + } + + out = value; + return true; +} + +bool tns::V8SetPrivateValue(Isolate* isolate, const Local& obj, const Local& privateKey, const Local& value) { + auto context = GetCreationContextOrCurrent(isolate, obj); + auto res = obj->SetPrivate(context, privateKey, value); + + if (res.IsNothing()) { + throw tns::NativeScriptException("Failed to Set Private Value"); + } + + return res.FromMaybe(false); +} diff --git a/test-app/runtime/src/main/cpp/V8GlobalHelpers.h b/test-app/runtime/src/main/cpp/V8GlobalHelpers.h index 6483574b8..95af8a002 100644 --- a/test-app/runtime/src/main/cpp/V8GlobalHelpers.h +++ b/test-app/runtime/src/main/cpp/V8GlobalHelpers.h @@ -19,7 +19,11 @@ v8::Local GetCreationContextOrCurrent(v8::Isolate* isolate, const v bool V8GetPrivateValue(v8::Isolate* isolate, const v8::Local& obj, const v8::Local& propName, v8::Local& out); +bool V8GetPrivateValue(v8::Isolate* isolate, const v8::Local& obj, const v8::Local& privateKey, v8::Local& out); + bool V8SetPrivateValue(v8::Isolate* isolate, const v8::Local& obj, const v8::Local& propName, const v8::Local& value); + +bool V8SetPrivateValue(v8::Isolate* isolate, const v8::Local& obj, const v8::Local& privateKey, const v8::Local& value); } #endif /* V8GLOBALHELPERS_H_ */ diff --git a/test-app/runtime/src/main/cpp/V8StringConstants.cpp b/test-app/runtime/src/main/cpp/V8StringConstants.cpp index 897f68a52..451b503f5 100644 --- a/test-app/runtime/src/main/cpp/V8StringConstants.cpp +++ b/test-app/runtime/src/main/cpp/V8StringConstants.cpp @@ -40,6 +40,14 @@ Local V8StringConstants::GetNullNodeName(Isolate* isolate) { return Local::New(isolate, *consts->NULL_NODE_NAME_PERSISTENT); } +Local V8StringConstants::GetNullNodePrivate(Isolate* isolate) { + return GetConstantsForIsolate(isolate)->NULL_NODE_PRIVATE.Get(isolate); +} + +Local V8StringConstants::GetCastMarkerPrivate(Isolate* isolate) { + return GetConstantsForIsolate(isolate)->CAST_MARKER_PRIVATE.Get(isolate); +} + Local V8StringConstants::GetIsPrototypeImplementationObject(Isolate* isolate) { auto consts = GetConstantsForIsolate(isolate); @@ -134,6 +142,7 @@ const string V8StringConstants::CLASS_IMPLEMENTATION_OBJECT = "t::ClassImplement const string V8StringConstants::EXTEND = "extend"; const string V8StringConstants::NULL_OBJECT = "null"; const string V8StringConstants::NULL_NODE_NAME = "nullNode"; +const string V8StringConstants::CAST_MARKER = "t::cast"; const string V8StringConstants::IS_PROTOTYPE_IMPLEMENTATION_OBJECT = "__isPrototypeImplementationObject"; const string V8StringConstants::NATIVE_EXCEPTION = "nativeException"; const string V8StringConstants::STACK_TRACE = "stackTrace"; diff --git a/test-app/runtime/src/main/cpp/V8StringConstants.h b/test-app/runtime/src/main/cpp/V8StringConstants.h index 0b895bacc..6d1345a1c 100644 --- a/test-app/runtime/src/main/cpp/V8StringConstants.h +++ b/test-app/runtime/src/main/cpp/V8StringConstants.h @@ -47,6 +47,10 @@ class V8StringConstants { static v8::Local GetImplementationObject(v8::Isolate* isolate); + static v8::Local GetNullNodePrivate(v8::Isolate* isolate); + + static v8::Local GetCastMarkerPrivate(v8::Isolate* isolate); + static const std::string CLASS_IMPLEMENTATION_OBJECT; static const std::string DEBUG_NAME; static const std::string EXTEND; @@ -67,6 +71,7 @@ class V8StringConstants { static const std::string UNCAUGHT_ERROR; static const std::string DISCARDED_ERROR; static const std::string IMPLEMENTATION_OBJECT; + static const std::string CAST_MARKER; /* * The structure contains Persistent handles to the most commonly used v8 String values; @@ -146,6 +151,12 @@ class V8StringConstants { str = String::NewFromUtf8(isolate, IMPLEMENTATION_OBJECT.c_str()).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(); + CAST_MARKER_PRIVATE.Reset(isolate, Private::ForApi(isolate, str)); }; ~PerIsolateV8Constants() { @@ -205,6 +216,8 @@ class V8StringConstants { v8::Persistent* UNCAUGHT_ERROR_PERSISTENT = nullptr; v8::Persistent* DISCARDED_ERROR_PERSISTENT = nullptr; v8::Persistent* IMPLEMENTATION_OBJECT_PERSISTENT = nullptr; + v8::Global NULL_NODE_PRIVATE; + v8::Global CAST_MARKER_PRIVATE; }; private: diff --git a/test-app/runtime/src/main/cpp/WorkerWrapper.cpp b/test-app/runtime/src/main/cpp/WorkerWrapper.cpp index b9019eb16..ae349b282 100644 --- a/test-app/runtime/src/main/cpp/WorkerWrapper.cpp +++ b/test-app/runtime/src/main/cpp/WorkerWrapper.cpp @@ -786,6 +786,7 @@ void WorkerWrapper::BackgroundLooper(std::shared_ptr self) { // ART aborts if a native thread exits while still attached. This must be // the very last JNI-touching action on this thread. + JEnv::UnpinThreadEnv(); jvm->DetachCurrentThread(); } 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 71800c0d9..6a85ee825 100644 --- a/test-app/runtime/src/main/cpp/com_tns_Runtime.cpp +++ b/test-app/runtime/src/main/cpp/com_tns_Runtime.cpp @@ -2,6 +2,7 @@ #include "Runtime.h" #include "NativeScriptException.h" #include "CallbackHandlers.h" +#include "ArgConverter.h" #include #include @@ -92,6 +93,7 @@ extern "C" JNIEXPORT void Java_com_tns_Runtime_setManualInstrumentationModeLegac } extern "C" JNIEXPORT void Java_com_tns_Runtime_initNativeScript(JNIEnv* _env, jobject obj, jint runtimeId, jstring filesPath, jstring nativeLibDir, jboolean verboseLoggingEnabled, jboolean isDebuggable, jstring packageName, jobjectArray args, jstring callingDir, jint maxLogcatObjectSize, jboolean forceLog) { + JEnv::EntryScope envScope(_env); try { Runtime::Init(_env, obj, runtimeId, filesPath, nativeLibDir, verboseLoggingEnabled, isDebuggable, packageName, args, callingDir, maxLogcatObjectSize, forceLog); } catch (NativeScriptException& e) { @@ -126,6 +128,7 @@ Runtime* TryGetRuntime(int runtimeId) { } extern "C" JNIEXPORT void Java_com_tns_Runtime_runModule(JNIEnv* _env, jobject obj, jint runtimeId, jstring scriptFile) { + JEnv::EntryScope envScope(_env); auto runtime = TryGetRuntime(runtimeId); if (runtime == nullptr) { return; @@ -154,6 +157,7 @@ extern "C" JNIEXPORT void Java_com_tns_Runtime_runModule(JNIEnv* _env, jobject o } extern "C" JNIEXPORT jobject Java_com_tns_Runtime_runScript(JNIEnv* _env, jobject obj, jint runtimeId, jstring scriptFile) { + JEnv::EntryScope envScope(_env); jobject result = nullptr; auto runtime = TryGetRuntime(runtimeId); @@ -184,7 +188,8 @@ 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, jobjectArray packagedArgs) { +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) { + JEnv::EntryScope envScope(_env); jobject result = nullptr; auto runtime = TryGetRuntime(runtimeId); @@ -200,7 +205,7 @@ extern "C" JNIEXPORT jobject Java_com_tns_Runtime_callJSMethodNative(JNIEnv* _en v8::Context::Scope context_scope(context); try { - result = runtime->CallJSMethodNative(_env, obj, javaObjectID, methodName, retType, isConstructor, packagedArgs); + result = runtime->CallJSMethodNative(_env, obj, javaObjectID, methodName, retType, isConstructor, JavaCallArgs{argTypes, argValues, argRefs}); } catch (NativeScriptException& e) { e.ReThrowToJava(); } catch (std::exception e) { @@ -216,6 +221,7 @@ extern "C" JNIEXPORT jobject Java_com_tns_Runtime_callJSMethodNative(JNIEnv* _en } extern "C" JNIEXPORT void Java_com_tns_Runtime_createJSInstanceNative(JNIEnv* _env, jobject obj, jint runtimeId, jobject javaObject, jint javaObjectID, jstring className) { + JEnv::EntryScope envScope(_env); auto runtime = TryGetRuntime(runtimeId); if (runtime == nullptr) { return; 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 84c70dc36..5bcf17000 100644 --- a/test-app/runtime/src/main/java/com/tns/Runtime.java +++ b/test-app/runtime/src/main/java/com/tns/Runtime.java @@ -42,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, Object... packagedArgs) throws NativeScriptException; + private native Object callJSMethodNative(int runtimeId, int javaObjectID, String methodName, int retType, boolean isConstructor, int[] argTypes, long[] argValues, Object[] argRefs) throws NativeScriptException; private native void createJSInstanceNative(int runtimeId, Object javaObject, int javaObjectID, String canonicalName); @@ -1273,36 +1273,89 @@ private static String describeInstance(Object javaObject) { return (javaObject == null) ? "null" : "an instance of " + javaObject.getClass().getName(); } - // Packages args in format (typeID, value, null) except for objects where it is - // (typeid, javaObjectID, canonicalName) - // if javaObject has no javaObjecID meaning javascript object does not - // exists for this object we assign one. - private Object[] packageArgs(Object... args) { - int len = (args != null) ? (args.length * 3) : 0; - Object[] packagedArgs = new Object[len]; - - if (len > 0) { - int jsArgsIndex = 0; - - for (int i = 0; i < args.length; i++) { - Object value = args[i]; - int typeId = TypeIDs.GetObjectTypeId(value); - String javaClassPath = null; - - if (typeId == TypeIDs.JsObject) { - javaClassPath = value.getClass().getName(); - value = getOrCreateJavaObjectID(value); + /* + * 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). + */ + private static final class PackagedArgs { + final int[] types; + final long[] values; + final Object[] refs; + + PackagedArgs(int[] types, long[] values, Object[] refs) { + this.types = types; + this.values = values; + this.refs = refs; + } + } + + private static final PackagedArgs NO_ARGS = new PackagedArgs(new int[0], new long[0], null); + + // If a javaObject has no javaObjectID, meaning no JavaScript object exists + // for it yet, it is assigned one. + private PackagedArgs packageArgs(Object... args) { + int len = (args != null) ? args.length : 0; + if (len == 0) { + return NO_ARGS; + } + + int[] types = new int[len]; + long[] values = new long[len]; + Object[] refs = null; + + for (int i = 0; i < len; i++) { + Object value = args[i]; + int typeId; + + if (value == null) { + typeId = TypeIDs.Null; + } else if (value instanceof Integer) { + typeId = TypeIDs.Int; + values[i] = (Integer) value; + } else if (value instanceof Boolean) { + typeId = TypeIDs.Boolean; + values[i] = ((Boolean) value) ? 1 : 0; + } else if (value instanceof Character) { + typeId = TypeIDs.Char; + values[i] = (Character) value; + } else if (value instanceof Byte) { + typeId = TypeIDs.Byte; + values[i] = (Byte) value; + } else if (value instanceof Short) { + typeId = TypeIDs.Short; + values[i] = (Short) value; + } else if (value instanceof Long) { + typeId = TypeIDs.Long; + values[i] = (Long) value; + } else if (value instanceof Float) { + typeId = TypeIDs.Float; + values[i] = Float.floatToRawIntBits((Float) value); + } else if (value instanceof Double) { + typeId = TypeIDs.Double; + values[i] = Double.doubleToRawLongBits((Double) value); + } else if (value instanceof String) { + typeId = TypeIDs.string; + if (refs == null) { + refs = new Object[len]; } - - packagedArgs[jsArgsIndex++] = typeId; - - packagedArgs[jsArgsIndex++] = value; - - packagedArgs[jsArgsIndex++] = javaClassPath; + refs[i] = value; + } else { + typeId = TypeIDs.JsObject; + if (refs == null) { + refs = new Object[len]; + } + refs[i] = value.getClass().getName(); + values[i] = getOrCreateJavaObjectID(value); } + + types[i] = typeId; } - return packagedArgs; + return new PackagedArgs(types, values, refs); } static Class getClassForName(String className) { @@ -1406,9 +1459,9 @@ private Object dispatchCallJSMethodNative(final Object javaObject, final String if (enableMultithreadedJavascript || isWorkThread) { int javaObjectID = resolveJavaObjectID(javaObject, methodName); - Object[] packagedArgs = packageArgs(tmpArgs); + PackagedArgs packagedArgs = packageArgs(tmpArgs); try { - ret = callJSMethodNative(getRuntimeId(), javaObjectID, methodName, returnType, isConstructor, packagedArgs); + ret = callJSMethodNative(getRuntimeId(), javaObjectID, methodName, returnType, isConstructor, packagedArgs.types, packagedArgs.values, packagedArgs.refs); } catch (NativeScriptException e) { // With multithreaded JS the owner can be torn down between the // routing check and the native call, which then throws @@ -1446,8 +1499,8 @@ public void run() { arr[2] = e; return; } - final Object[] packagedArgs = packageArgs(tmpArgs); - arr[0] = callJSMethodNative(getRuntimeId(), javaObjectID, methodName, returnType, isCtor, packagedArgs); + final PackagedArgs packagedArgs = packageArgs(tmpArgs); + arr[0] = callJSMethodNative(getRuntimeId(), javaObjectID, methodName, returnType, isCtor, packagedArgs.types, packagedArgs.values, packagedArgs.refs); } catch (NativeScriptException e) { if (discardUncaughtJsExceptions && !e.isEscapedFromJs()) { String errorMessage = "Error on \"" + Thread.currentThread().getName() + "\" thread for callJSMethodNative\n";