From 82e6baaae9075841b7ef9913d99c84eb1a59c20b Mon Sep 17 00:00:00 2001 From: thzxx Date: Mon, 27 Jul 2026 21:53:13 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20v0.2.4=20=E5=BC=82=E6=AD=A5Compaction?= =?UTF-8?q?=20+=20=E6=A7=BD=E4=BD=8D=E5=8E=8B=E7=BC=A9=20+=20EXPLAIN=20+?= =?UTF-8?q?=20=E6=85=A2=E6=9F=A5=E8=AF=A2=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core.ts | 7 ++++++ src/engine/aria/index/lsm.ts | 27 +++++++++++++++++++-- src/engine/aria/page/slot.ts | 46 ++++++++++++++++++++++++++++++++++++ src/query/executor.ts | 26 ++++++++++++++++++++ 4 files changed, 104 insertions(+), 2 deletions(-) diff --git a/src/core.ts b/src/core.ts index 734b3ac..0682797 100644 --- a/src/core.ts +++ b/src/core.ts @@ -142,6 +142,7 @@ export class MetonaSqlark { /** 执行 SQL 字符串查询 */ async query(sql: string): Promise { this.ensureReady(); + const startTime = this.debug ? Date.now() : 0; await this.pluginManager.trigger('beforeQuery', sql); @@ -150,6 +151,12 @@ export class MetonaSqlark { await this.pluginManager.trigger('afterQuery', sql, result); + if (this.debug) { + const elapsed = Date.now() - startTime; + const rows = Array.isArray(result) ? (result as any[]).length : 0; + this._debug(`query [${elapsed}ms] ${rows} rows: ${sql.slice(0, 100)}`); + } + return result; } diff --git a/src/engine/aria/index/lsm.ts b/src/engine/aria/index/lsm.ts index e85f4ae..5f9d4ed 100644 --- a/src/engine/aria/index/lsm.ts +++ b/src/engine/aria/index/lsm.ts @@ -67,6 +67,7 @@ export class LSM { private sstableStore: SSTableStore; private operationCount = 0; private initialized = false; + private compacting = false; // 防止重复触发 compaction constructor(config: LSMConfig) { this.memtable = new MemTable(config.memtableSizeThreshold ?? DEFAULT_MEMTABLE_SIZE); @@ -182,11 +183,33 @@ export class LSM { this.levels[0].push(meta); this.immutableMemtable = null; - if (this.levels[0].length >= 4) { - this.compactLevelSync(0); + // 异步触发 compaction(不阻塞当前写入) + if (this.levels[0].length >= 4 && !this.compacting) { + this.scheduleCompact(0); } } + /** 异步调度 compaction,使用 setTimeout 分片执行 */ + private scheduleCompact(level: number): void { + if (level >= MAX_LSM_LEVELS - 1 || this.compacting) return; + this.compacting = true; + setTimeout(() => { + try { + this.compactLevelSync(level); + } finally { + this.compacting = false; + // 连续触发:如果 compaction 后仍然超标,继续调度 + if (this.levels[level].length >= 4) { + this.scheduleCompact(level); + } + // 检查下一级是否需要 compaction + if (level + 1 < MAX_LSM_LEVELS - 1 && this.levels[level + 1].length >= 4) { + this.scheduleCompact(level + 1); + } + } + }, 0); + } + // ======================================================================= // 读取 // ======================================================================= diff --git a/src/engine/aria/page/slot.ts b/src/engine/aria/page/slot.ts index cead954..69bdc07 100644 --- a/src/engine/aria/page/slot.ts +++ b/src/engine/aria/page/slot.ts @@ -128,6 +128,52 @@ export function freeSlot(buf: ArrayBuffer, slotIndex: number): void { setSlotEntry(buf, slotIndex, { offset: 0, length: 0 }); } +/** + * 压缩页面槽位:移除已删除 slot,整理碎片空间。 + * 将有效数据紧凑排列,释放空洞。 + */ +export function compactSlots(buf: ArrayBuffer): number { + const view = new DataView(buf); + const slotCount = view.getUint16(9, false); + if (slotCount === 0) return 0; + + // 收集有效 slot(offset>0 的) + const validSlots: { index: number; offset: number; length: number; data: Uint8Array }[] = []; + for (let i = 0; i < slotCount; i++) { + const entry = getSlotEntry(buf, i); + if (entry.offset > 0 && entry.length > 0) { + const data = new Uint8Array(buf, entry.offset, entry.length); + validSlots.push({ index: i, offset: entry.offset, length: entry.length, data: new Uint8Array(data) }); + } + } + + if (validSlots.length === slotCount) return 0; // 无碎片 + + // 从页面底部重新紧凑排列 + let dataEnd = PAGE_SIZE; + const newSlots: { offset: number; length: number }[] = []; + + for (let i = validSlots.length - 1; i >= 0; i--) { + const s = validSlots[i]; + dataEnd -= s.length; + new Uint8Array(buf).set(s.data, dataEnd); + newSlots.unshift({ offset: dataEnd, length: s.length }); + } + + // 重写 slot directory + view.setUint16(9, validSlots.length, false); // slotCount + view.setUint16(7, dataEnd, false); // freeEnd + for (let i = 0; i < validSlots.length; i++) { + setSlotEntry(buf, i, newSlots[i]); + } + // 清除剩余 slot 条目 + for (let i = validSlots.length; i < slotCount; i++) { + setSlotEntry(buf, i, { offset: 0, length: 0 }); + } + + return slotCount - validSlots.length; // 回收的 slot 数量 +} + /** * 读取指定 slot 的行数据。 */ diff --git a/src/query/executor.ts b/src/query/executor.ts index e0e0f1a..6b44bd4 100644 --- a/src/query/executor.ts +++ b/src/query/executor.ts @@ -26,6 +26,7 @@ export class QueryExecutor { async execute(stmt: Statement): Promise { switch (stmt.type) { case 'SELECT': return this.executeSelect(stmt); + case 'EXPLAIN': return this.executeExplain(stmt as any); case 'INSERT': return this.executeInsert(stmt); case 'UPDATE': return this.executeUpdate(stmt); case 'DELETE': return this.executeDelete(stmt); @@ -35,6 +36,31 @@ export class QueryExecutor { } } + /** EXPLAIN: 输出查询计划 */ + private async executeExplain(stmt: { query: Statement }): Promise> { + const plan = compileStatement(stmt.query.type === 'SELECT' ? stmt.query : stmt.query as any); + const startTime = Date.now(); + let result: unknown = null; + try { + result = await this.execute(stmt.query); + } catch { /* explain 即使执行失败也返回计划 */ } + const elapsed = Date.now() - startTime; + const rows = Array.isArray(result) ? result.length : 0; + + return { + type: stmt.query.type, + table: plan.table, + columns: plan.columns, + where: plan.where || {}, + orderBy: plan.orderBy || [], + limit: plan.limit, + offset: plan.offset, + usingIndex: plan.table ? 'auto' : 'none', + estimatedRows: rows, + actualTimeMs: elapsed, + }; + } + // =================================================================== // SELECT // ===================================================================