diff --git a/test/bench/bench-scan.mjs b/test/bench/bench-scan.mjs new file mode 100644 index 0000000..065818d --- /dev/null +++ b/test/bench/bench-scan.mjs @@ -0,0 +1,146 @@ +// 掃描層效能基準(jsdom)。度量 render 之 CPU 掃描成本——確定性計數(el.matches() 呼叫數) +// +壁鐘中位數。jsdom 無版面(getClientRects 回 0),故 lineEdge/gapTrim 不執行、此基準僅覆 +// 蓋 charify/jiya/jinze/spacing/orphan/longWord 之掃描;版面層(S1)另見 bench-layout。 +// +// 用法:node test/bench/bench-scan.mjs [scale] [iters] +// scale:段落規模乘數(預設 60);iters:計時重複次數取中位數(預設 9)。 +// +// 建立兩種壓力結構:寬(多兄弟段落,測線性)+深(巢狀塊測 orphan S2、巢狀行內測祖先游走/ +// insidePill)。計數用正確之 Element.prototype.matches 補丁(非 HTMLBodyElement.prototype)。 + +import { JSDOM } from "jsdom"; +import { createJuzhen } from "../../dist/juzhen.mjs"; + +const scale = Number(process.argv[2] || 60); +const iters = Number(process.argv[3] || 9); + +/** 一段混合行內:透明(strong/em/span)、pill(code)、isolate(mark)、opaque(button)、 + * 標點(句內/句末/括號引號)、中西混排、長英文詞、末尾實義字。 */ +function para(i) +{ + return '

第' + i + '段開頭文字' + + '粗體巢狀斜體內容與' + + '雙層透明codeSample片段' + + '及標記字' + + '含 Latin word 與 supercalifragilistic 長詞,' + + '句內點號、頓號;冒號:「引號」(括號)測試。末尾實義字甲乙丙丁。

'; +} + +/** 深巢狀塊(測 orphan S2 之逐塊 querySelector 掃子樹):div 套 depth 層,內含葉段落。 */ +function deepBlock(depth, inner) +{ + let s = inner; + for (let d = 0; d < depth; d += 1) { s = '
' + s + '
'; } + return s; +} + +/** 深巢狀透明行內(測 insidePill/祖先游走:每文本節點沿祖先鏈走 depth 層)。 */ +function deepInline(depth, i) +{ + let s = '這是深層巢狀行內的文字內容甲乙丙丁。' + + 'codeX中間再一段實義字戊己庚辛。'; + for (let d = 0; d < depth; d += 1) { s = '' + s + ''; } + return '

段' + i + s + '

'; +} + +function buildHtml() +{ + let html = '
'; + // 寬:scale 段普通段落。 + for (let i = 0; i < scale; i += 1) { html += para(i); } + // 深塊:若干「深 8 層 div 包一葉段落」的容器(放大 S2 的 O(Σ塊子樹))。 + for (let i = 0; i < Math.max(4, scale / 6); i += 1) + { + html += deepBlock(8, para(1000 + i)); + } + // 深行內:若干「深 10 層 span」的段落(放大 insidePill/祖先游走)。 + for (let i = 0; i < Math.max(4, scale / 6); i += 1) + { + html += deepInline(10, 2000 + i); + } + html += '
'; + return html; +} + +const HTML = buildHtml(); + +function freshDom() +{ + const dom = new JSDOM("" + HTML + ""); + globalThis.document = dom.window.document; + globalThis.NodeFilter = dom.window.NodeFilter; + globalThis.Node = dom.window.Node; + globalThis.window = dom.window; + return dom; +} + +/** 找到 matches 所在原型(Element.prototype),返回 { proto, restore }。 */ +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; }; +} + +function median(arr) +{ + const s = arr.slice().sort((a, b) => a - b); + return s[Math.floor(s.length / 2)]; +} + +// ---- 計數(單次,確定性)---- +{ + const dom = freshDom(); + const counter = { n: 0 }; + const restore = patchMatches(dom, counter); + const jz = createJuzhen(); + jz.render(dom.window.document.body); + restore(); + const doc = dom.window.document; + console.log("結構規模:"); + console.log(" 文本節點掃描後 jz-char:", doc.querySelectorAll("jz-char").length); + console.log(" jz-hws:", doc.querySelectorAll("jz-hws").length, + "| jz-jinze:", doc.querySelectorAll("jz-jinze").length, + "| jz-orphan:", doc.querySelectorAll("jz-orphan").length); + console.log(" DOM 元素總數:", doc.querySelectorAll("*").length); + console.log("計數(確定性):"); + console.log(" render 期間 el.matches() 呼叫數:", counter.n); +} + +// ---- 計時(render 中位數)---- +{ + const times = []; + for (let k = 0; k < iters; k += 1) + { + const dom = freshDom(); + const jz = createJuzhen(); + const t0 = performance.now(); + jz.render(dom.window.document.body); + times.push(performance.now() - t0); + } + console.log("計時(" + iters + " 次中位數):"); + console.log(" render:", median(times).toFixed(2), "ms (min", + Math.min(...times).toFixed(2) + ")"); +} + +// ---- render→revert 往返計時(第三層增量對照之基線)---- +{ + const times = []; + for (let k = 0; k < iters; k += 1) + { + const dom = freshDom(); + const jz = createJuzhen(); + jz.render(dom.window.document.body); + const t0 = performance.now(); + jz.revert(dom.window.document.body); + jz.render(dom.window.document.body); + times.push(performance.now() - t0); + } + console.log(" revert+render 全樹往返:", median(times).toFixed(2), "ms"); +}