Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,23 @@ docker run --rm \

- `-c, --config <configure-file>`:指定配置文件(默认 `./.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 <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
Expand Down
14 changes: 7 additions & 7 deletions __tests__/batch-lint.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down
10 changes: 5 additions & 5 deletions __tests__/configure.spec.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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", () => {
Expand Down
31 changes: 31 additions & 0 deletions __tests__/run-file-lint.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 1 addition & 2 deletions src/cli/run-lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
2 changes: 1 addition & 1 deletion src/lint-md.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion src/utils/adaptive-concurrency.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 1 addition & 2 deletions src/utils/configure.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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)) {
Expand Down