Skip to content

Commit

Permalink
⚡️ Remove artificial 4ms nextTick() delay when running in a browser
Browse files Browse the repository at this point in the history
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
  • Loading branch information
alecgibson committed Mar 28, 2024
1 parent 5259d0e commit 57e0045
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 2 deletions.
2 changes: 2 additions & 0 deletions .mocharc.yml
Expand Up @@ -2,3 +2,5 @@ reporter: spec
check-leaks: true
recursive: true
file: test/setup.js
globals:
- MessageChannel # Set/unset to test nextTick()
15 changes: 13 additions & 2 deletions lib/util.js
Expand Up @@ -95,9 +95,20 @@ exports.nextTick = function(callback) {
args[i - 1] = arguments[i];
}

setTimeout(function() {
if (typeof MessageChannel === 'undefined') {
return setTimeout(triggerCallback);
}

var channel = new MessageChannel();
channel.port1.onmessage = function() {
triggerCallback();
channel.port1.close();
};
channel.port2.postMessage('');

function triggerCallback() {
callback.apply(null, args);
});
}
};

exports.clone = function(obj) {
Expand Down
33 changes: 33 additions & 0 deletions test/util-test.js
Expand Up @@ -45,6 +45,39 @@ describe('util', function() {
});
expect(called).to.be.false;
});

describe('without MessageChannel', function() {
var _MessageChannel;

before(function() {
_MessageChannel = global.MessageChannel;
delete global.MessageChannel;
});

after(function() {
global.MessageChannel = _MessageChannel;
});

it('uses a different ponyfill', function(done) {
expect(process.nextTick).to.be.undefined;

util.nextTick(function(arg1, arg2, arg3) {
expect(arg1).to.equal('foo');
expect(arg2).to.equal(123);
expect(arg3).to.be.undefined;
done();
}, 'foo', 123);
});

it('calls asynchronously', function(done) {
var called = false;
util.nextTick(function() {
called = true;
done();
});
expect(called).to.be.false;
});
});
});
});
});

0 comments on commit 57e0045

Please sign in to comment.