Performance
Fatou is a compiled Rust tool where the alternatives are Julia programs running in a Julia runtime. This page measures where that difference shows up: how fast it formats, against Runic and JuliaFormatter, and how quickly it responds and how much memory it holds, against the Julia language servers LanguageServer.jl and JETLS.
Every number here comes from a benchmark you can re-run: task bench for
formatter throughput and task bench-lsp for language-server speed and memory.
Both write a committed artifact that this page renders; nothing is measured at
site-build time, so a figure that moves has to be re-measured and committed
deliberately.
Methodology
We measure each tool in a warm loop: the tool is loaded once, run through a
few warmup calls, and then timed over many iterations. This deliberately
excludes process startup and first-call JIT compilation for the Julia tools,
which would otherwise dominate and obscure the actual formatting cost. In other
words, these numbers reflect a long-lived editor or language-server session, not
the cold julia -e ... command-line invocation. That cold path is measured
separately in Cold start below.
Because each tool runs in its own runtime, we report throughput in MB/s, which normalizes for byte count and stays comparable even when tools cover different files. Each tool formats with its own default style; we are measuring speed, not comparing output. A file counts for a tool only if that tool formats it without error, and any skips are reported.
Corpora
Two real-world projects, both pinned to a tag, picked to pull in opposite directions:
- JuliaSyntax.jl, the parser Fatou targets for parity: dense branching, large token tables, and the code Fatou is best equipped to handle. Home turf.
- DataFrames.jl, ordinary library code of the kind users actually format: docstring-heavy, macro-heavy, built around a large indexing DSL, and roughly 2.6x the size of the JuliaSyntax tree.
Scenarios
- Single file (three of them), through each tool’s pure
String -> Stringformatter (fatou::formatter::format,Runic.format_string,JuliaFormatter.format_text). The three targets span both size and shape:parse_stream.jl(42 KB of dense parser internals),kinds.jl(24 KB that is almost entirely one flat macro/data table), andabstractdataframe.jl(100 KB of docstring- and macro-heavy application code). - Project (one per corpus): the whole
src/tree, driven through each tool’s own directory entry point, so file discovery, IO, and the tool’s internal parallel scheduling all count. This is the “format my whole project” path. Fatou usesfatou::formatter::check_paths(glob/directory discovery plus rayon-parallel formatting, read-only); JuliaFormatter usesformat(dir; overwrite = false)(recursive, thread-parallel, read-only). Runic is excluded from these scenarios by design: it has no in-process directory API (itsformat_fileis single-file only, and directory walking lives solely in its CLI), so there is nothing to measure on the same terms.
Reproduce with task bench (after reloading the devenv shell so Runic is on
the Julia path). Results are written to bench/results.json.
Setup
- Corpora: JuliaSyntax.jl
v0.4.10(09576ca), DataFrames.jlv1.8.2(946c72a) - Versions: Fatou
0.20.0, Runic1.9.0, JuliaFormatter2.12.6, Julia1.13.0 - Host: AMD Ryzen 9 7900 12-Core Processor (Linux x86_64)
- Machine:
terra - Warm-loop iterations: 50 single, 20 project; 3 warmup
- Cold-start iterations: 5 fresh-process runs (single file)
Results
Single files and whole projects get a chart each: they measure different work at different sizes, and sharing one axis buries that. In both, Fatou is the baseline on the dashed line at 1, and every other tool’s time is plotted relative to it, so faster tools fall below the line and slower tools rise above it.
Single files
String -> String formatter, in the tool's own default style. Hover a dot for the exact figures.Data table
parse_stream.jl (JuliaSyntax/src/parse_stream.jl)
| Tool | Files | Bytes | Median (ms) | Throughput (MB/s) | Relative |
|---|---|---|---|---|---|
| Fatou | 1 | 41,937 | 4.1 | 10.26 | baseline |
| Runic | 1 | 41,937 | 15.8 | 2.65 | 3.88x |
| JuliaFormatter | 1 | 41,937 | 7.8 | 5.35 | 1.92x |
kinds.jl (JuliaSyntax/src/kinds.jl)
| Tool | Files | Bytes | Median (ms) | Throughput (MB/s) | Relative |
|---|---|---|---|---|---|
| Fatou | 1 | 24,442 | 1.5 | 15.79 | baseline |
| Runic | 1 | 24,442 | 12.6 | 1.94 | 8.13x |
| JuliaFormatter | 1 | 24,442 | 4.1 | 5.97 | 2.64x |
abstractdataframe.jl (DataFrames/src/abstractdataframe/abstractdataframe.jl)
| Tool | Files | Bytes | Median (ms) | Throughput (MB/s) | Relative |
|---|---|---|---|---|---|
| Fatou | 1 | 100,388 | 5.6 | 18.04 | baseline |
| Runic | 1 | 100,388 | 31.5 | 3.19 | 5.66x |
| JuliaFormatter | 1 | 100,388 | 13.7 | 7.35 | 2.46x |
Projects
Runic is absent because it has no in-process directory API. Hover a dot for the exact figures.Data table
JuliaSyntax (JuliaSyntax/src)
| Tool | Files | Bytes | Median (ms) | Throughput (MB/s) | Relative |
|---|---|---|---|---|---|
| Fatou | 15 | 332,783 | 10.6 | 31.37 | baseline |
| JuliaFormatter | 15 | 332,783 | 27.3 | 12.20 | 2.57x |
DataFrames (DataFrames/src)
| Tool | Files | Bytes | Median (ms) | Throughput (MB/s) | Relative |
|---|---|---|---|---|---|
| Fatou | 36 | 870,581 | 9.9 | 87.52 | baseline |
| JuliaFormatter | 36 | 870,581 | 34.0 | 25.59 | 3.42x |
Cold start
The warm loop above is the right model for an editor or language server that
stays resident, but it hides the cost a command-line user pays on the very first
run. This section measures that cold start directly: each tool is invoked as
a fresh process that starts up, formats the single file once, and exits. For the
Julia tools that means paying Julia’s startup, package loading, and first-call
JIT compilation every time, through the same julia -e 'using ...' path a shell
user would take; Fatou, a compiled binary, pays only process startup through
fatou format. Only one file (parse_stream.jl) is measured, since the numbers
are dominated by fixed startup and compilation cost, not by the file’s size.
fatou format; the Julia tools run through the same julia -e 'using ...' path a shell user takes, so Julia startup, package load, and first-call compilation all count. Hover a dot for the exact figures.Data table
| Tool | Median time | Throughput (MB/s) | vs Fatou |
|---|---|---|---|
| Fatou | 5.9 ms | 7.10 | baseline |
| Runic | 189.1 ms | 0.22 | 32.01x |
| JuliaFormatter | 287.1 ms | 0.15 | 48.60x |
Language servers
The comparison here is not against the formatters but against LanguageServer.jl, which runs a Julia runtime and indexes the whole environment through a SymbolServer child process, and JETLS, which runs a Julia runtime and performs real type inference through JET.
This is not like-for-like work, and the numbers should not be read as if it were. Those two servers know things Fatou cannot: JETLS can tell you a method call will not resolve at the types it actually gets, because it ran the inference. Fatou’s semantics are static, with no Julia runtime anywhere in the pipeline, so it never pays for one. What follows measures what an editor session costs, not the price of equivalent analysis.
Methodology
Every server opens the same workspace and is driven through the same scripted session over stdio, the boring one an editor produces on open:
initialize -> initialized -> wait for the server to go quiet
-> didOpen the largest files in the tree -> diagnostics
-> documentSymbol and hover -> wait for it to go quiet again
What ends each phase is quiescence, not a fixed wait: a phase is over once aggregate CPU across the process tree stays under 5% of one core for five seconds. The servers here differ by two orders of magnitude in how long they take to finish thinking, and any fixed sleep would flatter one end of that range.
The speed results split an editor session into cold readiness and warm requests:
- Initialize is the
initializerequest round trip from a fresh process. - Workspace ready runs from process start to the beginning of the final quiet window after background indexing.
- Open files ready runs from the burst of
didOpennotifications through diagnostics and the beginning of the next quiet window. - Document symbols and hover are warm stdio round trips after the server has settled. They span three open files; hover targets the first source-defined symbol in each file.
- Definition, references, and rename use
_namesat a pinned call site inselection.jl. Its definitions live elsewhere, and its references span the project. References include declarations; rename constructs theWorkspaceEditbut does not apply it.
Each target gets two unmeasured warmup rounds, then 20 measured rounds. The warm-request plot shows the median and p95. Its tooltips and collapsed detail table also report the median serialized result size and how many symbols, locations, or edits each server returns across how many files. Those counts matter: two servers are not doing comparable work when one returns fewer destinations or edits.
Sampling covers the whole process tree every 150 ms, so a server that fans work out to a helper process is charged for it — which is exactly what LanguageServer.jl’s SymbolServer pass is. Three milestones come out of each run:
- Baseline — the handshake is done and nothing is open yet. This is the floor a server costs for existing.
- Settled — files open, diagnostics in, the tree quiet again. This is the figure that matters: what the session holds while you work.
- Peak — the maximum over every sample. For a server with a short-lived helper this is the only milestone that ever sees it, which is why LanguageServer.jl peaks well above where it settles.
Resident set size is what the memory plot shows. The harness also records proportional set size, which splits shared pages between the processes mapping them; at settle the two agree within a few megabytes for all three servers, so nothing here is an artifact of double-counted shared memory.
Setup
- Workspace: DataFrames.jl
v1.8.2(946c72a), instantiated - Session: 5 files opened (343 KiB of source), diagnostics, symbols, and hovers
- Navigation target:
_namesinsrc/abstractdataframe/selection.jlat line 383 - Versions: Fatou
0.18.0, LanguageServer.jl5.0.0, JETLS7e01ca58(2026-08-08), Julia1.12.6 - Host: AMD Ryzen 9 7900 12-Core Processor (Linux x86_64, 61 GB RAM)
- Machine:
terra
Speed
Readiness and warm requests get separate plots, in seconds and milliseconds respectively. Both use a logarithmic time axis so the fastest and slowest responses remain visible; farther left is faster. Server colors stay the same across these plots and the memory plot below. Expand the detail tables for the underlying measurements and returned work.
Readiness
Readiness data
| Server | Initialize | Workspace ready | Open files ready |
|---|---|---|---|
| Fatou | 1 ms | 152 ms | 54 ms |
| LanguageServer.jl | 4.14 s | 27.70 s | 3.30 s |
| JETLS | 5.88 s | 9.88 s | 13.54 s |
Warm requests
Request timings and returned work
| Server | Request | Median | p95 | Returned work |
|---|---|---|---|---|
| Fatou | Document symbols | 1.29 ms | 1.68 ms | 54–123 symbols (median 70), 24 KiB |
| Fatou | Hover | 0.06 ms | 0.13 ms | 1 result, 162 B |
| Fatou | Go to definition | 0.28 ms | 0.40 ms | 5 locations in 4 files, 879 B |
| Fatou | Find references | 0.54 ms | 0.68 ms | 135 locations in 17 files, 23 KiB |
| Fatou | Rename | 0.61 ms | 0.69 ms | 135 edits in 17 files, 17 KiB |
| LanguageServer.jl | Document symbols | 5.42 ms | 7.96 ms | 372–626 symbols (median 519), 117 KiB |
| LanguageServer.jl | Hover | 0.09 ms | 0.35 ms | 1 result, 709 B |
| LanguageServer.jl | Go to definition | 1.30 ms | 1.38 ms | 5 locations in 4 files, 884 B |
| LanguageServer.jl | Find references | 54.30 ms | 84.15 ms | 135 locations in 17 files, 23 KiB |
| LanguageServer.jl | Rename | 54.57 ms | 59.51 ms | 135 edits in 17 files, 18 KiB |
| JETLS | Document symbols | 2.08 ms | 5.84 ms | 348–578 symbols (median 477), 131 KiB |
| JETLS | Hover | 1.28 ms | 4.23 ms | 1 result, 1 KiB |
| JETLS | Go to definition | 0.62 ms | 0.87 ms | 2 locations in 1 file, 761 B |
| JETLS | Find references | 1.07 ms | 4.38 ms | 135 locations in 17 files, 23 KiB |
| JETLS | Rename | 22.36 ms | 41.46 ms | 135 edits in 17 files, 18 KiB |
Warm requests show median / p95. Each target ran 20 measured rounds after 2 warmup rounds; symbols and hover span 3 files, while definition, references, and rename use _names in src/abstractdataframe/selection.jl.
Readiness is measured once per server because starting and indexing the Julia servers dominates a run. The warm requests have enough repetitions to expose their distribution, but they intentionally measure cached editor queries—not the first analysis of a newly opened file.
Memory
Memory data
| Server | Baseline | Settled | Peak | vs Fatou | Settled after | Doing |
|---|---|---|---|---|---|---|
| Fatou | 79 MB | 84 MB | 93 MB | baseline | 9 s | static analysis, no Julia runtime |
| LanguageServer.jl | 965 MB | 1104 MB | 1567 MB | 13.1x | 41 s | Julia runtime plus a SymbolServer pass over the environment |
| JETLS | 792 MB | 1998 MB | 2111 MB | 23.7x | 33 s | Julia runtime plus type inference through JET |
Two caveats worth carrying away from that figure. Julia’s resident memory includes garbage the collector has not returned yet, and there is no way to ask a server to collect through the protocol — these are the numbers the operating system sees, which is also the number your laptop feels, but a forced collection would hand some of it back. And every server is measured once per run rather than averaged over many, since each Julia server takes the better part of a minute to settle; Fatou’s figure moves by a few megabytes between runs, and the Julia servers’ by a few tens. The gap is two orders of magnitude wider than either, so neither caveat threatens the conclusion.
Fatou’s own footprint decomposes roughly into a fixed engine cost, the package index for the workspace’s dependency closure, and the open files themselves. The index is the dominant term, and it scales with how many packages the environment resolves to — not with how much code you are editing.
One-shot runs
The language server stays resident; fatou format, fatou lint, and
fatou parse do not. For a CI job or a pre-commit hook what matters is the
high-water mark of a process that lives for a few milliseconds.
| Command | Over | Input | Peak RSS | Wall |
|---|---|---|---|---|
fatou format --check | src tree | 850 KiB | 39.2 MB | 20 ms |
fatou lint | src tree | 850 KiB | 26.6 MB | 30 ms |
fatou format --check | one file | 98 KiB | 10.3 MB | 10 ms |
fatou lint | one file | 98 KiB | 9.3 MB | 10 ms |
fatou parse | one file | 98 KiB | 7.7 MB | 10 ms |
fatou --version | process floor | - | 4.6 MB | 0 ms |
true | harness floor | - | 2.2 MB | 0 ms |
The src tree is 36 files, 850 KiB. Lowest of 5 runs each, measured with GNU time.
The whole-tree cases run the files in parallel, so their peak holds several syntax trees at once. That peak is a function of how many files are in flight together, not of how long the file list is: pointing Fatou at ten times the code does not cost ten times the memory.