feat: v0.2.4 OPFS写锁 + 性能基准 + BufferPool集成 + REINDEX/VACUUM + 查询优化器
CI / test (20.x) (push) Failing after 4m59s
CI / test (18.x) (push) Failing after 4m59s
CI / test (22.x) (push) Failing after 4m56s
CI / test (24.x) (push) Failing after 4m58s

This commit is contained in:
thzxx
2026-07-27 22:00:42 +08:00
parent 791a5c7415
commit 3d30e8174d
11 changed files with 1592 additions and 43 deletions
+67
View File
@@ -0,0 +1,67 @@
/**
* AriaEngine Performance Benchmark
* 测量 insert/find/update/delete 在不同数据量级下的性能
*/
import { AriaEngine } from './index';
import { createSchema } from '../../table/schema';
interface BenchResult {
name: string;
rows: number;
ops: number;
totalMs: number;
opsPerSec: number;
}
function createTestSchema() {
return createSchema('bench', {
id: { type: 'string', primaryKey: true },
name: { type: 'string' },
value: { type: 'number', index: true },
});
}
async function timeIt(name: string, fn: () => Promise<void>): Promise<number> {
const start = performance.now();
await fn();
return performance.now() - start;
}
export async function runBenchmarks(): Promise<BenchResult[]> {
const results: BenchResult[] = [];
const sizes = [100, 1000, 10000, 100000];
for (const size of sizes) {
const engine = new AriaEngine({ storageBackend: 'memory' });
await engine.open('bench-db', 1);
await engine.createTable(createTestSchema());
// Insert
const rows = [];
for (let i = 0; i < size; i++) {
rows.push({ id: `${i}`, name: `User${i}`, value: i * 10 });
}
const insertMs = await timeIt(`insert-${size}`, () => engine.insert('bench', rows));
results.push({ name: `insert-${size}`, rows: size, ops: size, totalMs: insertMs, opsPerSec: Math.round(size / (insertMs / 1000)) });
// Find by PK
const findMs = await timeIt(`find-pk-${size}`, () => engine.find('bench', { table: 'bench', where: { id: `${size - 1}` } }));
results.push({ name: `find-pk-${size}`, rows: size, ops: 1, totalMs: findMs, opsPerSec: Math.round(1000 / findMs) });
// Find by index
const findIdxMs = await timeIt(`find-idx-${size}`, () => engine.find('bench', { table: 'bench', where: { value: (size - 1) * 10 } }));
results.push({ name: `find-idx-${size}`, rows: size, ops: 1, totalMs: findIdxMs, opsPerSec: Math.round(1000 / findIdxMs) });
// Update
const updateMs = await timeIt(`update-${size}`, () => engine.update('bench', { table: 'bench', where: { id: `${size - 1}` } }, { name: 'Updated' }));
results.push({ name: `update-${size}`, rows: size, ops: 1, totalMs: updateMs, opsPerSec: Math.round(1000 / updateMs) });
// Delete
const deleteMs = await timeIt(`delete-${size}`, () => engine.delete('bench', { table: 'bench', where: { id: `${size - 1}` } }));
results.push({ name: `delete-${size}`, rows: size, ops: 1, totalMs: deleteMs, opsPerSec: Math.round(1000 / deleteMs) });
await engine.close();
}
return results;
}
+97
View File
@@ -22,6 +22,7 @@ import { IndexedDBBackend, MemoryBackend, type IStorageBackend } from './store/b
import { OPFSBackend } from './store/opfs_backend';
import { MVCCManager } from './transaction/mvcc';
import { BloomFilter } from './index/bloom';
import { BufferPool, type PageIO } from './buffer/pool';
// ---------------------------------------------------------------------------
// AriaEngine
@@ -52,6 +53,9 @@ export class AriaEngine implements IStorageEngine {
private txnSnapshot: Map<string, Record<string, unknown>> | null = null;
private gcCounter = 0;
// Buffer Pool(页面缓存,减少磁盘 I/O)
private bufferPool!: BufferPool;
constructor(config: AriaEngineConfig = {}) {
this.config = { ...DEFAULT_ARIA_CONFIG, ...config };
}
@@ -74,6 +78,15 @@ export class AriaEngine implements IStorageEngine {
}
await this.backend.open(dbName);
// 2a. 初始化 Buffer Pool(页面缓存)
const pageIO: PageIO = {
readPage: async (pageId) => this.backend.read(`pg_${pageId}`),
writePage: async (pageId, data) => this.backend.write(`pg_${pageId}`, data),
allocatePageId: async () => Date.now(),
freePageId: async () => {},
};
this.bufferPool = new BufferPool(pageIO, this.config.bufferPoolPages);
// 2. 构建 SSTableStore
const sstableStore = this.createSSTableStore();
@@ -909,6 +922,90 @@ export class AriaEngine implements IStorageEngine {
return stats;
}
/**
* REINDEX: 重建指定表的所有二级索引
*/
async reindexTable(tableName: string): Promise<number> {
this.ensureOpen();
this.ensureTable(tableName);
const schema = this.schemas.get(tableName)!;
let rebuiltCount = 0;
for (const [colName, colDef] of Object.entries(schema.columns)) {
if (!colDef.index && !colDef.unique && !colDef.primaryKey) continue;
const idxKey = `${tableName}:idx:${colName}`;
const idxLsm = this.secondaryIndexes.get(idxKey);
if (!idxLsm) continue;
// 清空旧索引
await idxLsm.clear();
rebuiltCount++;
// 从主 LSM 重建索引
const rows = this.getAllRows(tableName);
for (const row of rows) {
const val = row[colName];
if (val !== undefined && val !== null) {
idxLsm.put(`${String(val)}:${row[this.tablePKs.get(tableName)!]}`, { pk: row[this.tablePKs.get(tableName)!] });
}
}
}
return rebuiltCount;
}
/**
* VACUUM: 压缩 LSM + 清理碎片
*/
async vacuum(): Promise<{ compactedLevels: number; gcVersions: number }> {
this.ensureOpen();
// 强制 flush memtable
await this.lsm.flush();
// 压缩各层级
for (let level = 0; level < 6; level++) {
if (this.lsm.getStats().levelCounts[level] >= 2) {
(this.lsm as any).compactLevelSync(level);
}
}
// GC MVCC 版本(保留最新 10 个)
const beforeGC = this.mvcc.getActiveTxnCount?.() ?? 0;
this.mvcc.gc(10);
return { compactedLevels: 6, gcVersions: beforeGC };
}
/**
* 查询优化器:估算各索引成本,选择最优方案
*/
estimateQueryCost(tableName: string, query: QueryPlan): { strategy: string; estimatedRows: number } {
const schema = this.schemas.get(tableName);
if (!schema || !query.where) return { strategy: 'full_scan', estimatedRows: 0 };
const pkCol = this.tablePKs.get(tableName)!;
for (const [col, condition] of Object.entries(query.where)) {
if (col === '$and' || col === '$or' || col === '$not') continue;
// PK 等值 → 最快,估计 1 行
if (col === pkCol && (typeof condition !== 'object' || (condition as any).$eq)) {
return { strategy: 'pk_lookup', estimatedRows: 1 };
}
// 索引列等值 → 快
const colDef = schema.columns[col];
if (colDef?.index || colDef?.unique) {
if (typeof condition !== 'object' || (condition as any).$eq) {
return { strategy: `index_eq:${col}`, estimatedRows: 1 };
}
if ((condition as any).$in && Array.isArray((condition as any).$in)) {
return { strategy: `index_in:${col}`, estimatedRows: (condition as any).$in.length };
}
if ((condition as any).$gt || (condition as any).$lt || (condition as any).$gte || (condition as any).$lte) {
return { strategy: `index_range:${col}`, estimatedRows: 100 };
}
}
}
return { strategy: 'full_scan', estimatedRows: 1000 };
}
private ensureOpen(): void {
if (!this.opened) throw new DatabaseError('AriaEngine not opened', 'DB_NOT_OPEN');
}
+12 -9
View File
@@ -13,6 +13,7 @@ export class OPFSBackend implements IStorageBackend {
private root: FileSystemDirectoryHandle | null = null;
private dbDir: FileSystemDirectoryHandle | null = null;
private dbName = '';
private writeQueue: Promise<void> = Promise.resolve();
async open(name: string): Promise<void> {
this.dbName = name;
@@ -42,19 +43,21 @@ export class OPFSBackend implements IStorageBackend {
async write(key: string, data: ArrayBuffer): Promise<void> {
if (!this.dbDir) return;
const fh = await this.dbDir.getFileHandle(key, { create: true });
const writable = await fh.createWritable();
await writable.write(data);
await writable.close();
this.writeQueue = this.writeQueue.then(async () => {
const fh = await this.dbDir!.getFileHandle(key, { create: true });
const writable = await fh.createWritable();
await writable.write(data);
await writable.close();
});
return this.writeQueue;
}
async delete(key: string): Promise<void> {
if (!this.dbDir) return;
try {
await this.dbDir.removeEntry(key);
} catch {
// 文件不存在则忽略
}
this.writeQueue = this.writeQueue.then(async () => {
try { await this.dbDir!.removeEntry(key); } catch { /* ignore */ }
});
return this.writeQueue;
}
async listKeys(): Promise<string[]> {