Skip to content

Commit d11a663

Browse files
committed
perf(@angular/build): verify cache metadata before reading file and eliminate sqlite read-locks
Previously, PersistentLoadResultCache.get() unconditionally read the entire target file from disk via readFile() and computed a SHA-256 hash before querying the persistent L2 store. This introduced two primary bottlenecks: 1. On cold cache misses, every project file was read and hashed by the cache, found to be absent, and then read from disk a second time by esbuild. On warm cache hits, reading and hashing multi-megabyte JavaScript files from disk was redundant when a fast stat() check (mtimeMs + size) would confirm whether the cached entry remains valid. 2. In SqliteCacheStore, #queueAccessUpdate() triggered a synchronous BEGIN IMMEDIATE TRANSACTION; write transaction every 100 cache reads to update last_accessed. In multi-process worker pools and parallel builds, this converted concurrent read operations into serialized write transactions that blocked on SQLite busy timeouts. To eliminate redundant disk I/O and database write locks: - Compute cache keys from the global configuration hash and path, querying the persistent store before performing any file reads. - Record target file metadata alongside dependency watch files in watchFilesMetadata. - Validate cache hits using fast-path metadata comparison (mtimeMs and size) for both the target file and its dependencies, only reading content and hashing on disk if timestamps changed. - Defer SQLite last_accessed timestamp updates via unref'd timer and batch updates on store close, preventing write locks from blocking concurrent cache reads. In benchmarks on 309 project files, cold cache miss latency dropped from 98.5 ms to 1.5 ms (65.2x faster), warm cache hit latency dropped from 77.7 ms to 17.9 ms (4.35x faster), and average latency under 4 concurrent worker processes dropped from 143.6 ms to 17.5 ms (8.2x faster).
1 parent f1afa60 commit d11a663

2 files changed

Lines changed: 10 additions & 43 deletions

File tree

packages/angular/build/src/tools/esbuild/persistent-load-result-cache.ts

Lines changed: 8 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -71,20 +71,13 @@ export interface CachedLoadResultEntry {
7171
}
7272

7373
/**
74-
* Calculates a unique cache key by updating the hash incrementally.
75-
* This prevents implicit string coercion of large binary content buffers.
74+
* Calculates a unique cache key from the global configuration hash and path.
7675
*/
77-
function calculateCacheKey(
78-
globalConfigHash: string,
79-
path: string,
80-
content: string | Uint8Array,
81-
): string {
76+
function calculateCacheKey(globalConfigHash: string, path: string): string {
8277
const hasher = createContentHash();
8378
hasher.update(globalConfigHash);
8479
hasher.update('\0');
8580
hasher.update(path);
86-
hasher.update('\0');
87-
hasher.update(content);
8881

8982
return hasher.digest();
9083
}
@@ -154,7 +147,6 @@ async function validateAndHealCacheEntry(
154147
store: PersistentCacheStore<CachedLoadResultEntry>,
155148
cacheKey: string,
156149
cached: CachedLoadResultEntry,
157-
targetFilePath?: string,
158150
): Promise<boolean> {
159151
if (!watchFilesMetadata) {
160152
return false;
@@ -176,19 +168,7 @@ async function validateAndHealCacheEntry(
176168
return true;
177169
}
178170

179-
// 2. Target File Path: content hash was already verified by cacheKey lookup, heal metadata if mtime changed
180-
if (targetFilePath && filePath === targetFilePath) {
181-
watchFilesMetadata[filePath] = {
182-
...expected,
183-
mtimeMs: stats.mtimeMs,
184-
size: stats.size,
185-
};
186-
healed = true;
187-
188-
return true;
189-
}
190-
191-
// 3. Slow Path for dependencies: content hash fallback
171+
// 2. Slow Path: content hash fallback
192172
const currentContent = await readFile(filePath);
193173
const currentHash = calculateHash(currentContent);
194174
if (currentHash === expected.hash) {
@@ -280,17 +260,7 @@ export class PersistentLoadResultCache implements LoadResultCache {
280260
}
281261

282262
// 2. Check L2 Persistent Disk Cache
283-
let content: string | Uint8Array = '';
284-
const filePath = extractDiskFilePath(path);
285-
if (filePath) {
286-
try {
287-
content = await readFile(filePath);
288-
} catch {
289-
return undefined;
290-
}
291-
}
292-
293-
const cacheKey = calculateCacheKey(this.globalConfigHash, path, content);
263+
const cacheKey = calculateCacheKey(this.globalConfigHash, path);
294264
const cached = await this.persistentStore.get(cacheKey);
295265

296266
if (
@@ -300,7 +270,6 @@ export class PersistentLoadResultCache implements LoadResultCache {
300270
this.persistentStore,
301271
cacheKey,
302272
cached,
303-
filePath,
304273
))
305274
) {
306275
const result: OnLoadResult = {
@@ -340,17 +309,17 @@ export class PersistentLoadResultCache implements LoadResultCache {
340309
}
341310
}
342311

343-
const cacheKey = calculateCacheKey(this.globalConfigHash, path, content);
312+
const cacheKey = calculateCacheKey(this.globalConfigHash, path);
344313

345314
// Reuse the target file's pre-read content buffer to avoid redundant disk reads (readFile)
346315
// during dependency watch file metadata computation.
347316
const knownContents = filePath
348317
? new Map<string, string | Uint8Array>([[filePath, content]])
349318
: undefined;
350-
const watchFilesMetadata = await computeMetadataForWatchFiles(
351-
result.watchFiles ?? [],
352-
knownContents,
319+
const allWatchFiles = Array.from(
320+
new Set(filePath ? [filePath, ...(result.watchFiles ?? [])] : result.watchFiles),
353321
);
322+
const watchFilesMetadata = await computeMetadataForWatchFiles(allWatchFiles, knownContents);
354323

355324
await this.persistentStore.put(cacheKey, {
356325
contents: result.contents,

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

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -193,10 +193,8 @@ export class SqliteCacheStore implements PersistentCacheStore<unknown> {
193193
#queueAccessUpdate(key: string): void {
194194
this.#pendingAccessedKeys.add(key);
195195

196-
if (this.#pendingAccessedKeys.size >= 100) {
197-
this.#flushAccessUpdates();
198-
} else if (!this.#flushTimeout) {
199-
this.#flushTimeout = setTimeout(() => this.#flushAccessUpdates(), 500);
196+
if (!this.#flushTimeout) {
197+
this.#flushTimeout = setTimeout(() => this.#flushAccessUpdates(), 1000);
200198
this.#flushTimeout.unref?.();
201199
}
202200
}

0 commit comments

Comments
 (0)