Skip to content

Commit 29816a8

Browse files
fix: perform format operation in chunks (#115)
* feat: perform format operation in chunks * move mapSettledWithConcurrency to utils.ts * swap concurrency calc method * add back missing config check * Update src/index.ts Co-authored-by: fisker Cheung <lionkay@gmail.com> * Remove useless calculations Co-authored-by: fisker Cheung <lionkay@gmail.com> --------- Co-authored-by: fisker Cheung <lionkay@gmail.com>
1 parent 2d89e2a commit 29816a8

3 files changed

Lines changed: 80 additions & 27 deletions

File tree

‎src/index.ts‎

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import fs from "node:fs/promises";
22
import isBinaryPath from "is-binary-path";
33
import stringify from "json-sorted-stringify";
4+
import os from "node:os";
45
import path from "node:path";
56
import process from "node:process";
67
import Cache from "./cache.js";
@@ -21,6 +22,7 @@ import {
2122
getProjectPath,
2223
getStdin,
2324
getTargetsPaths,
25+
mapSettledWithConcurrency,
2426
} from "./utils.js";
2527
import {
2628
fastRelativePath,
@@ -179,34 +181,32 @@ async function runGlobs(options: Options, pluginsDefaultOptions: PluginsOptions,
179181
const cache = shouldCache ? new Cache(cacheVersion, projectPath, getCacheRootPath(rootPath), options, stdout) : undefined;
180182
const prettier = await makePrettier(options, cache);
181183

182-
//TODO: Maybe do work in chunks here, as keeping too many formatted files in memory can be a problem
183-
const filesResults = await Promise.allSettled(
184-
filesPathsTargets.map(async (filePath) => {
185-
const isIgnored = () => (ignoreManual ? ignoreManual(filePath) : getIgnoreResolved(filePath, ignoreNames));
186-
const isCacheable = () => cache?.has(filePath, isIgnored);
187-
const isExplicitlyIncluded = () => filesExplicitPathsSet.has(filePath);
188-
const isForceIncluded = options.dump && isExplicitlyIncluded();
189-
const isExcluded = cache ? !(await isCacheable()) : await isIgnored();
190-
if (!isForceIncluded && isExcluded) return;
191-
const getFormatOptions = async (): Promise<FormatOptions> => {
192-
const editorConfig = options.editorConfig ? getEditorConfigFormatOptions(await getEditorConfigResolved(filePath, editorConfigNames)) : {};
193-
const prettierConfig = prettierManualConfig || (options.config ? await getPrettierConfigResolved(filePath, prettierConfigNames) : {});
194-
const formatOptions = { ...editorConfig, ...prettierConfig, ...options.formatOptions };
195-
return formatOptions;
196-
};
197-
try {
198-
if (options.check || options.list) {
199-
return await prettier.checkWithPath(filePath, getFormatOptions, cliContextConfig, pluginsDefaultOptions, pluginsCustomOptions);
200-
} else if (options.write) {
201-
return await prettier.writeWithPath(filePath, getFormatOptions, cliContextConfig, pluginsDefaultOptions, pluginsCustomOptions);
202-
} else {
203-
return await prettier.formatWithPath(filePath, getFormatOptions, cliContextConfig, pluginsDefaultOptions, pluginsCustomOptions);
204-
}
205-
} finally {
206-
spinner?.update(fastRelativePath(rootPath, filePath));
184+
const concurrency = options.parallel ? (options.parallelWorkers ?? Math.max(1, (os.availableParallelism?.() ?? os.cpus().length) - 1)) : 1;
185+
const filesResults = await mapSettledWithConcurrency(filesPathsTargets, concurrency, async (filePath) => {
186+
const isIgnored = () => (ignoreManual ? ignoreManual(filePath) : getIgnoreResolved(filePath, ignoreNames));
187+
const isCacheable = () => cache?.has(filePath, isIgnored);
188+
const isExplicitlyIncluded = () => filesExplicitPathsSet.has(filePath);
189+
const isForceIncluded = options.dump && isExplicitlyIncluded();
190+
const isExcluded = cache ? !(await isCacheable()) : await isIgnored();
191+
if (!isForceIncluded && isExcluded) return;
192+
const getFormatOptions = async (): Promise<FormatOptions> => {
193+
const editorConfig = options.editorConfig ? getEditorConfigFormatOptions(await getEditorConfigResolved(filePath, editorConfigNames)) : {};
194+
const prettierConfig = prettierManualConfig || (options.config ? await getPrettierConfigResolved(filePath, prettierConfigNames) : {});
195+
const formatOptions = { ...editorConfig, ...prettierConfig, ...options.formatOptions };
196+
return formatOptions;
197+
};
198+
try {
199+
if (options.check || options.list) {
200+
return await prettier.checkWithPath(filePath, getFormatOptions, cliContextConfig, pluginsDefaultOptions, pluginsCustomOptions);
201+
} else if (options.write) {
202+
return await prettier.writeWithPath(filePath, getFormatOptions, cliContextConfig, pluginsDefaultOptions, pluginsCustomOptions);
203+
} else {
204+
return await prettier.formatWithPath(filePath, getFormatOptions, cliContextConfig, pluginsDefaultOptions, pluginsCustomOptions);
207205
}
208-
}),
209-
);
206+
} finally {
207+
spinner?.update(fastRelativePath(rootPath, filePath));
208+
}
209+
});
210210

211211
spinner?.stop("Checking formatting...");
212212

‎src/utils.ts‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,36 @@ function zipObjectUnless<T extends Key, U>(keys: T[], values: U[], unless: (valu
635635
return map;
636636
}
637637

638+
async function mapSettledWithConcurrency<T, R>(
639+
values: readonly T[],
640+
concurrency: number,
641+
mapper: (value: T, index: number) => Promise<R>,
642+
): Promise<PromiseSettledResult<R>[]> {
643+
const results = new Array<PromiseSettledResult<R>>(values.length);
644+
let nextIndex = 0;
645+
646+
async function runNext(): Promise<void> {
647+
while (true) {
648+
const index = nextIndex++;
649+
if (index >= values.length) return;
650+
651+
try {
652+
results[index] = {
653+
status: "fulfilled",
654+
value: await mapper(values[index], index),
655+
};
656+
} catch (reason) {
657+
results[index] = { status: "rejected", reason };
658+
}
659+
}
660+
}
661+
662+
const runnersCount = Math.min(values.length, concurrency);
663+
await Promise.all(Array.from({ length: runnersCount }, runNext));
664+
665+
return results;
666+
}
667+
638668
export {
639669
castArray,
640670
fastJoinedPath,
@@ -671,6 +701,7 @@ export {
671701
isString,
672702
isTruthy,
673703
isUndefined,
704+
mapSettledWithConcurrency,
674705
memoize,
675706
negate,
676707
noop,
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { expect, test } from "@jest/globals";
2+
import { mapSettledWithConcurrency } from "../../dist/utils.js";
3+
4+
test("bounds concurrency and preserves settled-result order", async () => {
5+
let active = 0;
6+
let maxActive = 0;
7+
8+
const results = await mapSettledWithConcurrency([0, 1, 2, 3, 4, 5], 3, async (value) => {
9+
active += 1;
10+
maxActive = Math.max(maxActive, active);
11+
await new Promise((resolve) => setImmediate(resolve));
12+
active -= 1;
13+
if (value === 2) throw new Error("expected failure");
14+
return value * 2;
15+
});
16+
17+
expect(maxActive).toBe(3);
18+
expect(results.map((result) => result.status)).toEqual(["fulfilled", "fulfilled", "rejected", "fulfilled", "fulfilled", "fulfilled"]);
19+
expect(results[0]).toEqual({ status: "fulfilled", value: 0 });
20+
expect(results[2]).toMatchObject({ status: "rejected", reason: new Error("expected failure") });
21+
expect(results[5]).toEqual({ status: "fulfilled", value: 10 });
22+
});

0 commit comments

Comments
 (0)