Turbolog:消除闪烁
每帧前清空控制台会使 70%~81% 的 DevTools 绘制出现明显空白。追加帧后,这一比例降至 1%,且全部发生在首帧。
每个 Text.js 帧都是一次 console.log 调用。在 0.4 之前,每帧之前还会调用 console.clear()。在负载下,控制台会在帧之间闪现空白。
测量闪烁
为了测量闪烁,我们构建了 bench/devtools。它启动 Chrome for Testing,将真实的 DevTools 前端作为页面打开,并加入测量代码:
- 在 DevTools 前端的每次
requestAnimationFrame中,检查控制台视口内是否有完整的 32 行帧。没有完整帧的绘制算作空白绘制。 - 随后解码
Page.startScreencast帧,从像素层面确认结果。 - 每帧的第 0 行都带有
FRAME:<id> · T:<epochMs>,以便前端识别当前显示的帧及其延迟。
每帧之前调用 console.clear() 时,100×32 的仪表盘以 15 fps 运行,DevTools 绘制中有 70.6% 为空白。在 60 fps 下,这一比例为 81.1%。
原因
DevTools 的 ConsoleView 对这两种调用的处理不同:
console.clear()调用consoleCleared(),同步使视口失效。所有消息在同一个任务中消失。console.log()调用addConsoleMessage(),只通过viewportThrottler调度刷新,后者是一个超时时间为 50 ms 的Throttler。
因此,每帧都会立即移除旧帧,稍后才绘制新帧。在 15 fps 下,每 66 ms 中约有 50 ms 不显示任何内容。
追加帧
从 0.4 开始,Turbolog 采用追加帧的方式。DevTools 保持在控制台底部,因此新帧会在一次绘制中替换旧帧。开头带有 margin-top:100vh 的零宽 span(幕布)将先前的帧推到视野之外,并独占一行,确保 DevTools 的源代码链接不会挤窄第 0 行。
控制台最终仍需清空。清空只发生在首帧、invalidate() 之后、累积 100 帧后的空闲时,以及达到 300 帧的硬性上限时。
匹配节流器的节奏
以超过 DevTools 绘制能力的速度发送帧,不会加快任何环节。节流器等待期间到达的日志会重启节流器,因此会丢帧:在 60 fps 下,DevTools 在发出的 295 帧中只绘制了 67 帧。Turbolog 将帧间隔控制在至少 66 ms,约为 15 fps,并在间隔允许时仍然同步渲染离散输入。
打破相位锁定
清空操作还会为后续每帧增加约 50 ms 的延迟。清空后,DevTools 会插入一条自动生成的 Console was cleared 行。它占用节流器的即时执行机会,将下一帧推到 50 ms 定时器上,而在稳定的 66 ms 节奏下,后续每帧都会锁定在该定时器上。
现在,渲染器在每次清空后会等待一次 110 ms。此后,每帧都在上次刷新完成超过 50 ms 后到达,并走 DevTools 的即时处理路径。显示延迟的中位数从 63 ms 降至 10.4 ms。
结果
bun bench/devtools/run.ts --strategy=native,clear --fps=15,60 --seconds=5,100×32 仪表盘,真实 DevTools 前端:
| 驱动方式 | FPS | 空白绘制 | 已绘制 / 已发出 | 延迟 p50 |
|---|---|---|---|---|
| 每帧清空 | 15 | 70.6% | 70 / 75 | 60.4 ms |
| 追加(1.0) | 15 | 1.0% | 74 / 75 | 10.4 ms |
| 每帧清空 | 60 | 81.1% | 45 / 300 | 9.8 ms |
| 追加,不限制节奏 | 60 | 0.8% | 67 / 295 | 31.2 ms |
剩余的 1% 来自首帧,首帧会主动清空控制台。这些空白绘制显示的是 Console was cleared 行,而非空控制台。
减少每个 span 的属性
DevTools 源代码揭示了另一项开销。它将 %c 样式的每个长属性复制到每个行包装元素上,因此 font: 和 margin: 简写会展开为每个 span 约 33 个属性。现在 span 仅使用长属性,每个约 7 个,并使用十六进制颜色。
追加模式已在 Text.js 0.4 中发布,相位锁定修复随 1.0 发布。两者都不需要修改应用代码。