Skip to content

Commit 57e0045

Browse files
committed
⚡️ Remove artificial 4ms nextTick() delay when running in a browser
Fixes #646 ShareDB makes heavy use of [`nextTick()`][1]. It notably calls it every time we [send a message over a `StreamSocket`][2]. If ShareDB is running both `Backend` and `Client` in a browser (eg in client tests), `nextTick()` will [fall back to `setTimeout()`][3]. However, according to the [HTML standard][4]: > Timers can be nested; after five such nested timers, however, the > interval is forced to be at least four milliseconds. So using `setTimeout()` can incur a penalty of 4ms in the browser, just idling. Over the course of a test suite, which makes a lot of fast ShareDB requests in series, these delays can add up to many seconds or even minutes of idle CPU time. This change adds an alternative `nextTick()` implementation, using `MessageChannel`, which is present in both [Node.js][5] and [HTML][6] (with slightly different APIs, but close enough for our purposes). This class offers a way of waiting for a tick on the event loop, without the arbitrary 4ms delay. `MessageChannel` is [supported back to even IE10][7], and since Node.js v10.5.0, but if for some reason it's missing, we'll still fall back to `setTimeout()`. [1]: https://github.com/share/sharedb/blob/5259d0e0b66c50ff9e745fe34a55c5fb16c57a8e/lib/util.js#L88 [2]: https://github.com/share/sharedb/blob/5259d0e0b66c50ff9e745fe34a55c5fb16c57a8e/lib/stream-socket.js#L58 [3]: https://github.com/share/sharedb/blob/5259d0e0b66c50ff9e745fe34a55c5fb16c57a8e/lib/util.js#L98 [4]: https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#timers [5]: https://nodejs.org/api/worker_threads.html#class-messagechannel [6]: https://html.spec.whatwg.org/multipage/web-messaging.html#message-channels [7]: https://caniuse.com/mdn-api_messagechannel_messagechannel
1 parent 5259d0e commit 57e0045

File tree

3 files changed

+48
-2
lines changed

3 files changed

+48
-2
lines changed

.mocharc.yml

+2
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,5 @@ reporter: spec
22
check-leaks: true
33
recursive: true
44
file: test/setup.js
5+
globals:
6+
- MessageChannel # Set/unset to test nextTick()

lib/util.js

+13-2
Original file line numberDiff line numberDiff line change
@@ -95,9 +95,20 @@ exports.nextTick = function(callback) {
9595
args[i - 1] = arguments[i];
9696
}
9797

98-
setTimeout(function() {
98+
if (typeof MessageChannel === 'undefined') {
99+
return setTimeout(triggerCallback);
100+
}
101+
102+
var channel = new MessageChannel();
103+
channel.port1.onmessage = function() {
104+
triggerCallback();
105+
channel.port1.close();
106+
};
107+
channel.port2.postMessage('');
108+
109+
function triggerCallback() {
99110
callback.apply(null, args);
100-
});
111+
}
101112
};
102113

103114
exports.clone = function(obj) {

test/util-test.js

+33
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,39 @@ describe('util', function() {
4545
});
4646
expect(called).to.be.false;
4747
});
48+
49+
describe('without MessageChannel', function() {
50+
var _MessageChannel;
51+
52+
before(function() {
53+
_MessageChannel = global.MessageChannel;
54+
delete global.MessageChannel;
55+
});
56+
57+
after(function() {
58+
global.MessageChannel = _MessageChannel;
59+
});
60+
61+
it('uses a different ponyfill', function(done) {
62+
expect(process.nextTick).to.be.undefined;
63+
64+
util.nextTick(function(arg1, arg2, arg3) {
65+
expect(arg1).to.equal('foo');
66+
expect(arg2).to.equal(123);
67+
expect(arg3).to.be.undefined;
68+
done();
69+
}, 'foo', 123);
70+
});
71+
72+
it('calls asynchronously', function(done) {
73+
var called = false;
74+
util.nextTick(function() {
75+
called = true;
76+
done();
77+
});
78+
expect(called).to.be.false;
79+
});
80+
});
4881
});
4982
});
5083
});

0 commit comments

Comments
 (0)