resourceloader: Optimise and simplify state propagation logic
aka "handlePending 2.0",
aka "don't recurse 300x before executing a module",
aka "don't break DevTools flame graphs".
== Impact
Comparison based on viewing the default Main page on Chrome stable.
The local MediaWiki has a few extensions installed (EventLogging,
ULS, Navigation Timing).
Measured by alternating between before/after and logging 'mediaWikiLoadEnd'
from ext.navigationTiming.js, and evaluating the following on the console:
```
({ responseStart: performance.timing.responseStart - performance.timing.navigationStart,
domInteractive: performance.timing.domInteractive - performance.timing.navigationStart,
domComplete: performance.timing.domComplete - performance.timing.navigationStart,
loadEventEnd: performance.timing.loadEventEnd - performance.timing.navigationStart });
```
This was repeated five times, and I picked three results based on similar
responseStart values. This provides a fairer comparison by avoiding bias of
fluctuation from the network/server. The actual values ended up slightly
favouring the older code.
| -------------- | ---------------- | ---------------- | -------- |
| | Before | After | Avg diff |
| -------------- | ---------------- | ---------------- | -------- |
| responseStart | 1044, 1001, 1016 | 1025, 1023, 1024 | +3ms |
| domInteractive | 2080, 2069, 2059 | 1872, 2101, 2050 | -61ms |
| domComplete | 4361, 4239, 3927 | 3691, 4023, 3981 | -227ms |
| loadEventEnd | 4366, 4244, 3932 | 3691, 4023, 3982 | -282ms |
| mwLoadEnd | 4524, 4416, 4113 | 3994, 4320, 4297 | -147ms |
| -------------- | ---------------- | ---------------- | -------- |
== Implementation
While technically a single logical change, this commit does
resolve multiple long-standing issues and inefficiencies.
* handlePending (now called doPropagation) was called way more
often than needed. When a load.php response arrived with calls
to mw.loader.implement(), each one could execute and immediately
call handlePending().
Now, the first implementation in a batch schedule one call
doPropagation(), and the later ones ride along that one call.
* Most calls to handlePending were only able to execute one
pending module, which in turn created its own handlePending
call that started all over again. This meant that by the time
control returned to an outer call, there was nothing left to
do, except it still needed to continue its iteration over the
whole registry before knowing there was nothing left to dos.
* Due to recursive calls to handlePending() from execute(), and
due to immediate execution from implement() - as called from
load.php or asyncEval - the stack was often already 100s of
level deep before *starting* the execution of a module.
Such deep stacks caused:
- a larger memory footprint (for the stacks themselves).
- confusing flame graphs. It was impossible to analyze
performance of module initialisation, I typically could only
look at code from dom-ready handlers or other events.
The stacks were so big, they actually caused rendering
bugs inside Chrome where higher parts of the stack would be
trimmed, and thus related code would no longer be visually
grouped by the same parent.
- confusing error messages (long stack traces).
* The eager execution from mw.loader.implement() calls meant
that it was not possible to separate the parsing/loading of
code (by load.php and asyncEval), from the execution of code.
Now, this is separated by a 1ms yield (in practice it's
larger than 1ms, but it has high priority). This means that the
batch of localStorage eval() and the batch response from
load.php can now first do one type of work (allocating of functions
and objects, stashing them in the registry), and then later the
other type of work (execution of the module code) - with some
breathing room allowed for rendering.
Bug: T127328
Bug: T202703
Change-Id: I499ae3f095545abcc03e8989f54422b1997738d3