Skip to content

Commit da7eb67

Browse files
kkaefermourner
authored andcommitted
queso backport: Use MessageChannel instead of setTimeout to avoid processing delays (#8677)
* use MessageChannel instead of setTimeout to avoid processing delays * move invocation throttling to ThrottledInvoker class also adds a fallback for environments that don't support MessageChannel
1 parent 4eb7d4e commit da7eb67

File tree

3 files changed

+119
-9
lines changed

3 files changed

+119
-9
lines changed

debug/animate.html

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<title>Mapbox GL JS debug page</title>
5+
<meta charset='utf-8'>
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
7+
<link rel='stylesheet' href='../dist/mapbox-gl.css' />
8+
<style>
9+
#map { width: 764px; height: 400px; }
10+
</style>
11+
</head>
12+
13+
<body>
14+
<div id='map'></div>
15+
16+
<script src='../dist/mapbox-gl-dev.js'></script>
17+
<script src='access_token_generated.js'></script>
18+
<script>
19+
20+
var map = window.map = new mapboxgl.Map({
21+
container: 'map',
22+
style: 'mapbox://styles/mapbox/streets-v11',
23+
center: [0, 0],
24+
zoom: 2
25+
});
26+
27+
var radius = 20;
28+
29+
function pointOnCircle(angle) {
30+
return {
31+
"type": "Point",
32+
"coordinates": [
33+
Math.cos(angle) * radius,
34+
Math.sin(angle) * radius
35+
]
36+
};
37+
}
38+
39+
map.on('load', function () {
40+
// Add a source and layer displaying a point which will be animated in a circle.
41+
map.addSource('point', {
42+
"type": "geojson",
43+
"data": pointOnCircle(0)
44+
});
45+
46+
map.addLayer({
47+
"id": "point",
48+
"source": "point",
49+
"type": "circle",
50+
"paint": {
51+
"circle-radius": 10,
52+
"circle-color": "#007cbf"
53+
}
54+
});
55+
56+
function animateMarker(timestamp) {
57+
// Update the data to a new position based on the animation timestamp. The
58+
// divisor in the expression `timestamp / 1000` controls the animation speed.
59+
map.getSource('point').setData(pointOnCircle(timestamp / 1000));
60+
61+
// Request the next frame of the animation.
62+
requestAnimationFrame(animateMarker);
63+
}
64+
65+
// Start the animation.
66+
animateMarker(0);
67+
});
68+
</script>
69+
70+
</body>
71+
</html>

src/util/actor.js

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import { bindAll } from './util';
44
import { serialize, deserialize } from './web_worker_transfer';
5+
import ThrottledInvoker from './throttled_invoker';
56

67
import type {Transferable} from '../types/transferable';
78
import type {Cancelable} from '../types/cancelable';
@@ -26,7 +27,7 @@ class Actor {
2627
tasks: { number: any };
2728
taskQueue: Array<number>;
2829
cancelCallbacks: { number: Cancelable };
29-
taskTimeout: ?TimeoutID;
30+
invoker: ThrottledInvoker;
3031

3132
static taskId: number;
3233

@@ -37,9 +38,9 @@ class Actor {
3738
this.callbacks = {};
3839
this.tasks = {};
3940
this.taskQueue = [];
40-
this.taskTimeout = null;
4141
this.cancelCallbacks = {};
4242
bindAll(['receive', 'process'], this);
43+
this.invoker = new ThrottledInvoker(this.process);
4344
this.target.addEventListener('message', this.receive, false);
4445
}
4546

@@ -108,18 +109,15 @@ class Actor {
108109
// is necessary because we want to keep receiving messages, and in particular,
109110
// <cancel> messages. Some tasks may take a while in the worker thread, so before
110111
// executing the next task in our queue, postMessage preempts this and <cancel>
111-
// messages can be processed.
112+
// messages can be processed. We're using a MessageChannel object to get throttle the
113+
// process() flow to one at a time.
112114
this.tasks[id] = data;
113115
this.taskQueue.push(id);
114-
if (!this.taskTimeout) {
115-
this.taskTimeout = setTimeout(this.process, 0);
116-
}
116+
this.invoker.trigger();
117117
}
118118
}
119119

120120
process() {
121-
// Reset the timeout ID so that we know that no process call is scheduled in the future yet.
122-
this.taskTimeout = null;
123121
if (!this.taskQueue.length) {
124122
return;
125123
}
@@ -130,7 +128,7 @@ class Actor {
130128
// current task. This is necessary so that processing continues even if the current task
131129
// doesn't execute successfully.
132130
if (this.taskQueue.length) {
133-
this.taskTimeout = setTimeout(this.process, 0);
131+
this.invoker.trigger();
134132
}
135133
if (!task) {
136134
// If the task ID doesn't have associated task data anymore, it was canceled.

src/util/throttled_invoker.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// @flow
2+
3+
/**
4+
* Invokes the wrapped function in a non-blocking way when trigger() is called. Invocation requests
5+
* are ignored until the function was actually invoked.
6+
*
7+
* @private
8+
*/
9+
class ThrottledInvoker {
10+
_channel: MessageChannel;
11+
_triggered: boolean;
12+
_callback: Function
13+
14+
constructor(callback: Function) {
15+
this._callback = callback;
16+
this._triggered = false;
17+
if (typeof MessageChannel !== 'undefined') {
18+
this._channel = new MessageChannel();
19+
this._channel.port2.onmessage = () => {
20+
this._triggered = false;
21+
this._callback();
22+
};
23+
}
24+
}
25+
26+
trigger() {
27+
if (!this._triggered) {
28+
this._triggered = true;
29+
if (this._channel) {
30+
this._channel.port1.postMessage(true);
31+
} else {
32+
setTimeout(() => {
33+
this._triggered = false;
34+
this._callback();
35+
}, 0);
36+
}
37+
}
38+
}
39+
}
40+
41+
export default ThrottledInvoker;

0 commit comments

Comments
 (0)