Skip to content

Commit

Permalink
Implement atomic reporting of UI changes in Runtime Scheduler (facebo…
Browse files Browse the repository at this point in the history
…ok#41157)

Summary:


This is internal until we're ready to test this outside of Meta.
Changelog: [internal]

## Context

In the new architecture, every time we commit a new tree in React we send a transaction to the host platform to make all the necessary mutations in the underlying native views.

This can be bad for user experience, because the user might see quick changes to the UI in succession for related changes. It also breaks the semantics of things like layout effects and ref callbacks, which are core to the React programming model and should work across all platforms.

The main semantic that this behavior breaks in React Native is that layout effects are supposed to be blocking for paint. That means that any state updates (or UI mutations) done in layout effects should be applied to the UI atomically with the original changes that triggered them, so users see a single update where the final state is applied. This doesn't work in React Native as none of the commits coming from React are blocked waiting for effects, and instead they're all mounted/applied as they come.

This isn't only a problem for React, but also for future Web-like APIs that rely on microtasks. Those are also assumed to block paint in browsers, and we don't support that behavior either.

## Changes

Now that we're adding support for a well-defined event loop in React Native, we can add a new step to notify UI changes to the host platform in specific points in time, after macrotasks and microtasks are done (the "Update the rendering" step defined on the [Web specification](https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model)).

This implements that step in the new `RuntimeScheduler`. This works by batching all the notifications from the `UIManager` to `MountingCoordinator` and calling all those methods from `RuntimeScheduler` at the right time.

There will be cases where the notifications will be to mount the same tree multiple times, but the mounting coordinator already handles this correctly (would mount the last version of the tree for each surface ID the first time, and be a no-op the other times).

This change will reduce the amount of mount operations we do on the main thread, which means that we could potentially remove the push model from Android if performance is acceptable with this.

NOTE: This only works with the modern runtime scheduler, and only makes sense when used with microtasks enabled too and background executor disabled.

Reviewed By: javache

Differential Revision: D49536327
  • Loading branch information
rubennorte authored and facebook-github-bot committed Oct 24, 2023
1 parent 98e7ecd commit 2898b48
Show file tree
Hide file tree
Showing 8 changed files with 128 additions and 29 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,10 @@ void RuntimeScheduler::callExpiredTasks(jsi::Runtime& runtime) {
return runtimeSchedulerImpl_->callExpiredTasks(runtime);
}

void RuntimeScheduler::scheduleRenderingUpdate(
RuntimeSchedulerRenderingUpdate&& renderingUpdate) {
return runtimeSchedulerImpl_->scheduleRenderingUpdate(
std::move(renderingUpdate));
}

} // namespace facebook::react
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

namespace facebook::react {

using RuntimeSchedulerRenderingUpdate = std::function<void()>;

// This is a temporary abstract class for RuntimeScheduler forks to implement
// (and use them interchangeably).
class RuntimeSchedulerBase {
Expand All @@ -32,6 +34,8 @@ class RuntimeSchedulerBase {
virtual SchedulerPriority getCurrentPriorityLevel() const noexcept = 0;
virtual RuntimeSchedulerTimePoint now() const noexcept = 0;
virtual void callExpiredTasks(jsi::Runtime& runtime) = 0;
virtual void scheduleRenderingUpdate(
RuntimeSchedulerRenderingUpdate&& renderingUpdate) = 0;
};

// This is a proxy for RuntimeScheduler implementation, which will be selected
Expand Down Expand Up @@ -130,6 +134,9 @@ class RuntimeScheduler final : RuntimeSchedulerBase {
*/
void callExpiredTasks(jsi::Runtime& runtime) override;

void scheduleRenderingUpdate(
RuntimeSchedulerRenderingUpdate&& renderingUpdate) override;

private:
// Actual implementation, stored as a unique pointer to simplify memory
// management.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,15 @@ void RuntimeScheduler_Legacy::callExpiredTasks(jsi::Runtime& runtime) {
currentPriority_ = previousPriority;
}

void RuntimeScheduler_Legacy::scheduleRenderingUpdate(
RuntimeSchedulerRenderingUpdate&& renderingUpdate) {
SystraceSection s("RuntimeScheduler::scheduleRenderingUpdate");

if (renderingUpdate != nullptr) {
renderingUpdate();
}
}

#pragma mark - Private

void RuntimeScheduler_Legacy::scheduleWorkLoopIfNecessary() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ class RuntimeScheduler_Legacy final : public RuntimeSchedulerBase {
*/
void callExpiredTasks(jsi::Runtime& runtime) override;

void scheduleRenderingUpdate(
RuntimeSchedulerRenderingUpdate&& renderingUpdate) override;

private:
std::priority_queue<
std::shared_ptr<Task>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@
namespace facebook::react {

namespace {
// Looping on \c drainMicrotasks until it completes or hits the retries bound.
/**
* This is partially equivalent to the "Perform a microtask checkpoint" step in
* the Web event loop. See
* https://html.spec.whatwg.org/multipage/webappapis.html#perform-a-microtask-checkpoint.
*
* Iterates on \c drainMicrotasks until it completes or hits the retries bound.
*/
void executeMicrotasks(jsi::Runtime& runtime) {
SystraceSection s("RuntimeScheduler::executeMicrotasks");

Expand Down Expand Up @@ -175,6 +181,19 @@ void RuntimeScheduler_Modern::callExpiredTasks(jsi::Runtime& runtime) {
startWorkLoop(runtime, true);
}

void RuntimeScheduler_Modern::scheduleRenderingUpdate(
RuntimeSchedulerRenderingUpdate&& renderingUpdate) {
SystraceSection s("RuntimeScheduler::scheduleRenderingUpdate");

if (CoreFeatures::blockPaintForUseLayoutEffect) {
pendingRenderingUpdates_.push(renderingUpdate);
} else {
if (renderingUpdate != nullptr) {
renderingUpdate();
}
}
}

#pragma mark - Private

void RuntimeScheduler_Modern::scheduleTask(std::shared_ptr<Task> task) {
Expand Down Expand Up @@ -278,11 +297,31 @@ void RuntimeScheduler_Modern::executeTask(
executeMacrotask(runtime, task, didUserCallbackTimeout);

if (CoreFeatures::enableMicrotasks) {
// "Perform a microtask checkpoint" step.
executeMicrotasks(runtime);
}

// TODO report long tasks
// TODO update rendering
if (CoreFeatures::blockPaintForUseLayoutEffect) {
// "Update the rendering" step.
updateRendering();
}
}

/**
* This is partially equivalent to the "Update the rendering" step in the Web
* event loop. See
* https://html.spec.whatwg.org/multipage/webappapis.html#update-the-rendering.
*/
void RuntimeScheduler_Modern::updateRendering() {
SystraceSection s("RuntimeScheduler::updateRendering");

while (!pendingRenderingUpdates_.empty()) {
auto& pendingRenderingUpdate = pendingRenderingUpdates_.front();
if (pendingRenderingUpdate != nullptr) {
pendingRenderingUpdate();
}
pendingRenderingUpdates_.pop();
}
}

void RuntimeScheduler_Modern::executeMacrotask(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,15 @@ class RuntimeScheduler_Modern final : public RuntimeSchedulerBase {
*/
void callExpiredTasks(jsi::Runtime& runtime) override;

/**
* Schedules a function that notifies or applies UI changes in the host
* platform, to be executed during the "Update the rendering" step of the
* event loop. If the step is not enabled, the function is executed
* immediately.
*/
void scheduleRenderingUpdate(
RuntimeSchedulerRenderingUpdate&& renderingUpdate) override;

private:
std::atomic<uint_fast8_t> syncTaskRequests_{0};

Expand Down Expand Up @@ -167,6 +176,8 @@ class RuntimeScheduler_Modern final : public RuntimeSchedulerBase {
std::shared_ptr<Task> task,
bool didUserCallbackTimeout) const;

void updateRendering();

/*
* Returns a time point representing the current point in time. May be called
* from multiple threads.
Expand All @@ -178,6 +189,8 @@ class RuntimeScheduler_Modern final : public RuntimeSchedulerBase {
* scheduled.
*/
bool isWorkLoopScheduled_{false};

std::queue<RuntimeSchedulerRenderingUpdate> pendingRenderingUpdates_;
};

} // namespace facebook::react
Original file line number Diff line number Diff line change
Expand Up @@ -124,17 +124,41 @@ TEST_P(RuntimeSchedulerTest, scheduleSingleTask) {
EXPECT_EQ(stubQueue_->size(), 0);
}

TEST_P(RuntimeSchedulerTest, scheduleSingleTaskWithMicrotasks) {
TEST_P(RuntimeSchedulerTest, scheduleNonBatchedRenderingUpdate) {
CoreFeatures::blockPaintForUseLayoutEffect = false;

bool didRunRenderingUpdate = false;

runtimeScheduler_->scheduleRenderingUpdate(
[&]() { didRunRenderingUpdate = true; });

EXPECT_TRUE(didRunRenderingUpdate);
}

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

bool didRunTask = false;
bool didRunMicrotask = false;
CoreFeatures::blockPaintForUseLayoutEffect = true;

uint nextOperationPosition = 1;

uint taskPosition = 0;
uint microtaskPosition = 0;
uint updateRenderingPosition = 0;

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

runtimeScheduler_->scheduleRenderingUpdate([&]() {
updateRenderingPosition = nextOperationPosition;
nextOperationPosition++;
});

auto microtaskCallback = jsi::Function::createFromHostFunction(
*runtime_,
Expand All @@ -144,7 +168,8 @@ TEST_P(RuntimeSchedulerTest, scheduleSingleTaskWithMicrotasks) {
const jsi::Value& /*unused*/,
const jsi::Value* arguments,
size_t /*unused*/) -> jsi::Value {
didRunMicrotask = true;
microtaskPosition = nextOperationPosition;
nextOperationPosition++;
return jsi::Value::undefined();
});

Expand All @@ -162,13 +187,16 @@ TEST_P(RuntimeSchedulerTest, scheduleSingleTaskWithMicrotasks) {
runtimeScheduler_->scheduleTask(
SchedulerPriority::NormalPriority, std::move(callback));

EXPECT_FALSE(didRunTask);
EXPECT_EQ(taskPosition, 0);
EXPECT_EQ(microtaskPosition, 0);
EXPECT_EQ(updateRenderingPosition, 0);
EXPECT_EQ(stubQueue_->size(), 1);

stubQueue_->tick();

EXPECT_TRUE(didRunTask);
EXPECT_TRUE(didRunMicrotask);
EXPECT_EQ(taskPosition, 1);
EXPECT_EQ(microtaskPosition, 2);
EXPECT_EQ(updateRenderingPosition, 3);
EXPECT_EQ(stubQueue_->size(), 0);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,24 +294,18 @@ void Scheduler::uiManagerDidFinishTransaction(
SystraceSection s("Scheduler::uiManagerDidFinishTransaction");

if (delegate_ != nullptr) {
if (CoreFeatures::blockPaintForUseLayoutEffect) {
auto weakRuntimeScheduler =
contextContainer_->find<std::weak_ptr<RuntimeScheduler>>(
"RuntimeScheduler");
auto runtimeScheduler = weakRuntimeScheduler.has_value()
? weakRuntimeScheduler.value().lock()
: nullptr;
if (runtimeScheduler && !mountSynchronously) {
runtimeScheduler->scheduleTask(
SchedulerPriority::UserBlockingPriority,
[delegate = delegate_,
mountingCoordinator =
std::move(mountingCoordinator)](jsi::Runtime&) {
delegate->schedulerDidFinishTransaction(mountingCoordinator);
});
} else {
delegate_->schedulerDidFinishTransaction(mountingCoordinator);
}
auto weakRuntimeScheduler =
contextContainer_->find<std::weak_ptr<RuntimeScheduler>>(
"RuntimeScheduler");
auto runtimeScheduler = weakRuntimeScheduler.has_value()
? weakRuntimeScheduler.value().lock()
: nullptr;
if (runtimeScheduler && !mountSynchronously) {
runtimeScheduler->scheduleRenderingUpdate(
[delegate = delegate_,
mountingCoordinator = std::move(mountingCoordinator)]() {
delegate->schedulerDidFinishTransaction(mountingCoordinator);
});
} else {
delegate_->schedulerDidFinishTransaction(mountingCoordinator);
}
Expand Down

0 comments on commit 2898b48

Please sign in to comment.