Files
MetonaSqlark/site/benchmark.html
T
thzxx a56a496148
CI / test (18.x) (push) Successful in 11m2s
CI / test (20.x) (push) Successful in 10m55s
CI / test (22.x) (push) Successful in 10m49s
CI / test (24.x) (push) Successful in 10m53s
CI / e2e (push) Successful in 9m51s
docs: README/site 全量同步 v0.6.1(版本徽章/套件数/残留 IndexedDB 描述清理/OPFSEngine 列移除/v0.6.1 里程碑/级联环与原子事务特性)
2026-08-10 14:35:47 +08:00

295 lines
13 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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.1</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>KVStore</code>(自研 KV 引擎)与 <code>Aria</code>(LSM-Tree 自研引擎)在纯内存 / 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-OPFS = LSM-TreeOPFS 后端,<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>