mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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:
committed by
Facebook GitHub Bot
parent
b163ed8655
commit
a16f7dc547
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user