Skip to content

Commit

Permalink
Create new version of RuntimeScheduler (facebook#40944)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: facebook#40944

## Summary

This creates a new version of `RuntimeScheduler` that's intended to be backwards compatible but with a few notable changes:
1. `scheduleTask` is now thread-safe.
2. `scheduleWork` is now just an alias of `scheduleTask` with immediate priority (to preserve the yielding semantics it had over other tasks).
3. Yielding mechanism has changed, to make lower priority tasks to yield to higher priority tasks, instead of just yielding to `scheduleWork` and `executeNowOnTheSameThread`.

We don't expect this to have any impact in performance or user perceivable behavior, so we consider it a short-lived refactor. When we validate this assumptions in a complex application we'll delete the old version and only keep the fork.

## Motivation

The main motivation for this refactor is to reduce the amount of unnecessary interruptions of running tasks (via `shouldYield`) that are only used to schedule asynchronous tasks from native.

The `scheduleWork` method is the only available mechanism exposed to native APIs to schedule work in the JS thread (as the existing version of `scheduleTask` is only meant to be called from JS). This mechanism **always** asks for any running tasks in the scheduler to yield, so these tasks are always considered to have the highest priority. This makes sense for discrete user events, but not for many other use cases coming from native (e.g.: notifying network responses could be UserBlocking, Normal or Low depending on the use case).

We need a way to schedule tasks from native with other kinds of priorities, so we don't always have to interrupt what's currently executing if it has a higher priority than what we're scheduling.

## Changes

**General APIs:**

This centralizes scheduling in only 2 APIs in `RuntimeScheduler` (which already exist in the legacy version):
* `scheduleTask`, which is non-blocking for the caller and can be used from any thread. This always uses the task queue in the scheduler and a new yielding mechanism.
* `executeNowOnTheSameThread`, which is blocking for the caller and asks any task executing in the scheduler to yield. These tasks don't go through the task queue and instead queue through the existing synchronization mechanism in `RuntimeExecutor`. The yielding mechanism for these tasks is preserved.

`scheduleWork` will be deprecated and it's just an alias for `scheduleTask` with an immediate priority (to preserve a similar behavior).

**Yielding behavior:**

Before, tasks would only yield to tasks scheduled via `scheduleWork` and `executeNowOnTheSameThread` (those tasks didn't go through the task queue).

With this implementation, tasks would now yield to any task that has a higher position in the task queue. That means we reuse the existing mechanism to avoid lower priority tasks to never execute because higher priority tasks never stop coming.

All tasks would yield to requests for synchronous access (via `executeNowOnTheSameThread`) as did the current implementation.

Changelog: [internal]

Differential Revision: D49316881

fbshipit-source-id: 5e8f1563b7d1c34641c83667ac2b381ae4d8938c
  • Loading branch information
rubennorte authored and facebook-github-bot committed Oct 16, 2023
1 parent 6e2a947 commit f7b5a4d
Show file tree
Hide file tree
Showing 9 changed files with 1,198 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include "RuntimeScheduler.h"
#include "RuntimeScheduler_Legacy.h"
#include "RuntimeScheduler_Modern.h"
#include "SchedulerPriorityUtils.h"

#include <react/renderer/debug/SystraceSection.h>
Expand All @@ -15,11 +16,28 @@

namespace facebook::react {

namespace {
std::unique_ptr<RuntimeSchedulerBase> getRuntimeSchedulerImplementation(
RuntimeExecutor runtimeExecutor,
bool useModernRuntimeScheduler,
std::function<RuntimeSchedulerTimePoint()> now) {
if (useModernRuntimeScheduler) {
return std::make_unique<RuntimeScheduler_Modern>(
std::move(runtimeExecutor), std::move(now));
} else {
return std::make_unique<RuntimeScheduler_Legacy>(
std::move(runtimeExecutor), std::move(now));
}
}
} // namespace

RuntimeScheduler::RuntimeScheduler(
RuntimeExecutor runtimeExecutor,
bool useModernRuntimeScheduler,
std::function<RuntimeSchedulerTimePoint()> now)
: runtimeSchedulerImpl_(std::make_unique<RuntimeScheduler_Legacy>(
: runtimeSchedulerImpl_(getRuntimeSchedulerImplementation(
std::move(runtimeExecutor),
useModernRuntimeScheduler,
std::move(now))) {}

void RuntimeScheduler::scheduleWork(RawCallback&& callback) const noexcept {
Expand All @@ -28,13 +46,13 @@ void RuntimeScheduler::scheduleWork(RawCallback&& callback) const noexcept {

std::shared_ptr<Task> RuntimeScheduler::scheduleTask(
SchedulerPriority priority,
jsi::Function&& callback) noexcept {
jsi::Function&& callback) const noexcept {
return runtimeSchedulerImpl_->scheduleTask(priority, std::move(callback));
}

std::shared_ptr<Task> RuntimeScheduler::scheduleTask(
SchedulerPriority priority,
RawCallback&& callback) noexcept {
RawCallback&& callback) const noexcept {
return runtimeSchedulerImpl_->scheduleTask(priority, std::move(callback));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@ class RuntimeSchedulerBase {
virtual void executeNowOnTheSameThread(RawCallback&& callback) = 0;
virtual std::shared_ptr<Task> scheduleTask(
SchedulerPriority priority,
jsi::Function&& callback) noexcept = 0;
jsi::Function&& callback) const noexcept = 0;
virtual std::shared_ptr<Task> scheduleTask(
SchedulerPriority priority,
RawCallback&& callback) noexcept = 0;
RawCallback&& callback) const noexcept = 0;
virtual void cancelTask(Task& task) noexcept = 0;
virtual bool getShouldYield() const noexcept = 0;
virtual bool getIsSynchronous() const noexcept = 0;
Expand All @@ -38,8 +38,9 @@ class RuntimeSchedulerBase {
// at runtime based on a feature flag.
class RuntimeScheduler final : RuntimeSchedulerBase {
public:
RuntimeScheduler(
explicit RuntimeScheduler(
RuntimeExecutor runtimeExecutor,
bool useModernRuntimeScheduler = false,
std::function<RuntimeSchedulerTimePoint()> now =
RuntimeSchedulerClock::now);

Expand Down Expand Up @@ -74,11 +75,11 @@ class RuntimeScheduler final : RuntimeSchedulerBase {
*/
std::shared_ptr<Task> scheduleTask(
SchedulerPriority priority,
jsi::Function&& callback) noexcept override;
jsi::Function&& callback) const noexcept override;

std::shared_ptr<Task> scheduleTask(
SchedulerPriority priority,
RawCallback&& callback) noexcept override;
RawCallback&& callback) const noexcept override;

/*
* Cancelled task will never be executed.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,14 @@ void RuntimeScheduler_Legacy::scheduleWork(

std::shared_ptr<Task> RuntimeScheduler_Legacy::scheduleTask(
SchedulerPriority priority,
jsi::Function&& callback) noexcept {
jsi::Function&& callback) const noexcept {
SystraceSection s(
"RuntimeScheduler::scheduleTask",
"priority",
serialize(priority),
"callbackType",
"jsi::Function");

auto expirationTime = now_() + timeoutForSchedulerPriority(priority);
auto task =
std::make_shared<Task>(priority, std::move(callback), expirationTime);
Expand All @@ -51,7 +58,14 @@ std::shared_ptr<Task> RuntimeScheduler_Legacy::scheduleTask(

std::shared_ptr<Task> RuntimeScheduler_Legacy::scheduleTask(
SchedulerPriority priority,
RawCallback&& callback) noexcept {
RawCallback&& callback) const noexcept {
SystraceSection s(
"RuntimeScheduler::scheduleTask",
"priority",
serialize(priority),
"callbackType",
"RawCallback");

auto expirationTime = now_() + timeoutForSchedulerPriority(priority);
auto task =
std::make_shared<Task>(priority, std::move(callback), expirationTime);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ namespace facebook::react {

class RuntimeScheduler_Legacy final : public RuntimeSchedulerBase {
public:
RuntimeScheduler_Legacy(
explicit RuntimeScheduler_Legacy(
RuntimeExecutor runtimeExecutor,
std::function<RuntimeSchedulerTimePoint()> now =
RuntimeSchedulerClock::now);
Expand Down Expand Up @@ -55,11 +55,11 @@ class RuntimeScheduler_Legacy final : public RuntimeSchedulerBase {
*/
std::shared_ptr<Task> scheduleTask(
SchedulerPriority priority,
jsi::Function&& callback) noexcept override;
jsi::Function&& callback) const noexcept override;

std::shared_ptr<Task> scheduleTask(
SchedulerPriority priority,
RawCallback&& callback) noexcept override;
RawCallback&& callback) const noexcept override;

/*
* Cancelled task will never be executed.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
/*
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

#include "RuntimeScheduler_Modern.h"
#include "SchedulerPriorityUtils.h"

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

namespace facebook::react {

#pragma mark - Public

RuntimeScheduler_Modern::RuntimeScheduler_Modern(
RuntimeExecutor runtimeExecutor,
std::function<RuntimeSchedulerTimePoint()> now)
: runtimeExecutor_(std::move(runtimeExecutor)), now_(std::move(now)) {}

void RuntimeScheduler_Modern::scheduleWork(
RawCallback&& callback) const noexcept {
SystraceSection s("RuntimeScheduler::scheduleWork");
scheduleTask(SchedulerPriority::ImmediatePriority, std::move(callback));
}

std::shared_ptr<Task> RuntimeScheduler_Modern::scheduleTask(
SchedulerPriority priority,
jsi::Function&& callback) const noexcept {
SystraceSection s(
"RuntimeScheduler::scheduleTask",
"priority",
serialize(priority),
"callbackType",
"jsi::Function");

auto expirationTime = now_() + timeoutForSchedulerPriority(priority);
auto task =
std::make_shared<Task>(priority, std::move(callback), expirationTime);

scheduleTask(task);

return task;
}

std::shared_ptr<Task> RuntimeScheduler_Modern::scheduleTask(
SchedulerPriority priority,
RawCallback&& callback) const noexcept {
SystraceSection s(
"RuntimeScheduler::scheduleTask",
"priority",
serialize(priority),
"callbackType",
"RawCallback");

auto expirationTime = now_() + timeoutForSchedulerPriority(priority);
auto task =
std::make_shared<Task>(priority, std::move(callback), expirationTime);

scheduleTask(task);

return task;
}

bool RuntimeScheduler_Modern::getShouldYield() const noexcept {
std::shared_lock lock(taskQueueMutex_);

return syncTaskRequests_ > 0 ||
(!taskQueue_.empty() && taskQueue_.top() != currentTask_);
}

bool RuntimeScheduler_Modern::getIsSynchronous() const noexcept {
return isSynchronous_;
}

void RuntimeScheduler_Modern::cancelTask(Task& task) noexcept {
task.callback.reset();
}

SchedulerPriority RuntimeScheduler_Modern::getCurrentPriorityLevel()
const noexcept {
return currentPriority_;
}

RuntimeSchedulerTimePoint RuntimeScheduler_Modern::now() const noexcept {
return now_();
}

void RuntimeScheduler_Modern::executeNowOnTheSameThread(
RawCallback&& callback) {
SystraceSection s("RuntimeScheduler::executeNowOnTheSameThread");

syncTaskRequests_++;

executeSynchronouslyOnSameThread_CAN_DEADLOCK(
runtimeExecutor_,
[this, callback = std::move(callback)](jsi::Runtime& runtime) mutable {
SystraceSection s2(
"RuntimeScheduler::executeNowOnTheSameThread callback");

syncTaskRequests_--;

isSynchronous_ = true;

auto currentTime = now_();
auto priority = SchedulerPriority::ImmediatePriority;
auto expirationTime =
currentTime + timeoutForSchedulerPriority(priority);
auto task = std::make_shared<Task>(
priority, std::move(callback), expirationTime);

executeTask(runtime, task, currentTime);

isSynchronous_ = false;
});

bool shouldScheduleWorkLoop = false;

{
std::unique_lock lock(taskQueueMutex_);

if (!taskQueue_.empty() && !isWorkLoopScheduled_) {
isWorkLoopScheduled_ = true;
shouldScheduleWorkLoop = true;
}
}

if (shouldScheduleWorkLoop) {
scheduleWorkLoop();
}
}

// This will be replaced by microtasks
void RuntimeScheduler_Modern::callExpiredTasks(jsi::Runtime& runtime) {
SystraceSection s("RuntimeScheduler::callExpiredTasks");
startWorkLoop(runtime, true);
}

#pragma mark - Private

void RuntimeScheduler_Modern::scheduleTask(std::shared_ptr<Task> task) const {
bool shouldScheduleWorkLoop = false;

{
std::unique_lock lock(taskQueueMutex_);

if (taskQueue_.empty() && !isWorkLoopScheduled_) {
isWorkLoopScheduled_ = true;
shouldScheduleWorkLoop = true;
}

taskQueue_.push(task);
}

if (shouldScheduleWorkLoop) {
scheduleWorkLoop();
}
}

void RuntimeScheduler_Modern::scheduleWorkLoop() const {
runtimeExecutor_(
[this](jsi::Runtime& runtime) { startWorkLoop(runtime, false); });
}

void RuntimeScheduler_Modern::startWorkLoop(
jsi::Runtime& runtime,
bool onlyExpired) const {
SystraceSection s("RuntimeScheduler::startWorkLoop");

auto previousPriority = currentPriority_;

try {
while (syncTaskRequests_ == 0) {
auto currentTime = now_();
auto topPriorityTask = selectTask(currentTime, onlyExpired);

if (!topPriorityTask) {
// No pending work to do.
// Events will restart the loop when necessary.
break;
}

executeTask(runtime, topPriorityTask, currentTime);
}
} catch (jsi::JSError& error) {
handleFatalError(runtime, error);
}

currentPriority_ = previousPriority;
}

std::shared_ptr<Task> RuntimeScheduler_Modern::selectTask(
RuntimeSchedulerTimePoint currentTime,
bool onlyExpired) const {
std::unique_lock lock(taskQueueMutex_);

// It's safe to reset the flag here, as its access is also synchronized with
// the access to the task queue.
isWorkLoopScheduled_ = false;

// Skip executed tasks
while (!taskQueue_.empty() && !taskQueue_.top()->callback) {
taskQueue_.pop();
}

if (!taskQueue_.empty()) {
auto task = taskQueue_.top();
auto didUserCallbackTimeout = task->expirationTime <= currentTime;
if (!onlyExpired || didUserCallbackTimeout) {
return task;
}
}

return nullptr;
}

void RuntimeScheduler_Modern::executeTask(
jsi::Runtime& runtime,
const std::shared_ptr<Task>& task,
RuntimeSchedulerTimePoint currentTime) const {
auto didUserCallbackTimeout = task->expirationTime <= currentTime;

SystraceSection s(
"RuntimeScheduler::executeTask",
"priority",
serialize(task->priority),
"didUserCallbackTimeout",
didUserCallbackTimeout);

currentTask_ = task;
currentPriority_ = task->priority;

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
Loading

0 comments on commit f7b5a4d

Please sign in to comment.