test(bench): 掃描層效能基準(jsdom,確定性 el.matches() 計數+計時)
Tier 2 基準設施:test/bench/bench-scan.mjs。建寬(多兄弟段落)+深(巢狀塊測 orphan、巢狀行內 測祖先游走)壓力結構,度量 render 之 el.matches() 呼叫數(確定性)與壁鐘中位數,供逐層提速實測。 正確補 Element.prototype.matches(非 HTMLBodyElement.prototype)。jsdom 無版面故僅覆蓋掃描層; 版面層(S1)另備 headless Chrome 基準。
This commit is contained in:
@@ -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 '<p>第' + i + '段開頭文字'
|
||||
+ '<strong>粗體<em>巢狀斜體</em>內容</strong>與'
|
||||
+ '<span><span>雙層透明<code>codeSample</code>片段</span></span>'
|
||||
+ '及<mark>標記字</mark>又<button>按鈕文字</button>'
|
||||
+ '含 Latin word 與 supercalifragilistic 長詞,'
|
||||
+ '句內點號、頓號;冒號:「引號」(括號)測試。末尾實義字甲乙丙丁。</p>';
|
||||
}
|
||||
|
||||
/** 深巢狀塊(測 orphan S2 之逐塊 querySelector 掃子樹):div 套 depth 層,內含葉段落。 */
|
||||
function deepBlock(depth, inner)
|
||||
{
|
||||
let s = inner;
|
||||
for (let d = 0; d < depth; d += 1) { s = '<div>' + s + '</div>'; }
|
||||
return s;
|
||||
}
|
||||
|
||||
/** 深巢狀透明行內(測 insidePill/祖先游走:每文本節點沿祖先鏈走 depth 層)。 */
|
||||
function deepInline(depth, i)
|
||||
{
|
||||
let s = '這是深層巢狀行內的文字內容甲乙丙丁。'
|
||||
+ '<code>codeX</code>中間再一段實義字戊己庚辛。';
|
||||
for (let d = 0; d < depth; d += 1) { s = '<span>' + s + '</span>'; }
|
||||
return '<p>段' + i + s + '</p>';
|
||||
}
|
||||
|
||||
function buildHtml()
|
||||
{
|
||||
let html = '<section lang="zh-Hant">';
|
||||
// 寬: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 += '</section>';
|
||||
return html;
|
||||
}
|
||||
|
||||
const HTML = buildHtml();
|
||||
|
||||
function freshDom()
|
||||
{
|
||||
const dom = new JSDOM("<!doctype html><html><head></head><body>" + HTML + "</body></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");
|
||||
}
|
||||
Reference in New Issue
Block a user