Skip to content
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

Closed
wants to merge 1 commit into from

Conversation

robik
Copy link
Contributor

@robik robik commented Jun 3, 2024

Summary:

Implements requestIdleCallback and cancelIdleCallback

Notes

Proposed implementation does yet cover all WHATWG eventloop requirements.

  • Deadline computation is not implemented and is polyfilled by giving each callback 50ms, rather than it being shared between other idle callbacks.
  • The requested callbacks are called with lowest priority by the scheduler as of now, but the execution is not as described in the standard.

Changelog:

  • [GENERAL] [ADDED] - Implemented requestIdleCallback and cancelIdleCallback

Test Plan:

@facebook-github-bot facebook-github-bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jun 3, 2024
@robik robik changed the title feat: Implement requestIdleCallback feat: Implement requestIdleCallback (#44636) Jun 3, 2024
@analysis-bot
Copy link

analysis-bot commented Jun 3, 2024

Platform Engine Arch Size (bytes) Diff
android hermes arm64-v8a 20,431,243 +65,684
android hermes armeabi-v7a n/a --
android hermes x86 n/a --
android hermes x86_64 n/a --
android jsc arm64-v8a 23,635,260 +65,637
android jsc armeabi-v7a n/a --
android jsc x86 n/a --
android jsc x86_64 n/a --

Base commit: e78742a
Branch: main

Copy link
Contributor

@rubennorte rubennorte left a 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.

@robik
Copy link
Contributor Author

robik commented Jun 5, 2024

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? 🤔

@rubennorte
Copy link
Contributor

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 RuntimeScheduler. The JS layer is so thin that's probably not worth testing it. We're in discussions to have proper integration tests covering both JS and C++, and we'd be able to test this e2e, but we're not there yet.

@robik robik force-pushed the robik/request-idle-callback branch from cbed54b to 2e4c6bf Compare June 11, 2024 10:07
Copy link
Contributor

@rubennorte rubennorte left a 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.

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
Copy link
Contributor

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.

Copy link
Contributor

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.

Copy link
Contributor Author

@robik robik Jun 11, 2024

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 idleTasks 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 😅.

Copy link
Contributor

@rubennorte rubennorte Jun 12, 2024

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 :)

@facebook-github-bot
Copy link
Contributor

@rubennorte has imported this pull request. If you are a Meta employee, you can view this diff on Phabricator.

Copy link
Contributor

@cortinico cortinico left a 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

@rubennorte
Copy link
Contributor

Let's try to avoid having to add yet another .so

@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).

@robik robik force-pushed the robik/request-idle-callback branch 2 times, most recently from 1c6a1eb to e4d89a7 Compare June 12, 2024 08:42
@cortinico
Copy link
Contributor

Let's try to avoid having to add yet another .so

@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).

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 SoLoader.loadLibrary() can be static and we should be aiming at that direction.

}
);

auto timeoutDuration = std::chrono::duration<double, std::milli>(timeout.value_or(0.0));
Copy link
Contributor

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.

Copy link
Contributor Author

@robik robik Jun 13, 2024

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? 🤔

Copy link
Contributor

@rubennorte rubennorte Jun 13, 2024

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.

@facebook-github-bot
Copy link
Contributor

@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));
Copy link
Contributor

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.

Copy link
Contributor Author

@robik robik Jun 13, 2024

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(
Copy link
Contributor

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.

Copy link
Contributor Author

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();
Copy link
Contributor

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));
Copy link
Contributor

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 {
Copy link
Contributor

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);
Copy link
Contributor

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>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They're still here 🤔

@facebook-github-bot
Copy link
Contributor

@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();
Copy link
Contributor

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.

@rubennorte rubennorte marked this pull request as ready for review June 14, 2024 18:10
@rubennorte rubennorte dismissed cortinico’s stale review June 14, 2024 18:11

the feedback was addressed already

Copy link

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?

@facebook-github-bot facebook-github-bot added the Merged This PR has been merged. label Jun 17, 2024
@github-actions github-actions bot added the Merged This PR has been merged. label Jun 17, 2024
@facebook-github-bot
Copy link
Contributor

@rubennorte merged this pull request in abfadc6.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. Merged This PR has been merged.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants