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

RJS-2852: Adding a CallInvoker-based scheduler for Core on React Native #6791

Merged
merged 11 commits into from
Jul 18, 2024

Conversation

kraenhansen
Copy link
Member

@kraenhansen kraenhansen commented Jul 16, 2024

What, How & Why?

This implements a realm::util::Scheduler for the React Native platform through the React Native CallInvoker API.

It's my current understanding that all active implementations of this API that a React Native app will construct, utilize the RuntimeSchedulerCallInvoker which in turn drop into either RuntimeScheduler_Legacy or RuntimeScheduler_Modern, both of which have invariants that we want when Core schedules calling of listeners:

  1. Running tasks using a RuntimeExecutor (ensuring the function will be called when it is safe to do so)
  2. Draining microtasks after execution (effectively solving Async JSI functions with promises block the event loop indefinitely  facebook/react-native#33006)
  3. Reporting uncaught JSError exceptions via handleJSError (effectively solving Refactor throwing uncaught exceptions from callbacks dispatched onto the event loop from C++ #6772 on React Native)

This fixes #2655 and #3571 and #6772 on React Native (iOS and Android) and replace the workaround introduced with #4389, #4725 and 03c30e7.

@kraenhansen kraenhansen added T-Internal no-changelog no-jira-ticket Skip checking the PR title for Jira reference labels Jul 16, 2024
@kraenhansen kraenhansen self-assigned this Jul 16, 2024
@cla-bot cla-bot bot added the cla: yes label Jul 16, 2024
Comment on lines 61 to 68
m_js_call_invoker->invokeAsync(
// Using low priority to avoid blocking rendering
facebook::react::SchedulerPriority::LowPriority,
// Wrapping the func in a shared_ptr to ensure it outlives the invocation
[func = std::make_shared<util::UniqueFunction<void()>>(std::move(func))] {
(*func)();
});
}
Copy link
Member

Choose a reason for hiding this comment

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

[ptr = func.release())] { util::UniqueFunction<void()>(ptr)(); }

LowPriority seems suspect to me. We do want it to be below user input, but processing refreshes promptly is pretty important.

Copy link
Member Author

@kraenhansen kraenhansen Jul 16, 2024

Choose a reason for hiding this comment

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

These are the available values https://github.com/facebook/react-native/blob/8d0cbbf0e6435e0a29d1d5d218b93c9eef1a2dcd/packages/react-native/ReactCommon/callinvoker/ReactCommon/SchedulerPriority.h#L14-L20

enum class SchedulerPriority : int {
  ImmediatePriority = 1,
  UserBlockingPriority = 2,
  NormalPriority = 3,
  LowPriority = 4,
  IdlePriority = 5,
};

Unfortunately not greatly documented 🤔
NormalPriority might be better.

Copy link
Member

Choose a reason for hiding this comment

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

Glancing at the implementation, it looks like it always runs the highest priority waiting task unless a lower priority task has hit a timeout (250ms for UserBlocking, 5s for Normal, 10s for Low). If there's some specific Normal priority thing that we know we need to not interrupt then using Low makes sense, but otherwise I would default to Normal?

Copy link
Member Author

@kraenhansen kraenhansen Jul 17, 2024

Choose a reason for hiding this comment

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

[ptr = func.release())] { util::UniqueFunction<void()>(ptr)(); }

I try to refrain from c-style castings 🙈 It seems to work, but what ensure the original function owned by func ever gets destructed?

The docs for release says:

If not null, the returned pointer must be used at a later point to construct a new UniqueFunction.

Copy link
Member

Choose a reason for hiding this comment

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

There's no casts involved there. That's constructing a new UniqueFunction using the impl pointer (which takes ownership of the pointer) and then immediately invoking it. The function is then destroyed at the end of the statement as it's an unnamed temporary.

Copy link
Member Author

@kraenhansen kraenhansen Jul 17, 2024

Choose a reason for hiding this comment

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

I don't know how I misread that as a cast 🙈 What about the case where the lambda is dropped and never called, that would leak the function, right? (since no new UniqueFunction takes ownership) .. although very unlikely. I like that your suggestion doesn't need to perform a heap allocation - and I've pushed an update to incorporate it 👍

Copy link
Member

Choose a reason for hiding this comment

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

This relies on CallInvoker guaranteeing that it'll invoke the function it's given exactly once; more than once and it's a use-after-free and zero times is a memory leak. This is generally a very safe thing to rely on as basically everything gets way more complicated otherwise.

std::shared_ptr<Scheduler> scheduler_{};


class ReactScheduler : public realm::util::Scheduler {
Copy link
Member

Choose a reason for hiding this comment

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

This is very minor, but this type and the global variable should be in an anonymous namespace so that the symbols are not exported.

Copy link
Contributor

@kneth kneth left a comment

Choose a reason for hiding this comment

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

It is a better solution that the old workaround.

Please include an entry in CHANGELOG.md - maybe under "Internal" if you don't consider it a bug fix.

@@ -79,7 +79,7 @@ - (void)dealloc {
// See https://github.com/facebook/react-native/pull/43396#issuecomment-2178586017 for context
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we remove the comments?

@@ -79,7 +79,7 @@ - (void)dealloc {
// See https://github.com/facebook/react-native/pull/43396#issuecomment-2178586017 for context
// If it was, we could use the enablement of "microtasks" to avoid the overhead of calling the invokeAsync on every call from C++ into JS.
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we remove the comments?

Comment on lines +6 to +8
".wireit",
"packages/realm/.wireit",
"integration-tests/environments/react-native-test-app/.wireit"
Copy link
Member Author

Choose a reason for hiding this comment

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

I got annoyed by these being indexed by watchman.

@kraenhansen kraenhansen requested a review from tgoyne July 17, 2024 10:07
@kraenhansen kraenhansen changed the title Adding a CallInvoker-based scheduler for Core on React Native RJS-2852: Adding a CallInvoker-based scheduler for Core on React Native Jul 17, 2024
@kraenhansen kraenhansen removed the no-jira-ticket Skip checking the PR title for Jira reference label Jul 17, 2024
@kraenhansen kraenhansen merged commit 13e70d7 into main Jul 18, 2024
27 of 29 checks passed
@kraenhansen kraenhansen deleted the kh/rn-call-invoker-scheduler branch July 18, 2024 04:24
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Aug 17, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
4 participants