515 lines
19 KiB
HTML
515 lines
19 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>LLM 性能测试工具</title>
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<style>
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100vh;
|
|
padding: 20px;
|
|
}
|
|
.container {
|
|
max-width: 1400px;
|
|
margin: 0 auto;
|
|
}
|
|
h1 {
|
|
text-align: center;
|
|
color: white;
|
|
margin-bottom: 30px;
|
|
font-size: 2.5em;
|
|
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
|
|
}
|
|
.card {
|
|
background: white;
|
|
border-radius: 16px;
|
|
padding: 25px;
|
|
margin-bottom: 20px;
|
|
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
|
|
}
|
|
.card h2 {
|
|
color: #333;
|
|
margin-bottom: 20px;
|
|
padding-bottom: 10px;
|
|
border-bottom: 3px solid #667eea;
|
|
}
|
|
.form-group {
|
|
margin-bottom: 15px;
|
|
}
|
|
label {
|
|
display: block;
|
|
margin-bottom: 5px;
|
|
color: #555;
|
|
font-weight: 600;
|
|
}
|
|
input, select, textarea {
|
|
width: 100%;
|
|
padding: 12px;
|
|
border: 2px solid #e0e0e0;
|
|
border-radius: 8px;
|
|
font-size: 14px;
|
|
transition: border-color 0.3s;
|
|
}
|
|
input:focus, select:focus, textarea:focus {
|
|
outline: none;
|
|
border-color: #667eea;
|
|
}
|
|
textarea {
|
|
min-height: 100px;
|
|
resize: vertical;
|
|
}
|
|
.btn {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border: none;
|
|
padding: 15px 30px;
|
|
border-radius: 8px;
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|
}
|
|
.btn:hover {
|
|
transform: translateY(-2px);
|
|
box-shadow: 0 5px 20px rgba(102, 126, 234, 0.4);
|
|
}
|
|
.btn:disabled {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
transform: none;
|
|
}
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
gap: 20px;
|
|
}
|
|
.metric-card {
|
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
|
padding: 20px;
|
|
border-radius: 12px;
|
|
text-align: center;
|
|
}
|
|
.metric-value {
|
|
font-size: 2.5em;
|
|
font-weight: bold;
|
|
color: #667eea;
|
|
}
|
|
.metric-label {
|
|
color: #666;
|
|
margin-top: 5px;
|
|
}
|
|
.chart-container {
|
|
position: relative;
|
|
height: 400px;
|
|
margin-top: 20px;
|
|
}
|
|
.progress-bar {
|
|
width: 100%;
|
|
height: 30px;
|
|
background: #e0e0e0;
|
|
border-radius: 15px;
|
|
overflow: hidden;
|
|
margin: 20px 0;
|
|
}
|
|
.progress-fill {
|
|
height: 100%;
|
|
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
|
transition: width 0.3s;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: white;
|
|
font-weight: bold;
|
|
}
|
|
.log-output {
|
|
background: #1e1e1e;
|
|
color: #d4d4d4;
|
|
padding: 15px;
|
|
border-radius: 8px;
|
|
font-family: 'Consolas', monospace;
|
|
font-size: 13px;
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
white-space: pre-wrap;
|
|
}
|
|
.status-running { color: #ffa500; }
|
|
.status-success { color: #4caf50; }
|
|
.status-error { color: #f44336; }
|
|
table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-top: 15px;
|
|
}
|
|
th, td {
|
|
padding: 12px;
|
|
text-align: left;
|
|
border-bottom: 1px solid #e0e0e0;
|
|
}
|
|
th {
|
|
background: #f5f5f5;
|
|
font-weight: 600;
|
|
}
|
|
tr:hover {
|
|
background: #f9f9f9;
|
|
}
|
|
.tabs {
|
|
display: flex;
|
|
gap: 10px;
|
|
margin-bottom: 20px;
|
|
}
|
|
.tab {
|
|
padding: 10px 20px;
|
|
background: #e0e0e0;
|
|
border: none;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
font-weight: 600;
|
|
transition: all 0.3s;
|
|
}
|
|
.tab.active {
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
}
|
|
.tab-content {
|
|
display: none;
|
|
}
|
|
.tab-content.active {
|
|
display: block;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>🚀 LLM 性能测试工具</h1>
|
|
|
|
<div class="tabs">
|
|
<button class="tab active" onclick="showTab('config')">⚙️ 配置</button>
|
|
<button class="tab" onclick="showTab('test')">🧪 测试</button>
|
|
<button class="tab" onclick="showTab('results')">📊 结果</button>
|
|
</div>
|
|
|
|
<!-- 配置页面 -->
|
|
<div id="config" class="tab-content active">
|
|
<div class="card">
|
|
<h2>API 配置</h2>
|
|
<div class="form-group">
|
|
<label>API 类型</label>
|
|
<select id="apiType">
|
|
<option value="openai">OpenAI API</option>
|
|
<option value="custom">自定义 API (兼容 OpenAI)</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>API Base URL</label>
|
|
<input type="text" id="apiBase" placeholder="https://api.openai.com/v1" value="https://api.openai.com/v1">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>API Key</label>
|
|
<input type="password" id="apiKey" placeholder="sk-...">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>模型配置</h2>
|
|
<div class="form-group">
|
|
<label>模型名称</label>
|
|
<input type="text" id="modelName" placeholder="gpt-3.5-turbo" value="gpt-3.5-turbo">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Temperature</label>
|
|
<input type="number" id="temperature" value="0.7" min="0" max="2" step="0.1">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Max Tokens</label>
|
|
<input type="number" id="maxTokens" value="1000" min="1" max="8192">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>测试用例</h2>
|
|
<div class="form-group">
|
|
<label>测试提示词</label>
|
|
<textarea id="testPrompt" placeholder="输入测试用的提示词...">请详细解释量子计算的基本原理,包括叠加态、纠缠和量子门等概念。</textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>并发请求数</label>
|
|
<input type="number" id="concurrency" value="5" min="1" max="50">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>每个并发请求次数</label>
|
|
<input type="number" id="requestsPerConcurrency" value="3" min="1" max="100">
|
|
</div>
|
|
<button class="btn" onclick="saveConfig()">💾 保存配置</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 测试页面 -->
|
|
<div id="test" class="tab-content">
|
|
<div class="card">
|
|
<h2>运行测试</h2>
|
|
<div class="form-group">
|
|
<label>测试名称</label>
|
|
<input type="text" id="testName" placeholder="测试 #1" value="性能测试 {{ now }}">
|
|
</div>
|
|
<button class="btn" id="runBtn" onclick="runTest()">▶️ 开始测试</button>
|
|
|
|
<div id="progressSection" style="display:none; margin-top: 20px;">
|
|
<div class="progress-bar">
|
|
<div class="progress-fill" id="progressBar" style="width: 0%">0%</div>
|
|
</div>
|
|
<div class="log-output" id="logOutput"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 结果页面 -->
|
|
<div id="results" class="tab-content">
|
|
<div class="card">
|
|
<h2>性能指标</h2>
|
|
<div class="grid" id="metricsGrid">
|
|
<div class="metric-card">
|
|
<div class="metric-value" id="ttftValue">-</div>
|
|
<div class="metric-label">TTFT (首Token时间)</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-value" id="tpsValue">-</div>
|
|
<div class="metric-label">TPS (每秒Token数)</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-value" id="latencyValue">-</div>
|
|
<div class="metric-label">平均延迟</div>
|
|
</div>
|
|
<div class="metric-card">
|
|
<div class="metric-value" id="totalTimeValue">-</div>
|
|
<div class="metric-label">总耗时</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>响应时间分布</h2>
|
|
<div class="chart-container">
|
|
<canvas id="latencyChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>TPS 趋势</h2>
|
|
<div class="chart-container">
|
|
<canvas id="tpsChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>详细结果</h2>
|
|
<table id="resultsTable">
|
|
<thead>
|
|
<tr>
|
|
<th>请求ID</th>
|
|
<th>状态</th>
|
|
<th>TTFT (ms)</th>
|
|
<th>TPS</th>
|
|
<th>总Token数</th>
|
|
<th>总耗时 (ms)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="resultsBody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
let currentTest = null;
|
|
let latencyChart = null;
|
|
let tpsChart = null;
|
|
|
|
function showTab(tabId) {
|
|
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
|
|
document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
|
|
event.target.classList.add('active');
|
|
document.getElementById(tabId).classList.add('active');
|
|
}
|
|
|
|
function saveConfig() {
|
|
const config = {
|
|
api_type: document.getElementById('apiType').value,
|
|
api_base: document.getElementById('apiBase').value,
|
|
api_key: document.getElementById('apiKey').value,
|
|
model: document.getElementById('modelName').value,
|
|
temperature: parseFloat(document.getElementById('temperature').value),
|
|
max_tokens: parseInt(document.getElementById('maxTokens').value),
|
|
prompt: document.getElementById('testPrompt').value,
|
|
concurrency: parseInt(document.getElementById('concurrency').value),
|
|
requests_per_concurrency: parseInt(document.getElementById('requestsPerConcurrency').value)
|
|
};
|
|
|
|
fetch('/api/config', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify(config)
|
|
})
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
alert('配置已保存!');
|
|
});
|
|
}
|
|
|
|
async function runTest() {
|
|
const btn = document.getElementById('runBtn');
|
|
const progressSection = document.getElementById('progressSection');
|
|
const logOutput = document.getElementById('logOutput');
|
|
const progressBar = document.getElementById('progressBar');
|
|
|
|
btn.disabled = true;
|
|
progressSection.style.display = 'block';
|
|
logOutput.textContent = '';
|
|
|
|
const testName = document.getElementById('testName').value;
|
|
|
|
try {
|
|
const response = await fetch('/api/run-test', {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json'},
|
|
body: JSON.stringify({name: testName})
|
|
});
|
|
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
|
|
while (true) {
|
|
const {done, value} = await reader.read();
|
|
if (done) break;
|
|
|
|
const text = decoder.decode(value);
|
|
const lines = text.split('\n');
|
|
|
|
for (const line of lines) {
|
|
if (line.trim()) {
|
|
try {
|
|
const data = JSON.parse(line.replace(/^data: /, ''));
|
|
if (data.log) {
|
|
logOutput.textContent += data.log + '\n';
|
|
logOutput.scrollTop = logOutput.scrollHeight;
|
|
}
|
|
if (data.progress) {
|
|
progressBar.style.width = data.progress + '%';
|
|
progressBar.textContent = Math.round(data.progress) + '%';
|
|
}
|
|
if (data.complete) {
|
|
currentTest = data.results;
|
|
displayResults(data.results);
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
}
|
|
} finally {
|
|
btn.disabled = false;
|
|
}
|
|
}
|
|
|
|
function displayResults(results) {
|
|
// 更新指标卡片
|
|
document.getElementById('ttftValue').textContent =
|
|
results.avg_ttft ? results.avg_ttft.toFixed(2) + 'ms' : '-';
|
|
document.getElementById('tpsValue').textContent =
|
|
results.avg_tps ? results.avg_tps.toFixed(2) : '-';
|
|
document.getElementById('latencyValue').textContent =
|
|
results.avg_latency ? results.avg_latency.toFixed(2) + 'ms' : '-';
|
|
document.getElementById('totalTimeValue').textContent =
|
|
results.total_time ? results.total_time.toFixed(2) + 's' : '-';
|
|
|
|
// 更新表格
|
|
const tbody = document.getElementById('resultsBody');
|
|
tbody.innerHTML = '';
|
|
results.requests.forEach((req, i) => {
|
|
const row = tbody.insertRow();
|
|
row.innerHTML = `
|
|
<td>${i + 1}</td>
|
|
<td class="${req.success ? 'status-success' : 'status-error'}">${req.success ? '✓' : '✗'}</td>
|
|
<td>${req.ttft?.toFixed(2) || '-'}</td>
|
|
<td>${req.tps?.toFixed(2) || '-'}</td>
|
|
<td>${req.total_tokens || '-'}</td>
|
|
<td>${req.total_time?.toFixed(2) || '-'}</td>
|
|
`;
|
|
});
|
|
|
|
// 绘制图表
|
|
drawCharts(results);
|
|
|
|
// 切换到结果页
|
|
document.querySelectorAll('.tab')[2].click();
|
|
}
|
|
|
|
function drawCharts(results) {
|
|
const requests = results.requests.filter(r => r.success);
|
|
|
|
// 延迟分布图
|
|
const ctx1 = document.getElementById('latencyChart').getContext('2d');
|
|
if (latencyChart) latencyChart.destroy();
|
|
|
|
latencyChart = new Chart(ctx1, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: requests.map((_, i) => `请求 ${i + 1}`),
|
|
datasets: [{
|
|
label: 'TTFT (ms)',
|
|
data: requests.map(r => r.ttft),
|
|
backgroundColor: 'rgba(102, 126, 234, 0.6)',
|
|
borderColor: 'rgba(102, 126, 234, 1)',
|
|
borderWidth: 1
|
|
}, {
|
|
label: '总耗时 (ms)',
|
|
data: requests.map(r => r.total_time),
|
|
backgroundColor: 'rgba(118, 75, 162, 0.6)',
|
|
borderColor: 'rgba(118, 75, 162, 1)',
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: { beginAtZero: true }
|
|
}
|
|
}
|
|
});
|
|
|
|
// TPS 趋势图
|
|
const ctx2 = document.getElementById('tpsChart').getContext('2d');
|
|
if (tpsChart) tpsChart.destroy();
|
|
|
|
tpsChart = new Chart(ctx2, {
|
|
type: 'line',
|
|
data: {
|
|
labels: requests.map((_, i) => `请求 ${i + 1}`),
|
|
datasets: [{
|
|
label: 'TPS',
|
|
data: requests.map(r => r.tps),
|
|
borderColor: 'rgba(102, 126, 234, 1)',
|
|
backgroundColor: 'rgba(102, 126, 234, 0.1)',
|
|
fill: true,
|
|
tension: 0.4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
y: { beginAtZero: true }
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
// 加载当前日期到测试名称
|
|
document.getElementById('testName').value = '性能测试 ' + new Date().toLocaleString('zh-CN');
|
|
</script>
|
|
</body>
|
|
</html>
|