新增: - AriaEngine 外键级联(CASCADE/SET NULL/RESTRICT)+ clearAll() 重置 API - 引擎级 alterTable:Aria DROP COLUMN 重写存储行 + schema 持久化 - 流式查询 queryStream / findStream(LSM 惰性扫描不物化) - FROM 派生表 / 多列 ON 哈希连接 / COUNT(DISTINCT) / NULLS FIRST/LAST - 普通列别名 + ORDER BY 别名 + 无表查询 + 字符串常量列 - 演示页引擎切换器(Memory/Aria)+ 预设自动重置 修复: - Aria WAL DROP_TABLE 崩溃恢复(删表复活)+ 恢复后 WAL 截断 - Memory update/delete 索引维护(unique 约束绕过) - 关联 EXISTS 绑定失效 / HAVING 标量子查询 / INSERT SELECT 位置错位 - 裸布尔列条件(WHERE done / CASE WHEN done) - Aria $in 重复行 / JOIN 主表 WHERE 下推 / DROP INDEX 报错 - ORDER BY/GROUP BY/SELECT 表前缀列 + SQL '' 标准转义 质量:894 测试 · 47 套件 · 81.5% 覆盖率
267 lines
12 KiB
HTML
267 lines
12 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.4.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>Aria</code>(LSM-Tree 自研引擎)两种模式的真实性能。
|
||
数据量分三档(1K / 10K / 50K 行),测量 <code>INSERT</code> / 主键查询 / 索引查询 / <code>UPDATE</code> / <code>DELETE</code> 的吞吐(ops/sec)。
|
||
<strong>测试结果取决于你的设备与浏览器</strong>,仅供参考。</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/10K)</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 模式为 LSM-Tree + MemTable 存储(<code>storageBackend: 'memory'</code>,无磁盘 I/O)。
|
||
INSERT 为单行批量写入(含 WAL 记录);UPDATE/DELETE 按主键条件执行。</p>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="../dist/metona-sqlark.js"></script>
|
||
<script>
|
||
const M = window.MetonaSqlark;
|
||
const SIZES = [1000, 10000, 50000];
|
||
const SIZES_FAST = [1000, 10000];
|
||
|
||
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) {
|
||
const db = new M.MetonaSqlark({
|
||
name: 'bench-' + mode + '-' + Date.now(),
|
||
mode,
|
||
diskEngine: '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;
|
||
}
|
||
|
||
async function timeIt(fn) {
|
||
const start = performance.now();
|
||
await fn();
|
||
return performance.now() - start;
|
||
}
|
||
|
||
async function runBench(mode, size) {
|
||
const db = await createDb(mode);
|
||
const rows = makeRows(size);
|
||
const result = { mode, 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));
|
||
|
||
// 主键查询 × 500
|
||
ms = await timeIt(async () => {
|
||
for (let i = 0; i < 500; i++) await db.query(`SELECT * FROM bench WHERE id = 'k${(i * 37) % size}'`);
|
||
});
|
||
result.pk = Math.round(500 / (ms / 1000));
|
||
|
||
// 索引查询 × 500
|
||
ms = await timeIt(async () => {
|
||
for (let i = 0; i < 500; i++) await db.query(`SELECT * FROM bench WHERE age = ${18 + ((i * 17) % 60)}`);
|
||
});
|
||
result.idx = Math.round(500 / (ms / 1000));
|
||
|
||
// UPDATE × 200(按主键)
|
||
ms = await timeIt(async () => {
|
||
for (let i = 0; i < 200; i++) await db.query(`UPDATE bench SET name = 'X${i}' WHERE id = 'k${(i * 53) % size}'`);
|
||
});
|
||
result.update = Math.round(200 / (ms / 1000));
|
||
|
||
// DELETE × 200(按主键)
|
||
ms = await timeIt(async () => {
|
||
for (let i = 0; i < 200; i++) await db.query(`DELETE FROM bench WHERE id = 'k${(i * 71) % size}'`);
|
||
});
|
||
result.del = Math.round(200 / (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 mode of ['memory', 'aria']) {
|
||
for (const size of sizes) {
|
||
setStatus(`正在测试 ${mode === 'memory' ? 'Memory' : 'Aria'} 引擎 · ${size.toLocaleString()} 行…`, true);
|
||
// 让 UI 刷新
|
||
await new Promise((r) => setTimeout(r, 30));
|
||
try {
|
||
const r = await runBench(mode, size);
|
||
results.push(r);
|
||
} catch (e) {
|
||
setStatus(`✗ ${mode}/${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>
|
||
`;
|
||
|
||
// 表格(按引擎分组)
|
||
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">${r.mode === 'memory' ? '🚀 Memory 引擎' : '🌲 Aria 引擎(LSM-Tree)'}</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>
|