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
This commit is contained in:
Andy Street
2016-01-12 04:51:13 -08:00
committed by facebook-github-bot-4
parent dd60964736
commit 72d1826ae3
11 changed files with 441 additions and 5 deletions

View File

@@ -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,

View File

@@ -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);
}
});
}
}

View File

@@ -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));
}
}

View File

@@ -18,7 +18,7 @@ public:
JSThreadState(const RefPtr<JSExecutorFactory>& 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 */);
});
}

View File

@@ -18,7 +18,7 @@ namespace react {
class JSExecutor;
typedef std::function<void(std::string)> FlushImmediateCallback;
typedef std::function<void(std::string, bool)> FlushImmediateCallback;
class JSExecutorFactory : public Countable {
public:

View File

@@ -3,6 +3,7 @@
#include "JSCExecutor.h"
#include <algorithm>
#include <atomic>
#include <sstream>
#include <string>
#include <fb/log.h>
@@ -11,6 +12,7 @@
#include <jni/fbjni/Exceptions.h>
#include <sys/time.h>
#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<JSContextRef, JSCExecutor*> s_globalContextRefToJSCExecutor;
static JSValueRef nativeFlushQueueImmediate(
JSContextRef ctx,
JSObjectRef function,
@@ -96,10 +99,13 @@ std::unique_ptr<JSExecutor> 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<JMessageQueueThread> 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,

View File

@@ -2,20 +2,28 @@
#pragma once
#include <memory>
#include <unordered_map>
#include <JavaScriptCore/JSContextRef.h>
#include "Executor.h"
#include "JSCHelpers.h"
#include "JSCWebWorker.h"
namespace facebook {
namespace react {
class JMessageQueueThread;
class JSCExecutorFactory : public JSExecutorFactory {
public:
virtual std::unique_ptr<JSExecutor> 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<JMessageQueueThread> getMessageQueueThread() override;
private:
JSGlobalContextRef m_context;
FlushImmediateCallback m_flushImmediateCallback;
std::unordered_map<int, JSCWebWorker> m_webWorkers;
std::unordered_map<int, Object> m_webWorkerJSObjs;
std::shared_ptr<JMessageQueueThread> 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);
};
} }

View File

@@ -0,0 +1,115 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#include <unistd.h>
#include <mutex>
#include <pthread.h>
#include <unordered_map>
#include <fb/assert.h>
#include <fb/log.h>
#include <folly/Memory.h>
#include <jni/fbjni/Exceptions.h>
#include <jni/LocalReference.h>
#include "JSCWebWorker.h"
#include "JSCHelpers.h"
#include "jni/JMessageQueueThread.h"
#include "jni/JSLoader.h"
#include "jni/WebWorkers.h"
#include "Value.h"
#include <JavaScriptCore/JSValueRef.h>
using namespace facebook::jni;
namespace facebook {
namespace react {
// TODO(9604425): thread safety
static std::unordered_map<JSContextRef, JSCWebWorker*> 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);
}
}
}

View File

@@ -0,0 +1,84 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#include <functional>
#include <mutex>
#include <string>
#include <thread>
#include <queue>
#include <JavaScriptCore/JSValueRef.h>
#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<JMessageQueueThread> 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<void()>&& runnable);
void postMessageToOwner(JSValueRef result);
int id_;
bool isFinished_ = false;
std::string scriptName_;
JSCWebWorkerOwner *owner_ = nullptr;
std::shared_ptr<JMessageQueueThread> ownerMessageQueueThread_;
std::unique_ptr<JMessageQueueThread> workerMessageQueueThread_;
JSGlobalContextRef context_ = nullptr;
static JSValueRef nativePostMessage(
JSContextRef ctx,
JSObjectRef function,
JSObjectRef thisObject,
size_t argumentCount,
const JSValueRef arguments[],
JSValueRef *exception);
};
}
}

View File

@@ -25,6 +25,10 @@ public:
*/
void runOnQueue(std::function<void()>&& runnable);
MessageQueueThread::javaobject jobj() {
return m_jobj.get();
}
/**
* Returns the current MessageQueueThread that owns this thread.
*/

View File

@@ -0,0 +1,29 @@
// Copyright 2004-present Facebook. All Rights Reserved.
#pragma once
#include <memory>
#include <jni.h>
#include <folly/Memory.h>
#include "JMessageQueueThread.h"
using namespace facebook::jni;
namespace facebook {
namespace react {
class WebWorkers : public JavaClass<WebWorkers> {
public:
static constexpr auto kJavaDescriptor = "Lcom/facebook/react/bridge/webworkers/WebWorkers;";
static std::unique_ptr<JMessageQueueThread> createWebWorkerThread(int id, JMessageQueueThread *ownerMessageQueueThread) {
static auto method = WebWorkers::javaClassStatic()->
getStaticMethod<MessageQueueThread::javaobject(jint, MessageQueueThread::javaobject)>("createWebWorkerThread");
auto res = method(WebWorkers::javaClassStatic(), id, ownerMessageQueueThread->jobj());
return folly::make_unique<JMessageQueueThread>(res);
}
};
} }