From 13b145cb09e08771dec788ac4d64cafa0babaa19 Mon Sep 17 00:00:00 2001 From: luojiyin Date: Tue, 1 Sep 2026 17:38:06 +0800 Subject: [PATCH] perf(files): reuse file stats for filtering and concurrency --- __tests__/batch-lint.spec.ts | 75 +++++++++++++--------- __tests__/file-stat-reuse.spec.ts | 102 ++++++++++++++++++++++++++++++ __tests__/max-file-size.spec.ts | 25 +++++--- __tests__/run-file-lint.spec.ts | 32 +++++++--- src/cli/run-lint.ts | 12 +++- src/utils/adaptive-concurrency.ts | 6 +- src/utils/file-stat.ts | 23 +++---- src/utils/filter-by-max-size.ts | 41 +++++------- 8 files changed, 226 insertions(+), 90 deletions(-) create mode 100644 __tests__/file-stat-reuse.spec.ts diff --git a/__tests__/batch-lint.spec.ts b/__tests__/batch-lint.spec.ts index 46ae270..802dc7b 100644 --- a/__tests__/batch-lint.spec.ts +++ b/__tests__/batch-lint.spec.ts @@ -5,7 +5,11 @@ import { Piscina } from "piscina"; import type { LintMdRulesConfig } from "@lint-md/core"; import { resolveAdaptiveConcurrency } from "../src/utils/adaptive-concurrency"; import { batchLint, keepLintItem } from "../src/utils/batch-lint"; -import { STAT_CONCURRENCY_LIMIT, getMaxFileSize } from "../src/utils/file-stat"; +import { + STAT_CONCURRENCY_LIMIT, + getMaxFileSize, + statFiles, +} from "../src/utils/file-stat"; import type { BatchLintItem } from "../src/types"; import { makeNotAppliedFix } from "./helpers/not-applied-fix"; @@ -258,29 +262,26 @@ describe("getMaxFileSize", () => { }); test("returns 0 for empty list", async () => { - expect(await getMaxFileSize([])).toBe(0); + expect(getMaxFileSize([])).toBe(0); }); test("returns size of the only file", async () => { - const file = path.join(tmpDir, "only.md"); - await writeFile(file, "hello"); - expect(await getMaxFileSize([file])).toBe(5); + expect(getMaxFileSize([{ path: "only.md", size: 5 }])).toBe(5); }); test("returns size of the largest file among many", async () => { - const small = path.join(tmpDir, "small.md"); - const large = path.join(tmpDir, "large.md"); - const medium = path.join(tmpDir, "medium.md"); - await writeFile(small, "a".repeat(10)); - await writeFile(large, "b".repeat(1000)); - await writeFile(medium, "c".repeat(500)); - - expect(await getMaxFileSize([small, large, medium])).toBe(1000); + expect( + getMaxFileSize([ + { path: "small.md", size: 10 }, + { path: "large.md", size: 1000 }, + { path: "medium.md", size: 500 }, + ]) + ).toBe(1000); }); test("rejects when a file cannot be stat-ed", async () => { const missing = path.join(tmpDir, "missing.md"); - await expect(getMaxFileSize([missing])).rejects.toThrow(); + await expect(statFiles([missing])).rejects.toThrow(); }); test("bounds concurrent stat calls to STAT_CONCURRENCY_LIMIT", async () => { @@ -310,9 +311,9 @@ describe("getMaxFileSize", () => { }); try { - const result = await getMaxFileSize(filePaths); + const result = await statFiles(filePaths); - expect(result).toBe(fileCount); + expect(getMaxFileSize(result)).toBe(fileCount); expect(statSpy).toHaveBeenCalledTimes(fileCount); expect(maxInFlight).toBeGreaterThan(1); expect(maxInFlight).toBeLessThanOrEqual(STAT_CONCURRENCY_LIMIT); @@ -346,7 +347,7 @@ describe("resolveAdaptiveConcurrency", () => { writeSizedFile("b.md", 100), writeSizedFile("c.md", 100), ]); - expect(await resolveAdaptiveConcurrency(2, files)).toEqual({ + expect(await resolveAdaptiveConcurrency(2, files, 0)).toEqual({ concurrency: 2, maxFileSize: null, requestedConcurrency: 2, @@ -355,7 +356,7 @@ describe("resolveAdaptiveConcurrency", () => { test("numeric threads > fileCount is clamped to fileCount", async () => { const file = await writeSizedFile("only.md", 100); - expect(await resolveAdaptiveConcurrency(100, [file])).toEqual({ + expect(await resolveAdaptiveConcurrency(100, [file], 0)).toEqual({ concurrency: 1, maxFileSize: null, requestedConcurrency: 100, @@ -367,7 +368,7 @@ describe("resolveAdaptiveConcurrency", () => { writeSizedFile("a.md", 100), writeSizedFile("b.md", 100), ]); - expect(await resolveAdaptiveConcurrency(0, files)).toEqual({ + expect(await resolveAdaptiveConcurrency(0, files, 0)).toEqual({ concurrency: 1, maxFileSize: null, requestedConcurrency: 0, @@ -383,7 +384,9 @@ describe("resolveAdaptiveConcurrency", () => { const statSpy = jest.spyOn(require("fs/promises"), "stat"); try { - expect(await resolveAdaptiveConcurrency(8, files)).toEqual({ + expect( + await resolveAdaptiveConcurrency(8, files, 10 * 1024 * 1024) + ).toEqual({ concurrency: 8, maxFileSize: null, requestedConcurrency: 8, @@ -397,7 +400,7 @@ describe("resolveAdaptiveConcurrency", () => { describe("auto threadCount", () => { test("empty file list → 0", async () => { - expect(await resolveAdaptiveConcurrency("auto", [])).toEqual({ + expect(await resolveAdaptiveConcurrency("auto", [], 0)).toEqual({ concurrency: 0, maxFileSize: 0, requestedConcurrency: availableParallelism(), @@ -414,12 +417,12 @@ describe("resolveAdaptiveConcurrency", () => { const statSpy = jest.spyOn(require("fs/promises"), "stat"); try { - expect(await resolveAdaptiveConcurrency("auto", files)).toEqual({ + expect(await resolveAdaptiveConcurrency("auto", files, 4096)).toEqual({ concurrency: Math.min(cpuLimit, files.length), maxFileSize: 4096, requestedConcurrency: cpuLimit, }); - expect(statSpy).toHaveBeenCalledTimes(files.length); + expect(statSpy).not.toHaveBeenCalled(); } finally { statSpy.mockRestore(); } @@ -431,7 +434,9 @@ describe("resolveAdaptiveConcurrency", () => { writeSizedFile("one-mib.md", 1024 * 1024), ]); const cpuLimit = availableParallelism(); - expect(await resolveAdaptiveConcurrency("auto", files)).toEqual({ + expect( + await resolveAdaptiveConcurrency("auto", files, 1024 * 1024) + ).toEqual({ concurrency: Math.min(cpuLimit, 2, files.length), maxFileSize: 1024 * 1024, requestedConcurrency: cpuLimit, @@ -440,7 +445,9 @@ describe("resolveAdaptiveConcurrency", () => { test("max file 1.5 MiB caps at 2", async () => { const file = await writeSizedFile("medium.md", 1.5 * 1024 * 1024); - expect(await resolveAdaptiveConcurrency("auto", [file])).toEqual({ + expect( + await resolveAdaptiveConcurrency("auto", [file], 1.5 * 1024 * 1024) + ).toEqual({ concurrency: 1, maxFileSize: 1.5 * 1024 * 1024, requestedConcurrency: availableParallelism(), @@ -449,7 +456,9 @@ describe("resolveAdaptiveConcurrency", () => { test("max file exactly 5 MiB forces 1", async () => { const file = await writeSizedFile("five-mib.md", 5 * 1024 * 1024); - expect(await resolveAdaptiveConcurrency("auto", [file])).toEqual({ + expect( + await resolveAdaptiveConcurrency("auto", [file], 5 * 1024 * 1024) + ).toEqual({ concurrency: 1, maxFileSize: 5 * 1024 * 1024, requestedConcurrency: availableParallelism(), @@ -458,7 +467,9 @@ describe("resolveAdaptiveConcurrency", () => { test("max file 6 MiB forces 1", async () => { const file = await writeSizedFile("six-mib.md", 6 * 1024 * 1024); - expect(await resolveAdaptiveConcurrency("auto", [file])).toEqual({ + expect( + await resolveAdaptiveConcurrency("auto", [file], 6 * 1024 * 1024) + ).toEqual({ concurrency: 1, maxFileSize: 6 * 1024 * 1024, requestedConcurrency: availableParallelism(), @@ -467,7 +478,7 @@ describe("resolveAdaptiveConcurrency", () => { test("single small file → 1", async () => { const file = await writeSizedFile("only.md", 100); - expect(await resolveAdaptiveConcurrency("auto", [file])).toEqual({ + expect(await resolveAdaptiveConcurrency("auto", [file], 100)).toEqual({ concurrency: 1, maxFileSize: 100, requestedConcurrency: availableParallelism(), @@ -476,7 +487,13 @@ describe("resolveAdaptiveConcurrency", () => { test("medium cap respects fileCount when files < 2", async () => { const file = await writeSizedFile("one-mib.md", 1.2 * 1024 * 1024); - expect(await resolveAdaptiveConcurrency("auto", [file])).toEqual({ + expect( + await resolveAdaptiveConcurrency( + "auto", + [file], + Math.floor(1.2 * 1024 * 1024) + ) + ).toEqual({ concurrency: 1, maxFileSize: Math.floor(1.2 * 1024 * 1024), requestedConcurrency: availableParallelism(), diff --git a/__tests__/file-stat-reuse.spec.ts b/__tests__/file-stat-reuse.spec.ts new file mode 100644 index 0000000..595822c --- /dev/null +++ b/__tests__/file-stat-reuse.spec.ts @@ -0,0 +1,102 @@ +import type { Stats } from "fs"; +import { availableParallelism } from "os"; +import { runFileLint } from "../src/cli/run-lint"; +import { batchLint } from "../src/utils/batch-lint"; +import { loadMdFiles } from "../src/utils/load-md-files"; +import type { ThreadCount } from "../src/types"; + +jest.mock("../src/utils/batch-lint", () => ({ + batchLint: jest.fn(), +})); +jest.mock("../src/utils/load-md-files", () => ({ + loadMdFiles: jest.fn(), +})); + +const mockBatchLint = batchLint as jest.MockedFunction; +const mockLoadMdFiles = loadMdFiles as jest.MockedFunction; + +describe("file stat reuse", () => { + const statCases: Array<[string, ThreadCount, number | null, number]> = [ + ["fixed threads without a size limit", 2, null, 0], + ["auto threads without a size limit", "auto", null, 2], + ["fixed threads with a size limit", 2, 1024, 2], + ]; + + beforeEach(() => { + jest.resetAllMocks(); + mockBatchLint.mockResolvedValue({ + allResults: [], + actionableResults: [], + }); + jest.spyOn(console, "error").mockImplementation(); + jest.spyOn(console, "log").mockImplementation(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + test.each(statCases)( + "%s performs the expected stat calls", + async (_name, threadCount, maxFileSizeBytes, expectedCalls) => { + const files = ["a.md", "b.md"]; + mockLoadMdFiles.mockResolvedValue(files); + const fsPromises = require("fs/promises"); + const statSpy = jest + .spyOn(fsPromises, "stat") + .mockResolvedValue({ size: 100 } as Stats); + + await runFileLint({ + excludeFiles: [], + extensions: [".md"], + files: ["*.md"], + isDev: false, + isFixMode: false, + maxFileSizeBytes, + rules: {}, + startTime: 0, + suppressWarnings: false, + threadCount, + }); + + expect(statSpy).toHaveBeenCalledTimes(expectedCalls); + } + ); + + test("stats each file once before filtering and adaptive concurrency", async () => { + const files = ["huge.md", "a.md", "b.md"]; + const sizes = new Map([ + ["huge.md", 20 * 1024 * 1024], + ["a.md", 100 * 1024], + ["b.md", 100 * 1024], + ]); + mockLoadMdFiles.mockResolvedValue(files); + const fsPromises = require("fs/promises"); + const statSpy = jest + .spyOn(fsPromises, "stat") + .mockImplementation(async (filePath) => { + return { size: sizes.get(String(filePath)) ?? 0 } as Stats; + }); + + await runFileLint({ + excludeFiles: [], + extensions: [".md"], + files: ["*.md"], + isDev: false, + isFixMode: false, + maxFileSizeBytes: 5 * 1024 * 1024, + rules: {}, + startTime: 0, + suppressWarnings: false, + threadCount: "auto", + }); + + expect(statSpy).toHaveBeenCalledTimes(files.length); + expect(mockBatchLint).toHaveBeenCalledWith( + Math.min(availableParallelism(), 2), + ["a.md", "b.md"], + false, + {} + ); + }); +}); diff --git a/__tests__/max-file-size.spec.ts b/__tests__/max-file-size.spec.ts index 7d65712..0ad8f04 100644 --- a/__tests__/max-file-size.spec.ts +++ b/__tests__/max-file-size.spec.ts @@ -4,7 +4,7 @@ import { tmpdir } from "os"; import * as path from "path"; import { spawnSync } from "child_process"; import { filterFilesByMaxSize } from "../src/utils/filter-by-max-size"; -import { STAT_CONCURRENCY_LIMIT } from "../src/utils/file-stat"; +import { STAT_CONCURRENCY_LIMIT, statFiles } from "../src/utils/file-stat"; import { parseSize } from "../src/utils/parse-size"; const TSX = path.resolve(__dirname, "../node_modules/tsx/dist/cli.mjs"); @@ -35,8 +35,11 @@ describe("filterFilesByMaxSize (unit)", () => { const errorSpy = jest.spyOn(console, "error").mockImplementation(() => {}); try { - const kept = await filterFilesByMaxSize([small, large], parseSize("1kb")); - expect(kept).toEqual([small]); + const kept = filterFilesByMaxSize( + await statFiles([small, large]), + parseSize("1kb") + ); + expect(kept.map(({ path }) => path)).toEqual([small]); expect(errorSpy).toHaveBeenCalledWith( expect.stringContaining("warning: skipped large Markdown file") ); @@ -52,8 +55,11 @@ describe("filterFilesByMaxSize (unit)", () => { await writeFile(a, VIOLATION, "utf8"); await writeFile(b, VIOLATION, "utf8"); - const kept = await filterFilesByMaxSize([a, b], parseSize("10mb")); - expect(kept.sort()).toEqual([a, b].sort()); + const kept = filterFilesByMaxSize( + await statFiles([a, b]), + parseSize("10mb") + ); + expect(kept.map(({ path }) => path).sort()).toEqual([a, b].sort()); }); test("bounds stat concurrency to STAT_CONCURRENCY_LIMIT", async () => { @@ -82,7 +88,10 @@ describe("filterFilesByMaxSize (unit)", () => { }); try { - const kept = await filterFilesByMaxSize(filePaths, parseSize("10mb")); + const kept = filterFilesByMaxSize( + await statFiles(filePaths), + parseSize("10mb") + ); expect(kept).toHaveLength(fileCount); expect(statSpy).toHaveBeenCalledTimes(fileCount); expect(maxInFlight).toBeGreaterThan(1); @@ -94,9 +103,7 @@ describe("filterFilesByMaxSize (unit)", () => { test("propagates stat failures", async () => { const missing = path.join(tmpDir, "missing.md"); - await expect( - filterFilesByMaxSize([missing], parseSize("10mb")) - ).rejects.toThrow(); + await expect(statFiles([missing])).rejects.toThrow(); }); }); diff --git a/__tests__/run-file-lint.spec.ts b/__tests__/run-file-lint.spec.ts index f04111d..63cb16c 100644 --- a/__tests__/run-file-lint.spec.ts +++ b/__tests__/run-file-lint.spec.ts @@ -1,6 +1,7 @@ import { resolveAdaptiveConcurrency } from "../src/utils/adaptive-concurrency"; import { batchLint } from "../src/utils/batch-lint"; import { filterFilesByMaxSize } from "../src/utils/filter-by-max-size"; +import { statFiles, type FileStat } from "../src/utils/file-stat"; import { loadMdFiles } from "../src/utils/load-md-files"; import { runTasksWithLimit } from "../src/utils/run-tasks-with-limit"; import { safeWriteFile } from "../src/utils/safe-write-file"; @@ -19,6 +20,10 @@ jest.mock("../src/utils/run-tasks-with-limit", () => ({ jest.mock("../src/utils/filter-by-max-size", () => ({ filterFilesByMaxSize: jest.fn(), })); +jest.mock("../src/utils/file-stat", () => ({ + ...jest.requireActual("../src/utils/file-stat"), + statFiles: jest.fn(), +})); jest.mock("../src/utils/load-md-files", () => ({ loadMdFiles: jest.fn(), })); @@ -31,6 +36,7 @@ const mockFilterFilesByMaxSize = filterFilesByMaxSize as jest.MockedFunction< typeof filterFilesByMaxSize >; const mockLoadMdFiles = loadMdFiles as jest.MockedFunction; +const mockStatFiles = statFiles as jest.MockedFunction; const mockResolveAdaptiveConcurrency = resolveAdaptiveConcurrency as jest.MockedFunction< typeof resolveAdaptiveConcurrency @@ -65,7 +71,8 @@ describe("runFileLint", () => { jest.resetAllMocks(); process.exitCode = undefined; mockLoadMdFiles.mockResolvedValue(["document.md"]); - mockFilterFilesByMaxSize.mockImplementation(async (files) => files); + mockStatFiles.mockResolvedValue([{ path: "document.md", size: 0 }]); + mockFilterFilesByMaxSize.mockImplementation((files) => files); mockResolveAdaptiveConcurrency.mockResolvedValue({ concurrency: 1, maxFileSize: null, @@ -120,7 +127,8 @@ describe("runFileLint", () => { test("reports size filtering when all discovered files are skipped", async () => { mockLoadMdFiles.mockResolvedValue(["large.md"]); - mockFilterFilesByMaxSize.mockResolvedValue([]); + mockStatFiles.mockResolvedValue([{ path: "large.md", size: 200 }]); + mockFilterFilesByMaxSize.mockReturnValue([]); const consoleErrorSpy = jest.spyOn(console, "error").mockImplementation(); const outcome = await runFileLint(makeOptions({ maxFileSizeBytes: 100 })); @@ -135,17 +143,22 @@ describe("runFileLint", () => { test("filters files before concurrency and batch decisions", async () => { mockLoadMdFiles.mockResolvedValue(["small.md", "large.md"]); - mockFilterFilesByMaxSize.mockResolvedValue(["small.md"]); + const fileStats: FileStat[] = [ + { path: "small.md", size: 50 }, + { path: "large.md", size: 200 }, + ]; + mockStatFiles.mockResolvedValue(fileStats); + mockFilterFilesByMaxSize.mockReturnValue([fileStats[0]]); await runFileLint(makeOptions({ maxFileSizeBytes: 100 })); - expect(mockFilterFilesByMaxSize).toHaveBeenCalledWith( - ["small.md", "large.md"], - 100 + expect(mockStatFiles).toHaveBeenCalledWith(["small.md", "large.md"]); + expect(mockFilterFilesByMaxSize).toHaveBeenCalledWith(fileStats, 100); + expect(mockResolveAdaptiveConcurrency).toHaveBeenCalledWith( + 2, + ["small.md"], + 50 ); - expect(mockResolveAdaptiveConcurrency).toHaveBeenCalledWith(2, [ - "small.md", - ]); expect(mockBatchLint).toHaveBeenCalledWith(1, ["small.md"], false, {}); expect(mockFilterFilesByMaxSize.mock.invocationCallOrder[0]).toBeLessThan( mockResolveAdaptiveConcurrency.mock.invocationCallOrder[0] @@ -155,6 +168,7 @@ describe("runFileLint", () => { test("returns success and reports timing after a clean lint", async () => { const outcome = await runFileLint(makeOptions()); + expect(mockStatFiles).not.toHaveBeenCalled(); expect(console.log).toHaveBeenCalledWith(""); expect(console.log).toHaveBeenCalledWith("⌛️Done in 25ms."); expect(outcome).toEqual({ exitCode: 0 }); diff --git a/src/cli/run-lint.ts b/src/cli/run-lint.ts index 5ce4a81..ef836d7 100644 --- a/src/cli/run-lint.ts +++ b/src/cli/run-lint.ts @@ -7,6 +7,7 @@ import { resolveAdaptiveConcurrency } from "../utils/adaptive-concurrency"; import { batchLint } from "../utils/batch-lint"; import { loadMdFiles } from "../utils/load-md-files"; import { filterFilesByMaxSize } from "../utils/filter-by-max-size"; +import { getMaxFileSize, statFiles, type FileStat } from "../utils/file-stat"; import { formatCoreError } from "../utils/format-core-error"; import { formatLintReport } from "../utils/format-lint-report"; import { getUnappliedFixesWarnings } from "../utils/report-unapplied-fixes"; @@ -157,8 +158,14 @@ export const runFileLint = async ({ return SUCCESS_EXIT; } + let fileStats: FileStat[] = []; + if (maxFileSizeBytes !== null || threadCount === "auto") { + fileStats = await statFiles(mdFiles); + } + if (maxFileSizeBytes !== null) { - mdFiles = await filterFilesByMaxSize(mdFiles, maxFileSizeBytes); + fileStats = filterFilesByMaxSize(fileStats, maxFileSizeBytes); + mdFiles = fileStats.map(({ path }) => path); if (!mdFiles.length) { console.error( @@ -170,7 +177,8 @@ export const runFileLint = async ({ const concurrencyDecision = await resolveAdaptiveConcurrency( threadCount, - mdFiles + mdFiles, + getMaxFileSize(fileStats) ); const effectiveThreads = concurrencyDecision.concurrency; diff --git a/src/utils/adaptive-concurrency.ts b/src/utils/adaptive-concurrency.ts index 8c8b94f..3680d0c 100644 --- a/src/utils/adaptive-concurrency.ts +++ b/src/utils/adaptive-concurrency.ts @@ -1,6 +1,5 @@ import { availableParallelism } from "os"; import type { ThreadCount } from "../types"; -import { getMaxFileSize } from "./file-stat"; const ONE_MIB = 1024 * 1024; const FIVE_MIB = 5 * ONE_MIB; @@ -16,7 +15,8 @@ export interface AdaptiveConcurrencyDecision { export const resolveAdaptiveConcurrency = async ( threadCount: ThreadCount, - mdFilePaths: string[] + mdFilePaths: string[], + maxFileSize: number ): Promise => { const requestedConcurrency = typeof threadCount === "number" ? threadCount : availableParallelism(); @@ -37,8 +37,6 @@ export const resolveAdaptiveConcurrency = async ( }; } - const maxFileSize = await getMaxFileSize(mdFilePaths); - let limit = requestedConcurrency; if (maxFileSize >= ADAPTIVE_HUGE_FILE_THRESHOLD) { limit = 1; diff --git a/src/utils/file-stat.ts b/src/utils/file-stat.ts index ad77ee5..32d0517 100644 --- a/src/utils/file-stat.ts +++ b/src/utils/file-stat.ts @@ -4,18 +4,19 @@ import { runTasksWithLimit } from "./run-tasks-with-limit"; // Bound stat calls to prevent file descriptor bursts in large repositories. export const STAT_CONCURRENCY_LIMIT = 128; -export const getMaxFileSize = async (filePaths: string[]): Promise => { - if (filePaths.length === 0) { - return 0; - } +export interface FileStat { + path: string; + size: number; +} - // Scan all files because dev output reports the true maximum size. - const sizes = await runTasksWithLimit( - filePaths.map( - (filePath) => () => stat(filePath).then((stats) => stats.size) - ), +export const statFiles = async (filePaths: string[]): Promise => + runTasksWithLimit( + filePaths.map((filePath) => async () => ({ + path: filePath, + size: (await stat(filePath)).size, + })), STAT_CONCURRENCY_LIMIT ); - return sizes.reduce((max, current) => (current > max ? current : max), 0); -}; +export const getMaxFileSize = (fileStats: FileStat[]): number => + fileStats.reduce((max, current) => Math.max(max, current.size), 0); diff --git a/src/utils/filter-by-max-size.ts b/src/utils/filter-by-max-size.ts index 95a8028..64a80cb 100644 --- a/src/utils/filter-by-max-size.ts +++ b/src/utils/filter-by-max-size.ts @@ -1,29 +1,18 @@ -import { stat } from "fs/promises"; -import { STAT_CONCURRENCY_LIMIT } from "./file-stat"; +import type { FileStat } from "./file-stat"; import { formatBytes } from "./parse-size"; -import { runTasksWithLimit } from "./run-tasks-with-limit"; -// Keep the stat limit aligned with getMaxFileSize() to prevent fd bursts. -// A stat failure propagates to the CLI error handler. -export const filterFilesByMaxSize = async ( - mdFiles: string[], +export const filterFilesByMaxSize = ( + fileStats: FileStat[], limitBytes: number -): Promise => { - const results = await runTasksWithLimit( - mdFiles.map((file) => async () => { - const { size } = await stat(file); - if (size > limitBytes) { - console.error( - `warning: skipped large Markdown file ${file}, size ${formatBytes( - size - )} exceeds limit ${formatBytes(limitBytes)}` - ); - return null; - } - return file; - }), - STAT_CONCURRENCY_LIMIT - ); - - return results.filter((file): file is string => file !== null); -}; +): FileStat[] => + fileStats.filter(({ path, size }) => { + if (size > limitBytes) { + console.error( + `warning: skipped large Markdown file ${path}, size ${formatBytes( + size + )} exceeds limit ${formatBytes(limitBytes)}` + ); + return false; + } + return true; + });