diff --git a/test-app/app/src/main/assets/app/mainpage.js b/test-app/app/src/main/assets/app/mainpage.js index 0a8c79b00..2bf269c7c 100644 --- a/test-app/app/src/main/assets/app/mainpage.js +++ b/test-app/app/src/main/assets/app/mainpage.js @@ -49,6 +49,7 @@ require("./tests/requireExceptionTests"); require("./tests/java-array-test"); require("./tests/field-access-test"); require("./tests/byte-buffer-test"); +require("./tests/shared-array-buffer-test"); require("./tests/dex-interface-implementation"); require("./tests/testInterfaceImplementation"); require("./tests/testRuntimeImplementedAPIs"); diff --git a/test-app/app/src/main/assets/app/shared b/test-app/app/src/main/assets/app/shared index 0e030139e..3a262b979 160000 --- a/test-app/app/src/main/assets/app/shared +++ b/test-app/app/src/main/assets/app/shared @@ -1 +1 @@ -Subproject commit 0e030139e7273975106cbedd69681f55d2c2fbf2 +Subproject commit 3a262b979c6b84cdfe69cd495436a7088d016505 diff --git a/test-app/app/src/main/assets/app/tests/shared-array-buffer-test.js b/test-app/app/src/main/assets/app/tests/shared-array-buffer-test.js new file mode 100644 index 000000000..759d5223c --- /dev/null +++ b/test-app/app/src/main/assets/app/tests/shared-array-buffer-test.js @@ -0,0 +1,91 @@ +describe("Tests SharedArrayBuffer conversion", function () { + it("should pass a SharedArrayBuffer to a Java method expecting ByteBuffer", function () { + var sab = new SharedArrayBuffer(8); + var view = new Uint8Array(sab); + for (var i = 0; i < 8; i++) { + view[i] = i + 1; + } + + // resolves the ByteBuffer.put(ByteBuffer) overload and copies from the + // direct buffer created over the SharedArrayBuffer's memory + var bb = java.nio.ByteBuffer.allocateDirect(8); + bb.put(sab); + bb.flip(); + + var roundTripped = new Uint8Array(ArrayBuffer.from(bb)); + for (var i = 0; i < 8; i++) { + expect(roundTripped[i]).toBe(i + 1); + } + }); + + it("should respect byteOffset and length of typed array views over a SharedArrayBuffer", function () { + var sab = new SharedArrayBuffer(16); + var full = new Uint8Array(sab); + for (var i = 0; i < 16; i++) { + full[i] = i; + } + + var slice = new Uint8Array(sab, 4, 8); // bytes 4..11 + + var bb = java.nio.ByteBuffer.allocateDirect(8); + bb.put(slice); + bb.flip(); + + var roundTripped = new Uint8Array(ArrayBuffer.from(bb)); + for (var i = 0; i < 8; i++) { + expect(roundTripped[i]).toBe(i + 4); + } + }); + + it("should share memory between the SharedArrayBuffer and the Java buffer (no copy)", function () { + var sab = new SharedArrayBuffer(4); + var view = new Uint8Array(sab); + view[0] = 42; + + // the holder keeps the direct ByteBuffer the runtime created over the + // SharedArrayBuffer's memory, so Java reads/writes go to the same bytes + var holder = new com.tns.tests.ByteBufferHolder(); + holder.hold(sab); + expect(holder.get(0)).toBe(42); + + // JS mutations after the call are visible through the Java buffer + view[0] = 99; + expect(holder.get(0)).toBe(99); + + // and Java mutations are visible through the SharedArrayBuffer + holder.put(1, 77); + expect(view[1]).toBe(77); + }); + + it("should share a SharedArrayBuffer's memory with a worker and a Java buffer at once", function (done) { + var sab = new SharedArrayBuffer(4); + var view = new Uint8Array(sab); + view[0] = 0; + + var holder = new com.tns.tests.ByteBufferHolder(); + holder.hold(sab); + + var worker = new Worker("../shared/Workers/EvalWorker.js"); + worker.postMessage({ + value: sab, + eval: "var v = new Uint8Array(value); v[0] = 42; v[3] = 99; postMessage('written');" + }); + // fail fast instead of waiting for the jasmine timeout if the worker + // errors before posting back + worker.onerror = function (e) { + expect("worker error: " + e.message).toBe(""); + worker.terminate(); + done(); + }; + worker.onmessage = function (msg) { + expect(msg.data).toBe("written"); + // the worker's write is visible both to this isolate and to Java + expect(view[0]).toBe(42); + expect(view[3]).toBe(99); + expect(holder.get(0)).toBe(42); + expect(holder.get(3)).toBe(99); + worker.terminate(); + done(); + }; + }); +}); diff --git a/test-app/app/src/main/java/com/tns/tests/ByteBufferHolder.java b/test-app/app/src/main/java/com/tns/tests/ByteBufferHolder.java new file mode 100644 index 000000000..8baa46e41 --- /dev/null +++ b/test-app/app/src/main/java/com/tns/tests/ByteBufferHolder.java @@ -0,0 +1,24 @@ +package com.tns.tests; + +import java.nio.ByteBuffer; + +/* + * Used by shared-array-buffer-test.js to verify that JS (Shared)ArrayBuffers + * marshal to Java as direct ByteBuffers over the same memory. + */ +public class ByteBufferHolder { + private ByteBuffer buffer; + + public ByteBuffer hold(ByteBuffer buffer) { + this.buffer = buffer; + return this.buffer; + } + + public byte get(int index) { + return buffer.get(index); + } + + public void put(int index, byte value) { + buffer.put(index, value); + } +} diff --git a/test-app/runtime/CMakeLists.txt b/test-app/runtime/CMakeLists.txt index 2eaeaa7ee..2f90b7eb4 100644 --- a/test-app/runtime/CMakeLists.txt +++ b/test-app/runtime/CMakeLists.txt @@ -99,6 +99,7 @@ add_library( src/main/cpp/ArrayHelper.cpp src/main/cpp/AssetExtractor.cpp src/main/cpp/CallbackHandlers.cpp + src/main/cpp/ConcurrentQueue.cpp src/main/cpp/Constants.cpp src/main/cpp/DirectBuffer.cpp src/main/cpp/FieldAccessor.cpp @@ -112,6 +113,7 @@ add_library( src/main/cpp/JsArgToArrayConverter.cpp src/main/cpp/JSONObjectHelper.cpp src/main/cpp/Logger.cpp + src/main/cpp/LooperTasks.cpp src/main/cpp/ManualInstrumentation.cpp src/main/cpp/MessageLoopTimer.cpp src/main/cpp/MetadataMethodInfo.cpp @@ -135,6 +137,8 @@ add_library( src/main/cpp/V8GlobalHelpers.cpp src/main/cpp/V8StringConstants.cpp src/main/cpp/WeakRef.cpp + src/main/cpp/WorkerMessage.cpp + src/main/cpp/WorkerWrapper.cpp src/main/cpp/Timers.cpp src/main/cpp/com_tns_AssetExtractor.cpp src/main/cpp/com_tns_Runtime.cpp diff --git a/test-app/runtime/src/main/cpp/CallbackHandlers.cpp b/test-app/runtime/src/main/cpp/CallbackHandlers.cpp index 709359a76..5ce450fef 100644 --- a/test-app/runtime/src/main/cpp/CallbackHandlers.cpp +++ b/test-app/runtime/src/main/cpp/CallbackHandlers.cpp @@ -17,6 +17,8 @@ #include "MethodCache.h" #include "SimpleProfiler.h" #include "Runtime.h" +#include "WorkerMessage.h" +#include "WorkerWrapper.h" #include #include @@ -56,11 +58,6 @@ void CallbackHandlers::Init(Isolate *isolate) { "()V"); assert(ENABLE_VERBOSE_LOGGING_METHOD_ID != nullptr); - INIT_WORKER_METHOD_ID = env.GetStaticMethodID(RUNTIME_CLASS, "initWorker", - "(Ljava/lang/String;Ljava/lang/String;I)V"); - - assert(INIT_WORKER_METHOD_ID != nullptr); - MetadataNode::Init(isolate); MethodCache::Init(); @@ -989,6 +986,72 @@ jobjectArray CallbackHandlers::GetJavaStringArray(JEnv &env, int length) { return (jobjectArray) env.NewGlobalRef(tmpArr); } +/* + * Resolves the `androidPriority` Worker option to an android.os.Process + * thread priority (nice value). Accepts the THREAD_PRIORITY_* names in + * camelCase or a raw nice value clamped to [-20, 19]. + * Defaults to THREAD_PRIORITY_BACKGROUND (10), the previously hardcoded value. + */ +static int GetWorkerThreadPriority(Isolate *isolate, Local context, + const v8::FunctionCallbackInfo &args) { + const int defaultPriority = 10; // android.os.Process.THREAD_PRIORITY_BACKGROUND + + if (args.Length() < 2 || !args[1]->IsObject()) { + return defaultPriority; + } + + auto options = args[1].As(); + Local value; + if (!options->Get(context, ArgConverter::ConvertToV8String(isolate, "androidPriority")) + .ToLocal(&value) || + value->IsNullOrUndefined()) { + return defaultPriority; + } + + if (value->IsNumber()) { + int priority = value->Int32Value(context).FromMaybe(defaultPriority); + if (priority < -20) { + priority = -20; + } else if (priority > 19) { + priority = 19; + } + return priority; + } + + if (value->IsString()) { + auto name = ArgConverter::ConvertToString(value.As()); + if (name == "lowest") { + return 19; + } else if (name == "background") { + return 10; + } else if (name == "lessFavorable") { + return 1; + } else if (name == "default") { + return 0; + } else if (name == "moreFavorable") { + return -1; + } else if (name == "foreground") { + return -2; + } else if (name == "display") { + return -4; + } else if (name == "urgentDisplay") { + return -8; + } else if (name == "video") { + return -10; + } else if (name == "audio") { + return -16; + } else if (name == "urgentAudio") { + return -19; + } + } + + throw NativeScriptException( + "Invalid value for the Worker 'androidPriority' option. Expected one of: " + "'lowest', 'background', 'lessFavorable', 'default', 'moreFavorable', " + "'foreground', 'display', 'urgentDisplay', 'video', 'audio', 'urgentAudio' " + "or a number between -20 and 19."); +} + void CallbackHandlers::NewThreadCallback(const v8::FunctionCallbackInfo &args) { try { if (!args.IsConstructCall()) { @@ -1043,8 +1106,8 @@ void CallbackHandlers::NewThreadCallback(const v8::FunctionCallbackInfoGetFrame( - isolate, 0)->GetScriptName(); - auto currentExecutingScriptNameStr = ArgConverter::ConvertToString( - currentExecutingScriptName.As()); - auto lastForwardSlash = currentExecutingScriptNameStr.find_last_of("/"); - auto currentDir = currentExecutingScriptNameStr.substr(0, lastForwardSlash + 1); - string fileSchema("file://"); - if (currentDir.compare(0, fileSchema.length(), fileSchema) == 0) { - currentDir = currentDir.substr(fileSchema.length()); + /* + * Relative worker paths are resolved against the calling module's + * directory. The caller may have no script name (e.g. eval'd code) or + * the script may not be found there - in both cases fall back to + * app-root-relative resolution, mirroring the iOS runtime. + */ + std::string currentDir = Constants::APP_ROOT_FOLDER_PATH; + auto stack = StackTrace::CurrentStackTrace(isolate, 1, StackTrace::kScriptName); + if (!stack.IsEmpty() && stack->GetFrameCount() > 0) { + auto currentExecutingScriptName = stack->GetFrame(isolate, 0)->GetScriptName(); + auto currentExecutingScriptNameStr = ArgConverter::ConvertToString( + currentExecutingScriptName); + auto lastForwardSlash = currentExecutingScriptNameStr.find_last_of("/"); + if (lastForwardSlash != std::string::npos) { + auto callerDir = currentExecutingScriptNameStr.substr(0, lastForwardSlash + 1); + string fileSchema("file://"); + if (callerDir.compare(0, fileSchema.length(), fileSchema) == 0) { + callerDir = callerDir.substr(fileSchema.length()); + } + currentDir = callerDir; + } } - // Will throw if path is invalid or doesn't exist - ModuleInternal::CheckFileExists(isolate, resolvedPath, currentDir); + // Will throw if the path is invalid or the file doesn't exist + try { + ModuleInternal::CheckFileExists(isolate, resolvedPath, currentDir); + } catch (NativeScriptException& e) { + if (currentDir == Constants::APP_ROOT_FOLDER_PATH) { + throw; + } + // not found next to the caller - retry against the app root + ModuleInternal::CheckFileExists(isolate, resolvedPath, + Constants::APP_ROOT_FOLDER_PATH); + currentDir = Constants::APP_ROOT_FOLDER_PATH; + } - auto workerId = nextWorkerId++; + auto workerId = WorkerWrapper::NextWorkerId(); V8SetPrivateValue(isolate, thiz, ArgConverter::ConvertToV8String(isolate, "workerId"), Number::New(isolate, workerId)); - auto persistentWorker = new Persistent(isolate, thiz); + // Resolve the jclass/jmethodID handles the worker thread will need, + // here on the main thread where class loading is safe. + WorkerWrapper::EnsureJniCached(); - id2WorkerMap.emplace(workerId, persistentWorker); + auto wrapper = std::make_shared(isolate, workerId, resolvedPath, + currentDir, priority, thiz); + WorkerWrapper::Insert(workerId, wrapper); DEBUG_WRITE("Called Worker constructor id=%d", workerId); - JEnv env; - Local filePathStr = ArgConverter::ConvertToV8String(isolate, resolvedPath); - JniLocalRef filePath(ArgConverter::ConvertToJavaString(filePathStr)); - JniLocalRef dirPath(env.NewStringUTF(currentDir.c_str())); - - env.CallStaticVoidMethod(RUNTIME_CLASS, INIT_WORKER_METHOD_ID, (jstring) filePath, - (jstring) dirPath, workerId); + wrapper->Start(); } catch (NativeScriptException &e) { e.ReThrowToV8(); } catch (std::exception e) { @@ -1122,20 +1204,25 @@ CallbackHandlers::WorkerObjectPostMessageCallback(const v8::FunctionCallbackInfo jsId); auto context = isolate->GetCurrentContext(); - auto objToStringify = args[0]->ToObject(context).ToLocalChecked(); - std::string msg = tns::JsonStringifyObject(isolate, objToStringify, false); - // get worker's ID that is associated on the other side - in Java + // get worker's ID that is associated with the WorkerWrapper auto id = jsId->Int32Value(context).ToChecked(); - JEnv env; - auto mId = env.GetStaticMethodID(RUNTIME_CLASS, "sendMessageFromMainToWorker", - "(ILjava/lang/String;)V"); + auto wrapper = WorkerWrapper::GetById(id); + if (wrapper == nullptr || wrapper->IsTerminating() || wrapper->IsClosing()) { + DEBUG_WRITE( + "MAIN: WorkerObjectPostMessageCallback - worker(id=%d) is terminated or closing. No message will be sent.", + id); + return; + } - jstring jmsg = env.NewStringUTF(msg.c_str()); - JniLocalRef jmsgRef(jmsg); + auto message = std::make_shared(); + if (message->Serialize(isolate, context, args[0]).IsNothing()) { + // a DataCloneError is already pending on the isolate + return; + } - env.CallStaticVoidMethod(RUNTIME_CLASS, mId, id, (jstring) jmsgRef); + wrapper->PostMessage(message); DEBUG_WRITE( "MAIN: WorkerObjectPostMessageCallback called postMessage on Worker object(id=%d)", @@ -1153,55 +1240,6 @@ CallbackHandlers::WorkerObjectPostMessageCallback(const v8::FunctionCallbackInfo } } -void CallbackHandlers::WorkerGlobalOnMessageCallback(Isolate *isolate, jstring message) { - auto context = isolate->GetCurrentContext(); - - try { - auto globalObject = context->Global(); - - TryCatch tc(isolate); - - auto callback = globalObject->Get(context, ArgConverter::ConvertToV8String(isolate, - "onmessage")).ToLocalChecked(); - auto isEmpty = callback.IsEmpty(); - auto isFunction = callback->IsFunction(); - - if (!isEmpty && isFunction) { - auto msgString = ArgConverter::jstringToV8String(isolate, message).As(); - Local msg; - JSON::Parse(context, msgString).ToLocal(&msg); - - auto obj = Object::New(isolate); - obj->DefineOwnProperty(isolate->GetCurrentContext(), - ArgConverter::ConvertToV8String(isolate, "data"), msg, - PropertyAttribute::ReadOnly); - Local args1[] = {obj}; - - auto func = callback.As(); - - func->Call(context, Undefined(isolate), 1, args1); - } else { - DEBUG_WRITE( - "WORKER: WorkerGlobalOnMessageCallback couldn't fire a worker's `onmessage` callback because it isn't implemented!"); - } - - if (tc.HasCaught()) { - // TODO: Pete: Will catch exceptions thrown artificially in postMessage callbacks inside of 'onmessage' implementation - CallWorkerScopeOnErrorHandle(isolate, tc); - } - } catch (NativeScriptException &ex) { - ex.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 CallbackHandlers::WorkerGlobalPostMessageCallback(const v8::FunctionCallbackInfo &args) { auto isolate = args.GetIsolate(); @@ -1222,83 +1260,23 @@ CallbackHandlers::WorkerGlobalPostMessageCallback(const v8::FunctionCallbackInfo return; } - auto context = isolate->GetCurrentContext(); - auto objToStringify = args[0]->ToObject(context).ToLocalChecked(); - std::string msg = tns::JsonStringifyObject(isolate, objToStringify, false); - - JEnv env; - auto mId = env.GetStaticMethodID(RUNTIME_CLASS, "sendMessageFromWorkerToMain", - "(Ljava/lang/String;)V"); - - auto jmsg = env.NewStringUTF(msg.c_str()); - JniLocalRef jmsgRef(jmsg); - - env.CallStaticVoidMethod(RUNTIME_CLASS, mId, (jstring) jmsgRef); - - DEBUG_WRITE("WORKER: WorkerGlobalPostMessageCallback called."); - } catch (NativeScriptException &ex) { - ex.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 -CallbackHandlers::WorkerObjectOnMessageCallback(Isolate *isolate, jint workerId, jstring message) { - try { - auto workerFound = CallbackHandlers::id2WorkerMap.find(workerId); - - if (workerFound == CallbackHandlers::id2WorkerMap.end()) { - // TODO: Pete: Throw exception + auto wrapper = WorkerWrapper::FromIsolate(isolate); + if (wrapper == nullptr || wrapper->IsTerminating()) { DEBUG_WRITE( - "MAIN: WorkerObjectOnMessageCallback no worker instance was found with workerId=%d.", - workerId); + "WORKER: WorkerGlobalPostMessageCallback - worker is terminating. No message will be sent."); return; } - auto workerPersistent = workerFound->second; - - if (workerPersistent->IsEmpty()) {// Object has been collected - DEBUG_WRITE( - "MAIN: WorkerObjectOnMessageCallback couldn't fire a worker(id=%d) object's `onmessage` callback because the worker has been Garbage Collected.", - workerId); - CallbackHandlers::id2WorkerMap.erase(workerId); + auto context = isolate->GetCurrentContext(); + auto message = std::make_shared(); + if (message->Serialize(isolate, context, args[0]).IsNothing()) { + // a DataCloneError is already pending on the isolate return; } - auto worker = Local::New(isolate, *workerPersistent); - - auto context = isolate->GetCurrentContext(); - auto callback = worker->Get(context, ArgConverter::ConvertToV8String(isolate, - "onmessage")).ToLocalChecked(); - auto isEmpty = callback.IsEmpty(); - auto isFunction = callback->IsFunction(); - - if (!isEmpty && isFunction) { - auto msgString = ArgConverter::jstringToV8String(isolate, message).As(); - Local msg; - JSON::Parse(context, msgString).ToLocal(&msg); - - auto obj = Object::New(isolate); - obj->DefineOwnProperty(context, - ArgConverter::ConvertToV8String(isolate, "data"), msg, - PropertyAttribute::ReadOnly); - Local args1[] = {obj}; + wrapper->PostMessageToParent(message); - auto func = callback.As(); - - func->Call(context, Undefined(isolate), 1, args1); - } else { - DEBUG_WRITE( - "MAIN: WorkerObjectOnMessageCallback couldn't fire a worker(id=%d) object's `onmessage` callback because it isn't implemented.", - workerId); - } + DEBUG_WRITE("WORKER: WorkerGlobalPostMessageCallback called."); } catch (NativeScriptException &ex) { ex.ReThrowToV8(); } catch (std::exception e) { @@ -1347,14 +1325,13 @@ CallbackHandlers::WorkerObjectTerminateCallback(const v8::FunctionCallbackInfoTerminate(); + } - // Remove persistent handle from id2WorkerMap - CallbackHandlers::ClearWorkerPersistent(id); + // Reset the persistent Worker object handle and drop the registry entry + WorkerWrapper::ClearWorkerOnParent(id); } catch (NativeScriptException &ex) { ex.ReThrowToV8(); } catch (std::exception e) { @@ -1414,11 +1391,10 @@ void CallbackHandlers::WorkerGlobalCloseCallback(const v8::FunctionCallbackInfo< CallWorkerScopeOnErrorHandle(isolate, tc); } - JEnv env; - auto mId = env.GetStaticMethodID(RUNTIME_CLASS, "workerScopeClose", - "()V"); - - env.CallStaticVoidMethod(RUNTIME_CLASS, mId); + auto wrapper = WorkerWrapper::FromIsolate(isolate); + if (wrapper != nullptr) { + wrapper->Close(); + } } catch (NativeScriptException &ex) { ex.ReThrowToV8(); } catch (std::exception e) { @@ -1432,8 +1408,54 @@ void CallbackHandlers::WorkerGlobalCloseCallback(const v8::FunctionCallbackInfo< } } +/* + * Extracts message/filename/stack/line info from a TryCatch into plain + * strings that can safely cross to the main thread. Robust against empty + * v8 messages (e.g. when execution was terminated). + */ +static void ExtractTryCatchInfo(Isolate *isolate, Local context, TryCatch &tc, + std::string &message, std::string &source, + std::string &stackTrace, int &lineno) { + message = ""; + source = ""; + stackTrace = ""; + lineno = 0; + + if (!tc.Message().IsEmpty()) { + lineno = tc.Message()->GetLineNumber(context).FromMaybe(0); + message = ArgConverter::ConvertToString(tc.Message()->Get()); + Local src; + if (tc.Message()->GetScriptResourceName()->ToString(context).ToLocal(&src)) { + source = ArgConverter::ConvertToString(src); + } + } + + if (message.empty() && !tc.Exception().IsEmpty()) { + Local exStr; + if (tc.Exception()->ToDetailString(context).ToLocal(&exStr)) { + message = ArgConverter::ConvertToString(exStr); + } + } + + Local outStackTrace = tc.StackTrace(context).FromMaybe(Local()); + if (!outStackTrace.IsEmpty()) { + Local stackTraceStr = + outStackTrace->ToDetailString(context).FromMaybe(Local()); + if (!stackTraceStr.IsEmpty()) { + stackTrace = ArgConverter::ConvertToString(stackTraceStr); + } + } +} + void CallbackHandlers::CallWorkerScopeOnErrorHandle(Isolate *isolate, TryCatch &tc) { try { + auto wrapper = WorkerWrapper::FromIsolate(isolate); + if (wrapper != nullptr && wrapper->IsTerminating()) { + // The worker was terminated mid-execution (e.g. terminate() + // interrupting a busy loop) - nothing to report. + return; + } + TryCatch innerTc(isolate); // See if `onerror` handle is implemented @@ -1446,7 +1468,7 @@ void CallbackHandlers::CallWorkerScopeOnErrorHandle(Isolate *isolate, TryCatch & auto isEmpty = callback.IsEmpty(); auto isFunction = callback->IsFunction(); - if (!isEmpty && isFunction) { + if (!isEmpty && isFunction && !tc.Message().IsEmpty()) { auto msg = tc.Message()->Get(); Local args1[] = {msg}; @@ -1462,125 +1484,22 @@ void CallbackHandlers::CallWorkerScopeOnErrorHandle(Isolate *isolate, TryCatch & } } - // will account for exceptions thrown inside the error handler - if (innerTc.HasCaught()) { - auto lno = innerTc.Message()->GetLineNumber(context).ToChecked(); - auto msg = innerTc.Message()->Get(); - Local outStackTrace = innerTc.StackTrace(context).FromMaybe(Local()); - Local stackTrace; - if (!outStackTrace.IsEmpty()) { - stackTrace = outStackTrace->ToDetailString(context).FromMaybe(Local()); - } - auto source = innerTc.Message()->GetScriptResourceName()->ToString( - context).ToLocalChecked(); - - auto runtime = Runtime::GetRuntime(isolate); - runtime->PassUncaughtExceptionFromWorkerToMainHandler(msg, stackTrace, source, lno); - } - - // throw so that it may bubble up to main - auto lno = tc.Message()->GetLineNumber(context).ToChecked(); - auto msg = tc.Message()->Get(); - auto source = tc.Message()->GetScriptResourceName()->ToString(context).ToLocalChecked(); - Local outStackTrace = tc.StackTrace(context).FromMaybe(Local()); - Local stackTrace; - if (!outStackTrace.IsEmpty()) { - stackTrace = outStackTrace->ToDetailString(context).FromMaybe(Local()); - } - - auto runtime = Runtime::GetRuntime(isolate); - runtime->PassUncaughtExceptionFromWorkerToMainHandler(msg, stackTrace, source, lno); - } catch (NativeScriptException &ex) { - ex.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 -CallbackHandlers::CallWorkerObjectOnErrorHandle(Isolate *isolate, jint workerId, jstring message, - jstring stackTrace, jstring filename, jint lineno, - jstring threadName) { - try { - auto workerFound = CallbackHandlers::id2WorkerMap.find(workerId); - - if (workerFound == CallbackHandlers::id2WorkerMap.end()) { - // TODO: Pete: Throw exception - DEBUG_WRITE( - "MAIN: CallWorkerObjectOnErrorHandle no worker instance was found with workerId=%d.", - workerId); + if (wrapper == nullptr) { return; } - auto workerPersistent = workerFound->second; - - if (workerPersistent->IsEmpty()) {// Object has been collected - DEBUG_WRITE( - "MAIN: WorkerObjectOnMessageCallback couldn't fire a worker(id=%d) object's `onmessage` callback because the worker has been Garbage Collected.", - workerId); - CallbackHandlers::id2WorkerMap.erase(workerId); - return; - } - - auto worker = Local::New(isolate, *workerPersistent); - - auto context = isolate->GetCurrentContext(); - auto callback = worker->Get(context, ArgConverter::ConvertToV8String(isolate, - "onerror")).ToLocalChecked(); - auto isEmpty = callback.IsEmpty(); - auto isFunction = callback->IsFunction(); - - if (!isEmpty && isFunction) { - auto errEvent = Object::New(isolate); - errEvent->Set(context, - ArgConverter::ConvertToV8String(isolate, "message"), - ArgConverter::jstringToV8String(isolate, message)); - errEvent->Set(context, - ArgConverter::ConvertToV8String(isolate, "stackTrace"), - ArgConverter::jstringToV8String(isolate, stackTrace)); - errEvent->Set(context, - ArgConverter::ConvertToV8String(isolate, "filename"), - ArgConverter::jstringToV8String(isolate, filename)); - errEvent->Set(context, - ArgConverter::ConvertToV8String(isolate, "lineno"), - Number::New(isolate, lineno)); - - Local args1[] = {errEvent}; + std::string message, source, stackTrace; + int lineno; - auto func = callback.As(); - - // Handle exceptions thrown in onmessage with the worker.onerror handler, if present - Local result; - func->Call(context, Undefined(isolate), 1, args1).ToLocal(&result); - if (!result.IsEmpty() && result->BooleanValue(isolate)) { - // Do nothing, exception is handled and does not need to be raised to application level - return; - } + // will account for exceptions thrown inside the error handler + if (innerTc.HasCaught()) { + ExtractTryCatchInfo(isolate, context, innerTc, message, source, stackTrace, lineno); + wrapper->PassUncaughtExceptionFromWorkerToParent(message, source, stackTrace, lineno); } - // Exception wasn't handled, or is critical -> Throw exception - auto strMessage = ArgConverter::jstringToString(message); - auto strFilename = ArgConverter::jstringToString(filename); - auto strThreadname = ArgConverter::jstringToString(threadName); - auto strStackTrace = ArgConverter::jstringToString(stackTrace); - - DEBUG_WRITE( - "Unhandled exception in '%s' thread. file: %s, line %d, message: %s\nStackTrace: %s", - strThreadname.c_str(), strFilename.c_str(), lineno, strMessage.c_str(), - strStackTrace.c_str()); - - // Do not throw exception? -// stringstream ss; -// ss << endl << "Unhandled exception in '" << strThreadname << "' thread. file: " << strFilename << -// ", line: " << lineno << endl << strMessage << endl; -// NativeScriptException ex(ss.str()); -// throw ex; + // bubble up to the main thread's Worker object `onerror` + ExtractTryCatchInfo(isolate, context, tc, message, source, stackTrace, lineno); + wrapper->PassUncaughtExceptionFromWorkerToParent(message, source, stackTrace, lineno); } catch (NativeScriptException &ex) { ex.ReThrowToV8(); } catch (std::exception e) { @@ -1594,28 +1513,6 @@ CallbackHandlers::CallWorkerObjectOnErrorHandle(Isolate *isolate, jint workerId, } } -void CallbackHandlers::ClearWorkerPersistent(int workerId) { - DEBUG_WRITE("ClearWorkerPersistent called for workerId=%d", workerId); - - auto workerFound = CallbackHandlers::id2WorkerMap.find(workerId); - - if (workerFound == CallbackHandlers::id2WorkerMap.end()) { - DEBUG_WRITE( - "MAIN | WORKER: ClearWorkerPersistent no worker instance was found with workerId=%d ! The worker may already be terminated.", - workerId); - return; - } - - auto workerPersistent = workerFound->second; - workerPersistent->Reset(); - - id2WorkerMap.erase(workerId); -} - -void CallbackHandlers::TerminateWorkerThread(Isolate *isolate) { - isolate->TerminateExecution(); -} - void CallbackHandlers::RemoveIsolateEntries(v8::Isolate *isolate) { for (auto &item: cache_) { if (item.second.isolate_ == isolate) { @@ -1791,9 +1688,6 @@ std::atomic_int64_t CallbackHandlers::count_ = {0}; std::atomic_uint64_t CallbackHandlers::frameCallbackCount_ = {0}; -int CallbackHandlers::nextWorkerId = 0; -robin_hood::unordered_map *> CallbackHandlers::id2WorkerMap; - short CallbackHandlers::MAX_JAVA_STRING_ARRAY_LENGTH = 100; jclass CallbackHandlers::RUNTIME_CLASS = nullptr; jclass CallbackHandlers::JAVA_LANG_STRING = nullptr; @@ -1803,7 +1697,6 @@ jmethodID CallbackHandlers::MAKE_INSTANCE_STRONG_ID = nullptr; jmethodID CallbackHandlers::GET_TYPE_METADATA = nullptr; jmethodID CallbackHandlers::ENABLE_VERBOSE_LOGGING_METHOD_ID = nullptr; jmethodID CallbackHandlers::DISABLE_VERBOSE_LOGGING_METHOD_ID = nullptr; -jmethodID CallbackHandlers::INIT_WORKER_METHOD_ID = nullptr; NumericCasts CallbackHandlers::castFunctions; diff --git a/test-app/runtime/src/main/cpp/CallbackHandlers.h b/test-app/runtime/src/main/cpp/CallbackHandlers.h index d079d4730..eddcca93d 100644 --- a/test-app/runtime/src/main/cpp/CallbackHandlers.h +++ b/test-app/runtime/src/main/cpp/CallbackHandlers.h @@ -24,14 +24,6 @@ namespace tns { class CallbackHandlers { public: - /* - * Stores persistent handles of all 'Worker' objects initialized on the main thread - * Note: No isolates different than that of the main thread should access this map - */ - static robin_hood::unordered_map *> id2WorkerMap; - - static int nextWorkerId; - static void Init(v8::Isolate *isolate); static v8::Local @@ -130,34 +122,22 @@ namespace tns { static void NewThreadCallback(const v8::FunctionCallbackInfo &args); /* - * main -> worker messaging - * Fired when a Worker instance's postMessage is called + * parent -> worker messaging + * Fired when a Worker instance's postMessage is called. + * Serializes the payload (structured clone) and queues it on the + * worker's C++ inbox. */ static void WorkerObjectPostMessageCallback(const v8::FunctionCallbackInfo &args); /* - * main -> worker messaging - * Fired when worker object has "postMessage" and the worker has implemented "onMessage" handler - * In case "onMessage" handler isn't implemented no exception is thrown - */ - static void WorkerGlobalOnMessageCallback(v8::Isolate *isolate, jstring message); - - /* - * worker -> main thread messaging - * Fired when a Worker script's "postMessage" is called + * worker -> parent messaging + * Fired when a Worker script's "postMessage" is called. + * Serializes the payload and posts it to the parent runtime's task queue. */ static void WorkerGlobalPostMessageCallback(const v8::FunctionCallbackInfo &args); - /* - * worker -> main messaging - * Fired when worker has sent a message to main and the worker object has implemented "onMessage" handler - * In case "onMessage" handler isn't implemented no exception is thrown - */ - static void - WorkerObjectOnMessageCallback(v8::Isolate *isolate, jint workerId, jstring message); - /* * Fired when a Worker instance's terminate is called (immediately stops execution of the thread) */ @@ -168,17 +148,6 @@ namespace tns { */ static void WorkerGlobalCloseCallback(const v8::FunctionCallbackInfo &args); - /* - * Clears the persistent Worker object handle associated with a workerId - * Occurs when calling a worker object's `terminate` or a worker thread's global scope `close` - */ - static void ClearWorkerPersistent(int workerId); - - /* - * Terminates the currently executing Isolate. No scripts can be executed after this call - */ - static void TerminateWorkerThread(v8::Isolate *isolate); - /* * Is called when an unhandled exception is thrown inside the worker * Will execute 'onerror' if one is provided inside the Worker Scope @@ -187,16 +156,6 @@ namespace tns { */ static void CallWorkerScopeOnErrorHandle(v8::Isolate *isolate, v8::TryCatch &tc); - /* - * Is called when an unhandled exception bubbles up from the worker scope to the main thread Worker Object - * Will execute `onerror` if one is implemented for the Worker Object instance - * Will throw a NativeScript Exception if 'onerror' isn't implemented or returns false - */ - static void - CallWorkerObjectOnErrorHandle(v8::Isolate *isolate, jint workerId, jstring message, - jstring stackTrace, jstring filename, jint lineno, - jstring threadName); - static void RemoveIsolateEntries(v8::Isolate *isolate); @@ -261,8 +220,6 @@ namespace tns { static jmethodID DISABLE_VERBOSE_LOGGING_METHOD_ID; - static jmethodID INIT_WORKER_METHOD_ID; - static NumericCasts castFunctions; static ArrayElementAccessor arrayElementAccessor; diff --git a/test-app/runtime/src/main/cpp/ConcurrentQueue.cpp b/test-app/runtime/src/main/cpp/ConcurrentQueue.cpp new file mode 100644 index 000000000..cc43b238c --- /dev/null +++ b/test-app/runtime/src/main/cpp/ConcurrentQueue.cpp @@ -0,0 +1,99 @@ +#include "ConcurrentQueue.h" + +#include +#include + +#include +#include + +#include "NativeScriptAssert.h" + +namespace tns { + +void ConcurrentQueue::Initialize(ALooper* looper, ALooper_callbackFunc performWork, + void* data) { + std::unique_lock lock(initializationMutex_); + if (terminated_) { + return; + } + + int fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); + if (fd == -1) { + DEBUG_WRITE_FORCE("ConcurrentQueue: eventfd failed: %s", strerror(errno)); + return; + } + + if (ALooper_addFd(looper, fd, ALOOPER_POLL_CALLBACK, ALOOPER_EVENT_INPUT, + performWork, data) != 1) { + DEBUG_WRITE_FORCE("ConcurrentQueue: ALooper_addFd failed"); + close(fd); + return; + } + + this->looper_ = looper; + ALooper_acquire(this->looper_); + this->fd_ = fd; +} + +void ConcurrentQueue::Push(std::shared_ptr message) { + // The lifecycle lock is held across the enqueue + wakeup so a concurrent + // Terminate() can never leave a message stranded in a queue nothing will + // ever drain. + std::unique_lock lock(initializationMutex_); + if (terminated_) { + // the consumer is gone - drop the message (and its backing stores) + return; + } + + { + std::unique_lock mlock(this->mutex_); + this->messagesQueue_.push(message); + } + + if (this->fd_ != -1) { + // The eventfd counter coalesces multiple signals into one wakeup, + // which is fine because the drain callback uses PopAll(). + uint64_t value = 1; + write(this->fd_, &value, sizeof(value)); + } +} + +std::vector> ConcurrentQueue::PopAll() { + std::unique_lock mlock(this->mutex_); + std::vector> messages; + + while (!this->messagesQueue_.empty()) { + messages.push_back(this->messagesQueue_.front()); + this->messagesQueue_.pop(); + } + + return messages; +} + +void ConcurrentQueue::Terminate() { + // Must run on the looper's own thread: removing an fd concurrently with an + // in-flight callback dispatch is racy. + std::unique_lock lock(initializationMutex_); + terminated_ = true; + + if (this->fd_ != -1) { + ALooper_removeFd(this->looper_, this->fd_); + close(this->fd_); + this->fd_ = -1; + } + + if (this->looper_ != nullptr) { + ALooper_release(this->looper_); + this->looper_ = nullptr; + } + + // Release anything a racing Push() enqueued before it observed + // terminated_ - nothing will drain the queue from here on. + { + std::unique_lock mlock(this->mutex_); + std::queue> empty; + this->messagesQueue_.swap(empty); + } +} + +} // namespace tns diff --git a/test-app/runtime/src/main/cpp/ConcurrentQueue.h b/test-app/runtime/src/main/cpp/ConcurrentQueue.h new file mode 100644 index 000000000..33526f443 --- /dev/null +++ b/test-app/runtime/src/main/cpp/ConcurrentQueue.h @@ -0,0 +1,38 @@ +#ifndef CONCURRENTQUEUE_H_ +#define CONCURRENTQUEUE_H_ + +#include +#include +#include +#include +#include + +#include "WorkerMessage.h" + +namespace tns { + +/* + * Thread-safe message inbox attached to an ALooper. + * Push() may be called from any thread; messages pushed before Initialize() + * are queued and can be drained explicitly once the looper is ready. + * Initialize()/PopAll()/Terminate() must be called on the looper's thread. + */ +struct ConcurrentQueue { +public: + void Initialize(ALooper* looper, ALooper_callbackFunc performWork, void* data); + void Push(std::shared_ptr message); + std::vector> PopAll(); + void Terminate(); + +private: + std::queue> messagesQueue_; + ALooper* looper_ = nullptr; + int fd_ = -1; + bool terminated_ = false; + std::mutex mutex_; + std::mutex initializationMutex_; +}; + +} // namespace tns + +#endif /* CONCURRENTQUEUE_H_ */ diff --git a/test-app/runtime/src/main/cpp/JsArgConverter.cpp b/test-app/runtime/src/main/cpp/JsArgConverter.cpp index 559ea2ef6..e5d82f0bd 100644 --- a/test-app/runtime/src/main/cpp/JsArgConverter.cpp +++ b/test-app/runtime/src/main/cpp/JsArgConverter.cpp @@ -246,7 +246,8 @@ bool JsArgConverter::ConvertArg(const Local &arg, int index) { obj = objectManager->GetJavaObjectByJsObject(jsObject); if (obj.IsNull() && (jsObject->IsTypedArray() || jsObject->IsArrayBuffer() || - jsObject->IsArrayBufferView())) { + jsObject->IsArrayBufferView() || + jsObject->IsSharedArrayBuffer())) { BufferCastType bufferCastType = tns::BufferCastType::Byte; shared_ptr store; @@ -258,6 +259,11 @@ bool JsArgConverter::ConvertArg(const Local &arg, int index) { store = array->GetBackingStore(); length = array->ByteLength(); data = static_cast(store->Data()); + } else if (jsObject->IsSharedArrayBuffer()) { + auto array = jsObject.As(); + store = array->GetBackingStore(); + length = array->ByteLength(); + data = static_cast(store->Data()); } else if (jsObject->IsArrayBufferView()) { auto array = jsObject.As(); offset = array->ByteOffset(); diff --git a/test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp b/test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp index 485e5efac..e7d424cbb 100644 --- a/test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp +++ b/test-app/runtime/src/main/cpp/JsArgToArrayConverter.cpp @@ -249,7 +249,7 @@ bool JsArgToArrayConverter::ConvertArg(Local context, const LocalIsTypedArray() || jsObj->IsArrayBuffer() || - jsObj->IsArrayBufferView())) { + jsObj->IsArrayBufferView() || jsObj->IsSharedArrayBuffer())) { BufferCastType bufferCastType = tns::BufferCastType::Byte; shared_ptr store; @@ -261,6 +261,11 @@ bool JsArgToArrayConverter::ConvertArg(Local context, const LocalGetBackingStore(); length = array->ByteLength(); data = static_cast(store->Data()); + } else if (jsObj->IsSharedArrayBuffer()) { + auto array = jsObj.As(); + store = array->GetBackingStore(); + length = array->ByteLength(); + data = static_cast(store->Data()); } else if (jsObj->IsArrayBufferView()) { auto array = jsObj.As(); diff --git a/test-app/runtime/src/main/cpp/LooperTasks.cpp b/test-app/runtime/src/main/cpp/LooperTasks.cpp new file mode 100644 index 000000000..7b4f353ec --- /dev/null +++ b/test-app/runtime/src/main/cpp/LooperTasks.cpp @@ -0,0 +1,105 @@ +#include "LooperTasks.h" + +#include +#include + +#include +#include +#include + +#include "NativeScriptAssert.h" +#include "NativeScriptException.h" + +namespace tns { + +void LooperTasks::Initialize(ALooper* looper) { + std::lock_guard lock(mutex_); + + int fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); + if (fd == -1) { + DEBUG_WRITE_FORCE("LooperTasks: eventfd failed: %s", strerror(errno)); + return; + } + + if (ALooper_addFd(looper, fd, ALOOPER_POLL_CALLBACK, ALOOPER_EVENT_INPUT, + LooperTasks::TasksReadyCallback, this) != 1) { + DEBUG_WRITE_FORCE("LooperTasks: ALooper_addFd failed"); + close(fd); + return; + } + + looper_ = looper; + ALooper_acquire(looper_); + fd_ = fd; +} + +void LooperTasks::Post(std::function task) { + std::lock_guard lock(mutex_); + if (terminated_) { + // The owning runtime is shutting down (or gone) - drop the task, + // matching the old "message to a terminated worker/main" semantics. + return; + } + + tasks_.push(std::move(task)); + + if (fd_ != -1) { + uint64_t value = 1; + write(fd_, &value, sizeof(value)); + } +} + +void LooperTasks::Terminate() { + // Must run on the looper's own thread: removing an fd concurrently with an + // in-flight callback dispatch is racy. + std::lock_guard lock(mutex_); + terminated_ = true; + + if (fd_ != -1) { + ALooper_removeFd(looper_, fd_); + close(fd_); + fd_ = -1; + } + + if (looper_ != nullptr) { + ALooper_release(looper_); + looper_ = nullptr; + } +} + +int LooperTasks::TasksReadyCallback(int fd, int events, void* data) { + uint64_t value; + read(fd, &value, sizeof(value)); + + static_cast(data)->Drain(); + return 1; +} + +void LooperTasks::Drain() { + std::vector> tasks; + { + std::lock_guard lock(mutex_); + while (!tasks_.empty()) { + tasks.push_back(std::move(tasks_.front())); + tasks_.pop(); + } + } + + for (auto& task : tasks) { + // A C++ exception must never propagate out of an ALooper callback. + // Tasks that need to surface JS/Java errors do so themselves + // (e.g. via NativeScriptException::ReThrowToJava, which only sets a + // pending Java exception). + try { + task(); + } catch (NativeScriptException& ex) { + ex.ReThrowToJava(); + } catch (std::exception& ex) { + DEBUG_WRITE_FORCE("Error: c++ exception in looper task: %s", ex.what()); + } catch (...) { + DEBUG_WRITE_FORCE("Error: unknown c++ exception in looper task!"); + } + } +} + +} // namespace tns diff --git a/test-app/runtime/src/main/cpp/LooperTasks.h b/test-app/runtime/src/main/cpp/LooperTasks.h new file mode 100644 index 000000000..ba5409e6b --- /dev/null +++ b/test-app/runtime/src/main/cpp/LooperTasks.h @@ -0,0 +1,41 @@ +#ifndef LOOPERTASKS_H_ +#define LOOPERTASKS_H_ + +#include +#include +#include +#include + +namespace tns { + +/* + * A task queue bound to one runtime's looper - the Android analogue of the + * iOS runtime's ExecuteOnRunLoop(runtime->RuntimeLoop(), ...). + * Each Runtime (main or worker) owns one; child workers post their outbound + * messages, errors and cleanup notifications to their parent runtime's queue. + * + * Post() may be called from any thread; tasks posted after Terminate() are + * dropped. Initialize()/Terminate() must run on the looper's own thread. + * Held via shared_ptr by the owning Runtime and via weak_ptr by child + * WorkerWrappers, so a child posting to an already-destroyed parent is safe. + */ +class LooperTasks { +public: + void Initialize(ALooper* looper); + void Post(std::function task); + void Terminate(); + +private: + static int TasksReadyCallback(int fd, int events, void* data); + void Drain(); + + std::mutex mutex_; + std::queue> tasks_; + ALooper* looper_ = nullptr; + int fd_ = -1; + bool terminated_ = false; +}; + +} // namespace tns + +#endif /* LOOPERTASKS_H_ */ diff --git a/test-app/runtime/src/main/cpp/MethodCache.cpp b/test-app/runtime/src/main/cpp/MethodCache.cpp index 587933728..24a51afac 100644 --- a/test-app/runtime/src/main/cpp/MethodCache.cpp +++ b/test-app/runtime/src/main/cpp/MethodCache.cpp @@ -135,7 +135,7 @@ string MethodCache::GetType(Isolate* isolate, const v8::Local& value) if (value->IsArray()) { type = "array"; - } else if (value->IsArrayBuffer() || value->IsInt8Array() || value->IsUint8Array() || value->IsUint8ClampedArray()) { + } else if (value->IsArrayBuffer() || value->IsSharedArrayBuffer() || value->IsInt8Array() || value->IsUint8Array() || value->IsUint8ClampedArray()) { type = "bytebuffer"; } else if (value->IsInt16Array() || value->IsUint16Array()) { type = "shortbuffer"; diff --git a/test-app/runtime/src/main/cpp/Runtime.cpp b/test-app/runtime/src/main/cpp/Runtime.cpp index aab1050ec..df0fb6e60 100644 --- a/test-app/runtime/src/main/cpp/Runtime.cpp +++ b/test-app/runtime/src/main/cpp/Runtime.cpp @@ -137,7 +137,10 @@ Runtime::Runtime(JNIEnv* env, jobject runtime, int id) m_runtime = env->NewGlobalRef(runtime); m_objectManager = new ObjectManager(m_runtime); m_loopTimer = new MessageLoopTimer(); - s_id2RuntimeCache.emplace(id, this); + { + 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"); @@ -150,9 +153,12 @@ Runtime::Runtime(JNIEnv* env, jobject runtime, int id) } Runtime* Runtime::GetRuntime(int runtimeId) { - auto itFound = s_id2RuntimeCache.find(runtimeId); - auto runtime = - (itFound != s_id2RuntimeCache.end()) ? itFound->second : nullptr; + Runtime* runtime = nullptr; + { + std::lock_guard lock(s_runtimeCacheMutex); + auto itFound = s_id2RuntimeCache.find(runtimeId); + runtime = (itFound != s_id2RuntimeCache.end()) ? itFound->second : nullptr; + } if (runtime == nullptr) { stringstream ss; @@ -164,15 +170,31 @@ Runtime* Runtime::GetRuntime(int runtimeId) { } Runtime* Runtime::GetRuntime(v8::Isolate* isolate) { - auto it = s_isolate2RuntimesCache.find(isolate); + /* + * Hot path, called from V8 callbacks on the isolate's own thread. The slot + * is written once in PrepareV8Runtime before the isolate runs any JS, so + * reading it requires no lock. + */ + auto runtime = static_cast( + isolate->GetData((uint32_t)Runtime::IsolateData::RUNTIME)); + if (runtime != nullptr) { + return runtime; + } - if (it == s_isolate2RuntimesCache.end()) { + // covers the window during isolate setup before SetData has run + { + std::lock_guard lock(s_runtimeCacheMutex); + auto it = s_isolate2RuntimesCache.find(isolate); + runtime = (it != s_isolate2RuntimesCache.end()) ? it->second : nullptr; + } + + if (runtime == nullptr) { stringstream ss; ss << "Cannot find runtime for isolate: " << isolate; throw NativeScriptException(ss.str()); } - return it->second; + return runtime; } Runtime* Runtime::GetRuntimeFromIsolateData(v8::Isolate* isolate) { @@ -296,9 +318,7 @@ void Runtime::RunModule(const char* moduleName) { m_module.Load(context, moduleName); } -void Runtime::RunWorker(jstring scriptFile) { - // TODO: Pete: Why do I crash here with a JNI error (accessing bad jni) - string filePath = ArgConverter::jstringToString(scriptFile); +void Runtime::RunWorker(const std::string& filePath) { auto context = this->GetContext(); m_module.LoadWorker(context, filePath); } @@ -518,28 +538,6 @@ void Runtime::PassExceptionToJsNative(JNIEnv* env, jobject obj, NativeScriptException::CallJsFuncWithErr(errObj, isDiscarded); } -void Runtime::PassUncaughtExceptionFromWorkerToMainHandler( - Local message, Local stackTrace, - Local filename, int lineno) { - JEnv env; - auto runtimeClass = env.GetObjectClass(m_runtime); - - auto mId = env.GetStaticMethodID( - runtimeClass, "passUncaughtExceptionFromWorkerToMain", - "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;I)V"); - - auto jMsg = ArgConverter::ConvertToJavaString(message); - auto jfileName = ArgConverter::ConvertToJavaString(filename); - auto stckTrace = ArgConverter::ConvertToJavaString(stackTrace); - - JniLocalRef jMsgLocal(jMsg); - JniLocalRef jfileNameLocal(jfileName); - JniLocalRef stTrace(stckTrace); - - env.CallStaticVoidMethod(runtimeClass, mId, (jstring)jMsgLocal, - (jstring)jfileNameLocal, (jstring)stTrace, lineno); -} - static void InitializeV8() { Runtime::platform = v8::platform::NewDefaultPlatform().release(); V8::InitializePlatform(Runtime::platform); @@ -576,7 +574,10 @@ Isolate* Runtime::PrepareV8Runtime(const string& filesPath, m_realtimeOrigin = platform->CurrentClockTimeMillis(); isolateFrame.log("Isolate.New"); - s_isolate2RuntimesCache[isolate] = this; + { + std::lock_guard lock(s_runtimeCacheMutex); + s_isolate2RuntimesCache[isolate] = this; + } v8::Locker locker(isolate); Isolate::Scope isolate_scope(isolate); HandleScope handleScope(isolate); @@ -698,11 +699,6 @@ Isolate* Runtime::PrepareV8Runtime(const string& filesPath, globalTemplate->Set(ArgConverter::ConvertToV8String(isolate, "URLPattern"), URLPatternImpl::GetCtor(isolate)); - /* - * Attach `Worker` object constructor only to the main thread (isolate)'s - * global object Workers should not be created from within other Workers, for - * now - */ if (!s_mainThreadInitialized) { m_isMainThread = true; pipe2(m_mainLooper_fd, O_NONBLOCK | O_CLOEXEC); @@ -726,7 +722,32 @@ Isolate* Runtime::PrepareV8Runtime(const string& filesPath, ALooper_addFd(m_mainLooper, m_mainLooper_fd[0], ALOOPER_POLL_CALLBACK, ALOOPER_EVENT_INPUT, CallbackHandlers::RunOnMainThreadFdCallback, nullptr); + } + /* + * Emulate a `WorkerGlobalScope` + * Attach 'postMessage', 'close' to the global object + */ + else { + m_isMainThread = false; + auto postMessageFuncTemplate = FunctionTemplate::New( + isolate, CallbackHandlers::WorkerGlobalPostMessageCallback); + globalTemplate->Set( + ArgConverter::ConvertToV8String(isolate, "__ns__worker"), + Boolean::New(isolate, true)); + globalTemplate->Set(ArgConverter::ConvertToV8String(isolate, "postMessage"), + postMessageFuncTemplate); + auto closeFuncTemplate = FunctionTemplate::New( + isolate, CallbackHandlers::WorkerGlobalCloseCallback); + globalTemplate->Set(ArgConverter::ConvertToV8String(isolate, "close"), + closeFuncTemplate); + } + /* + * Attach the `Worker` constructor to every isolate's global object - workers + * may be created from the main thread or from within other workers + * (mirroring the iOS runtime). + */ + { Local workerFuncTemplate = FunctionTemplate::New(isolate, CallbackHandlers::NewThreadCallback); Local prototype = workerFuncTemplate->PrototypeTemplate(); @@ -748,24 +769,15 @@ Isolate* Runtime::PrepareV8Runtime(const string& filesPath, globalTemplate->Set(ArgConverter::ConvertToV8String(isolate, "Worker"), workerFuncTemplate); } + /* - * Emulate a `WorkerGlobalScope` - * Attach 'postMessage', 'close' to the global object + * Per-runtime task queue used by child workers to deliver messages, errors + * and cleanup notifications to this runtime's thread. The looper exists for + * every runtime: Java prepares it before initNativeScript on both the main + * and worker threads. */ - else { - m_isMainThread = false; - auto postMessageFuncTemplate = FunctionTemplate::New( - isolate, CallbackHandlers::WorkerGlobalPostMessageCallback); - globalTemplate->Set( - ArgConverter::ConvertToV8String(isolate, "__ns__worker"), - Boolean::New(isolate, true)); - globalTemplate->Set(ArgConverter::ConvertToV8String(isolate, "postMessage"), - postMessageFuncTemplate); - auto closeFuncTemplate = FunctionTemplate::New( - isolate, CallbackHandlers::WorkerGlobalCloseCallback); - globalTemplate->Set(ArgConverter::ConvertToV8String(isolate, "close"), - closeFuncTemplate); - } + m_looperTasks = std::make_shared(); + m_looperTasks->Initialize(ALooper_forThread()); SimpleProfiler::Init(isolate, globalTemplate); @@ -951,8 +963,16 @@ void Runtime::PerformanceNowCallback( } void Runtime::DestroyRuntime() { - s_id2RuntimeCache.erase(m_id); - s_isolate2RuntimesCache.erase(m_isolate); + { + std::lock_guard lock(s_runtimeCacheMutex); + s_id2RuntimeCache.erase(m_id); + s_isolate2RuntimesCache.erase(m_isolate); + } + if (m_looperTasks != nullptr) { + // runs on this runtime's own thread; children still holding a weak_ptr + // will have their posts dropped from now on + m_looperTasks->Terminate(); + } tns::disposeIsolate(m_isolate); } @@ -970,6 +990,7 @@ 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; bool Runtime::s_mainThreadInitialized = false; v8::Platform* Runtime::platform = nullptr; int Runtime::m_androidVersion = Runtime::GetAndroidVersion(); diff --git a/test-app/runtime/src/main/cpp/Runtime.h b/test-app/runtime/src/main/cpp/Runtime.h index bedb70e43..c317eebe3 100644 --- a/test-app/runtime/src/main/cpp/Runtime.h +++ b/test-app/runtime/src/main/cpp/Runtime.h @@ -12,6 +12,8 @@ #include "MessageLoopTimer.h" #include "File.h" #include "Timers.h" +#include "LooperTasks.h" +#include #include #include #include @@ -21,7 +23,8 @@ class Runtime { public: enum IsolateData { RUNTIME = 0, - CONSTANTS = 1 + CONSTANTS = 1, + WORKER_WRAPPER = 2 }; ~Runtime(); @@ -51,7 +54,7 @@ class Runtime { void RunModule(JNIEnv* _env, jobject obj, jstring scriptFile); void RunModule(const char *moduleName); - void RunWorker(jstring scriptFile); + 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); void CreateJSInstanceNative(JNIEnv* _env, jobject obj, jobject javaObject, jint javaObjectID, jstring className); @@ -60,7 +63,6 @@ class Runtime { bool NotifyGC(JNIEnv* env, jobject obj); bool TryCallGC(); void PassExceptionToJsNative(JNIEnv* env, jobject obj, jthrowable exception, jstring message, jstring fullStackTrace, jstring jsStackTrace, jboolean isDiscarded); - void PassUncaughtExceptionFromWorkerToMainHandler(v8::Local message, v8::Local stackTrace, v8::Local filename, int lineno); void DestroyRuntime(); void Lock(); @@ -79,6 +81,18 @@ class Runtime { static ALooper* GetMainLooper() { return m_mainLooper; } + static JavaVM* GetJVM() { + return s_jvm; + } + + /* + * Task queue bound to this runtime's looper. Child workers hold a + * weak_ptr to their parent runtime's queue for worker -> parent + * delivery (messages, errors, cleanup notifications). + */ + std::shared_ptr GetLooperTasks() const { + return m_looperTasks; + } private: Runtime(JNIEnv* env, jobject runtime, int id); @@ -99,6 +113,8 @@ class Runtime { MessageLoopTimer* m_loopTimer; + std::shared_ptr m_looperTasks; + int64_t m_lastUsedMemory; v8::Persistent* m_gcFunc; @@ -126,6 +142,12 @@ class Runtime { static robin_hood::unordered_map s_isolate2RuntimesCache; + /* + * Guards the two caches above: runtimes are now constructed and + * destroyed concurrently on native-spawned worker threads. + */ + static std::mutex s_runtimeCacheMutex; + static JavaVM* s_jvm; static jmethodID GET_USED_MEMORY_METHOD_ID; diff --git a/test-app/runtime/src/main/cpp/WorkerMessage.cpp b/test-app/runtime/src/main/cpp/WorkerMessage.cpp new file mode 100644 index 000000000..a2387328c --- /dev/null +++ b/test-app/runtime/src/main/cpp/WorkerMessage.cpp @@ -0,0 +1,160 @@ +#include "WorkerMessage.h" + +#include + +#include "ArgConverter.h" + +using namespace v8; + +namespace tns { +namespace worker { +namespace { + +void ThrowDataCloneException(Local context, Local message) { + Isolate* isolate = context->GetIsolate(); + std::string msg = "DataCloneError: " + ArgConverter::ConvertToString(message); + isolate->ThrowException( + Exception::Error(ArgConverter::ConvertToV8String(isolate, msg))); +} + +class SerializerDelegate : public ValueSerializer::Delegate { +public: + SerializerDelegate(Isolate* isolate, Local context, Message* m) + : isolate_(isolate), context_(context), msg_(m) {} + + void ThrowDataCloneError(Local message) override { + ThrowDataCloneException(context_, message); + } + + Maybe WriteHostObject(Isolate* isolate, Local object) override { + // Host objects (e.g. Java proxies) carry no transferable native state; + // they are recreated as plain objects on the receiving side. + return Just(true); + } + + Maybe GetSharedArrayBufferId( + Isolate* isolate, Local shared_array_buffer) override { + uint32_t i; + for (i = 0; i < seen_shared_array_buffers_.size(); ++i) { + if (seen_shared_array_buffers_[i].Get(isolate) == shared_array_buffer) { + return Just(i); + } + } + + seen_shared_array_buffers_.emplace_back( + Global{isolate, shared_array_buffer}); + msg_->AddSharedArrayBuffer(shared_array_buffer->GetBackingStore()); + return Just(i); + } + + ValueSerializer* serializer = nullptr; + +private: + Isolate* isolate_; + Local context_; + Message* msg_; + std::vector> seen_shared_array_buffers_; + + friend class tns::worker::Message; +}; + +class DeserializerDelegate : public ValueDeserializer::Delegate { +public: + DeserializerDelegate( + Message* m, Isolate* isolate, + const std::vector>& shared_array_buffers) + : shared_array_buffers_(shared_array_buffers) {} + + MaybeLocal ReadHostObject(Isolate* isolate) override { + EscapableHandleScope scope(isolate); + Local object = Object::New(isolate); + return scope.Escape(object).As(); + } + + MaybeLocal GetSharedArrayBufferFromId( + Isolate* isolate, uint32_t clone_id) override { + if (clone_id >= shared_array_buffers_.size()) { + return MaybeLocal(); + } + return shared_array_buffers_[clone_id]; + } + + ValueDeserializer* deserializer = nullptr; + +private: + const std::vector>& shared_array_buffers_; +}; + +} // namespace + +Maybe Message::Serialize(Isolate* isolate, Local context, + Local input) { + HandleScope handle_scope(isolate); + Context::Scope context_scope(context); + + // Verify that we're not silently overwriting an existing message. + assert(main_message_buf_.is_empty()); + + SerializerDelegate delegate(isolate, context, this); + ValueSerializer serializer(isolate, &delegate); + delegate.serializer = &serializer; + + serializer.WriteHeader(); + if (serializer.WriteValue(context, input).IsNothing()) { + return Nothing(); + } + + // The serializer gave us a buffer allocated using `malloc()`. + std::pair data = serializer.Release(); + assert(data.first != nullptr); + main_message_buf_ = + MallocedBuffer(reinterpret_cast(data.first), data.second); + return Just(true); +} + +MaybeLocal Message::Deserialize(Isolate* isolate, Local context) { + Context::Scope context_scope(context); + EscapableHandleScope handle_scope(isolate); + + // Attach all transferred SharedArrayBuffers to their new Isolate. + std::vector> shared_array_buffers; + for (uint32_t i = 0; i < shared_array_buffers_.size(); ++i) { + Local sab = + SharedArrayBuffer::New(isolate, shared_array_buffers_[i]); + shared_array_buffers.push_back(sab); + } + + DeserializerDelegate delegate(this, isolate, shared_array_buffers); + ValueDeserializer deserializer( + isolate, reinterpret_cast(main_message_buf_.data), + main_message_buf_.size, &delegate); + delegate.deserializer = &deserializer; + + // Attach all transferred ArrayBuffers to their new Isolate. + for (uint32_t i = 0; i < array_buffers_.size(); ++i) { + Local ab = + ArrayBuffer::New(isolate, std::move(array_buffers_[i])); + deserializer.TransferArrayBuffer(i, ab); + } + + if (deserializer.ReadHeader(context).IsNothing()) { + return MaybeLocal(); + } + + Local return_value; + if (!deserializer.ReadValue(context).ToLocal(&return_value)) { + return MaybeLocal(); + } + + return handle_scope.Escape(return_value); +} + +void Message::AddSharedArrayBuffer(std::shared_ptr backing_store) { + shared_array_buffers_.emplace_back(std::move(backing_store)); +} + +Message::Message(MallocedBuffer&& payload) + : main_message_buf_(std::move(payload)) {} + +} // namespace worker +} // namespace tns diff --git a/test-app/runtime/src/main/cpp/WorkerMessage.h b/test-app/runtime/src/main/cpp/WorkerMessage.h new file mode 100644 index 000000000..734fbad35 --- /dev/null +++ b/test-app/runtime/src/main/cpp/WorkerMessage.h @@ -0,0 +1,103 @@ +#ifndef WORKERMESSAGE_H_ +#define WORKERMESSAGE_H_ + +#include +#include +#include +#include "v8.h" + +namespace tns { + +template +inline T* Malloc(size_t n) { + // n is an element count (see UncheckedRealloc), not a byte count + return static_cast(malloc(sizeof(T) * n)); +} + +template +T* UncheckedRealloc(T* pointer, size_t n) { + size_t full_size = sizeof(T) * n; + + if (full_size == 0) { + free(pointer); + return nullptr; + } + + void* allocated = realloc(pointer, full_size); + + return static_cast(allocated); +} + +template +struct MallocedBuffer { + T* data; + size_t size; + + T* release() { + T* ret = data; + data = nullptr; + return ret; + } + + void Truncate(size_t new_size) { + size = new_size; + } + + void Realloc(size_t new_size) { + Truncate(new_size); + data = UncheckedRealloc(data, new_size); + } + + bool is_empty() const { return data == nullptr; } + + MallocedBuffer() : data(nullptr), size(0) {} + explicit MallocedBuffer(size_t size) : data(Malloc(size)), size(size) {} + MallocedBuffer(T* data, size_t size) : data(data), size(size) {} + MallocedBuffer(MallocedBuffer&& other) : data(other.data), size(other.size) { + other.data = nullptr; + } + MallocedBuffer& operator=(MallocedBuffer&& other) { + this->~MallocedBuffer(); + return *new (this) MallocedBuffer(std::move(other)); + } + ~MallocedBuffer() { free(data); } + MallocedBuffer(const MallocedBuffer&) = delete; + MallocedBuffer& operator=(const MallocedBuffer&) = delete; +}; + +namespace worker { + +/* + * A structured-clone payload that can cross isolate/thread boundaries. + * Serialized with v8::ValueSerializer on the sending isolate and + * deserialized with v8::ValueDeserializer on the receiving one. + * SharedArrayBuffer contents are shared via their backing stores. + */ +class Message { + public: + Message(MallocedBuffer&& payload = MallocedBuffer()); + Message(Message&& other) = default; + Message& operator=(Message&& other) = default; + Message& operator=(const Message&) = delete; + Message(const Message&) = delete; + + v8::Maybe Serialize(v8::Isolate* isolate, + v8::Local context, + v8::Local input); + v8::MaybeLocal Deserialize(v8::Isolate* isolate, + v8::Local context); + + // Called when a new SharedArrayBuffer object is encountered in the + // incoming value's structure. + void AddSharedArrayBuffer(std::shared_ptr backing_store); + + private: + MallocedBuffer main_message_buf_; + std::vector> array_buffers_; + std::vector> shared_array_buffers_; +}; + +} // namespace worker +} // namespace tns + +#endif /* WORKERMESSAGE_H_ */ diff --git a/test-app/runtime/src/main/cpp/WorkerWrapper.cpp b/test-app/runtime/src/main/cpp/WorkerWrapper.cpp new file mode 100644 index 000000000..2566c0d3a --- /dev/null +++ b/test-app/runtime/src/main/cpp/WorkerWrapper.cpp @@ -0,0 +1,570 @@ +#include "WorkerWrapper.h" + +#include +#include + +#include + +#include "ArgConverter.h" +#include "CallbackHandlers.h" +#include "JEnv.h" +#include "JniLocalRef.h" +#include "NativeScriptAssert.h" +#include "NativeScriptException.h" +#include "Runtime.h" + +using namespace v8; + +namespace tns { + +WorkerWrapper::WorkerWrapper(Isolate* parentIsolate, int workerId, std::string workerPath, + std::string callingDir, int priority, + Local workerObject) + : parentIsolate_(parentIsolate), + // runs on the parent's thread, where the parent runtime is alive + parentTasks_(Runtime::GetRuntime(parentIsolate)->GetLooperTasks()), + workerIsolate_(nullptr), + runtime_(nullptr), + workerId_(workerId), + workerPath_(std::move(workerPath)), + callingDir_(std::move(callingDir)), + // workerPath_ (not workerPath) - the parameter was just moved from + threadName_("W" + std::to_string(workerId) + ": " + workerPath_), + priority_(priority), + poWorker_(new Persistent(parentIsolate, workerObject)), + isClosing_(false), + isTerminating_(false), + isDisposed_(false), + javaLooperRef_(nullptr) {} + +void WorkerWrapper::Start() { + auto self = shared_from_this(); + std::thread thread([self]() { + self->BackgroundLooper(self); + }); + thread.detach(); +} + +void WorkerWrapper::PostMessage(std::shared_ptr message) { + if (!isTerminating_ && !isClosing_) { + queue_.Push(message); + } +} + +void WorkerWrapper::PostMessageToParent(std::shared_ptr message) { + if (isTerminating_) { + return; + } + + auto parentTasks = parentTasks_.lock(); + if (parentTasks == nullptr) { + // the parent runtime is gone (e.g. a parent worker that shut down) + return; + } + + int workerId = workerId_; + parentTasks->Post([workerId, message]() { + WorkerWrapper::FireMessageOnParentWorkerObject(workerId, message); + }); +} + +void WorkerWrapper::Terminate() { + if (isClosing_ || isDisposed_) { + // The worker is already shutting down on its own; nothing to do. + return; + } + + bool wasTerminating = isTerminating_.exchange(true); + if (wasTerminating) { + return; + } + + Isolate* isolate = workerIsolate_.load(); + if (isolate != nullptr) { + // The only v8 call that is legal from another thread - interrupts any + // JS currently running on the worker (e.g. a busy loop). + isolate->TerminateExecution(); + } + + QuitLooper(); +} + +void WorkerWrapper::Close() { + bool wasClosing = isClosing_.exchange(true); + if (wasClosing) { + return; + } + + // Once the current callback unwinds, Looper.loop() returns and the + // thread proceeds to cleanup. Pending messages are dropped, matching the + // previous front-of-queue TerminateAndCloseThread behavior. + QuitLooper(); +} + +void WorkerWrapper::QuitLooper() { + std::lock_guard lock(looperMutex_); + if (javaLooperRef_ != nullptr) { + JEnv env; + env.CallVoidMethod(javaLooperRef_, LOOPER_QUIT_METHOD_ID); + } +} + +int WorkerWrapper::DrainCallback(int fd, int events, void* data) { + uint64_t value; + read(fd, &value, sizeof(value)); + + auto wrapper = static_cast(data); + wrapper->DrainPendingTasks(); + return 1; +} + +void WorkerWrapper::DrainPendingTasks() { + Isolate* isolate = workerIsolate_.load(); + if (isolate == nullptr || isTerminating_) { + return; + } + + auto messages = queue_.PopAll(); + if (messages.empty()) { + return; + } + + v8::Locker locker(isolate); + Isolate::Scope isolate_scope(isolate); + HandleScope handle_scope(isolate); + auto context = runtime_->GetContext(); + Context::Scope context_scope(context); + auto globalObject = context->Global(); + + for (auto& message : messages) { + if (isTerminating_ || isClosing_) { + break; + } + + TryCatch tc(isolate); + + Local callback; + if (!globalObject->Get(context, ArgConverter::ConvertToV8String(isolate, "onmessage")) + .ToLocal(&callback) || + !callback->IsFunction()) { + DEBUG_WRITE( + "WORKER: couldn't fire a worker's `onmessage` callback because it isn't implemented!"); + continue; + } + + Local data; + if (message->Deserialize(isolate, context).ToLocal(&data)) { + auto event = Object::New(isolate); + event->DefineOwnProperty(context, ArgConverter::ConvertToV8String(isolate, "data"), + data, PropertyAttribute::ReadOnly); + Local args[] = {event}; + callback.As()->Call(context, Undefined(isolate), 1, args); + } + + if (tc.HasCaught() && !isTerminating_) { + CallbackHandlers::CallWorkerScopeOnErrorHandle(isolate, tc); + } + } +} + +void WorkerWrapper::FireMessageOnParentWorkerObject(int workerId, + std::shared_ptr message) { + auto wrapper = WorkerWrapper::GetById(workerId); + if (wrapper == nullptr) { + DEBUG_WRITE("MAIN: no worker instance was found with workerId=%d.", workerId); + return; + } + + Isolate* isolate = wrapper->parentIsolate_; + v8::Locker locker(isolate); + Isolate::Scope isolate_scope(isolate); + HandleScope handle_scope(isolate); + + if (wrapper->poWorker_ == nullptr || wrapper->poWorker_->IsEmpty()) { + DEBUG_WRITE( + "MAIN: couldn't fire a worker(id=%d) object's `onmessage` callback because the worker has been cleared.", + workerId); + return; + } + + auto worker = Local::New(isolate, *wrapper->poWorker_); + auto context = Runtime::GetRuntime(isolate)->GetContext(); + Context::Scope context_scope(context); + + try { + Local callback; + if (!worker->Get(context, ArgConverter::ConvertToV8String(isolate, "onmessage")) + .ToLocal(&callback) || + !callback->IsFunction()) { + DEBUG_WRITE( + "MAIN: couldn't fire a worker(id=%d) object's `onmessage` callback because it isn't implemented.", + workerId); + return; + } + + Local data; + if (message->Deserialize(isolate, context).ToLocal(&data)) { + auto event = Object::New(isolate); + event->DefineOwnProperty(context, ArgConverter::ConvertToV8String(isolate, "data"), + data, PropertyAttribute::ReadOnly); + Local args[] = {event}; + callback.As()->Call(context, Undefined(isolate), 1, args); + } + } catch (NativeScriptException& ex) { + ex.ReThrowToV8(); + } +} + +void WorkerWrapper::PassUncaughtExceptionFromWorkerToParent(const std::string& message, + const std::string& filename, + const std::string& stackTrace, + int lineno) { + auto parentTasks = parentTasks_.lock(); + if (parentTasks == nullptr) { + // the parent runtime is gone (e.g. a parent worker that shut down) + return; + } + + int workerId = workerId_; + std::string threadName = threadName_; + Isolate* parentIsolate = parentIsolate_; + + parentTasks->Post([workerId, message, filename, stackTrace, lineno, threadName, + parentIsolate]() { + v8::Locker locker(parentIsolate); + Isolate::Scope isolate_scope(parentIsolate); + HandleScope handle_scope(parentIsolate); + auto context = Runtime::GetRuntime(parentIsolate)->GetContext(); + Context::Scope context_scope(context); + + WorkerWrapper::FireErrorOnParentWorkerObject(workerId, message, stackTrace, filename, + lineno, threadName); + }); +} + +void WorkerWrapper::FireErrorOnParentWorkerObject(int workerId, const std::string& message, + const std::string& stackTrace, + const std::string& filename, int lineno, + const std::string& threadName) { + auto wrapper = WorkerWrapper::GetById(workerId); + if (wrapper == nullptr) { + DEBUG_WRITE("MAIN: no worker instance was found with workerId=%d.", workerId); + return; + } + + Isolate* isolate = wrapper->parentIsolate_; + + try { + if (wrapper->poWorker_ == nullptr || wrapper->poWorker_->IsEmpty()) { + DEBUG_WRITE( + "MAIN: couldn't fire a worker(id=%d) object's `onerror` callback because the worker has been cleared.", + workerId); + return; + } + + auto worker = Local::New(isolate, *wrapper->poWorker_); + auto context = Runtime::GetRuntime(isolate)->GetContext(); + + Local callback; + bool hasOnError = + worker->Get(context, ArgConverter::ConvertToV8String(isolate, "onerror")) + .ToLocal(&callback) && + callback->IsFunction(); + + if (hasOnError) { + auto errEvent = Object::New(isolate); + errEvent->Set(context, ArgConverter::ConvertToV8String(isolate, "message"), + ArgConverter::ConvertToV8String(isolate, message)); + errEvent->Set(context, ArgConverter::ConvertToV8String(isolate, "stackTrace"), + ArgConverter::ConvertToV8String(isolate, stackTrace)); + errEvent->Set(context, ArgConverter::ConvertToV8String(isolate, "filename"), + ArgConverter::ConvertToV8String(isolate, filename)); + errEvent->Set(context, ArgConverter::ConvertToV8String(isolate, "lineno"), + Number::New(isolate, lineno)); + + Local args[] = {errEvent}; + + // If the handler returns a truthy value the exception is handled + // and must not be raised to application level + Local result; + callback.As()->Call(context, Undefined(isolate), 1, args).ToLocal(&result); + if (!result.IsEmpty() && result->BooleanValue(isolate)) { + return; + } + } + + DEBUG_WRITE( + "Unhandled exception in '%s' thread. file: %s, line %d, message: %s\nStackTrace: %s", + threadName.c_str(), filename.c_str(), lineno, message.c_str(), + stackTrace.c_str()); + } catch (NativeScriptException& ex) { + ex.ReThrowToV8(); + } +} + +void WorkerWrapper::BackgroundLooper(std::shared_ptr self) { + JavaVM* jvm = Runtime::GetJVM(); + JNIEnv* jniEnv = nullptr; + + JavaVMAttachArgs attachArgs; + attachArgs.version = JNI_VERSION_1_6; + attachArgs.name = const_cast(threadName_.c_str()); + attachArgs.group = nullptr; + jvm->AttachCurrentThread(&jniEnv, &attachArgs); + + // pthread names are limited to 15 chars + pthread_setname_np(pthread_self(), threadName_.substr(0, 15).c_str()); + + int runtimeId = -1; + + try { + JEnv env; + + // Performs the cgroup/scheduling-policy move in addition to the nice + // value, exactly like the previous Java-side + // Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND) call. + env.CallStaticVoidMethod(PROCESS_CLASS, SET_THREAD_PRIORITY_METHOD_ID, priority_); + + if (!isTerminating_ && !isClosing_) { + // Prepares the Java Looper for this thread and creates the + // per-worker com.tns.Runtime (which creates the worker isolate on + // this thread via initNativeScript -> PrepareV8Runtime). + JniLocalRef callingDir(env.NewStringUTF(callingDir_.c_str())); + runtimeId = env.CallStaticIntMethod(RUNTIME_CLASS, INIT_WORKER_RUNTIME_METHOD_ID, + workerId_, (jstring) callingDir); + runtime_ = Runtime::GetRuntime(runtimeId); + + { + std::lock_guard lock(looperMutex_); + JniLocalRef looper(env.CallStaticObjectMethod(LOOPER_CLASS, MY_LOOPER_METHOD_ID)); + javaLooperRef_ = env.NewGlobalRef(looper); + } + + // Looper.prepare() ran above, so ALooper_forThread() returns the + // native looper backing the Java one - fds added here are pumped + // by Looper.loop(). + queue_.Initialize(ALooper_forThread(), WorkerWrapper::DrainCallback, this); + + Isolate* isolate = runtime_->GetIsolate(); + + { + v8::Locker locker(isolate); + Isolate::Scope isolate_scope(isolate); + HandleScope handle_scope(isolate); + isolate->SetData((uint32_t) Runtime::IsolateData::WORKER_WRAPPER, this); + workerIsolate_.store(isolate); + + auto context = runtime_->GetContext(); + Context::Scope context_scope(context); + + // (A future worker inspector would be created here, before the + // script runs, mirroring the iOS runtime.) + + if (!isTerminating_) { + runtime_->RunWorker(workerPath_); + } + } + + // Deliver messages that were posted before the worker was ready + // (replaces the old Java Handshake + pendingWorkerMessages). + DrainPendingTasks(); + + if (!isTerminating_ && !isClosing_) { + // Blocks, pumping Java Handler messages (cross-thread Java->JS + // calls), timers and the worker inbox until quit() is called. + env.CallStaticVoidMethod(RUNTIME_CLASS, RUN_WORKER_LOOP_METHOD_ID); + } + } + } catch (NativeScriptException& ex) { + if (jniEnv->ExceptionCheck()) { + jniEnv->ExceptionClear(); + } + if (!isTerminating_) { + PassUncaughtExceptionFromWorkerToParent(ex.GetErrorMessage(), workerPath_, "", 0); + } + } catch (std::exception& ex) { + DEBUG_WRITE_FORCE("Worker(id=%d) error: c++ exception: %s", workerId_, ex.what()); + } catch (...) { + DEBUG_WRITE_FORCE("Worker(id=%d) error: unknown c++ exception!", workerId_); + } + + // ----- Shutdown (close, terminate or bootstrap failure) ----- + + isTerminating_ = true; + + // Terminate any workers this worker created (nested workers). Their + // Worker object persistents live in this isolate, so they must be + // released before the isolate is disposed below. Each child cascades to + // its own children during its shutdown. + { + Isolate* isolate = workerIsolate_.load(); + if (isolate != nullptr) { + TerminateChildren(isolate); + } + } + + // On this thread: safe to unregister the inbox fd from the looper. + queue_.Terminate(); + + if (runtime_ != nullptr) { + try { + // Java-side detach (GcListener.unsubscribe + runtimeCache.remove) + // must happen before the isolate is disposed, preserving the old + // WorkerThreadHandler -> TerminateWorkerCallback ordering. + JEnv env; + env.CallStaticVoidMethod(RUNTIME_CLASS, DETACH_WORKER_RUNTIME_METHOD_ID, runtimeId); + } catch (NativeScriptException& ex) { + if (jniEnv->ExceptionCheck()) { + jniEnv->ExceptionClear(); + } + DEBUG_WRITE_FORCE("Worker(id=%d) error while detaching Java runtime: %s", workerId_, + ex.GetErrorMessage().c_str()); + } + + // Take the isolate from the runtime, not from workerIsolate_: if the + // bootstrap failed between initWorkerRuntime and the workerIsolate_ + // publish (e.g. a JNI error while resolving the looper), the atomic is + // still null while the isolate very much needs disposing. + workerIsolate_.store(nullptr); + Isolate* isolate = runtime_->GetIsolate(); + { + v8::Locker locker(isolate); + Isolate::Scope isolate_scope(isolate); + HandleScope handle_scope(isolate); + runtime_->DestroyRuntime(); + } + isolate->Dispose(); + + // The Runtime destructor still makes JNI calls - it must run before + // DetachCurrentThread below. + delete runtime_; + runtime_ = nullptr; + } + + { + std::lock_guard lock(looperMutex_); + if (javaLooperRef_ != nullptr) { + jniEnv->DeleteGlobalRef(javaLooperRef_); + javaLooperRef_ = nullptr; + } + } + + isDisposed_ = true; + + // Notify the parent thread so the Worker object's persistent handle and + // the registry entry are released (no-op if terminate() or the parent's + // own shutdown already cleared them). + if (auto parentTasks = parentTasks_.lock()) { + int workerId = workerId_; + parentTasks->Post([workerId]() { + WorkerWrapper::ClearWorkerOnParent(workerId); + }); + } + + // ART aborts if a native thread exits while still attached. This must be + // the very last JNI-touching action on this thread. + jvm->DetachCurrentThread(); +} + +int WorkerWrapper::NextWorkerId() { + return nextWorkerId_.fetch_add(1, std::memory_order_relaxed) + 1; +} + +std::shared_ptr WorkerWrapper::GetById(int workerId) { + std::lock_guard lock(registryMutex_); + auto it = registry_.find(workerId); + return it != registry_.end() ? it->second : nullptr; +} + +void WorkerWrapper::Insert(int workerId, std::shared_ptr wrapper) { + std::lock_guard lock(registryMutex_); + registry_.emplace(workerId, std::move(wrapper)); +} + +void WorkerWrapper::ClearWorkerOnParent(int workerId) { + std::shared_ptr wrapper; + { + std::lock_guard lock(registryMutex_); + auto it = registry_.find(workerId); + if (it == registry_.end()) { + return; + } + wrapper = it->second; + registry_.erase(it); + } + + if (wrapper->poWorker_ != nullptr) { + v8::Locker locker(wrapper->parentIsolate_); + wrapper->poWorker_->Reset(); + delete wrapper->poWorker_; + wrapper->poWorker_ = nullptr; + } +} + +void WorkerWrapper::TerminateChildren(Isolate* parentIsolate) { + std::vector> children; + { + std::lock_guard lock(registryMutex_); + for (auto& entry : registry_) { + if (entry.second->parentIsolate_ == parentIsolate) { + children.push_back(entry.second); + } + } + } + + for (auto& child : children) { + DEBUG_WRITE( + "Terminating nested worker(id=%d) because its parent is shutting down", + child->workerId_); + child->Terminate(); + ClearWorkerOnParent(child->workerId_); + } +} + +WorkerWrapper* WorkerWrapper::FromIsolate(Isolate* isolate) { + return static_cast( + isolate->GetData((uint32_t) Runtime::IsolateData::WORKER_WRAPPER)); +} + +void WorkerWrapper::EnsureJniCached() { + if (RUNTIME_CLASS != nullptr) { + return; + } + + JEnv env; + + RUNTIME_CLASS = env.FindClass("com/tns/Runtime"); + assert(RUNTIME_CLASS != nullptr); + INIT_WORKER_RUNTIME_METHOD_ID = + env.GetStaticMethodID(RUNTIME_CLASS, "initWorkerRuntime", "(ILjava/lang/String;)I"); + RUN_WORKER_LOOP_METHOD_ID = env.GetStaticMethodID(RUNTIME_CLASS, "runWorkerLoop", "()V"); + DETACH_WORKER_RUNTIME_METHOD_ID = + env.GetStaticMethodID(RUNTIME_CLASS, "detachWorkerRuntime", "(I)V"); + + LOOPER_CLASS = env.FindClass("android/os/Looper"); + assert(LOOPER_CLASS != nullptr); + MY_LOOPER_METHOD_ID = + env.GetStaticMethodID(LOOPER_CLASS, "myLooper", "()Landroid/os/Looper;"); + LOOPER_QUIT_METHOD_ID = env.GetMethodID(LOOPER_CLASS, "quit", "()V"); + + PROCESS_CLASS = env.FindClass("android/os/Process"); + assert(PROCESS_CLASS != nullptr); + SET_THREAD_PRIORITY_METHOD_ID = + env.GetStaticMethodID(PROCESS_CLASS, "setThreadPriority", "(I)V"); +} + +std::mutex WorkerWrapper::registryMutex_; +std::map> WorkerWrapper::registry_; +std::atomic_int WorkerWrapper::nextWorkerId_(0); + +jclass WorkerWrapper::RUNTIME_CLASS = nullptr; +jclass WorkerWrapper::LOOPER_CLASS = nullptr; +jclass WorkerWrapper::PROCESS_CLASS = nullptr; +jmethodID WorkerWrapper::INIT_WORKER_RUNTIME_METHOD_ID = nullptr; +jmethodID WorkerWrapper::RUN_WORKER_LOOP_METHOD_ID = nullptr; +jmethodID WorkerWrapper::DETACH_WORKER_RUNTIME_METHOD_ID = nullptr; +jmethodID WorkerWrapper::MY_LOOPER_METHOD_ID = nullptr; +jmethodID WorkerWrapper::LOOPER_QUIT_METHOD_ID = nullptr; +jmethodID WorkerWrapper::SET_THREAD_PRIORITY_METHOD_ID = nullptr; + +} // namespace tns diff --git a/test-app/runtime/src/main/cpp/WorkerWrapper.h b/test-app/runtime/src/main/cpp/WorkerWrapper.h new file mode 100644 index 000000000..d37632386 --- /dev/null +++ b/test-app/runtime/src/main/cpp/WorkerWrapper.h @@ -0,0 +1,177 @@ +#ifndef WORKERWRAPPER_H_ +#define WORKERWRAPPER_H_ + +#include + +#include +#include +#include +#include +#include + +#include "ConcurrentQueue.h" +#include "WorkerMessage.h" +#include "v8.h" + +namespace tns { + +class LooperTasks; +class Runtime; + +/* + * Owns a worker's native thread and its lifecycle, mirroring the iOS + * runtime's WorkerWrapper. The thread is a std::thread that attaches to the + * JVM, prepares a Java Looper (so worker/plugin code can keep using Android + * Handlers) and then drives that single looper, which pumps both Java + * messages and the worker's C++ inbox (ConcurrentQueue + eventfd). + * + * Messaging is done entirely in C++ with V8 ValueSerializer payloads: + * - parent -> worker: queue_ + eventfd wakeup on the worker looper + * - worker -> parent: the parent runtime's LooperTasks queue + * + * The parent may be the main thread or another worker (nested workers); a + * worker's children are terminated when the worker itself shuts down. + */ +class WorkerWrapper : public std::enable_shared_from_this { +public: + WorkerWrapper(v8::Isolate* parentIsolate, int workerId, std::string workerPath, + std::string callingDir, int priority, + v8::Local workerObject); + + int WorkerId() const { return workerId_; } + bool IsTerminating() const { return isTerminating_; } + bool IsClosing() const { return isClosing_; } + bool IsDisposed() const { return isDisposed_; } + + /* + * Spawns the (detached) worker thread. The thread holds a shared_ptr to + * this wrapper, keeping it alive until the thread fully shuts down. + */ + void Start(); + + /* + * parent -> worker. Queues a serialized message and wakes the worker + * looper. Messages posted before the worker finishes bootstrapping are + * drained right after the worker script runs. + */ + void PostMessage(std::shared_ptr message); + + /* + * worker -> parent. Posts a task that fires the Worker object's + * `onmessage` on the parent runtime's thread. + */ + void PostMessageToParent(std::shared_ptr message); + + /* + * Called on the parent's thread. Interrupts running JS (TerminateExecution + * is the one v8 call that is legal cross-thread) and quits the worker + * looper. + */ + void Terminate(); + + /* + * Called on the worker thread (self.close()). Lets the current callback + * unwind, then the looper quits and the thread shuts down gracefully. + */ + void Close(); + + /* + * Posts the worker object's `onerror` invocation to the parent's thread. + * Strings only - must not hold any v8 handles from the worker isolate. + */ + void PassUncaughtExceptionFromWorkerToParent(const std::string& message, + const std::string& filename, + const std::string& stackTrace, + int lineno); + + /* + * Registry of live workers, keyed by workerId. Replaces the old + * CallbackHandlers::id2WorkerMap. Guarded by a mutex because the worker + * shutdown path posts cleanup from the worker thread. + */ + static int NextWorkerId(); + static std::shared_ptr GetById(int workerId); + static void Insert(int workerId, std::shared_ptr wrapper); + + /* + * Parent thread only: resets the Worker object's persistent handle and + * removes the wrapper from the registry. Idempotent. + */ + static void ClearWorkerOnParent(int workerId); + + /* + * Terminates and clears all workers whose parent is the given isolate. + * Must run on the parent's thread, before the parent isolate is disposed + * (the children's Worker object persistents live in that isolate). + * Cascades: each child terminates its own children during shutdown. + */ + static void TerminateChildren(v8::Isolate* parentIsolate); + + /* + * Resolves the wrapper of a worker isolate via its isolate data slot. + * Returns nullptr on the main isolate. + */ + static WorkerWrapper* FromIsolate(v8::Isolate* isolate); + + /* + * Resolves the jclass/jmethodID handles used by the worker thread + * bootstrap, before the first worker starts. The first call is always on + * the main thread (nested workers require a main-thread worker first), + * where class loading is safe. + */ + static void EnsureJniCached(); + +private: + void BackgroundLooper(std::shared_ptr self); + void DrainPendingTasks(); + void QuitLooper(); + static int DrainCallback(int fd, int events, void* data); + static void FireMessageOnParentWorkerObject(int workerId, + std::shared_ptr message); + static void FireErrorOnParentWorkerObject(int workerId, const std::string& message, + const std::string& stackTrace, + const std::string& filename, int lineno, + const std::string& threadName); + + v8::Isolate* parentIsolate_; + // The parent runtime's task queue; weak so a child outliving its parent + // just drops its posts instead of touching a dead runtime. + std::weak_ptr parentTasks_; + std::atomic workerIsolate_; + Runtime* runtime_; + + const int workerId_; + const std::string workerPath_; + const std::string callingDir_; + const std::string threadName_; + const int priority_; + + v8::Persistent* poWorker_; + + std::atomic_bool isClosing_; + std::atomic_bool isTerminating_; + std::atomic_bool isDisposed_; + + ConcurrentQueue queue_; + + std::mutex looperMutex_; + jobject javaLooperRef_; + + static std::mutex registryMutex_; + static std::map> registry_; + static std::atomic_int nextWorkerId_; + + static jclass RUNTIME_CLASS; + static jclass LOOPER_CLASS; + static jclass PROCESS_CLASS; + static jmethodID INIT_WORKER_RUNTIME_METHOD_ID; + static jmethodID RUN_WORKER_LOOP_METHOD_ID; + static jmethodID DETACH_WORKER_RUNTIME_METHOD_ID; + static jmethodID MY_LOOPER_METHOD_ID; + static jmethodID LOOPER_QUIT_METHOD_ID; + static jmethodID SET_THREAD_PRIORITY_METHOD_ID; +}; + +} // namespace tns + +#endif /* WORKERWRAPPER_H_ */ 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 a05d869fb..5b6981156 100644 --- a/test-app/runtime/src/main/cpp/com_tns_Runtime.cpp +++ b/test-app/runtime/src/main/cpp/com_tns_Runtime.cpp @@ -153,34 +153,6 @@ extern "C" JNIEXPORT void Java_com_tns_Runtime_runModule(JNIEnv* _env, jobject o } } -extern "C" JNIEXPORT void Java_com_tns_Runtime_runWorker(JNIEnv* _env, jobject obj, jint runtimeId, jstring scriptFile) { - auto runtime = TryGetRuntime(runtimeId); - if (runtime == nullptr) { - return; - } - - auto isolate = runtime->GetIsolate(); - v8::Locker locker(isolate); - v8::Isolate::Scope isolate_scope(isolate); - v8::HandleScope handleScope(isolate); - auto context = runtime->GetContext(); - v8::Context::Scope context_scope(context); - - try { - runtime->RunWorker(scriptFile); - } catch (NativeScriptException& e) { - e.ReThrowToJava(); - } catch (std::exception e) { - stringstream ss; - ss << "Error: c++ exception: " << e.what() << endl; - NativeScriptException nsEx(ss.str()); - nsEx.ReThrowToJava(); - } catch (...) { - NativeScriptException nsEx(std::string("Error: c++ exception!")); - nsEx.ReThrowToJava(); - } -} - extern "C" JNIEXPORT jobject Java_com_tns_Runtime_runScript(JNIEnv* _env, jobject obj, jint runtimeId, jstring scriptFile) { jobject result = nullptr; @@ -380,105 +352,6 @@ extern "C" JNIEXPORT jint Java_com_tns_Runtime_getCurrentRuntimeIdLegacy(JNIEnv* return getCurrentRuntimeIdCritical_impl(); } -extern "C" JNIEXPORT void Java_com_tns_Runtime_WorkerGlobalOnMessageCallback(JNIEnv* env, jobject obj, jint runtimeId, jstring msg) { - // Worker Thread runtime - auto runtime = TryGetRuntime(runtimeId); - if (runtime == nullptr) { - // TODO: Pete: Log message informing the developer of the failure - } - - auto isolate = runtime->GetIsolate(); - - v8::Locker locker(isolate); - v8::Isolate::Scope isolate_scope(isolate); - v8::HandleScope handleScope(isolate); - auto context = runtime->GetContext(); - v8::Context::Scope context_scope(context); - - CallbackHandlers::WorkerGlobalOnMessageCallback(isolate, msg); -} - -extern "C" JNIEXPORT void Java_com_tns_Runtime_WorkerObjectOnMessageCallback(JNIEnv* env, jobject obj, jint runtimeId, jint workerId, jstring msg) { - // Main Thread runtime - auto runtime = TryGetRuntime(runtimeId); - if (runtime == nullptr) { - // TODO: Pete: Log message informing the developer of the failure - } - - auto isolate = runtime->GetIsolate(); - - v8::Locker locker(isolate); - v8::Isolate::Scope isolate_scope(isolate); - v8::HandleScope handleScope(isolate); - auto context = runtime->GetContext(); - v8::Context::Scope context_scope(context); - - CallbackHandlers::WorkerObjectOnMessageCallback(isolate, workerId, msg); -} - -extern "C" JNIEXPORT void Java_com_tns_Runtime_TerminateWorkerCallback(JNIEnv* env, jobject obj, jint runtimeId) { - // Worker Thread runtime - auto runtime = TryGetRuntime(runtimeId); - if (runtime == nullptr) { - // TODO: Pete: Log message informing the developer of the failure - } - - auto isolate = runtime->GetIsolate(); - - { - v8::Locker locker(isolate); - v8::Isolate::Scope isolate_scope(isolate); - v8::HandleScope handleScope(isolate); - - CallbackHandlers::TerminateWorkerThread(isolate); - - runtime->DestroyRuntime(); - } - - isolate->Dispose(); - - delete runtime; -} - -extern "C" JNIEXPORT void Java_com_tns_Runtime_ClearWorkerPersistent(JNIEnv* env, jobject obj, jint runtimeId, jint workerId) { - // Worker Thread runtime - auto runtime = TryGetRuntime(runtimeId); - if (runtime == nullptr) { - // TODO: Pete: Log message informing the developer of the failure - } - - auto isolate = runtime->GetIsolate(); - - v8::Locker locker(isolate); - v8::Isolate::Scope isolate_scope(isolate); - v8::HandleScope handleScope(isolate); - auto context = runtime->GetContext(); - v8::Context::Scope context_scope(context); - - CallbackHandlers::ClearWorkerPersistent(workerId); -} - -extern "C" JNIEXPORT void Java_com_tns_Runtime_CallWorkerObjectOnErrorHandleMain(JNIEnv* env, jobject obj, jint runtimeId, jint workerId, jstring message, jstring stackTrace, jstring filename, jint lineno, jstring threadName) { - // Main Thread runtime - auto runtime = TryGetRuntime(runtimeId); - if (runtime == nullptr) { - // TODO: Pete: Log message informing the developer of the failure - } - - auto isolate = runtime->GetIsolate(); - v8::Locker locker(isolate); - v8::Isolate::Scope isolate_scope(isolate); - v8::HandleScope handleScope(isolate); - auto context = runtime->GetContext(); - v8::Context::Scope context_scope(context); - - try { - CallbackHandlers::CallWorkerObjectOnErrorHandle(isolate, workerId, message, stackTrace, filename, lineno, threadName); - } catch (NativeScriptException& e) { - e.ReThrowToJava(); - } -} - extern "C" JNIEXPORT void Java_com_tns_Runtime_ResetDateTimeConfigurationCache(JNIEnv* _env, jobject obj, jint runtimeId) { auto runtime = TryGetRuntime(runtimeId); if (runtime == nullptr) { diff --git a/test-app/runtime/src/main/cpp/conversions/objects/JSToJavaObjectsConverter.cpp b/test-app/runtime/src/main/cpp/conversions/objects/JSToJavaObjectsConverter.cpp index e92bb9e26..d9ad333d2 100644 --- a/test-app/runtime/src/main/cpp/conversions/objects/JSToJavaObjectsConverter.cpp +++ b/test-app/runtime/src/main/cpp/conversions/objects/JSToJavaObjectsConverter.cpp @@ -118,7 +118,8 @@ bool tns::ConvertJavaScriptObject( obj = objectManager->GetJavaObjectByJsObject(jsObject); if (obj.IsNull() && (jsObject->IsTypedArray() || jsObject->IsArrayBuffer() || - jsObject->IsArrayBufferView())) { + jsObject->IsArrayBufferView() || + jsObject->IsSharedArrayBuffer())) { JavaObjectCastType bufferCastType = tns::JavaObjectCastType::Byte; std::shared_ptr store; @@ -130,6 +131,10 @@ bool tns::ConvertJavaScriptObject( auto array = jsObject.As(); store = array->GetBackingStore(); length = array->ByteLength(); + } else if (jsObject->IsSharedArrayBuffer()) { + auto array = jsObject.As(); + store = array->GetBackingStore(); + length = array->ByteLength(); } else if (jsObject->IsArrayBufferView()) { auto array = jsObject.As(); diff --git a/test-app/runtime/src/main/java/com/tns/JavaScriptErrorMessage.java b/test-app/runtime/src/main/java/com/tns/JavaScriptErrorMessage.java deleted file mode 100644 index 851bccd06..000000000 --- a/test-app/runtime/src/main/java/com/tns/JavaScriptErrorMessage.java +++ /dev/null @@ -1,41 +0,0 @@ -package com.tns; - -public class JavaScriptErrorMessage { - private String message; - private String stackTrace; - private String filename; - private int lineno; - private String threadName; - - JavaScriptErrorMessage(String message, String stackTrace, String filename, int lineno) { - this.message = message; - this.filename = filename; - this.stackTrace = stackTrace; - this.lineno = lineno; - } - - JavaScriptErrorMessage(String message, String stackTrace, String filename, int lineno, String threadName) { - this(message, stackTrace, filename, lineno); - this.threadName = threadName; - } - - public String getMessage() { - return message; - } - - public String getStackTrace() { - return stackTrace; - } - - public String getFilename() { - return filename; - } - - public int getLineno() { - return lineno; - } - - public String getThreadName() { - return threadName; - } -} diff --git a/test-app/runtime/src/main/java/com/tns/MessageType.java b/test-app/runtime/src/main/java/com/tns/MessageType.java deleted file mode 100644 index efd94e188..000000000 --- a/test-app/runtime/src/main/java/com/tns/MessageType.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.tns; - -/** - * Created by pkanev on 8/30/2016. - */ -public class MessageType { - public static int Handshake = 0; - public static int MainToWorker = 1; - public static int WorkerToMain = 2; - public static int TerminateThread = 4; - public static int CloseWorker = 6; - public static int BubbleUpException = 7; - public static int TerminateAndCloseThread = 8; -} 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 b610ddb0c..bb8f3db19 100644 --- a/test-app/runtime/src/main/java/com/tns/Runtime.java +++ b/test-app/runtime/src/main/java/com/tns/Runtime.java @@ -1,10 +1,7 @@ package com.tns; import android.os.Handler; -import android.os.HandlerThread; import android.os.Looper; -import android.os.Message; -import android.os.Process; import com.tns.bindings.ProxyGenerator; import com.tns.system.classes.caching.impl.ClassCacheImpl; @@ -27,10 +24,8 @@ import java.util.Date; import java.util.HashMap; import java.util.Map; -import java.util.Queue; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.ExecutionException; import java.util.concurrent.FutureTask; import java.util.concurrent.RunnableFuture; @@ -45,8 +40,6 @@ private native void initNativeScript(int runtimeId, String filesPath, String nat private native void runModule(int runtimeId, String filePath) throws NativeScriptException; - private native void runWorker(int runtimeId, String filePath) throws NativeScriptException; - 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; @@ -112,16 +105,6 @@ public static void SetManualInstrumentationMode(String mode) { } } - private static native void WorkerGlobalOnMessageCallback(int runtimeId, String message); - - private static native void WorkerObjectOnMessageCallback(int runtimeId, int workerId, String message); - - private static native void TerminateWorkerCallback(int runtimeId); - - private static native void ClearWorkerPersistent(int runtimeId, int workerId); - - private static native void CallWorkerObjectOnErrorHandleMain(int runtimeId, int workerId, String message, String stackTrace, String filename, int lineno, String threadName) throws NativeScriptException; - private static native void ResetDateTimeConfigurationCache(int runtimeId); void passUncaughtExceptionToJs(Throwable ex, String message, String fullStackTrace, String jsStackTrace) { @@ -205,30 +188,16 @@ public int compare(Method lhs, Method rhs) { private final int runtimeId; - private boolean isTerminating; - /* - Used to map to Handler, for messaging between Main and the other Workers + The worker's id (0 for the main thread's runtime) */ private final int workerId; - /* - Used by all Worker threads to communicate with the Main thread - */ - private Handler mainThreadHandler; - private static AtomicInteger nextRuntimeId = new AtomicInteger(0); private final static ThreadLocal currentRuntime = new ThreadLocal(); private final static Map runtimeCache = new ConcurrentHashMap<>(); - public static Map> pendingWorkerMessages = new ConcurrentHashMap<>(); public static boolean nativeLibraryLoaded; - /* - Holds reference to all Worker Threads' handlers - Note: Should only be used on the main thread - */ - private Map workerIdToHandler = new HashMap<>(); - 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) { @@ -258,9 +227,6 @@ public Runtime(StaticConfiguration config, DynamicConfiguration dynamicConfigura this.dynamicConfig = dynamicConfiguration; this.threadScheduler = dynamicConfiguration.myThreadScheduler; this.workerId = dynamicConfiguration.workerId; - if (dynamicConfiguration.mainThreadScheduler != null) { - this.mainThreadHandler = dynamicConfiguration.mainThreadScheduler.getHandler(); - } // if multithreadedJS, make all maps concurrent or synchronized: if (config.appConfig.getEnableMultithreadedJavascript()) { this.strongInstances = new ConcurrentHashMap<>(); @@ -273,13 +239,15 @@ public Runtime(StaticConfiguration config, DynamicConfiguration dynamicConfigura } classResolver = new ClassResolver(classStorageService); - currentRuntime.set(this); - - runtimeCache.put(this.runtimeId, this); - gcListener = GcListener.getInstance(config.appConfig.getGcThrottleTime(), config.appConfig.getMemoryCheckInterval(), config.appConfig.getFreeMemoryRatio()); // capture static configuration to allow native lookups when currentRuntime is unavailable staticConfiguration = config; + + // publish the instance only after everything that can throw has + // completed, so a failed construction is never reachable through + // the thread-local or the cache + currentRuntime.set(this); + runtimeCache.put(this.runtimeId, this); } finally { frame.close(); } @@ -515,225 +483,73 @@ public void releaseNativeCounterpart(int nativeObjectId) { } } - private static class WorkerThreadHandler extends Handler { - - WorkerThreadHandler(Looper looper){ - super(looper); - } - - @Override - public void handleMessage(Message msg) { - Runtime currentRuntime = Runtime.getCurrentRuntime(); - - if (currentRuntime.isTerminating) { - if (currentRuntime.logger.isEnabled()) { - currentRuntime.logger.write("Worker(id=" + currentRuntime.workerId + ") is terminating, it will not process the message."); - } - - return; - } - - /* - Handle messages coming from the Main thread - */ - if (msg.arg1 == MessageType.MainToWorker) { - /* - Calls the Worker script's onmessage implementation with arg -> msg.obj.toString() - */ - WorkerGlobalOnMessageCallback(currentRuntime.runtimeId, msg.obj.toString()); - } else if (msg.arg1 == MessageType.TerminateThread) { - currentRuntime.isTerminating = true; - GcListener.unsubscribe(currentRuntime); - - runtimeCache.remove(currentRuntime.runtimeId); - - TerminateWorkerCallback(currentRuntime.runtimeId); - - if (currentRuntime.logger.isEnabled()) { - currentRuntime.logger.write("Worker(id=" + currentRuntime.workerId + ", name=\"" + Thread.currentThread().getName() + "\") has terminated execution. Don't make further function calls to it."); - } - - this.getLooper().quit(); - } else if (msg.arg1 == MessageType.TerminateAndCloseThread) { - Message msgToMain = Message.obtain(); - msgToMain.arg1 = MessageType.CloseWorker; - msgToMain.arg2 = currentRuntime.workerId; - - currentRuntime.mainThreadHandler.sendMessage(msgToMain); - - currentRuntime.isTerminating = true; - GcListener.unsubscribe(currentRuntime); - - runtimeCache.remove(currentRuntime.runtimeId); - - TerminateWorkerCallback(currentRuntime.runtimeId); - - if (currentRuntime.logger.isEnabled()) { - currentRuntime.logger.write("Worker(id=" + currentRuntime.workerId + ", name=\"" + Thread.currentThread().getName() + "\") has terminated execution. Don't make further function calls to it."); - } - - this.getLooper().quit(); - } - } + /* + This method initializes the runtime and should always be called first and through the main thread + in order to set static configuration that all following workers can use + */ + public static Runtime initializeRuntimeWithConfiguration(StaticConfiguration config) { + staticConfiguration = config; + WorkThreadScheduler mainThreadScheduler = new WorkThreadScheduler(new Handler(Looper.myLooper())); + DynamicConfiguration dynamicConfiguration = new DynamicConfiguration(0, mainThreadScheduler, null); + Runtime runtime = initRuntime(dynamicConfiguration); + return runtime; } - private static class WorkerThread extends HandlerThread { - - private Integer workerId; - private ThreadScheduler mainThreadScheduler; - private String filePath; - private String callingJsDir; - - public WorkerThread(String name, Integer workerId, ThreadScheduler mainThreadScheduler, String callingJsDir) { - super("W" + workerId + ": " + name); - this.filePath = name; - this.workerId = workerId; - this.mainThreadScheduler = mainThreadScheduler; - this.callingJsDir = callingJsDir; - } - - public void startRuntime() { - final Handler handler = new Handler(this.getLooper()); - - handler.post((new Runnable() { - @Override - public void run() { - Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); - - WorkThreadScheduler workThreadScheduler = new WorkThreadScheduler(new WorkerThreadHandler(handler.getLooper())); - - DynamicConfiguration dynamicConfiguration = new DynamicConfiguration(workerId, workThreadScheduler, mainThreadScheduler, callingJsDir); - - if (staticConfiguration.logger.isEnabled()) { - staticConfiguration.logger.write("Worker (id=" + workerId + ")'s Runtime is initializing!"); - } - - Runtime runtime = initRuntime(dynamicConfiguration); - - if (staticConfiguration.logger.isEnabled()) { - staticConfiguration.logger.write("Worker (id=" + workerId + ")'s Runtime initialized!"); - } - - /* - Send a message to the Main Thread to `shake hands`, - Main Thread will cache the Worker Handler for later use - */ - Message msg = Message.obtain(); - msg.arg1 = MessageType.Handshake; - msg.arg2 = runtime.runtimeId; + /* + Called via JNI from a native-spawned worker thread (WorkerWrapper), after the + thread has attached to the JVM and before the worker script runs. + Prepares the thread's Java Looper - this must happen before initRuntime so + that the native runtime (timers, worker inbox, message loop) binds its fds + to the looper that runWorkerLoop later pumps - and creates the worker's + Runtime with a Looper-backed scheduler so cross-thread Java->JS calls + (dispatchCallJSMethodNative, runScript) keep working. + Should only be called after the static configuration has been initialized. + */ + @RuntimeCallable + public static int initWorkerRuntime(int workerId, String callingJsDir) { + Looper.prepare(); - runtime.mainThreadHandler.sendMessage(msg); - runtime.runWorker(runtime.runtimeId, filePath); + WorkThreadScheduler workThreadScheduler = new WorkThreadScheduler(new Handler(Looper.myLooper())); + DynamicConfiguration dynamicConfiguration = new DynamicConfiguration(workerId, workThreadScheduler, null, callingJsDir); - runtime.processPendingMessages(); - } - })); + if (staticConfiguration.logger.isEnabled()) { + staticConfiguration.logger.write("Worker (id=" + workerId + ")'s Runtime is initializing!"); } - } - private void processPendingMessages() { - Queue messages = Runtime.pendingWorkerMessages.get(this.getWorkerId()); - if (messages == null) { - return; - } - - Handler handler = this.getHandler(); - while (!messages.isEmpty()) { - handler.sendMessage(messages.poll()); - } - } + Runtime runtime = initRuntime(dynamicConfiguration); - private static class MainThreadHandler extends Handler { - public MainThreadHandler(Looper looper) { - super(looper); + if (staticConfiguration.logger.isEnabled()) { + staticConfiguration.logger.write("Worker (id=" + workerId + ")'s Runtime initialized!"); } - @Override - public void handleMessage(Message msg) { - /* - Handle messages coming from a Worker thread - */ - if (msg.arg1 == MessageType.WorkerToMain) { - /* - Calls the Worker (with id - workerId) object's onmessage implementation with arg -> msg.obj.toString() - */ - WorkerObjectOnMessageCallback(Runtime.getCurrentRuntime().runtimeId, msg.arg2, msg.obj.toString()); - } - /* - Handle a 'Handshake' message sent from a new Worker, - so that the Main may cache it and send messages to it later - */ - else if (msg.arg1 == MessageType.Handshake) { - int senderRuntimeId = msg.arg2; - Runtime workerRuntime = runtimeCache.get(senderRuntimeId); - Runtime mainRuntime = Runtime.getCurrentRuntime(); - - // If worker has had its close/terminate called before the threads could shake hands - if (workerRuntime == null) { - if (mainRuntime.logger.isEnabled()) { - mainRuntime.logger.write("Main thread couldn't shake hands with worker (runtimeId: " + workerRuntime + ") because it has been terminated!"); - } - - return; - } - - /* - Main thread now has a reference to the Worker's handler, - so messaging between the two threads can begin - */ - mainRuntime.workerIdToHandler.put(workerRuntime.getWorkerId(), workerRuntime.getHandler()); - - if (mainRuntime.logger.isEnabled()) { - mainRuntime.logger.write("Worker thread (workerId:" + workerRuntime.getWorkerId() + ") shook hands with the main thread!"); - } - } else if (msg.arg1 == MessageType.CloseWorker) { - Runtime currentRuntime = Runtime.getCurrentRuntime(); - - // remove reference to a Worker thread's handler that is in the process of closing - currentRuntime.workerIdToHandler.put(msg.arg2, null); - - ClearWorkerPersistent(currentRuntime.runtimeId, msg.arg2); - } - /* - Handle unhandled exceptions/errors coming from the worker thread - */ - else if (msg.arg1 == MessageType.BubbleUpException) { - Runtime currentRuntime = Runtime.getCurrentRuntime(); - - int workerId = msg.arg2; - JavaScriptErrorMessage errorMessage = (JavaScriptErrorMessage) msg.obj; - - CallWorkerObjectOnErrorHandleMain(currentRuntime.runtimeId, workerId, errorMessage.getMessage(), errorMessage.getStackTrace(), errorMessage.getFilename(), errorMessage.getLineno(), errorMessage.getThreadName()); - } - } + return runtime.getRuntimeId(); } - /* - This method initializes the runtime and should always be called first and through the main thread - in order to set static configuration that all following workers can use + Called via JNI from the native worker thread. Blocks pumping the worker's + looper (Java Handler messages and the native fds registered on it) until + the looper is quit by WorkerWrapper on terminate()/close(). */ - public static Runtime initializeRuntimeWithConfiguration(StaticConfiguration config) { - staticConfiguration = config; - WorkThreadScheduler mainThreadScheduler = new WorkThreadScheduler(new MainThreadHandler(Looper.myLooper())); - DynamicConfiguration dynamicConfiguration = new DynamicConfiguration(0, mainThreadScheduler, null); - Runtime runtime = initRuntime(dynamicConfiguration); - return runtime; + @RuntimeCallable + public static void runWorkerLoop() { + Looper.loop(); } /* - This method should be called via native code only after the static configuration has been initialized. - It will use the static configuration for all following calls to initialize a new runtime. + Called via JNI from the native worker thread during shutdown, before the + worker's isolate is disposed. */ @RuntimeCallable - public static void initWorker(String jsFileName, String callingJsDir, int id) { - // This method will always be called from the Main thread - Runtime runtime = Runtime.getCurrentRuntime(); - ThreadScheduler mainThreadScheduler = runtime.getDynamicConfig().myThreadScheduler; + public static void detachWorkerRuntime(int runtimeId) { + Runtime runtime = runtimeCache.remove(runtimeId); + if (runtime != null) { + GcListener.unsubscribe(runtime); - WorkerThread worker = new WorkerThread(jsFileName, id, mainThreadScheduler, callingJsDir); - worker.start(); - worker.startRuntime(); + if (runtime.logger != null && runtime.logger.isEnabled()) { + runtime.logger.write("Worker(id=" + runtime.workerId + ", name=\"" + Thread.currentThread().getName() + "\") has terminated execution. Don't make further function calls to it."); + } + } + currentRuntime.remove(); } /* @@ -742,8 +558,18 @@ public static void initWorker(String jsFileName, String callingJsDir, int id) { */ private static Runtime initRuntime(DynamicConfiguration dynamicConfiguration) { Runtime runtime = new Runtime(staticConfiguration, dynamicConfiguration); - runtime.init(); - runtime.runScript(new File(staticConfiguration.appDir, "internal/ts_helpers.js")); + try { + runtime.init(); + runtime.runScript(new File(staticConfiguration.appDir, "internal/ts_helpers.js")); + } catch (Throwable t) { + // the constructor already registered the instance - roll back so a + // failed bootstrap doesn't leave a stale, half-initialized runtime + // reachable through the caches + runtimeCache.remove(runtime.getRuntimeId()); + currentRuntime.remove(); + GcListener.unsubscribe(runtime); + throw t; + } return runtime; } @@ -1546,161 +1372,4 @@ private static boolean useGlobalRefs() { return useGlobalRefs; } - /* - ====================================================================== - ====================================================================== - Workers messaging callbacks - ====================================================================== - ====================================================================== - */ - @RuntimeCallable - public static void sendMessageFromMainToWorker(int workerId, String message) { - Runtime currentRuntime = Runtime.getCurrentRuntime(); - - Message msg = Message.obtain(); - msg.obj = message; - msg.arg1 = MessageType.MainToWorker; - - boolean hasKey = currentRuntime.workerIdToHandler.containsKey(workerId); - Handler workerHandler = currentRuntime.workerIdToHandler.get(workerId); - - // TODO: Pete: Ensure that we won't end up in an endless loop. Can we get an invalid workerId? - /* - If workHandler is null then the new Worker Thread still hasn't completed initializing - - OR - - The workHandler is null because it has been closed; Check if its key is still in the map - */ - if (workerHandler == null) { - // Attempt to send a message to a closed worker, throw error or just log a message - if (hasKey) { - if (currentRuntime.logger.isEnabled()) { - currentRuntime.logger.write("Worker(id=" + msg.arg2 + ") that you are trying to send a message to has been terminated. No message will be sent."); - } - - return; - } - - if (currentRuntime.logger.isEnabled()) { - currentRuntime.logger.write("Worker(id=" + msg.arg2 + ")'s handler still not initialized. Requeueing message for Worker(id=" + msg.arg2 + ")"); - } - - if (pendingWorkerMessages.get(workerId) == null) { - pendingWorkerMessages.put(workerId, new ConcurrentLinkedQueue()); - } - - Queue messages = pendingWorkerMessages.get(workerId); - messages.add(msg); - - return; - } - - if (!workerHandler.getLooper().getThread().isAlive()) { - return; - } - - workerHandler.sendMessage(msg); - } - - @RuntimeCallable - public static void sendMessageFromWorkerToMain(String message) { - Runtime currentRuntime = Runtime.getCurrentRuntime(); - - Message msg = Message.obtain(); - msg.arg1 = MessageType.WorkerToMain; - - /* - Send the workerId associated with the JavaScript Worker object - */ - msg.arg2 = currentRuntime.getWorkerId(); - msg.obj = message; - - currentRuntime.mainThreadHandler.sendMessage(msg); - } - - @RuntimeCallable - public static void workerObjectTerminate(int workerId) { - // Thread should always be main here - Runtime currentRuntime = Runtime.getCurrentRuntime(); - final long ResendDelay = 1000; - - Message msg = Message.obtain(); - - boolean hasKey = currentRuntime.workerIdToHandler.containsKey(workerId); - Handler workerHandler = currentRuntime.workerIdToHandler.get(workerId); - - msg.arg1 = MessageType.TerminateThread; - msg.arg2 = workerId; - - /* - If workHandler is null then the new Worker Thread still hasn't completed initializing - - OR - - The workHandler is null because it has been closed; Check if its key is still in the map - */ - if (workerHandler == null) { - // Attempt to send a message to a closed worker, throw error or just log a message - if (hasKey) { - if (currentRuntime.logger.isEnabled()) { - currentRuntime.logger.write("Worker(id=" + msg.arg2 + ") is already terminated. No message will be sent."); - } - - return; - } else { - if (currentRuntime.logger.isEnabled()) { - currentRuntime.logger.write("Worker(id=" + msg.arg2 + ")'s handler still not initialized. Requeueing terminate() message for Worker(id=" + msg.arg2 + ")"); - } - - if (pendingWorkerMessages.get(workerId) == null) { - pendingWorkerMessages.put(workerId, new ConcurrentLinkedQueue()); - } - - Queue messages = pendingWorkerMessages.get(workerId); - messages.add(msg); - return; - } - } - - // Worker was closed during this 'terminate' call, nothing to do here - if (!workerHandler.getLooper().getThread().isAlive()) { - return; - } - - // 'terminate' message must be executed immediately - workerHandler.sendMessageAtFrontOfQueue(msg); - - // Set value for workerId key to null - currentRuntime.workerIdToHandler.put(workerId, null); - } - - @RuntimeCallable - public static void workerScopeClose() { - // Thread should always be a worker - Runtime currentRuntime = Runtime.getCurrentRuntime(); - - Message msgToWorker = Message.obtain(); - msgToWorker.arg1 = MessageType.TerminateAndCloseThread; - - currentRuntime.getHandler().sendMessageAtFrontOfQueue(msgToWorker); - } - - @RuntimeCallable - public static void passUncaughtExceptionFromWorkerToMain(String message, String filename, String stackTrace, int lineno) { - // Thread should always be a worker - Runtime currentRuntime = Runtime.getCurrentRuntime(); - - Message msg = Message.obtain(); - msg.arg1 = MessageType.BubbleUpException; - msg.arg2 = currentRuntime.workerId; - - String threadName = currentRuntime.getHandler().getLooper().getThread().getName(); - JavaScriptErrorMessage error = new JavaScriptErrorMessage(message, stackTrace, filename, lineno, threadName); - - msg.obj = error; - - // TODO: Pete: Should we treat the message with higher priority? - currentRuntime.mainThreadHandler.sendMessage(msg); - } }