feat: v0.2.4 异步Compaction + 槽位压缩 + EXPLAIN + 慢查询日志
This commit is contained in:
@@ -142,6 +142,7 @@ export class MetonaSqlark {
|
||||
/** 执行 SQL 字符串查询 */
|
||||
async query(sql: string): Promise<unknown> {
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
// =======================================================================
|
||||
// 读取
|
||||
// =======================================================================
|
||||
|
||||
@@ -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 的行数据。
|
||||
*/
|
||||
|
||||
@@ -26,6 +26,7 @@ export class QueryExecutor {
|
||||
async execute(stmt: Statement): Promise<unknown> {
|
||||
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<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
|
||||
// ===================================================================
|
||||
|
||||
Reference in New Issue
Block a user