diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java index da3750cfe..70af8e380 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/CatalystInstanceImpl.java @@ -402,7 +402,8 @@ public class CatalystInstanceImpl implements CatalystInstance { private void decrementPendingJSCalls() { int newPendingCalls = mPendingJSCalls.decrementAndGet(); - Assertions.assertCondition(newPendingCalls >= 0); + // TODO(9604406): handle case of web workers injecting messages to main thread + //Assertions.assertCondition(newPendingCalls >= 0); boolean isNowIdle = newPendingCalls == 0; Systrace.traceCounter( Systrace.TRACE_TAG_REACT_JAVA_BRIDGE, diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/ProxyQueueThreadExceptionHandler.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/ProxyQueueThreadExceptionHandler.java new file mode 100644 index 000000000..1be2d8a3c --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/queue/ProxyQueueThreadExceptionHandler.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.bridge.queue; + +/** + * An Exception handler that posts the Exception to be thrown on the given delegate + * MessageQueueThread. + */ +public class ProxyQueueThreadExceptionHandler implements QueueThreadExceptionHandler { + + private final MessageQueueThread mDelegateThread; + + public ProxyQueueThreadExceptionHandler(MessageQueueThread delegateThread) { + mDelegateThread = delegateThread; + } + + @Override + public void handleException(final Exception e) { + mDelegateThread.runOnQueue( + new Runnable() { + @Override + public void run() { + throw new RuntimeException(e); + } + }); + } +} diff --git a/ReactAndroid/src/main/java/com/facebook/react/bridge/webworkers/WebWorkers.java b/ReactAndroid/src/main/java/com/facebook/react/bridge/webworkers/WebWorkers.java new file mode 100644 index 000000000..1cc874e9c --- /dev/null +++ b/ReactAndroid/src/main/java/com/facebook/react/bridge/webworkers/WebWorkers.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ + +package com.facebook.react.bridge.webworkers; + +import com.facebook.proguard.annotations.DoNotStrip; +import com.facebook.react.bridge.queue.MessageQueueThread; +import com.facebook.react.bridge.queue.MessageQueueThreadImpl; +import com.facebook.react.bridge.queue.ProxyQueueThreadExceptionHandler; + +@DoNotStrip +public class WebWorkers { + + /** + * Creates a new MessageQueueThread for a background web worker owned by the JS thread with the + * given MessageQueueThread. + */ + public static MessageQueueThread createWebWorkerThread(int id, MessageQueueThread ownerThread) { + return MessageQueueThreadImpl.startNewBackgroundThread( + "web-worker-" + id, + new ProxyQueueThreadExceptionHandler(ownerThread)); + } +} diff --git a/ReactAndroid/src/main/jni/react/Bridge.cpp b/ReactAndroid/src/main/jni/react/Bridge.cpp index 90a50d098..b9b0580c4 100644 --- a/ReactAndroid/src/main/jni/react/Bridge.cpp +++ b/ReactAndroid/src/main/jni/react/Bridge.cpp @@ -18,7 +18,7 @@ public: JSThreadState(const RefPtr& jsExecutorFactory, Bridge::Callback&& callback) : m_callback(callback) { - m_jsExecutor = jsExecutorFactory->createJSExecutor([this, callback] (std::string queueJSON) { + m_jsExecutor = jsExecutorFactory->createJSExecutor([this, callback] (std::string queueJSON, bool isEndOfBatch) { m_callback(parseMethodCalls(queueJSON), false /* = isEndOfBatch */); }); } diff --git a/ReactAndroid/src/main/jni/react/Executor.h b/ReactAndroid/src/main/jni/react/Executor.h index bef2d0724..dd09d5906 100644 --- a/ReactAndroid/src/main/jni/react/Executor.h +++ b/ReactAndroid/src/main/jni/react/Executor.h @@ -18,7 +18,7 @@ namespace react { class JSExecutor; -typedef std::function FlushImmediateCallback; +typedef std::function FlushImmediateCallback; class JSExecutorFactory : public Countable { public: diff --git a/ReactAndroid/src/main/jni/react/JSCExecutor.cpp b/ReactAndroid/src/main/jni/react/JSCExecutor.cpp index a657a72f1..aa1102294 100644 --- a/ReactAndroid/src/main/jni/react/JSCExecutor.cpp +++ b/ReactAndroid/src/main/jni/react/JSCExecutor.cpp @@ -3,6 +3,7 @@ #include "JSCExecutor.h" #include +#include #include #include #include @@ -11,6 +12,7 @@ #include #include #include "Value.h" +#include "jni/JMessageQueueThread.h" #include "jni/OnLoad.h" #ifdef WITH_JSC_EXTRA_TRACING @@ -48,6 +50,7 @@ namespace facebook { namespace react { static std::unordered_map s_globalContextRefToJSCExecutor; + static JSValueRef nativeFlushQueueImmediate( JSContextRef ctx, JSObjectRef function, @@ -96,10 +99,13 @@ std::unique_ptr JSCExecutorFactory::createJSExecutor(FlushImmediateC JSCExecutor::JSCExecutor(FlushImmediateCallback cb) : m_flushImmediateCallback(cb) { m_context = JSGlobalContextCreateInGroup(nullptr, nullptr); + m_messageQueueThread = JMessageQueueThread::currentMessageQueueThread(); s_globalContextRefToJSCExecutor[m_context] = this; installGlobalFunction(m_context, "nativeFlushQueueImmediate", nativeFlushQueueImmediate); installGlobalFunction(m_context, "nativeLoggingHook", nativeLoggingHook); installGlobalFunction(m_context, "nativePerformanceNow", nativePerformanceNow); + installGlobalFunction(m_context, "nativeStartWorker", nativeStartWorker); + installGlobalFunction(m_context, "nativePostMessageToWorker", nativePostMessageToWorker); #ifdef WITH_FB_JSC_TUNING configureJSCForAndroid(); @@ -219,13 +225,55 @@ void JSCExecutor::handleMemoryPressureCritical() { } void JSCExecutor::flushQueueImmediate(std::string queueJSON) { - m_flushImmediateCallback(queueJSON); + m_flushImmediateCallback(queueJSON, false); +} + +// WebWorker impl + +JSGlobalContextRef JSCExecutor::getContext() { + return m_context; +} + +std::shared_ptr JSCExecutor::getMessageQueueThread() { + return m_messageQueueThread; +} + +void JSCExecutor::onMessageReceived(int workerId, const std::string& json) { + Value rebornJSMsg = Value::fromJSON(m_context, String(json.c_str())); + JSValueRef args[] = { rebornJSMsg }; + Object& worker = m_webWorkerJSObjs.at(workerId); + + Value onmessageValue = worker.getProperty("onmessage"); + if (onmessageValue.isUndefined()) { + return; + } + onmessageValue.asObject().callAsFunction(1, args); + + m_flushImmediateCallback(flush(), true); +} + +int JSCExecutor::addWebWorker(const std::string& script, JSValueRef workerRef) { + static std::atomic_int nextWorkerId(0); + int workerId = nextWorkerId++; + + m_webWorkers.emplace(std::piecewise_construct, std::forward_as_tuple(workerId), std::forward_as_tuple(workerId, this, script)); + Object workerObj = Value(m_context, workerRef).asObject(); + workerObj.makeProtected(); + m_webWorkerJSObjs.emplace(workerId, std::move(workerObj)); + return workerId; +} + +void JSCExecutor::postMessageToWebWorker(int workerId, JSValueRef message, JSValueRef *exn) { + JSCWebWorker& worker = m_webWorkers.at(workerId); + worker.postMessage(message); } static JSValueRef createErrorString(JSContextRef ctx, const char *msg) { return JSValueMakeString(ctx, String(msg)); } +// Native JS hooks + static JSValueRef nativeFlushQueueImmediate( JSContextRef ctx, JSObjectRef function, @@ -253,6 +301,66 @@ static JSValueRef nativeFlushQueueImmediate( return JSValueMakeUndefined(ctx); } +JSValueRef JSCExecutor::nativeStartWorker( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception) { + if (argumentCount != 2) { + *exception = createErrorString(ctx, "Got wrong number of args"); + return JSValueMakeUndefined(ctx); + } + + std::string scriptFile = Value(ctx, arguments[0]).toString().str(); + + JSValueRef worker = arguments[1]; + + JSCExecutor *executor; + try { + executor = s_globalContextRefToJSCExecutor.at(JSContextGetGlobalContext(ctx)); + } catch (std::out_of_range& e) { + *exception = createErrorString(ctx, "Global JS context didn't map to a valid executor"); + return JSValueMakeUndefined(ctx); + } + + int workerId = executor->addWebWorker(scriptFile, worker); + + return JSValueMakeNumber(ctx, workerId); +} + +JSValueRef JSCExecutor::nativePostMessageToWorker( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception) { + if (argumentCount != 2) { + *exception = createErrorString(ctx, "Got wrong number of args"); + return JSValueMakeUndefined(ctx); + } + + double workerDouble = JSValueToNumber(ctx, arguments[0], exception); + if (workerDouble != workerDouble) { + *exception = createErrorString(ctx, "Got invalid worker id"); + return JSValueMakeUndefined(ctx); + } + + JSCExecutor *executor; + try { + executor = s_globalContextRefToJSCExecutor.at(JSContextGetGlobalContext(ctx)); + } catch (std::out_of_range& e) { + *exception = createErrorString(ctx, "Global JS context didn't map to a valid executor"); + return JSValueMakeUndefined(ctx); + } + + executor->postMessageToWebWorker((int) workerDouble, arguments[1], exception); + + return JSValueMakeUndefined(ctx); +} + static JSValueRef nativeLoggingHook( JSContextRef ctx, JSObjectRef function, diff --git a/ReactAndroid/src/main/jni/react/JSCExecutor.h b/ReactAndroid/src/main/jni/react/JSCExecutor.h index 507424735..030648bb9 100644 --- a/ReactAndroid/src/main/jni/react/JSCExecutor.h +++ b/ReactAndroid/src/main/jni/react/JSCExecutor.h @@ -2,20 +2,28 @@ #pragma once +#include +#include #include #include "Executor.h" #include "JSCHelpers.h" +#include "JSCWebWorker.h" namespace facebook { namespace react { +class JMessageQueueThread; + class JSCExecutorFactory : public JSExecutorFactory { public: virtual std::unique_ptr createJSExecutor(FlushImmediateCallback cb) override; }; -class JSCExecutor : public JSExecutor { +class JSCExecutor : public JSExecutor, public JSCWebWorkerOwner { public: + /** + * Should be invoked from the JS thread. + */ explicit JSCExecutor(FlushImmediateCallback flushImmediateCallback); ~JSCExecutor() override; @@ -41,10 +49,34 @@ public: void flushQueueImmediate(std::string queueJSON); void installNativeHook(const char *name, JSObjectCallAsFunctionCallback callback); + virtual void onMessageReceived(int workerId, const std::string& message) override; + virtual JSGlobalContextRef getContext() override; + virtual std::shared_ptr getMessageQueueThread() override; private: JSGlobalContextRef m_context; FlushImmediateCallback m_flushImmediateCallback; + std::unordered_map m_webWorkers; + std::unordered_map m_webWorkerJSObjs; + std::shared_ptr m_messageQueueThread; + + int addWebWorker(const std::string& script, JSValueRef workerRef); + void postMessageToWebWorker(int worker, JSValueRef message, JSValueRef *exn); + + static JSValueRef nativeStartWorker( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception); + static JSValueRef nativePostMessageToWorker( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception); }; } } diff --git a/ReactAndroid/src/main/jni/react/JSCWebWorker.cpp b/ReactAndroid/src/main/jni/react/JSCWebWorker.cpp new file mode 100644 index 000000000..b385c1472 --- /dev/null +++ b/ReactAndroid/src/main/jni/react/JSCWebWorker.cpp @@ -0,0 +1,115 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include "JSCWebWorker.h" +#include "JSCHelpers.h" +#include "jni/JMessageQueueThread.h" +#include "jni/JSLoader.h" +#include "jni/WebWorkers.h" +#include "Value.h" + +#include + +using namespace facebook::jni; + +namespace facebook { +namespace react { + +// TODO(9604425): thread safety +static std::unordered_map s_globalContextRefToJSCWebWorker; + +JSCWebWorker::JSCWebWorker(int id, JSCWebWorkerOwner *owner, std::string scriptSrc) : + id_(id), + scriptName_(std::move(scriptSrc)), + owner_(owner) { + ownerMessageQueueThread_ = owner->getMessageQueueThread(); + FBASSERTMSGF(ownerMessageQueueThread_, "Owner MessageQueueThread must not be null"); + workerMessageQueueThread_ = WebWorkers::createWebWorkerThread(id, ownerMessageQueueThread_.get()); + FBASSERTMSGF(workerMessageQueueThread_, "Failed to create worker thread"); + + workerMessageQueueThread_->runOnQueue([this] () { + initJSVMAndLoadScript(); + }); +} + +JSCWebWorker::~JSCWebWorker() { + // TODO(9604430): Implement tear down +} + +void JSCWebWorker::postMessage(JSValueRef msg) { + std::string msgString = Value(owner_->getContext(), msg).toJSONString(); + + workerMessageQueueThread_->runOnQueue([this, msgString] () { + if (isFinished()) { + return; + } + Value rebornJSMsg = Value::fromJSON(context_, String(msgString.c_str())); + JSValueRef args[] = { rebornJSMsg }; + Value onmessageValue = Object::getGlobalObject(context_).getProperty("onmessage"); + onmessageValue.asObject().callAsFunction(1, args); + }); +} + +void JSCWebWorker::finish() { + isFinished_ = true; + // TODO(9604430): Implement tear down +} + +bool JSCWebWorker::isFinished() { + return isFinished_; +} + +void JSCWebWorker::initJSVMAndLoadScript() { + FBASSERTMSGF(!isFinished(), "Worker was already finished!"); + FBASSERTMSGF(!context_, "Worker JS VM was already created!"); + + context_ = JSGlobalContextCreateInGroup( + NULL, // use default JS 'global' object + NULL // create new group (i.e. new VM) + ); + s_globalContextRefToJSCWebWorker[context_] = this; + + // TODO(9604438): Protect against script does not exist + std::string script = loadScriptFromAssets(scriptName_); + evaluateScript(context_, String(script.c_str()), String(scriptName_.c_str())); + + installGlobalFunction(context_, "postMessage", nativePostMessage); +} + +void JSCWebWorker::postMessageToOwner(JSValueRef msg) { + std::string msgString = Value(context_, msg).toJSONString(); + ownerMessageQueueThread_->runOnQueue([this, msgString] () { + owner_->onMessageReceived(id_, msgString); + }); +} + +JSValueRef JSCWebWorker::nativePostMessage( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception) { + if (argumentCount != 1) { + *exception = makeJSCException(ctx, "postMessage got wrong number of arguments"); + return JSValueMakeUndefined(ctx); + } + JSValueRef msg = arguments[0]; + JSCWebWorker *webWorker = s_globalContextRefToJSCWebWorker.at(JSContextGetGlobalContext(ctx)); + webWorker->postMessageToOwner(msg); + + return JSValueMakeUndefined(ctx); +} + +} +} diff --git a/ReactAndroid/src/main/jni/react/JSCWebWorker.h b/ReactAndroid/src/main/jni/react/JSCWebWorker.h new file mode 100644 index 000000000..b4a45ef97 --- /dev/null +++ b/ReactAndroid/src/main/jni/react/JSCWebWorker.h @@ -0,0 +1,84 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#include +#include +#include +#include +#include + +#include + +#include "Value.h" + +using namespace facebook::jni; + +namespace facebook { +namespace react { + +class JMessageQueueThread; + +/** + * A class that can own the lifecycle, receive messages from, and dispatch messages + * to JSCWebWorkers. + */ +class JSCWebWorkerOwner { +public: + /** + * Called when a worker has posted a message with `postMessage`. + */ + virtual void onMessageReceived(int workerId, const std::string& message) = 0; + virtual JSGlobalContextRef getContext() = 0; + + /** + * Should return the owner's MessageQueueThread. Calls to onMessageReceived will be enqueued + * on this thread. + */ + virtual std::shared_ptr getMessageQueueThread() = 0; +}; + +/** + * Implementation of a web worker for JSC. The web worker should be created from the owner's + * (e.g., owning JSCExecutor instance) JS MessageQueueThread. The worker is responsible for + * creating its own MessageQueueThread. + * + * During operation, the JSCExecutor should call postMessage **from its own MessageQueueThread** + * to send messages to the worker. The worker will handle enqueueing those messages on its own + * MessageQueueThread as appropriate. When the worker has a message to post to the owner, it will + * enqueue a call to owner->onMessageReceived on the owner's MessageQueueThread. + */ +class JSCWebWorker { +public: + explicit JSCWebWorker(int id, JSCWebWorkerOwner *owner, std::string script); + ~JSCWebWorker(); + + /** + * Post a message to be received by the worker on its thread. This must be called from + * ownerMessageQueueThread_. + */ + void postMessage(JSValueRef msg); + void finish(); + bool isFinished(); +private: + void initJSVMAndLoadScript(); + void postRunnableToEventLoop(std::function&& runnable); + void postMessageToOwner(JSValueRef result); + + int id_; + bool isFinished_ = false; + std::string scriptName_; + JSCWebWorkerOwner *owner_ = nullptr; + std::shared_ptr ownerMessageQueueThread_; + std::unique_ptr workerMessageQueueThread_; + JSGlobalContextRef context_ = nullptr; + + static JSValueRef nativePostMessage( + JSContextRef ctx, + JSObjectRef function, + JSObjectRef thisObject, + size_t argumentCount, + const JSValueRef arguments[], + JSValueRef *exception); +}; + +} +} diff --git a/ReactAndroid/src/main/jni/react/jni/JMessageQueueThread.h b/ReactAndroid/src/main/jni/react/jni/JMessageQueueThread.h index 1a0f52bc9..c2c875a6d 100644 --- a/ReactAndroid/src/main/jni/react/jni/JMessageQueueThread.h +++ b/ReactAndroid/src/main/jni/react/jni/JMessageQueueThread.h @@ -25,6 +25,10 @@ public: */ void runOnQueue(std::function&& runnable); + MessageQueueThread::javaobject jobj() { + return m_jobj.get(); + } + /** * Returns the current MessageQueueThread that owns this thread. */ diff --git a/ReactAndroid/src/main/jni/react/jni/WebWorkers.h b/ReactAndroid/src/main/jni/react/jni/WebWorkers.h new file mode 100644 index 000000000..2a506280a --- /dev/null +++ b/ReactAndroid/src/main/jni/react/jni/WebWorkers.h @@ -0,0 +1,29 @@ +// Copyright 2004-present Facebook. All Rights Reserved. + +#pragma once + +#include + +#include +#include + +#include "JMessageQueueThread.h" + +using namespace facebook::jni; + +namespace facebook { +namespace react { + +class WebWorkers : public JavaClass { +public: + static constexpr auto kJavaDescriptor = "Lcom/facebook/react/bridge/webworkers/WebWorkers;"; + + static std::unique_ptr createWebWorkerThread(int id, JMessageQueueThread *ownerMessageQueueThread) { + static auto method = WebWorkers::javaClassStatic()-> + getStaticMethod("createWebWorkerThread"); + auto res = method(WebWorkers::javaClassStatic(), id, ownerMessageQueueThread->jobj()); + return folly::make_unique(res); + } +}; + +} }