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:
wing
2025-11-13 23:25:40 +08:00
parent fccbb78641
commit ca166ceb0a
7 changed files with 3170 additions and 1 deletions
+1 -1
View File
@@ -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%)
---
+402
View File
@@ -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() - 清空数据');
+436
View File
@@ -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>
+571
View File
@@ -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>