Skip to content

Commit eb75e90

Browse files
committed
refactor(@angular/build): optimize pruning in sqlite cache store
Previously, cache eviction routines during close executed as independent autocommit transactions. In addition, the LRU size pruning query calculated running payload sizes with a window function across the entire cache table on every build, even when total database size was well below maxPayloadSize. Pruning operations in close are now wrapped in an immediate transaction to ensure atomicity and reduce transaction commit overhead. Furthermore, total database size is checked using SQLite page pragmas prior to size-based pruning, allowing the expensive window aggregate query to be skipped when the total cache size is already within limits.
1 parent 29b9bff commit eb75e90

2 files changed

Lines changed: 58 additions & 17 deletions

File tree

packages/angular/build/src/tools/esbuild/sqlite-cache-store.ts

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -146,22 +146,45 @@ export class SqliteCacheStore implements PersistentCacheStore<unknown> {
146146
// Flush any pending access updates in one transaction before pruning
147147
this.#flushAccessUpdates();
148148

149-
// 1. Delete items older than N days
150-
this.#db
151-
.prepare("DELETE FROM cache WHERE last_accessed < unixepoch('now', ?);")
152-
.run(`-${this.ttlDays} days`);
153-
154-
// 2. Prune oldest items if payload exceeds maxPayloadSize
155-
const pruneStmt = this.#db.prepare(`
156-
DELETE FROM cache WHERE key IN (
157-
SELECT key FROM (
158-
SELECT key,
159-
sum(length(key) + length(value)) OVER (ORDER BY last_accessed DESC, key DESC) as running_size
160-
FROM cache
161-
) WHERE running_size > ?
162-
);
163-
`);
164-
pruneStmt.run(this.maxPayloadSize);
149+
this.#db.exec('BEGIN IMMEDIATE TRANSACTION;');
150+
try {
151+
// 1. Delete items older than N days
152+
this.#db
153+
.prepare("DELETE FROM cache WHERE last_accessed < unixepoch('now', ?);")
154+
.run(`-${this.ttlDays} days`);
155+
156+
// 2. Prune oldest items if payload exceeds maxPayloadSize
157+
// Skip the expensive window aggregate query if total database size is below maxPayloadSize
158+
const sizeResult = this.#db
159+
.prepare(
160+
'SELECT (page_count - freelist_count) * page_size AS total_size ' +
161+
'FROM pragma_page_count(), pragma_freelist_count(), pragma_page_size();',
162+
)
163+
.get() as { total_size?: number } | undefined;
164+
165+
if ((sizeResult?.total_size ?? 0) > this.maxPayloadSize) {
166+
this.#db
167+
.prepare(
168+
`DELETE FROM cache WHERE key IN (
169+
SELECT key FROM (
170+
SELECT key,
171+
sum(length(key) + length(value)) OVER (ORDER BY last_accessed DESC, key DESC) as running_size
172+
FROM cache
173+
) WHERE running_size > ?
174+
);`,
175+
)
176+
.run(this.maxPayloadSize);
177+
}
178+
179+
this.#db.exec('COMMIT;');
180+
} catch (error) {
181+
try {
182+
this.#db.exec('ROLLBACK;');
183+
} catch {
184+
// Ignore rollback errors if transaction was not active
185+
}
186+
throw error;
187+
}
165188
} catch {
166189
// Pruning errors should not block build success
167190
} finally {
@@ -176,7 +199,11 @@ export class SqliteCacheStore implements PersistentCacheStore<unknown> {
176199
this.#setStmt = undefined;
177200
this.#updateAccessedStmt = undefined;
178201

179-
this.#db.close();
202+
try {
203+
this.#db.close();
204+
} catch {
205+
// Failure to close should not block build success
206+
}
180207
this.#db = undefined;
181208
}
182209
}

packages/angular/build/src/tools/esbuild/sqlite-cache-store_spec.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,20 @@ describe('SqliteCacheStore', () => {
187187
checkStore.close();
188188
});
189189

190+
it('should not prune items when total database size is within maxPayloadSize on close', async () => {
191+
store.close();
192+
193+
const sizeStore = new SqliteCacheStore(cachePath, 1024 * 1024);
194+
await sizeStore.set('k1', 'value1');
195+
await sizeStore.set('k2', 'value2');
196+
sizeStore.close();
197+
198+
const checkStore = new SqliteCacheStore(cachePath);
199+
expect(checkStore.has('k1')).toBeTrue();
200+
expect(checkStore.has('k2')).toBeTrue();
201+
checkStore.close();
202+
});
203+
190204
it('should create an index on last_accessed and key', async () => {
191205
// Trigger db initialization
192206
await store.set('test-key', 'test-value');

0 commit comments

Comments
 (0)