From 72d1826ae3a8847ba1e4fd33621a02d495258496 Mon Sep 17 00:00:00 2001 From: Andy Street Date: Tue, 12 Jan 2016 04:51:13 -0800 Subject: [PATCH] WebWorkers: Implement initial WebWorkers API Summary: public Implements a basic WebWorkers API that allows posting messages between the main JS thread and a worker background thread. It follows the existing webworkers API from JS. Currently passed memory needs to be JSON serializable and is copied (unfortunately, this is what webkit does as well, but with a more advanced serialization/deserialization process). There are a lot of TODO's: I'll add tasks for them once this is accepted. Reviewed By: lexs Differential Revision: D2779349 fb-gh-sync-id: 8ed04c115d36acf0264ef1f6a12a65dd0c14ff18 --- .../react/bridge/CatalystInstanceImpl.java | 3 +- .../ProxyQueueThreadExceptionHandler.java | 34 ++++++ .../react/bridge/webworkers/WebWorkers.java | 29 +++++ ReactAndroid/src/main/jni/react/Bridge.cpp | 2 +- ReactAndroid/src/main/jni/react/Executor.h | 2 +- .../src/main/jni/react/JSCExecutor.cpp | 110 ++++++++++++++++- ReactAndroid/src/main/jni/react/JSCExecutor.h | 34 +++++- .../src/main/jni/react/JSCWebWorker.cpp | 115 ++++++++++++++++++ .../src/main/jni/react/JSCWebWorker.h | 84 +++++++++++++ .../main/jni/react/jni/JMessageQueueThread.h | 4 + .../src/main/jni/react/jni/WebWorkers.h | 29 +++++ 11 files changed, 441 insertions(+), 5 deletions(-) create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/queue/ProxyQueueThreadExceptionHandler.java create mode 100644 ReactAndroid/src/main/java/com/facebook/react/bridge/webworkers/WebWorkers.java create mode 100644 ReactAndroid/src/main/jni/react/JSCWebWorker.cpp create mode 100644 ReactAndroid/src/main/jni/react/JSCWebWorker.h create mode 100644 ReactAndroid/src/main/jni/react/jni/WebWorkers.h 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); + } +}; + +} }