跳到正文
zenn · 游戏开发社区· 来源· maruhana·· 4 小时前AI 评分51

截取画面来测量操作延迟时,发现测量工具本身反而更慢。改用无头浏览器按帧为单位进行测量

画面を撮って操作の遅れを測ったら、測る道具のほうが遅かった。ヘッドレスのブラウザでフレーム単位に測る

正文 · AI 翻译

LAN パーティの記事シリーズ。テーブルを囲むノート PC とスマホ、中央のルーター、上に南京錠付きの証明書の線画

我在家里的电脑上搭了个服务器,正在做一个4人合作的烹饪游戏,用手机浏览器就能玩。整体的介绍写在入口文章里。

客户端预测的文章中,我通过计算估算了从按下按键到角色动起来之间的延迟。虽然写了计算过程,但没有测量在画面上实际是多少毫秒。于是,趁着做文章竖版视频的机会,我打算从输入到画面上显示出来,从录制的画面那一侧来测量。

如果在页面中读取 performance.now(),可以测量从输入到状态更新为止的时长。但那是"页面判断出的时刻",而非实际显示在屏幕上的时刻。这次我想了解的是后者,因此为了查看屏幕上的实际显示效果,我决定对屏幕进行截图。

截取方式:直接调用 Chrome DevTools Protocol

以 --remote-debugging-port 参数启动无头 Chrome,从 Node 端通过 WebSocket 直接对接 Chrome DevTools Protocol(CDP)。Node 24 本身已内置 WebSocket,因此没有添加任何依赖。

用于测量的主要命令为以下 4 个。

命令 用途是什么?
Emulation.setDeviceMetricsOverride 创建一个 1080 × 1920 的视口
Page.startScreencast 逐帧以 JPEG 格式接收已渲染的画面
Page.screencastFrameAck 回复收到的指令
Input.dispatchKeyEvent 按照既定的时间间隔按下并松开按键
const frames = [];
on('Page.screencastFrame', async (p) => {
  // timestamp は秒。ミリ秒にして控える
  frames.push({ t: p.metadata.timestamp * 1000, data: p.data });
  await send('Page.screencastFrameAck', { sessionId: p.sessionId });
});
await send('Page.startScreencast', {
  format: 'jpeg', quality: 85, maxWidth: 1080, maxHeight: 1920, everyNthFrame: 1,
});

每条指令附带的 metadata.timestamp 在协议定义中被描述为 "Frame swap timestamp."。它既不是开始绘制的时间,也不是我方收到的时间,而是指令被替换的时间。类型为 Network.TimeSinceEpoch,单位为秒。按下按键的时间是在发送 Input.dispatchKeyEvent 之前通过 Node 的 Date.now() 记录的。由于两者都使用同一台 PC 的时钟,取差值即为延迟。不过按此标准,从 Node 向 Chrome 传递指令的 WebSocket 交接时间也会被计入延迟中。

现行协议定义中,未收到 ack 即被发送的指令数量有上限(maxFramesInFlight,默认为 3),还有一条规定:收到 ack 后立即发送最后一条已绘制的指令 sendLastFrame。未确认所测的 Chrome 154 是否包含这两项。本次每次收到指令后立即返回 ack。

启动参数目前仍写着 --headless=new,但根据 Chrome 官方文档,从 Chrome 132.0.6793.0 起,旧的无头模式已拆分为单独的 chrome-headless-shell 二进制文件。官方说明只写了传递纯 --headless 的用法,似乎已无需添加 =new。

游戏画面使用 Phaser 的 WebGL 渲染。按压方式为:右短按 5 次、右长按 1 次、上按 1 次,共 7 次。

陷阱 1:仅凭画面整体变化无法确定起始时刻

最初,我在整个画面上计算与上一帧的差异,将按压后差异首次跳升的帧定为"起始时刻"。阈值根据按压前 400 毫秒内差异的均值和标准差确定。

另外,我还设计了一种方法:用归一化互相关追踪角色头顶的名牌,将名牌首次偏移 2 像素以上的帧定为"起始时刻"。将两种方法同时应用到同一帧序列上,结果如下。

按压次数 追踪名牌 画面整体差异
1 48 ms 48 ms
2 66 ms 0 ms
3 70 ms 70 ms
4 62 ms 62 ms
5 66 ms 66 ms
6 145 ms 29 ms
7 未找到 39 ms

第 2 次的 0 毫秒意味着,在按键后紧接着的那一帧中就已经超过了阈值。在那个时刻,角色还无法移动。29 毫秒和 39 毫秒也都还未超过按键后的 40 毫秒。厨房里有闪烁的提灯和走动的客人,它拾取的是与按键无关、自行移动的物体。

我怀疑阈值的设定方式,用「平均 + 2σ」「平均 + 3σ」「平均的 2 倍」三种方法重新计算,但 7 次得到的值都相同。问题不在阈值,而在于观察范围内混入了其他的运动。

棘手的是,7 次中有 4 次给出与名牌相同的值。如果全部都不一致就能发现了,但超过半数都是吻合的,所以不与其他方法并列对比就无法判断哪个是错的。

陷阱 2:自己放置的「按压印记」会在与角色相同的格子上亮起

对于竖屏视频,我想加一个标识来显示当前正按着哪个键。我在屏幕左下角放了一个 position: fixed 方框,在捕获阶段捕获页面上的 keydown,然后就地将其变为红色。由于不经过游戏逻辑处理,本意是按键瞬间就会点亮。

我也数了那个标记变红的第一张卡。

按下的次数 直到指示灯亮起为止 直到铭牌开始移动
1 48 ms 48 ms
2 66 ms 66 ms
3 70 ms 70 ms
4 62 ms 62 ms
5 66 ms 66 ms
6 145 ms 145 ms
7 39 毫秒 未找到

印章在按下后的第 2 至 5 帧出现,中位数为 66 毫秒。这相当于帧间隔中位数(14.5 毫秒)的 4.5 倍。帧间隔以 14.5 毫秒居多,但平均约为 21 毫秒,偶尔会有较大的空隙。第 6 次的 145 毫秒也落在按下后第 5 帧以内。并且第 1 至 6 次与名牌开始移动的帧完全相同(时刻完全一致)。

即使改变 DOM 盒子的颜色,屏幕上呈现的也是下一帧要绘制的画面。加入预测机制的游戏,在接收到按键后同样会在下一帧移动己方角色。由于两者都出现在「下一帧」中,因此落在同一帧上。将此标记设为 0 时,有预测机制的延迟看起来就是 0。

由此还可以看出另一件事:即便完全不经过游戏处理的盒子,从按下到画面呈现也需要 39~145 毫秒。输入的传递、下一帧的绘制、切换、拍摄交接,应该全部包含在这其中,用这种方式能测到的下限就在那里。具体内Breakdown尚未测量。

视频的标记不再依赖渲染。根据按下时刻计算帧号,拍摄后再烧录进视频中。这样,标记的点亮便与按下的帧一致了。

陷阱 3:使用软件渲染时,可拍摄帧数降为 1/5 至 1/13

为应对没有 GPU 的环境下的相同情况,也拍摄了使用软件(SwiftShader)渲染 WebGL 时的结果。参数为 --use-gl=angle --use-angle=swiftshader --enable-unsafe-swiftshader。

回 GPU(默认) 软件
1 47.7 fps 3.8 fps
2 43.6 fps 4.0 fps
3 46.8 fps 9.6 fps
中央値 46.8 fps 4.0 fps

每秒4帧,每帧即为250毫秒。后文提到的有预测与无预测之间的差异(约110毫秒),甚至完全被一整帧所吞没。输入的时间分配相同,拍摄完成所需的时间却延长到了13〜19秒(GPU上约为11.5秒)。

但,不能把这个数字当作基准。在同样的 PC 上之前测得 GPU 为 64 fps、软件渲染为 9.1 fps。中央值之比约为 1/12,但同一轮次中 GPU 与软件渲染的比值在 5 到 13 倍之间波动,上次的比值为 7 倍。能说的只是"差了好几倍",绝对值因天而异。如果要留下 fps 记录,就决定每次都在现场测量并一并写入。

即便如此仍能测出的数字

以下是通过追踪名札的方法,在相同条件下对比有预测与无预测所得到的值。在同一台 PC 中,从无头 Chrome 连接到 localhost。带有延迟的行是通过临时改造,在服务器发送和接收时分别加入 100 毫秒等待,从而制造出 200 毫秒的往返延迟。所有值均为各 6 次测试的中位数。

通信延迟 无预测 含预测
未添加 173 ms(142〜211) 63 ms(50〜81)
添加了往返 200 ms 401 ms(374〜431) 60 ms(40〜91)

有预测的 63 毫秒与陷阱 2 标记的中位数(66 毫秒)几乎重合(拍摄日期不同,但陷阱 2 的同一帧列中,名牌和标记处于同一帧)。也就是说,有预测的角色,在这个工具可见的最早帧中,已经在动了。有预测的真实延迟是否比这更小,这个工具无法判断。有预测与无预测之间的差距(约 110 毫秒)则明显大于工具的下限,这一点可以说得出来。

加上延迟后,无预测从 173 毫秒变为 401 毫秒,加上的 200 毫秒往返几乎原封不动地叠加了上去。有预测保持 60 毫秒,与加之前相同。有预测的角色不等通信,就在工具下限附近持续运动。

在客户端预测的文章中,无预测的延迟被计算为平均 170 毫秒、最坏 220 毫秒。拍摄的画面中为 142〜211 毫秒,接近计算值。不过这个值中应该也包含工具的延迟,所以不能断言完全吻合。

拍摄的其他陷阱

  • 启动后立即按的按钮没有反应。游戏入口的按钮是静态 HTML,在脚本加载之前就已经存在于 DOM 中。可以按,但什么都不会发生。改为等待body加上输入方式类之后再按。
  • 超出纵向滚动盒子顶部显示区域的部分,即使用 Page.captureScreenshot 的 captureBeyondViewport 也拍不到。没画出来的东西就拍不到。想拍的时候,把盒子放回正常文档流里再往下挪了
  • 用 display: contents 叠加当时的 CSS 来制作视口的方法,写在了关于手机 CSS 的文章中「测量方法」一节。可以在不改动标记的情况下复现过去的外观

关于其他方法

  • 用 Puppeteer 或 Playwright 不就好了吗。两者在 Chromium 中都是基于 CDP 运行的。Playwright 的录屏(recordVideo)生成的是视频文件,不附带逐帧时间戳。从 v1.61 开始,page.screencast.start() 的 onFrame 可以接收逐帧时间戳(浏览器显示的时间戳),仅用 Playwright 似乎也能做到同样的事。该时间戳是否与 CDP 的 metadata.timestamp 相同未经验证。如果是 Chromium,也可以打开 CDP 会话。这次为了不用额外添加依赖,直接调用了 CDP
  • 在页面内部测量不就好了吗。从输入到状态更新的话,那样确实更准确。但那不是出现在屏幕上的时刻。有无预测的差异,我想在屏幕显示的那一刻来观察
  • 使用 Event Timing API 或 Performance 面板不就行了吗。Event Timing API 的 duration 返回的是按键处理之后到下一次绘制完成之间的时间(以 8 毫秒为单位)。但那是印字框变红的那一帧,既不是角色动起来的那一帧,也不是画面显示出来的时刻。可以用来将陷阱 2 中的 66 毫秒拆分为页面内部和外部两部分。Chrome 的 Performance 面板或追踪记录中,输入与帧按时间顺序排列,似乎可以为内部分项提供线索,但本次并未使用。按下时刻的基准,本次采用的是发送前的 Date.now(),但也可以考虑使用页面接收时刻(event.timeStamp),或将 timestamp 传递给 Input.dispatchKeyEvent。
  • 整体差异的阈值稍加调整应该就可以了。换过 3 种方式,得到的值都一样。这次,把观察范围限定在想要追踪的对象(名牌)上。
  • 用 requestAnimationFrame 输出印不就好了吗。只要依赖绘制,在本次结果中就无法从"下一帧"中逃脱。这次决定从按下时刻开始计算。

尚未核实的事项

  • 所有测量均在无头 Chrome 中完成。未在实际手机设备上进行测量。实际设备从启动到画面显示所需的时间与此是另一回事。
  • 在有预测的情况下,耗时约 60 毫秒,无法降至 60 fps 单帧(约 17 毫秒)的原因是:未分项测量。输入传递、渲染、替换、捕获交接中哪一项占比最大并不清楚。
  • 帧间隔并不恒定(中位数 14.5 毫秒,均值约 21 毫秒)。比这更细微的差异,用这种方法无法判断。
  • fps 的对比各做了 3 次,延迟对比各做了 6〜7 次,次数较少。

总结

用拍摄画面来测量时,测量工具本身也存在延迟和分辨率限制。本次测试中,即便是理应最快显示的标记,中位数也要 66 毫秒;若用软件渲染,一帧则变为 250 毫秒。这两个数值都与想要测出的差异相当,甚至更大。

所有陷阱的共同点在于,并非工具不返回结果,而是返回了一个看似合理的数字。整体差异有超过一半是准确的,标记也能干净利落地输出 0。因此要先测量工具本身。在相同条件下测好与按键无关的标记何时出现、每秒能拍多少帧,就能划出一条分界线:比这个更小的差异无法下结论。

LAN 派对的文章


注記

  • 所有数值均是在同一台 Windows PC 上,通过 CDP 操作无头 Chrome 截取的帧中测得的。陷阱 1〜3 使用 Chrome 154,fps 每个条件测 3 次,延迟测 7 次按键(名牌为 6 次)。有预测与无预测的对比(含加了延迟的条件)是另一天各测 6 次的,未记录当时的 Chrome 版本
  • 之前测得的 fps(64 和 9.1)是同一台 PC 在另一天截取的记录
  • 未测量延迟的细分(输入分发、渲染、帧切换、截图交接),也未在智能手机实机上测量

来源:zenn · 游戏开发社区 · zenn.dev