Tracking issue for a defect in Vale itself (measured on 3.20.0), reported upstream separately. Filed here so the constraint our own code works around has a reference, and so the reproduction is ours to re-run when a new Vale is vendored.
What we measured
Two findings. The second turns the first from "slow" into "silent".
1. Cost scales with the size of a single Markdown block, not with file size.
| file |
bytes |
alerts |
time |
huge.md — 42,500 sentences, no blank lines (one block) |
3,187,512 |
42,500 |
80,688 ms |
wrapped.md — the same 42,500 sentences, blank line after each |
3,230,011 |
42,500 |
4,242 ms |
The control is larger on disk, produces the identical alert count, and runs 19x faster. The only difference is where the blank lines are.
Sweeping one block upward (equivalent generated content, one file per run):
| single block |
time |
| 128 KB |
194 ms |
| 256 KB |
542 ms |
| 512 KB |
2,060 ms |
| 3 MB |
~80 s |
The same byte totals split into ordinary paragraphs stay close to linear: 128 KB 42 ms, 256 KB 67 ms, 512 KB 158 ms, 1 MB 485 ms, 3 MB 3,855 ms.
2. Vale writes nothing until every file has been linted.
Interrupted after 5 s, well past the point where both small files were done (they take 28 ms on their own):
$ vale --output=JSON --no-exit small-a.md small-b.md huge.md > out.json &
$ sleep 5
$ kill -TERM $!
$ wc -c out.json
0 out.json
Zero bytes, not truncated JSON. Same for --output=line and the default CLI output, so it is the reporting phase rather than the JSON writer.
Why it matters here
One oversized block does not merely delay a run. Under any external time limit it costs the whole run its findings, including files that finished in milliseconds. That is the failure #321 was opened for, and it is the reason a timeout can never salvage partial results — there are no partial results to salvage.
It also means our current guard measures the wrong quantity. VALE_MAX_FILE_BYTES (#323) caps file size, but:
- a 3 MB well-formed document sails past a 128 KB cap and costs ~4 s, and
- a 512 KB single-block file may sit under a plausible cap and cost ~2 s.
File size is a proxy. It is the right proxy for now, because it is the only thing cheap to measure before spawning Vale — reading and scanning every candidate for block structure would cost more than the guard saves. Keeping #323 as-is is deliberate, not an oversight.
Reproduction
Config, .vale.ini:
StylesPath = styles
MinAlertLevel = suggestion
[*.md]
Repro.Hedging = warning
styles/Repro/Hedging.yml:
extends: existence
message: "Avoid hedging: '%s'"
level: warning
ignorecase: true
tokens:
- simply
- basically
Two normal-sized files:
printf 'This is simply a small file.\n' > small-a.md
printf 'This is basically another small file.\n' > small-b.md
One 3 MB file as a single block:
{
printf '# Heading\n\n'
i=0
while [ $i -lt 42500 ]; do
printf 'This is simply a sentence of prose that a writer might reasonably produce. '
i=$((i + 1))
done
printf '\n'
} > huge.md
The control, same sentences with a blank line after each:
{
printf '# Heading\n\n'
i=0
while [ $i -lt 42500 ]; do
printf 'This is simply a sentence of prose that a writer might reasonably produce.\n\n'
i=$((i + 1))
done
} > wrapped.md
Then:
vale --output=JSON --no-exit small-a.md small-b.md # 28 ms
vale --output=JSON --no-exit huge.md # 80688 ms, 42500 alerts
vale --output=JSON --no-exit wrapped.md # 4242 ms, 42500 alerts
POSIX sh only — no seq, no yes, no timeout (absent on stock macOS, which is why the interrupt demo uses & + sleep + kill). --no-exit keeps a nonzero exit from found alerts from masking the behaviour; it is not required to reproduce.
Measured on darwin/arm64, macOS 26.5.1, Apple silicon, vendored Vale 3.20.0.
What changes if upstream fixes it
Either fix independently removes the practical damage, so neither is a prerequisite for the other.
Refs #321
Refs #323
Refs #324
Tracking issue for a defect in Vale itself (measured on 3.20.0), reported upstream separately. Filed here so the constraint our own code works around has a reference, and so the reproduction is ours to re-run when a new Vale is vendored.
What we measured
Two findings. The second turns the first from "slow" into "silent".
1. Cost scales with the size of a single Markdown block, not with file size.
huge.md— 42,500 sentences, no blank lines (one block)wrapped.md— the same 42,500 sentences, blank line after eachThe control is larger on disk, produces the identical alert count, and runs 19x faster. The only difference is where the blank lines are.
Sweeping one block upward (equivalent generated content, one file per run):
The same byte totals split into ordinary paragraphs stay close to linear: 128 KB 42 ms, 256 KB 67 ms, 512 KB 158 ms, 1 MB 485 ms, 3 MB 3,855 ms.
2. Vale writes nothing until every file has been linted.
Interrupted after 5 s, well past the point where both small files were done (they take 28 ms on their own):
Zero bytes, not truncated JSON. Same for
--output=lineand the default CLI output, so it is the reporting phase rather than the JSON writer.Why it matters here
One oversized block does not merely delay a run. Under any external time limit it costs the whole run its findings, including files that finished in milliseconds. That is the failure #321 was opened for, and it is the reason a timeout can never salvage partial results — there are no partial results to salvage.
It also means our current guard measures the wrong quantity.
VALE_MAX_FILE_BYTES(#323) caps file size, but:File size is a proxy. It is the right proxy for now, because it is the only thing cheap to measure before spawning Vale — reading and scanning every candidate for block structure would cost more than the guard saves. Keeping #323 as-is is deliberate, not an oversight.
Reproduction
Config,
.vale.ini:styles/Repro/Hedging.yml:Two normal-sized files:
One 3 MB file as a single block:
{ printf '# Heading\n\n' i=0 while [ $i -lt 42500 ]; do printf 'This is simply a sentence of prose that a writer might reasonably produce. ' i=$((i + 1)) done printf '\n' } > huge.mdThe control, same sentences with a blank line after each:
{ printf '# Heading\n\n' i=0 while [ $i -lt 42500 ]; do printf 'This is simply a sentence of prose that a writer might reasonably produce.\n\n' i=$((i + 1)) done } > wrapped.mdThen:
POSIX
shonly — noseq, noyes, notimeout(absent on stock macOS, which is why the interrupt demo uses&+sleep+kill).--no-exitkeeps a nonzero exit from found alerts from masking the behaviour; it is not required to reproduce.Measured on darwin/arm64, macOS 26.5.1, Apple silicon, vendored Vale 3.20.0.
What changes if upstream fixes it
Either fix independently removes the practical damage, so neither is a prerequisite for the other.
Refs #321
Refs #323
Refs #324