← 部落格

Turbolog:消除閃爍

每個影格前清除主控台,會使 DevTools 在 70%~81% 的繪製中呈現空白。附加影格後,空白比例降至 1%,且全都出現在第一個影格。

Adam

每個 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 下,有 70.6% 的 DevTools 繪製呈現空白。在 60 fps 下則是 81.1%。

原因

DevTools 的 ConsoleView 對這兩種呼叫採用不同的處理方式:

  1. console.clear() 呼叫 consoleCleared(),以同步方式使可視區域失效。所有訊息都在同一個工作中消失。
  2. 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
每個影格清除1570.6%70 / 7560.4 ms
附加(1.0)151.0%74 / 7510.4 ms
每個影格清除6081.1%45 / 3009.8 ms
附加,未調整節奏600.8%67 / 29531.2 ms

剩下的 1% 是第一個影格,會刻意執行清除。其空白繪製顯示的是 Console was cleared 那一列,而不是空的主控台。

減少每個 span 的屬性

DevTools 的原始碼揭露了另一項成本。它會將 %c 樣式的每個個別屬性複製到每列的包裝元素,因此 font: 和 margin: 簡寫會展開成每個 span 約 33 個屬性。現在 span 只使用個別屬性,每個約 7 個,並使用十六進位色碼。

附加模式已隨 Text.js 0.4 推出,相位鎖定修正則隨 1.0 推出。兩者都不需要修改應用程式碼。