473 lines
15 KiB
C++
473 lines
15 KiB
C++
|
/*
|
||
|
* 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 "TimerManager.h"
|
||
|
|
||
|
#include <cxxreact/SystraceSection.h>
|
||
|
#include <react/featureflags/ReactNativeFeatureFlags.h>
|
||
|
|
||
|
#include <cmath>
|
||
|
#include <utility>
|
||
|
|
||
|
namespace facebook::react {
|
||
|
|
||
|
namespace {
|
||
|
|
||
|
double coerceNumberTimeout(jsi::Runtime& rt, const jsi::Value& timeout) {
|
||
|
double delay = 0.0;
|
||
|
|
||
|
// fast-path
|
||
|
if (timeout.isNumber()) {
|
||
|
delay = timeout.getNumber();
|
||
|
} else {
|
||
|
// perform number coercion for timeout to be web spec compliant
|
||
|
auto numberCtor = rt.global().getPropertyAsObject(rt, "Number");
|
||
|
auto delayNumberObject =
|
||
|
numberCtor.getFunction(rt).callAsConstructor(rt, timeout).getObject(rt);
|
||
|
auto delayNumericValue =
|
||
|
delayNumberObject.getPropertyAsFunction(rt, "valueOf")
|
||
|
.callWithThis(rt, delayNumberObject);
|
||
|
delay = delayNumericValue.isNumber() ? delayNumericValue.getNumber() : 0;
|
||
|
}
|
||
|
|
||
|
return std::isnan(delay) ? 0.0 : std::max(0.0, delay);
|
||
|
}
|
||
|
|
||
|
inline const char* getTimerSourceName(TimerSource source) {
|
||
|
switch (source) {
|
||
|
case TimerSource::Unknown:
|
||
|
return "unknown";
|
||
|
case TimerSource::SetTimeout:
|
||
|
return "setTimeout";
|
||
|
case TimerSource::SetInterval:
|
||
|
return "setInterval";
|
||
|
case TimerSource::RequestAnimationFrame:
|
||
|
return "requestAnimationFrame";
|
||
|
}
|
||
|
return "unknown";
|
||
|
}
|
||
|
|
||
|
} // namespace
|
||
|
|
||
|
TimerManager::TimerManager(
|
||
|
std::unique_ptr<PlatformTimerRegistry> platformTimerRegistry) noexcept
|
||
|
: platformTimerRegistry_(std::move(platformTimerRegistry)) {}
|
||
|
|
||
|
void TimerManager::setRuntimeExecutor(
|
||
|
RuntimeExecutor runtimeExecutor) noexcept {
|
||
|
runtimeExecutor_ = runtimeExecutor;
|
||
|
}
|
||
|
|
||
|
TimerHandle TimerManager::createReactNativeMicrotask(
|
||
|
jsi::Function&& callback,
|
||
|
std::vector<jsi::Value>&& args) {
|
||
|
// Get the id for the callback.
|
||
|
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 timerID;
|
||
|
}
|
||
|
|
||
|
void TimerManager::callReactNativeMicrotasks(jsi::Runtime& runtime) {
|
||
|
std::vector<TimerHandle> reactNativeMicrotasksQueue;
|
||
|
while (!reactNativeMicrotasksQueue_.empty()) {
|
||
|
reactNativeMicrotasksQueue.clear();
|
||
|
reactNativeMicrotasksQueue.swap(reactNativeMicrotasksQueue_);
|
||
|
|
||
|
for (auto reactNativeMicrotaskID : reactNativeMicrotasksQueue) {
|
||
|
// ReactNativeMicrotasks can clear other scheduled reactNativeMicrotasks.
|
||
|
auto it = timers_.find(reactNativeMicrotaskID);
|
||
|
if (it != timers_.end()) {
|
||
|
it->second.invoke(runtime);
|
||
|
|
||
|
// Invoking a timer has the potential to delete it. Do not re-use the
|
||
|
// existing iterator to erase it from the map.
|
||
|
timers_.erase(reactNativeMicrotaskID);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
TimerHandle TimerManager::createTimer(
|
||
|
jsi::Function&& callback,
|
||
|
std::vector<jsi::Value>&& args,
|
||
|
double delay,
|
||
|
TimerSource source) {
|
||
|
// Get the id for the callback.
|
||
|
TimerHandle timerID = timerIndex_++;
|
||
|
|
||
|
SystraceSection s(
|
||
|
"TimerManager::createTimer",
|
||
|
"id",
|
||
|
timerID,
|
||
|
"type",
|
||
|
getTimerSourceName(source),
|
||
|
"delay",
|
||
|
delay);
|
||
|
|
||
|
timers_.emplace(
|
||
|
std::piecewise_construct,
|
||
|
std::forward_as_tuple(timerID),
|
||
|
std::forward_as_tuple(
|
||
|
std::move(callback),
|
||
|
std::move(args),
|
||
|
/* repeat */ false,
|
||
|
source));
|
||
|
|
||
|
platformTimerRegistry_->createTimer(timerID, delay);
|
||
|
|
||
|
return timerID;
|
||
|
}
|
||
|
|
||
|
TimerHandle TimerManager::createRecurringTimer(
|
||
|
jsi::Function&& callback,
|
||
|
std::vector<jsi::Value>&& args,
|
||
|
double delay,
|
||
|
TimerSource source) {
|
||
|
// Get the id for the callback.
|
||
|
TimerHandle timerID = timerIndex_++;
|
||
|
|
||
|
SystraceSection s(
|
||
|
"TimerManager::createRecurringTimer",
|
||
|
"id",
|
||
|
timerID,
|
||
|
"type",
|
||
|
getTimerSourceName(source),
|
||
|
"delay",
|
||
|
delay);
|
||
|
|
||
|
timers_.emplace(
|
||
|
std::piecewise_construct,
|
||
|
std::forward_as_tuple(timerID),
|
||
|
std::forward_as_tuple(
|
||
|
std::move(callback), std::move(args), /* repeat */ true, source));
|
||
|
|
||
|
platformTimerRegistry_->createRecurringTimer(timerID, delay);
|
||
|
|
||
|
return timerID;
|
||
|
}
|
||
|
|
||
|
void TimerManager::deleteReactNativeMicrotask(
|
||
|
jsi::Runtime& runtime,
|
||
|
TimerHandle timerHandle) {
|
||
|
if (timerHandle < 0) {
|
||
|
throw jsi::JSError(
|
||
|
runtime, "clearReactNativeMicrotask was called with an invalid handle");
|
||
|
}
|
||
|
|
||
|
auto it = std::find(
|
||
|
reactNativeMicrotasksQueue_.begin(),
|
||
|
reactNativeMicrotasksQueue_.end(),
|
||
|
timerHandle);
|
||
|
if (it != reactNativeMicrotasksQueue_.end()) {
|
||
|
reactNativeMicrotasksQueue_.erase(it);
|
||
|
timers_.erase(timerHandle);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void TimerManager::deleteTimer(jsi::Runtime& runtime, TimerHandle timerHandle) {
|
||
|
if (timerHandle < 0) {
|
||
|
throw jsi::JSError(runtime, "clearTimeout called with an invalid handle");
|
||
|
}
|
||
|
|
||
|
platformTimerRegistry_->deleteTimer(timerHandle);
|
||
|
timers_.erase(timerHandle);
|
||
|
}
|
||
|
|
||
|
void TimerManager::deleteRecurringTimer(
|
||
|
jsi::Runtime& runtime,
|
||
|
TimerHandle timerHandle) {
|
||
|
if (timerHandle < 0) {
|
||
|
throw jsi::JSError(runtime, "clearInterval called with an invalid handle");
|
||
|
}
|
||
|
|
||
|
platformTimerRegistry_->deleteTimer(timerHandle);
|
||
|
timers_.erase(timerHandle);
|
||
|
}
|
||
|
|
||
|
void TimerManager::callTimer(TimerHandle timerHandle) {
|
||
|
runtimeExecutor_([this, timerHandle](jsi::Runtime& runtime) {
|
||
|
auto it = timers_.find(timerHandle);
|
||
|
if (it != timers_.end()) {
|
||
|
auto& timerCallback = it->second;
|
||
|
bool repeats = timerCallback.repeat;
|
||
|
|
||
|
{
|
||
|
SystraceSection s(
|
||
|
"TimerManager::callTimer",
|
||
|
"id",
|
||
|
timerHandle,
|
||
|
"type",
|
||
|
getTimerSourceName(timerCallback.source));
|
||
|
timerCallback.invoke(runtime);
|
||
|
}
|
||
|
|
||
|
if (!repeats) {
|
||
|
// Invoking a timer has the potential to delete it. Do not re-use the
|
||
|
// existing iterator to erase it from the map.
|
||
|
timers_.erase(timerHandle);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
void TimerManager::attachGlobals(jsi::Runtime& runtime) {
|
||
|
// Install host functions for timers.
|
||
|
// TODO (T45786383): Add missing timer functions from JSTimers
|
||
|
|
||
|
// Ensure that we don't define `setImmediate` and `clearImmediate` if
|
||
|
// microtasks are enabled (as we polyfill them using `queueMicrotask` then).
|
||
|
if (ReactNativeFeatureFlags::disableEventLoopOnBridgeless()) {
|
||
|
runtime.global().setProperty(
|
||
|
runtime,
|
||
|
"setImmediate",
|
||
|
jsi::Function::createFromHostFunction(
|
||
|
runtime,
|
||
|
jsi::PropNameID::forAscii(runtime, "setImmediate"),
|
||
|
2, // Function, ...args
|
||
|
[this](
|
||
|
jsi::Runtime& rt,
|
||
|
const jsi::Value& thisVal,
|
||
|
const jsi::Value* args,
|
||
|
size_t count) {
|
||
|
if (count == 0) {
|
||
|
throw jsi::JSError(
|
||
|
rt,
|
||
|
"setImmediate must be called with at least one argument (a function to call)");
|
||
|
}
|
||
|
|
||
|
if (!args[0].isObject() || !args[0].asObject(rt).isFunction(rt)) {
|
||
|
// Do not throw any error to match web spec
|
||
|
return timerIndex_++;
|
||
|
}
|
||
|
auto callback = args[0].getObject(rt).getFunction(rt);
|
||
|
|
||
|
// Package up the remaining argument values into one place.
|
||
|
std::vector<jsi::Value> moreArgs;
|
||
|
for (size_t extraArgNum = 1; extraArgNum < count; extraArgNum++) {
|
||
|
moreArgs.emplace_back(rt, args[extraArgNum]);
|
||
|
}
|
||
|
|
||
|
return createReactNativeMicrotask(
|
||
|
std::move(callback), std::move(moreArgs));
|
||
|
}));
|
||
|
|
||
|
runtime.global().setProperty(
|
||
|
runtime,
|
||
|
"clearImmediate",
|
||
|
jsi::Function::createFromHostFunction(
|
||
|
runtime,
|
||
|
jsi::PropNameID::forAscii(runtime, "clearImmediate"),
|
||
|
1, // handle
|
||
|
[this](
|
||
|
jsi::Runtime& rt,
|
||
|
const jsi::Value& thisVal,
|
||
|
const jsi::Value* args,
|
||
|
size_t count) {
|
||
|
if (count > 0 && args[0].isNumber()) {
|
||
|
auto handle = (TimerHandle)args[0].asNumber();
|
||
|
deleteReactNativeMicrotask(rt, handle);
|
||
|
}
|
||
|
return jsi::Value::undefined();
|
||
|
}));
|
||
|
}
|
||
|
|
||
|
runtime.global().setProperty(
|
||
|
runtime,
|
||
|
"setTimeout",
|
||
|
jsi::Function::createFromHostFunction(
|
||
|
runtime,
|
||
|
jsi::PropNameID::forAscii(runtime, "setTimeout"),
|
||
|
3, // Function, delay, ...args
|
||
|
[this](
|
||
|
jsi::Runtime& rt,
|
||
|
const jsi::Value& thisVal,
|
||
|
const jsi::Value* args,
|
||
|
size_t count) {
|
||
|
if (count == 0) {
|
||
|
throw jsi::JSError(
|
||
|
rt,
|
||
|
"setTimeout must be called with at least one argument (the function to call).");
|
||
|
}
|
||
|
|
||
|
if (!args[0].isObject() || !args[0].asObject(rt).isFunction(rt)) {
|
||
|
// Do not throw any error to match web spec
|
||
|
return timerIndex_++;
|
||
|
}
|
||
|
|
||
|
auto callback = args[0].getObject(rt).getFunction(rt);
|
||
|
auto delay = count > 1 ? coerceNumberTimeout(rt, args[1]) : 0.0;
|
||
|
|
||
|
// Package up the remaining argument values into one place.
|
||
|
std::vector<jsi::Value> moreArgs;
|
||
|
for (size_t extraArgNum = 2; extraArgNum < count; extraArgNum++) {
|
||
|
moreArgs.emplace_back(rt, args[extraArgNum]);
|
||
|
}
|
||
|
|
||
|
return createTimer(
|
||
|
std::move(callback),
|
||
|
std::move(moreArgs),
|
||
|
delay,
|
||
|
TimerSource::SetTimeout);
|
||
|
}));
|
||
|
|
||
|
runtime.global().setProperty(
|
||
|
runtime,
|
||
|
"clearTimeout",
|
||
|
jsi::Function::createFromHostFunction(
|
||
|
runtime,
|
||
|
jsi::PropNameID::forAscii(runtime, "clearTimeout"),
|
||
|
1, // timerID
|
||
|
[this](
|
||
|
jsi::Runtime& rt,
|
||
|
const jsi::Value& thisVal,
|
||
|
const jsi::Value* args,
|
||
|
size_t count) {
|
||
|
if (count > 0 && args[0].isNumber()) {
|
||
|
auto handle = (TimerHandle)args[0].asNumber();
|
||
|
deleteTimer(rt, handle);
|
||
|
}
|
||
|
return jsi::Value::undefined();
|
||
|
}));
|
||
|
|
||
|
runtime.global().setProperty(
|
||
|
runtime,
|
||
|
"setInterval",
|
||
|
jsi::Function::createFromHostFunction(
|
||
|
runtime,
|
||
|
jsi::PropNameID::forAscii(runtime, "setInterval"),
|
||
|
3, // Function, delay, ...args
|
||
|
[this](
|
||
|
jsi::Runtime& rt,
|
||
|
const jsi::Value& thisVal,
|
||
|
const jsi::Value* args,
|
||
|
size_t count) {
|
||
|
if (count == 0) {
|
||
|
throw jsi::JSError(
|
||
|
rt,
|
||
|
"setInterval must be called with at least one argument (the function to call).");
|
||
|
}
|
||
|
|
||
|
if (!args[0].isObject() || !args[0].asObject(rt).isFunction(rt)) {
|
||
|
throw jsi::JSError(
|
||
|
rt, "The first argument to setInterval must be a function.");
|
||
|
}
|
||
|
auto callback = args[0].getObject(rt).getFunction(rt);
|
||
|
auto delay = count > 1
|
||
|
? coerceNumberTimeout(rt, jsi::Value{rt, args[1]})
|
||
|
: 0.0;
|
||
|
|
||
|
// Package up the remaining argument values into one place.
|
||
|
std::vector<jsi::Value> moreArgs;
|
||
|
for (size_t extraArgNum = 2; extraArgNum < count; extraArgNum++) {
|
||
|
moreArgs.emplace_back(rt, args[extraArgNum]);
|
||
|
}
|
||
|
|
||
|
return createRecurringTimer(
|
||
|
std::move(callback),
|
||
|
std::move(moreArgs),
|
||
|
delay,
|
||
|
TimerSource::SetInterval);
|
||
|
}));
|
||
|
|
||
|
runtime.global().setProperty(
|
||
|
runtime,
|
||
|
"clearInterval",
|
||
|
jsi::Function::createFromHostFunction(
|
||
|
runtime,
|
||
|
jsi::PropNameID::forAscii(runtime, "clearInterval"),
|
||
|
1, // timerID
|
||
|
[this](
|
||
|
jsi::Runtime& rt,
|
||
|
const jsi::Value& thisVal,
|
||
|
const jsi::Value* args,
|
||
|
size_t count) {
|
||
|
if (count > 0 && args[0].isNumber()) {
|
||
|
auto handle = (TimerHandle)args[0].asNumber();
|
||
|
deleteRecurringTimer(rt, handle);
|
||
|
}
|
||
|
return jsi::Value::undefined();
|
||
|
}));
|
||
|
|
||
|
runtime.global().setProperty(
|
||
|
runtime,
|
||
|
"requestAnimationFrame",
|
||
|
jsi::Function::createFromHostFunction(
|
||
|
runtime,
|
||
|
jsi::PropNameID::forAscii(runtime, "requestAnimationFrame"),
|
||
|
1, // callback
|
||
|
[this](
|
||
|
jsi::Runtime& rt,
|
||
|
const jsi::Value& thisVal,
|
||
|
const jsi::Value* args,
|
||
|
size_t count) {
|
||
|
if (count == 0) {
|
||
|
throw jsi::JSError(
|
||
|
rt,
|
||
|
"requestAnimationFrame must be called with at least one argument (i.e: a callback)");
|
||
|
}
|
||
|
|
||
|
if (!args[0].isObject() || !args[0].asObject(rt).isFunction(rt)) {
|
||
|
throw jsi::JSError(
|
||
|
rt,
|
||
|
"The first argument to requestAnimationFrame must be a function.");
|
||
|
}
|
||
|
|
||
|
auto callback = jsi::Function::createFromHostFunction(
|
||
|
rt,
|
||
|
jsi::PropNameID::forAscii(rt, "RN$rafFn"),
|
||
|
0,
|
||
|
[callbackContainer = std::make_shared<jsi::Function>(
|
||
|
args[0].getObject(rt).getFunction(rt))](
|
||
|
jsi::Runtime& rt,
|
||
|
const jsi::Value& thisVal,
|
||
|
const jsi::Value* args,
|
||
|
size_t count) {
|
||
|
auto performance =
|
||
|
rt.global().getPropertyAsObject(rt, "performance");
|
||
|
auto nowFn = performance.getPropertyAsFunction(rt, "now");
|
||
|
auto now = nowFn.callWithThis(rt, performance, {});
|
||
|
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.
|
||
|
return createTimer(
|
||
|
std::move(callback),
|
||
|
std::vector<jsi::Value>(),
|
||
|
/* delay */ 0,
|
||
|
TimerSource::RequestAnimationFrame);
|
||
|
}));
|
||
|
|
||
|
runtime.global().setProperty(
|
||
|
runtime,
|
||
|
"cancelAnimationFrame",
|
||
|
jsi::Function::createFromHostFunction(
|
||
|
runtime,
|
||
|
jsi::PropNameID::forAscii(runtime, "cancelAnimationFrame"),
|
||
|
1, // timerID
|
||
|
[this](
|
||
|
jsi::Runtime& rt,
|
||
|
const jsi::Value& thisVal,
|
||
|
const jsi::Value* args,
|
||
|
size_t count) {
|
||
|
if (count > 0 && args[0].isNumber()) {
|
||
|
auto handle = (TimerHandle)args[0].asNumber();
|
||
|
deleteTimer(rt, handle);
|
||
|
}
|
||
|
return jsi::Value::undefined();
|
||
|
}));
|
||
|
}
|
||
|
|
||
|
} // namespace facebook::react
|