Improve spec-compliance of bridgeless timer implementation (#44380)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/44380

* setInterval's second argument is optional, and defaults to 0
* setTimeout is spec'ed to return a positive integer.

There's also no need to use HostObjects here to represent the timer index, it just hurts performance and makes this code more complex for no clear reason.

Changelog: [General][Fixed] New architecture timer methods now return integers instead of an opaque object.

Reviewed By: RSNara

Differential Revision: D56863422

fbshipit-source-id: fd3e75303662d865083d01e2bfe8633bac151a0e
This commit is contained in:
Pieter De Baets
2024-05-03 08:04:05 -07:00
committed by Facebook GitHub Bot
parent b163ed8655
commit a16f7dc547
3 changed files with 99 additions and 143 deletions
@@ -21,125 +21,134 @@ void TimerManager::setRuntimeExecutor(
runtimeExecutor_ = runtimeExecutor;
}
std::shared_ptr<TimerHandle> TimerManager::createReactNativeMicrotask(
TimerHandle TimerManager::createReactNativeMicrotask(
jsi::Function&& callback,
std::vector<jsi::Value>&& args) {
auto sharedCallback = std::make_shared<TimerCallback>(
std::move(callback), std::move(args), /* repeat */ false);
// Get the id for the callback.
uint32_t timerID = timerIndex_++;
timers_[timerID] = std::move(sharedCallback);
TimerHandle timerID = timerIndex_++;
timers_.emplace(
std::piecewise_construct,
std::forward_as_tuple(timerID),
std::forward_as_tuple(
std::move(callback), std::move(args), /* repeat */ false));
reactNativeMicrotasksQueue_.push_back(timerID);
return std::make_shared<TimerHandle>(timerID);
return timerID;
}
void TimerManager::callReactNativeMicrotasks(jsi::Runtime& runtime) {
std::vector<uint32_t> reactNativeMicrotasksQueue;
std::vector<TimerHandle> reactNativeMicrotasksQueue;
while (!reactNativeMicrotasksQueue_.empty()) {
reactNativeMicrotasksQueue.clear();
reactNativeMicrotasksQueue.swap(reactNativeMicrotasksQueue_);
for (auto reactNativeMicrotaskID : reactNativeMicrotasksQueue) {
// ReactNativeMicrotasks can clear other scheduled reactNativeMicrotasks.
if (timers_.count(reactNativeMicrotaskID) > 0) {
timers_[reactNativeMicrotaskID]->invoke(runtime);
timers_.erase(reactNativeMicrotaskID);
auto it = timers_.find(reactNativeMicrotaskID);
if (it != timers_.end()) {
it->second.invoke(runtime);
timers_.erase(it);
}
}
}
}
std::shared_ptr<TimerHandle> TimerManager::createTimer(
TimerHandle TimerManager::createTimer(
jsi::Function&& callback,
std::vector<jsi::Value>&& args,
double delay) {
auto sharedCallback = std::make_shared<TimerCallback>(
std::move(callback), std::move(args), false);
// Get the id for the callback.
uint32_t timerID = timerIndex_++;
timers_[timerID] = std::move(sharedCallback);
TimerHandle timerID = timerIndex_++;
timers_.emplace(
std::piecewise_construct,
std::forward_as_tuple(timerID),
std::forward_as_tuple(
std::move(callback), std::move(args), /* repeat */ false));
platformTimerRegistry_->createTimer(timerID, delay);
return std::make_shared<TimerHandle>(timerID);
return timerID;
}
std::shared_ptr<TimerHandle> TimerManager::createRecurringTimer(
TimerHandle TimerManager::createRecurringTimer(
jsi::Function&& callback,
std::vector<jsi::Value>&& args,
double delay) {
auto sharedCallback = std::make_shared<TimerCallback>(
std::move(callback), std::move(args), true);
// Get the id for the callback.
uint32_t timerID = timerIndex_++;
timers_[timerID] = std::move(sharedCallback);
TimerHandle timerID = timerIndex_++;
timers_.emplace(
std::piecewise_construct,
std::forward_as_tuple(timerID),
std::forward_as_tuple(
std::move(callback), std::move(args), /* repeat */ true));
platformTimerRegistry_->createRecurringTimer(timerID, delay);
return std::make_shared<TimerHandle>(timerID);
return timerID;
}
void TimerManager::deleteReactNativeMicrotask(
jsi::Runtime& runtime,
std::shared_ptr<TimerHandle> timerHandle) {
if (timerHandle == nullptr) {
TimerHandle timerHandle) {
if (timerHandle < 0) {
throw jsi::JSError(
runtime, "clearReactNativeMicrotask was called with an invalid handle");
}
for (auto it = reactNativeMicrotasksQueue_.begin();
it != reactNativeMicrotasksQueue_.end();
it++) {
if ((*it) == timerHandle->index()) {
reactNativeMicrotasksQueue_.erase(it);
break;
}
auto it = std::find(
reactNativeMicrotasksQueue_.begin(),
reactNativeMicrotasksQueue_.end(),
timerHandle);
if (it != reactNativeMicrotasksQueue_.end()) {
reactNativeMicrotasksQueue_.erase(it);
}
if (timers_.find(timerHandle->index()) != timers_.end()) {
timers_.erase(timerHandle->index());
if (timers_.find(timerHandle) != timers_.end()) {
timers_.erase(timerHandle);
}
}
void TimerManager::deleteTimer(
jsi::Runtime& runtime,
std::shared_ptr<TimerHandle> timerHandle) {
if (timerHandle == nullptr) {
void TimerManager::deleteTimer(jsi::Runtime& runtime, TimerHandle timerHandle) {
if (timerHandle < 0) {
throw jsi::JSError(runtime, "clearTimeout called with an invalid handle");
}
platformTimerRegistry_->deleteTimer(timerHandle->index());
if (timers_.find(timerHandle->index()) != timers_.end()) {
timers_.erase(timerHandle->index());
platformTimerRegistry_->deleteTimer(timerHandle);
if (timers_.find(timerHandle) != timers_.end()) {
timers_.erase(timerHandle);
}
}
void TimerManager::deleteRecurringTimer(
jsi::Runtime& runtime,
std::shared_ptr<TimerHandle> timerHandle) {
if (timerHandle == nullptr) {
TimerHandle timerHandle) {
if (timerHandle < 0) {
throw jsi::JSError(runtime, "clearInterval called with an invalid handle");
}
platformTimerRegistry_->deleteTimer(timerHandle->index());
if (timers_.find(timerHandle->index()) != timers_.end()) {
timers_.erase(timerHandle->index());
platformTimerRegistry_->deleteTimer(timerHandle);
auto it = timers_.find(timerHandle);
if (it != timers_.end()) {
timers_.erase(it);
}
}
void TimerManager::callTimer(uint32_t timerID) {
runtimeExecutor_([this, timerID](jsi::Runtime& runtime) {
void TimerManager::callTimer(TimerHandle timerHandle) {
runtimeExecutor_([this, timerHandle](jsi::Runtime& runtime) {
SystraceSection s("TimerManager::callTimer");
if (timers_.count(timerID) > 0) {
timers_[timerID]->invoke(runtime);
// Invoking a timer has the potential to delete it. Double check the timer
// still exists before accessing it again.
if (timers_.count(timerID) > 0 && !timers_[timerID]->repeat) {
timers_.erase(timerID);
auto it = timers_.find(timerHandle);
if (it != timers_.end()) {
bool repeats = it->second.repeat;
it->second.invoke(runtime);
if (!repeats) {
// Invoking a timer has the potential to delete it. Double check the
// timer still exists before accessing it again.
it = timers_.find(timerHandle);
if (it != timers_.end()) {
timers_.erase(it);
}
}
}
});
@@ -180,9 +189,8 @@ void TimerManager::attachGlobals(jsi::Runtime& runtime) {
moreArgs.emplace_back(rt, args[extraArgNum]);
}
auto handle = createReactNativeMicrotask(
return createReactNativeMicrotask(
std::move(callback), std::move(moreArgs));
return jsi::Object::createFromHostObject(rt, handle);
}));
runtime.global().setProperty(
@@ -197,13 +205,10 @@ void TimerManager::attachGlobals(jsi::Runtime& runtime) {
const jsi::Value& thisVal,
const jsi::Value* args,
size_t count) {
if (count == 0 || !args[0].isObject() ||
!args[0].asObject(rt).isHostObject<TimerHandle>(rt)) {
return jsi::Value::undefined();
if (count > 0 && args[0].isNumber()) {
auto handle = (TimerHandle)args[0].asNumber();
deleteReactNativeMicrotask(rt, handle);
}
std::shared_ptr<TimerHandle> handle =
args[0].asObject(rt).asHostObject<TimerHandle>(rt);
deleteReactNativeMicrotask(rt, handle);
return jsi::Value::undefined();
}));
@@ -245,9 +250,7 @@ void TimerManager::attachGlobals(jsi::Runtime& runtime) {
moreArgs.emplace_back(rt, args[extraArgNum]);
}
auto handle =
createTimer(std::move(callback), std::move(moreArgs), delay);
return jsi::Object::createFromHostObject(rt, handle);
return createTimer(std::move(callback), std::move(moreArgs), delay);
}));
runtime.global().setProperty(
@@ -262,13 +265,10 @@ void TimerManager::attachGlobals(jsi::Runtime& runtime) {
const jsi::Value& thisVal,
const jsi::Value* args,
size_t count) {
if (count == 0 || !args[0].isObject() ||
!args[0].asObject(rt).isHostObject<TimerHandle>(rt)) {
return jsi::Value::undefined();
if (count > 0 && args[0].isNumber()) {
auto handle = (TimerHandle)args[0].asNumber();
deleteTimer(rt, handle);
}
std::shared_ptr<TimerHandle> host =
args[0].asObject(rt).asHostObject<TimerHandle>(rt);
deleteTimer(rt, host);
return jsi::Value::undefined();
}));
@@ -284,10 +284,10 @@ void TimerManager::attachGlobals(jsi::Runtime& runtime) {
const jsi::Value& thisVal,
const jsi::Value* args,
size_t count) {
if (count < 2) {
if (count == 0) {
throw jsi::JSError(
rt,
"setInterval must be called with at least two arguments (the function to call and the delay).");
"setInterval must be called with at least one argument (the function to call).");
}
if (!args[0].isObject() || !args[0].asObject(rt).isFunction(rt)) {
@@ -295,12 +295,8 @@ void TimerManager::attachGlobals(jsi::Runtime& runtime) {
rt, "The first argument to setInterval must be a function.");
}
auto callback = args[0].getObject(rt).getFunction(rt);
if (!args[1].isNumber()) {
throw jsi::JSError(
rt, "The second argument to setInterval must be a number.");
}
auto delay = args[1].getNumber();
auto delay =
count > 1 && args[1].isNumber() ? args[1].getNumber() : 0;
// Package up the remaining argument values into one place.
std::vector<jsi::Value> moreArgs;
@@ -308,9 +304,8 @@ void TimerManager::attachGlobals(jsi::Runtime& runtime) {
moreArgs.emplace_back(rt, args[extraArgNum]);
}
auto handle = createRecurringTimer(
return createRecurringTimer(
std::move(callback), std::move(moreArgs), delay);
return jsi::Object::createFromHostObject(rt, handle);
}));
runtime.global().setProperty(
@@ -325,13 +320,10 @@ void TimerManager::attachGlobals(jsi::Runtime& runtime) {
const jsi::Value& thisVal,
const jsi::Value* args,
size_t count) {
if (count == 0 || !args[0].isObject() ||
!args[0].asObject(rt).isHostObject<TimerHandle>(rt)) {
return jsi::Value::undefined();
if (count > 0 && args[0].isNumber()) {
auto handle = (TimerHandle)args[0].asNumber();
deleteRecurringTimer(rt, handle);
}
std::shared_ptr<TimerHandle> host =
args[0].asObject(rt).asHostObject<TimerHandle>(rt);
deleteRecurringTimer(rt, host);
return jsi::Value::undefined();
}));
@@ -359,12 +351,11 @@ void TimerManager::attachGlobals(jsi::Runtime& runtime) {
"The first argument to requestAnimationFrame must be a function.");
}
using CallbackContainer = std::tuple<jsi::Function>;
auto callback = jsi::Function::createFromHostFunction(
rt,
jsi::PropNameID::forAscii(rt, "RN$rafFn"),
0,
[callbackContainer = std::make_shared<CallbackContainer>(
[callbackContainer = std::make_shared<jsi::Function>(
args[0].getObject(rt).getFunction(rt))](
jsi::Runtime& rt,
const jsi::Value& thisVal,
@@ -374,19 +365,16 @@ void TimerManager::attachGlobals(jsi::Runtime& runtime) {
rt.global().getPropertyAsObject(rt, "performance");
auto nowFn = performance.getPropertyAsFunction(rt, "now");
auto now = nowFn.callWithThis(rt, performance, {});
return std::get<0>(*callbackContainer)
.call(rt, {std::move(now)});
return callbackContainer->call(rt, {std::move(now)});
});
// The current implementation of requestAnimationFrame is the same
// as setTimeout(0). This isn't exactly how requestAnimationFrame
// is supposed to work on web, and may change in the future.
auto handle = createTimer(
return createTimer(
std::move(callback),
std::vector<jsi::Value>(),
/* delay */ 0);
return jsi::Object::createFromHostObject(rt, handle);
}));
runtime.global().setProperty(
@@ -401,13 +389,10 @@ void TimerManager::attachGlobals(jsi::Runtime& runtime) {
const jsi::Value& thisVal,
const jsi::Value* args,
size_t count) {
if (count == 0 || !args[0].isObject() ||
!args[0].asObject(rt).isHostObject<TimerHandle>(rt)) {
return jsi::Value::undefined();
if (count > 0 && args[0].isNumber()) {
auto handle = (TimerHandle)args[0].asNumber();
deleteTimer(rt, handle);
}
std::shared_ptr<TimerHandle> host =
args[0].asObject(rt).asHostObject<TimerHandle>(rt);
deleteTimer(rt, host);
return jsi::Value::undefined();
}));
}
@@ -16,31 +16,12 @@
namespace facebook::react {
/*
* A HostObject subclass representing the result of a setTimeout call.
* Can be used as an argument to clearTimeout.
*/
class TimerHandle : public jsi::HostObject {
public:
explicit TimerHandle(uint32_t index) : index_(index) {}
uint32_t index() const {
return index_;
}
~TimerHandle() override = default;
private:
// Index in the timeouts_ map of the owning SetTimeoutQueue.
uint32_t index_;
};
using TimerHandle = int;
/*
* Wraps a jsi::Function to make it copyable so we can pass it into a lambda.
*/
struct TimerCallback {
TimerCallback(TimerCallback&&) = default;
TimerCallback(
jsi::Function callback,
std::vector<jsi::Value> args,
@@ -67,49 +48,45 @@ class TimerManager {
void callReactNativeMicrotasks(jsi::Runtime& runtime);
void callTimer(uint32_t);
void callTimer(TimerHandle handle);
void attachGlobals(jsi::Runtime& runtime);
private:
std::shared_ptr<TimerHandle> createReactNativeMicrotask(
TimerHandle createReactNativeMicrotask(
jsi::Function&& callback,
std::vector<jsi::Value>&& args);
void deleteReactNativeMicrotask(
jsi::Runtime& runtime,
std::shared_ptr<TimerHandle> handle);
void deleteReactNativeMicrotask(jsi::Runtime& runtime, TimerHandle handle);
std::shared_ptr<TimerHandle> createTimer(
TimerHandle createTimer(
jsi::Function&& callback,
std::vector<jsi::Value>&& args,
double delay);
void deleteTimer(jsi::Runtime& runtime, std::shared_ptr<TimerHandle> handle);
void deleteTimer(jsi::Runtime& runtime, TimerHandle handle);
std::shared_ptr<TimerHandle> createRecurringTimer(
TimerHandle createRecurringTimer(
jsi::Function&& callback,
std::vector<jsi::Value>&& args,
double delay);
void deleteRecurringTimer(
jsi::Runtime& runtime,
std::shared_ptr<TimerHandle> handle);
void deleteRecurringTimer(jsi::Runtime& runtime, TimerHandle handle);
RuntimeExecutor runtimeExecutor_;
std::unique_ptr<PlatformTimerRegistry> platformTimerRegistry_;
// A map (id => callback func) of the currently active JS timers
std::unordered_map<uint32_t, std::shared_ptr<TimerCallback>> timers_;
std::unordered_map<TimerHandle, TimerCallback> timers_;
// Each timeout that is registered on this queue gets a sequential id. This
// is the global count from which those are assigned.
uint32_t timerIndex_{0};
TimerHandle timerIndex_{0};
// The React Native microtask queue is used to back public APIs including
// `queueMicrotask`, `clearImmediate`, and `setImmediate` (which is used by
// the Promise polyfill) when the JSVM microtask mechanism is not used.
std::vector<uint32_t> reactNativeMicrotasksQueue_;
std::vector<TimerHandle> reactNativeMicrotasksQueue_;
};
} // namespace facebook::react
@@ -537,16 +537,10 @@ TEST_F(ReactInstanceTest, testSetIntervalWithInvalidArgs) {
EXPECT_EQ(
getErrorMessage("setInterval();"),
"setInterval must be called with at least two arguments (the function to call and the delay).");
EXPECT_EQ(
getErrorMessage("setInterval(() => {});"),
"setInterval must be called with at least two arguments (the function to call and the delay).");
"setInterval must be called with at least one argument (the function to call).");
EXPECT_EQ(
getErrorMessage("setInterval('invalid', 100);"),
"The first argument to setInterval must be a function.");
EXPECT_EQ(
getErrorMessage("setInterval(() => {}, 'invalid');"),
"The second argument to setInterval must be a number.");
}
TEST_F(ReactInstanceTest, testClearInterval) {