diff --git a/README.md b/README.md index aab74ef..a99082c 100644 --- a/README.md +++ b/README.md @@ -77,13 +77,23 @@ docker run --rm \ - `-c, --config `:指定配置文件(默认 `./.lintmdrc`) - `-f, --fix`:自动修复可修复问题 -- `-t, --threads [thread-count]`:设置并发线程数;传 `auto` 时按文件大小自适应,默认取 CPU 核数 +- `-t, --threads [thread-count]`:设置并发线程数;默认 `auto` 按文件大小使用保守上限,传数字可显式覆盖自动限制 - `-s, --suppress-warnings`:忽略 warning 对退出码的影响(便于 CI 渐进接入) - `-i, --stdin`:从标准输入读取 Markdown 内容 - `--max-file-size `:跳过超过指定大小的 Markdown 文件(如 `5mb`、`500kb`、`1gb`),并向 stderr 输出警告 - `-d, --dev`:开发调试模式 - `-v, --version`:查看版本 +未指定 `--threads` 时,程序等同于使用 `--threads auto`。`auto` 根据过滤后最大文件的大小设置 worker 上限: + +| 过滤后最大文件大小 | worker 上限 | +| --- | ---: | +| 小于 `1 MiB` | 4 | +| 大于或等于 `1 MiB`,且小于 `5 MiB` | 2 | +| 大于或等于 `5 MiB` | 1 | + +在 `auto` 模式下,实际 worker 数不会超过 CPU 并行度、文件数量和上述文件大小上限。显式传入 `--threads N` 会覆盖自动限制,但仍不会超过文件数量。 + ## 配置示例(`.lintmdrc`) ```json diff --git a/__tests__/batch-lint.spec.ts b/__tests__/batch-lint.spec.ts index 802dc7b..8835760 100644 --- a/__tests__/batch-lint.spec.ts +++ b/__tests__/batch-lint.spec.ts @@ -407,18 +407,18 @@ describe("resolveAdaptiveConcurrency", () => { }); }); - test("small files (< 1 MiB) use cpuLimit clamped to fileCount", async () => { - const files = await Promise.all([ - writeSizedFile("a.md", 1024), - writeSizedFile("b.md", 2048), - writeSizedFile("c.md", 4096), - ]); + test("small files (< 1 MiB) cap concurrency at 4", async () => { + const files = await Promise.all( + Array.from({ length: 8 }, (_, index) => + writeSizedFile(`small-${index}.md`, 4096) + ) + ); const cpuLimit = availableParallelism(); const statSpy = jest.spyOn(require("fs/promises"), "stat"); try { expect(await resolveAdaptiveConcurrency("auto", files, 4096)).toEqual({ - concurrency: Math.min(cpuLimit, files.length), + concurrency: Math.min(cpuLimit, 4, files.length), maxFileSize: 4096, requestedConcurrency: cpuLimit, }); diff --git a/__tests__/configure.spec.ts b/__tests__/configure.spec.ts index f3cf998..3ce7567 100644 --- a/__tests__/configure.spec.ts +++ b/__tests__/configure.spec.ts @@ -1,5 +1,5 @@ import { mkdtempSync, rmSync, writeFileSync } from "fs"; -import { cpus, tmpdir } from "os"; +import { tmpdir } from "os"; import * as path from "path"; import { CliError } from "../src/cli/cli-error"; import { @@ -268,10 +268,10 @@ describe("configuration validation", () => { }); }); - test("uses the CPU count when threads are not specified", () => { - expect(getThreadCount(undefined)).toBe(cpus().length); - expect(getThreadCount(false)).toBe(cpus().length); - expect(getThreadCount(true)).toBe(cpus().length); + test("uses auto when threads are not specified", () => { + expect(getThreadCount(undefined)).toBe("auto"); + expect(getThreadCount(false)).toBe("auto"); + expect(getThreadCount(true)).toBe("auto"); }); test("accepts positive integer thread counts", () => { diff --git a/__tests__/run-file-lint.spec.ts b/__tests__/run-file-lint.spec.ts index 63cb16c..4c46d28 100644 --- a/__tests__/run-file-lint.spec.ts +++ b/__tests__/run-file-lint.spec.ts @@ -174,6 +174,37 @@ describe("runFileLint", () => { expect(outcome).toEqual({ exitCode: 0 }); }); + test("reports the small-file auto cap in development mode", async () => { + const fileStats = Array.from({ length: 8 }, (_, index) => ({ + path: `small-${index}.md`, + size: 512 * 1024, + })); + mockLoadMdFiles.mockResolvedValue(fileStats.map(({ path }) => path)); + mockStatFiles.mockResolvedValue(fileStats); + mockResolveAdaptiveConcurrency.mockResolvedValue({ + concurrency: 4, + maxFileSize: 512 * 1024, + requestedConcurrency: 16, + }); + + await runFileLint( + makeOptions({ + isDev: true, + threadCount: "auto", + }) + ); + + expect(console.log).toHaveBeenCalledWith( + "[lint-md] Adaptive concurrency: requested auto, effective 4, max file 0.50 MiB" + ); + expect(mockBatchLint).toHaveBeenCalledWith( + 4, + fileStats.map(({ path }) => path), + false, + {} + ); + }); + test("reports rule failures to stderr and returns failure without timing", async () => { const failedResult: BatchLintItem = { path: "failed.md", diff --git a/src/cli/run-lint.ts b/src/cli/run-lint.ts index ef836d7..73e8633 100644 --- a/src/cli/run-lint.ts +++ b/src/cli/run-lint.ts @@ -184,8 +184,7 @@ export const runFileLint = async ({ if (isDev && concurrencyDecision.maxFileSize !== null) { const { maxFileSize, requestedConcurrency } = concurrencyDecision; - const adaptiveApplied = maxFileSize >= 1024 * 1024; - if (adaptiveApplied && effectiveThreads < requestedConcurrency) { + if (effectiveThreads < requestedConcurrency) { const maxMiB = (maxFileSize / (1024 * 1024)).toFixed(2); console.log( `[lint-md] Adaptive concurrency: requested auto, effective ${effectiveThreads}, max file ${maxMiB} MiB` diff --git a/src/lint-md.ts b/src/lint-md.ts index 215dced..3c612be 100644 --- a/src/lint-md.ts +++ b/src/lint-md.ts @@ -46,7 +46,7 @@ export const createProgram = (): Command => { .option("-d, --dev", "open dev mode(开启开发者模式)") .option( "-t, --threads [thread-count]", - 'Number of worker threads, or "auto" to cap concurrency for large files. Default: CPU count.(执行 Lint / Fix 的线程数,传 "auto" 时根据文件大小自适应)' + 'Worker threads, or "auto" for conservative file-size limits. Default: auto.(执行 Lint / Fix 的线程数。默认 auto;传数字可显式覆盖自动限制)' ) .option( "-s, --suppress-warnings", diff --git a/src/utils/adaptive-concurrency.ts b/src/utils/adaptive-concurrency.ts index 3680d0c..b380da0 100644 --- a/src/utils/adaptive-concurrency.ts +++ b/src/utils/adaptive-concurrency.ts @@ -3,6 +3,7 @@ import type { ThreadCount } from "../types"; const ONE_MIB = 1024 * 1024; const FIVE_MIB = 5 * ONE_MIB; +const ADAPTIVE_SMALL_CAP = 4; const ADAPTIVE_MEDIUM_CAP = 2; const ADAPTIVE_LARGE_FILE_THRESHOLD = ONE_MIB; const ADAPTIVE_HUGE_FILE_THRESHOLD = FIVE_MIB; @@ -37,7 +38,7 @@ export const resolveAdaptiveConcurrency = async ( }; } - let limit = requestedConcurrency; + let limit = Math.min(requestedConcurrency, ADAPTIVE_SMALL_CAP); if (maxFileSize >= ADAPTIVE_HUGE_FILE_THRESHOLD) { limit = 1; } else if (maxFileSize >= ADAPTIVE_LARGE_FILE_THRESHOLD) { diff --git a/src/utils/configure.ts b/src/utils/configure.ts index fcd298f..d2d194e 100644 --- a/src/utils/configure.ts +++ b/src/utils/configure.ts @@ -1,5 +1,4 @@ import * as fs from "fs"; -import { availableParallelism } from "os"; import * as path from "path"; import { CliError } from "../cli/cli-error"; import type { CLIConfig, ThreadCount } from "../types"; @@ -129,7 +128,7 @@ export const getThreadCount = ( } if (typeof threadCount !== "number" && typeof threadCount !== "string") { - return availableParallelism(); + return "auto"; } if (typeof threadCount === "string" && !/^[1-9]\d*$/.test(threadCount)) {