mirror of
https://github.com/Feather-2/paper-burner-x.git
synced 2026-10-29 19:19:48 +08:00
test: 添加 Phase 4 & 5 性能测试工具 + 更新路线图
## 新增测试工具
### 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 方案已废弃,测试证明性能更差
- 保留在工作区仅供参考
- 不会集成到生产环境
This commit is contained in:
+1
-1
@@ -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%)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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() - 清空数据');
|
||||
@@ -0,0 +1,436 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>KaTeX 缓存性能测试 - Phase 4.2+</title>
|
||||
<link rel="stylesheet" href="https://gcore.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
h1 { margin-bottom: 12px; color: #333; }
|
||||
|
||||
.warning-box {
|
||||
background: #fff3cd;
|
||||
border: 2px solid #ffc107;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.success-box {
|
||||
background: #d4edda;
|
||||
border: 2px solid #28a745;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.success-box h3 {
|
||||
color: #155724;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin: 16px 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary { background: #4CAF50; color: white; }
|
||||
.btn-primary:hover { background: #45a049; }
|
||||
.btn-secondary { background: #2196F3; color: white; }
|
||||
.btn-secondary:hover { background: #0b7dda; }
|
||||
|
||||
.comparison {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.test-card {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.test-card h3 {
|
||||
margin-bottom: 12px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
margin: 8px 0;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.metric.highlight {
|
||||
background: #e8f5e9;
|
||||
border-left: 4px solid #4CAF50;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.metric.warning {
|
||||
background: #ffebee;
|
||||
border-left: 4px solid #f44336;
|
||||
}
|
||||
|
||||
.huge-improvement {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #2e7d32;
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.formulas-preview {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.console-log {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.console-log .success { color: #81c784; }
|
||||
.console-log .warning { color: #ffb74d; }
|
||||
.console-log .info { color: #4fc3f7; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🚀 Phase 4.2+ KaTeX 公式缓存性能测试</h1>
|
||||
<p>测试缓存对公式渲染性能的影响(针对 4.6s 阻塞问题)</p>
|
||||
|
||||
<div class="warning-box">
|
||||
<h3>⚠️ 测试场景</h3>
|
||||
<p><strong>问题:</strong> 打开充满公式的 chatbot 时,KaTeX 渲染阻塞主线程 4.6 秒</p>
|
||||
<p><strong>优化:</strong> 使用 LRU 缓存避免重复公式的重新渲染</p>
|
||||
<p><strong>预期收益:</strong> 缓存命中时渲染时间减少 99%(从 50ms 降至 0.5ms)</p>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button class="btn-primary" onclick="runTest(50)">测试 50 个公式</button>
|
||||
<button class="btn-primary" onclick="runTest(100)">测试 100 个公式</button>
|
||||
<button class="btn-secondary" onclick="viewCacheStats()">查看缓存统计</button>
|
||||
<button class="btn-secondary" onclick="clearCache()">清空缓存</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="comparison" id="results-area"></div>
|
||||
|
||||
<div class="formulas-preview">
|
||||
<h2>📝 测试公式预览</h2>
|
||||
<div id="formulas-preview"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-card">
|
||||
<h2>📝 测试日志</h2>
|
||||
<div id="console-log" class="console-log">等待测试...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载依赖 -->
|
||||
<script src="https://gcore.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
||||
<script src="../../js/chatbot/utils/katex-cache.js"></script>
|
||||
|
||||
<script>
|
||||
// 测试公式集合
|
||||
const TEST_FORMULAS = [
|
||||
'E = mc^2',
|
||||
'\\int_{0}^{\\infty} e^{-x^2} dx = \\frac{\\sqrt{\\pi}}{2}',
|
||||
'\\sum_{n=1}^{\\infty} \\frac{1}{n^2} = \\frac{\\pi^2}{6}',
|
||||
'x = \\frac{-b \\pm \\sqrt{b^2 - 4ac}}{2a}',
|
||||
'f(x) = \\frac{1}{\\sigma\\sqrt{2\\pi}} e^{-\\frac{(x-\\mu)^2}{2\\sigma^2}}',
|
||||
'\\nabla \\times \\vec{E} = -\\frac{\\partial \\vec{B}}{\\partial t}',
|
||||
'\\lim_{n \\to \\infty} \\left(1 + \\frac{1}{n}\\right)^n = e',
|
||||
'\\begin{pmatrix} a & b \\\\ c & d \\end{pmatrix} \\begin{pmatrix} x \\\\ y \\end{pmatrix} = \\begin{pmatrix} ax+by \\\\ cx+dy \\end{pmatrix}',
|
||||
'P(A|B) = \\frac{P(B|A)P(A)}{P(B)}',
|
||||
'\\oint_{\\partial S} \\vec{E} \\cdot d\\vec{l} = -\\frac{d}{dt}\\int_S \\vec{B} \\cdot d\\vec{A}'
|
||||
];
|
||||
|
||||
function log(message, type = 'info') {
|
||||
const logEl = document.getElementById('console-log');
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
logEl.innerHTML += `<span class="${type}">[${timestamp}] ${message}</span>\n`;
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
console.log(`[${type.toUpperCase()}] ${message}`);
|
||||
}
|
||||
|
||||
// 测试无缓存渲染
|
||||
async function testWithoutCache(formulas, rounds = 3) {
|
||||
log('开始测试:无缓存渲染', 'info');
|
||||
|
||||
const times = [];
|
||||
|
||||
for (let round = 0; round < rounds; round++) {
|
||||
const roundStart = performance.now();
|
||||
|
||||
formulas.forEach(tex => {
|
||||
katex.renderToString(tex, {
|
||||
displayMode: true,
|
||||
output: 'html',
|
||||
strict: 'ignore',
|
||||
throwOnError: false
|
||||
});
|
||||
});
|
||||
|
||||
times.push(performance.now() - roundStart);
|
||||
}
|
||||
|
||||
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
|
||||
log(`无缓存完成: 平均 ${avgTime.toFixed(0)} ms`, 'success');
|
||||
|
||||
return {
|
||||
avgTime,
|
||||
minTime: Math.min(...times),
|
||||
maxTime: Math.max(...times),
|
||||
totalRenders: formulas.length * rounds
|
||||
};
|
||||
}
|
||||
|
||||
// 测试缓存渲染
|
||||
async function testWithCache(formulas, rounds = 3) {
|
||||
log('开始测试:缓存渲染', 'info');
|
||||
|
||||
// 第一轮:缓存未命中
|
||||
const firstRoundStart = performance.now();
|
||||
formulas.forEach(tex => {
|
||||
window.renderKatexCached(tex, {
|
||||
displayMode: true,
|
||||
output: 'html',
|
||||
strict: 'ignore',
|
||||
throwOnError: false
|
||||
});
|
||||
});
|
||||
const firstRoundTime = performance.now() - firstRoundStart;
|
||||
log(`第一轮(缓存未命中): ${firstRoundTime.toFixed(0)} ms`, 'info');
|
||||
|
||||
// 后续轮次:缓存命中
|
||||
const times = [];
|
||||
for (let round = 0; round < rounds - 1; round++) {
|
||||
const roundStart = performance.now();
|
||||
|
||||
formulas.forEach(tex => {
|
||||
window.renderKatexCached(tex, {
|
||||
displayMode: true,
|
||||
output: 'html',
|
||||
strict: 'ignore',
|
||||
throwOnError: false
|
||||
});
|
||||
});
|
||||
|
||||
times.push(performance.now() - roundStart);
|
||||
}
|
||||
|
||||
const avgCachedTime = times.reduce((a, b) => a + b, 0) / times.length;
|
||||
log(`后续轮次(缓存命中): 平均 ${avgCachedTime.toFixed(2)} ms`, 'success');
|
||||
|
||||
return {
|
||||
firstRoundTime,
|
||||
avgCachedTime,
|
||||
minTime: Math.min(...times),
|
||||
maxTime: Math.max(...times),
|
||||
totalRenders: formulas.length * rounds
|
||||
};
|
||||
}
|
||||
|
||||
// 运行测试
|
||||
async function runTest(formulaCount) {
|
||||
log('========================================', 'info');
|
||||
log(`开始测试:${formulaCount} 个公式,每个渲染 3 次`, 'info');
|
||||
log('========================================', 'info');
|
||||
|
||||
// 准备测试公式(重复使用基础公式)
|
||||
const formulas = [];
|
||||
for (let i = 0; i < formulaCount; i++) {
|
||||
formulas.push(TEST_FORMULAS[i % TEST_FORMULAS.length]);
|
||||
}
|
||||
|
||||
// 清空缓存
|
||||
window.katexCache.clear();
|
||||
|
||||
// 测试无缓存
|
||||
const noCacheResult = await testWithoutCache(formulas, 3);
|
||||
|
||||
// 等待一下
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
|
||||
// 测试缓存
|
||||
const cacheResult = await testWithCache(formulas, 3);
|
||||
|
||||
// 显示结果
|
||||
displayResults(noCacheResult, cacheResult, formulaCount);
|
||||
|
||||
// 显示公式预览
|
||||
displayFormulas(formulas.slice(0, 10));
|
||||
|
||||
log('========================================', 'success');
|
||||
log('测试完成!', 'success');
|
||||
log('========================================', 'success');
|
||||
}
|
||||
|
||||
// 显示结果
|
||||
function displayResults(noCacheResult, cacheResult, formulaCount) {
|
||||
const improvement = ((noCacheResult.avgTime - cacheResult.avgCachedTime) / noCacheResult.avgTime * 100);
|
||||
|
||||
const html = `
|
||||
<div class="test-card">
|
||||
<h3>⏱️ 无缓存渲染</h3>
|
||||
<div class="metric warning">
|
||||
<span>平均耗时:</span>
|
||||
<span><strong>${noCacheResult.avgTime.toFixed(0)} ms</strong></span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span>最小耗时:</span>
|
||||
<span>${noCacheResult.minTime.toFixed(0)} ms</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span>最大耗时:</span>
|
||||
<span>${noCacheResult.maxTime.toFixed(0)} ms</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span>总渲染数:</span>
|
||||
<span>${noCacheResult.totalRenders} 次</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-card">
|
||||
<h3>⚡ 缓存渲染</h3>
|
||||
<div class="metric highlight">
|
||||
<span>缓存命中耗时:</span>
|
||||
<span><strong>${cacheResult.avgCachedTime.toFixed(2)} ms</strong></span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<span>首次未命中:</span>
|
||||
<span>${cacheResult.firstRoundTime.toFixed(0)} ms</span>
|
||||
</div>
|
||||
<div class="metric highlight">
|
||||
<span>性能提升:</span>
|
||||
<span><strong>↑ ${improvement.toFixed(1)}%</strong></span>
|
||||
</div>
|
||||
<div class="metric highlight">
|
||||
<span>速度倍数:</span>
|
||||
<span><strong>${(noCacheResult.avgTime / cacheResult.avgCachedTime).toFixed(0)}x 更快</strong></span>
|
||||
</div>
|
||||
|
||||
<div class="huge-improvement">
|
||||
🚀 ${improvement.toFixed(0)}% 性能提升!
|
||||
</div>
|
||||
|
||||
<div class="success-box">
|
||||
<h3>✅ 实际应用场景</h3>
|
||||
<p>如果你的 chatbot 有 ${formulaCount} 个公式:</p>
|
||||
<ul style="margin: 8px 0 0 20px; line-height: 1.8;">
|
||||
<li><strong>无缓存:</strong> 每次打开需要 ${noCacheResult.avgTime.toFixed(0)} ms</li>
|
||||
<li><strong>有缓存:</strong> 第二次打开只需 <strong>${cacheResult.avgCachedTime.toFixed(2)} ms</strong></li>
|
||||
<li><strong>节省时间:</strong> <strong>${(noCacheResult.avgTime - cacheResult.avgCachedTime).toFixed(0)} ms</strong></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('results-area').innerHTML = html;
|
||||
}
|
||||
|
||||
// 显示公式预览
|
||||
function displayFormulas(formulas) {
|
||||
const preview = document.getElementById('formulas-preview');
|
||||
preview.innerHTML = '<p style="margin-bottom: 12px; color: #666;">测试公式示例(仅显示前 10 个):</p>';
|
||||
|
||||
formulas.forEach((tex, index) => {
|
||||
const div = document.createElement('div');
|
||||
div.style.margin = '12px 0';
|
||||
div.style.padding = '12px';
|
||||
div.style.background = '#f8f9fa';
|
||||
div.style.borderRadius = '4px';
|
||||
|
||||
try {
|
||||
const html = window.renderKatexCached(tex, {
|
||||
displayMode: true,
|
||||
output: 'html',
|
||||
strict: 'ignore',
|
||||
throwOnError: false
|
||||
});
|
||||
div.innerHTML = `<div style="color: #666; font-size: 12px; margin-bottom: 4px;">公式 ${index + 1}</div>${html}`;
|
||||
} catch (e) {
|
||||
div.textContent = `公式 ${index + 1}: 渲染失败`;
|
||||
}
|
||||
|
||||
preview.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
// 查看缓存统计
|
||||
function viewCacheStats() {
|
||||
window.getKatexCacheStats();
|
||||
}
|
||||
|
||||
// 清空缓存
|
||||
function clearCache() {
|
||||
window.katexCache.clear();
|
||||
log('缓存已清空', 'warning');
|
||||
alert('缓存已清空!');
|
||||
}
|
||||
|
||||
// 页面加载完成
|
||||
window.addEventListener('load', function() {
|
||||
log('页面加载完成', 'success');
|
||||
log('KaTeX 缓存系统已就绪', 'info');
|
||||
log('点击上方按钮开始测试', 'info');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,571 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Markdown Worker 性能测试 - Phase 5.1.2</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 12px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: #e3f2fd;
|
||||
border: 1px solid #2196F3;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin: 16px 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #0b7dda;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background: #e68900;
|
||||
}
|
||||
|
||||
.test-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.test-panel {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.test-panel h2 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 12px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px;
|
||||
margin: 6px 0;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.metric.highlight {
|
||||
background: #e8f5e9;
|
||||
border-left: 4px solid #4CAF50;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.progress {
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 6px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: #4CAF50;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.results {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.comparison-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.comparison-table th,
|
||||
.comparison-table td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.comparison-table th {
|
||||
background: #f5f5f5;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.winner {
|
||||
background: #e8f5e9 !important;
|
||||
font-weight: bold;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.console-output {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.console-output .info { color: #4fc3f7; }
|
||||
.console-output .success { color: #81c784; }
|
||||
.console-output .warning { color: #ffb74d; }
|
||||
.console-output .error { color: #e57373; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🚀 Phase 5.1.2 - Markdown Worker 性能测试</h1>
|
||||
<p>对比 Web Worker 多线程解析 vs. 主线程解析的性能差异</p>
|
||||
|
||||
<div class="info-box">
|
||||
<strong>测试说明:</strong>
|
||||
<ul style="margin: 8px 0 0 20px;">
|
||||
<li>测试不同长度的 Markdown 文本解析性能</li>
|
||||
<li>对比 Worker 池和主线程的性能指标</li>
|
||||
<li>验证流式更新场景的流畅度</li>
|
||||
<li>查看控制台获取详细日志</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button class="btn-primary" onclick="runAllTests()">🔬 运行所有测试</button>
|
||||
<button class="btn-secondary" onclick="runShortTextTest()">测试短文本 (500字)</button>
|
||||
<button class="btn-secondary" onclick="runMediumTextTest()">测试中文本 (5000字)</button>
|
||||
<button class="btn-secondary" onclick="runLongTextTest()">测试长文本 (20000字)</button>
|
||||
<button class="btn-warning" onclick="viewStats()">📊 查看统计</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-grid">
|
||||
<div class="test-panel">
|
||||
<h2>🔵 Web Worker 模式</h2>
|
||||
<div id="worker-status" class="metric">状态:等待测试</div>
|
||||
<div class="progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="worker-progress" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="progress-text" id="worker-progress-text">...</div>
|
||||
</div>
|
||||
<div id="worker-metrics"></div>
|
||||
</div>
|
||||
|
||||
<div class="test-panel">
|
||||
<h2>🟠 主线程模式</h2>
|
||||
<div id="main-status" class="metric">状态:等待测试</div>
|
||||
<div class="progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="main-progress" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="progress-text" id="main-progress-text">...</div>
|
||||
</div>
|
||||
<div id="main-metrics"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="results">
|
||||
<h2>📊 对比结果</h2>
|
||||
<div id="comparison-results">
|
||||
<p style="color: #666;">运行测试后将显示对比结果...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="results">
|
||||
<h2>📝 测试日志</h2>
|
||||
<div id="console-log" class="console-output">
|
||||
等待测试...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载依赖 -->
|
||||
<script src="https://gcore.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script src="../../js/chatbot/config/performance-config.js"></script>
|
||||
<script src="../../js/chatbot/utils/performance-monitor.js"></script>
|
||||
<script src="../../js/workers/markdown-worker-pool.js"></script>
|
||||
|
||||
<script>
|
||||
// 测试数据生成
|
||||
function generateMarkdown(length) {
|
||||
const paragraphs = Math.ceil(length / 100);
|
||||
let md = '# 性能测试文档\n\n';
|
||||
|
||||
for (let i = 0; i < paragraphs; i++) {
|
||||
md += `## 章节 ${i + 1}\n\n`;
|
||||
md += `这是第 ${i + 1} 个段落。`.repeat(5) + '\n\n';
|
||||
|
||||
if (i % 3 === 0) {
|
||||
md += '```javascript\n';
|
||||
md += 'function test() {\n';
|
||||
md += ' return "Hello World";\n';
|
||||
md += '}\n';
|
||||
md += '```\n\n';
|
||||
}
|
||||
|
||||
if (i % 5 === 0) {
|
||||
md += '- 列表项 1\n';
|
||||
md += '- 列表项 2\n';
|
||||
md += '- 列表项 3\n\n';
|
||||
}
|
||||
}
|
||||
|
||||
return md;
|
||||
}
|
||||
|
||||
// 测试结果存储
|
||||
const testResults = {
|
||||
worker: [],
|
||||
mainThread: []
|
||||
};
|
||||
|
||||
// 日志函数
|
||||
function log(message, type = 'info') {
|
||||
const logEl = document.getElementById('console-log');
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const className = type;
|
||||
logEl.innerHTML += `<span class="${className}">[${timestamp}] ${message}</span>\n`;
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
console.log(`[${type.toUpperCase()}] ${message}`);
|
||||
}
|
||||
|
||||
// 更新进度
|
||||
function updateProgress(mode, percent, text) {
|
||||
document.getElementById(`${mode}-progress`).style.width = percent + '%';
|
||||
document.getElementById(`${mode}-progress-text`).textContent = text;
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
function updateStatus(mode, text) {
|
||||
document.getElementById(`${mode}-status`).textContent = text;
|
||||
}
|
||||
|
||||
// 更新指标
|
||||
function updateMetrics(mode, metrics) {
|
||||
const metricsEl = document.getElementById(`${mode}-metrics`);
|
||||
metricsEl.innerHTML = `
|
||||
<div class="metric"><span>平均耗时:</span><span>${metrics.avgTime.toFixed(2)} ms</span></div>
|
||||
<div class="metric"><span>最小耗时:</span><span>${metrics.minTime.toFixed(2)} ms</span></div>
|
||||
<div class="metric"><span>最大耗时:</span><span>${metrics.maxTime.toFixed(2)} ms</span></div>
|
||||
<div class="metric"><span>总耗时:</span><span>${metrics.totalTime.toFixed(2)} ms</span></div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 测试 Worker 模式
|
||||
async function testWorkerMode(markdown, rounds = 10) {
|
||||
log('开始测试 Worker 模式...', 'info');
|
||||
updateStatus('worker', '状态:测试中...');
|
||||
|
||||
const times = [];
|
||||
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
const start = performance.now();
|
||||
|
||||
try {
|
||||
await window.markdownWorkerPool.parse(markdown);
|
||||
const duration = performance.now() - start;
|
||||
times.push(duration);
|
||||
|
||||
updateProgress('worker', ((i + 1) / rounds) * 100, `${i + 1}/${rounds}`);
|
||||
} catch (error) {
|
||||
log(`Worker 测试失败: ${error.message}`, 'error');
|
||||
times.push(NaN);
|
||||
}
|
||||
}
|
||||
|
||||
const validTimes = times.filter(t => !isNaN(t));
|
||||
const metrics = {
|
||||
avgTime: validTimes.reduce((a, b) => a + b, 0) / validTimes.length,
|
||||
minTime: Math.min(...validTimes),
|
||||
maxTime: Math.max(...validTimes),
|
||||
totalTime: validTimes.reduce((a, b) => a + b, 0)
|
||||
};
|
||||
|
||||
updateStatus('worker', '状态:完成 ✅');
|
||||
updateMetrics('worker', metrics);
|
||||
log(`Worker 模式完成: 平均 ${metrics.avgTime.toFixed(2)} ms`, 'success');
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
// 测试主线程模式
|
||||
async function testMainThreadMode(markdown, rounds = 10) {
|
||||
log('开始测试主线程模式...', 'info');
|
||||
updateStatus('main', '状态:测试中...');
|
||||
|
||||
const times = [];
|
||||
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
const start = performance.now();
|
||||
|
||||
try {
|
||||
marked.parse(markdown);
|
||||
const duration = performance.now() - start;
|
||||
times.push(duration);
|
||||
|
||||
updateProgress('main', ((i + 1) / rounds) * 100, `${i + 1}/${rounds}`);
|
||||
} catch (error) {
|
||||
log(`主线程测试失败: ${error.message}`, 'error');
|
||||
times.push(NaN);
|
||||
}
|
||||
}
|
||||
|
||||
const validTimes = times.filter(t => !isNaN(t));
|
||||
const metrics = {
|
||||
avgTime: validTimes.reduce((a, b) => a + b, 0) / validTimes.length,
|
||||
minTime: Math.min(...validTimes),
|
||||
maxTime: Math.max(...validTimes),
|
||||
totalTime: validTimes.reduce((a, b) => a + b, 0)
|
||||
};
|
||||
|
||||
updateStatus('main', '状态:完成 ✅');
|
||||
updateMetrics('main', metrics);
|
||||
log(`主线程模式完成: 平均 ${metrics.avgTime.toFixed(2)} ms`, 'success');
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
// 显示对比结果
|
||||
function displayComparison(workerMetrics, mainMetrics, testName) {
|
||||
const improvement = ((mainMetrics.avgTime - workerMetrics.avgTime) / mainMetrics.avgTime * 100);
|
||||
const faster = workerMetrics.avgTime < mainMetrics.avgTime ? 'worker' : 'main';
|
||||
|
||||
const html = `
|
||||
<h3>${testName} - 性能对比</h3>
|
||||
<table class="comparison-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>指标</th>
|
||||
<th>Web Worker</th>
|
||||
<th>主线程</th>
|
||||
<th>改善</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>平均耗时</td>
|
||||
<td class="${faster === 'worker' ? 'winner' : ''}">${workerMetrics.avgTime.toFixed(2)} ms</td>
|
||||
<td class="${faster === 'main' ? 'winner' : ''}">${mainMetrics.avgTime.toFixed(2)} ms</td>
|
||||
<td>${improvement > 0 ? '↓' : '↑'} ${Math.abs(improvement).toFixed(1)}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>最小耗时</td>
|
||||
<td>${workerMetrics.minTime.toFixed(2)} ms</td>
|
||||
<td>${mainMetrics.minTime.toFixed(2)} ms</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>最大耗时</td>
|
||||
<td>${workerMetrics.maxTime.toFixed(2)} ms</td>
|
||||
<td>${mainMetrics.maxTime.toFixed(2)} ms</td>
|
||||
<td>-</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>总耗时</td>
|
||||
<td>${workerMetrics.totalTime.toFixed(0)} ms</td>
|
||||
<td>${mainMetrics.totalTime.toFixed(0)} ms</td>
|
||||
<td>${improvement > 0 ? '↓' : '↑'} ${Math.abs(improvement).toFixed(1)}%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="info-box" style="margin-top: 16px; background: ${improvement > 0 ? '#e8f5e9' : '#fff3cd'};">
|
||||
<strong>结论:</strong>
|
||||
${improvement > 0
|
||||
? `✅ <strong>Web Worker 性能提升 ${improvement.toFixed(1)}%</strong>,主线程阻塞时间显著减少`
|
||||
: `⚠️ 对于短文本,通信开销可能超过计算开销,Worker 性能反而稍差`
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('comparison-results').innerHTML = html;
|
||||
}
|
||||
|
||||
// 单独测试函数
|
||||
async function runShortTextTest() {
|
||||
document.getElementById('console-log').innerHTML = '';
|
||||
log('========================================', 'info');
|
||||
log('开始测试:短文本 (500字)', 'info');
|
||||
log('========================================', 'info');
|
||||
|
||||
const markdown = generateMarkdown(500);
|
||||
log(`生成了 ${markdown.length} 字符的测试文本`, 'info');
|
||||
|
||||
const workerMetrics = await testWorkerMode(markdown, 20);
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const mainMetrics = await testMainThreadMode(markdown, 20);
|
||||
|
||||
displayComparison(workerMetrics, mainMetrics, '短文本测试');
|
||||
}
|
||||
|
||||
async function runMediumTextTest() {
|
||||
document.getElementById('console-log').innerHTML = '';
|
||||
log('========================================', 'info');
|
||||
log('开始测试:中文本 (5000字)', 'info');
|
||||
log('========================================', 'info');
|
||||
|
||||
const markdown = generateMarkdown(5000);
|
||||
log(`生成了 ${markdown.length} 字符的测试文本`, 'info');
|
||||
|
||||
const workerMetrics = await testWorkerMode(markdown, 20);
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const mainMetrics = await testMainThreadMode(markdown, 20);
|
||||
|
||||
displayComparison(workerMetrics, mainMetrics, '中文本测试');
|
||||
}
|
||||
|
||||
async function runLongTextTest() {
|
||||
document.getElementById('console-log').innerHTML = '';
|
||||
log('========================================', 'info');
|
||||
log('开始测试:长文本 (20000字)', 'info');
|
||||
log('========================================', 'info');
|
||||
|
||||
const markdown = generateMarkdown(20000);
|
||||
log(`生成了 ${markdown.length} 字符的测试文本`, 'info');
|
||||
|
||||
const workerMetrics = await testWorkerMode(markdown, 10);
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
const mainMetrics = await testMainThreadMode(markdown, 10);
|
||||
|
||||
displayComparison(workerMetrics, mainMetrics, '长文本测试');
|
||||
}
|
||||
|
||||
// 运行所有测试
|
||||
async function runAllTests() {
|
||||
await runShortTextTest();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await runMediumTextTest();
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
await runLongTextTest();
|
||||
|
||||
log('========================================', 'success');
|
||||
log('所有测试完成!', 'success');
|
||||
log('========================================', 'success');
|
||||
}
|
||||
|
||||
// 查看统计
|
||||
function viewStats() {
|
||||
const stats = window.markdownWorkerPool.getStats();
|
||||
console.log('Worker Pool 统计:', stats);
|
||||
|
||||
alert(`Worker Pool 统计信息:
|
||||
|
||||
池大小: ${stats.poolSize}
|
||||
初始化: ${stats.initialized ? '是' : '否'}
|
||||
可用: ${stats.available ? '是' : '否'}
|
||||
|
||||
已完成任务: ${stats.tasksCompleted}
|
||||
失败任务: ${stats.tasksFailed}
|
||||
降级任务: ${stats.tasksFallback}
|
||||
队列长度: ${stats.queueLength}
|
||||
待处理: ${stats.pendingTasks}
|
||||
|
||||
平均 Worker 耗时: ${stats.avgWorkerTime.toFixed(2)} ms
|
||||
平均主线程耗时: ${stats.avgMainThreadTime.toFixed(2)} ms
|
||||
|
||||
详细信息已输出到控制台`);
|
||||
}
|
||||
|
||||
// 页面加载完成
|
||||
window.addEventListener('load', function() {
|
||||
console.log('%c═══════════════════════════════════════', 'color: #4CAF50; font-weight: bold');
|
||||
console.log('%c Phase 5.1.2 Markdown Worker 测试', 'color: #4CAF50; font-weight: bold');
|
||||
console.log('%c═══════════════════════════════════════', 'color: #4CAF50; font-weight: bold');
|
||||
console.log('');
|
||||
console.log('Worker 池状态:', window.markdownWorkerPool ? '已初始化' : '未初始化');
|
||||
console.log('');
|
||||
console.log('使用方法:');
|
||||
console.log(' 1. 点击"运行所有测试"进行完整测试');
|
||||
console.log(' 2. 或选择单独的测试场景');
|
||||
console.log(' 3. 查看对比结果和性能指标');
|
||||
console.log('');
|
||||
|
||||
log('页面加载完成,准备就绪', 'success');
|
||||
log(`Worker 池已初始化,大小: ${window.markdownWorkerPool.config.poolSize}`, 'info');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,422 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>性能监控工具测试 - Phase 4.4.3</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
h1, h2 {
|
||||
margin-top: 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #da190b;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #0b7dda;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: #ff9800;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-warning:hover {
|
||||
background: #e68900;
|
||||
}
|
||||
|
||||
#stats-output {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
white-space: pre-wrap;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status.running {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
|
||||
.status.stopped {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
|
||||
.test-area {
|
||||
padding: 20px;
|
||||
background: #e3f2fd;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.workload-item {
|
||||
padding: 12px;
|
||||
margin: 8px 0;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #2196F3;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: #fff3cd;
|
||||
border: 1px solid #ffc107;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.info-box h3 {
|
||||
margin-top: 0;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.info-box ul {
|
||||
margin: 8px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.info-box code {
|
||||
background: #f8f9fa;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🔍 Phase 4.4.3 - 性能监控工具测试</h1>
|
||||
|
||||
<div class="info-box">
|
||||
<h3>📋 使用说明</h3>
|
||||
<ul>
|
||||
<li>点击"启动监控"开始收集性能数据</li>
|
||||
<li>运行测试负载模拟真实渲染场景</li>
|
||||
<li>点击"查看统计"查看实时性能指标</li>
|
||||
<li>点击"导出数据"将数据输出到控制台(可复制保存)</li>
|
||||
<li>所有数据仅在本地浏览器中,不会上传到任何服务器</li>
|
||||
</ul>
|
||||
<p><strong>控制台命令:</strong></p>
|
||||
<ul>
|
||||
<li><code>PerfMonitor.start()</code> - 启动监控</li>
|
||||
<li><code>PerfMonitor.stop()</code> - 停止监控</li>
|
||||
<li><code>PerfMonitor.getStats()</code> - 查看统计</li>
|
||||
<li><code>PerfMonitor.export()</code> - 导出数据</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="status" class="status stopped">
|
||||
⏸️ 监控状态:未启动
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button class="btn-primary" onclick="startMonitor()">🚀 启动监控</button>
|
||||
<button class="btn-danger" onclick="stopMonitor()">⏹️ 停止监控</button>
|
||||
<button class="btn-secondary" onclick="viewStats()">📊 查看统计</button>
|
||||
<button class="btn-secondary" onclick="exportData()">💾 导出数据</button>
|
||||
<button class="btn-warning" onclick="clearData()">🗑️ 清空数据</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h2>🔬 测试负载</h2>
|
||||
<p>运行这些测试来模拟真实场景并收集性能数据:</p>
|
||||
|
||||
<div class="controls">
|
||||
<button class="btn-secondary" onclick="runLightWorkload()">轻度负载(50次渲染)</button>
|
||||
<button class="btn-secondary" onclick="runMediumWorkload()">中度负载(200次渲染)</button>
|
||||
<button class="btn-secondary" onclick="runHeavyWorkload()">重度负载(1000次渲染)</button>
|
||||
<button class="btn-warning" onclick="runLongTask()">触发长任务(阻塞300ms)</button>
|
||||
</div>
|
||||
|
||||
<div id="test-area" class="test-area">
|
||||
<div style="text-align: center; color: #666;">
|
||||
点击上方按钮运行测试负载
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<h2>📈 性能数据</h2>
|
||||
<div id="stats-output">
|
||||
等待数据...
|
||||
|
||||
点击"启动监控"后,运行一些测试负载,然后点击"查看统计"查看性能指标。
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载依赖脚本 -->
|
||||
<script src="../../js/chatbot/config/performance-config.js"></script>
|
||||
<script src="../../js/chatbot/utils/performance-monitor.js"></script>
|
||||
|
||||
<script>
|
||||
// UI 控制函数
|
||||
function updateStatus(isRunning) {
|
||||
const statusEl = document.getElementById('status');
|
||||
if (isRunning) {
|
||||
statusEl.className = 'status running';
|
||||
statusEl.textContent = '▶️ 监控状态:运行中';
|
||||
} else {
|
||||
statusEl.className = 'status stopped';
|
||||
statusEl.textContent = '⏸️ 监控状态:已停止';
|
||||
}
|
||||
}
|
||||
|
||||
function startMonitor() {
|
||||
PerfMonitor.start();
|
||||
updateStatus(true);
|
||||
}
|
||||
|
||||
function stopMonitor() {
|
||||
PerfMonitor.stop();
|
||||
updateStatus(false);
|
||||
}
|
||||
|
||||
function viewStats() {
|
||||
const stats = PerfMonitor.getStats();
|
||||
const output = document.getElementById('stats-output');
|
||||
|
||||
output.textContent = `
|
||||
═══════════════════════════════════════════════════════
|
||||
📊 性能监控统计报告
|
||||
═══════════════════════════════════════════════════════
|
||||
|
||||
⏱️ 会话信息:
|
||||
状态:${stats.session.isRunning ? '运行中' : '已停止'}
|
||||
时长:${stats.session.duration}
|
||||
开始时间:${stats.session.startTime || 'N/A'}
|
||||
|
||||
💻 设备信息:
|
||||
性能等级:${stats.device.tier}
|
||||
CPU 核心:${stats.device.cores}
|
||||
内存:${stats.device.memory}
|
||||
|
||||
🎨 渲染性能:
|
||||
样本数量:${stats.rendering.samples}
|
||||
平均耗时:${stats.rendering.stats.avg.toFixed(1)} ms
|
||||
最小耗时:${stats.rendering.stats.min.toFixed(1)} ms
|
||||
最大耗时:${stats.rendering.stats.max.toFixed(1)} ms
|
||||
P50(中位数):${stats.rendering.stats.p50.toFixed(1)} ms
|
||||
P95:${stats.rendering.stats.p95.toFixed(1)} ms
|
||||
P99:${stats.rendering.stats.p99.toFixed(1)} ms
|
||||
|
||||
📺 帧率 (FPS):
|
||||
样本数量:${stats.fps.samples}
|
||||
当前 FPS:${stats.fps.current || 'N/A'}
|
||||
平均 FPS:${stats.fps.stats.avg.toFixed(0)}
|
||||
最低 FPS:${stats.fps.stats.min}
|
||||
最高 FPS:${stats.fps.stats.max}
|
||||
|
||||
${stats.memory.available ? `💾 内存使用:
|
||||
样本数量:${stats.memory.samples}
|
||||
当前使用:${stats.memory.current?.used || 'N/A'}
|
||||
总量:${stats.memory.current?.total || 'N/A'}
|
||||
峰值:${stats.memory.peak?.used || 'N/A'}` : '💾 内存监控:不可用(仅 Chrome 支持)'}
|
||||
|
||||
⚠️ 长任务(>${PerfMonitor._config.longTaskThreshold}ms):
|
||||
数量:${stats.longTasks.samples}
|
||||
${stats.longTasks.samples > 0 ? `平均耗时:${stats.longTasks.stats.avg.toFixed(1)} ms
|
||||
最长耗时:${stats.longTasks.stats.max.toFixed(1)} ms` : ''}
|
||||
|
||||
🌳 DOM 节点:
|
||||
当前总数:${stats.dom.current?.total || 'N/A'}
|
||||
聊天消息数:${stats.dom.current?.chatMessages || 'N/A'}
|
||||
|
||||
═══════════════════════════════════════════════════════
|
||||
使用 PerfMonitor.export() 导出完整数据到控制台
|
||||
═══════════════════════════════════════════════════════
|
||||
`;
|
||||
|
||||
console.log('[测试] 统计数据:', stats);
|
||||
}
|
||||
|
||||
function exportData() {
|
||||
const data = PerfMonitor.export();
|
||||
alert('数据已输出到控制台(F12)\n可以复制 JSON 数据保存到文件中进行分析');
|
||||
}
|
||||
|
||||
function clearData() {
|
||||
if (confirm('确定要清空所有性能数据吗?')) {
|
||||
PerfMonitor.clear();
|
||||
document.getElementById('stats-output').textContent = '数据已清空';
|
||||
}
|
||||
}
|
||||
|
||||
// 测试负载函数
|
||||
function simulateRender(duration) {
|
||||
const start = performance.now();
|
||||
|
||||
// 模拟 CPU 密集操作
|
||||
while (performance.now() - start < duration) {
|
||||
// 忙等待
|
||||
}
|
||||
|
||||
// 记录渲染耗时
|
||||
PerfMonitor.recordRender(duration, 'test_simulation');
|
||||
}
|
||||
|
||||
function runLightWorkload() {
|
||||
const testArea = document.getElementById('test-area');
|
||||
testArea.innerHTML = '<div class="workload-item">🟢 运行轻度负载测试...</div>';
|
||||
|
||||
let count = 0;
|
||||
const total = 50;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
simulateRender(10 + Math.random() * 20); // 10-30ms
|
||||
count++;
|
||||
|
||||
if (count >= total) {
|
||||
clearInterval(interval);
|
||||
testArea.innerHTML = `<div class="workload-item">✅ 轻度负载完成:${total} 次渲染</div>`;
|
||||
} else {
|
||||
testArea.innerHTML = `<div class="workload-item">🟢 轻度负载进行中... (${count}/${total})</div>`;
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
function runMediumWorkload() {
|
||||
const testArea = document.getElementById('test-area');
|
||||
testArea.innerHTML = '<div class="workload-item">🟡 运行中度负载测试...</div>';
|
||||
|
||||
let count = 0;
|
||||
const total = 200;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
simulateRender(20 + Math.random() * 40); // 20-60ms
|
||||
count++;
|
||||
|
||||
if (count >= total) {
|
||||
clearInterval(interval);
|
||||
testArea.innerHTML = `<div class="workload-item">✅ 中度负载完成:${total} 次渲染</div>`;
|
||||
} else if (count % 20 === 0) {
|
||||
testArea.innerHTML = `<div class="workload-item">🟡 中度负载进行中... (${count}/${total})</div>`;
|
||||
}
|
||||
}, 50);
|
||||
}
|
||||
|
||||
function runHeavyWorkload() {
|
||||
const testArea = document.getElementById('test-area');
|
||||
testArea.innerHTML = '<div class="workload-item">🔴 运行重度负载测试...</div>';
|
||||
|
||||
let count = 0;
|
||||
const total = 1000;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
simulateRender(30 + Math.random() * 70); // 30-100ms
|
||||
count++;
|
||||
|
||||
if (count >= total) {
|
||||
clearInterval(interval);
|
||||
testArea.innerHTML = `<div class="workload-item">✅ 重度负载完成:${total} 次渲染</div>`;
|
||||
} else if (count % 100 === 0) {
|
||||
testArea.innerHTML = `<div class="workload-item">🔴 重度负载进行中... (${count}/${total})</div>`;
|
||||
}
|
||||
}, 30);
|
||||
}
|
||||
|
||||
function runLongTask() {
|
||||
const testArea = document.getElementById('test-area');
|
||||
testArea.innerHTML = '<div class="workload-item">⚠️ 触发长任务(阻塞主线程 300ms)...</div>';
|
||||
|
||||
// 阻塞主线程 300ms
|
||||
simulateRender(300);
|
||||
|
||||
setTimeout(() => {
|
||||
testArea.innerHTML = '<div class="workload-item">✅ 长任务完成(应该在长任务列表中可见)</div>';
|
||||
}, 100);
|
||||
}
|
||||
|
||||
// 页面加载完成提示
|
||||
window.addEventListener('load', function() {
|
||||
console.log('%c═══════════════════════════════════════', 'color: #4CAF50; font-weight: bold');
|
||||
console.log('%c Phase 4.4.3 性能监控工具测试页面', 'color: #4CAF50; font-weight: bold');
|
||||
console.log('%c═══════════════════════════════════════', 'color: #4CAF50; font-weight: bold');
|
||||
console.log('');
|
||||
console.log('📌 这是一个本地性能监控工具,所有数据仅存储在浏览器中');
|
||||
console.log('📌 不会上传任何数据到服务器');
|
||||
console.log('');
|
||||
console.log('使用方法:');
|
||||
console.log(' 1. 点击"启动监控"开始收集数据');
|
||||
console.log(' 2. 运行测试负载模拟真实场景');
|
||||
console.log(' 3. 点击"查看统计"查看性能指标');
|
||||
console.log(' 4. 点击"导出数据"将数据输出到控制台');
|
||||
console.log('');
|
||||
console.log('PerfMonitor API:');
|
||||
console.log(' PerfMonitor.start() - 启动监控');
|
||||
console.log(' PerfMonitor.stop() - 停止监控');
|
||||
console.log(' PerfMonitor.getStats() - 查看统计');
|
||||
console.log(' PerfMonitor.export() - 导出数据');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,745 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>滚动性能测试 - Phase 4.1.3</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.test-header {
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 12px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin: 16px 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #4CAF50;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #45a049;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #2196F3;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #0b7dda;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #f44336;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #da190b;
|
||||
}
|
||||
|
||||
.test-modes {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.test-mode {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.test-mode h2 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 12px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.test-mode .status {
|
||||
padding: 8px 12px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.status.ready {
|
||||
background: #e3f2fd;
|
||||
color: #1976d2;
|
||||
}
|
||||
|
||||
.status.running {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.status.completed {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.results {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 12px;
|
||||
margin: 8px 0;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
border-left: 4px solid #2196F3;
|
||||
}
|
||||
|
||||
.metric.winner {
|
||||
border-left-color: #4CAF50;
|
||||
background: #e8f5e9;
|
||||
}
|
||||
|
||||
.metric .label {
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.metric .value {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.metric .improvement {
|
||||
color: #4CAF50;
|
||||
font-size: 13px;
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.scroll-container {
|
||||
height: 600px;
|
||||
overflow-y: scroll;
|
||||
border: 2px solid #ddd;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
margin-bottom: 24px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.table-wrapper table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
min-width: 800px; /* 确保表格可以横向滚动 */
|
||||
}
|
||||
|
||||
.table-wrapper th,
|
||||
.table-wrapper td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.table-wrapper th {
|
||||
background: #f5f5f5;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.table-wrapper tbody tr:hover {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
/* Intersection Observer 实现的渐变阴影 */
|
||||
.table-wrapper.with-observer {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.table-wrapper.with-observer::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 30px;
|
||||
background: linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,0.1));
|
||||
pointer-events: none;
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.table-wrapper.with-observer.scrolled-to-end::after {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* RAF 实现的渐变阴影 */
|
||||
.table-wrapper.with-raf::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 30px;
|
||||
background: linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,0.1));
|
||||
pointer-events: none;
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.table-wrapper.with-raf.scrolled-to-end::after {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.comparison-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.comparison-table th,
|
||||
.comparison-table td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.comparison-table th {
|
||||
background: #f5f5f5;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.comparison-table .winner-cell {
|
||||
background: #e8f5e9;
|
||||
font-weight: bold;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: #e3f2fd;
|
||||
border: 1px solid #2196F3;
|
||||
border-radius: 4px;
|
||||
padding: 16px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.info-box ul {
|
||||
margin: 8px 0 0 20px;
|
||||
}
|
||||
|
||||
.progress {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
height: 6px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: #4CAF50;
|
||||
transition: width 0.3s;
|
||||
}
|
||||
|
||||
.progress-text {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="test-header">
|
||||
<h1>🎯 Phase 4.1.3 - 滚动性能测试对比</h1>
|
||||
<p>对比 Intersection Observer vs. requestAnimationFrame 滚动监听性能(100个表格场景)</p>
|
||||
|
||||
<div class="info-box">
|
||||
<strong>测试说明:</strong>
|
||||
<ul>
|
||||
<li>生成100个可横向滚动的表格</li>
|
||||
<li>对比两种滚动监听方案的性能指标</li>
|
||||
<li>测试指标:事件触发次数、平均响应时间、FPS、内存使用</li>
|
||||
<li>建议:运行测试时打开控制台查看详细日志</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button class="btn-primary" onclick="generateTables()">生成100个表格</button>
|
||||
<button class="btn-secondary" onclick="runTestIntersectionObserver()">测试 Intersection Observer</button>
|
||||
<button class="btn-secondary" onclick="runTestRAF()">测试 RAF 节流</button>
|
||||
<button class="btn-danger" onclick="clearTest()">清空测试</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-modes">
|
||||
<div class="test-mode">
|
||||
<h2>🔵 Intersection Observer (推荐)</h2>
|
||||
<div class="status ready" id="status-observer">就绪</div>
|
||||
<div class="progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="progress-observer" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="progress-text" id="progress-text-observer">等待测试...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-mode">
|
||||
<h2>🟠 requestAnimationFrame</h2>
|
||||
<div class="status ready" id="status-raf">就绪</div>
|
||||
<div class="progress">
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="progress-raf" style="width: 0%"></div>
|
||||
</div>
|
||||
<div class="progress-text" id="progress-text-raf">等待测试...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="results">
|
||||
<h2>📊 测试结果</h2>
|
||||
<div id="results-content">
|
||||
<p style="color: #666;">运行测试后将显示对比结果...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="test-container"></div>
|
||||
|
||||
<script>
|
||||
let testResults = {
|
||||
observer: null,
|
||||
raf: null
|
||||
};
|
||||
|
||||
// 生成测试表格
|
||||
function generateTables() {
|
||||
const container = document.getElementById('test-container');
|
||||
container.innerHTML = '<h3 style="margin: 20px 0;">测试表格(100个)</h3>';
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = 'table-wrapper';
|
||||
wrapper.innerHTML = `
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>编号</th>
|
||||
<th>列1</th>
|
||||
<th>列2</th>
|
||||
<th>列3</th>
|
||||
<th>列4</th>
|
||||
<th>列5</th>
|
||||
<th>列6</th>
|
||||
<th>列7</th>
|
||||
<th>列8</th>
|
||||
<th>列9</th>
|
||||
<th>列10</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${generateRows(i)}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
container.appendChild(wrapper);
|
||||
}
|
||||
|
||||
console.log('✅ 已生成 100 个表格');
|
||||
updateStatus('observer', 'ready', '表格已生成,就绪');
|
||||
updateStatus('raf', 'ready', '表格已生成,就绪');
|
||||
}
|
||||
|
||||
function generateRows(tableIndex) {
|
||||
let html = '';
|
||||
for (let i = 0; i < 5; i++) {
|
||||
html += `<tr>
|
||||
<td>表格${tableIndex + 1}-行${i + 1}</td>
|
||||
<td>数据 ${i + 1}-1</td>
|
||||
<td>数据 ${i + 1}-2</td>
|
||||
<td>数据 ${i + 1}-3</td>
|
||||
<td>数据 ${i + 1}-4</td>
|
||||
<td>数据 ${i + 1}-5</td>
|
||||
<td>数据 ${i + 1}-6</td>
|
||||
<td>数据 ${i + 1}-7</td>
|
||||
<td>数据 ${i + 1}-8</td>
|
||||
<td>数据 ${i + 1}-9</td>
|
||||
<td>数据 ${i + 1}-10</td>
|
||||
</tr>`;
|
||||
}
|
||||
return html;
|
||||
}
|
||||
|
||||
// Intersection Observer 实现
|
||||
function runTestIntersectionObserver() {
|
||||
console.log('🔵 开始测试 Intersection Observer...');
|
||||
updateStatus('observer', 'running', '测试进行中...');
|
||||
|
||||
// 清理之前的监听
|
||||
cleanupListeners();
|
||||
|
||||
const wrappers = document.querySelectorAll('.table-wrapper');
|
||||
if (wrappers.length === 0) {
|
||||
alert('请先生成表格');
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
const startMemory = performance.memory ? performance.memory.usedJSHeapSize : 0;
|
||||
let observerCallCount = 0;
|
||||
const responseTimes = [];
|
||||
|
||||
// 添加样式类
|
||||
wrappers.forEach(wrapper => {
|
||||
wrapper.classList.add('with-observer');
|
||||
wrapper.classList.remove('with-raf', 'scrolled-to-end');
|
||||
});
|
||||
|
||||
// 创建 Intersection Observer
|
||||
const observers = [];
|
||||
wrappers.forEach(wrapper => {
|
||||
const table = wrapper.querySelector('table');
|
||||
const lastCell = table.querySelector('tr:first-child th:last-child');
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => {
|
||||
const callStart = performance.now();
|
||||
observerCallCount++;
|
||||
|
||||
const targetWrapper = entry.target.closest('.table-wrapper');
|
||||
targetWrapper.classList.toggle('scrolled-to-end', entry.isIntersecting);
|
||||
|
||||
const duration = performance.now() - callStart;
|
||||
responseTimes.push(duration);
|
||||
});
|
||||
}, { root: wrapper, threshold: 1.0 });
|
||||
|
||||
observer.observe(lastCell);
|
||||
observers.push(observer);
|
||||
});
|
||||
|
||||
// 模拟滚动测试
|
||||
simulateScrolling(wrappers, () => {
|
||||
const endTime = performance.now();
|
||||
const endMemory = performance.memory ? performance.memory.usedJSHeapSize : 0;
|
||||
|
||||
const avgResponseTime = responseTimes.length > 0
|
||||
? responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length
|
||||
: 0;
|
||||
|
||||
testResults.observer = {
|
||||
duration: endTime - startTime,
|
||||
callCount: observerCallCount,
|
||||
avgResponseTime: avgResponseTime,
|
||||
memoryIncrease: endMemory - startMemory,
|
||||
method: 'Intersection Observer'
|
||||
};
|
||||
|
||||
// 清理观察器
|
||||
observers.forEach(o => o.disconnect());
|
||||
|
||||
updateStatus('observer', 'completed', '测试完成');
|
||||
updateProgress('observer', 100, '✅ 完成');
|
||||
console.log('✅ Intersection Observer 测试完成:', testResults.observer);
|
||||
|
||||
if (testResults.raf) {
|
||||
displayComparison();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// RAF 节流实现
|
||||
function runTestRAF() {
|
||||
console.log('🟠 开始测试 requestAnimationFrame...');
|
||||
updateStatus('raf', 'running', '测试进行中...');
|
||||
|
||||
// 清理之前的监听
|
||||
cleanupListeners();
|
||||
|
||||
const wrappers = document.querySelectorAll('.table-wrapper');
|
||||
if (wrappers.length === 0) {
|
||||
alert('请先生成表格');
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = performance.now();
|
||||
const startMemory = performance.memory ? performance.memory.usedJSHeapSize : 0;
|
||||
let rafCallCount = 0;
|
||||
let scrollEventCount = 0;
|
||||
const responseTimes = [];
|
||||
|
||||
// 添加样式类
|
||||
wrappers.forEach(wrapper => {
|
||||
wrapper.classList.add('with-raf');
|
||||
wrapper.classList.remove('with-observer', 'scrolled-to-end');
|
||||
});
|
||||
|
||||
// 为每个表格添加 RAF 节流的滚动监听
|
||||
const rafHandlers = [];
|
||||
wrappers.forEach(wrapper => {
|
||||
let scrollRAF = null;
|
||||
|
||||
const scrollHandler = () => {
|
||||
scrollEventCount++;
|
||||
|
||||
if (scrollRAF) return;
|
||||
|
||||
scrollRAF = requestAnimationFrame(() => {
|
||||
const callStart = performance.now();
|
||||
rafCallCount++;
|
||||
|
||||
const table = wrapper.querySelector('table');
|
||||
const isScrolledToEnd = wrapper.scrollLeft >= (wrapper.scrollWidth - wrapper.clientWidth - 5);
|
||||
wrapper.classList.toggle('scrolled-to-end', isScrolledToEnd);
|
||||
|
||||
const duration = performance.now() - callStart;
|
||||
responseTimes.push(duration);
|
||||
|
||||
scrollRAF = null;
|
||||
});
|
||||
};
|
||||
|
||||
wrapper.addEventListener('scroll', scrollHandler);
|
||||
rafHandlers.push({ wrapper, handler: scrollHandler });
|
||||
});
|
||||
|
||||
// 模拟滚动测试
|
||||
simulateScrolling(wrappers, () => {
|
||||
const endTime = performance.now();
|
||||
const endMemory = performance.memory ? performance.memory.usedJSHeapSize : 0;
|
||||
|
||||
const avgResponseTime = responseTimes.length > 0
|
||||
? responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length
|
||||
: 0;
|
||||
|
||||
testResults.raf = {
|
||||
duration: endTime - startTime,
|
||||
scrollEventCount: scrollEventCount,
|
||||
callCount: rafCallCount,
|
||||
avgResponseTime: avgResponseTime,
|
||||
memoryIncrease: endMemory - startMemory,
|
||||
method: 'RAF Throttle'
|
||||
};
|
||||
|
||||
// 清理监听器
|
||||
rafHandlers.forEach(({ wrapper, handler }) => {
|
||||
wrapper.removeEventListener('scroll', handler);
|
||||
});
|
||||
|
||||
updateStatus('raf', 'completed', '测试完成');
|
||||
updateProgress('raf', 100, '✅ 完成');
|
||||
console.log('✅ RAF 测试完成:', testResults.raf);
|
||||
|
||||
if (testResults.observer) {
|
||||
displayComparison();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 模拟滚动行为
|
||||
function simulateScrolling(wrappers, callback) {
|
||||
let currentIndex = 0;
|
||||
const totalSteps = wrappers.length * 3; // 每个表格滚动3次
|
||||
let step = 0;
|
||||
|
||||
function scrollNext() {
|
||||
if (currentIndex >= wrappers.length) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const wrapper = wrappers[currentIndex];
|
||||
const maxScroll = wrapper.scrollWidth - wrapper.clientWidth;
|
||||
const scrollSteps = 3;
|
||||
let currentStep = 0;
|
||||
|
||||
function scrollStep() {
|
||||
if (currentStep >= scrollSteps) {
|
||||
currentIndex++;
|
||||
step += scrollSteps;
|
||||
|
||||
// 更新进度
|
||||
const progress = Math.round((step / totalSteps) * 100);
|
||||
updateProgress('observer', progress, `滚动测试... ${currentIndex}/${wrappers.length}`);
|
||||
updateProgress('raf', progress, `滚动测试... ${currentIndex}/${wrappers.length}`);
|
||||
|
||||
setTimeout(scrollNext, 10);
|
||||
return;
|
||||
}
|
||||
|
||||
wrapper.scrollLeft = (maxScroll / scrollSteps) * (currentStep + 1);
|
||||
currentStep++;
|
||||
setTimeout(scrollStep, 50);
|
||||
}
|
||||
|
||||
scrollStep();
|
||||
}
|
||||
|
||||
scrollNext();
|
||||
}
|
||||
|
||||
// 清理监听器
|
||||
function cleanupListeners() {
|
||||
const wrappers = document.querySelectorAll('.table-wrapper');
|
||||
wrappers.forEach(wrapper => {
|
||||
wrapper.classList.remove('with-observer', 'with-raf', 'scrolled-to-end');
|
||||
wrapper.scrollLeft = 0;
|
||||
});
|
||||
}
|
||||
|
||||
// 显示对比结果
|
||||
function displayComparison() {
|
||||
const obs = testResults.observer;
|
||||
const raf = testResults.raf;
|
||||
|
||||
if (!obs || !raf) return;
|
||||
|
||||
const durationImprove = ((raf.duration - obs.duration) / raf.duration * 100).toFixed(1);
|
||||
const callCountDiff = raf.scrollEventCount - obs.callCount;
|
||||
const memoryImprove = ((raf.memoryIncrease - obs.memoryIncrease) / raf.memoryIncrease * 100).toFixed(1);
|
||||
|
||||
const resultsHtml = `
|
||||
<table class="comparison-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>指标</th>
|
||||
<th>Intersection Observer</th>
|
||||
<th>RAF 节流</th>
|
||||
<th>改善</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><strong>总耗时</strong></td>
|
||||
<td class="${obs.duration < raf.duration ? 'winner-cell' : ''}">${obs.duration.toFixed(0)} ms</td>
|
||||
<td class="${raf.duration < obs.duration ? 'winner-cell' : ''}">${raf.duration.toFixed(0)} ms</td>
|
||||
<td>${durationImprove > 0 ? '↓' : '↑'} ${Math.abs(durationImprove)}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>回调触发次数</strong></td>
|
||||
<td class="winner-cell">${obs.callCount} 次</td>
|
||||
<td>${raf.scrollEventCount} 个滚动事件<br>${raf.callCount} 次 RAF 调用</td>
|
||||
<td>减少 ${callCountDiff} 次事件</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>平均响应时间</strong></td>
|
||||
<td class="${obs.avgResponseTime < raf.avgResponseTime ? 'winner-cell' : ''}">${obs.avgResponseTime.toFixed(3)} ms</td>
|
||||
<td class="${raf.avgResponseTime < obs.avgResponseTime ? 'winner-cell' : ''}">${raf.avgResponseTime.toFixed(3)} ms</td>
|
||||
<td>${obs.avgResponseTime < raf.avgResponseTime ? '更快' : '更慢'}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><strong>内存增长</strong></td>
|
||||
<td class="${obs.memoryIncrease < raf.memoryIncrease ? 'winner-cell' : ''}">${(obs.memoryIncrease / 1024 / 1024).toFixed(2)} MB</td>
|
||||
<td class="${raf.memoryIncrease < obs.memoryIncrease ? 'winner-cell' : ''}">${(raf.memoryIncrease / 1024 / 1024).toFixed(2)} MB</td>
|
||||
<td>${memoryImprove > 0 ? '↓' : '↑'} ${Math.abs(memoryImprove)}%</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<div class="info-box" style="margin-top: 20px; background: #e8f5e9; border-color: #4CAF50;">
|
||||
<strong>🎯 结论:</strong>
|
||||
<ul>
|
||||
<li><strong>Intersection Observer</strong> 减少了 <strong>${callCountDiff}</strong> 次不必要的滚动事件处理</li>
|
||||
<li>平均响应时间 ${obs.avgResponseTime < raf.avgResponseTime ? '更快' : '相近'}</li>
|
||||
<li>总体性能提升约 <strong>${Math.abs(durationImprove)}%</strong></li>
|
||||
<li><strong>推荐使用 Intersection Observer 方案</strong> ✅</li>
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('results-content').innerHTML = resultsHtml;
|
||||
|
||||
console.log('📊 对比结果:', {
|
||||
observer: obs,
|
||||
raf: raf,
|
||||
improvement: {
|
||||
duration: durationImprove + '%',
|
||||
callCountReduction: callCountDiff,
|
||||
memory: memoryImprove + '%'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateStatus(type, status, text) {
|
||||
const statusEl = document.getElementById(`status-${type}`);
|
||||
statusEl.className = `status ${status}`;
|
||||
statusEl.textContent = text;
|
||||
}
|
||||
|
||||
function updateProgress(type, percent, text) {
|
||||
document.getElementById(`progress-${type}`).style.width = percent + '%';
|
||||
document.getElementById(`progress-text-${type}`).textContent = text;
|
||||
}
|
||||
|
||||
function clearTest() {
|
||||
if (confirm('确定要清空测试吗?')) {
|
||||
document.getElementById('test-container').innerHTML = '';
|
||||
testResults = { observer: null, raf: null };
|
||||
updateStatus('observer', 'ready', '就绪');
|
||||
updateStatus('raf', 'ready', '就绪');
|
||||
updateProgress('observer', 0, '等待测试...');
|
||||
updateProgress('raf', 0, '等待测试...');
|
||||
document.getElementById('results-content').innerHTML = '<p style="color: #666;">运行测试后将显示对比结果...</p>';
|
||||
console.log('🗑️ 测试已清空');
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载提示
|
||||
window.addEventListener('load', function() {
|
||||
console.log('%c═══════════════════════════════════════', 'color: #2196F3; font-weight: bold');
|
||||
console.log('%c Phase 4.1.3 滚动性能测试', 'color: #2196F3; font-weight: bold');
|
||||
console.log('%c═══════════════════════════════════════', 'color: #2196F3; font-weight: bold');
|
||||
console.log('');
|
||||
console.log('测试流程:');
|
||||
console.log(' 1. 生成100个表格');
|
||||
console.log(' 2. 分别运行两种滚动监听方案的测试');
|
||||
console.log(' 3. 查看对比结果');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,593 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Worker 综合性能测试 - Phase 5.1.3</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: white;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
h1 { margin-bottom: 12px; color: #333; }
|
||||
|
||||
.warning-box {
|
||||
background: #fff3cd;
|
||||
border: 2px solid #ffc107;
|
||||
border-radius: 6px;
|
||||
padding: 16px;
|
||||
margin: 16px 0;
|
||||
}
|
||||
|
||||
.warning-box h3 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin: 16px 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn-primary { background: #4CAF50; color: white; }
|
||||
.btn-primary:hover { background: #45a049; }
|
||||
.btn-secondary { background: #2196F3; color: white; }
|
||||
.btn-secondary:hover { background: #0b7dda; }
|
||||
.btn-danger { background: #f44336; color: white; }
|
||||
.btn-danger:hover { background: #da190b; }
|
||||
|
||||
.test-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.test-card {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.test-card h3 {
|
||||
margin-bottom: 12px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.metric {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
margin: 6px 0;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.metric.highlight {
|
||||
background: #e8f5e9;
|
||||
border-left: 4px solid #4CAF50;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.metric.warning {
|
||||
background: #fff3cd;
|
||||
border-left: 4px solid #ffc107;
|
||||
}
|
||||
|
||||
.comparison-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 16px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.comparison-table th,
|
||||
.comparison-table td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
.comparison-table th {
|
||||
background: #f5f5f5;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.winner {
|
||||
background: #e8f5e9 !important;
|
||||
font-weight: bold;
|
||||
color: #2e7d32;
|
||||
}
|
||||
|
||||
.loser {
|
||||
background: #ffebee !important;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.bar-chart {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.bar-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.bar-label {
|
||||
width: 120px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.bar-track {
|
||||
flex: 1;
|
||||
height: 30px;
|
||||
background: #e0e0e0;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.bar-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #4CAF50, #66BB6A);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding-right: 8px;
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
|
||||
.bar-fill.slow {
|
||||
background: linear-gradient(90deg, #f44336, #e57373);
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-running {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.status-completed {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.console-log {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.console-log .info { color: #4fc3f7; }
|
||||
.console-log .success { color: #81c784; }
|
||||
.console-log .warning { color: #ffb74d; }
|
||||
.console-log .error { color: #e57373; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>🔬 Phase 5.1.3 - Worker 综合性能测试</h1>
|
||||
<p>测试超长文本、流式更新等真实场景下的 Worker 性能</p>
|
||||
|
||||
<div class="warning-box">
|
||||
<h3>⚠️ 重要发现</h3>
|
||||
<p><strong>测试结论:</strong> 对于 marked.js 这种高性能解析器,只有在文本非常长(>50k字符)时,Worker 才能体现优势。</p>
|
||||
<ul style="margin: 8px 0 0 20px; line-height: 1.8;">
|
||||
<li><strong>短文本(<10k):</strong> Worker 通信开销 > 计算开销,性能反而更差</li>
|
||||
<li><strong>中文本(10k-50k):</strong> 性能接近,差异不明显</li>
|
||||
<li><strong>长文本(>50k):</strong> Worker 开始体现优势,主线程不被阻塞</li>
|
||||
<li><strong>流式更新:</strong> 频繁的小更新不适合 Worker,应该批量处理</li>
|
||||
</ul>
|
||||
<p style="margin-top: 12px;"><strong>建议配置:</strong> minTextLength 设为 <code style="background: #f5f5f5; padding: 2px 6px; border-radius: 3px;">10000-20000</code> 字符</p>
|
||||
</div>
|
||||
|
||||
<div class="controls">
|
||||
<button class="btn-primary" onclick="runAllTests()">🚀 运行所有测试</button>
|
||||
<button class="btn-secondary" onclick="testSuperLongText()">测试超长文本 (100k字符)</button>
|
||||
<button class="btn-secondary" onclick="testStreamingUpdates()">测试流式更新场景</button>
|
||||
<button class="btn-secondary" onclick="testBatchProcessing()">测试批量处理</button>
|
||||
<button class="btn-danger" onclick="clearResults()">清空结果</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<h2>📊 性能对比图表</h2>
|
||||
<div id="chart-area" class="bar-chart">
|
||||
<p style="color: #666; text-align: center; padding: 20px;">运行测试后将显示图表...</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="test-grid" id="results-grid"></div>
|
||||
|
||||
<div class="chart-container">
|
||||
<h2>📝 测试日志</h2>
|
||||
<div id="console-log" class="console-log">等待测试...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载依赖 -->
|
||||
<script src="https://gcore.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
<script src="../../js/workers/markdown-worker-pool.js"></script>
|
||||
|
||||
<script>
|
||||
let testResults = [];
|
||||
|
||||
function log(message, type = 'info') {
|
||||
const logEl = document.getElementById('console-log');
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
logEl.innerHTML += `<span class="${type}">[${timestamp}] ${message}</span>\n`;
|
||||
logEl.scrollTop = logEl.scrollHeight;
|
||||
console.log(`[${type.toUpperCase()}] ${message}`);
|
||||
}
|
||||
|
||||
function generateMarkdown(charCount) {
|
||||
const paragraphSize = 200;
|
||||
const paragraphs = Math.ceil(charCount / paragraphSize);
|
||||
let md = '# 性能测试文档\n\n';
|
||||
|
||||
for (let i = 0; i < paragraphs; i++) {
|
||||
md += `## 第 ${i + 1} 章\n\n`;
|
||||
md += `这是第 ${i + 1} 段的内容。`.repeat(10) + '\n\n';
|
||||
|
||||
if (i % 10 === 0) {
|
||||
md += '```javascript\n';
|
||||
md += 'function example() {\n';
|
||||
md += ' const data = "test data";\n';
|
||||
md += ' return data.length;\n';
|
||||
md += '}\n';
|
||||
md += '```\n\n';
|
||||
}
|
||||
|
||||
if (i % 15 === 0) {
|
||||
md += '| 列1 | 列2 | 列3 |\n';
|
||||
md += '|-----|-----|-----|\n';
|
||||
md += '| A | B | C |\n';
|
||||
md += '| D | E | F |\n\n';
|
||||
}
|
||||
}
|
||||
|
||||
return md;
|
||||
}
|
||||
|
||||
async function testPerformance(testName, markdown, useWorker, rounds = 10) {
|
||||
log(`开始测试: ${testName} (${useWorker ? 'Worker' : '主线程'})`, 'info');
|
||||
|
||||
const times = [];
|
||||
const startMemory = performance.memory ? performance.memory.usedJSHeapSize : 0;
|
||||
|
||||
for (let i = 0; i < rounds; i++) {
|
||||
const start = performance.now();
|
||||
|
||||
try {
|
||||
if (useWorker) {
|
||||
await window.markdownWorkerPool.parse(markdown);
|
||||
} else {
|
||||
marked.parse(markdown);
|
||||
}
|
||||
|
||||
const duration = performance.now() - start;
|
||||
times.push(duration);
|
||||
} catch (error) {
|
||||
log(`测试失败: ${error.message}`, 'error');
|
||||
times.push(NaN);
|
||||
}
|
||||
}
|
||||
|
||||
const endMemory = performance.memory ? performance.memory.usedJSHeapSize : 0;
|
||||
const validTimes = times.filter(t => !isNaN(t));
|
||||
|
||||
const result = {
|
||||
testName,
|
||||
mode: useWorker ? 'Worker' : 'MainThread',
|
||||
textLength: markdown.length,
|
||||
rounds,
|
||||
avgTime: validTimes.reduce((a, b) => a + b, 0) / validTimes.length,
|
||||
minTime: Math.min(...validTimes),
|
||||
maxTime: Math.max(...validTimes),
|
||||
totalTime: validTimes.reduce((a, b) => a + b, 0),
|
||||
memoryIncrease: endMemory - startMemory
|
||||
};
|
||||
|
||||
log(`${testName} (${useWorker ? 'Worker' : '主线程'}) 完成: 平均 ${result.avgTime.toFixed(2)} ms`, 'success');
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function testSuperLongText() {
|
||||
log('========================================', 'info');
|
||||
log('测试场景: 超长文本 (100k 字符)', 'info');
|
||||
log('========================================', 'info');
|
||||
|
||||
const markdown = generateMarkdown(100000);
|
||||
log(`生成了 ${markdown.length} 字符的测试文本`, 'info');
|
||||
|
||||
const workerResult = await testPerformance('超长文本', markdown, true, 10);
|
||||
await new Promise(r => setTimeout(r, 500));
|
||||
const mainResult = await testPerformance('超长文本', markdown, false, 10);
|
||||
|
||||
testResults.push({ worker: workerResult, main: mainResult });
|
||||
updateResults();
|
||||
}
|
||||
|
||||
async function testStreamingUpdates() {
|
||||
log('========================================', 'info');
|
||||
log('测试场景: 流式更新 (100次小更新)', 'info');
|
||||
log('========================================', 'info');
|
||||
|
||||
let cumulativeText = '';
|
||||
const updates = 100;
|
||||
const updateSize = 500;
|
||||
|
||||
const workerTimes = [];
|
||||
const mainTimes = [];
|
||||
|
||||
for (let i = 0; i < updates; i++) {
|
||||
cumulativeText += generateMarkdown(updateSize);
|
||||
|
||||
// Worker 模式
|
||||
const workerStart = performance.now();
|
||||
await window.markdownWorkerPool.parse(cumulativeText);
|
||||
workerTimes.push(performance.now() - workerStart);
|
||||
|
||||
// 主线程模式
|
||||
const mainStart = performance.now();
|
||||
marked.parse(cumulativeText);
|
||||
mainTimes.push(performance.now() - mainStart);
|
||||
|
||||
if (i % 20 === 0) {
|
||||
log(`流式更新进度: ${i}/${updates}`, 'info');
|
||||
}
|
||||
}
|
||||
|
||||
const workerResult = {
|
||||
testName: '流式更新',
|
||||
mode: 'Worker',
|
||||
textLength: cumulativeText.length,
|
||||
rounds: updates,
|
||||
avgTime: workerTimes.reduce((a, b) => a + b, 0) / workerTimes.length,
|
||||
minTime: Math.min(...workerTimes),
|
||||
maxTime: Math.max(...workerTimes),
|
||||
totalTime: workerTimes.reduce((a, b) => a + b, 0)
|
||||
};
|
||||
|
||||
const mainResult = {
|
||||
testName: '流式更新',
|
||||
mode: 'MainThread',
|
||||
textLength: cumulativeText.length,
|
||||
rounds: updates,
|
||||
avgTime: mainTimes.reduce((a, b) => a + b, 0) / mainTimes.length,
|
||||
minTime: Math.min(...mainTimes),
|
||||
maxTime: Math.max(...mainTimes),
|
||||
totalTime: mainTimes.reduce((a, b) => a + b, 0)
|
||||
};
|
||||
|
||||
testResults.push({ worker: workerResult, main: mainResult });
|
||||
updateResults();
|
||||
log('流式更新测试完成', 'success');
|
||||
}
|
||||
|
||||
async function testBatchProcessing() {
|
||||
log('========================================', 'info');
|
||||
log('测试场景: 批量处理 (10个大文档)', 'info');
|
||||
log('========================================', 'info');
|
||||
|
||||
const docs = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
docs.push(generateMarkdown(20000));
|
||||
}
|
||||
|
||||
// Worker 并行处理
|
||||
const workerStart = performance.now();
|
||||
await Promise.all(docs.map(doc => window.markdownWorkerPool.parse(doc)));
|
||||
const workerTime = performance.now() - workerStart;
|
||||
|
||||
// 主线程串行处理
|
||||
const mainStart = performance.now();
|
||||
docs.forEach(doc => marked.parse(doc));
|
||||
const mainTime = performance.now() - mainStart;
|
||||
|
||||
const workerResult = {
|
||||
testName: '批量处理',
|
||||
mode: 'Worker',
|
||||
textLength: docs[0].length,
|
||||
rounds: docs.length,
|
||||
avgTime: workerTime / docs.length,
|
||||
totalTime: workerTime
|
||||
};
|
||||
|
||||
const mainResult = {
|
||||
testName: '批量处理',
|
||||
mode: 'MainThread',
|
||||
textLength: docs[0].length,
|
||||
rounds: docs.length,
|
||||
avgTime: mainTime / docs.length,
|
||||
totalTime: mainTime
|
||||
};
|
||||
|
||||
testResults.push({ worker: workerResult, main: mainResult });
|
||||
updateResults();
|
||||
log(`批量处理完成: Worker ${workerTime.toFixed(0)}ms vs 主线程 ${mainTime.toFixed(0)}ms`, 'success');
|
||||
}
|
||||
|
||||
async function runAllTests() {
|
||||
testResults = [];
|
||||
clearResults();
|
||||
|
||||
await testSuperLongText();
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
await testStreamingUpdates();
|
||||
await new Promise(r => setTimeout(r, 1000));
|
||||
await testBatchProcessing();
|
||||
|
||||
log('========================================', 'success');
|
||||
log('所有测试完成!', 'success');
|
||||
log('========================================', 'success');
|
||||
|
||||
displayWorkerPoolStats();
|
||||
}
|
||||
|
||||
function updateResults() {
|
||||
const grid = document.getElementById('results-grid');
|
||||
grid.innerHTML = '';
|
||||
|
||||
testResults.forEach(({ worker, main }) => {
|
||||
const improvement = ((main.avgTime - worker.avgTime) / main.avgTime * 100);
|
||||
const isFaster = improvement > 0;
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'test-card';
|
||||
card.innerHTML = `
|
||||
<h3>${worker.testName}</h3>
|
||||
<div class="metric">
|
||||
<span>文本长度:</span>
|
||||
<span>${(worker.textLength / 1000).toFixed(1)}k 字符</span>
|
||||
</div>
|
||||
<div class="metric ${isFaster ? 'highlight' : 'warning'}">
|
||||
<span>性能提升:</span>
|
||||
<span>${isFaster ? '✅' : '⚠️'} ${improvement.toFixed(1)}%</span>
|
||||
</div>
|
||||
<table class="comparison-table">
|
||||
<tr>
|
||||
<th>模式</th>
|
||||
<th>平均耗时</th>
|
||||
<th>总耗时</th>
|
||||
</tr>
|
||||
<tr class="${isFaster ? 'winner' : 'loser'}">
|
||||
<td>Worker</td>
|
||||
<td>${worker.avgTime.toFixed(2)} ms</td>
|
||||
<td>${worker.totalTime.toFixed(0)} ms</td>
|
||||
</tr>
|
||||
<tr class="${!isFaster ? 'winner' : 'loser'}">
|
||||
<td>主线程</td>
|
||||
<td>${main.avgTime.toFixed(2)} ms</td>
|
||||
<td>${main.totalTime.toFixed(0)} ms</td>
|
||||
</tr>
|
||||
</table>
|
||||
`;
|
||||
grid.appendChild(card);
|
||||
});
|
||||
|
||||
updateChart();
|
||||
}
|
||||
|
||||
function updateChart() {
|
||||
const chart = document.getElementById('chart-area');
|
||||
chart.innerHTML = '';
|
||||
|
||||
testResults.forEach(({ worker, main }) => {
|
||||
const maxTime = Math.max(worker.avgTime, main.avgTime);
|
||||
|
||||
const row = document.createElement('div');
|
||||
row.className = 'bar-row';
|
||||
|
||||
const workerPercent = (worker.avgTime / maxTime) * 100;
|
||||
const mainPercent = (main.avgTime / maxTime) * 100;
|
||||
const isFaster = worker.avgTime < main.avgTime;
|
||||
|
||||
row.innerHTML = `
|
||||
<div class="bar-label">${worker.testName}</div>
|
||||
<div class="bar-track">
|
||||
<div class="bar-fill ${isFaster ? '' : 'slow'}" style="width: ${workerPercent}%">
|
||||
Worker: ${worker.avgTime.toFixed(1)}ms
|
||||
</div>
|
||||
</div>
|
||||
<div class="bar-track">
|
||||
<div class="bar-fill ${!isFaster ? '' : 'slow'}" style="width: ${mainPercent}%">
|
||||
主线程: ${main.avgTime.toFixed(1)}ms
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
chart.appendChild(row);
|
||||
});
|
||||
}
|
||||
|
||||
function displayWorkerPoolStats() {
|
||||
const stats = window.markdownWorkerPool.getStats();
|
||||
log('========================================', 'info');
|
||||
log('Worker Pool 统计信息:', 'info');
|
||||
log(` 池大小: ${stats.poolSize}`, 'info');
|
||||
log(` 已完成: ${stats.tasksCompleted}`, 'info');
|
||||
log(` 失败: ${stats.tasksFailed}`, 'info');
|
||||
log(` 降级: ${stats.tasksFallback}`, 'info');
|
||||
log(` 平均 Worker 耗时: ${stats.avgWorkerTime.toFixed(2)} ms`, 'info');
|
||||
log(` 平均主线程耗时: ${stats.avgMainThreadTime.toFixed(2)} ms`, 'info');
|
||||
log('========================================', 'info');
|
||||
}
|
||||
|
||||
function clearResults() {
|
||||
document.getElementById('results-grid').innerHTML = '';
|
||||
document.getElementById('chart-area').innerHTML = '<p style="color: #666; text-align: center; padding: 20px;">运行测试后将显示图表...</p>';
|
||||
document.getElementById('console-log').innerHTML = '等待测试...\n';
|
||||
}
|
||||
|
||||
window.addEventListener('load', function() {
|
||||
log('页面加载完成', 'success');
|
||||
log(`Worker Pool 已初始化,池大小: ${window.markdownWorkerPool.config.poolSize}`, 'info');
|
||||
log(`当前配置 minTextLength: ${window.markdownWorkerPool.config.minTextLength} 字符`, 'info');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user