resourceloader: Optimise and simplify state propagation logic
authorTimo Tijhof <krinklemail@gmail.com>
Sat, 8 Sep 2018 20:04:17 +0000 (21:04 +0100)
committerRoan Kattouw <roan.kattouw@gmail.com>
Thu, 13 Sep 2018 23:19:58 +0000 (16:19 -0700)
commit6a83163b1cbad14e988ec0fbc7aede50290ae13b
tree0efbfbba158d873962b3e36deb6f271730bf045d
parent345618340dcab03d517a99bf13cbd0126a149957
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
resources/src/startup/mediawiki.js