clearStack optimizations

* Avoid setTimeout in Node, because we don't need the overhead there.
* Still call setTimeout in browsers to prevent the tab from being killed.
* Use queueMicrotask in Safari, because it's dramatically faster than
  MessageChannel there.
* Continue to use MessageChannel in other supported browsers becuase it's
  somewhat faster than queueMicrotask there.
* Don't use setImmediate any more because there's a faster alternative in
  all supported envs.

In jasmine-core's own test suite, this yields a roughly 50-70% speedup
in Node, ~20% in Edge, and 75-90%(!) in Safari.
This commit is contained in:
Steve Gravrock
2022-08-13 08:54:32 -07:00
parent e022e6199c
commit 79c6bbc189
4 changed files with 322 additions and 187 deletions

View File

@@ -2789,8 +2789,32 @@ getJasmineRequireObj().CallTracker = function(j$) {
getJasmineRequireObj().clearStack = function(j$) {
const maxInlineCallCount = 10;
function messageChannelImpl(global, setTimeout) {
const channel = new global.MessageChannel();
function browserQueueMicrotaskImpl(global) {
const { setTimeout, queueMicrotask } = global;
let currentCallCount = 0;
return function clearStack(fn) {
currentCallCount++;
if (currentCallCount < maxInlineCallCount) {
queueMicrotask(fn);
} else {
currentCallCount = 0;
setTimeout(fn);
}
};
}
function nodeQueueMicrotaskImpl(global) {
const { queueMicrotask } = global;
return function(fn) {
queueMicrotask(fn);
};
}
function messageChannelImpl(global) {
const { MessageChannel, setTimeout } = global;
const channel = new MessageChannel();
let head = {};
let tail = head;
@@ -2801,7 +2825,7 @@ getJasmineRequireObj().clearStack = function(j$) {
delete head.task;
if (taskRunning) {
global.setTimeout(task, 0);
setTimeout(task, 0);
} else {
try {
taskRunning = true;
@@ -2827,29 +2851,31 @@ getJasmineRequireObj().clearStack = function(j$) {
}
function getClearStack(global) {
let currentCallCount = 0;
const realSetTimeout = global.setTimeout;
const setTimeoutImpl = function clearStack(fn) {
Function.prototype.apply.apply(realSetTimeout, [global, [fn, 0]]);
};
const NODE_JS =
global.process &&
global.process.versions &&
typeof global.process.versions.node === 'string';
if (j$.isFunction_(global.setImmediate)) {
const realSetImmediate = global.setImmediate;
return function(fn) {
currentCallCount++;
const SAFARI =
global.navigator &&
/^((?!chrome|android).)*safari/i.test(global.navigator.userAgent);
if (currentCallCount < maxInlineCallCount) {
realSetImmediate(fn);
} else {
currentCallCount = 0;
setTimeoutImpl(fn);
}
};
} else if (!j$.util.isUndefined(global.MessageChannel)) {
return messageChannelImpl(global, setTimeoutImpl);
if (NODE_JS) {
// Unlike browsers, Node doesn't require us to do a periodic setTimeout
// so we avoid the overhead.
return nodeQueueMicrotaskImpl(global);
} else if (
SAFARI ||
j$.util.isUndefined(global.MessageChannel) /* tests */
) {
// queueMicrotask is dramatically faster than MessageChannel in Safari.
// Some of our own integration tests provide a mock queueMicrotask in all
// environments because it's simpler to mock than MessageChannel.
return browserQueueMicrotaskImpl(global);
} else {
return setTimeoutImpl;
// MessageChannel is faster than queueMicrotask in supported browsers
// other than Safari.
return messageChannelImpl(global);
}
}

View File

@@ -9,160 +9,216 @@ describe('ClearStack', function() {
});
});
it('uses setImmediate when available', function() {
const setImmediate = jasmine
.createSpy('setImmediate')
.and.callFake(function(fn) {
fn();
}),
global = { setImmediate: setImmediate },
clearStack = jasmineUnderTest.getClearStack(global);
let called = false;
describe('in Safari', function() {
usesQueueMicrotaskWithSetTimeout(function() {
return {
navigator: {
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.0.8 (KHTML, like Gecko) Version/15.1 Safari/605.0.8'
},
// queueMicrotask should be used even though MessageChannel is present
MessageChannel: fakeMessageChannel
};
});
});
clearStack(function() {
called = true;
describe('in browsers other than Safari', function() {
usesMessageChannel(function() {
return {
navigator: {
// Chrome's user agent string contains "Safari" so it's a good test case
userAgent:
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36'
}
};
});
expect(called).toBe(true);
expect(setImmediate).toHaveBeenCalled();
describe('when MessageChannel is unavailable', function() {
usesQueueMicrotaskWithSetTimeout(function() {
return {
navigator: {
userAgent: 'CERN-LineMode/2.15 libwww/2.17b3',
MessageChannel: undefined
}
};
});
});
});
it('uses setTimeout instead of setImmediate every 10 calls to make sure we release the CPU', function() {
const setImmediate = jasmine.createSpy('setImmediate'),
setTimeout = jasmine.createSpy('setTimeout'),
global = { setImmediate: setImmediate, setTimeout: setTimeout },
clearStack = jasmineUnderTest.getClearStack(global);
clearStack(function() {});
clearStack(function() {});
clearStack(function() {});
clearStack(function() {});
clearStack(function() {});
clearStack(function() {});
clearStack(function() {});
clearStack(function() {});
clearStack(function() {});
expect(setImmediate).toHaveBeenCalled();
expect(setTimeout).not.toHaveBeenCalled();
clearStack(function() {});
expect(setImmediate.calls.count()).toEqual(9);
expect(setTimeout.calls.count()).toEqual(1);
clearStack(function() {});
expect(setImmediate.calls.count()).toEqual(10);
expect(setTimeout.calls.count()).toEqual(1);
});
it('uses MessageChannels when available', function() {
const fakeChannel = {
port1: {},
port2: {
postMessage: function() {
fakeChannel.port1.onmessage();
describe('in Node', function() {
usesQueueMicrotaskWithoutSetTimeout(function() {
return {
process: {
versions: {
node: '3.1415927'
}
}
},
global = {
MessageChannel: function() {
return fakeChannel;
}
},
clearStack = jasmineUnderTest.getClearStack(global);
let called = false;
};
});
});
clearStack(function() {
called = true;
function usesMessageChannel(makeGlobal) {
it('uses MessageChannel', function() {
const global = {
...makeGlobal(),
MessageChannel: fakeMessageChannel
};
const clearStack = jasmineUnderTest.getClearStack(global);
let called = false;
clearStack(function() {
called = true;
});
expect(called).toBe(true);
});
expect(called).toBe(true);
});
it('uses setTimeout instead of MessageChannel every 10 calls to make sure we release the CPU', function() {
const fakeChannel = {
port1: {},
port2: {
postMessage: jasmine
.createSpy('postMessage')
.and.callFake(function() {
fakeChannel.port1.onmessage();
})
}
},
setTimeout = jasmine.createSpy('setTimeout'),
global = {
it('uses setTimeout instead of MessageChannel every 10 calls to make sure we release the CPU', function() {
const fakeChannel = fakeMessageChannel();
spyOn(fakeChannel.port2, 'postMessage');
const setTimeout = jasmine.createSpy('setTimeout');
const global = {
...makeGlobal(),
setTimeout,
MessageChannel: function() {
return fakeChannel;
},
setTimeout: setTimeout
},
clearStack = jasmineUnderTest.getClearStack(global);
clearStack(function() {});
clearStack(function() {});
clearStack(function() {});
clearStack(function() {});
clearStack(function() {});
clearStack(function() {});
clearStack(function() {});
clearStack(function() {});
clearStack(function() {});
expect(fakeChannel.port2.postMessage).toHaveBeenCalled();
expect(setTimeout).not.toHaveBeenCalled();
clearStack(function() {});
expect(fakeChannel.port2.postMessage.calls.count()).toEqual(9);
expect(setTimeout.calls.count()).toEqual(1);
clearStack(function() {});
expect(fakeChannel.port2.postMessage.calls.count()).toEqual(10);
expect(setTimeout.calls.count()).toEqual(1);
});
it('calls setTimeout when onmessage is called recursively', function() {
const fakeChannel = {
port1: {},
port2: {
postMessage: function() {
fakeChannel.port1.onmessage();
}
}
},
setTimeout = jasmine.createSpy('setTimeout'),
global = {
MessageChannel: function() {
return fakeChannel;
},
setTimeout: setTimeout
},
clearStack = jasmineUnderTest.getClearStack(global),
fn = jasmine.createSpy('second clearStack function');
};
const clearStack = jasmineUnderTest.getClearStack(global);
clearStack(function() {
clearStack(fn);
for (let i = 0; i < 9; i++) {
clearStack(function() {});
}
expect(fakeChannel.port2.postMessage).toHaveBeenCalled();
expect(setTimeout).not.toHaveBeenCalled();
clearStack(function() {});
expect(fakeChannel.port2.postMessage).toHaveBeenCalledTimes(9);
expect(setTimeout).toHaveBeenCalledTimes(1);
clearStack(function() {});
expect(fakeChannel.port2.postMessage).toHaveBeenCalledTimes(10);
expect(setTimeout).toHaveBeenCalledTimes(1);
});
expect(fn).not.toHaveBeenCalled();
expect(setTimeout).toHaveBeenCalledWith(fn, 0);
});
it('calls setTimeout when onmessage is called recursively', function() {
const setTimeout = jasmine.createSpy('setTimeout');
const global = {
...makeGlobal(),
setTimeout,
MessageChannel: fakeMessageChannel
};
const clearStack = jasmineUnderTest.getClearStack(global);
const fn = jasmine.createSpy('second clearStack function');
it('falls back to setTimeout', function() {
const setTimeout = jasmine
.createSpy('setTimeout')
.and.callFake(function(fn) {
clearStack(function() {
clearStack(fn);
});
expect(fn).not.toHaveBeenCalled();
expect(setTimeout).toHaveBeenCalledWith(fn, 0);
});
}
function usesQueueMicrotaskWithSetTimeout(makeGlobal) {
it('uses queueMicrotask', function() {
const global = {
...makeGlobal(),
queueMicrotask: function(fn) {
fn();
}),
global = { setTimeout: setTimeout },
clearStack = jasmineUnderTest.getClearStack(global);
let called = false;
}
};
const clearStack = jasmineUnderTest.getClearStack(global);
let called = false;
clearStack(function() {
called = true;
clearStack(function() {
called = true;
});
expect(called).toBe(true);
});
expect(called).toBe(true);
expect(setTimeout).toHaveBeenCalledWith(jasmine.any(Function), 0);
});
it('uses setTimeout instead of queueMicrotask every 10 calls to make sure we release the CPU', function() {
const queueMicrotask = jasmine.createSpy('queueMicrotask');
const setTimeout = jasmine.createSpy('setTimeout');
const global = {
...makeGlobal(),
queueMicrotask,
setTimeout
};
const clearStack = jasmineUnderTest.getClearStack(global);
for (let i = 0; i < 9; i++) {
clearStack(function() {});
}
expect(queueMicrotask).toHaveBeenCalled();
expect(setTimeout).not.toHaveBeenCalled();
clearStack(function() {});
expect(queueMicrotask).toHaveBeenCalledTimes(9);
expect(setTimeout).toHaveBeenCalledTimes(1);
clearStack(function() {});
expect(queueMicrotask).toHaveBeenCalledTimes(10);
expect(setTimeout).toHaveBeenCalledTimes(1);
});
}
function usesQueueMicrotaskWithoutSetTimeout(makeGlobal) {
it('uses queueMicrotask', function() {
const global = {
...makeGlobal(),
queueMicrotask: function(fn) {
fn();
}
};
const clearStack = jasmineUnderTest.getClearStack(global);
let called = false;
clearStack(function() {
called = true;
});
expect(called).toBe(true);
});
it('does not use setTimeout', function() {
const queueMicrotask = jasmine.createSpy('queueMicrotask');
const setTimeout = jasmine.createSpy('setTimeout');
const global = {
...makeGlobal(),
queueMicrotask,
setTimeout
};
const clearStack = jasmineUnderTest.getClearStack(global);
clearStack(function() {});
clearStack(function() {});
clearStack(function() {});
clearStack(function() {});
clearStack(function() {});
clearStack(function() {});
clearStack(function() {});
clearStack(function() {});
clearStack(function() {});
clearStack(function() {});
expect(queueMicrotask).toHaveBeenCalledTimes(10);
expect(setTimeout).not.toHaveBeenCalled();
});
}
function fakeMessageChannel() {
const channel = {
port1: {},
port2: {
postMessage: function() {
channel.port1.onmessage();
}
}
};
return channel;
}
});

View File

@@ -436,6 +436,9 @@ describe('Env integration', function() {
},
clearTimeout: function(fn, delay) {
clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
@@ -473,6 +476,9 @@ describe('Env integration', function() {
},
clearTimeout: function(fn) {
clearTimeout(fn);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
@@ -529,6 +535,9 @@ describe('Env integration', function() {
},
clearTimeout: function(fn, delay) {
clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
@@ -545,8 +554,8 @@ describe('Env integration', function() {
env.it('fails', function(specDone) {
setTimeout(function() {
specDone();
setTimeout(function() {
setTimeout(function() {
queueMicrotask(function() {
queueMicrotask(function() {
global.onerror('fail');
});
});
@@ -578,6 +587,9 @@ describe('Env integration', function() {
},
clearTimeout: function(fn, delay) {
clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
@@ -638,6 +650,9 @@ describe('Env integration', function() {
},
clearTimeout: function(fn, delay) {
clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
@@ -677,6 +692,9 @@ describe('Env integration', function() {
},
clearTimeout: function(fn) {
clearTimeout(fn);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
@@ -1427,8 +1445,8 @@ describe('Env integration', function() {
global: {
setTimeout: globalSetTimeout,
clearTimeout: clearTimeout,
setImmediate: function(cb) {
return setTimeout(cb, 0);
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
}
});
@@ -1501,8 +1519,8 @@ describe('Env integration', function() {
clearTimeout: clearTimeout,
setInterval: setInterval,
clearInterval: clearInterval,
setImmediate: function(cb) {
return realSetTimeout(cb, 0);
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
}
});
@@ -2625,6 +2643,9 @@ describe('Env integration', function() {
clearTimeout: function(fn, delay) {
clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
},
onerror: function() {}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
@@ -2680,6 +2701,9 @@ describe('Env integration', function() {
clearTimeout: function(fn, delay) {
clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
},
onerror: originalOnerror
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
@@ -2869,6 +2893,9 @@ describe('Env integration', function() {
},
clearTimeout: function(fn, delay) {
return clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
}
};
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);

View File

@@ -1,8 +1,32 @@
getJasmineRequireObj().clearStack = function(j$) {
const maxInlineCallCount = 10;
function messageChannelImpl(global, setTimeout) {
const channel = new global.MessageChannel();
function browserQueueMicrotaskImpl(global) {
const { setTimeout, queueMicrotask } = global;
let currentCallCount = 0;
return function clearStack(fn) {
currentCallCount++;
if (currentCallCount < maxInlineCallCount) {
queueMicrotask(fn);
} else {
currentCallCount = 0;
setTimeout(fn);
}
};
}
function nodeQueueMicrotaskImpl(global) {
const { queueMicrotask } = global;
return function(fn) {
queueMicrotask(fn);
};
}
function messageChannelImpl(global) {
const { MessageChannel, setTimeout } = global;
const channel = new MessageChannel();
let head = {};
let tail = head;
@@ -13,7 +37,7 @@ getJasmineRequireObj().clearStack = function(j$) {
delete head.task;
if (taskRunning) {
global.setTimeout(task, 0);
setTimeout(task, 0);
} else {
try {
taskRunning = true;
@@ -39,29 +63,31 @@ getJasmineRequireObj().clearStack = function(j$) {
}
function getClearStack(global) {
let currentCallCount = 0;
const realSetTimeout = global.setTimeout;
const setTimeoutImpl = function clearStack(fn) {
Function.prototype.apply.apply(realSetTimeout, [global, [fn, 0]]);
};
const NODE_JS =
global.process &&
global.process.versions &&
typeof global.process.versions.node === 'string';
if (j$.isFunction_(global.setImmediate)) {
const realSetImmediate = global.setImmediate;
return function(fn) {
currentCallCount++;
const SAFARI =
global.navigator &&
/^((?!chrome|android).)*safari/i.test(global.navigator.userAgent);
if (currentCallCount < maxInlineCallCount) {
realSetImmediate(fn);
} else {
currentCallCount = 0;
setTimeoutImpl(fn);
}
};
} else if (!j$.util.isUndefined(global.MessageChannel)) {
return messageChannelImpl(global, setTimeoutImpl);
if (NODE_JS) {
// Unlike browsers, Node doesn't require us to do a periodic setTimeout
// so we avoid the overhead.
return nodeQueueMicrotaskImpl(global);
} else if (
SAFARI ||
j$.util.isUndefined(global.MessageChannel) /* tests */
) {
// queueMicrotask is dramatically faster than MessageChannel in Safari.
// Some of our own integration tests provide a mock queueMicrotask in all
// environments because it's simpler to mock than MessageChannel.
return browserQueueMicrotaskImpl(global);
} else {
return setTimeoutImpl;
// MessageChannel is faster than queueMicrotask in supported browsers
// other than Safari.
return messageChannelImpl(global);
}
}