setTimeout 过早调用函数
setTimeout calling the function prematurely
我的库有基于实时的测试用例,我注意到测试会随机失败并出现 1 毫秒的错误:
expect(received).toBeGreaterThanOrEqual(expected)
Expected: >= 1000
Received: 999
这似乎是因为 setTimeout 过早地调用了函数。
所以我单独写了一个测试脚本:
let last = Date.now()
setTimeout(next, 1000)
function next() {
if (Date.now() - last < 1000) process.exit(1)
last = Date.now()
setTimeout(next, 1000)
}
在Node.jsv12.19.0、v14.15.3、v15.4.0上,会随机失败:有时脚本可以继续到运行,有时脚本很快就会退出。
这不仅发生在我的本地计算机上,而且发生在 Github 的 CI 服务器上。
我的问题:这是一个错误吗?或者 setTimeout 的某种预期行为?或者Date.now() - time
总是需要加1毫秒?
更新:这里使用 git-bisect
是罪魁祸首:
2c409a285359faae58227da283a4c7e5cd9a2f0c is the first bad commit
commit 2c409a285359faae58227da283a4c7e5cd9a2f0c
Date: Tue Aug 25 13:36:37 2020 -0600
perf_hooks: add idleTime and event loop util
Use uv_metrics_idle_time() to return a high resolution millisecond timer
of the amount of time the event loop has been idle since it was
initialized.
Include performance.eventLoopUtilization() API to handle the math of
calculating the idle and active times. This has been added to prevent
accidental miscalculations of the event loop utilization. Such as not
taking into consideration offsetting nodeTiming.loopStart or timing
differences when being called from a Worker thread.
PR-URL: https://github.com/nodejs/node/pull/34938
这似乎是一个错误,而不是预期的行为。我会投票反对总是添加 1ms,因为行为不一致。 (但是,它会早于 1 毫秒吗?我没有观察到超过 1 毫秒)您可以通过以下方法解决该问题:
const origSetTimeout = setTimeout;
setTimeout = (f, ms, ...args) => {
let o;
const when = Date.now() + ms,
check = ()=> {
let t = when - Date.now();
if (t > 0) Object.assign(o, origSetTimeout(check, t));
else f(...args);
};
return o = origSetTimeout(check, ms);
};
它允许 clearTimeout()
即使在解决问题时也是如此。
这是一个模拟问题并每 3 秒交替解决方法的浏览器代码:
// Simulate the problem
const realOrigSetTimeout = setTimeout;
setTimeout = (func, ms, ...args) => realOrigSetTimeout(func, ms - Math.random(), ...args);
const ms = 200;
let when = Date.now() + ms;
setTimeout(next, ms);
function next() {
let now = Date.now();
setTimeout(next, ms);
console.log(now < when ? 'premature' : 'ok');
when = now + ms;
}
function workAround() {
console.log('Applying workaround');
const origSetTimeout = setTimeout;
setTimeout = (f, ms, ...args) => {
let o;
const when = Date.now() + ms,
check = ()=> {
let t = when - Date.now();
if (t > 0) Object.assign(o, origSetTimeout(check, t));
else f(...args);
};
return o = origSetTimeout(check, ms);
};
setTimeout(_=>{
console.log('Removing workaround');
setTimeout = origSetTimeout;
setTimeout(workAround, 3000);
}, 3000);
}
setTimeout(workAround, 3000);
下面是一个 nodejs 代码,可以清楚地显示问题('p' 在点中)并在按 enter 后应用解决方法。
'use strict';
const ms = 1;
let when = Date.now() + ms;
setTimeout(next, ms);
function next() {
let now = Date.now();
setTimeout(next, ms);
process.stdout.write(now < when ? 'p' : '.');
when = now + ms;
}
process.stdin.on('readable', _=> {
console.log('enabling workaround');
const origSetTimeout = setTimeout;
setTimeout = (f, ms, ...args) => {
let o;
const when = Date.now() + ms,
check = ()=> {
let t = when - Date.now();
if (t > 0) Object.assign(o, origSetTimeout(check, t));
else f(...args);
};
return o = origSetTimeout(check, ms);
};
});
我的库有基于实时的测试用例,我注意到测试会随机失败并出现 1 毫秒的错误:
expect(received).toBeGreaterThanOrEqual(expected)
Expected: >= 1000
Received: 999
这似乎是因为 setTimeout 过早地调用了函数。
所以我单独写了一个测试脚本:
let last = Date.now()
setTimeout(next, 1000)
function next() {
if (Date.now() - last < 1000) process.exit(1)
last = Date.now()
setTimeout(next, 1000)
}
在Node.jsv12.19.0、v14.15.3、v15.4.0上,会随机失败:有时脚本可以继续到运行,有时脚本很快就会退出。 这不仅发生在我的本地计算机上,而且发生在 Github 的 CI 服务器上。
我的问题:这是一个错误吗?或者 setTimeout 的某种预期行为?或者Date.now() - time
总是需要加1毫秒?
更新:这里使用 git-bisect
是罪魁祸首:
2c409a285359faae58227da283a4c7e5cd9a2f0c is the first bad commit
commit 2c409a285359faae58227da283a4c7e5cd9a2f0c
Date: Tue Aug 25 13:36:37 2020 -0600
perf_hooks: add idleTime and event loop util
Use uv_metrics_idle_time() to return a high resolution millisecond timer
of the amount of time the event loop has been idle since it was
initialized.
Include performance.eventLoopUtilization() API to handle the math of
calculating the idle and active times. This has been added to prevent
accidental miscalculations of the event loop utilization. Such as not
taking into consideration offsetting nodeTiming.loopStart or timing
differences when being called from a Worker thread.
PR-URL: https://github.com/nodejs/node/pull/34938
这似乎是一个错误,而不是预期的行为。我会投票反对总是添加 1ms,因为行为不一致。 (但是,它会早于 1 毫秒吗?我没有观察到超过 1 毫秒)您可以通过以下方法解决该问题:
const origSetTimeout = setTimeout;
setTimeout = (f, ms, ...args) => {
let o;
const when = Date.now() + ms,
check = ()=> {
let t = when - Date.now();
if (t > 0) Object.assign(o, origSetTimeout(check, t));
else f(...args);
};
return o = origSetTimeout(check, ms);
};
它允许 clearTimeout()
即使在解决问题时也是如此。
这是一个模拟问题并每 3 秒交替解决方法的浏览器代码:
// Simulate the problem
const realOrigSetTimeout = setTimeout;
setTimeout = (func, ms, ...args) => realOrigSetTimeout(func, ms - Math.random(), ...args);
const ms = 200;
let when = Date.now() + ms;
setTimeout(next, ms);
function next() {
let now = Date.now();
setTimeout(next, ms);
console.log(now < when ? 'premature' : 'ok');
when = now + ms;
}
function workAround() {
console.log('Applying workaround');
const origSetTimeout = setTimeout;
setTimeout = (f, ms, ...args) => {
let o;
const when = Date.now() + ms,
check = ()=> {
let t = when - Date.now();
if (t > 0) Object.assign(o, origSetTimeout(check, t));
else f(...args);
};
return o = origSetTimeout(check, ms);
};
setTimeout(_=>{
console.log('Removing workaround');
setTimeout = origSetTimeout;
setTimeout(workAround, 3000);
}, 3000);
}
setTimeout(workAround, 3000);
下面是一个 nodejs 代码,可以清楚地显示问题('p' 在点中)并在按 enter 后应用解决方法。
'use strict';
const ms = 1;
let when = Date.now() + ms;
setTimeout(next, ms);
function next() {
let now = Date.now();
setTimeout(next, ms);
process.stdout.write(now < when ? 'p' : '.');
when = now + ms;
}
process.stdin.on('readable', _=> {
console.log('enabling workaround');
const origSetTimeout = setTimeout;
setTimeout = (f, ms, ...args) => {
let o;
const when = Date.now() + ms,
check = ()=> {
let t = when - Date.now();
if (t > 0) Object.assign(o, origSetTimeout(check, t));
else f(...args);
};
return o = origSetTimeout(check, ms);
};
});