Skip to content

Commit b0ac394

Browse files
committed
perf(@angular/build): resolve asset patterns concurrently with bounded concurrency
Avoid sequentially globbing asset entries one-by-one in `resolveAssets` by utilizing `mapConcurrent` bounded by `MAX_CONCURRENT_READS` (16). Additionally, add a fast path for static, non-dynamic file patterns using direct file stats instead of directory walks. (cherry picked from commit fb7efc8)
1 parent 5570c91 commit b0ac394

3 files changed

Lines changed: 37 additions & 16 deletions

File tree

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

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ import { readFile, stat } from 'node:fs/promises';
3232
import { isAbsolute } from 'node:path';
3333
import { fileURLToPath } from 'node:url';
3434
import type { Cache as PersistentCacheStore } from '../../utils/cache';
35-
import { mapConcurrent, runConcurrent } from '../../utils/concurrency';
35+
import { MAX_CONCURRENT_READS, mapConcurrent, runConcurrent } from '../../utils/concurrency';
3636
import { calculateHash, createContentHash } from '../../utils/hash';
3737
import { LoadResultCache, MemoryLoadResultCache } from './load-result-cache';
3838

@@ -112,9 +112,6 @@ export function extractDiskFilePath(path: string): string | undefined {
112112
return isAbsolute(path) ? path : undefined;
113113
}
114114

115-
/** Maximum number of concurrent file system read/stat operations to prevent OS file descriptor exhaustion. */
116-
const MAX_CONCURRENT_READS = 16;
117-
118115
/**
119116
* Validates that all imported watch files exist on disk and their contents match.
120117
* Performs a fast-path metadata check (mtime + size) first, falling back to content hashing.

‎packages/angular/build/src/utils/concurrency.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
/**
10+
* Maximum number of concurrent file system read/stat operations to prevent OS file descriptor exhaustion.
11+
*/
12+
export const MAX_CONCURRENT_READS = 16;
13+
914
/**
1015
* Executes an asynchronous function for each item in an array concurrently up to a specified limit.
1116
*

‎packages/angular/build/src/utils/resolve-assets.ts‎

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9+
import { lstat, stat } from 'node:fs/promises';
910
import path from 'node:path';
10-
import { glob } from 'tinyglobby';
11+
import { glob, isDynamicPattern } from 'tinyglobby';
12+
import { MAX_CONCURRENT_READS, mapConcurrent } from './concurrency';
1113
import { isSubDirectory } from './path';
1214

1315
/**
@@ -26,29 +28,46 @@ export async function resolveAssets(
2628
}[],
2729
root: string,
2830
): Promise<{ source: string; destination: string }[]> {
29-
const outputFiles: { source: string; destination: string }[] = [];
30-
31-
for (const entry of entries) {
31+
const resolvedEntries = await mapConcurrent(entries, MAX_CONCURRENT_READS, async (entry) => {
3232
if (!isSubDirectory(root, entry.input)) {
3333
throw new Error(`The ${entry.input} asset path must be within the workspace root.`);
3434
}
3535

3636
const cwd = path.resolve(root, entry.input);
3737

38+
if (!entry.ignore?.length && !isDynamicPattern(entry.glob)) {
39+
const src = path.join(cwd, entry.glob);
40+
if (isSubDirectory(cwd, src)) {
41+
try {
42+
const stats = await (entry.followSymlinks ? stat(src) : lstat(src));
43+
if (stats.isFile()) {
44+
const filePath = entry.flatten ? path.basename(entry.glob) : entry.glob;
45+
46+
return [{ source: src, destination: path.join(entry.output, filePath) }];
47+
}
48+
49+
if (!stats.isSymbolicLink()) {
50+
return [];
51+
}
52+
} catch {
53+
// File does not exist or cannot be accessed.
54+
return [];
55+
}
56+
}
57+
}
58+
3859
const files = await glob(entry.glob, {
3960
cwd,
4061
dot: true,
4162
ignore: entry.ignore ? [...DEFAULT_ASSET_IGNORE, ...entry.ignore] : DEFAULT_ASSET_IGNORE,
4263
followSymbolicLinks: entry.followSymlinks,
4364
});
4465

45-
for (const file of files) {
46-
const src = path.join(cwd, file);
47-
const filePath = entry.flatten ? path.basename(file) : file;
48-
49-
outputFiles.push({ source: src, destination: path.join(entry.output, filePath) });
50-
}
51-
}
66+
return files.map((file) => ({
67+
source: path.join(cwd, file),
68+
destination: path.join(entry.output, entry.flatten ? path.basename(file) : file),
69+
}));
70+
});
5271

53-
return outputFiles;
72+
return resolvedEntries.flat();
5473
}

0 commit comments

Comments
 (0)