From 3238519cce6c3a37b7c9b310a91377bfab4dc202 Mon Sep 17 00:00:00 2001 From: neverland Date: Sat, 22 Aug 2026 12:31:32 +0800 Subject: [PATCH 1/3] perf(fmt): bypass worker pool for single files --- packages/rstack/src/fmt/runner.ts | 67 ++++++++++++------- packages/rstack/src/fmt/worker.ts | 4 +- .../tests/fmt/runnerWorkerPreflight.test.ts | 10 ++- 3 files changed, 53 insertions(+), 28 deletions(-) diff --git a/packages/rstack/src/fmt/runner.ts b/packages/rstack/src/fmt/runner.ts index 89ad3a9a..c7ebbd8a 100644 --- a/packages/rstack/src/fmt/runner.ts +++ b/packages/rstack/src/fmt/runner.ts @@ -39,7 +39,7 @@ interface RunCache { hashOptions: ReturnType; } -interface FmtWorkerPoolResult { +interface FmtFilesResult { files: FmtFileResult[]; processedFileCount: number; } @@ -193,13 +193,38 @@ const runPriorityTasks = async ( return results; }; -/** Processes files in a worker pool while preserving input order. */ -const runWithWorkers = async ( +/** Collects per-file outcomes while preserving cache and processed-count semantics. */ +const collectFmtResults = ( + results: FmtFileRun[], + cache?: RunCache, +): FmtFilesResult => { + const processedFiles: FmtFileResult[] = []; + let processedFileCount = 0; + + for (const { outcome, key, entry } of results) { + if (key !== undefined && entry) { + cache?.store.set(key, entry); + } + if (outcome === 'unsupported') { + continue; + } + + processedFileCount++; + if (outcome !== 'unchanged') { + processedFiles.push(outcome); + } + } + + return { files: processedFiles, processedFileCount }; +}; + +/** Processes one pending file locally and multiple pending files in a worker pool. */ +const runFmtTasks = async ( files: FmtFileRequest[], shouldWrite: boolean, maxWorkers?: number, cache?: RunCache, -): Promise => { +): Promise => { const tasks = files.map((file) => createRunTask(file, cache)); const pendingFileCount = tasks.reduce( (count, task) => count + (isCachedUnsupported(task) ? 0 : 1), @@ -209,6 +234,19 @@ const runWithWorkers = async ( return { files: [], processedFileCount: 0 }; } + // One pending file cannot benefit from parallelism, so avoid worker startup and IPC overhead. + if (pendingFileCount === 1) { + const { formatFile } = await import('./worker.ts'); + const formatFileOnMainThread: FormatFile = (file, write, fileCache) => + formatFile({ file, shouldWrite: write, cache: fileCache }); + const results = await Promise.all( + tasks.map((task) => + runFmtFile(task, shouldWrite, formatFileOnMainThread), + ), + ); + return collectFmtResults(results, cache); + } + const { createWorkerPool } = await import('./workerPool.ts'); const workerPool = await createWorkerPool(pendingFileCount, maxWorkers); @@ -221,24 +259,7 @@ const runWithWorkers = async ( runFmtFile(task, shouldWrite, workerPool.formatFile), ), ); - const processedFiles: FmtFileResult[] = []; - let processedFileCount = 0; - - for (const { outcome, key, entry } of results) { - if (key !== undefined && entry) { - cache?.store.set(key, entry); - } - if (outcome === 'unsupported') { - continue; - } - - processedFileCount++; - if (outcome !== 'unchanged') { - processedFiles.push(outcome); - } - } - - return { files: processedFiles, processedFileCount }; + return collectFmtResults(results, cache); } finally { await workerPool.terminate(); } @@ -284,7 +305,7 @@ const runFmtFiles = async ({ const result = files.length === 0 ? { files: [], processedFileCount: 0 } - : await runWithWorkers(files, shouldWrite, maxWorkers, runCache); + : await runFmtTasks(files, shouldWrite, maxWorkers, runCache); await runCache?.store.save().catch(() => false); return { diff --git a/packages/rstack/src/fmt/worker.ts b/packages/rstack/src/fmt/worker.ts index 35d47f49..f48ae439 100644 --- a/packages/rstack/src/fmt/worker.ts +++ b/packages/rstack/src/fmt/worker.ts @@ -16,8 +16,8 @@ const hashContent = (content: string | Uint8Array): string => hash('sha256', content, 'base64url').slice(0, 16); /** - * Use synchronous direct I/O inside the dedicated worker to avoid libuv - * scheduling overhead. This prioritizes throughput over crash-safe replacement. + * Use synchronous direct I/O in workers and the one-file main-thread path to avoid + * libuv scheduling overhead. This prioritizes throughput over crash-safe replacement. */ const formatFile = async ({ file, diff --git a/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts b/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts index cb3dec45..f6e24e21 100644 --- a/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts +++ b/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts @@ -68,7 +68,7 @@ test('does not start the worker pool when every parser result is cached as unsup }); }); -test('starts the worker pool for a path-only unsupported entry without an extension', async () => { +test('does not start the worker pool when only one file remains pending', async () => { await withTempProject(async (rootPath) => { const { cache, file } = await createCachedUnsupportedFile( rootPath, @@ -81,7 +81,11 @@ test('starts the worker pool for a path-only unsupported entry without an extens mode: 'check', cache, }), - ).rejects.toThrow('worker startup failed'); - expect(mocks.workerPoolCalls).toEqual([[1, undefined]]); + ).resolves.toEqual({ + exitCode: 2, + files: [], + processedFileCount: 0, + }); + expect(mocks.workerPoolCalls).toEqual([]); }); }); From 6de9887c4ca82f20dccab4897d47cee19f18e9c2 Mon Sep 17 00:00:00 2001 From: neverland Date: Sat, 22 Aug 2026 13:26:09 +0800 Subject: [PATCH 2/3] docs(fmt): clarify synchronous I/O tradeoff --- packages/rstack/src/fmt/worker.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rstack/src/fmt/worker.ts b/packages/rstack/src/fmt/worker.ts index f48ae439..40d7a764 100644 --- a/packages/rstack/src/fmt/worker.ts +++ b/packages/rstack/src/fmt/worker.ts @@ -16,8 +16,8 @@ const hashContent = (content: string | Uint8Array): string => hash('sha256', content, 'base64url').slice(0, 16); /** - * Use synchronous direct I/O in workers and the one-file main-thread path to avoid - * libuv scheduling overhead. This prioritizes throughput over crash-safe replacement. + * Synchronous file I/O avoids libuv scheduling overhead in workers and single-file + * main-thread runs. This favors throughput over crash-safe file replacement. */ const formatFile = async ({ file, From d20bf45992a379cd48780aa33b47c8215a0d17be Mon Sep 17 00:00:00 2001 From: neverland Date: Sat, 22 Aug 2026 13:32:04 +0800 Subject: [PATCH 3/3] test(fmt): preserve extensionless cache coverage --- packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts b/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts index f6e24e21..4f1aeba5 100644 --- a/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts +++ b/packages/rstack/tests/fmt/runnerWorkerPreflight.test.ts @@ -68,12 +68,13 @@ test('does not start the worker pool when every parser result is cached as unsup }); }); -test('does not start the worker pool when only one file remains pending', async () => { +test('rechecks a path-only unsupported entry on the main thread', async () => { await withTempProject(async (rootPath) => { const { cache, file } = await createCachedUnsupportedFile( rootPath, 'script', ); + writeProjectFile(rootPath, 'script', '#!/usr/bin/env node\nconst value=1'); await expect( runFmtFiles({ @@ -82,9 +83,9 @@ test('does not start the worker pool when only one file remains pending', async cache, }), ).resolves.toEqual({ - exitCode: 2, - files: [], - processedFileCount: 0, + exitCode: 1, + files: [{ path: file.path, status: 'different' }], + processedFileCount: 1, }); expect(mocks.workerPoolCalls).toEqual([]); });