void Time::EnableHighResolutionTimer(bool enable) {
base::AutoLock lock(g_high_res_lock.Get());
if (g_high_res_timer_enabled == enable)
return;
g_high_res_timer_enabled = enable;
if (!g_high_res_timer_count)
return;
// Since g_high_res_timer_count != 0, an ActivateHighResolutionTimer(true)
// was called which called timeBeginPeriod with g_high_res_timer_enabled
// with a value which is the opposite of |enable|. With that information we
// call timeEndPeriod with the same value used in timeBeginPeriod and
// therefore undo the period effect.
if (enable) {
timeEndPeriod(kMinTimerIntervalLowResMs);
timeBeginPeriod(kMinTimerIntervalHighResMs);
} else {
timeEndPeriod(kMinTimerIntervalHighResMs);
timeBeginPeriod(kMinTimerIntervalLowResMs);
}
}
其中,kMinTimerIntervalLowResMs = 4,kMinTimerIntervalHighResMs = 1。timeBeginPeriod 以及timeEndPeriod 是 Windows 提供的用来修改系统 timer interval 的函数。也就是说在接电源时,我们能拿到的最小的 timer interval 是1ms,而使用电池时,是4ms。由于我们的循环不断地调用了 setTimeout,根据 W3C 规范,最小的间隔也是 4ms,所以松口气,这个对我们的影响不大。
又一个精度问题
回到开头,我们发现测试结果显示,setTimeout 的间隔并不是稳定在 4ms 的,而是在不断地波动。而 测试结果也显示,间隔在 48ms 和 49ms 之间跳动。原因是,在 Chromium 和 Node.js 的 event loop 中,等待 IO 事件的那个 Windows 函数调用的精度,受当前系统的计时器影响。游戏逻辑的实现需要用到 requestAnimationFrame 函数(不停更新画布),这个函数可以帮我们将计时器间隔至少设置为 kMinTimerIntervalLowResMs(因为他需要一个16ms的计时器,触发了高精度计时器的要求)。测试机使用电源的时候,系统的 timer interval 是 1ms,所以测试结果有 ±1ms 的误差。如果你的电脑没有被更改系统计时器间隔,运行上面那个#48的测试,max可能会到达48+16=64ms。
使用 Chromium 的 setTimeout 实现,我们可以将 setTimeout(fn, 1) 的误差控制在 4ms 左右,而 setTimeout(fn, 48) 的误差可以控制在 1ms 左右。于是,我们的心中有了一幅新的蓝图,它让我们的代码看起来像是这样:
复制代码 代码如下:
/* Get the max interval deviation */
var deviation = getMaxIntervalDeviation(bucketSize); // bucketSsize = 48, deviation = 2;
function gameLoop() {
var now = Date.now();
if (previousBucket + bucketSize <= now) {
previousBucket = now;
doLogic();
}
if (previousBucket + bucketSize - Date.now() > deviation) {
// Wait 46ms. The actual delay is less than 48ms.
setTimeout(gameLoop, bucketSize - deviation);
} else {
// Busy waiting. Use setImmediate instead of process.nextTick because the former does not block IO events.
setImmediate(gameLoop);
}
}
上面的代码让我们等待一个误差小于 bucket_size( bucket_size – deviation) 的时间而不是直接等于一个 bucket_size,46ms 的 delay 即便发生了最大的误差,根据上文的理论,实际间隔也是小于48ms的。剩下的时间我们使用忙等待的方法,确保我们的 gameLoop 在足够精确的 interval 下执行。
虽然我们利用 Chromium 在一定程度上解决了问题,但这显然不够优雅。
还记得我们最初的要求吗?我们的服务器端代码是应该可以脱离 Node-Webkit 客户端的,直接在一台有 Node.js 环境的电脑中运行。如果直接跑上面的代码,deviation 的值至少是16ms,也就是说在每一个48ms中,我们要忙等待16ms的时间。CPU使用率蹭蹭蹭就上去了。
意想不到的惊喜