295 lines
13 KiB
HTML
295 lines
13 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>⚡ 性能基准 — MetonaSqlark v0.6.0</title>
|
||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'><rect width='32' height='32' rx='8' fill='%236366f1'/><text x='16' y='22' text-anchor='middle' font-size='20' fill='white'>◈</text></svg>">
|
||
<style>
|
||
:root {
|
||
--bg:#0a0a0f; --surface:#0d1117; --surface2:#161b22; --border:#30363d;
|
||
--primary:#6366f1; --accent:#06b6d4; --accent2:#ec4899; --green:#22c55e;
|
||
--text:#e2e8f0; --text2:#8b949e; --radius:12px;
|
||
--gradient:linear-gradient(135deg,#6366f1,#06b6d4,#ec4899);
|
||
}
|
||
* { margin:0; padding:0; box-sizing:border-box; }
|
||
body { font-family:'Inter',-apple-system,system-ui,sans-serif; background:var(--bg); color:var(--text); min-height:100vh; }
|
||
header {
|
||
height:56px; display:flex; align-items:center; justify-content:space-between; padding:0 24px;
|
||
background:var(--surface); border-bottom:1px solid var(--border); position:sticky; top:0; z-index:10;
|
||
}
|
||
.logo { font-weight:800; font-size:1.1rem; display:flex; align-items:center; gap:10px; }
|
||
.logo .icon { width:28px; height:28px; border-radius:7px; background:var(--gradient); display:flex; align-items:center; justify-content:center; font-size:0.85rem; font-weight:900; color:#fff; }
|
||
.logo span { background:var(--gradient); -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
|
||
nav { display:flex; gap:20px; }
|
||
nav a { color:var(--text2); text-decoration:none; font-size:0.88rem; transition:.2s; }
|
||
nav a:hover,.nav-active { color:var(--text); }
|
||
.container { max-width:1000px; margin:0 auto; padding:32px 24px 60px; }
|
||
.hero { margin-bottom:28px; }
|
||
.hero h1 { font-size:1.6rem; margin-bottom:8px; }
|
||
.hero p { color:var(--text2); font-size:0.92rem; line-height:1.7; }
|
||
.hero p code { background:var(--surface2); padding:2px 6px; border-radius:5px; font-size:0.85em; }
|
||
.controls { display:flex; gap:12px; align-items:center; margin-bottom:24px; flex-wrap:wrap; }
|
||
.btn {
|
||
display:inline-flex; align-items:center; gap:6px; padding:10px 22px; border-radius:8px;
|
||
font-weight:600; font-size:0.85rem; cursor:pointer; border:none; transition:.25s; font-family:inherit;
|
||
}
|
||
.btn-run { background:var(--gradient); color:#fff; box-shadow:0 4px 15px rgba(99,102,241,0.35); }
|
||
.btn-run:hover { transform:translateY(-1px); box-shadow:0 6px 25px rgba(99,102,241,0.5); }
|
||
.btn-run:active { transform:scale(0.97); }
|
||
.btn-run:disabled { opacity:0.5; cursor:not-allowed; transform:none; }
|
||
.btn-small { background:var(--surface2); color:var(--text2); border:1px solid var(--border); font-size:0.8rem; padding:8px 16px; }
|
||
.btn-small:hover { color:var(--text); border-color:var(--text2); }
|
||
select {
|
||
background:var(--surface2); color:var(--text); border:1px solid var(--border); border-radius:8px;
|
||
padding:9px 12px; font-size:0.85rem; font-family:inherit; outline:none;
|
||
}
|
||
.status { font-size:0.85rem; color:var(--text2); }
|
||
.status .spinner { display:inline-block; width:14px; height:14px; border:2px solid var(--border); border-top-color:var(--accent); border-radius:50%; animation:spin 0.8s linear infinite; vertical-align:-2px; }
|
||
@keyframes spin { to { transform:rotate(360deg); } }
|
||
table { width:100%; border-collapse:collapse; background:var(--surface); border-radius:var(--radius); overflow:hidden; border:1px solid var(--border); }
|
||
th,td { padding:12px 16px; text-align:left; font-size:0.88rem; border-bottom:1px solid var(--border); }
|
||
th { background:var(--surface2); color:var(--text2); font-size:0.78rem; text-transform:uppercase; letter-spacing:0.8px; white-space:nowrap; }
|
||
tr:last-child td { border-bottom:none; }
|
||
td.num { font-family:'JetBrains Mono',monospace; text-align:right; }
|
||
td.best { color:var(--green); font-weight:700; }
|
||
.group-header td { background:rgba(99,102,241,0.08); font-weight:700; color:var(--accent); }
|
||
.section { margin-bottom:32px; }
|
||
.section h2 { font-size:1.05rem; margin-bottom:14px; color:var(--text); display:flex; align-items:center; gap:8px; }
|
||
.section h2 .tag { font-size:0.68rem; padding:3px 8px; border-radius:999px; background:var(--surface2); border:1px solid var(--border); color:var(--text2); font-weight:600; }
|
||
.note { font-size:0.8rem; color:var(--text2); margin-top:14px; line-height:1.7; }
|
||
.note code { background:var(--surface2); padding:2px 5px; border-radius:4px; }
|
||
.summary-cards { display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:14px; margin-bottom:28px; }
|
||
.card { background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); padding:18px; }
|
||
.card .val { font-size:1.5rem; font-weight:800; background:var(--gradient); -webkit-background-clip:text; -webkit-text-fill-color:transparent; }
|
||
.card .lbl { font-size:0.78rem; color:var(--text2); margin-top:4px; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<div class="logo"><div class="icon">◈</div><span>MetonaSqlark</span></div>
|
||
<nav>
|
||
<a href="index.html">首页</a>
|
||
<a href="docs.html">文档</a>
|
||
<a href="demo.html">演示</a>
|
||
<a href="benchmark.html" class="nav-active">基准</a>
|
||
</nav>
|
||
</header>
|
||
|
||
<div class="container">
|
||
<div class="hero">
|
||
<h1>⚡ 性能基准测试</h1>
|
||
<p>在浏览器中实测 <code>Memory</code> 与 <code>Aria</code>(LSM-Tree 自研引擎)在纯内存 / IndexedDB / OPFS(页面化 4KB 存储)三种存储后端下的真实性能。
|
||
数据量分档(1K / 10K 行),测量 <code>INSERT</code> / 主键查询 / 索引查询 / <code>UPDATE</code> / <code>DELETE</code> 的吞吐(ops/sec)。
|
||
<strong>测试结果取决于你的设备与浏览器</strong>,仅供参考。磁盘后端结果反映真实持久化成本(WAL full 同步模式)。</p>
|
||
</div>
|
||
|
||
<div class="controls">
|
||
<button class="btn btn-run" id="run-btn" onclick="runAll()">▶ 运行完整基准</button>
|
||
<button class="btn btn-small" onclick="runAll(true)">⚡ 快速模式(仅 1K)</button>
|
||
<button class="btn btn-small" onclick="window.location.reload()">↻ 重置</button>
|
||
<span class="status" id="status"></span>
|
||
</div>
|
||
|
||
<div class="summary-cards" id="summary"></div>
|
||
|
||
<div class="section" id="result-section" style="display:none">
|
||
<h2>测试结果 <span class="tag">ops/sec</span></h2>
|
||
<table id="result-table">
|
||
<thead><tr><th>引擎</th><th>数据量</th><th>INSERT</th><th>主键查询</th><th>索引查询</th><th>UPDATE</th><th>DELETE</th></tr></thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
<p class="note">注:Memory = 纯内存 Map 引擎;Aria-memory = LSM-Tree(内存后端,无磁盘 I/O);
|
||
Aria-IndexedDB = LSM-Tree(IDB 后端,整 value SSTable);Aria-OPFS = LSM-Tree(OPFS 后端,<strong>4KB 页面化存储</strong> + BufferPool LRU 缓存 + WAL 分片真追加)。
|
||
磁盘后端使用 <code>walSyncMode: 'full'</code>(每条写入同步落盘,最安全的崩溃一致性)。
|
||
INSERT 为单行批量写入(含 WAL 记录);UPDATE/DELETE 按主键条件执行。</p>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="../dist/metona-sqlark.js"></script>
|
||
<script>
|
||
const M = window.MetonaSqlark;
|
||
const SIZES = [1000, 10000];
|
||
const SIZES_FAST = [1000];
|
||
|
||
// [label, mode, diskEngine]
|
||
const ENGINES = [
|
||
['memory', 'memory', 'memory'],
|
||
['aria-memory', 'aria', 'memory'],
|
||
['kvstore', 'disk', 'opfs'],
|
||
['aria-opfs', 'aria', 'opfs'],
|
||
];
|
||
|
||
let cancelled = false;
|
||
|
||
function setStatus(text, spin) {
|
||
const el = document.getElementById('status');
|
||
el.innerHTML = (spin ? '<span class="spinner"></span> ' : '') + text;
|
||
}
|
||
function setRunning(on) {
|
||
document.getElementById('run-btn').disabled = on;
|
||
}
|
||
|
||
function makeRows(n) {
|
||
const rows = [];
|
||
for (let i = 0; i < n; i++) {
|
||
rows.push({ id: 'k' + i, name: 'User' + i, age: 18 + (i % 60), city: 'City' + (i % 100) });
|
||
}
|
||
return rows;
|
||
}
|
||
|
||
async function createDb(mode, diskEngine) {
|
||
if (mode === 'memory') {
|
||
const db = new M.MetonaSqlark({ name: 'bench-mem-' + Date.now(), mode: 'memory' });
|
||
await db.init();
|
||
await db.defineTable('bench', {
|
||
id: { type: 'string', primaryKey: true },
|
||
name: { type: 'string' },
|
||
age: { type: 'number', index: true },
|
||
city: { type: 'string' },
|
||
});
|
||
return db;
|
||
}
|
||
// aria 模式:磁盘后端走真实持久化(WAL full 同步)
|
||
const db = await M.create({
|
||
name: 'bench-aria-' + diskEngine + '-' + Date.now(),
|
||
mode: 'aria',
|
||
diskEngine,
|
||
aria: { walSyncMode: 'full', checkpointInterval: 100000, memtableSizeThreshold: 64 * 1024 * 1024 },
|
||
});
|
||
await db.defineTable('bench', {
|
||
id: { type: 'string', primaryKey: true },
|
||
name: { type: 'string' },
|
||
age: { type: 'number', index: true },
|
||
city: { type: 'string' },
|
||
});
|
||
return db;
|
||
}
|
||
|
||
async function timeIt(fn) {
|
||
const start = performance.now();
|
||
await fn();
|
||
return performance.now() - start;
|
||
}
|
||
|
||
async function runBench(label, mode, diskEngine, size) {
|
||
const db = await createDb(mode, diskEngine);
|
||
const rows = makeRows(size);
|
||
const result = { mode: label, size, insert: 0, pk: 0, idx: 0, update: 0, del: 0 };
|
||
|
||
try {
|
||
// INSERT(批量)
|
||
let ms = await timeIt(() => db.table('bench').insertMany(rows));
|
||
result.insert = Math.round(size / (ms / 1000));
|
||
|
||
// 主键查询 × 300
|
||
ms = await timeIt(async () => {
|
||
for (let i = 0; i < 300; i++) await db.query(`SELECT * FROM bench WHERE id = 'k${(i * 37) % size}'`);
|
||
});
|
||
result.pk = Math.round(300 / (ms / 1000));
|
||
|
||
// 索引查询 × 300
|
||
ms = await timeIt(async () => {
|
||
for (let i = 0; i < 300; i++) await db.query(`SELECT * FROM bench WHERE age = ${18 + ((i * 17) % 60)}`);
|
||
});
|
||
result.idx = Math.round(300 / (ms / 1000));
|
||
|
||
// UPDATE × 150(按主键)
|
||
ms = await timeIt(async () => {
|
||
for (let i = 0; i < 150; i++) await db.query(`UPDATE bench SET name = 'X${i}' WHERE id = 'k${(i * 53) % size}'`);
|
||
});
|
||
result.update = Math.round(150 / (ms / 1000));
|
||
|
||
// DELETE × 150(按主键)
|
||
ms = await timeIt(async () => {
|
||
for (let i = 0; i < 150; i++) await db.query(`DELETE FROM bench WHERE id = 'k${(i * 71) % size}'`);
|
||
});
|
||
result.del = Math.round(150 / (ms / 1000));
|
||
} finally {
|
||
await db.close();
|
||
}
|
||
return result;
|
||
}
|
||
|
||
async function runAll(quick) {
|
||
if (cancelled) { cancelled = false; }
|
||
const sizes = quick ? SIZES_FAST : SIZES;
|
||
const btn = document.getElementById('run-btn');
|
||
btn.textContent = '⏳ 测试中…';
|
||
setRunning(true);
|
||
document.getElementById('result-section').style.display = 'none';
|
||
document.getElementById('summary').innerHTML = '';
|
||
|
||
const results = [];
|
||
for (const [label, mode, diskEngine] of ENGINES) {
|
||
for (const size of sizes) {
|
||
setStatus(`正在测试 ${label} · ${size.toLocaleString()} 行…`, true);
|
||
// 让 UI 刷新
|
||
await new Promise((r) => setTimeout(r, 30));
|
||
try {
|
||
const r = await runBench(label, mode, diskEngine, size);
|
||
results.push(r);
|
||
} catch (e) {
|
||
setStatus(`✗ ${label}/${size} 失败: ${e.message}`, false);
|
||
console.error(e);
|
||
}
|
||
}
|
||
}
|
||
|
||
renderResults(results);
|
||
setStatus('✓ 测试完成', false);
|
||
btn.textContent = '▶ 运行完整基准';
|
||
setRunning(false);
|
||
}
|
||
|
||
function renderResults(results) {
|
||
const tbody = document.querySelector('#result-table tbody');
|
||
tbody.innerHTML = '';
|
||
|
||
// 汇总卡片
|
||
const summary = document.getElementById('summary');
|
||
let bestInsert = 0, bestInsertMode = '', bestPk = 0, bestPkMode = '', totalTests = 0;
|
||
for (const r of results) {
|
||
totalTests++;
|
||
if (r.insert > bestInsert) { bestInsert = r.insert; bestInsertMode = r.mode; }
|
||
if (r.pk > bestPk) { bestPk = r.pk; bestPkMode = r.mode; }
|
||
}
|
||
summary.innerHTML = `
|
||
<div class="card"><div class="val">${results.length} 组</div><div class="lbl">测试组数</div></div>
|
||
<div class="card"><div class="val">${bestInsert.toLocaleString()} ops/s</div><div class="lbl">INSERT 峰值(${bestInsertMode})</div></div>
|
||
<div class="card"><div class="val">${bestPk.toLocaleString()} ops/s</div><div class="lbl">主键查询峰值(${bestPkMode})</div></div>
|
||
`;
|
||
|
||
// 表格(按引擎分组)
|
||
const GROUP_LABEL = {
|
||
'memory': '🚀 Memory 引擎',
|
||
'aria-memory': '🌲 Aria 引擎(内存后端)',
|
||
'kvstore': '💾 KVStore 引擎(disk 模式 · OPFS 介质)',
|
||
'aria-opfs': '🌲 Aria 引擎(OPFS · 4KB 页面化)',
|
||
};
|
||
let lastMode = '';
|
||
for (const r of results) {
|
||
if (r.mode !== lastMode) {
|
||
lastMode = r.mode;
|
||
const row = document.createElement('tr');
|
||
row.className = 'group-header';
|
||
row.innerHTML = `<td colspan="6">${GROUP_LABEL[r.mode] || r.mode}</td>`;
|
||
tbody.appendChild(row);
|
||
}
|
||
const row = document.createElement('tr');
|
||
const fmt = (v) => `<td class="num">${v ? v.toLocaleString() : '—'}</td>`;
|
||
row.innerHTML = `
|
||
<td>${r.mode}</td>
|
||
<td>${r.size.toLocaleString()} 行</td>
|
||
${fmt(r.insert)}${fmt(r.pk)}${fmt(r.idx)}${fmt(r.update)}${fmt(r.del)}
|
||
`;
|
||
tbody.appendChild(row);
|
||
}
|
||
|
||
document.getElementById('result-section').style.display = 'block';
|
||
document.getElementById('result-section').scrollIntoView({ behavior: 'smooth' });
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|