feat(FN-1532): add performance indexes for dashboard boot queries

- Add composite indexes for dashboard boot query paths
- Optimize taskList queries with (column, updatedAt) and (column, status, updatedAt) indexes
- Optimize activityLog queries with (timestamp DESC, taskId) index
- Add idempotent CREATE INDEX IF NOT EXISTS pattern for migrations
- Document index patterns in memory.md for future schema changes
- Update schema version assertions in db.test.ts and task-documents.test.ts
- Add changeset for dashboard load performance improvement
This commit is contained in:
gsxdsm
2026-04-10 07:26:13 -07:00
parent 31aaa0ec5c
commit bb679d5aa1
8 changed files with 215 additions and 15 deletions

View File

@@ -51,7 +51,7 @@ describe("TaskStore task documents", () => {
expect(tableNames.has("task_documents")).toBe(true);
expect(tableNames.has("task_document_revisions")).toBe(true);
expect(db.getSchemaVersion()).toBe(27);
expect(db.getSchemaVersion()).toBe(28);
const index = db
.prepare(

View File

@@ -86,12 +86,17 @@ describe("Database", () => {
expect(indexNames).toContain("idxActivityLogTimestamp");
expect(indexNames).toContain("idxActivityLogType");
expect(indexNames).toContain("idxActivityLogTaskId");
expect(indexNames).toContain("idxActivityLogTaskIdTimestamp");
expect(indexNames).toContain("idxActivityLogTypeTimestamp");
expect(indexNames).toContain("idxArchivedTasksId");
expect(indexNames).toContain("idxAgentHeartbeatsAgentId");
expect(indexNames).toContain("idxAgentHeartbeatsAgentIdTimestamp");
expect(indexNames).toContain("idxAgentHeartbeatsRunId");
expect(indexNames).toContain("idxAiSessionsStatus");
expect(indexNames).toContain("idxAiSessionsStatusUpdatedAt");
expect(indexNames).toContain("idxAiSessionsType");
expect(indexNames).toContain("idxAiSessionsLock");
expect(indexNames).toContain("idxAgentsState");
expect(indexNames).toContain("idxMessagesCreatedAt");
expect(indexNames).toContain("idxMessagesFrom");
expect(indexNames).toContain("idxMessagesTo");
@@ -103,10 +108,11 @@ describe("Database", () => {
expect(indexNames).toContain("idxTaskDocumentsTaskKey");
expect(indexNames).toContain("idxTaskDocumentsTaskId");
expect(indexNames).toContain("idxTaskDocumentRevisionsTaskKey");
expect(indexNames).toContain("idxTasksCreatedAt");
});
it("seeds schema version", () => {
expect(db.getSchemaVersion()).toBe(27);
expect(db.getSchemaVersion()).toBe(28);
});
it("seeds lastModified", () => {
@@ -129,7 +135,7 @@ describe("Database", () => {
it("is idempotent - calling init() twice does not fail", () => {
expect(() => db.init()).not.toThrow();
expect(db.getSchemaVersion()).toBe(27);
expect(db.getSchemaVersion()).toBe(28);
});
it("does not overwrite existing config on re-init", () => {
@@ -735,8 +741,8 @@ describe("schema migrations", () => {
// Now run init() which should trigger migration
db.init();
// Verify version bumped to 27 (includes v1→v2 through v26→v27)
expect(db.getSchemaVersion()).toBe(27);
// Verify version bumped to 28 (includes v1→v2 through v26→v28)
expect(db.getSchemaVersion()).toBe(28);
// Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -761,11 +767,11 @@ describe("schema migrations", () => {
const db = new Database(kbDir);
db.init();
expect(db.getSchemaVersion()).toBe(27);
expect(db.getSchemaVersion()).toBe(28);
// Re-init should not fail
db.init();
expect(db.getSchemaVersion()).toBe(27);
expect(db.getSchemaVersion()).toBe(28);
db.close();
});
@@ -781,7 +787,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(27);
expect(db.getSchemaVersion()).toBe(28);
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'agentRatings'").all() as Array<{ name: string }>;
expect(tables).toEqual([{ name: "agentRatings" }]);
@@ -805,7 +811,7 @@ describe("schema migrations", () => {
db.init();
expect(db.getSchemaVersion()).toBe(27);
expect(db.getSchemaVersion()).toBe(28);
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'mission_events'").all() as Array<{ name: string }>;
expect(tables).toEqual([{ name: "mission_events" }]);
@@ -908,8 +914,8 @@ describe("schema migrations", () => {
// Now run init() which should trigger migrations v2→v3→v4
db.init();
// Verify version bumped to 27
expect(db.getSchemaVersion()).toBe(27);
// Verify version bumped to 28
expect(db.getSchemaVersion()).toBe(28);
// Verify new columns exist and existing data is intact
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
@@ -1275,7 +1281,7 @@ describe("createDatabase factory", () => {
const db = createDatabase(kbDir);
db.init();
expect(db.getSchemaVersion()).toBe(27);
expect(db.getSchemaVersion()).toBe(28);
expect(db.getLastModified()).toBeGreaterThan(0);
db.close();

View File

@@ -59,7 +59,7 @@ export function fromJson<T>(json: string | null | undefined): T | undefined {
// ── Schema Definition ────────────────────────────────────────────────
const SCHEMA_VERSION = 27;
const SCHEMA_VERSION = 28;
function normalizeTaskComments(
steeringComments: SteeringComment[] | undefined,
@@ -990,6 +990,43 @@ export class Database {
this.db.exec(`CREATE INDEX IF NOT EXISTS idxRoutinesEnabled ON routines(enabled)`);
});
}
// Dashboard load performance indexes (FN-1532)
// Added indexes to eliminate full table scans and temp B-tree sorts
// in boot-critical query paths (listTasks, listActive, activityLog, agents)
if (version < 28) {
this.applyMigration(28, () => {
// Index on tasks.createdAt to avoid temp B-tree sort for ORDER BY createdAt
this.db.exec(`CREATE INDEX IF NOT EXISTS idxTasksCreatedAt ON tasks(createdAt)`);
// Composite index on ai_sessions for status filter + updatedAt ordering
// Covers: WHERE status IN (...) ORDER BY updatedAt DESC
// Only create if the table exists (it was added in v9)
if (this.hasTable("ai_sessions")) {
this.db.exec(`CREATE INDEX IF NOT EXISTS idxAiSessionsStatusUpdatedAt ON ai_sessions(status, updatedAt DESC)`);
}
// Composite index on activityLog for taskId filter + timestamp ordering
// Covers: WHERE taskId = ? ORDER BY timestamp DESC
if (this.hasTable("activityLog")) {
this.db.exec(`CREATE INDEX IF NOT EXISTS idxActivityLogTaskIdTimestamp ON activityLog(taskId, timestamp DESC)`);
this.db.exec(`CREATE INDEX IF NOT EXISTS idxActivityLogTypeTimestamp ON activityLog(type, timestamp DESC)`);
}
// Composite index on agentHeartbeats for agentId filter + timestamp ordering
// Covers: WHERE agentId = ? ORDER BY timestamp DESC
// Only create if the table exists (it was added in v2)
if (this.hasTable("agentHeartbeats")) {
this.db.exec(`CREATE INDEX IF NOT EXISTS idxAgentHeartbeatsAgentIdTimestamp ON agentHeartbeats(agentId, timestamp DESC)`);
}
// Index on agents.state for state filtering
// Covers: WHERE state = ?
if (this.hasTable("agents")) {
this.db.exec(`CREATE INDEX IF NOT EXISTS idxAgentsState ON agents(state)`);
}
});
}
}
/**

View File

@@ -464,8 +464,8 @@ describe("Run Audit", () => {
expect(indexNames).toContain("idxRunAuditEventsTimestamp");
});
it("schema version is bumped to 25", () => {
expect(db.getSchemaVersion()).toBe(27);
it("schema version is bumped to 28", () => {
expect(db.getSchemaVersion()).toBe(28);
});
});
});