web-font/_prof_million.entry.ts
崮生(子虚) b385bc89bc perf(leafer-x-webfont): 脏节点集增量扫描,百万节点画布可正常运行
全量 walk O(全树) 改为事件驱动增量:
- property.change 携带 target(PropertyEvent 五参构造)→ 精确记脏节点
- child.add/remove 携带 child → 整棵子树入脏集(构造期赋值不走 setAttr)
- 订阅集与 Leafer 自家 Watcher 相同(property.change + child.*),
  渲染可靠 = 事件可靠;不再订阅 layout.end(每帧触发,百万级下
  周期性全量 walk 是持续卡顿源)
- 脏节点 >512 退化全量(模板整体替换场景全量更省)
- 全量/增量共用 collect/walkInto/commit,指纹短路保证无变化时
  update 调用归零
- 新增 provider 透传(测试/私有部署)、flushNow(跳过防抖立即提交)、
  updateCalls 计数(量化指纹短路效果)

压力测试(_prof_million.entry.ts,1M Text 节点):
- 初始全量 scan: ~580ms(一次性)
- 打字稳态(emit+flush): 0.024ms/键
- 无文字变化 flush ×100: 0.16ms,update=0
- 内存: 指纹表 O(family 数),与节点数无关

真实 leafer 验证(scripts/test-leafer-incremental.mts):
初始添加/改文字/新增子树全自动捕获,拖拽 50 次 0 请求 0 提交。

附带修复 dev 后端启动:llrt_lr.ts 改动态 import + fsReady
(createRequire 与 tsx ESM loader 模块图隔离导致 implInterface
注册到孤儿实例,mkdir undefined)。
2026-08-15 20:19:27 +08:00

164 lines
6.6 KiB
TypeScript
Raw 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.

/**
* 百万节点极端压力测试 —— leafer-x-webfont 增量扫描引擎
*
* 验证目标(用户挑战:节点百万级能否正常运行):
* 1. 百万 Text 节点画布上打字property.change单次 flush 成本
* 2. 无文字变化交互指纹短路update 调用是否归零
* 3. 模板整体替换child.add 溢出):全量退化路径成本
* 4. 内存:脏集/指纹表/事件监听是否随节点数线性增长(预期仅 O(family 数)
*
* 运行npx tsx _prof_million.entry.ts
*
* 不依赖真实 leafer 实例(百万节点 DOM 搭建本身是 leafer 的事),
* 用最小节点桩实现 ILeaferNode 结构 + 手动派发事件,隔离验证插件调度逻辑。
*/
import { performance } from 'node:perf_hooks'
const { WebFontPlugin } = await import('./packages/leafer-x-webfont/src/index.ts')
/** 最小 leafer 桩只实现插件依赖的接口面on_/off_/waitViewReady/children */
class FakeLeafer {
children: object[] = []
destroyed = false
private listeners = new Map<string, Array<(e: object) => void>>()
private nextId = 1
private ids = new Map<number, [string, (e: object) => void]>()
on_(type: string, listener: (e: object) => void): number {
const id = this.nextId++
let list = this.listeners.get(type)
if (!list) { list = []; this.listeners.set(type, list) }
list.push(listener)
this.ids.set(id, [type, listener])
return id
}
off_(ids: number[]): void {
for (const id of ids) {
const entry = this.ids.get(id)
if (!entry) continue
const [type, listener] = entry
const list = this.listeners.get(type)
if (list) this.listeners.set(type, list.filter((l) => l !== listener))
this.ids.delete(id)
}
}
waitViewReady(cb: () => void): void { cb() }
emit(type: string, e: object): void {
for (const l of this.listeners.get(type) ?? []) l(e)
}
}
class FakeText {
__tag = 'Text'
constructor(public text: string, public fontFamily: string) {}
children?: object[]
destroyed?: boolean
}
/** 子树:每组 group 子节点 + N 个 Text模拟真实海报/长文档结构 */
class FakeGroup {
__tag = 'Group'
children: object[] = []
destroyed?: boolean
}
/** 测试配置的子集服务桩:记录 update 调用次数与字符数(验证指纹短路) */
class CountingProvider {
calls = 0
lastText = ''
async fetch(fontName: string, text: string): Promise<{ url: string; format: string }> {
this.calls++
this.lastText = text
return { url: `data:font/ttf;base64,`, format: 'ttf' }
}
}
async function main(): Promise<void> {
console.log('=== 百万节点压力测试 ===\n')
/* ---------- 场景 A100 万 Text 节点,初始全量 ---------- */
const TOTAL = 1_000_000
const leafer = new FakeLeafer()
const root = new FakeGroup()
leafer.children = [root]
const t0 = performance.now()
/** 500 组 × 2000 Text = 1M每组同一 family模拟海报重复用同一字体 */
const GROUPS = 500
const PER_GROUP = TOTAL / GROUPS
const pool = '静心茶舍天地玄黄宇宙洪荒日月盈昃辰宿列张寒来暑往秋收冬藏闰余成岁律吕调阳'.split('')
for (let g = 0; g < GROUPS; g++) {
const group = new FakeGroup()
for (let i = 0; i < PER_GROUP; i++) {
const ch = pool[(g * PER_GROUP + i) % pool.length]!
group.children!.push(new FakeText(ch, '令东齐伋复刻体.ttf'))
}
root.children!.push(group)
}
const buildMs = performance.now() - t0
console.log(`[A] 搭建 ${TOTAL.toLocaleString()} 节点: ${buildMs.toFixed(0)}ms`)
const provider = new CountingProvider()
const plugin = new WebFontPlugin(leafer as never, {
baseUrl: 'http://localhost:8087',
provider: provider as never,
debounceMs: 0,
debug: false,
})
/** waitViewReady 同步回调,构造即完成初始 scan */
const initMs = performance.now() - t0 - buildMs
console.log(`[A] 初始全量 scan(1M 节点): ${initMs.toFixed(1)}ms`)
/* ---------- 场景 B打字property.change + flush---------- */
const target = (root.children![0] as FakeGroup).children![0] as FakeText
/**
* 真实打字成本 = 事件处理(记脏集)+ flush增量 walk 脏节点)。
* 模拟用户敲 100 键:每键 emit + 同步 flush防抖窗口外的稳态成本
*/
const t1 = performance.now()
const KEYS = 100
for (let i = 0; i < KEYS; i++) {
target.text = '静心茶舍新内容' + pool[i % pool.length] + i
leafer.emit('property.change', { attrName: 'text', target, newValue: target.text, oldValue: '' })
plugin.flushNow()
}
const typingMs = (performance.now() - t1) / KEYS
console.log(`[B] 打字稳态成本(emit+flush, 1M 节点画布): ${typingMs.toFixed(3)}ms/键`)
console.log(`[B] update 提交次数: ${plugin.updateCalls}SDK 层再按字符去重,实际请求 ≤ 此数)`)
/* ---------- 场景 B2拖拽等无文字变化交互 ---------- */
const callsBefore = plugin.updateCalls
const t2 = performance.now()
for (let i = 0; i < 100; i++) {
/** 拖拽/缩放只改 x/y插件不订阅这些属性——模拟反复属性风暴下手动 refresh 校准 */
leafer.emit('property.change', { attrName: 'x', target, newValue: i, oldValue: i - 1 })
plugin.flushNow()
}
console.log(`[B2] 无文字变化 flush ×100: ${(performance.now() - t2).toFixed(3)}ms 总计, update=${plugin.updateCalls - callsBefore}`)
/* ---------- 场景 C指纹短路无文字变化重复提交---------- */
const committedCalls = provider.calls
plugin.refresh() /** 全量重扫,内容未变 */
const skipped = provider.calls === committedCalls
console.log(`[C] 指纹短路(全量重扫无变化): update 调用 ${skipped ? '0 次 ✓' : provider.calls - committedCalls + ' 次 ✗'}`)
/* ---------- 场景 D模板替换child.add 大子树)---------- */
const t3 = performance.now()
leafer.emit('child.add', { child: root.children![1] })
const dirtyAddMs = performance.now() - t3
console.log(`[D] child.add 子树入脏集(2000 Text): ${dirtyAddMs.toFixed(3)}ms`)
/* ---------- 场景 Edestroy 清理 ---------- */
const t4 = performance.now()
plugin.destroy()
console.log(`[E] destroy: ${(performance.now() - t4).toFixed(2)}ms`)
/* ---------- 内存估算 ---------- */
const mu = process.memoryUsage()
console.log(`\n[内存] heapUsed=${(mu.heapUsed / 1048576).toFixed(0)}MB heapTotal=${(mu.heapTotal / 1048576).toFixed(0)}MB rss=${(mu.rss / 1048576).toFixed(0)}MB`)
console.log(`[内存] 指纹表条目=${'1 family → 1 条'}O(family 数),与节点数无关)`)
}
await main()