病理報告 · 03 強制同步版面重排(Layout Thrashing)

實測數字只取自 docs/measurements/2026-07-26-reproducibility-4x-5.json (2026-07-26T05:52:29.671Z 的完整掃描,sweep.problems 為空陣列 = 無異常輪, sweep.consoleErrors 為空)。其他 session 的數字只以「另一 session」或「已作廢」 的身分出現,並逐一標明來源。跨 session 絕對值不可比(phase1:455-456phase2:944-945)。

03 強制同步版面重排(Layout Thrashing)

病症一句話:對 800 列的清單交替「讀 offsetHeight、寫 width」, 每次點擊觸發中位 829~964ms 的強制同步版面重排(本 session、4x 宣告節流, 三輪 forcedMedianrecords[mode=broken,run=1..3].forcedMedian); 讀寫分離之後,三輪皆無任何強制版面幀records[mode=fixed-batched,run=1..3].forcedMediannull)。

⚠️ 方法論警語(跨 session,各自標明來源):這句話必須標 session。 同一份程式一個字沒改,三次量測的 broken forced median 分別是 744.5 / 716.4 / 678.3(2026-07-25 session,docs/measurements/2026-07-25-reproducibility-4x.json, 經 phase2:1234-1253 裁決為現行有效組)、 1158 / 1274 / 1376(2026-07-26 上午 session,docs/measurements/2026-07-26-reproducibility-4x.json, 登記於 phase1:448-449phase2:941-942)、 828.85 / 953.75 / 964.45(本份 JSON)—— 最大與最小差約兩倍, 而每個 session 內部離散度都通過判準。不標條件的那個數字會被當成這個病的常數。

⚠️「一幀都不再產生」這種絕對句也要標 session:0725 是兩輪零幀、一輪有幀, 0726 上午三輪都有幀(0.1 / 0.1 / 0.4ms,出處:模板已填範例的治療梯度警語), 本份 JSON 三輪零強制版面幀。同一句話在三個 session 有三種真值。

凍結條件

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

CPU throttle 4x —— 宣告值records[*].cpuThrottlesweep.cpuThrottlingRate=4)。JS 偵測不到(protocol.ts:55「無法從 JS 偵測,只能由使用者宣告」),且本份 JSON 內部沒有 1x 臂,無法從資料檢核節流是否真的套用
驅動方式 CDP 機器驅動(sweep.driverInput.dispatchMouseEvent),非人手
viewport 800 × 600(src/specimens.ts:10FROZEN_VIEWPORT,經 :183 掛進本標本 meta;records 未記錄此欄)
refreshHz 60(records[*].refreshHz,六筆一致)
操作程序 click · 10 次 · 間隔 2500ms(src/specimens.ts:176-181)。節拍護欄:dispatchSpanNominalMs 25000,實測 dispatchSpanMs 24999.5~25002.4(六筆),偏差 ≤ 2.4ms —— 節拍確實交付。本標本走間隔迴圈而非絕對排程,名目值定義為 reps × I(tools/reproducibility.mjs:658-663 的註解)
buildId 0.1.0-ms191c71records[*].buildId,六筆一致)
protocolVersion 1(src/protocol.ts:8PROTOCOL_VERSION 常數)。⚠️ 本份 records 沒有這個欄位,此值取自程式碼,不是取自量測資料

⚠️ 方法論警語(來源:模板已填範例,教訓出自 0725/0726 兩個 session): 先前版本的報告曾有一段「4x 對 1x 的換算檢核:比值 4.0× ⇒ 節流確實生效」,已刪除。 它是跨 JSON 相除(1x 探針來自 phase1:237-243 的拋棄式量測、4x 來自另一 session 另一個 buildId), 而同一個算式在 0726 上午那輪給出 7.12× —— 兩個獨立浮動量的商不是檢核。 本份 JSON 同樣沒有 1x 臂,所以 throttle 這一格只能是宣告。 要真的檢核,得在同一次 session、同一份 JSON 裡跑一支 1x 臂。

動工前登記的預期

登記出處:docs/phase1-expected-results.md:142-210(標本 #3 段,動工前盲預測)。 這一節不准回頭改去迎合實測。 修正只出現在該檔「修正紀錄」(phase1:218-263)。

實測

三輪,同一組凍結條件。離散度算在每輪的 median 上,不是 max —— protocol.ts:309:「抗離群。可重現性判定用這個,不用 max」(模板引用的 :290 已漂移, 內容同一句)。相對離散度 =(三輪 max − 三輪 min)÷ 三輪的 median。

mode 三輪 forced median 相對離散度 絕對全距 判定
broken 交替讀寫 828.85 / 953.75 / 964.45 ms 14.2%(= 135.6 ÷ 953.75) 135.6ms ✅ 可重現
fixed-batched 讀寫分離 無 entry / 無 entry / 無 entry —(無 entry,無離散度可算) ✅ 可重現(三輪一致零幀)

出處:records[mode=broken,run=1..3].forcedMedianrecords[mode=fixed-batched,run=1..3].forcedMedian(皆 null)。

⚠️ null 是「沒有產生任何強制版面幀」,不是「量到 0」。 治療臂三輪 forcedSamples 皆為空陣列;第一輪的 forcedPeak 是 0、後兩輪是 null, 這個差別有意義,見兇手歸因的警語。 模板前言明文:「未量測」與「量到 0」是兩件事, 「標本 #3 治療版的全部結論就建立在這個區別上」。

INP median 另計(records[*].stats.median,每輪 10 次互動的 median; inp 欄本身是 max 不是 p98,六筆 isMaxNotP98true):

mode 三輪 INP median 相對離散度 判定
broken 892 / 1016 / 1032 ms 13.8%(= 140 ÷ 1016) ✅ 可重現
fixed-batched 48 / 48 / 48 ms 0.0% ✅ 可重現

臂間比值 21.2×(= 1016 ÷ 48,兩臂各取三輪 median 的中位,同一份 JSON 內部)。 治療臂 48ms 高於 INP 雜訊底線 16ms,比值可報。

兇手歸因

代表互動(INP 那一筆)的三段拆解,records[*].inputDelay / .processing / .presentation

broken fixed-batched
inputDelay 2.8 / 3.6 / 4.4 ms 2.1 / 2.0 / 1.8 ms
processing 1215.5 / 1725.3 / 1396.3 ms 10.5 / 9.0 / 10.1 ms
presentation 29.7 / 31.1 / 31.3 ms 43.4 / 69.0 / 44.1 ms
LoAF forcedStyleAndLayout(僅標本自己的 script) median 828.85 / 953.75 / 964.45 ms 無 entry / 無 entry / 無 entry
sourceFunctionName interleavedReadWrite(三輪) 見下方警語

三輪兇手段一致:是,processing。與 inputDelay 相差約 300~500 倍(兩個半數量級), 與登記值(phase1:189src/specimens.ts:168culprit: 'processing')一致。

broken 三輪 loafPickedBy 皆為 forcedStyleAndLayoutrecords[mode=broken,run=1..3].loafPickedBy),所以 forcedFn = interleavedReadWrite 是有效的兇手證據 —— 挑幀準則正是「強制版面貢獻最大的那一幀」。

⚠️ 治療臂的 forcedFn 不構成證據records[mode=fixed-batched,run=1].forcedFnbatchedReadThenWrite, 但同筆 loafPickedByspecimenScriptDuration —— 那一幀是「全部幀的強制版面貢獻皆為 0 時退回 script 時長最大」的備援路徑挑出來的(tools/reproducibility.mjs:440-442: 「不是前者時,forcedFn 與 specimenScript 只是『本輪最重的一幀』,不代表兇手」)。 它證明的只是治療版有一幀較長的動畫幀(該幀 specimenScript 7.8ms、 該輪 presentation 43.4ms),不是「有強制版面幀」。 第 2、3 輪連備援幀都沒有:forcedFnnullloafPickedBynone

同筆的 forcedPeak: 0 也因此不是「無 entry 卻寫 0」:capture 邏輯在無逐點擊樣本時 落回備援幀的 forced 值(tools/reproducibility.mjs:438),0 是那一幀實際量到的 強制版面貢獻。後兩輪無幀可落回,所以是 null。三個值三種語意,不可混讀。

⚠️ 另一 session 的對照(來源:模板已填範例引用的 0725 / 0726 上午資料): 0726 上午三輪治療臂都有強制版面幀(0.1 / 0.1 / 0.4ms,三輪 forcedFnbatchedReadThenWriteloafPickedByforcedStyleAndLayout)—— 那時它反而是正面證據keepNames 讓治療版殘餘的那一幀也歸因到它自己,不是歸給外殼, 正好回答 phase1:203-206 登記的風險 #3。本份 JSON 三輪零幀,這條證據線這輪不存在, 但風險 #3 的另一半仍有答案:治療臂沒有卡在任何非零底線上,外殼沒有混進來。

presentation 繼承 duration 的 8ms 量化(duration 六筆全是 8 的倍數: 1248 / 1760 / 1432 / 56 / 80 / 56)。對量級對照無影響, 只在替兩個都已經很快的方案排名時咬人。blockingDuration 是整幀的值,含外殼, 規格上無法拆到單一 script;forcedStyleAndLayoutDuration 是逐 script 的, 所以只有它能乾淨濾掉外殼貢獻(spec:1067 說本標本「數字最可信」的理由)。

治療梯度

治療 forced median 相對病變 代價
讀寫分離(先全讀進 Int32Array,再全寫;specimens/03-layout-thrashing.ts:180-200 無 entry / 無 entry / 無 entry 不報比值 —— 分母無 entry,除式無定義 需要 mount 時預配的 800 格暫存(:89,點擊當下不配置,避免把配置與 GC 成本算進治療臂,陷阱 #12);讀與寫不能再交錯寫在同一個迴圈裡

「716ms ÷ 0.1ms = 7163 倍」這種算式描述的是除數多小,不是治療多有效; 治療臂零 entry 時誠實的寫法就是「治療版沒有產生任何強制版面幀」。

輔助指標(同一份 JSON 內部):

layoutChecksum 兩臂皆 54888records[*].custom.layoutChecksum, 六筆全部相同)—— 讀寫分離沒有偷工,第 k 次點擊後的最終 DOM 完全相同 (自我驗證機制見 specimens/03-layout-thrashing.ts:210-229)。 這個檢核必須有:否則「治療版比較快」最無聊的解釋就是它少做了事。

⚠️ 數字搬運警語(來源:模板已填範例):checksum 曾被誤寫成 54168 —— 那是 phase1:258 那支 1x、四次點擊校準探針的值。 checksum 是最後一次點擊的高度總和,第 k 次讀到的高度取決於第 k−1 次寫入的相位, 四次與十次必然落在不同相位,兩個數字本來就不該相同。 本份 JSON 是十次點擊,六筆全部 54888。

與登記的差異

量測過程中修掉的儀器缺陷(不是標本缺陷;教訓出自另一 session,標明來源):

⚠️ 來源:0725 session(模板已填範例「與登記的差異」節)。 第一版拿 forced 峰值跨輪比,算出離散度 52%,判定不可重現; 改用每輪 median 之後是 9.2%。不可重現的是儀器。 本份 JSON 自己也重演了這一課:records[mode=broken,run=2]forcedPeak 1630.1ms 是該輪 median 953.75 的 1.7 倍,被 median 吸收 —— 判定用 median 不用 max 不是風格偏好,是這組資料反覆證明的規則。

⚠️ 無來源數字警語(來源:phase2:1234-1257 的裁決):0725 session 曾同時流傳兩組 「每輪 median」——745 / 716 / 678(9.2%)與 721 / 709 / 751(5.9%)。 逐項反推原始資料後,後者不是任何算式的結果,無來源,作廢phase2:1253)。 一組看起來很具體、還附了離散度的數字在檔案裡活了整輪都沒人問它從哪來 —— 所以本報告每個數字都附欄位路徑或行號。

誠實揭露