Event Loop
Everyone should understand the Event Loop mechanism. Let me repeat the summary first.
The Event Loop of Node.js is different from that of Javascript. Intuitively, there are two more APIs, setImmediate
and process.nextTick
. Secondly, due to the different runtimes, Html Standrad will consider that different sources such as multiple pages and DOM operations will have different task queues. Not so much to consider in the Node.js Event Loop.
According to my understanding, the two sides are conceptually consistent and can be summarized like this (or see here):
- task queue task queue. Some events, etc. will be defined as tasks, which are often called MacroTask (macro task) to correspond to MicroTask. Each time, the task at the head of the queue will be obtained for execution.
- microtask queue Microtask queue. There will be a microtask queue, and the microtask queue will generally be cleared within a Task.
- So back and forth.
Performance measurement
After the above understanding, there is a simple way to measure performance: how many Event Loop cycles are completed per second, or how many MacroTasks are executed, so that we can roughly know the execution of synchronous code in the code .
Test function
class MacroTaskChecker {<!-- --> constructor(macroTaskDispatcher, count = 1000, cb = () => {<!-- --> }) {<!-- --> this.macroTaskDispatcher = macroTaskDispatcher this.COUNT = count this.cb = cb } start(cb) {<!-- --> this.cb = cb || this.cb this.stop = false const scope = () => {<!-- --> let count = this. COUNT const startTime = performance.now() const fn = () => {<!-- --> count-- if (count > 0) this. macroTaskDispatcher(fn) else {<!-- --> const endTime = performance. now() // After executing COUNT macro tasks, calculate the average number of macro tasks executed per second this.cb({<!-- --> avg: this. COUNT / (endTime - startTime) * 1000, timestamp: endTime }) !this.stop & amp; & amp; this.macroTaskDispatcher(scope) } } this. macroTaskDispatcher(fn) } scope() } stop() {<!-- --> this.stop = true } }
Afterwards, some infinite loops are executed to test whether intensive synchronous code execution can be detected.
function meaninglessRun(time) {<!-- --> console.time('meaninglessRun') for (let i = time; i--; i > 0) {<!-- --> // do nothing } console.timeEnd('meaninglessRun') } setTimeout(() => {<!-- --> meaninglessRun(1000 * 1000 * 1000) }, 1000 * 5) setTimeout(() => {<!-- --> checker. stop() console.log('stop') }, 1000 * 20)
setTimeout
const checker = new MacroTaskChecker(setTimeout, 100) checker.start(v => console.log(`time: ${<!-- -->v.timestamp.toFixed(2)} avg: ${<!-- -->v.avg.toFixed(2 )}`))
It can be clearly seen from the output that avg drops when the synchronization is blocked. However, there will be a significant gap between testing on browser and node.js.
// node.js time: 4837.47 avg: 825.14 time: 4958.18 avg: 829.83 meaninglessRun: 918.626ms time: 6001.69 avg: 95.95 time: 6125.72 avg: 817.18 time: 6285.07 avg: 635.16 // browser time: 153529.90 avg: 205.21 time: 154023.40 avg: 204.46 meaninglessRun: 924.463ms time: 155424.00 avg: 71.62 time: 155908.80 avg: 208.29 time: 156383.70 avg: 213.04
Although our purpose is achieved, using setTimeout is not completely able to accurately record every task. According to HTML Standrad and MDN, setTimeout waits at least 4ms. From this perspective, browser avg * 4ms ≈\approx 1000ms. And node.js probably did not follow the agreement on the browser side, but it did not execute to record every loop.
setImmediate
If using setImmediate
of node.js:
const checker = new MacroTaskChecker(setImmediate, 1000 * 10)
It can be seen that the number of executions is about an order of magnitude higher than that of Node.js setTimeout
:
time: 4839.71 avg: 59271.54 time: 5032.99 avg: 51778.84 meaninglessRun: 922.182ms time: 6122.44 avg: 9179.95 time: 6338.32 avg: 46351.38 time: 6536.66 avg: 50459.77
According to the explanation in the Node.js documentation, setImmediate
will be executed in the check phase of each loop (phase). Using setImmediate
should be able to accurately record every Loop. The number of cycles on my machine is probably between 40000 and 60000.
window. postMessage
Since there is no setImmediate
on the browser, we can use window.postMessage
to implement one according to the guidelines on MDN.
If you want to implement a 0ms delay timer in the browser, you can refer to the ?
window.postMessage()
mentioned here
const fns = [] window.addEventListener("message", () => {<!-- --> const currentFns = [...fns] fns.length = 0 currentFns. forEach(fn => fn()) }, true); function messageChannelMacroTaskDispatcher(fn) {<!-- --> fns. push(fn) window. postMessage(1) }
It can be seen that the magnitude of node.js setImmediate
is consistent.
time: 78769.70 avg: 51759.83 time: 78975.60 avg: 48614.49 meaninglessRun: 921.143 ms time: 80111.50 avg: 8805.14 time: 80327.00 avg: 46425.26 time: 80539.10 avg: 47169.81
MessageChannel
browser
In theory, it should be possible for the browser to use MessageChannel
, and avoid invalid messages being received by other window.addEventListener("message", handler)
:
const {<!-- --> port1, port2 } = new MessageChannel(); const fns = [] port1.onmessage = () => {<!-- --> const currentFns = [...fns] fns.length = 0 currentFns. forEach(fn => fn()) }; function messageChannelMacroTaskDispatcher(fn) {<!-- --> fns. push(fn) port2. postMessage(1) }
I don’t quite understand why it is more frequent than window.postMessage
. If you start two checkers at the same time, you can see that the logs appear in pairs, which means that everyone in a loop executes it only once. My guess is that the implementation of window.postMessage
consumes more.
time: 54974.80 avg: 68823.12 time: 55121.00 avg: 68493.15 meaninglessRun: 925.160888671875 ms time: 56204.60 avg: 9229.35 time: 56353.00 avg: 67430.88 time: 56503.10 avg: 66666.67 // Execute together wp=window.postMessage mc=MessageChannel wp time: 43307.90 avg: 25169.90 mc time: 43678.40 avg: 27005.13 wp time: 43678.60 avg: 26990.55 mc time: 44065.80 avg: 25833.12 wp time: 44066.00 avg: 25819.78 mc time: 44458.40 avg: 25484.20
node
There is also MessageChannel on node.js, can it also be used to measure the number of loops?
mc time: 460.99 avg: 353930.80 mc time: 489.52 avg: 355088.11 mc time: 520.30 avg: 326384.64 mc time: 551.78 avg: 320427.29
The magnitude is very abnormal. In theory, it should not exceed setImmediate
. If the checkers of setImmediate
and setTimeout
are enabled at the same time:
... (messagechannel) time: 1231.10 avg: 355569.31 (messagechannel) time: 1260.14 avg: 345825.77 (setImmediate) time: 1269.95 avg: 339.27 (setTimeout) time: 1270.09 avg: 339.13 (messagechannel) time: 1293.80 avg: 298141.74 (messagechannel) time: 1322.50 avg: 349939.04 ...
Obviously, it is not a macro task. I guess MessageChannel is classified as the same level as socket in node.js, that is, tasks after exceeding the threshold will move to the next loop.
Summary
It is quite interesting to use this method to test performance, but this indicator feels too unstable when used officially (even if nothing is done, there will be 20%-30% vibration). It is recommended to combine it with other serious methods (such as performance, etc.).
At the same time, this method is very likely to affect the normal Event Loop. For example, there will be a pull stage in Node.js. After all microtasks are executed, if there is no timer, it will stay in this stage , ready to execute the next microtask that occurs immediately.
By the way, I reviewed the Event Loop. Unexpectedly, there is such a big gap between MessageChannel on both sides.