Skip to content

Track upstream: Vale lint time is superlinear in single-block size, and no output is emitted until the run finishes #325

Description

@thecodedrift

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    CLIRelated to the taskless CLIbugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions