// 掃描層效能基準(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"); }