Files
cjk-autospace/test/bench/bench-incremental.mjs
admin c5a503580c perf+feat: 掃描/佈局提速(中間/S4+S1)+增量恢復(Tier3 C1/C3/C2)
承 Tier1(S2+S3,已於 main 168e9e7)。本批為效能第二層+第三層,全程獨立複驗(差分/往返/
增量≡全量/進程隔離測速/真瀏覽器)+4 份真人撰寫真實文檔驗收,行為保真、提速真實。

掃描層(無狀態、保行為):
- 中間發現+S4:insidePill/isAvoided 之「逐文本節點沿祖先鏈」以 ancestorOrSelfPill/…Avoided
  迭代回填記憶化(每元素恰算一次、兄弟共用祖先鏈快取),O(文本×depth)→O(元素)。實測掃描層
  el.matches() 累計約降五成;深嵌套 render 進程隔離墻鐘 pre-Tier1 707ms→254ms(2.8×)。

佈局層(僅瀏覽器、正確性敏感):
- S1 gapTrim 對齊門:尾隨 margin 僅在右緣 flush 之對齊可見,唯 left/start(ltr) 之 ragged 右緣
  可整批跳過,省其 O(gaps) 次同步重排。實測非兩端對齊內容 gapTrim 數千 ms→數十 ms;justify/
  center/right/RTL 保留完整逐一定案、行為不變(真瀏覽器差分:justify trim 集指紋逐位相同)。

增量恢復(第三層,向下兼容 API):
- C1 持久化 Finder:跨 render 復用元素鍵快取(category/level/feature/祖先游走)。正確性靠
  失效:新增 finder.invalidate(el) 清整棵子樹快取(祖先屬性變則後代快取變脏),revert 自動失效
  其子樹(安全預設)。
- C3 rerender(root)=revert+render;局部恢復(實測編輯 400 段中一段 766×、matches 1441×)。
- C2 observe/disconnect:MutationObserver 自動增量——內容/分類屬性變化重處理最近塊、新增之塊
  直接 render;render/revert 期間暫停觀察,自身注入絕不自觸發。
- append 修復(IV-3 差分複驗發現、單測漏掉):向葉塊(帶 jz-orphan)追加子塊使其轉非葉,須撤
  容器陳舊 orphan——追加塊時若塊級父有 jz-orphan 直接子則改重處理該父。

測試 130→140(+S2 非葉塊、C1/C3、C2 觀察/失效/append、不自觸發)。基準設施:bench-incremental
(增量恢復)、bench-layout(版面層,headless Chrome)。dist 重建。
2026-07-12 02:20:20 +08:00

111 lines
3.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 增量恢復基準(jsdom)。度量「頁面局部變化」之恢復成本:全樹 revert+render(舊法)
// vs 局部 rerender(第三層 C3C2)。確定性 el.matches() 計數 計時中位數。
//
// 用法:node test/bench/bench-incremental.mjs [blocks] [iters]
import { JSDOM } from "jsdom";
import { createJuzhen } from "../../dist/juzhen.mjs";
const BLOCKS = Number(process.argv[2] || 400);
const ITERS = Number(process.argv[3] || 7);
function buildHtml(n)
{
let html = '<main lang="zh-Hant">';
for (let i = 0; i < n; i += 1)
{
html += '<p>第' + i + '段落文字<strong>粗體</strong>與 Latin word 混排,'
+ '句內點號、頓號;「引號」(括號)末尾實義字甲乙丙丁。</p>';
}
html += '</main>';
return html;
}
const HTML = buildHtml(BLOCKS);
function fresh()
{
const dom = new JSDOM("<!doctype html><html><body>" + HTML + "</body></html>");
globalThis.document = dom.window.document;
globalThis.NodeFilter = dom.window.NodeFilter;
globalThis.Node = dom.window.Node;
globalThis.window = dom.window;
globalThis.MutationObserver = dom.window.MutationObserver;
return dom;
}
function patchMatches(dom, counter)
{
let p = dom.window.document.querySelector("strong");
let proto = Object.getPrototypeOf(p);
while (proto && !Object.prototype.hasOwnProperty.call(proto, "matches"))
{
proto = Object.getPrototypeOf(proto);
}
const orig = proto.matches;
proto.matches = function (s) { counter.n += 1; return orig.call(this, s); };
return () => { proto.matches = orig; };
}
const median = (a) => a.slice().sort((x, y) => x - y)[Math.floor(a.length / 2)];
// ---- 場景:編輯「中間某一段」後恢復 ----
function editTarget(doc) { return doc.querySelectorAll("p")[Math.floor(BLOCKS / 2)]; }
// 全樹法:revert(root)+render(root)。
function fullCost(measureMatches)
{
const times = [];
let matches = 0;
for (let k = 0; k < ITERS; k += 1)
{
const dom = fresh();
const doc = dom.window.document;
const jz = createJuzhen();
jz.render(doc.body);
const p = editTarget(doc);
p.textContent = "改後第 K 段,新的實義字戊己庚辛。";
let restore, counter;
if (measureMatches && k === 0) { counter = { n: 0 }; restore = patchMatches(dom, counter); }
const t0 = performance.now();
jz.revert(doc.body);
jz.render(doc.body);
times.push(performance.now() - t0);
if (restore) { restore(); matches = counter.n; }
}
return { ms: median(times), matches };
}
// 增量法:rerender(改動塊)。
function incCost(measureMatches)
{
const times = [];
let matches = 0;
for (let k = 0; k < ITERS; k += 1)
{
const dom = fresh();
const doc = dom.window.document;
const jz = createJuzhen();
jz.render(doc.body);
const p = editTarget(doc);
p.textContent = "改後第 K 段,新的實義字戊己庚辛。";
let restore, counter;
if (measureMatches && k === 0) { counter = { n: 0 }; restore = patchMatches(dom, counter); }
const t0 = performance.now();
jz.rerender(p);
times.push(performance.now() - t0);
if (restore) { restore(); matches = counter.n; }
}
return { ms: median(times), matches };
}
const full = fullCost(true);
const inc = incCost(true);
console.log("增量恢復基準(" + BLOCKS + " 段;編輯中間一段後恢復):");
console.log(" 全樹 revert+render :", full.ms.toFixed(2), "ms |",
"el.matches()", full.matches);
console.log(" 局部 rerender(塊) :", inc.ms.toFixed(2), "ms |",
"el.matches()", inc.matches);
console.log(" 提速:時間 " + (full.ms / inc.ms).toFixed(1) + "× | matches "
+ (full.matches / Math.max(1, inc.matches)).toFixed(1) + "×");