-
Notifications
You must be signed in to change notification settings - Fork 395
Promises executor dominates over timeouts #4129
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
Comments
Thanks for the detailed report! FTR, the code is here: scala-js/library/src/main/scala/scala/scalajs/concurrent/QueueExecutionContext.scala Lines 46 to 62 in 430f915
There's nothing that obviously tries to short-circuit the event loop. We explicitly call then for every job, which is always supposed to yield to event loop. But perhaps it doesn't for already-completed Promises?
|
I dug deeper myself! It appears that both But at any rate, this implies that |
fyi - V8 has separates queues for tasks, microtask queue (used e.g. for promise.then) and macrotask queue (used e.g. for setTimeout): The spec for promises and jobs deliberately doesn't restrict how priority should work for different types of tasks: |
The fact that I can get things to stack overflow by using |
If that is true, that's a huge bug in V8, and we should report it if it hasn't already been reported. Because that is clearly violating the specs of Promises. |
It's like, all over StackOverflow (recursive constructor), so it's certainly not an unknown thing. I think it's also very telling that the polyfills for Hunting around a bit, I think that @oleg-py hit it right on the head: this is a difference between the micro- and macrotask queues. The microtask queue, by spec, doesn't trampoline until all tasks are exhausted. Which is to say that if you have an eager microtask which enqueues an additional microtask callback, the stack depth will increase proportionally. This is in contrast to macrotasks, which tick each time (source: the first paragraph of this section). This isn't a violation of the spec, it's just what
|
The link you mention explains that the constructor runs synchronously, but that a callback to Now, I agree with the microtask v macrotask explanation. That explains why endless chains of
I'm reluctant to abandon the |
What's the advantage to this though? Why not just always use I do believe we need to use the macrotask loop, otherwise neither timers nor IO will work correctly. This is basically a matter of fairness in scheduling.
It's not an issue for them for two reasons. First, you don't see JavaScript applications that are as pervasively Also remember that most people using I would also disagree with the characterization that The scenario where pure |
i think setImmediate is what's needed here, too - but https://developer.mozilla.org/en-US/docs/Web/API/Window/setImmediate says "This method is not expected to become standard, and is only implemented by recent builds of Internet Explorer and Node.js 0.10+. It meets resistance both from Gecko (Firefox) and Webkit (Google/Apple)." and relying on a polyfill that abuses there's no way to regain fairness by most often using |
That would help. If you want to straight-up avoid polyfills, it's not a horrible thing to do. It's still going to affect timer granularity to a significant degree, but perhaps that doesn't matter so much. Like, The trick is how do you figure that out? Like we have no way of knowing, within the Is that really better than the polyfill? Like, if you look at the implementation, it's actually relying on some relatively robust mechanisms all things considered. By "robust" I mean "literally what the Chrome team suggests you use instead of |
For anyone who finds this issue who is looking for a cargo-cultable quick fix… Cats Effect now has a partial implementation of a To be clear, my preferred solution here is still to upstream this functionality, since I do believe that |
FWIW Scala.js' HTML executor also needs to work around this: |
At this point, I observe the following:
So I think trying to build an execution context that yields every once in a while to the macrotask queue is the best we can do. I can take a stab at this. |
There's a new wrinkle in this btw. The only way you can reliably yield to the macrotask queue in a cross-platform fashion would be to use The problem is that you're going to end up messing significantly with sequencing and interleaving between parallel operations this way, since in any race between two One route you could take, if you really don't want to do the full The only downside to the above is you basically have to detect the webworker environment and fall back to (speaking of which, I'm still in the hunt for a good way to run ScalaJS within web workers in a CI environment, for exactly this reason) |
Thank you for pointing this out. Knowing this quite shifts my perspective on this: We might have to accept that there isn't a good default Note that Scala.js has it as an explicit goal to not rely on DOM APIs, but only things standardized in ECMAScript ( Maybe we should look at this from a different angle:
|
@sjrd what are your thoughts on this? ATM I think the best we can do in Scala.js core is somehow warn when |
I'm still very uneasy about this whole thing. I have reread the whole discussion here. It I have to reluctantly agree that Yes,
In addition, all the engines that support timers support I would like to say that other than that, we stick 100% to ES 2015. However I also have to admit that 1 case of polyfilling a non-ECMAScript API leaked into our implementation, a long time ago: using scala-js/javalanglib/src/main/scala/java/lang/System.scala Lines 72 to 92 in 88a19cf
At least that one has a compliant fallback (if less precise). Using even a polyfill of Remains the option of warning against ATM I don't see any real solution. All the choices we have are damned. |
I wouldn't actually suggest something else (for portable usage). Simply allow users to disable the warning (ideally with the existing |
Hum, yes, I guess that makes sense. |
I wasn't aware of the policy against polyfills. That design decision makes a ton of sense and I support it. My thoughts for what they're worth… Leave the Promises executor in place exactly as it is, but warn whenever it (or global) is used. In the warning, point users to a separate library which ports setImmediate to ScalaJS. I'm happy to offer the PolyfillExecutionContext from Cats Effect as the starting point. It's definitely clear that promises as an executor cause some serious problems in certain common types of execution modalities, but the problems aren't severe or ubiquitous enough (IMO) to warrant full on removal. A disableable warning and a blessed separate library seems best. |
@djspiewak would you be willing to publish the execution context of Cats Effect separately? If yes, I'll be happy to implement the relevant compiler warnings and point folks to it. @sjrd would that work for you? |
Yes, that sounds reasonable. |
Ping @djspiewak |
Sorry! Missed this. And yes I'm quite willing to do that. This would also make it a little easier to facilitate contributions from people who may have relevant knowledge on it, but who would rather not dive into Cats Effect itself. |
Absolutely! Web execution scheduling and Cats Effect seems to be two quite different pairs of shoes. In that case, I'll start investigating options of how we can warn when the global execution context is used. |
It seems that from a user interface POV, we have the following options to disable a warning when importing
@sjrd opinions? |
I think we should offer the global compiler option and an Solutions that are not JVM compatible are not useful, IMO. |
Who is this waiting on? @djspiewak, do you simply need a repo under the scala-js org or is there anything else needed? (seems like |
@gzm0 We're waiting on the repository creation. :-) Once that happens, we can PR the basic skeleton pretty quickly. It'll need GHA approval from an admin unless y'all give us write/admin access, but after that things should smooth out. |
I created https://github.com/scala-js/scala-js-macrotask-executor and added you as a maintainer. I'll leave it up to @sjrd how to organize permissions. |
The above project now contains the polyfill executor implementation, as well as almost all the associated testing and CI infrastructure. The only thing it's missing at present is the infrastructure for testing under webworker environments, which we're working on. Incidentally, doing this uncovered the fact that the polyfill is not working on jsdom. It doesn't fail anything, but it falls back to Plan of record here is the following: typelevel/cats-effect#2314 |
Quick update: since yesterday we now have WebWorker test infrastructure running, as well as patched the polyfill for JSDOM. |
I'll leave this issue open to track the culmination of @gzm0's compiler warning idea. In the meantime, over in polyfill land, I'll open an issue to track the drive towards 1.0.0. |
QQ is there already a readme somewhere that explains why using the global execution context is potentially bad? I doubt I'll come up with something short enough for an error message. So I was thinking to just refer to a page. |
Does the readme at https://github.com/scala-js/scala-js-macrotask-executor work? |
"The global execution context is based on Promises, which exhibit unfair scheduling semantics with respect to other elements of the event loop, such as I/O events, timers, and UI rendering. It is recommended that users employ the macrotask executor, to be found here: https://github.com/scala-js/scala-js-macrotask-executor" Ish? |
I took a stab at making @djspiewak's snippet hopefully a bit easier to follow for the less-informed:
|
The behavior in the presence of imports is unfortunately not ideal here, especially when the implicit ExecutionContext is imported. Ideally, we'd like a warning on the import already and the ability to silence the warning once for an entire file. However, imports get fully resolved in the typer phase, so this would require processing imports in the pre-typer phase, where the entire suppression infrastructure is not available (it requires typing of the @nowarn annotations). Therefore, we live with the sub-optimal situation, especially given that there is a compiler flag to disable the warnings overall.
The behavior in the presence of imports is unfortunately not ideal here, especially when the implicit ExecutionContext is imported. Ideally, we'd like a warning on the import already and the ability to silence the warning once for an entire file. However, imports get fully resolved in the typer phase, so this would require processing imports in the pre-typer phase, where the entire suppression infrastructure is not available (it requires typing of the @nowarn annotations). Therefore, we live with the sub-optimal situation, especially given that there is a compiler flag to disable the warnings overall.
The behavior in the presence of imports is unfortunately not ideal here, especially when the implicit ExecutionContext is imported. Ideally, we'd like a warning on the import already and the ability to silence the warning once for an entire file. However, imports get fully resolved in the typer phase, so this would require processing imports in the pre-typer phase, where the entire suppression infrastructure is not available (it requires typing of the @nowarn annotations). Therefore, we live with the sub-optimal situation, especially given that there is a compiler flag to disable the warnings overall.
The behavior in the presence of imports is unfortunately not ideal here, especially when the implicit ExecutionContext is imported. Ideally, we'd like a warning on the import already and the ability to silence the warning once for an entire file. However, imports get fully resolved in the typer phase, so this would require processing imports in the pre-typer phase, where the entire suppression infrastructure is not available (it requires typing of the @nowarn annotations). Therefore, we live with the sub-optimal situation, especially given that there is a compiler flag to disable the warnings overall.
The behavior in the presence of imports is unfortunately not ideal here, especially when the implicit ExecutionContext is imported. Ideally, we'd like a warning on the import already and the ability to silence the warning once for an entire file. However, imports get fully resolved in the typer phase, so this would require processing imports in the pre-typer phase, where the entire suppression infrastructure is not available (it requires typing of the @nowarn annotations). Therefore, we live with the sub-optimal situation, especially given that there is a compiler flag to disable the warnings overall.
The behavior in the presence of imports is unfortunately not ideal here, especially when the implicit ExecutionContext is imported. Ideally, we'd like a warning on the import already and the ability to silence the warning once for an entire file. However, imports get fully resolved in the typer phase, so this would require processing imports in the pre-typer phase, where the entire suppression infrastructure is not available (it requires typing of the @nowarn annotations). Therefore, we live with the sub-optimal situation, especially given that there is a compiler flag to disable the warnings overall.
The behavior in the presence of imports is unfortunately not ideal here, especially when the implicit ExecutionContext is imported. Ideally, we'd like a warning on the import already and the ability to silence the warning once for an entire file. However, imports get fully resolved in the typer phase, so this would require processing imports in the pre-typer phase, where the entire suppression infrastructure is not available (it requires typing of the @nowarn annotations). Therefore, we live with the sub-optimal situation, especially given that there is a compiler flag to disable the warnings overall.
The behavior in the presence of imports is unfortunately not ideal here, especially when the implicit ExecutionContext is imported. Ideally, we'd like a warning on the import already and the ability to silence the warning once for an entire file. However, imports get fully resolved in the typer phase, so this would require processing imports in the pre-typer phase, where the entire suppression infrastructure is not available (it requires typing of the @nowarn annotations). Therefore, we live with the sub-optimal situation, especially given that there is a compiler flag to disable the warnings overall.
The behavior in the presence of imports is unfortunately not ideal here, especially when the implicit ExecutionContext is imported. Ideally, we'd like a warning on the import already and the ability to silence the warning once for an entire file. However, imports get fully resolved in the typer phase, so this would require processing imports in the pre-typer phase, where the entire suppression infrastructure is not available (it requires typing of the @nowarn annotations). Therefore, we live with the sub-optimal situation, especially given that there is a compiler flag to disable the warnings overall.
The behavior in the presence of imports is unfortunately not ideal here, especially when the implicit ExecutionContext is imported. Ideally, we'd like a warning on the import already and the ability to silence the warning once for an entire file. However, imports get fully resolved in the typer phase, so this would require processing imports in the pre-typer phase, where the entire suppression infrastructure is not available (it requires typing of the @nowarn annotations). Therefore, we live with the sub-optimal situation, especially given that there is a compiler flag to disable the warnings overall.
The behavior in the presence of imports is unfortunately not ideal here, especially when the implicit ExecutionContext is imported. Ideally, we'd like a warning on the import already and the ability to silence the warning once for an entire file. However, imports get fully resolved in the typer phase, so this would require processing imports in the pre-typer phase, where the entire suppression infrastructure is not available (it requires typing of the @nowarn annotations). Therefore, we live with the sub-optimal situation, especially given that there is a compiler flag to disable the warnings overall.
The behavior in the presence of imports is unfortunately not ideal here, especially when the implicit ExecutionContext is imported. Ideally, we'd like a warning on the import already and the ability to silence the warning once for an entire file. However, imports get fully resolved in the typer phase, so this would require processing imports in the pre-typer phase, where the entire suppression infrastructure is not available (it requires typing of the @nowarn annotations). Therefore, we live with the sub-optimal situation, especially given that there is a compiler flag to disable the warnings overall.
Fix #4129: Warn if the default global execution context is used.
First, some table-setting:
This does what you would expect: it loops for a while and then cancels.
So does the following:
Again, loops for a little while and then cancels.
This, however, loops forever:
It appears that if any outstanding work is available on the
promises
queue, it will dominate over the timeouts queue. My guess without looking at the code is that this is a consequence of a well-intentioned optimization within ScalaJS itself which fails to yield, since it definitely doesn't reproduce within Node.The text was updated successfully, but these errors were encountered: