Skip to content

Commit bbe246f

Browse files
authored
feat(perf): optimize materialization and sync caching (#14)
* perf(verify): optimize file existence checks * perf: normalization * perf(sync): cache docs presence checks * ci: add Node.js 18,20,22 matrix * ci: run full OS matrix only on PRs * fix(cache): improve verifyCache error handling
1 parent 2bdbcee commit bbe246f

7 files changed

Lines changed: 56 additions & 32 deletions

File tree

.github/workflows/audit.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ jobs:
2020
- name: Setup Node
2121
uses: actions/setup-node@v4
2222
with:
23-
node-version: 20
23+
node-version: 22
2424
cache: pnpm
2525

2626
- name: Install dependencies

.github/workflows/ci.yml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ jobs:
1313
strategy:
1414
fail-fast: false
1515
matrix:
16-
os: [ubuntu-latest, macos-latest, windows-latest]
16+
os: ${{ fromJSON(github.event_name == 'pull_request' && '["ubuntu-latest","macos-latest","windows-latest"]' || '["ubuntu-latest"]') }}
17+
node-version: [18, 20, 22]
1718
steps:
1819
- name: Checkout
1920
uses: actions/checkout@v4
@@ -26,7 +27,7 @@ jobs:
2627
- name: Setup Node
2728
uses: actions/setup-node@v4
2829
with:
29-
node-version: 20
30+
node-version: ${{ matrix.node-version }}
3031
cache: pnpm
3132

3233
- name: Install dependencies

.github/workflows/pkg-pr-new-preview.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ jobs:
3131
- name: Setup Node
3232
uses: actions/setup-node@v4
3333
with:
34-
node-version: 20
34+
node-version: 22
3535
cache: pnpm
3636

3737
- name: Install dependencies

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ docs.lock
1111
coverage
1212
TODO.md
1313
.docs/
14+
benchmarks/

src/materialize.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,14 @@ export const materializeSource = async (params: MaterializeParams) => {
133133
onlyFiles: true,
134134
followSymbolicLinks: false,
135135
});
136-
files.sort((left, right) =>
137-
normalizePath(left).localeCompare(normalizePath(right)),
138-
);
136+
const entries = files
137+
.map((relativePath) => ({
138+
relativePath,
139+
normalized: normalizePath(relativePath),
140+
}))
141+
.sort((left, right) => left.normalized.localeCompare(right.normalized));
139142
const targetDirs = new Set<string>();
140-
for (const relativePath of files) {
143+
for (const { relativePath } of entries) {
141144
targetDirs.add(path.dirname(relativePath));
142145
}
143146
await Promise.all(
@@ -149,7 +152,10 @@ export const materializeSource = async (params: MaterializeParams) => {
149152
let fileCount = 0;
150153
const concurrency = Math.max(
151154
1,
152-
Math.min(files.length, Math.max(8, Math.min(128, os.cpus().length * 8))),
155+
Math.min(
156+
entries.length,
157+
Math.max(8, Math.min(128, os.cpus().length * 8)),
158+
),
153159
);
154160
const manifestPath = path.join(tempDir, MANIFEST_FILENAME);
155161
const manifestStream = createWriteStream(manifestPath, {
@@ -177,12 +183,11 @@ export const materializeSource = async (params: MaterializeParams) => {
177183
});
178184
};
179185

180-
for (let i = 0; i < files.length; i += concurrency) {
181-
const batch = files.slice(i, i + concurrency);
186+
for (let i = 0; i < entries.length; i += concurrency) {
187+
const batch = entries.slice(i, i + concurrency);
182188
const results = await Promise.all(
183-
batch.map(async (relativePath) => {
184-
const relNormalized = normalizePath(relativePath);
185-
const filePath = path.join(params.repoDir, relativePath);
189+
batch.map(async (entry) => {
190+
const filePath = path.join(params.repoDir, entry.relativePath);
186191
const fileHandle = await openFileNoFollow(filePath);
187192
if (!fileHandle) {
188193
return null;
@@ -192,7 +197,7 @@ export const materializeSource = async (params: MaterializeParams) => {
192197
if (!stats.isFile()) {
193198
return null;
194199
}
195-
const targetPath = path.join(tempDir, relativePath);
200+
const targetPath = path.join(tempDir, entry.relativePath);
196201
ensureSafePath(tempDir, targetPath);
197202
if (stats.size >= STREAM_COPY_THRESHOLD_BYTES) {
198203
const reader = createReadStream(filePath, {
@@ -206,7 +211,7 @@ export const materializeSource = async (params: MaterializeParams) => {
206211
await writeFile(targetPath, data);
207212
}
208213
return {
209-
path: relNormalized,
214+
path: entry.normalized,
210215
size: stats.size,
211216
};
212217
} finally {

src/sync.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,7 @@ export const runSync = async (options: SyncOptions, deps: SyncDeps = {}) => {
275275
const defaults = plan.defaults;
276276
const runFetch = deps.fetchSource ?? fetchSource;
277277
const runMaterialize = deps.materializeSource ?? materializeSource;
278+
const docsPresence = new Map<string, boolean>();
278279
const buildJobs = async (ids?: string[], force?: boolean) => {
279280
const pick = ids?.length
280281
? plan.results.filter((result) => ids.includes(result.id))
@@ -285,9 +286,16 @@ export const runSync = async (options: SyncOptions, deps: SyncDeps = {}) => {
285286
if (!source) {
286287
return null;
287288
}
288-
const docsPresent = await hasDocs(plan.cacheDir, result.id);
289+
if (force) {
290+
return { result, source };
291+
}
292+
let docsPresent = docsPresence.get(result.id);
293+
if (docsPresent === undefined) {
294+
docsPresent = await hasDocs(plan.cacheDir, result.id);
295+
docsPresence.set(result.id, docsPresent);
296+
}
289297
const needsMaterialize =
290-
force || result.status !== "up-to-date" || !docsPresent;
298+
result.status !== "up-to-date" || !docsPresent;
291299
return needsMaterialize ? { result, source } : null;
292300
}),
293301
);

src/verify.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,18 @@ export const verifyCache = async (options: VerifyOptions) => {
4646
let sizeMismatchCount = 0;
4747
for await (const entry of streamManifestEntries(directory)) {
4848
const filePath = path.join(directory, entry.path);
49-
if (!(await exists(filePath))) {
50-
missingCount += 1;
51-
continue;
52-
}
53-
const info = await stat(filePath);
54-
if (info.size !== entry.size) {
55-
sizeMismatchCount += 1;
49+
try {
50+
const info = await stat(filePath);
51+
if (info.size !== entry.size) {
52+
sizeMismatchCount += 1;
53+
}
54+
} catch (error) {
55+
const code = (error as NodeJS.ErrnoException).code;
56+
if (code === "ENOENT" || code === "ENOTDIR") {
57+
missingCount += 1;
58+
continue;
59+
}
60+
throw error;
5661
}
5762
}
5863
const issues: string[] = [];
@@ -74,13 +79,17 @@ export const verifyCache = async (options: VerifyOptions) => {
7479
ok: issues.length === 0,
7580
issues,
7681
};
77-
} catch (_error) {
78-
return {
79-
ok: false,
80-
issues: [
81-
label === "source" ? "missing manifest" : "missing target manifest",
82-
],
83-
};
82+
} catch (error) {
83+
const code = (error as NodeJS.ErrnoException).code;
84+
if (code === "ENOENT" || code === "ENOTDIR") {
85+
return {
86+
ok: false,
87+
issues: [
88+
label === "source" ? "missing manifest" : "missing target manifest",
89+
],
90+
};
91+
}
92+
throw error;
8493
}
8594
};
8695

0 commit comments

Comments
 (0)