Port performance improvements from PR #15

- SQLite performance pragmas: synchronous=NORMAL, 64MB cache,
  memory temp store, 256MB mmap (safe with WAL mode)
- Batch insert for unresolved refs: single transaction instead of
  N individual inserts per file
- Symbol caching (warmCaches): pre-load all nodes into memory maps
  before resolution, eliminating repeated SQLite queries per ref
- Async file I/O: fs.stat/readFile in indexFile() are now non-blocking
- Denormalize filePath/language onto UnresolvedReference: avoids N
  node lookups during resolution, with schema migration v2

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Colby McHenry
2026-02-09 23:45:34 -06:00
co-authored by Claude Opus 4.6
parent e07250ed60
commit d80900f653
7 changed files with 149 additions and 23 deletions
+10
View File
@@ -41,6 +41,11 @@ export class DatabaseConnection {
// Wait up to 2 minutes if database is locked by another process
// (indexing operations can hold locks for extended periods)
db.pragma('busy_timeout = 120000');
// Performance tuning
db.pragma('synchronous = NORMAL'); // Safe with WAL mode
db.pragma('cache_size = -64000'); // 64 MB page cache
db.pragma('temp_store = MEMORY'); // Temp tables in memory
db.pragma('mmap_size = 268435456'); // 256 MB memory-mapped I/O
// Run schema initialization
const schemaPath = path.join(__dirname, 'schema.sql');
@@ -66,6 +71,11 @@ export class DatabaseConnection {
// Wait up to 2 minutes if database is locked by another process
// (indexing operations can hold locks for extended periods)
db.pragma('busy_timeout = 120000');
// Performance tuning
db.pragma('synchronous = NORMAL');
db.pragma('cache_size = -64000');
db.pragma('temp_store = MEMORY');
db.pragma('mmap_size = 268435456');
// Check and run migrations if needed
const conn = new DatabaseConnection(db, dbPath);
+11 -12
View File
@@ -9,7 +9,7 @@ import Database from 'better-sqlite3';
/**
* Current schema version
*/
export const CURRENT_SCHEMA_VERSION = 1;
export const CURRENT_SCHEMA_VERSION = 2;
/**
* Migration definition
@@ -27,17 +27,16 @@ interface Migration {
* Future migrations go here.
*/
const migrations: Migration[] = [
// Example migration for version 2 (when needed):
// {
// version: 2,
// description: 'Add support for module resolution',
// up: (db) => {
// db.exec(`
// ALTER TABLE nodes ADD COLUMN module_path TEXT;
// CREATE INDEX idx_nodes_module_path ON nodes(module_path);
// `);
// },
// },
{
version: 2,
description: 'Add filePath and language to unresolved_refs for performance',
up: (db) => {
db.exec(`
ALTER TABLE unresolved_refs ADD COLUMN file_path TEXT;
ALTER TABLE unresolved_refs ADD COLUMN language TEXT;
`);
},
},
];
/**
+47 -2
View File
@@ -73,6 +73,8 @@ interface UnresolvedRefRow {
reference_kind: string;
line: number;
col: number;
file_path: string | null;
language: string | null;
candidates: string | null;
}
@@ -422,6 +424,14 @@ export class QueryBuilder {
return rows.map(rowToNode);
}
/**
* Get all nodes in the database
*/
getAllNodes(): Node[] {
const rows = this.db.prepare('SELECT * FROM nodes').all() as NodeRow[];
return rows.map(rowToNode);
}
/**
* Search nodes by name using FTS with fallback to LIKE for better matching
*
@@ -778,8 +788,8 @@ export class QueryBuilder {
insertUnresolvedRef(ref: UnresolvedReference): void {
if (!this.stmts.insertUnresolved) {
this.stmts.insertUnresolved = this.db.prepare(`
INSERT INTO unresolved_refs (from_node_id, reference_name, reference_kind, line, col, candidates)
VALUES (@fromNodeId, @referenceName, @referenceKind, @line, @col, @candidates)
INSERT INTO unresolved_refs (from_node_id, reference_name, reference_kind, line, col, file_path, language, candidates)
VALUES (@fromNodeId, @referenceName, @referenceKind, @line, @col, @filePath, @language, @candidates)
`);
}
@@ -789,10 +799,41 @@ export class QueryBuilder {
referenceKind: ref.referenceKind,
line: ref.line,
col: ref.column,
filePath: ref.filePath ?? null,
language: ref.language ?? null,
candidates: ref.candidates ? JSON.stringify(ref.candidates) : null,
});
}
/**
* Insert multiple unresolved references in a single transaction
*/
insertUnresolvedRefsBatch(refs: UnresolvedReference[]): void {
if (refs.length === 0) return;
if (!this.stmts.insertUnresolved) {
this.stmts.insertUnresolved = this.db.prepare(`
INSERT INTO unresolved_refs (from_node_id, reference_name, reference_kind, line, col, file_path, language, candidates)
VALUES (@fromNodeId, @referenceName, @referenceKind, @line, @col, @filePath, @language, @candidates)
`);
}
this.db.transaction(() => {
for (const ref of refs) {
this.stmts.insertUnresolved!.run({
fromNodeId: ref.fromNodeId,
referenceName: ref.referenceName,
referenceKind: ref.referenceKind,
line: ref.line,
col: ref.column,
filePath: ref.filePath ?? null,
language: ref.language ?? null,
candidates: ref.candidates ? JSON.stringify(ref.candidates) : null,
});
}
})();
}
/**
* Delete unresolved references from a node
*/
@@ -821,6 +862,8 @@ export class QueryBuilder {
referenceKind: row.reference_kind as EdgeKind,
line: row.line,
column: row.col,
filePath: row.file_path ?? undefined,
language: (row.language as Language) ?? undefined,
candidates: row.candidates ? safeJsonParse<string[]>(row.candidates, []) : undefined,
}));
}
@@ -836,6 +879,8 @@ export class QueryBuilder {
referenceKind: row.reference_kind as EdgeKind,
line: row.line,
column: row.col,
filePath: row.file_path ?? undefined,
language: (row.language as Language) ?? undefined,
candidates: row.candidates ? safeJsonParse<string[]>(row.candidates, []) : undefined,
}));
}
+4
View File
@@ -11,6 +11,8 @@ CREATE TABLE IF NOT EXISTS schema_versions (
-- Insert initial version
INSERT INTO schema_versions (version, applied_at, description)
VALUES (1, strftime('%s', 'now') * 1000, 'Initial schema');
INSERT INTO schema_versions (version, applied_at, description)
VALUES (2, strftime('%s', 'now') * 1000, 'Add filePath and language to unresolved_refs');
-- =============================================================================
-- Core Tables
@@ -73,6 +75,8 @@ CREATE TABLE IF NOT EXISTS unresolved_refs (
reference_kind TEXT NOT NULL,
line INTEGER NOT NULL,
col INTEGER NOT NULL,
file_path TEXT,
language TEXT,
candidates TEXT, -- JSON array
FOREIGN KEY (from_node_id) REFERENCES nodes(id) ON DELETE CASCADE
);