release: v0.3.2 — 质量加固 + SQL扩展 + 表达式 + 并发同步
v0.2.6 质量加固: - 修复 AriaEngine 二级索引 SSTable 互相覆盖(命名空间隔离) - 修复 LSM 多版本读取顺序错误 + MergeIterator 取最新来源 - 重写 LZ4 压缩器(往返一致性 + 缓冲区溢出) - sstableCache LRU 上限 + 预加载兜底(BufferPool 配置生效) - 修复 React/Vue 集成 import type 运行时 bug + exports 子路径 - 新增 38 个测试(LZ4往返/Crypto/集成), 删除伪测试 v0.3.0 SQL 功能扩展: - 多语句 parseAll + 事务语句 BEGIN/COMMIT/ROLLBACK - INSERT INTO ... SELECT + UNION/UNION ALL + EXISTS 关联子查询 - CREATE/DROP INDEX 五引擎实现 + 别名 WHERE 修复 - benchmark 页面 + 36 个新测试 v0.3.1 表达式与性能: - CASE WHEN 表达式(SELECT 列/WHERE/聚合) - JOIN + 关联子查询逐行绑定 - WAL 批量组提交(写放大 O(N)→O(1)) - 修复 pending frozen 可见性 + flush 缓存竞争 v0.3.2 并发: - CASE WHEN 用于 WHERE/聚合 + JOIN 哈希连接 - 多标签页同步(multiTabSync + BroadcastChannel) - IndexedDB schema 持久化(reopen 后表结构恢复) - 修复 where-matcher 顶层 $not - 修复 CJS 产物 .js 被 ESM 解析(exports 空) — .cjs 后缀 + exports 修正 - 836 测试 / 44 套件 / 81.0% 覆盖率
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
<!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.3.2</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>
|
||||
Reference in New Issue
Block a user