Repository navigation
One large markdown file blocks the whole Vale run, because Vale is quadratic in file size #321
Description
check abandons every file's findings when a single markdown file is large enough to exceed the Vale timeout. It is the same shape #300 fixed for unparseable files, on a path #300 did not cover, and the size threshold is far lower than it looks because Vale is quadratic.
Found while investigating a flaky test (#262 follow-up, #320), not in the field. Nobody has hit it yet that we know of.
Vale is quadratic in the size of ONE file
Measured against the pinned binary (@taskless/vale-darwin-arm64@3.19.0-...), one existence rule, one file, prose repeated to size:
| size | time | linear would be |
|---|---|---|
| 40KB | 0.077s | — |
| 80KB | 0.30s | 0.15s |
| 160KB | 0.91s | 0.31s |
| 320KB | 3.50s | 0.62s |
| 640KB | 14.3s | 1.23s |
| 1MB | 48.5s | 2.0s |
Each doubling costs roughly 4x. This is upstream Vale's behaviour, not ours.
It is worth stressing that this is per-FILE, not per-corpus. The same ~1MB of prose spread across 400 files takes 190ms, versus 48s in one file. Volume is fine; size is not.
What our timeout does with that
VALE_TIMEOUT_MS is 60_000 (packages/cli/src/rules/vale/run.ts:47), and a timeout returns blocking: true and returns immediately:
return { status: "timeout", blocking: true, message: attempt.message };It is NOT excluded-and-retried the way an unparseable file now is. The retry loop added for #300 narrows the run on a config error naming a target file; a timeout has no such path.
So one file over roughly 1.1MB takes Vale past 60s, and the entire run is blocked. Every other file's findings are lost, which is precisely the complaint in #300: "a checker's silence stops meaning anything."
Why the threshold is lower in practice
The 1.1MB figure is for a single rule on one machine. Real projects have several rules, slower or contended hardware, and CI runners. The quadratic means the margin does not degrade gently: 640KB is already 14 seconds, so a project can sit well inside the timeout while being one long note away from falling off it, and the failure arrives as a whole-run block rather than as a slow file.
Markdown of that size is unusual but not exotic: a generated changelog, an exported vault note, a data table pasted into prose. #300 was found on a 328-file markdown vault.
Suggested direction
Not obvious, and worth deciding rather than patching:
- Treat a timeout like a parse error where possible. Vale's timeout gives us no path, so there is nothing to exclude on the retry. Running per-file to attribute it would undo the batching the current design relies on.
- Report partial results instead of abandoning them. If a timeout kills a batch, findings already parsed from stdout could still be returned alongside a blocking notice naming what was not checked. That keeps one slow file from costing every other file's findings, which is the actual harm.
- Reconsider whether a timeout must be blocking at all. It is blocking today so a silently disabled engine cannot read as a clean pass, which is the right instinct. A partial result plus an explicit "these files were not checked" notice may serve that better than discarding everything.
- A per-file size guard that reports an oversized file as its own finding, the way an unparseable one now is, rather than letting it consume the batch's whole budget.
Repro
- A project with one Vale rule.
- A single
.mdfile of ~1.2MB of prose. npx @taskless/cli check . --json- The run blocks on the timeout;
resultscarries nothing for any other file.
The measurements above are reproducible with the binary directly, which is the faster way to see the curve.
Activity
fix(cli): skip an oversized file instead of letting Vale time out on it
test(cli): pin that a bare [section] pattern does not recurse like --…
test(cli): pin that a bare [section] pattern does not recurse like --…