-
Notifications
You must be signed in to change notification settings - Fork 24.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Implement requestIdleCallback (#44636) #44759
Conversation
Base commit: e78742a |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for working on this, Robert! This is a great starting point. Please take a look at my comments and let me know if you have any questions.
packages/react-native/src/private/webapis/microtasks/specs/NativeMicrotasks.js
Outdated
Show resolved
Hide resolved
packages/react-native/ReactCommon/react/runtime/TimerManager.cpp
Outdated
Show resolved
Hide resolved
packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler.h
Outdated
Show resolved
Hide resolved
packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp
Outdated
Show resolved
Hide resolved
packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeSchedulerBinding.h
Outdated
Show resolved
Hide resolved
packages/react-native/ReactCommon/react/nativemodule/microtasks/NativeMicrotasks.h
Outdated
Show resolved
Hide resolved
packages/react-native/ReactCommon/react/nativemodule/microtasks/IdleDeadline.cpp
Outdated
Show resolved
Hide resolved
packages/react-native/ReactCommon/react/nativemodule/microtasks/IdleDeadline.cpp
Outdated
Show resolved
Hide resolved
packages/react-native/ReactCommon/react/nativemodule/microtasks/IdleDeadline.cpp
Outdated
Show resolved
Hide resolved
Also, what is the preferred approach for writing tests for this? C++ and JS unit tests? Are any existing ones I can base them on? 🤔 |
You can just test the native bits using a similar test to the one we use for |
cbed54b
to
2e4c6bf
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great! Please address the comments and take a look at the failing CI jobs.
packages/react-native/ReactCommon/react/nativemodule/idlecallbacks/IdleTaskRef.h
Outdated
Show resolved
Hide resolved
packages/react-native/ReactCommon/react/nativemodule/idlecallbacks/IdleTaskRef.cpp
Outdated
Show resolved
Hide resolved
packages/react-native/ReactCommon/react/nativemodule/idlecallbacks/NativeIdleCallbacks.cpp
Outdated
Show resolved
Hide resolved
packages/react-native/ReactCommon/react/nativemodule/idlecallbacks/NativeIdleCallbacks.cpp
Outdated
Show resolved
Hide resolved
packages/react-native/ReactCommon/react/nativemodule/idlecallbacks/NativeIdleCallbacks.cpp
Outdated
Show resolved
Hide resolved
size_t /* unused */ | ||
) noexcept -> jsi::Value { | ||
auto didTimeout = args[0].getBool(); | ||
// Below is a partial solution and does not comply to WHATWG event-loop standards |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How is this not spec compliant? The spec doesn't require this to align with the frame duration and it allows implementations to specific a static deadline.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I just re-read the PR summary and I understood. I forgot about this problem when we changed the design. I think it's fine because we check getShouldYield
which would return true
if there's any other higher priority task pending execution. It might be good to explain this in the comment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if that is related (or is) frame alignment, but we have separate and fixed deadlines for each idleTask
. This might be problematic if there are lots of idleTask
s which might impact the performance.
Unless I misunderstood or missed anything, the deadline should be computed at RuntimeScheduler
(or Scheduler::EventPipeConclusion
perhaps?), reduced by requestAnimationFrame
and timers run time and then read from IdleCallbacks
TurboModule. I postponed this to reduce changes in RuntimeScheduler
API as the initial design had this right 😅.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So the reason why we have a fixed time for all idle callbacks on Web is that we need to yield the execution of JS at regular intervals to process rendering. That's done in the main thread in JS, so this is never an issue for us. In the case that we need to process a user interaction, we'd be scheduling a task with a higher priority in the scheduler, which would make getShouldYield
return true, so we'd return 0
the next time the current idle callback checks the time remaining, and we'd stop running idle callbacks after that. Essentially, it's the same behavior as on Web but with a different implementation.
Would be good to clarify this behavior in a comment here :)
packages/react-native/ReactCommon/react/renderer/runtimescheduler/RuntimeScheduler_Legacy.cpp
Show resolved
Hide resolved
@rubennorte has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's try to avoid having to add yet another .so
packages/react-native/ReactCommon/react/nativemodule/idlecallbacks/CMakeLists.txt
Outdated
Show resolved
Hide resolved
@cortinico do we have any so merging mechanism in OSS builds? I think we're moving away from forcing static on libraries internally and not sure if we should change it in this case (Robert was following the same example we have for the microtask native module). |
1c6a1eb
to
e4d89a7
Compare
Yes we do. Unless you have a valid reason for making this library a dynamic library, it should default to be static. Copying from other libraries is sadly going to push us more work down the line. Most of the libs we don't explicitly load with |
packages/react-native/ReactCommon/react/nativemodule/idlecallbacks/IdleTaskRef.cpp
Outdated
Show resolved
Hide resolved
} | ||
); | ||
|
||
auto timeoutDuration = std::chrono::duration<double, std::milli>(timeout.value_or(0.0)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should use the default param in scheduleIdleTask
instead of 0
when timeout
isn't set, because otherwise we'd be using the priority of normal tasks instead of the priority of idle tasks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this function is for idle callbacks only, maybe it would be better to make the timeout
max(timeout, timeoutForPriotity(SchedulerPriority::Idle))
in the scheduleIdleTask
function body? This way we don't allow queueing normal tasks with this and the API contract is more clear. WDYT? 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
timeoutForPriotity(SchedulerPriority::Idle)
is infinite, so that max
will always give you this value. The moment you specify a timeout you're kinda saying, after this consider this to be a normal task, so I think it should be fine. If anything, we should do something like expirationTime = now() + timeout + timeoutForPriority(SchedulerPriority::Normal)
, so worst case scenario (timeout = 0) this is scheduled as a normal task. That should be handled in RuntimeScheduler_Modern
though, and we should still honor the defaults here if the timeout wasn't defined.
With this code, we still have the problem of requestIdleCallback(cb)
being scheduled as a normal priority task, instead of as a regular idle priority task without a timeout.
packages/react-native/ReactCommon/react/nativemodule/idlecallbacks/NativeIdleCallbacks.cpp
Outdated
Show resolved
Hide resolved
packages/react-native/ReactCommon/react/nativemodule/idlecallbacks/NativeIdleCallbacks.h
Outdated
Show resolved
Hide resolved
packages/react-native/ReactCommon/react/nativemodule/microtasks/NativeMicrotasks.cpp
Outdated
Show resolved
Hide resolved
packages/react-native/ReactCommon/react/nativemodule/microtasks/NativeMicrotasks.h
Outdated
Show resolved
Hide resolved
@rubennorte has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
timeout = options.value().timeout; | ||
} | ||
|
||
auto userCallback = std::make_shared<jsi::Function>(std::move(callback)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks very sketchy. We shouldn't have shared pointers to jsi::Function
because it prevents some of the safety mechanisms defined in that class. We should define the parameter in this function as SyncCallback
instead, which allows us to move it into the lambda.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've just checked the code and it looks like I cannot use the RawCallback&& variant
. This is because it uses the std::function
under the hood, which is required to be copyable, so we cannot move capture the callback
into the lambda.
I've pushed the version that uses SyncCallback
but still uses shared_ptr
|
||
auto userCallback = std::make_shared<jsi::Function>(std::move(callback)); | ||
|
||
auto wrappedCallback = jsi::Function::createFromHostFunction( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This doesn't need to be a host function, because we're not calling it directly but passing it to the runtime scheduler. I can just be an std::function
that calls the SyncCallback
that we receive via params.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh right, I just (wrongly) assumed that I need access to didTimeout
argument at the moment of writing
const jsi::Value* args, | ||
size_t /* unused */ | ||
) noexcept -> jsi::Value { | ||
auto didTimeout = args[0].getBool(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just realized this is wrong. If we have something like this:
requestIdleCallback(() => {
/* ... */
}, 10);
And this executes after 1 second, didTimeout
would be false because, as an implementation detail, we actually used the timeout of the normal priority tasks, which is 5 seconds. We should check the current time here and use that to determine if it exceeded the timeout instead.
timeout = options.value().timeout; | ||
} | ||
|
||
auto userCallbackShared = std::make_shared<SyncCallback<void(jsi::Object)>>(std::move(userCallback)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me try to find a way to pass this without the shared pointer tomorrow.
|
||
auto wrappedCallback = [runtimeScheduler, userCallbackShared]( | ||
jsi::Runtime& runtime | ||
) noexcept -> void { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This shouldn't have noexcept
because the user callback that you're calling here can throw.
auto deadline = runtimeScheduler->now() + 50ms; | ||
|
||
jsi::Object idleDeadline {runtime}; | ||
idleDeadline.setProperty(runtime, "didTimeout", false); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should check the time and set this property accordingly.
@@ -7,6 +7,8 @@ | |||
|
|||
#pragma once | |||
|
|||
#include <memory> | |||
#include <unordered_map> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
They're still here 🤔
@rubennorte has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator. |
} | ||
|
||
auto now = runtimeScheduler->now(); | ||
double remainingTime = std::chrono::duration_cast<std::chrono::milliseconds>(deadline - now).count(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For future reference, this deadline
here is garbage because it's passed as a reference to this host function and the value is being cleared from the stack of makeTimeRemainingFunction
, which made timeRemaining()
always return 0. The fix is to copy the value to the lambda.
I'll fix this before landing.
ff37b8d
to
288335a
Compare
288335a
to
cc9179d
Compare
the feedback was addressed already
This pull request was successfully merged by @robik in abfadc6. When will my fix make it into a release? | How to file a pick request? |
@rubennorte merged this pull request in abfadc6. |
Summary:
Implements
requestIdleCallback
andcancelIdleCallback
Notes
Proposed implementation does yet cover all WHATWG eventloop requirements.
50ms
, rather than it being shared between other idle callbacks.Changelog:
requestIdleCallback
andcancelIdleCallback
Test Plan: