Skip to content

Commit 67ffb1c

Browse files
author
Aleks Totic
committedJun 17, 2016
Test rig complete
1 parent 8594631 commit 67ffb1c

File tree

4 files changed

+326
-84
lines changed

4 files changed

+326
-84
lines changed
 

‎README.md

+76-84
Original file line numberDiff line numberDiff line change
@@ -35,24 +35,15 @@ The event loop is the mastermind that orchestrates:
3535

3636
* paint: when do DOM changes get rendered
3737

38-
It is formally specified in whatwg's [HTML standard](https://html.spec.whatwg.org/#event-loops).
38+
It is formally specified in whatwg's [HTML standard](https://html.spec.whatwg.org/multipage/webappapis.html#event-loops).
3939

4040
The current specification is incomplete. The event loop behavior differs between browsers. Further work is being done to clarify the issues:
4141

4242
* [Rendering Processing Model](https://docs.google.com/document/d/1Mw6qNw8UAEfW96CXaXRVYPPZjqQS3YdK7v57wFttAhs/edit?pref=2&pli=1#)
4343

44-
This document is a developer-oriented description of what event loop does. It tries to be readable, and might skip over edge cases for clarity.
45-
46-
## Why is the event loop interesting?
47-
48-
Event loop is in charge.
49-
All the application code is scheduled for execution by the event loop.
50-
51-
Event loop is interesing because it influences performance and correctness of your app.
52-
53-
If your event loop takes more than 16ms, animations might skip a frame. If your code is split into multiple event handlers, Promise callbacks, timeouts, observers, in what order will the code execute? Do you really need to `setTimeout(_=>{doIt();}, 1)` because things were not executing in the right order?
44+
* [Discussion on event order](https://github.com/w3c/pointerevents/issues/9)
5445

55-
Understanding the event loop will help you understand when your code gets executed, and its performance implications.
46+
This document is a developer-oriented description of what event loop does. It tries to be readable, and might skip over edge cases for clarity.
5647

5748
## Event loop description
5849

@@ -68,21 +59,33 @@ This is what the spec says:
6859
},
6960

7061
microtaskQueue: [
71-
// microtasks are:
72-
// - completed promises
7362
],
7463

64+
nextTask: function() {
65+
// Spec says:
66+
// "Select the oldest task on one of the event loop's task queues"
67+
// Which gives browser implementers freedom to implement
68+
// task queues
69+
for (let q of taskQueues)
70+
if (q.length > 0)
71+
return q.shift();
72+
return null;
73+
},
74+
7575
executeMicrotasks: function() {
76+
if (scriptExecuting)
77+
return;
7678
let microtasks = this.microtaskQueue;
7779
this.microtaskQueue = [];
7880
for (let t of microtasks)
7981
t.execute();
8082
},
81-
needsPaint: function() {
82-
return vSyncTime() && needsDomRepaint();
83+
84+
needsRendering: function() {
85+
return vSyncTime() && needsDomRerender();
8386
},
8487

85-
paintPipeline: function() {
88+
render: function() {
8689
resizeSteps();
8790
scrollSteps();
8891
mediaQuerySteps();
@@ -93,8 +96,10 @@ This is what the spec says:
9396

9497
intersectionObserverSteps();
9598

96-
updateStyle();
97-
updateLayout();
99+
while (resizeObserverSteps()) {
100+
updateStyle();
101+
updateLayout();
102+
}
98103
paint();
99104
}
100105
}
@@ -105,62 +110,11 @@ This is what the spec says:
105110
task.execute();
106111
}
107112
eventLoop.executeMicrotasks();
108-
if (eventLoop.needsPaint())
109-
eventLoop.paintPipeline();
110-
}
111-
112-
This is what really happens in Chrome:
113-
114-
eventLoop = {
115-
taskQueues: {
116-
events: [], // UI events from native GUI framework
117-
parser: [], // HTML parser
118-
callbacks: [], // setTimeout, requestIdleTask
119-
resources: [], // image loading
120-
domManipulation[]
121-
},
122-
123-
microtasks: [
124-
// microtasks are:
125-
// - completed promises
126-
],
127-
128-
executeMicrotasks: function() {
129-
let microtasks = this.microtasks;
130-
this.microtasks = [];
131-
for (let t of microtasks)
132-
t.execute();
133-
},
134-
needsPaint: function() {
135-
return vSyncTime() && needsDomRepaint();
136-
},
137-
138-
paintPipeline: function() {
139-
mediaQuerySteps();
140-
resizeSteps();
141-
scrollSteps();
142-
cssAnimationSteps();
143-
fullscreenRenderingSteps();
144-
145-
animationFrameCallbackSteps();
146-
147-
intersectionObserverSteps();
148-
149-
updateStyle();
150-
updateLayout();
151-
paint();
152-
}
113+
if (eventLoop.needsRendering())
114+
eventLoop.render();
153115
}
154116

155-
while(true) {
156-
task = eventLoop.nextTask();
157-
if (task) {
158-
task.execute();
159-
}
160-
eventLoop.executeMicrotasks();
161-
if (eventLoop.needsPaint())
162-
eventLoop.paintPipeline();
163-
}
117+
## Chrome implementation of the event loop
164118

165119
### Events
166120

@@ -184,31 +138,40 @@ Also manages requestAnimationFrame requests
184138

185139
sample events: Animation.finish, Animation.cancel, CSSAnimation.animationstart, CSSAnimation.animationiteration(CSSAnimation)
186140

187-
#### 3. ad-hoc dispatch
141+
#### 3. Custom dispatch
188142

189-
Triggers vary: mouse events, keyboard events, timers.
143+
Triggers vary: OS events, timers, document/element lifecycle events.
190144

191-
Many events get dispatched immediately, without pausing in a queue.
145+
Custom dispatch event do not pass through queues, they are fired
146+
directly.
147+
148+
There are no globally applicable delivery guarantees for custom
149+
events. Specific events might have event-specific guarantees
150+
about ordering.
192151

193152
#### 4. Microtask queue
194153

195154
Triggered most often by EndOfTaskRunner.didProcessTask().
196-
Tasks are run by TaskQueueManager.
197-
What are tasks? They are used internally by other dispatchers to schedule
198-
execution.
199155

156+
Tasks are run by TaskQueueManager. They are used internally by other dispatchers to schedule execution.
157+
158+
Microtask queue executes whenever task completes.
159+
160+
sample events: Image.onerror, Image.onload
161+
162+
Microtasks also contain Promise callbacks
200163

201164
### [Timers](https://developer.mozilla.org/en-US/Add-ons/Code_snippets/Timers)
202165

203166
#### [requestIdleCallback](https://w3c.github.io/requestidlecallback/#the-requestidlecallback-methodsetTimeout)
204167

205168
Triggered by internal timer when browser is idle.
206169

207-
#### requestAnimationFrame
170+
#### [requestAnimationFrame](https://html.spec.whatwg.org/multipage/webappapis.html#animation-frames)
208171

209172
Triggered by ScriptedAnimationController, that also handles events.
210173

211-
#### setTimeout, setInterval
174+
#### [Timers](https://html.spec.whatwg.org/multipage/webappapis.html#timers:dom-setinterval): setTimeout, setInterval
212175

213176
Triggered by WebTaskRunner, which runs on TaskQueue primitive.
214177

@@ -222,24 +185,53 @@ There are two ways of watching for changes:
222185

223186
2. Poll: poll for changes when it is time to broadcast.
224187

225-
#### MutationObserver
188+
#### [MutationObserver](https://dom.spec.whatwg.org/#mutation-observers)
226189

227190
Push-based.
228191

229192
Observations broadcast is placed on microtask queue.
230193

231-
#### IntersectionObserver
194+
#### [IntersectionObserver](http://rawgit.com/WICG/IntersectionObserver/master/index.html)
232195

233196
Poll-based.
234197

235-
Observations broadcast via timeout.
198+
Observations poll on layout, broadcast via 100ms timeout.
236199

237200
### Promises
238201

239202
Completed promises run callbacks after completion.
240203

241204
Callbacks are placed on the microtask queue.
242205

206+
## Developer takeaways
207+
208+
Now we understand the spec, and how Chrome implements it.
209+
210+
Event loop does not say much about when events are dispatched:
211+
212+
1. Events on the same queue are dispatched in order.
213+
214+
2. Events can be dispatched directly, bypassing the event loop task queues.
215+
216+
3. Microtasks get executed immediately after a task.
217+
218+
4. Render part of the loop gets executed on vSync, and delivers events in the following order:
219+
220+
1. 'resize' event
221+
222+
2. 'scroll' event
223+
224+
3. mediaquery listeners
225+
226+
4. 'CSSAnimation' events
227+
228+
5. Observers
229+
230+
6. rAF
231+
232+
## What really happens
233+
234+
We've build a test rig, 'shell.html' that
243235
## Multiple event loops and their interaction
244236

245237
## Examples

‎shell.css

+46
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
button {
2+
font-size: xx-large;
3+
4+
}
5+
.result {
6+
margin-top: 8px;
7+
padding: 8px;
8+
color: green;
9+
border-radius: 5px;
10+
border: 1px solid green;
11+
font-size: initial;
12+
}
13+
.result.error {
14+
color: red;
15+
background-color: rgba(255,0,0,0.2);
16+
border: 1px solid red;
17+
}
18+
.result.warn {
19+
color: #424218;
20+
background-color: rgba(255,255, 0, 0.2);
21+
border: 1px solid yellow;
22+
}
23+
iframe {
24+
margin-top: 16px;
25+
display:block;
26+
}
27+
28+
.animate {
29+
animation-duration: 0.2s;
30+
animation-name: slidein;
31+
}
32+
@keyframes slidein {
33+
from {
34+
margin-left: 100%;
35+
width: 100%;
36+
}
37+
to {
38+
margin-left: 0%;
39+
width: 100%;
40+
}
41+
}
42+
#slider {
43+
background-color: blue;
44+
width: 50px;
45+
height: 1px;
46+
}

‎shell.html

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
<link rel="stylesheet" type="text/css" href="shell.css">
6+
<title>event-loop test</title>
7+
</head>
8+
<body>
9+
<button id="testiframeButton" onclick="startTestInIFrame()" style="font-size:xx-large">Start test in iFrame</button>
10+
<script>
11+
'use strict';
12+
13+
14+
15+
function startTestInIFrame() {
16+
for (let el of document.querySelectorAll('iframe'))
17+
el.parentNode.removeChild(el);
18+
var iframe = document.createElement('iframe');
19+
iframe.setAttribute('src', 'test.html');
20+
iframe.width = '300px';
21+
iframe.height = '300px';
22+
document.body.appendChild(iframe);
23+
24+
window.setTimeout(_ => {
25+
// debugger;
26+
iframe.contentWindow.postMessage('startTest', '*');
27+
// iframe.width = "450px";
28+
// window.requestAnimationFrame(_ => {
29+
// iframe.width = "451px";
30+
// });
31+
}, 300);
32+
}
33+
34+
</script>
35+
</body>
36+
</html>

‎test.html

+168
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
<!doctype html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
<link rel="stylesheet" type="text/css" href="shell.css">
6+
<title>event-loop test</title>
7+
</head>
8+
<body style="height: 2000px;">
9+
10+
<button id="testButton" onclick="startTest()" style="font-size:xx-large">Redo test</script>
11+
<div style="overflow:hidden">
12+
<div id="slider"></div>
13+
</div>
14+
<script>
15+
'use strict';
16+
17+
var RESULTS = [];
18+
19+
function addResultToDocument(msg, type) {
20+
let el = document.createElement('div');
21+
if (type)
22+
el.classList.add(type);
23+
el.classList.add('result');
24+
el.innerText = msg;
25+
document.body.appendChild(el);
26+
}
27+
28+
function reportError(msg) {
29+
addResultToDocument(msg, 'error');
30+
}
31+
32+
function log(...args) {
33+
let line = args.join(' ');
34+
console.log(Math.round(performance.now()*100)/100 + ' ' + line);
35+
RESULTS.push(line);
36+
}
37+
38+
function assertOrder(r1, r2, msg) {
39+
var i1 = RESULTS.indexOf(r1);
40+
var i2 = RESULTS.indexOf(r2);
41+
if (i1 == -1)
42+
reportError(r1 + " not found");
43+
if (i2 == -1)
44+
reportError(r2 + " not found");
45+
if (i2 <= i1)
46+
reportError('"' + r1 + '" before "' + r2 + '": ' + msg);
47+
}
48+
49+
function endTest() {
50+
// Promise/rAF asserts
51+
assertOrder("script end", "promise 0", "promise handlers executed after script");
52+
assertOrder("promise 0", "promise 1", "promise handlers in order");
53+
54+
assertOrder("rAF 0", "rAF 1", "rAF in order");
55+
56+
assertOrder("promise 0", "rAF 0", "rAF after promise handlers");
57+
58+
assertOrder("promise 0", "timeout 0", "timeout after promise handlers");
59+
assertOrder("rAF 0 promise", "rAF 1", "promise handler immediately after rAF");
60+
61+
let p1 = RESULTS.indexOf('promise 1'), p0 = RESULTS.indexOf('promise 0');
62+
if ((p1 - p0) != 1)
63+
reportError("Microtasks queue not executed all at once, distance is " + (p1 - p0) );
64+
65+
// render events
66+
assertOrder("matchMedia", "resize", "matchMedia before resize");
67+
assertOrder("resize", "scroll", "resize before scroll");
68+
assertOrder("scroll", "rAF 0", "scroll before rAF");
69+
assertOrder("animationstart", "rAF 0", "css animation starts before rAF");
70+
71+
// addResultToDocument("rAF and timeout order is unspecified", "warn");
72+
if (!document.querySelector('.error'))
73+
addResultToDocument('tests passed');
74+
75+
document.querySelector('#slider').classList.remove('animate');
76+
}
77+
78+
79+
function startTest() {
80+
RESULTS = [];
81+
log("script start");
82+
for (let el of document.querySelectorAll('.result'))
83+
el.parentNode.removeChild(el);
84+
85+
// rAF
86+
87+
window.requestAnimationFrame( _ => {
88+
log("rAF 0");
89+
new Promise( (fulfill, reject) => {
90+
fulfill();
91+
})
92+
.then(_ => {
93+
log("rAF 0 promise");
94+
});
95+
});
96+
97+
window.requestAnimationFrame( _ => {
98+
log("rAF 1");
99+
});
100+
101+
// timeouts
102+
103+
window.setTimeout(_ => { log('timeout 0');}, 0);
104+
105+
window.setTimeout(_ => { log('timeout 1');}, 0);
106+
107+
// Promises
108+
let p = new Promise( (fulfill, reject) => {
109+
fulfill();
110+
// log("promise 0 fulfill");
111+
})
112+
.then(_ => {
113+
log("promise 0");
114+
return new Promise( fulfill => {
115+
fulfill();
116+
});
117+
})
118+
.then(_ => {
119+
log("promise 1");
120+
});
121+
122+
// CSS Animation
123+
document.querySelector('#slider').classList.remove('animate');
124+
document.querySelector('#slider').classList.add('animate');
125+
126+
// Scrolling
127+
128+
window.scrollBy(0,10);
129+
130+
// Resize
131+
window.parent.document.querySelector('iframe').width = "450px";
132+
133+
134+
// Test end
135+
window.setTimeout(_ => { endTest(); }, 500);
136+
137+
log("script end");
138+
139+
}
140+
141+
// iframe setup
142+
window.addEventListener('resize', _ => {
143+
log('resize');
144+
});
145+
window.addEventListener('scroll', _ => {
146+
log('scroll');
147+
});
148+
window.matchMedia("(min-width: 400px)").addListener( _ => {
149+
log('matchMedia');
150+
});
151+
document.querySelector('#slider').addEventListener('animationstart', _=> {
152+
log('animationstart');
153+
});
154+
155+
window.addEventListener('message', ev => {
156+
console.log('message', ev.data);
157+
switch(ev.data) {
158+
case 'startTest':
159+
window.requestAnimationFrame(startTest);
160+
break;
161+
default:
162+
break;
163+
}
164+
});
165+
166+
</script>
167+
</body>
168+
</html>

0 commit comments

Comments
 (0)
Please sign in to comment.