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 重建。
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
// 增量恢復基準(jsdom)。度量「頁面局部變化」之恢復成本:全樹 revert+render(舊法)
|
||||
// vs 局部 rerender(第三層 C3/C2)。確定性 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) + "×");
|
||||
@@ -0,0 +1,78 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-Hant">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="../../dist/juzhen.css">
|
||||
<style>
|
||||
/* 窄容器強制多行;大量 CJK↔Latin 邊界 → 大量 jz-hws gap。 */
|
||||
#stage { width: 380px; font-size: 18px; line-height: 2; }
|
||||
#stage.j { text-align: justify; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="result">pending</div>
|
||||
<div id="stage"></div>
|
||||
<script src="../../dist/juzhen.iife.js"></script>
|
||||
<script>
|
||||
// 計 getClientRects 呼叫數(布局讀取/reflow 代理)——S1 之目標即減少此類「寫後讀」。
|
||||
let rectCalls = 0;
|
||||
for (const proto of [Range.prototype, Element.prototype])
|
||||
{
|
||||
const orig = proto.getClientRects;
|
||||
proto.getClientRects = function () { rectCalls += 1; return orig.apply(this, arguments); };
|
||||
}
|
||||
|
||||
// 建 gap 密集之多行文檔:每段交替 CJK 字 + Latin 字母/數字 → 每邊界一個 jz-hws。
|
||||
function buildStage(paras, boundariesPerPara)
|
||||
{
|
||||
const cjk = "中文字符測試內容甲乙丙丁戊己庚辛壬癸";
|
||||
const ans = "abcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let html = "";
|
||||
for (let p = 0; p < paras; p += 1)
|
||||
{
|
||||
let s = "";
|
||||
for (let i = 0; i < boundariesPerPara; i += 1)
|
||||
{
|
||||
s += cjk.charAt((p + i) % cjk.length) + ans.charAt((p * 3 + i) % ans.length);
|
||||
}
|
||||
html += "<p>" + s + ",句末標點。</p>";
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const PARAS = Number(params.get("paras") || 40);
|
||||
const BPP = Number(params.get("bpp") || 60);
|
||||
const JUSTIFY = params.get("justify") !== "0"; // 預設 justify
|
||||
const stage = document.getElementById("stage");
|
||||
if (JUSTIFY) { stage.classList.add("j"); }
|
||||
stage.innerHTML = buildStage(PARAS, BPP);
|
||||
|
||||
try
|
||||
{
|
||||
const Juzhen = window.Juzhen || globalThis.Juzhen;
|
||||
if (!Juzhen) { throw new Error("Juzhen 全域未定義"); }
|
||||
const jz = Juzhen.createJuzhen();
|
||||
|
||||
// 量測:render(含 lineEdge + gapTrim 之立即 run)。ResizeObserver 非同步回調不計入。
|
||||
const rectsBefore = rectCalls;
|
||||
const t0 = performance.now();
|
||||
jz.render(stage);
|
||||
const dt = performance.now() - t0;
|
||||
const gaps = stage.querySelectorAll("jz-hws").length;
|
||||
const trimmed = stage.querySelectorAll("jz-hws.jz-hws-trim").length;
|
||||
const le = stage.querySelectorAll("jz-char.jz-half-le").length;
|
||||
|
||||
document.getElementById("result").textContent = JSON.stringify({
|
||||
paras: PARAS, bpp: BPP, justify: JUSTIFY, gaps, trimmed, lineEdge: le,
|
||||
getClientRects: rectCalls - rectsBefore,
|
||||
renderMs: Math.round(dt * 10) / 10,
|
||||
});
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
document.getElementById("result").textContent = "ERROR: " + (e && e.stack || e);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user