# Performance

<a id="benchmarks"></a>

These benchmarks compare the speed of Badness's formatter, linter, and language
server with other LaTeX tools. They also compare the language servers' memory
use. The tools differ in formatting style, lint coverage, and editor features,
so the timings alone cannot tell you which tool best suits your work.

## Formatter

We compare `badness` with [`tex-fmt`](https://github.com/wgunderwood/tex-fmt)
and [`latexindent`](https://github.com/cmhughes/latexindent.pl) on individual
documents. Files that `badness` cannot format are excluded from every tool's
results.

- **badness**: `0.23.0`
- **tex-fmt**: `0.5.7`
- **latexindent**: `3.24.7`
- **backend**: hyperfine (min runs: 3)
- **host**: linux/x86_64, AMD Ryzen 9 7900 12-Core Processor
- **generated**: 2026-09-21T14:09:42Z


### Single-file results

<div class="bench-chart-block">
<figure class="bench-figure">
<figcaption>Mean formatting time relative to <code>badness</code> on a logarithmic scale. The dashed line marks <code>badness</code> at 1; faster tools fall below it. Color distinguishes documents. Hover over a dot for times in milliseconds.</figcaption>
</figure>
<details class="bench-table" open>
<summary>Data table</summary>
<h3>small.tex (baseline) (1233 bytes, 48 lines)</h3>
<table>
<thead><tr><th>Tool</th><th>Mean (ms)</th><th>Min (ms)</th><th>Max (ms)</th><th>Relative</th></tr></thead>
<tbody>
<tr><td>badness</td><td>2.2280</td><td>1.5594</td><td>4.5042</td><td>baseline</td></tr>
<tr><td>tex-fmt</td><td>1.8447</td><td>1.4077</td><td>5.2116</td><td>1.2× faster</td></tr>
<tr><td>latexindent</td><td>60.4411</td><td>58.3468</td><td>63.3773</td><td>27.1× slower</td></tr>
</tbody>
</table>
<h3>cv.tex (6273 bytes, 275 lines)</h3>
<table>
<thead><tr><th>Tool</th><th>Mean (ms)</th><th>Min (ms)</th><th>Max (ms)</th><th>Relative</th></tr></thead>
<tbody>
<tr><td>badness</td><td>2.8968</td><td>2.1975</td><td>7.0406</td><td>baseline</td></tr>
<tr><td>tex-fmt</td><td>1.8836</td><td>1.4124</td><td>5.2885</td><td>1.5× faster</td></tr>
<tr><td>latexindent</td><td>67.6548</td><td>64.6806</td><td>72.6389</td><td>23.4× slower</td></tr>
</tbody>
</table>
<h3>masters_dissertation.tex (95383 bytes, 2458 lines)</h3>
<table>
<thead><tr><th>Tool</th><th>Mean (ms)</th><th>Min (ms)</th><th>Max (ms)</th><th>Relative</th></tr></thead>
<tbody>
<tr><td>badness</td><td>21.2434</td><td>19.9534</td><td>24.0947</td><td>baseline</td></tr>
<tr><td>tex-fmt</td><td>2.5175</td><td>2.1186</td><td>5.6226</td><td>8.4× faster</td></tr>
<tr><td>latexindent</td><td>1619.6812</td><td>1616.0399</td><td>1624.4684</td><td>76.2× slower</td></tr>
</tbody>
</table>
<h3>phd_dissertation.tex (730369 bytes, 27482 lines)</h3>
<table>
<thead><tr><th>Tool</th><th>Mean (ms)</th><th>Min (ms)</th><th>Max (ms)</th><th>Relative</th></tr></thead>
<tbody>
<tr><td>badness</td><td>206.0519</td><td>198.8200</td><td>214.6120</td><td>baseline</td></tr>
<tr><td>tex-fmt</td><td>9.3448</td><td>8.5763</td><td>12.3872</td><td>22.0× faster</td></tr>
<tr><td>latexindent</td><td>25787.0356</td><td>25586.9122</td><td>25980.9774</td><td>125.1× slower</td></tr>
</tbody>
</table>
</details>
</div>


### Whole-project results

This comparison measures the time to check formatting across the `.tex` files of
[`kks32/phd-thesis-template`]. Both tools compute the formatted output without
writing changes to disk. `latexindent` is omitted because it has no recursive
directory mode.

<div class="bench-chart-block">
<figure class="bench-figure">
<figcaption>Mean time to check formatting across the thesis project, relative to <code>badness</code> on a logarithmic scale. The dashed line marks <code>badness</code> at 1; faster tools fall below it. Hover over a dot for times in milliseconds.</figcaption>
</figure>
<details class="bench-table" open>
<summary>Data table</summary>
<h3>project (12 files) (47190 bytes, 1005 lines)</h3>
<table>
<thead><tr><th>Tool</th><th>Mean (ms)</th><th>Min (ms)</th><th>Max (ms)</th><th>Relative</th></tr></thead>
<tbody>
<tr><td>badness</td><td>6.0545</td><td>4.8906</td><td>9.2864</td><td>baseline</td></tr>
<tr><td>tex-fmt</td><td>2.5443</td><td>2.1472</td><td>6.0691</td><td>2.4× faster</td></tr>
</tbody>
</table>
</details>
</div>


## Linter

We compare `badness lint` with [`lacheck`](https://ctan.org/pkg/lacheck) and
[`chktex`](https://ctan.org/pkg/chktex) on the same individual documents. Each
linter checks for a different set of problems. Neither comparison tool has a
recursive directory mode, so this benchmark covers individual files only.

- **badness**: `0.23.0`
- **lacheck**: `1.30`
- **chktex**: `v1.7.9`
- **backend**: hyperfine (min runs: 3)
- **host**: linux/x86_64, AMD Ryzen 9 7900 12-Core Processor
- **generated**: 2026-09-21T14:09:42Z


<div class="bench-chart-block">
<figure class="bench-figure">
<figcaption>Mean linting time relative to <code>badness</code> on a logarithmic scale. The dashed line marks <code>badness</code> at 1; faster tools fall below it. Color distinguishes documents. Hover over a dot for times in milliseconds.</figcaption>
</figure>
<details class="bench-table" open>
<summary>Data table</summary>
<h3>small.tex (baseline) (1233 bytes, 48 lines)</h3>
<table>
<thead><tr><th>Tool</th><th>Mean (ms)</th><th>Min (ms)</th><th>Max (ms)</th><th>Relative</th></tr></thead>
<tbody>
<tr><td>badness</td><td>2.5283</td><td>1.9014</td><td>4.2127</td><td>baseline</td></tr>
<tr><td>lacheck</td><td>6.2830</td><td>5.7361</td><td>7.5064</td><td>2.5× slower</td></tr>
<tr><td>chktex</td><td>38.6138</td><td>33.0979</td><td>54.0230</td><td>15.3× slower</td></tr>
</tbody>
</table>
<h3>cv.tex (6273 bytes, 275 lines)</h3>
<table>
<thead><tr><th>Tool</th><th>Mean (ms)</th><th>Min (ms)</th><th>Max (ms)</th><th>Relative</th></tr></thead>
<tbody>
<tr><td>badness</td><td>2.9312</td><td>2.3599</td><td>4.5520</td><td>baseline</td></tr>
<tr><td>lacheck</td><td>6.5533</td><td>5.9206</td><td>9.3171</td><td>2.2× slower</td></tr>
<tr><td>chktex</td><td>37.9784</td><td>33.5403</td><td>46.2528</td><td>13.0× slower</td></tr>
</tbody>
</table>
<h3>masters_dissertation.tex (95383 bytes, 2458 lines)</h3>
<table>
<thead><tr><th>Tool</th><th>Mean (ms)</th><th>Min (ms)</th><th>Max (ms)</th><th>Relative</th></tr></thead>
<tbody>
<tr><td>badness</td><td>58.9658</td><td>54.1457</td><td>70.2911</td><td>baseline</td></tr>
<tr><td>lacheck</td><td>8.3681</td><td>7.7195</td><td>10.9566</td><td>7.0× faster</td></tr>
<tr><td>chktex</td><td>41.0897</td><td>36.1493</td><td>50.0436</td><td>1.4× faster</td></tr>
</tbody>
</table>
<h3>phd_dissertation.tex (730369 bytes, 27482 lines)</h3>
<table>
<thead><tr><th>Tool</th><th>Mean (ms)</th><th>Min (ms)</th><th>Max (ms)</th><th>Relative</th></tr></thead>
<tbody>
<tr><td>badness</td><td>201.2823</td><td>191.1012</td><td>207.7070</td><td>baseline</td></tr>
<tr><td>lacheck</td><td>21.0009</td><td>19.8983</td><td>23.0398</td><td>9.6× faster</td></tr>
<tr><td>chktex</td><td>68.7874</td><td>62.4486</td><td>81.9185</td><td>2.9× faster</td></tr>
</tbody>
</table>
</details>
</div>


## Language Server

We compare Badness with [TexLab](https://github.com/latex-lsp/texlab) by opening
the same five documents in the complete thesis project. The benchmark measures
how long each server takes to start, respond to editor requests, and finish
background work, as well as how much memory it uses.

- **Badness**: `0.23.0`
- **TexLab**: `5.26.0`
- **corpus**: [`phd-thesis-template` @ `3ce347686d75`](https://github.com/kks32/phd-thesis-template) (`v2.4`, 16 source files, 279004 bytes)
- **session**: 3 fresh runs per server; 5 open files (37773 bytes)
- **sampling**: every 0.15 s; quiet for 5.0 s; 60 s phase timeout
- **host**: Linux x86_64, AMD Ryzen 9 7900 12-Core Processor (61.9 GiB RAM)
- **generated**: 2026-09-21T14:17:30Z
- **navigation target**: `Aup91` in `Chapter1/chapter1.tex` at line 19


### Speed

The startup measurements cover three stages:

- **Initialize** measures the server's response to the editor's initialization
  request.
- **Workspace ready** measures the time from process start until background
  indexing settles.
- **Open files ready** measures the time from opening the documents until
  diagnostics arrive and background work settles.

After startup, we time requests for document symbols, hover information,
definitions, references, and renaming. The chart shows the median response
times. Tooltips and expandable tables include the 95th percentile (p95) and the
number of results returned, which can differ between servers.

#### Readiness

<div class="bench-chart-block" data-chart="lsp-readiness">
<figure class="bench-figure">
<figcaption>Time until each server is ready, on a logarithmic scale. Dots show medians across fresh sessions, and color distinguishes the waits. Lower is faster. Hover over a dot for minimum and maximum times.</figcaption>
</figure>
<details class="bench-table" open>
<summary>Data table</summary>

| Server | Wait | Median | Min | Max |
| --- | --- | ---: | ---: | ---: |
| Badness | Initialize | 1.000 ms | 1.000 ms | 1.000 ms |
| Badness | Workspace ready | 152.000 ms | 152.000 ms | 152.000 ms |
| Badness | Open files ready | 33.000 ms | 33.000 ms | 35.000 ms |
| TexLab | Initialize | 1.000 ms | 1.000 ms | 7.000 ms |
| TexLab | Workspace ready | 152.000 ms | 152.000 ms | 605.000 ms |
| TexLab | Open files ready | 40.000 ms | 38.000 ms | 42.000 ms |

</details>
</div>

#### Warm requests

<div class="bench-chart-block" data-chart="lsp-latency">
<figure class="bench-figure">
<figcaption>Warm request latency on a logarithmic scale. Dots show medians, and color distinguishes operations. Lower is faster. Hover over a dot for the 95th percentile, sample counts, and response details.</figcaption>
</figure>
<details class="bench-table" open>
<summary>Data table</summary>

| Server | Request | Median | p95 | Returned work | Samples |
| --- | --- | ---: | ---: | --- | ---: |
| Badness | Document symbols | 0.224 ms | 0.315 ms | 5–16 symbols (median 14), 3 KiB | 180 |
| Badness | Hover | 0.115 ms | 0.154 ms | 1 result, 206 B | 180 |
| Badness | Go to definition | 0.096 ms | 0.113 ms | 1 location in 1 file, 175 B | 60 |
| Badness | Find references | 0.103 ms | 0.139 ms | 2 locations in 2 files, 346 B | 60 |
| Badness | Rename | 0.091 ms | 0.119 ms | 2 edits in 2 files, 418 B | 60 |
| TexLab | Document symbols | 0.247 ms | 0.451 ms | 5–33 symbols (median 20), 5 KiB | 180 |
| TexLab | Hover | 0.077 ms | 0.119 ms | 1 result, 207 B | 180 |
| TexLab | Go to definition | 0.068 ms | 0.080 ms | 1 location in 1 file, 371 B | 60 |
| TexLab | Find references | 0.065 ms | 0.077 ms | 2 locations in 2 files, 346 B | 60 |
| TexLab | Rename | 0.066 ms | 0.080 ms | 2 edits in 2 files, 418 B | 60 |

</details>
</div>

_Warm requests show medians across all samples, with p95 in tooltips and the table. Each target ran 20 measured rounds in each of 3 fresh sessions after 2 warmup rounds; symbols and hover span 3 files, while definition, references, and rename use `Aup91` in `Chapter1/chapter1.tex`. Rename constructs the workspace edit but does not apply it._


### Memory

The chart shows median memory use across three fresh sessions, including child
processes. **RSS** counts resident memory, including shared pages in each
process. The tooltips also show **PSS**, which divides shared pages among the
processes using them to estimate their share of physical memory.

<div class="bench-chart-block" data-chart="lsp-memory">
<figure class="bench-figure">
<figcaption>Median whole-process-tree RSS across fresh processes. Baseline follows initialization; settled follows the open-file workload. Peak is the largest sample through the timed requests. Tooltips also show PSS.</figcaption>
</figure>
<details class="bench-table" open>
<summary>Data table</summary>

| Server | Milestone | RSS | PSS | Relative settled RSS |
| --- | --- | ---: | ---: | ---: |
| Badness | Baseline | 9.1 MB | 7.2 MB | - |
| Badness | Settled | 13.9 MB | 12.0 MB | baseline |
| Badness | Peak | 13.9 MB | 12.0 MB | - |
| TexLab | Baseline | 22.5 MB | 20.7 MB | - |
| TexLab | Settled | 29.6 MB | 27.7 MB | 2.13× |
| TexLab | Peak | 29.6 MB | 27.7 MB | - |

</details>
</div>


## Reproducibility

Run these commands from the repository root:

```sh
task bench:download  # Fetch the benchmark documents.
task bench          # Measure formatter and linter speed.
task bench:lsp      # Measure language-server speed and memory.
```

The scripts build `badness` in release mode. The formatter and linter
comparisons use the tools available on `PATH` and skip any that are missing.
Install [`hyperfine`][hyperfine] and `jq` for timing statistics; without them,
the script uses a shell loop that reports only mean times. The language-server
benchmark requires Linux, Python 3, and `texlab`. `task bench:memory` is an
alias for `task bench:lsp`.

The commands write `benches/benchmark_results.json` and
`benches/memory_results.json`. These committed files supply the charts, machine
details, and tool versions shown above. Building the documentation reads these
files without running the benchmarks. Neither benchmark runs in CI.

### Documents

The individual documents are a committed `small.tex` baseline and three files
from a pinned `tex-fmt` release: `cv.tex`, `masters_dissertation.tex`, and
`phd_dissertation.tex`. The thesis project comes from a pinned revision of
[`kks32/phd-thesis-template`]. `benches/documents/download.sh` records both
pins.

The formatter and linter benchmarks skip any document that `badness` cannot
format. For the project comparison, the script copies a fixed set of `.tex`
files into a temporary directory, excluding unsupported files from both tools.
This gives both formatters the same input files without interference from Git
ignore rules. The language servers use the complete project, including its
class, style, bibliography, and image files.

### Formatter and linter commands

For individual documents, each formatter reads from standard input and writes to
standard output:

  | Tool          | Invocation                                              |
  | ------------- | ------------------------------------------------------- |
  | `badness`     | `badness format --no-config --stdin-filepath bench.tex` |
  | `tex-fmt`     | `tex-fmt --stdin`                                       |
  | `latexindent` | `latexindent -g /dev/null -`                            |

The project comparison includes directory traversal and uses check mode:

  | Tool      | Invocation                                 |
  | --------- | ------------------------------------------ |
  | `badness` | `badness format --no-config --check <dir>` |
  | `tex-fmt` | `tex-fmt --check --recursive <dir>`        |

Each linter reads the document from its path:

  | Tool      | Invocation                        |
  | --------- | --------------------------------- |
  | `badness` | `badness lint --no-config <file>` |
  | `chktex`  | `chktex -q <file>`                |
  | `lacheck` | `lacheck <file>`                  |

With `hyperfine`, each command gets one warmup and at least three measured runs.
The script ignores exit codes because lint findings and formatting differences
can produce nonzero exits. The commands and timing loop are defined in
`benches/compare_format.sh`.

### Language-server sessions

`benches/compare_lsp_memory.sh` starts three fresh sessions each of
`badness lsp` and `texlab run`. In each session, the harness initializes the
server, waits for background work to settle, opens five documents, and collects
diagnostics using the server's pull or push model. It then requests document
symbols and citation or reference hovers and waits for background work to settle
again.

The timed symbol and hover requests cover three chapter files. Definition,
references, and rename use the `Aup91` citation in `Chapter1/chapter1.tex`,
whose entry is in `References/references.bib`. References include the
declaration. Rename computes edits without applying them. Each request target
gets two warmup rounds and 20 measured rounds per session. The chart aggregates
these samples across all three sessions. The results also record response sizes
and counts of symbols, locations, edits, and affected files.

The harness samples the server and all descendant processes through Linux
`/proc` every 150 ms. Background work has settled when CPU use stays below 5% of
one core for five seconds. A phase fails if it does not settle within 60
seconds. Workspace and open-file readiness timings end at the start of their
respective quiet periods.

Memory is recorded after initialization (**Baseline**) and after the open-file
workload settles (**Settled**). **Peak** is the largest sample through the timed
requests. The chart shows the median of each measurement across the three
sessions, and the JSON file retains the measurements from each session.

[hyperfine]: https://github.com/sharkdp/hyperfine
[`kks32/phd-thesis-template`]: https://github.com/kks32/phd-thesis-template
