Skip to content

Commit

Permalink
Implement support for microtasks in RuntimeScheduler (#41084)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #41084

Adds support for executing microtasks in `RuntimeScheduler`, the same way we did in `JSIExecutor` before (removed in D49536251 / #40870) but now after each actual task in the scheduler.

When we use microtasks in the scheduler, we ignore calls to execute expired tasks (which was used to call "React Native microtasks" that we had before). Those should now be regular microtasks in the runtime.

This is gated behind a feature flag until we've tested this broadly.

This is going to be tested in Hermes but we need to add support for microtasks in JSC (which has a no-op in its JSI interface).

Changelog: [internal]

Reviewed By: sammy-SC

Differential Revision: D49536262

fbshipit-source-id: 8f7ce54c266d1f25312a641abc4ef073d019281f
  • Loading branch information
rubennorte authored and facebook-github-bot committed Oct 23, 2023
1 parent 4bf96f1 commit 04042de
Show file tree
Hide file tree
Showing 12 changed files with 159 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ target_link_libraries(react_render_runtimescheduler
react_debug
react_render_core
react_render_debug
react_utils
runtimeexecutor)
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Pod::Spec.new do |s|
s.dependency "React-jsi"
s.dependency "React-runtimeexecutor"
s.dependency "React-callinvoker"
s.dependency "React-cxxreact"
s.dependency "React-debug"
s.dependency "React-rendererdebug"
s.dependency "React-utils"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,42 @@
#include "RuntimeScheduler_Modern.h"
#include "SchedulerPriorityUtils.h"

#include <cxxreact/ErrorUtils.h>
#include <react/renderer/debug/SystraceSection.h>
#include <react/utils/CoreFeatures.h>
#include <utility>
#include "ErrorUtils.h"

namespace facebook::react {

namespace {
// Looping on \c drainMicrotasks until it completes or hits the retries bound.
void executeMicrotasks(jsi::Runtime& runtime) {
SystraceSection s("RuntimeScheduler::executeMicrotasks");

uint8_t retries = 0;
// A heuristic number to guard infinite 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.");
}
}
} // namespace

#pragma mark - Public

RuntimeScheduler_Modern::RuntimeScheduler_Modern(
Expand Down Expand Up @@ -135,8 +165,12 @@ void RuntimeScheduler_Modern::executeNowOnTheSameThread(
}
}

// This will be replaced by microtasks
void RuntimeScheduler_Modern::callExpiredTasks(jsi::Runtime& runtime) {
// If we have first-class support for microtasks, this a no-op.
if (CoreFeatures::enableMicrotasks) {
return;
}

SystraceSection s("RuntimeScheduler::callExpiredTasks");
startWorkLoop(runtime, true);
}
Expand Down Expand Up @@ -241,17 +275,29 @@ void RuntimeScheduler_Modern::executeTask(
currentTask_ = task;
currentPriority_ = task->priority;

executeMacrotask(runtime, task, didUserCallbackTimeout);

if (CoreFeatures::enableMicrotasks) {
executeMicrotasks(runtime);
}

// TODO report long tasks
// TODO update rendering
}

void RuntimeScheduler_Modern::executeMacrotask(
jsi::Runtime& runtime,
std::shared_ptr<Task> task,
bool didUserCallbackTimeout) const {
SystraceSection s("RuntimeScheduler::executeMacrotask");

auto result = task->execute(runtime, didUserCallbackTimeout);

if (result.isObject() && result.getObject(runtime).isFunction(runtime)) {
// If the task returned a continuation callback, we re-assign it to the task
// and keep the task in the queue.
task->callback = result.getObject(runtime).getFunction(runtime);
}

// TODO execute microtasks
// TODO report long tasks
// TODO update rendering
}

} // namespace facebook::react
Original file line number Diff line number Diff line change
Expand Up @@ -152,14 +152,21 @@ class RuntimeScheduler_Modern final : public RuntimeSchedulerBase {
void scheduleTask(std::shared_ptr<Task> task);

/**
* Follows all the steps necessary to execute the given task (in the future,
* this will include executing microtasks, flushing rendering work, etc.)
* Follows all the steps necessary to execute the given task.
* Depending on feature flags, this could also execute its microtasks.
* In the future, this will include other steps in the Web event loop, like
* updating the UI in native, executing resize observer callbacks, etc.
*/
void executeTask(
jsi::Runtime& runtime,
const std::shared_ptr<Task>& task,
RuntimeSchedulerTimePoint currentTime);

void executeMacrotask(
jsi::Runtime& runtime,
std::shared_ptr<Task> task,
bool didUserCallbackTimeout) const;

/*
* Returns a time point representing the current point in time. May be called
* from multiple threads.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <hermes/hermes.h>
#include <jsi/jsi.h>
#include <react/renderer/runtimescheduler/RuntimeScheduler.h>
#include <react/utils/CoreFeatures.h>
#include <memory>
#include <semaphore>

Expand All @@ -24,7 +25,18 @@ class RuntimeSchedulerTest : public testing::TestWithParam<bool> {
protected:
void SetUp() override {
hostFunctionCallCount_ = 0;
runtime_ = facebook::hermes::makeHermesRuntime();

auto useModernRuntimeScheduler = GetParam();

CoreFeatures::enableMicrotasks = useModernRuntimeScheduler;

// Configuration that enables microtasks
::hermes::vm::RuntimeConfig::Builder runtimeConfigBuilder =
::hermes::vm::RuntimeConfig::Builder().withMicrotaskQueue(
useModernRuntimeScheduler);

runtime_ =
facebook::hermes::makeHermesRuntime(runtimeConfigBuilder.build());
stubErrorUtils_ = StubErrorUtils::createAndInstallIfNeeded(*runtime_);
stubQueue_ = std::make_unique<StubQueue>();

Expand All @@ -42,8 +54,6 @@ class RuntimeSchedulerTest : public testing::TestWithParam<bool> {
return stubClock_->getNow();
};

auto useModernRuntimeScheduler = GetParam();

runtimeScheduler_ = std::make_unique<RuntimeScheduler>(
runtimeExecutor, useModernRuntimeScheduler, stubNow);
}
Expand Down Expand Up @@ -114,6 +124,54 @@ TEST_P(RuntimeSchedulerTest, scheduleSingleTask) {
EXPECT_EQ(stubQueue_->size(), 0);
}

TEST_P(RuntimeSchedulerTest, scheduleSingleTaskWithMicrotasks) {
// Only for modern runtime scheduler
if (!GetParam()) {
return;
}

bool didRunTask = false;
bool didRunMicrotask = false;

auto callback = createHostFunctionFromLambda([&](bool /* unused */) {
didRunTask = true;

auto microtaskCallback = jsi::Function::createFromHostFunction(
*runtime_,
jsi::PropNameID::forUtf8(*runtime_, ""),
3,
[&](jsi::Runtime& /*unused*/,
const jsi::Value& /*unused*/,
const jsi::Value* arguments,
size_t /*unused*/) -> jsi::Value {
didRunMicrotask = true;
return jsi::Value::undefined();
});

// Hermes doesn't expose a C++ API to schedule microtasks, so we just access
// the API that it exposes to JS.
auto global = runtime_->global();
auto enqueueJobFn = global.getPropertyAsObject(*runtime_, "HermesInternal")
.getPropertyAsFunction(*runtime_, "enqueueJob");

enqueueJobFn.call(*runtime_, std::move(microtaskCallback));

return jsi::Value::undefined();
});

runtimeScheduler_->scheduleTask(
SchedulerPriority::NormalPriority, std::move(callback));

EXPECT_FALSE(didRunTask);
EXPECT_EQ(stubQueue_->size(), 1);

stubQueue_->tick();

EXPECT_TRUE(didRunTask);
EXPECT_TRUE(didRunMicrotask);
EXPECT_EQ(stubQueue_->size(), 0);
}

TEST_P(RuntimeSchedulerTest, scheduleImmediatePriorityTask) {
bool didRunTask = false;
auto callback =
Expand Down Expand Up @@ -521,7 +579,8 @@ TEST_P(RuntimeSchedulerTest, normalTaskYieldsToSynchronousAccess) {

EXPECT_EQ(syncTaskExecutionCount, 1);
EXPECT_TRUE(runtimeScheduler_->getShouldYield());
// The previous task is still in the queue (although it was executed already).
// The previous task is still in the queue (although it was executed
// already).
EXPECT_EQ(stubQueue_->size(), 1);

// Just empty the queue
Expand Down Expand Up @@ -597,8 +656,8 @@ TEST_P(RuntimeSchedulerTest, immediateTaskYieldsToSynchronousAccess) {

EXPECT_EQ(syncTaskExecutionCount, 1);
EXPECT_TRUE(runtimeScheduler_->getShouldYield());
// The previous task is still in the queue (although it was executed already),
// so the sync task scheduled the work loop to process it.
// The previous task is still in the queue (although it was executed
// already), so the sync task scheduled the work loop to process it.
EXPECT_EQ(stubQueue_->size(), 1);

// Just empty the queue
Expand Down Expand Up @@ -708,7 +767,7 @@ TEST_P(RuntimeSchedulerTest, sameThreadTaskCreatesImmediatePriorityTask) {
runtimeScheduler_->scheduleTask(
SchedulerPriority::ImmediatePriority, std::move(callback));

runtimeScheduler_->callExpiredTasks(runtime);
EXPECT_FALSE(didRunSubsequentTask);
});
});

Expand All @@ -724,7 +783,15 @@ TEST_P(RuntimeSchedulerTest, sameThreadTaskCreatesImmediatePriorityTask) {
t1.join();

EXPECT_TRUE(didRunSynchronousTask);
EXPECT_FALSE(didRunSubsequentTask);

EXPECT_EQ(stubQueue_->size(), 1);

stubQueue_->tick();

EXPECT_TRUE(didRunSubsequentTask);

EXPECT_EQ(stubQueue_->size(), 0);
}

TEST_P(RuntimeSchedulerTest, sameThreadTaskCreatesLowPriorityTask) {
Expand All @@ -746,11 +813,6 @@ TEST_P(RuntimeSchedulerTest, sameThreadTaskCreatesLowPriorityTask) {
runtimeScheduler_->scheduleTask(
SchedulerPriority::LowPriority, std::move(callback));

// Only for legacy runtime scheduler
if (!GetParam()) {
runtimeScheduler_->callExpiredTasks(runtime);
}

EXPECT_FALSE(didRunSubsequentTask);
});
});
Expand Down Expand Up @@ -844,7 +906,8 @@ TEST_P(RuntimeSchedulerTest, modernTwoThreadsRequestAccessToTheRuntime) {
// Notify that the second task can be scheduled.
signalTask1ToScheduleTask2.release();

// Wait for the second task to be scheduled before finishing this task
// Wait for the second task to be scheduled before finishing this
// task
signalTask2ToResumeTask1.acquire();

didRunSynchronousTask1 = true;
Expand All @@ -860,8 +923,8 @@ TEST_P(RuntimeSchedulerTest, modernTwoThreadsRequestAccessToTheRuntime) {

// Notify the first task that it can resume execution.
// As we can't do this after the task this from thread has been scheduled
// (because it's synchronous), we can just do a short wait instead in a new
// thread.
// (because it's synchronous), we can just do a short wait instead in a
// new thread.
std::thread t3([&signalTask2ToResumeTask1]() {
std::chrono::duration<int, std::milli> timespan(50);
std::this_thread::sleep_for(timespan);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@ target_link_libraries(
fb
jsi
jsireact
react_utils
)
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ Pod::Spec.new do |s|
s.dependency "React-jsi"
s.dependency "React-jserrorhandler"
s.dependency "React-runtimescheduler"
s.dependency "React-utils"

if ENV["USE_HERMES"] == nil || ENV["USE_HERMES"] == "1"
s.dependency "hermes-engine"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Pod::Spec.new do |s|
s.dependency "React-jsi"
s.dependency "React-nativeconfig"
s.dependency "React-jsitracing"
s.dependency "React-utils"

if ENV["USE_HERMES"] == nil || ENV["USE_HERMES"] == "1"
s.dependency "hermes-engine"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
#include <jsi/instrumentation.h>
#include <jsireact/JSIExecutor.h>
#include <react/renderer/runtimescheduler/RuntimeSchedulerBinding.h>
#include <react/utils/CoreFeatures.h>

#include <cxxreact/ReactMarker.h>
#include <iostream>
Expand Down Expand Up @@ -65,8 +66,14 @@ ReactInstance::ReactInstance(
SystraceSection s("ReactInstance::_runtimeExecutor[Callback]");
try {
callback(*strongRuntime);
if (auto strongTimerManager = weakTimerManager.lock()) {
strongTimerManager->callReactNativeMicrotasks(*strongRuntime);

// If we have first-class support for microtasks,
// they would've been called as part of the previous callback.
if (!CoreFeatures::enableMicrotasks) {
if (auto strongTimerManager = weakTimerManager.lock()) {
strongTimerManager->callReactNativeMicrotasks(
*strongRuntime);
}
}
} catch (jsi::JSError& originalError) {
handleJSError(*strongRuntime, originalError, true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,8 @@ void TimerManager::callTimer(uint32_t timerID) {
void TimerManager::attachGlobals(jsi::Runtime& runtime) {
// Install host functions for timers.
// TODO (T45786383): Add missing timer functions from JSTimers
// TODL (T96212789): Skip immediate APIs when JSVM microtask queue is used.
// TODO (T96212789): Remove when JSVM microtask queue is used everywhere in
// bridgeless mode. This is being overwritten in JS in that case.
runtime.global().setProperty(
runtime,
"setImmediate",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,6 @@ bool CoreFeatures::enableGranularShadowTreeStateReconciliation = false;
bool CoreFeatures::enableDefaultAsyncBatchedPriority = false;
bool CoreFeatures::enableClonelessStateProgression = false;
bool CoreFeatures::excludeYogaFromRawProps = false;
bool CoreFeatures::enableMicrotasks = false;

} // namespace facebook::react
4 changes: 4 additions & 0 deletions packages/react-native/ReactCommon/react/utils/CoreFeatures.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ class CoreFeatures {

// When enabled, rawProps in Props will not include Yoga specific props.
static bool excludeYogaFromRawProps;

// Enables the use of microtasks in Hermes (scheduling) and RuntimeScheduler
// (execution).
static bool enableMicrotasks;
};

} // namespace facebook::react

0 comments on commit 04042de

Please sign in to comment.