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$) { getJasmineRequireObj().clearStack = function(j$) {
const maxInlineCallCount = 10; const maxInlineCallCount = 10;
function messageChannelImpl(global, setTimeout) { function browserQueueMicrotaskImpl(global) {
const channel = new global.MessageChannel(); 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 head = {};
let tail = head; let tail = head;
@@ -2801,7 +2825,7 @@ getJasmineRequireObj().clearStack = function(j$) {
delete head.task; delete head.task;
if (taskRunning) { if (taskRunning) {
global.setTimeout(task, 0); setTimeout(task, 0);
} else { } else {
try { try {
taskRunning = true; taskRunning = true;
@@ -2827,29 +2851,31 @@ getJasmineRequireObj().clearStack = function(j$) {
} }
function getClearStack(global) { function getClearStack(global) {
let currentCallCount = 0; const NODE_JS =
const realSetTimeout = global.setTimeout; global.process &&
const setTimeoutImpl = function clearStack(fn) { global.process.versions &&
Function.prototype.apply.apply(realSetTimeout, [global, [fn, 0]]); typeof global.process.versions.node === 'string';
};
if (j$.isFunction_(global.setImmediate)) { const SAFARI =
const realSetImmediate = global.setImmediate; global.navigator &&
return function(fn) { /^((?!chrome|android).)*safari/i.test(global.navigator.userAgent);
currentCallCount++;
if (currentCallCount < maxInlineCallCount) { if (NODE_JS) {
realSetImmediate(fn); // Unlike browsers, Node doesn't require us to do a periodic setTimeout
} else { // so we avoid the overhead.
currentCallCount = 0; return nodeQueueMicrotaskImpl(global);
} else if (
setTimeoutImpl(fn); SAFARI ||
} j$.util.isUndefined(global.MessageChannel) /* tests */
}; ) {
} else if (!j$.util.isUndefined(global.MessageChannel)) { // queueMicrotask is dramatically faster than MessageChannel in Safari.
return messageChannelImpl(global, setTimeoutImpl); // 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 { } 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() { describe('in Safari', function() {
const setImmediate = jasmine usesQueueMicrotaskWithSetTimeout(function() {
.createSpy('setImmediate') return {
.and.callFake(function(fn) { navigator: {
fn(); 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'
global = { setImmediate: setImmediate }, },
clearStack = jasmineUnderTest.getClearStack(global); // queueMicrotask should be used even though MessageChannel is present
let called = false; MessageChannel: fakeMessageChannel
};
});
});
clearStack(function() { describe('in browsers other than Safari', function() {
called = true; 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); describe('when MessageChannel is unavailable', function() {
expect(setImmediate).toHaveBeenCalled(); 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() { describe('in Node', function() {
const setImmediate = jasmine.createSpy('setImmediate'), usesQueueMicrotaskWithoutSetTimeout(function() {
setTimeout = jasmine.createSpy('setTimeout'), return {
global = { setImmediate: setImmediate, setTimeout: setTimeout }, process: {
clearStack = jasmineUnderTest.getClearStack(global); versions: {
node: '3.1415927'
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();
} }
} }
}, };
global = { });
MessageChannel: function() { });
return fakeChannel;
}
},
clearStack = jasmineUnderTest.getClearStack(global);
let called = false;
clearStack(function() { function usesMessageChannel(makeGlobal) {
called = true; 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 = fakeMessageChannel();
spyOn(fakeChannel.port2, 'postMessage');
it('uses setTimeout instead of MessageChannel every 10 calls to make sure we release the CPU', function() { const setTimeout = jasmine.createSpy('setTimeout');
const fakeChannel = { const global = {
port1: {}, ...makeGlobal(),
port2: { setTimeout,
postMessage: jasmine
.createSpy('postMessage')
.and.callFake(function() {
fakeChannel.port1.onmessage();
})
}
},
setTimeout = jasmine.createSpy('setTimeout'),
global = {
MessageChannel: function() { MessageChannel: function() {
return fakeChannel; 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'), const clearStack = jasmineUnderTest.getClearStack(global);
global = {
MessageChannel: function() {
return fakeChannel;
},
setTimeout: setTimeout
},
clearStack = jasmineUnderTest.getClearStack(global),
fn = jasmine.createSpy('second clearStack function');
clearStack(function() { for (let i = 0; i < 9; i++) {
clearStack(fn); 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(); it('calls setTimeout when onmessage is called recursively', function() {
expect(setTimeout).toHaveBeenCalledWith(fn, 0); 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() { clearStack(function() {
const setTimeout = jasmine clearStack(fn);
.createSpy('setTimeout') });
.and.callFake(function(fn) {
expect(fn).not.toHaveBeenCalled();
expect(setTimeout).toHaveBeenCalledWith(fn, 0);
});
}
function usesQueueMicrotaskWithSetTimeout(makeGlobal) {
it('uses queueMicrotask', function() {
const global = {
...makeGlobal(),
queueMicrotask: function(fn) {
fn(); fn();
}), }
global = { setTimeout: setTimeout }, };
clearStack = jasmineUnderTest.getClearStack(global); const clearStack = jasmineUnderTest.getClearStack(global);
let called = false; let called = false;
clearStack(function() { clearStack(function() {
called = true; called = true;
});
expect(called).toBe(true);
}); });
expect(called).toBe(true); it('uses setTimeout instead of queueMicrotask every 10 calls to make sure we release the CPU', function() {
expect(setTimeout).toHaveBeenCalledWith(jasmine.any(Function), 0); 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: function(fn, delay) {
clearTimeout(fn, delay); clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
} }
}; };
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
@@ -473,6 +476,9 @@ describe('Env integration', function() {
}, },
clearTimeout: function(fn) { clearTimeout: function(fn) {
clearTimeout(fn); clearTimeout(fn);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
} }
}; };
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
@@ -529,6 +535,9 @@ describe('Env integration', function() {
}, },
clearTimeout: function(fn, delay) { clearTimeout: function(fn, delay) {
clearTimeout(fn, delay); clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
} }
}; };
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
@@ -545,8 +554,8 @@ describe('Env integration', function() {
env.it('fails', function(specDone) { env.it('fails', function(specDone) {
setTimeout(function() { setTimeout(function() {
specDone(); specDone();
setTimeout(function() { queueMicrotask(function() {
setTimeout(function() { queueMicrotask(function() {
global.onerror('fail'); global.onerror('fail');
}); });
}); });
@@ -578,6 +587,9 @@ describe('Env integration', function() {
}, },
clearTimeout: function(fn, delay) { clearTimeout: function(fn, delay) {
clearTimeout(fn, delay); clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
} }
}; };
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
@@ -638,6 +650,9 @@ describe('Env integration', function() {
}, },
clearTimeout: function(fn, delay) { clearTimeout: function(fn, delay) {
clearTimeout(fn, delay); clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
} }
}; };
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
@@ -677,6 +692,9 @@ describe('Env integration', function() {
}, },
clearTimeout: function(fn) { clearTimeout: function(fn) {
clearTimeout(fn); clearTimeout(fn);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
} }
}; };
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
@@ -1427,8 +1445,8 @@ describe('Env integration', function() {
global: { global: {
setTimeout: globalSetTimeout, setTimeout: globalSetTimeout,
clearTimeout: clearTimeout, clearTimeout: clearTimeout,
setImmediate: function(cb) { queueMicrotask: function(fn) {
return setTimeout(cb, 0); queueMicrotask(fn);
} }
} }
}); });
@@ -1501,8 +1519,8 @@ describe('Env integration', function() {
clearTimeout: clearTimeout, clearTimeout: clearTimeout,
setInterval: setInterval, setInterval: setInterval,
clearInterval: clearInterval, clearInterval: clearInterval,
setImmediate: function(cb) { queueMicrotask: function(fn) {
return realSetTimeout(cb, 0); queueMicrotask(fn);
} }
} }
}); });
@@ -2625,6 +2643,9 @@ describe('Env integration', function() {
clearTimeout: function(fn, delay) { clearTimeout: function(fn, delay) {
clearTimeout(fn, delay); clearTimeout(fn, delay);
}, },
queueMicrotask: function(fn) {
queueMicrotask(fn);
},
onerror: function() {} onerror: function() {}
}; };
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
@@ -2680,6 +2701,9 @@ describe('Env integration', function() {
clearTimeout: function(fn, delay) { clearTimeout: function(fn, delay) {
clearTimeout(fn, delay); clearTimeout(fn, delay);
}, },
queueMicrotask: function(fn) {
queueMicrotask(fn);
},
onerror: originalOnerror onerror: originalOnerror
}; };
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);
@@ -2869,6 +2893,9 @@ describe('Env integration', function() {
}, },
clearTimeout: function(fn, delay) { clearTimeout: function(fn, delay) {
return clearTimeout(fn, delay); return clearTimeout(fn, delay);
},
queueMicrotask: function(fn) {
queueMicrotask(fn);
} }
}; };
spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global); spyOn(jasmineUnderTest, 'getGlobal').and.returnValue(global);

View File

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