病理報告 · 標本 #6

docs/pathology-report-template.md 版型,八節全含,不刪節。

實測數字唯一來源docs/measurements/2026-07-26-reproducibility-4x-5.json (2026-07-26 下午完整六標本掃描,66 筆,isFullSweep: trueproblems: []consoleErrors: []measuredAt 2026-07-26T05:52:29.671Z)。 欄位路徑一律寫成 records[mode=…,run=…].欄位,指的是該 JSON 內 specimenId = 06-rerender-storm 的那 12 筆。其他輪(同日上午 -4x.json、2026-07-25)的數字 只以「另一 session」身分出現並逐一標明出處 —— 跨 session 絕對值不可比 (docs/phase2-expected-results.md:944-945,下稱 phase2)。

判準狀態phase2:1259-1338 檔尾追記已廢除 #6 的兩條比值判準 (≥ 6×renderRatio < 0.5),新判準生效於下一輪起、對本份 JSON 不回溯phase2:1334-1338)。所以本報告只報方向與絕對值,不寫「通過/未通過比值判準」; 文中出現的倍率一律是描述性的,並各自附警語。


06 高頻資料流造成的 re-render 風暴

病症一句話:按一次「開始推送」後靜置 5 秒,模擬資料流每 25ms 推一批 100 台裝置更新 (共 1000 台);病變版每收到一批就重建整張 1000 列清單,5 秒量測窗(60Hz,窗頂約 300 幀) 內掉幀峰值 275~281 —— 幾乎每一幀都掉,有渲染的幀距 median 約 200ms(一秒五格畫面); 治療二(細粒度)同一窗內掉幀 14~18,幀距貼回 16.7ms 的 vsync 底線。

⚠️ 這句話的絕對值只屬於本份 JSON(4x、CDP、2026-07-26 下午那一台機器的那一個小時)。 治療臂未飽和,絕對值是機器速度的函數:同日上午另一 session 的細粒度臂是 85 / 59 / 84(phase2:1085-1090),更早一輪還出現過 4~8 (specimens/06-rerender-storm.ts:19,下稱 ts)。病變臂因飽和反而對機器不敏感 (上午 281/284/277 → 本輪 281/277/275,phase2:1087)—— 於是兩臂的比值也是機器的函數phase2:1088-1090),單獨引用這句話時必須連 session 一起引。

凍結條件

同一組條件之間才能比較。任何一項改動,先前所有數字作廢。

CPU throttle 4x —— 宣告值(sweep.cpuThrottlecpuThrottlingRate: 4)。驅動器有實套 CDP Emulation.setCPUThrottlingRatetools/reproducibility.mjs bootShell),但 JS 偵測不到、本份 JSON 內部無 1x 臂可檢核,這一格只能是宣告
驅動方式 CDP 機器驅動(sweep.driverInput.dispatchMouseEvent)。本標本整輪只有一次真實點擊,其後靜置
viewport 800 × 600(FROZEN_VIEWPORTsrc/specimens.ts:411;CLS 與 LCP 都是 viewport 相對量)
refreshHz 60records[*].refreshHz,12 筆皆 60)。掉幀門檻與一幀預算由實測值推導(ts:173-179 targetFrameMs),不寫死 16.7ms
操作程序 click × 1(「開始推送」)→ 靜置,串流 5000ms 自動停止action: 'stream'repetitions: 1intervalMs: nullsrc/specimens.ts:403-409STREAM_DURATION_MSts:159)。串流長度=掉幀滾動窗(droppedFrameWindowMs: 5000src/protocol.ts:51)⇒ 窗頂 ≈ 300 幀
負載參數(凍結) DEVICE_COUNT 1000(ts:99)、PUSH_INTERVAL_MS 25(ts:129)、BATCH_SIZE 100(ts:132)。12 筆 records 的 custom.deviceCount / pushIntervalMs / batchSize 逐筆回報同值
buildId 0.1.0-ms191c71records[*].buildId,12 筆同)
protocolVersion 1(src/protocol.ts:8 的常數)。⚠️ 本份 JSON 的 records 沒有這個欄位(全檔 grep 零筆),此格取自程式碼,不是量測實錄
dispatchSpanMs / Nominal null × 12 —— 本標本不走絕對排程(單次點擊,無節拍器),是「不適用」不是 0

⚠️ 量測窗的登記值與程式從未對齊phase2:138 登記「10 秒」,程式從頭就是 5000ms。 裁決紀錄已追記現行有效值 5000ms(phase2:1104-1114),但同段也判定: ts:156-158 的註解宣稱「登記值已在該檔修正紀錄更正」在寫下當時是不存在的事實 (依規矩 4 屬缺陷等級)—— 該更正是裁決紀錄事後補上的。本報告依現行有效值 5000ms。

資料讀取註記:本標本 12 筆的 runId 是 run-2~run-16、缺 run-1/5/9/13。 依驅動器流程(換標本與每輪結尾的「重跑」各會推進 run context,capture 在點「重跑」之前、 以 runIdBefore 對回 history —— tools/reproducibility.mjs:651-655),缺號是簿記不是丟樣本; 此為推斷,已列入待複核。

動工前登記的預期

登記出處(分層,後者修正前者、原文皆未動): 原始登記 phase2:126-176(200 台 / 50ms)→ 校準 phase2:361-377(1000 台)→ 重新設計 phase2:741-825(25ms / 批 100、樹狀梯度、補充判定欄; 「本次所有新登記預期值都是推導值,還沒量過一次」phase2:823-825)→ 檔尾追記 phase2:1285-1300, 1319-1332(判準重立,生效於下一輪,不回溯本 JSON)。 這一節不准回頭改去迎合實測。

實測

三輪,同一組凍結條件。離散度算在每輪的值上、判定不用 maxsrc/protocol.ts:309「抗離群。可重現性判定用這個,不用 max」—— 模板引作 :290,行號已漂)。相對離散度 = (max − min) ÷ median-of-three。

主指標 custom.droppedFramesPeak(每輪一值,records[mode=…,run=1..3].custom.droppedFramesPeak):

mode 三輪主指標 相對離散度 絕對全距 判定
broken 每批重建整表 281 / 277 / 275 2.2%(6÷277) 6 幀 ✅ 可重現(飽和臂,貼窗頂)
fixed-batch 批次化 + rAF 234 / 238 / 232 2.6%(6÷234) 6 幀 ✅ 可重現(同樣貼近窗頂)
fixed-granular 只改變動節點 14 / 18 / 18 22.2%(4÷18) 4 幀 ≤ 底線 5 幀 ✅ 可重現(兩判準皆過)
fixed-backpressure 背壓降頻 137 / 135 / 136 1.5%(2÷136) 2 幀 ✅ 可重現

⚠️ 同日上午另一 session 的 fixed-granular 是 85 / 59 / 84、31.0% 擦線判 unstable (phase2:932)。本輪 14 / 18 / 18 通過,靠的是絕對全距落在 5 幀底線內 —— 未飽和治療臂的絕對值跨 session 漂了 4~6 倍而工作量一格沒動(phase2:1085-1088), 可重現的是「本輪之內」,不是那個數字本身。

補充判定欄 custom.renderFrameGapMedianMs(每輪 median,直接引用不重算):

mode 三輪(ms) 相對離散度 備註
broken 200.1 / 208.3 / 200.0 4.1%(8.3÷200.1) 一格畫面約 12 個 vsync
fixed-batch 100.1 / 116.6 / 116.6 14.2%(16.5÷116.6) 全距恰為一個刷新週期 —— 幀距落在 vsync 網格上,這是量化不是抖動
fixed-granular 16.7 / 16.7 / 16.7 0.0% 貼死 vsync 底線
fixed-backpressure 100.0 / 100.0 / 100.0 0.0% 有渲染的幀仍是整表重建的幀

計數器(逐輪,records[mode=…,run=1..3].custom.*):

mode batchesReceived batchesRendered rendersSkipped renderRatio updatesApplied catchupClamped
broken 201 / 202 / 201 201 / 202 / 201 0 / 0 / 0 1.0 × 3 19136 / 19234 / 19136 0 × 3
fixed-batch 206 / 201 / 203 42 / 41 / 42 0 / 0 / 0 0.21 × 3 15707 / 15374 / 16009 0 × 3
fixed-granular 200 / 200 / 200 191 / 185 / 187 0 / 0 / 0 0.96 / 0.93 / 0.94 18876 / 18813 / 18840 0 × 3
fixed-backpressure 201 / 204 / 204 26 / 26 / 26 137 / 144 / 142 0.13 × 3 13618 / 13720 / 13753 0 × 3

三件事這張表直接證明:

  1. 推送率被凍住了。 名目 5000 ÷ 25 = 200 批,四臂實收 200~206 —— 照時鐘補發修掉了舊實作的塌陷(舊:病變臂五秒只收到 48 批,phase2:422-423)。 catchupClamped 12 筆全 0:無任何一輪觸發「補發被砍即作廢」條款(ts:140-147)。
  2. 背壓那段程式碼活著。 rendersSkipped 137 / 144 / 142(前兩輪設計下三輪恆 0、 被判死程式碼,phase2:483-493;重設計後上午 session 已量到 140 / 143 / 142, phase2:922-924,本輪再現)。
  3. 可證偽判準未倒。 fixed-granularrendersSkipped 三輪皆 0(ts:34-35 登記: 若 > 0 樹狀梯度理由重寫)—— 本輪不需要重寫。

兇手歸因

登記的兇手是 loafphase2:161),不是 INP 段 —— 本標本整輪只有「開始推送」那一下點擊 (totalInteractions 12 筆皆 1,stats.n 皆 1,isMaxNotP98: true), INP 16~24ms 落在 16ms 底線與 8ms 量化網格上,四臂等價、不做任何臂間排名。 下表照模板填,INP 三段只為完整性列出:

broken fixed-batch fixed-granular fixed-backpressure
inp(單次點擊,n=1) 24 / 24 / 16 ms 16 / 24 / 24 ms 24 / 16 / 16 ms 24 / 24 / 24 ms
inputDelay 1.2 ~ 2.0 ms 0.9 ~ 2.0 ms 1.2 ~ 1.8 ms 1.9 ~ 2.1 ms
processing 2.5 ~ 3.9 ms 1.7 ~ 9.2 ms 3.1 ~ 9.6 ms 1.7 ~ 3.5 ms
presentation 11.2 ~ 20.2 ms 11.7 ~ 21.4 ms 11.0 ~ 13.2 ms 18.5 ~ 20.2 ms
LoAF forcedStyleAndLayout 無 entryforcedSamples: []forcedMedian: null × 3) 同左 同左 同左
loafPickedBy specimenScriptDuration × 3 同左 同左 同左
specimenScript(備援選出的幀) 141.3 / 143.5 / 141.8 ms 40.2 / 22.6 / 19.4 ms 16.8 / 21.1 / 20.0 ms 35.1 / 20.3 / 17.0 ms
sourceFunctionName(forcedFn 欄) pushDeviceBatch × 3 onAnimationFrame × 3 空字串 × 3 空字串 × 3

⚠️ 12 筆的 loafPickedBy 全是 specimenScriptDuration —— 那些幀是備援路徑選出來的, forcedFn不構成「有強制版面幀」的證據(模板已填範例的同一條警語; forcedMedian: null 是「無 entry」,不是量到 0)。這個標本本來就不該有強制版面: 它的病是重建太頻繁,不是讀寫交錯。forcedPeak 一欄 records 實錄為 0。

真正的歸因鏈是掉幀與幀距,而且有解析式可對帳:標本內建 dropped ≈ 300 − 5000/幀距ts:94)。代入本輪 broken 的幀距 median 200.1ms ⇒ 300 − 25 ≈ 275,與三輪實測峰值 275281 相符 —— 主指標與幀距兩個獨立量測互相咬合。 LoAF 側的佐證方向一致:broken 被選出的幀 141143ms、三輪皆歸因到 pushDeviceBatchkeepNames 生效,補發迴圈裡的連續重建正是該函式做的事), 治療臂全部落在 17~40ms。

三輪兇手一致性:broken 的 forcedFn / loafPickedBy / specimenScript 三輪一致 ✅ —— 但「一致的是備援幀的歸因」,宣稱強度止於此。 登記的 loaf.blockingDuration 帶(100~400ms,phase2:155)在本份 records 無此欄位,未入帳,不評。

presentation 繼承 duration 的 8ms 量化,會落在 8ms 網格上。 對量級對照無影響,只在替兩個都已經很快的方案排名時咬人。 blockingDuration 是整幀的值,含外殼,規格上無法拆到單一 script; forcedStyleAndLayoutDuration 是逐 script 的,所以只有它能乾淨濾掉外殼貢獻。

治療梯度

梯度是樹狀不是鏈狀phase2:784-793src/specimens.ts:381-391): 治療二與治療二乙都接在治療一之後、互為兄弟,各自相對父臂只翻一個變因。 背壓臂不是「治療三」—— 叫治療三會讓讀者以為它疊在細粒度之上。

broken ──► 治療一 fixed-batch ──┬──► 治療二   fixed-granular    (翻:每一次渲染的成本)
                                └──► 治療二乙 fixed-backpressure(翻:渲染的次數)
治療 父臂(翻的變因) 主指標(峰值,三輪) 相對父臂的方向 代價
治療一 · 批次化 + rAF broken(渲染的時機) 234 / 238 / 232 主指標飽和,這一對表達不了差異(275281 vs 232238,兩臂都貼窗頂);補充欄有差:幀距 200.1 → 116.6ms(median-of-three,描述性約 1.7×,不作判準宣稱) 更新最多晚一幀上畫,而此臂一幀是 100~117ms;仍整表重建,掉幀仍飽和
治療二 · 細粒度 fixed-batch(每次渲染的成本) 14 / 18 / 18 峰值 234238 → 1418,幀距 116.6 → 16.7ms 貼 vsync;最後一次渲染的 JS 自報 0.71.6ms(custom.renderScriptMs,快照值、只含 JS)對照父臂 15.216.8ms 必須維護每列節點參照;整表重建過後參照全部失效,寫舊參照是「畫面不動、程式不報錯」的安靜失敗(ts:481-483, 826-829
治療二乙 · 背壓降頻 fixed-batch(渲染的次數) 137 / 135 / 136 渲染次數 4142 → 26,峰值 232238 → 135~137;renderRatio 0.21 → 0.13(方向與下一輪生效的新判準同向,此處不宣稱通過) 畫面在被跳過的幀顯示的不是最新值rendersSkipped 137 / 144 / 142 逐輪上報(登記風險 #2 的實價);仍整表重建,有渲染的幀仍是 100ms

治療臂與底線的關係:fixed-granular 的峰值全距(4 幀)落在 5 幀雜訊底線、 但三輪值 14~18 本身在底線之上 —— 依模板規則它不屬「落進底線不報比值」的情形; 真正禁止報比值判準的理由是檔尾追記的不回溯條款。描述性倍率照 phase2:1089 的先例 給一次、連警語一起給:broken/granular = 277 ÷ 18 ≈ 15.4×,這個倍率是機器速度的函數 (同日上午同一算式得 3.3×,phase2:1088-1089),跨 session 引用它是無效的。

三個必須寫明的結構事實:

無效或反向的治療:本輪無。前兩輪的「治療一 1.04× 零效果」(phase2:497-504) 是舊參數未進作用區間,重設計後翻案 —— 見下一節。

與登記的差異

處置順序固定:① 查環境 ② 換更精確的驅動方式 ③ 最後才動 protocol。 本輪 sweep.problems 為空、catchupClamped 全 0、節拍欄不適用(無節拍器),①②無事可查。

誠實揭露