Skip to content

Commit

Permalink
Implement queueMicrotask and drainMicrotasks in JSC
Browse files Browse the repository at this point in the history
Summary:
Changelog: [internal]

## Context

We want to enable the new React Native event loop by default for all users on the new RN architecture (on the bridgeless initialization path more concretely), which requires support for microtasks in all the JS engines that the support (Hermes already has it, JSC doesn't).

## Changes

This adds initial support for microtasks in JSC, so we can schedule and execute microtasks in this runtime.

One limitation about this approach is that, AFAIK, the public API for JSC doesn't allow us to customize its internal microtask queue or specify the method to be used by its built-in `Promise` or native `async function`, so we're forced to continue using a polyfill in that case (which uses `setImmediate` that will be mapped to `queueMicrotask`).

Reviewed By: NickGerleman

Differential Revision: D54302534

fbshipit-source-id: 47f71620344a81bc6624917f77452106ffbf55a3
  • Loading branch information
rubennorte authored and cipolleschi committed Jun 28, 2024
1 parent d818083 commit 3f558bd
Showing 1 changed file with 30 additions and 2 deletions.
32 changes: 30 additions & 2 deletions packages/react-native/ReactCommon/jsc/JSCRuntime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
#include <atomic>
#include <condition_variable>
#include <cstdlib>
#include <deque>
#include <mutex>
#include <queue>
#include <sstream>
#include <thread>

Expand Down Expand Up @@ -51,6 +51,12 @@ class JSCRuntime : public jsi::Runtime {
const std::shared_ptr<const jsi::Buffer>& buffer,
const std::string& sourceURL) override;

// If we use this interface to implement microtasks in the host we need to
// polyfill `Promise` to use these methods, because JSC doesn't currently
// support providing a custom queue for its built-in implementation.
// Not doing this would result in a non-compliant behavior, as microtasks
// wouldn't execute in the order in which they were queued.
void queueMicrotask(const jsi::Function& callback) override;
bool drainMicrotasks(int maxMicrotasksHint = -1) override;

jsi::Object global() override;
Expand Down Expand Up @@ -265,6 +271,7 @@ class JSCRuntime : public jsi::Runtime {
std::atomic<bool> ctxInvalid_;
std::string desc_;
JSValueRef nativeStateSymbol_ = nullptr;
std::deque<jsi::Function> microtaskQueue_;
#ifndef NDEBUG
mutable std::atomic<intptr_t> objectCounter_;
mutable std::atomic<intptr_t> symbolCounter_;
Expand Down Expand Up @@ -380,6 +387,10 @@ JSCRuntime::JSCRuntime(JSGlobalContextRef ctx)
}

JSCRuntime::~JSCRuntime() {
// We need to clear the microtask queue to remove all references to the
// callbacks, so objectCounter_ would be 0 below.
microtaskQueue_.clear();

// On shutting down and cleaning up: when JSC is actually torn down,
// it calls JSC::Heap::lastChanceToFinalize internally which
// finalizes anything left over. But at this point,
Expand Down Expand Up @@ -436,7 +447,24 @@ jsi::Value JSCRuntime::evaluateJavaScript(
return createValue(res);
}

bool JSCRuntime::drainMicrotasks(int maxMicrotasksHint) {
void JSCRuntime::queueMicrotask(const jsi::Function& callback) {
microtaskQueue_.emplace_back(
jsi::Value(*this, callback).asObject(*this).asFunction(*this));
}

bool JSCRuntime::drainMicrotasks(int /*maxMicrotasksHint*/) {
// Note that new jobs can be enqueued during the draining.
while (!microtaskQueue_.empty()) {
jsi::Function callback = std::move(microtaskQueue_.front());

// We need to pop before calling the callback because that might throw.
// When that happens, the host will call `drainMicrotasks` again to execute
// the remaining microtasks, and this one shouldn't run again.
microtaskQueue_.pop_front();

callback.call(*this);
}

return true;
}

Expand Down

0 comments on commit 3f558bd

Please sign in to comment.