From 73108477589a18cecb303ef556fa3da02f8ca1b8 Mon Sep 17 00:00:00 2001 From: Xuan Huang Date: Fri, 23 Apr 2021 02:41:41 -0700 Subject: [PATCH] Perform Engine Microtasks in JSIExecutor Summary: Changelog: [Internal] This diff introduce a helper `performMicrotaskCheckpoint` to repetitively invoke `jsi::Runtime::drainMicrotasks` to exhaust the microtasks queue provided by JS VMs. Please refer to `jsi.h` for more details about the behavior of the API. Conceptually, the checkpoint needs to be performed whenever the JS stack is considered empty. In practice, we can just make a call whenever RN is returned from C++->JS calls. In the current RN, this happened in JSIExecutor: - `::callFunction` => `callFunctionReturnFlushedQueue_-> call` - `::invokeCallback` => `invokeCallbackAndReturnFlushedQueue_-> call` - `::flush` => `flushedQueue_->call` Each of them invoke a bound method on the JS bridge object. Note that `setImmediate` callbacks are executed before they returned. This means immmediates are invoked before engine microtasks. This is okay because the priority between `setImmediate` and engine "microtask" are not defined. (`setImmediate` is non-standard and RN already treat `setImmediate` in a similar priority as microtask for the existing Promise polyfill.) Reviewed By: RSNara Differential Revision: D27729702 fbshipit-source-id: b64b3705d2ff5100075d860c89f03a847369b7ac --- .../jsiexecutor/jsireact/JSIExecutor.cpp | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/ReactCommon/jsiexecutor/jsireact/JSIExecutor.cpp b/ReactCommon/jsiexecutor/jsireact/JSIExecutor.cpp index f1d4c517060..f1d16bacb39 100644 --- a/ReactCommon/jsiexecutor/jsireact/JSIExecutor.cpp +++ b/ReactCommon/jsiexecutor/jsireact/JSIExecutor.cpp @@ -7,6 +7,7 @@ #include "jsireact/JSIExecutor.h" +#include #include #include #include @@ -204,6 +205,30 @@ void JSIExecutor::registerBundle( ReactMarker::REGISTER_JS_SEGMENT_STOP, tag.c_str()); } +// Looping on \c drainMicrotasks until it completes or hits the retries bound. +static void performMicrotaskCheckpoint(jsi::Runtime &runtime) { + uint8_t retries = 0; + // A heuristic number to guard inifinite or absurd numbers of retries. + const static unsigned int kRetriesBound = 255; + + while (retries < kRetriesBound) { + try { + // The default behavior of \c drainMicrotasks is unbounded execution. + // We may want to make it bounded in the future. + if (runtime.drainMicrotasks()) { + break; + } + } catch (jsi::JSError &error) { + handleJSError(runtime, error, true); + } + retries++; + } + + if (retries == kRetriesBound) { + throw std::runtime_error("Hits microtasks retries bound."); + } +} + void JSIExecutor::callFunction( const std::string &moduleId, const std::string &methodId, @@ -240,6 +265,8 @@ void JSIExecutor::callFunction( std::runtime_error("Error calling " + moduleId + "." + methodId)); } + performMicrotaskCheckpoint(*runtime_); + callNativeModules(ret, true); } @@ -259,6 +286,8 @@ void JSIExecutor::invokeCallback( folly::to("Error invoking callback ", callbackId))); } + performMicrotaskCheckpoint(*runtime_); + callNativeModules(ret, true); } @@ -394,7 +423,9 @@ void JSIExecutor::callNativeModules(const Value &queue, bool isEndOfBatch) { void JSIExecutor::flush() { SystraceSection s("JSIExecutor::flush"); if (flushedQueue_) { - callNativeModules(flushedQueue_->call(*runtime_), true); + Value ret = flushedQueue_->call(*runtime_); + performMicrotaskCheckpoint(*runtime_); + callNativeModules(ret, true); return; } @@ -410,7 +441,9 @@ void JSIExecutor::flush() { // If calls were made, we bind to the JS bridge methods, and use them to // get the pending queue of native calls. bindBridge(); - callNativeModules(flushedQueue_->call(*runtime_), true); + Value ret = flushedQueue_->call(*runtime_); + performMicrotaskCheckpoint(*runtime_); + callNativeModules(ret, true); } else if (delegate_) { // If we have a delegate, we need to call it; we pass a null list to // callNativeModules, since we know there are no native calls, without