From ca166ceb0a0ca859b49f96a246d9028306591f51 Mon Sep 17 00:00:00 2001 From: wing Date: Thu, 13 Nov 2025 23:25:40 +0800 Subject: [PATCH] =?UTF-8?q?test:=20=E6=B7=BB=E5=8A=A0=20Phase=204=20&=205?= =?UTF-8?q?=20=E6=80=A7=E8=83=BD=E6=B5=8B=E8=AF=95=E5=B7=A5=E5=85=B7=20+?= =?UTF-8?q?=20=E6=9B=B4=E6=96=B0=E8=B7=AF=E7=BA=BF=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 新增测试工具 ### 1. Phase 4.4.3 - 性能监控工具(已集成生产) - js/chatbot/utils/performance-monitor.js - tests/performance/test-performance-monitor.html - 功能:FPS 监控、渲染时间追踪、内存监控、长任务检测 - 状态:✅ 已集成到 history_detail.html ### 2. Phase 4.1.3 - 滚动性能测试 - tests/performance/test-scroll-performance.html - 对比:Intersection Observer vs RAF 节流 - 结论:Intersection Observer 快 5.7% ### 3. Phase 4.2+ - KaTeX 缓存测试 - tests/performance/test-katex-cache.html - 验证:LRU 缓存性能提升 97%(50ms → 0.5ms) ### 4. Phase 5.1 - Web Worker 评估测试 - tests/performance/test-markdown-worker.html - tests/performance/test-worker-comprehensive.html - 结论:Worker 比主线程慢 60.5%(不推荐使用) - 详见:PHASE5_WORKER_CONCLUSION.md ## 路线图更新 - PHASE4-5-ROADMAP.md - 标记已完成任务 - 更新性能指标 - 添加测试工具文档 ## 注意 js/workers/ 目录(未提交): - Worker 方案已废弃,测试证明性能更差 - 保留在工作区仅供参考 - 不会集成到生产环境 --- PHASE4-5-ROADMAP.md | 2 +- js/chatbot/utils/performance-monitor.js | 402 ++++++++++ tests/performance/test-katex-cache.html | 436 ++++++++++ tests/performance/test-markdown-worker.html | 571 ++++++++++++++ .../performance/test-performance-monitor.html | 422 ++++++++++ .../performance/test-scroll-performance.html | 745 ++++++++++++++++++ .../test-worker-comprehensive.html | 593 ++++++++++++++ 7 files changed, 3170 insertions(+), 1 deletion(-) create mode 100644 js/chatbot/utils/performance-monitor.js create mode 100644 tests/performance/test-katex-cache.html create mode 100644 tests/performance/test-markdown-worker.html create mode 100644 tests/performance/test-performance-monitor.html create mode 100644 tests/performance/test-scroll-performance.html create mode 100644 tests/performance/test-worker-comprehensive.html diff --git a/PHASE4-5-ROADMAP.md b/PHASE4-5-ROADMAP.md index 83f3e5ba..8606d692 100644 --- a/PHASE4-5-ROADMAP.md +++ b/PHASE4-5-ROADMAP.md @@ -90,7 +90,7 @@ observer.observe(lastCell); #### 实施计划 - [x] **4.1.1** 实现 Intersection Observer 方案(已接入聊天表格渐变阴影) - [x] **4.1.2** 添加降级方案(RAF 节流,含 feature detection) -- [ ] **4.1.3** 性能测试对比(100 个表格场景,待补充系统测试) +- [x] **4.1.3** 性能测试对比(100 个表格场景,验证 IO 方案性能提升 5.7%) --- diff --git a/js/chatbot/utils/performance-monitor.js b/js/chatbot/utils/performance-monitor.js new file mode 100644 index 00000000..d6e24bdf --- /dev/null +++ b/js/chatbot/utils/performance-monitor.js @@ -0,0 +1,402 @@ +/** + * Paper Burner - 本地性能监控工具 + * Phase 4.4.3: 性能数据收集(仅本地,不上报) + * + * 使用方法: + * - 启动监控:PerfMonitor.start() + * - 停止监控:PerfMonitor.stop() + * - 查看实时数据:PerfMonitor.getStats() + * - 导出数据:PerfMonitor.export() + * - 清空数据:PerfMonitor.clear() + */ + +window.PerfMonitor = (function() { + // 性能数据存储(仅内存,不持久化) + var metrics = { + renderTime: [], // 渲染耗时记录 + fps: [], // FPS 记录 + longTasks: [], // 长任务记录 + memoryUsage: [], // 内存使用记录 + scrollEvents: [], // 滚动事件密度 + domNodes: [] // DOM 节点数量 + }; + + // 监控状态 + var state = { + isRunning: false, + startTime: null, + fpsAnimationId: null, + memoryIntervalId: null, + longTaskObserver: null + }; + + // 配置 + var config = { + maxSamples: 1000, // 每个指标最多保留样本数 + memoryCheckInterval: 5000, // 内存检测间隔 (ms) + fpsCheckInterval: 1000, // FPS 统计间隔 (ms) + longTaskThreshold: 50 // 长任务阈值 (ms) + }; + + /** + * FPS 监控 + */ + function measureFPS() { + var lastTime = performance.now(); + var frames = 0; + var lastRecordTime = lastTime; + + function loop() { + if (!state.isRunning) return; + + frames++; + var now = performance.now(); + + // 每秒统计一次 FPS + if (now >= lastRecordTime + config.fpsCheckInterval) { + var fps = Math.round((frames * 1000) / (now - lastRecordTime)); + + metrics.fps.push({ + fps: fps, + timestamp: Date.now() + }); + + // 限制样本数量 + if (metrics.fps.length > config.maxSamples) { + metrics.fps.shift(); + } + + frames = 0; + lastRecordTime = now; + } + + state.fpsAnimationId = requestAnimationFrame(loop); + } + + state.fpsAnimationId = requestAnimationFrame(loop); + } + + /** + * 内存监控(仅 Chrome 支持) + */ + function measureMemory() { + if (!performance.memory) return; + + try { + metrics.memoryUsage.push({ + used: performance.memory.usedJSHeapSize, + total: performance.memory.totalJSHeapSize, + limit: performance.memory.jsHeapSizeLimit, + timestamp: Date.now() + }); + + // 限制样本数量 + if (metrics.memoryUsage.length > config.maxSamples) { + metrics.memoryUsage.shift(); + } + } catch (e) { + console.warn('[PerfMonitor] Memory API error:', e); + } + } + + /** + * 长任务监控(需要 PerformanceObserver 支持) + */ + function observeLongTasks() { + if (!window.PerformanceObserver) return; + + try { + state.longTaskObserver = new PerformanceObserver(function(list) { + var entries = list.getEntries(); + for (var i = 0; i < entries.length; i++) { + var entry = entries[i]; + if (entry.duration >= config.longTaskThreshold) { + metrics.longTasks.push({ + duration: entry.duration, + startTime: entry.startTime, + timestamp: Date.now() + }); + + // 限制样本数量 + if (metrics.longTasks.length > config.maxSamples) { + metrics.longTasks.shift(); + } + } + } + }); + + // 监控 longtask(需要浏览器支持) + state.longTaskObserver.observe({ entryTypes: ['longtask'] }); + } catch (e) { + // longtask 类型可能不支持,降级为 measure + try { + state.longTaskObserver.observe({ entryTypes: ['measure'] }); + } catch (e2) { + console.warn('[PerfMonitor] PerformanceObserver not supported'); + } + } + } + + /** + * 记录渲染耗时 + */ + function recordRenderTime(duration, context) { + if (!state.isRunning) return; + + metrics.renderTime.push({ + duration: duration, + context: context || 'unknown', + timestamp: Date.now() + }); + + // 限制样本数量 + if (metrics.renderTime.length > config.maxSamples) { + metrics.renderTime.shift(); + } + } + + /** + * 记录 DOM 节点数量 + */ + function recordDOMNodes() { + if (!state.isRunning) return; + + var chatBody = document.querySelector('.chat-body'); + if (!chatBody) return; + + metrics.domNodes.push({ + total: document.querySelectorAll('*').length, + chatMessages: chatBody.querySelectorAll('.message').length, + timestamp: Date.now() + }); + + // 限制样本数量 + if (metrics.domNodes.length > config.maxSamples) { + metrics.domNodes.shift(); + } + } + + /** + * 计算统计数据 + */ + function calculateStats(arr, key) { + if (!arr || arr.length === 0) { + return { count: 0, min: 0, max: 0, avg: 0, p50: 0, p95: 0, p99: 0 }; + } + + var values = key + ? arr.map(function(item) { return item[key]; }) + : arr; + + var sorted = values.slice().sort(function(a, b) { return a - b; }); + var sum = sorted.reduce(function(acc, val) { return acc + val; }, 0); + + return { + count: sorted.length, + min: sorted[0], + max: sorted[sorted.length - 1], + avg: sum / sorted.length, + p50: sorted[Math.floor(sorted.length * 0.5)], + p95: sorted[Math.floor(sorted.length * 0.95)], + p99: sorted[Math.floor(sorted.length * 0.99)] + }; + } + + /** + * 获取性能统计数据 + */ + function getStats() { + var now = Date.now(); + var duration = state.startTime ? (now - state.startTime) / 1000 : 0; + + return { + session: { + isRunning: state.isRunning, + duration: duration.toFixed(1) + 's', + startTime: state.startTime ? new Date(state.startTime).toISOString() : null + }, + device: { + tier: window.PerformanceExperiment?.deviceTier || 'unknown', + cores: navigator.hardwareConcurrency || 'unknown', + memory: navigator.deviceMemory ? navigator.deviceMemory + 'GB' : 'unknown', + userAgent: navigator.userAgent + }, + rendering: { + samples: metrics.renderTime.length, + stats: calculateStats(metrics.renderTime, 'duration') + }, + fps: { + samples: metrics.fps.length, + stats: calculateStats(metrics.fps, 'fps'), + current: metrics.fps.length > 0 ? metrics.fps[metrics.fps.length - 1].fps : null + }, + memory: performance.memory ? { + samples: metrics.memoryUsage.length, + current: metrics.memoryUsage.length > 0 ? { + used: (metrics.memoryUsage[metrics.memoryUsage.length - 1].used / 1024 / 1024).toFixed(1) + 'MB', + total: (metrics.memoryUsage[metrics.memoryUsage.length - 1].total / 1024 / 1024).toFixed(1) + 'MB' + } : null, + peak: metrics.memoryUsage.length > 0 ? { + used: (Math.max.apply(null, metrics.memoryUsage.map(function(m) { return m.used; })) / 1024 / 1024).toFixed(1) + 'MB' + } : null + } : { available: false }, + longTasks: { + samples: metrics.longTasks.length, + stats: calculateStats(metrics.longTasks, 'duration'), + recent: metrics.longTasks.slice(-5) + }, + dom: { + samples: metrics.domNodes.length, + current: metrics.domNodes.length > 0 ? metrics.domNodes[metrics.domNodes.length - 1] : null + } + }; + } + + /** + * 导出原始数据(JSON 格式) + */ + function exportData() { + var data = { + exportTime: new Date().toISOString(), + session: getStats().session, + device: getStats().device, + metrics: { + renderTime: metrics.renderTime, + fps: metrics.fps, + longTasks: metrics.longTasks, + memoryUsage: metrics.memoryUsage, + domNodes: metrics.domNodes + } + }; + + // 输出到控制台 + console.log('[PerfMonitor] 导出数据:'); + console.log(JSON.stringify(data, null, 2)); + + // 返回数据,方便复制 + return data; + } + + /** + * 启动监控 + */ + function start() { + if (state.isRunning) { + console.warn('[PerfMonitor] 监控已在运行中'); + return; + } + + console.log('[PerfMonitor] 启动性能监控...'); + state.isRunning = true; + state.startTime = Date.now(); + + // 启动各项监控 + measureFPS(); + observeLongTasks(); + + // 定期采集内存和 DOM 信息 + state.memoryIntervalId = setInterval(function() { + measureMemory(); + recordDOMNodes(); + }, config.memoryCheckInterval); + + console.log('[PerfMonitor] 监控已启动。使用 PerfMonitor.getStats() 查看实时数据'); + } + + /** + * 停止监控 + */ + function stop() { + if (!state.isRunning) { + console.warn('[PerfMonitor] 监控未在运行'); + return; + } + + console.log('[PerfMonitor] 停止性能监控...'); + state.isRunning = false; + + // 停止各项监控 + if (state.fpsAnimationId) { + cancelAnimationFrame(state.fpsAnimationId); + state.fpsAnimationId = null; + } + + if (state.memoryIntervalId) { + clearInterval(state.memoryIntervalId); + state.memoryIntervalId = null; + } + + if (state.longTaskObserver) { + state.longTaskObserver.disconnect(); + state.longTaskObserver = null; + } + + // 显示统计摘要 + var stats = getStats(); + console.log('[PerfMonitor] 监控已停止。统计摘要:'); + console.table({ + '监控时长': stats.session.duration, + '平均渲染耗时': stats.rendering.stats.avg.toFixed(1) + 'ms', + 'P95渲染耗时': stats.rendering.stats.p95.toFixed(1) + 'ms', + '平均FPS': stats.fps.stats.avg.toFixed(0), + '最低FPS': stats.fps.stats.min, + '长任务数量': stats.longTasks.samples + }); + } + + /** + * 清空数据 + */ + function clear() { + metrics.renderTime = []; + metrics.fps = []; + metrics.longTasks = []; + metrics.memoryUsage = []; + metrics.scrollEvents = []; + metrics.domNodes = []; + console.log('[PerfMonitor] 数据已清空'); + } + + // 公开 API + return { + start: start, + stop: stop, + clear: clear, + getStats: getStats, + export: exportData, + recordRender: recordRenderTime, + + // 用于调试 + _metrics: metrics, + _state: state, + _config: config + }; +})(); + +// 自动集成到现有的渲染流程 +(function() { + // 拦截 ChatbotRenderState,自动记录渲染耗时 + if (window.ChatbotRenderState) { + var originalRenderState = window.ChatbotRenderState; + + // 包装 lastRenderDuration 的更新 + Object.defineProperty(window.ChatbotRenderState, 'lastRenderDuration', { + get: function() { + return this._lastRenderDuration || 0; + }, + set: function(value) { + this._lastRenderDuration = value; + // 自动记录到性能监控 + window.PerfMonitor.recordRender(value, 'chatbot_render'); + }, + configurable: true + }); + } +})(); + +console.log('[PerfMonitor] 性能监控工具已加载。使用方法:'); +console.log(' PerfMonitor.start() - 启动监控'); +console.log(' PerfMonitor.stop() - 停止监控'); +console.log(' PerfMonitor.getStats() - 查看统计'); +console.log(' PerfMonitor.export() - 导出数据'); +console.log(' PerfMonitor.clear() - 清空数据'); diff --git a/tests/performance/test-katex-cache.html b/tests/performance/test-katex-cache.html new file mode 100644 index 00000000..32737534 --- /dev/null +++ b/tests/performance/test-katex-cache.html @@ -0,0 +1,436 @@ + + + + + + KaTeX 缓存性能测试 - Phase 4.2+ + + + + +
+
+

🚀 Phase 4.2+ KaTeX 公式缓存性能测试

+

测试缓存对公式渲染性能的影响(针对 4.6s 阻塞问题)

+ +
+

⚠️ 测试场景

+

问题: 打开充满公式的 chatbot 时,KaTeX 渲染阻塞主线程 4.6 秒

+

优化: 使用 LRU 缓存避免重复公式的重新渲染

+

预期收益: 缓存命中时渲染时间减少 99%(从 50ms 降至 0.5ms)

+
+ +
+ + + + +
+
+ +
+ +
+

📝 测试公式预览

+
+
+ +
+

📝 测试日志

+
等待测试...
+
+
+ + + + + + + + diff --git a/tests/performance/test-markdown-worker.html b/tests/performance/test-markdown-worker.html new file mode 100644 index 00000000..6f315dc0 --- /dev/null +++ b/tests/performance/test-markdown-worker.html @@ -0,0 +1,571 @@ + + + + + + Markdown Worker 性能测试 - Phase 5.1.2 + + + +
+
+

🚀 Phase 5.1.2 - Markdown Worker 性能测试

+

对比 Web Worker 多线程解析 vs. 主线程解析的性能差异

+ +
+ 测试说明: +
    +
  • 测试不同长度的 Markdown 文本解析性能
  • +
  • 对比 Worker 池和主线程的性能指标
  • +
  • 验证流式更新场景的流畅度
  • +
  • 查看控制台获取详细日志
  • +
+
+ +
+ + + + + +
+
+ +
+
+

🔵 Web Worker 模式

+
状态:等待测试
+
+
+
+
+
...
+
+
+
+ +
+

🟠 主线程模式

+
状态:等待测试
+
+
+
+
+
...
+
+
+
+
+ +
+

📊 对比结果

+
+

运行测试后将显示对比结果...

+
+
+ +
+

📝 测试日志

+
+等待测试... +
+
+
+ + + + + + + + + + diff --git a/tests/performance/test-performance-monitor.html b/tests/performance/test-performance-monitor.html new file mode 100644 index 00000000..efa7a0e6 --- /dev/null +++ b/tests/performance/test-performance-monitor.html @@ -0,0 +1,422 @@ + + + + + + 性能监控工具测试 - Phase 4.4.3 + + + +
+

🔍 Phase 4.4.3 - 性能监控工具测试

+ +
+

📋 使用说明

+
    +
  • 点击"启动监控"开始收集性能数据
  • +
  • 运行测试负载模拟真实渲染场景
  • +
  • 点击"查看统计"查看实时性能指标
  • +
  • 点击"导出数据"将数据输出到控制台(可复制保存)
  • +
  • 所有数据仅在本地浏览器中,不会上传到任何服务器
  • +
+

控制台命令:

+
    +
  • PerfMonitor.start() - 启动监控
  • +
  • PerfMonitor.stop() - 停止监控
  • +
  • PerfMonitor.getStats() - 查看统计
  • +
  • PerfMonitor.export() - 导出数据
  • +
+
+ +
+ ⏸️ 监控状态:未启动 +
+ +
+ + + + + +
+
+ +
+

🔬 测试负载

+

运行这些测试来模拟真实场景并收集性能数据:

+ +
+ + + + +
+ +
+
+ 点击上方按钮运行测试负载 +
+
+
+ +
+

📈 性能数据

+
+等待数据... + +点击"启动监控"后,运行一些测试负载,然后点击"查看统计"查看性能指标。 +
+
+ + + + + + + + diff --git a/tests/performance/test-scroll-performance.html b/tests/performance/test-scroll-performance.html new file mode 100644 index 00000000..b6146291 --- /dev/null +++ b/tests/performance/test-scroll-performance.html @@ -0,0 +1,745 @@ + + + + + + 滚动性能测试 - Phase 4.1.3 + + + +
+

🎯 Phase 4.1.3 - 滚动性能测试对比

+

对比 Intersection Observer vs. requestAnimationFrame 滚动监听性能(100个表格场景)

+ +
+ 测试说明: +
    +
  • 生成100个可横向滚动的表格
  • +
  • 对比两种滚动监听方案的性能指标
  • +
  • 测试指标:事件触发次数、平均响应时间、FPS、内存使用
  • +
  • 建议:运行测试时打开控制台查看详细日志
  • +
+
+ +
+ + + + +
+
+ +
+
+

🔵 Intersection Observer (推荐)

+
就绪
+
+
+
+
+
等待测试...
+
+
+ +
+

🟠 requestAnimationFrame

+
就绪
+
+
+
+
+
等待测试...
+
+
+
+ +
+

📊 测试结果

+
+

运行测试后将显示对比结果...

+
+
+ +
+ + + + diff --git a/tests/performance/test-worker-comprehensive.html b/tests/performance/test-worker-comprehensive.html new file mode 100644 index 00000000..a21dc3f9 --- /dev/null +++ b/tests/performance/test-worker-comprehensive.html @@ -0,0 +1,593 @@ + + + + + + Worker 综合性能测试 - Phase 5.1.3 + + + +
+
+

🔬 Phase 5.1.3 - Worker 综合性能测试

+

测试超长文本、流式更新等真实场景下的 Worker 性能

+ +
+

⚠️ 重要发现

+

测试结论: 对于 marked.js 这种高性能解析器,只有在文本非常长(>50k字符)时,Worker 才能体现优势。

+
    +
  • 短文本(<10k): Worker 通信开销 > 计算开销,性能反而更差
  • +
  • 中文本(10k-50k): 性能接近,差异不明显
  • +
  • 长文本(>50k): Worker 开始体现优势,主线程不被阻塞
  • +
  • 流式更新: 频繁的小更新不适合 Worker,应该批量处理
  • +
+

建议配置: minTextLength 设为 10000-20000 字符

+
+ +
+ + + + + +
+
+ +
+

📊 性能对比图表

+
+

运行测试后将显示图表...

+
+
+ +
+ +
+

📝 测试日志

+
等待测试...
+
+
+ + + + + + + +