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
fix: allow Node.js to manage microtasks queue #28957
fix: allow Node.js to manage microtasks queue #28957
Conversation
Here's the "test case" (benchmark really): https://github.com/indutny/electron-napi-churn-demo Build instructions: npm install Test instructions (Node.js): node app/preload.js Test instructions (electron): ./node_modules/.bin/electron app/main.js Numbers: # Node.js
190.523221ms
# Electron (unpatched)
369.185357ms
# Electron (patched)
194.212882ms |
The downside of this approach is that malformed code like this: function hang() {
setImmediate(hang);
}
hang(); causes starvation of the main thread (while UV loop continues to run fine). |
@indutny is this something you plan to upstream? Also - could you please add some release notes? |
Can we check if |
I'm not sure if it makes much sense for Node.js, since there is no extra cost for waiting for a next uv loop iteration there. @addaleax could you take a look a the patch? I'd appreciate to hear your opinion on this issue.
Done, sorry for not writing these immediately! |
This is a great idea! Let me come up with a patch real quick! |
btw, as you can imagine I've ran into this while investigating performance issues in a real app. In the case of that app |
@deepak1556 @codebytere all resolved except for submitting it as a PR to Node.js (see above). |
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 think if you want to upstream this, it might be nicer to move the if (size == 0) { ... }
block in this function to after the if (popped_value) { ... }
block (and thus avoiding the call to uv_idle_stop()
if it’s not necessary), instead of restarting the idle handle even if there are not actually any new items in the queue?
patches/node/node-api_poll_immediately_in_threadsafe_function.patch
Outdated
Show resolved
Hide resolved
shell/common/node_bindings.cc
Outdated
|
||
// Make sure that `setImmediate` yields to browser to avoid starvation | ||
auto* immediate_idle_handle = | ||
reinterpret_cast<uv_handle_t*>(env->immediate_idle_handle()); |
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.
Is there a public API that Node.js could reasonable introduce at some point here (without exposing the idle handle itself, which is really more of an implementation detail)?
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.
Sounds like a good idea to me. This PR just wants to know if setImmediate
is scheduled for run in order to interleave uv_run()
with browser's event loop and avoid starvation.
(Argh, wanted to move my comment to top-level and copy-paste failed me) I've change the patch to make it avoid calling With this in mind, would you say I should submit it upstream (Node.js)? |
@indutny Right, but … my point is more like, after the
Yeah, I agree. Maybe it would also make sense to do something here that matches what we do for e.g. incoming messages on MessagePorts, i.e. loop over the queue synchronously up to a specific iteration number limit, and only return control back to the event loop and defer the other iterations if that limit has been exceeded? (Because the only reason I could imagine |
The trouble is that microtasks queue is emptied after
I see. As an extra sanity check, I think this could be a great idea. Maybe we don't even need that idle handle check if there is a limit to the execution count anyway (cc @deepak1556 @codebytere )? Correct me if I'm wrong @addaleax, but Node.js uses 1000 as a limit for same tick message processing and maybe Electron should do here the same? |
Yes, or the patch to Node.js could also do the same thing here already if that makes sense (in which case upstreaming it also seems like a good idea). |
I actually had a patch in works that did that and many other things 😂, but then microtasks queue killed whole idea. Let me give it few more tryouts to see why if (!tick_info->has_tick_scheduled()) {
env_->context()->GetMicrotaskQueue()->PerformCheckpoint(env_->isolate());
perform_stopping_check();
} |
Well… if If the |
@addaleax could |
@indutny That really depends on the JS callback here :) But, sure, if the callback doesn’t schedule any |
@addaleax the callback is basically an empty |
Here's the PR with the speed improvements for Node.js: nodejs/node#38506 I've just tested the electron build without that extra |
FWIW, Electron only has |
Maybe the |
@addaleax I don't know, sounds like the call happens only on destructor and this in turn happens only after I can try adding some debug printing to the build to see why it is such. Let me see. |
Oh gosh, I see now that the condition is not has_tick_scheduled. My bad! Yeah, it is funky. |
Alright, now I have a better understanding of what's going on in Electron thanks to invaluable help of @addaleax . Electron uses scoped microtask policy (with some floating v8 patches so that node's explicit policy use doesn't crash it), and so the microtasks queue is not emptied until we left Seems like there are two ways to go forward with this:
My impression is that the preference (correct me if I'm wrong @addaleax ) should go for the first one. In this case, it looks like no changes to the electron's code base would be needed other than pulling up the patches from Node.js upstream! |
Yeah, I’m sorry, I thought that that would help, but ultimately, neither leaving a MicrotasksScope nor calling MicrotaskQueue::PerformCheckpoint() have any effect when inside another MicrotasksScope. I’m wondering why Electron has the outer MicrotasksScope in the first place – can we avoid that somehow? |
I'm going to try out changing the microtasks policy right before entering uv_run() and then changing it back to what it was before that. Wish me luck! |
/trop run backport |
The backport process for this PR has been manually initiated - here we go! :D |
Thanks! |
I have automatically backported this PR to "12-x-y", please check out #28972 |
I have automatically backported this PR to "13-x-y", please check out #28973 |
I have automatically backported this PR to "11-x-y", please check out #28974 |
Description of Change
When
uv_run()
resulted in invocation of JS functions the microtaskqueue checkpoint in Node's CallbackScope was a no-op because the
expected microtask queue policy was
kExplicit
and Electron ran underkScoped
policy. This change switches policy tokExplicit
rightbefore
uv_run()
and reverts it back to original value afteruv_run()
completes to provide better compatibility with Node.
cc @codebytere since you touched this code.
Checklist
npm test
passesRelease Notes
Notes: Allow Node.js to manage microtasks queue by using explicit microtasks policy before calling
uv_run()