feat: v0.2.4 异步Compaction + 槽位压缩 + EXPLAIN + 慢查询日志
CI / test (18.x) (push) Failing after 5m0s
CI / test (20.x) (push) Failing after 4m58s
CI / test (22.x) (push) Failing after 4m58s
CI / test (24.x) (push) Failing after 4m57s

This commit is contained in:
thzxx
2026-07-27 21:53:13 +08:00
parent da999d607b
commit 82e6baaae9
4 changed files with 104 additions and 2 deletions
+7
View File
@@ -142,6 +142,7 @@ export class MetonaSqlark {
/** 执行 SQL 字符串查询 */ /** 执行 SQL 字符串查询 */
async query(sql: string): Promise<unknown> { async query(sql: string): Promise<unknown> {
this.ensureReady(); this.ensureReady();
const startTime = this.debug ? Date.now() : 0;
await this.pluginManager.trigger('beforeQuery', sql); await this.pluginManager.trigger('beforeQuery', sql);
@@ -150,6 +151,12 @@ export class MetonaSqlark {
await this.pluginManager.trigger('afterQuery', sql, result); 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; return result;
} }
+25 -2
View File
@@ -67,6 +67,7 @@ export class LSM {
private sstableStore: SSTableStore; private sstableStore: SSTableStore;
private operationCount = 0; private operationCount = 0;
private initialized = false; private initialized = false;
private compacting = false; // 防止重复触发 compaction
constructor(config: LSMConfig) { constructor(config: LSMConfig) {
this.memtable = new MemTable(config.memtableSizeThreshold ?? DEFAULT_MEMTABLE_SIZE); this.memtable = new MemTable(config.memtableSizeThreshold ?? DEFAULT_MEMTABLE_SIZE);
@@ -182,11 +183,33 @@ export class LSM {
this.levels[0].push(meta); this.levels[0].push(meta);
this.immutableMemtable = null; this.immutableMemtable = null;
if (this.levels[0].length >= 4) { // 异步触发 compaction(不阻塞当前写入)
this.compactLevelSync(0); 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);
}
// ======================================================================= // =======================================================================
// 读取 // 读取
// ======================================================================= // =======================================================================
+46
View File
@@ -128,6 +128,52 @@ export function freeSlot(buf: ArrayBuffer, slotIndex: number): void {
setSlotEntry(buf, slotIndex, { offset: 0, length: 0 }); 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;
// 收集有效 slotoffset>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 的行数据。 * 读取指定 slot 的行数据。
*/ */
+26
View File
@@ -26,6 +26,7 @@ export class QueryExecutor {
async execute(stmt: Statement): Promise<unknown> { async execute(stmt: Statement): Promise<unknown> {
switch (stmt.type) { switch (stmt.type) {
case 'SELECT': return this.executeSelect(stmt); case 'SELECT': return this.executeSelect(stmt);
case 'EXPLAIN': return this.executeExplain(stmt as any);
case 'INSERT': return this.executeInsert(stmt); case 'INSERT': return this.executeInsert(stmt);
case 'UPDATE': return this.executeUpdate(stmt); case 'UPDATE': return this.executeUpdate(stmt);
case 'DELETE': return this.executeDelete(stmt); case 'DELETE': return this.executeDelete(stmt);
@@ -35,6 +36,31 @@ export class QueryExecutor {
} }
} }
/** EXPLAIN: 输出查询计划 */
private async executeExplain(stmt: { query: Statement }): Promise<Record<string, unknown>> {
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 // SELECT
// =================================================================== // ===================================================================