Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 -> String formatter (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), and abstractdataframe.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 uses fatou::formatter::check_paths (glob/directory discovery plus rayon-parallel formatting, read-only); JuliaFormatter uses format(dir; overwrite = false) (recursive, thread-parallel, read-only). Runic is excluded from these scenarios by design: it has no in-process directory API (its format_file is 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.jl v1.8.2 (946c72a)
  • Versions: Fatou 0.20.0, Runic 1.9.0, JuliaFormatter 2.12.6, Julia 1.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

Formatting time relative to Fatou on a log scale (lower is faster). One dot per file, grouped at each tool and colored by file; Fatou sits on the dashed baseline at 1 and slower tools appear above it. Each file goes through the tool's pure 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)

ToolFilesBytesMedian (ms)Throughput (MB/s)Relative
Fatou141,9374.110.26baseline
Runic141,93715.82.653.88x
JuliaFormatter141,9377.85.351.92x

kinds.jl (JuliaSyntax/src/kinds.jl)

ToolFilesBytesMedian (ms)Throughput (MB/s)Relative
Fatou124,4421.515.79baseline
Runic124,44212.61.948.13x
JuliaFormatter124,4424.15.972.64x

abstractdataframe.jl (DataFrames/src/abstractdataframe/abstractdataframe.jl)

ToolFilesBytesMedian (ms)Throughput (MB/s)Relative
Fatou1100,3885.618.04baseline
Runic1100,38831.53.195.66x
JuliaFormatter1100,38813.77.352.46x

Projects

Formatting time relative to Fatou on a log scale (lower is faster). One dot per project, grouped at each tool and colored by project; Fatou sits on the dashed baseline at 1 and slower tools appear above it. Each tool walks the whole source tree through its own directory entry point, so file discovery, IO, and internal parallelism all count; Runic is absent because it has no in-process directory API. Hover a dot for the exact figures.
Data table

JuliaSyntax (JuliaSyntax/src)

ToolFilesBytesMedian (ms)Throughput (MB/s)Relative
Fatou15332,78310.631.37baseline
JuliaFormatter15332,78327.312.202.57x

DataFrames (DataFrames/src)

ToolFilesBytesMedian (ms)Throughput (MB/s)Relative
Fatou36870,5819.987.52baseline
JuliaFormatter36870,58134.025.593.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.

Median cold-start time relative to Fatou on a logarithmic scale (lower is faster). Fatou is the dashed baseline at 1; each Julia tool sits above at its slowdown factor. Each run is a brand-new process that starts up, formats once, and exits. Fatou runs through 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
Cold start: parse_stream.jl, one fresh process per run
ToolMedian timeThroughput (MB/s)vs Fatou
Fatou5.9 ms7.10baseline
Runic189.1 ms0.2232.01x
JuliaFormatter287.1 ms0.1548.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 initialize request 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 didOpen notifications 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 _names at a pinned call site in selection.jl. Its definitions live elsewhere, and its references span the project. References include declarations; rename constructs the WorkspaceEdit but 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: _names in src/abstractdataframe/selection.jl at line 383
  • Versions: Fatou 0.18.0, LanguageServer.jl 5.0.0, JETLS 7e01ca58 (2026-08-08), Julia 1.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 time in seconds on a log scale (left is faster). Each dot is one measurement of a phase for one server; the phases have different starting points and are not additive. Hover a dot for the exact figure.
Readiness data
ServerInitializeWorkspace readyOpen files ready
Fatou1 ms152 ms54 ms
LanguageServer.jl4.14 s27.70 s3.30 s
JETLS5.88 s9.88 s13.54 s

Warm requests

Warm request latency in milliseconds on a log scale (left is faster). Filled dots show medians; hollow dots show p95, joined to the median for the same server and request. These spans are not confidence intervals. Hover for timings and returned work, or expand the table below. Servers can return different amounts of work for the same request.
Request timings and returned work
ServerRequestMedianp95Returned work
FatouDocument symbols1.29 ms1.68 ms54–123 symbols (median 70), 24 KiB
FatouHover0.06 ms0.13 ms1 result, 162 B
FatouGo to definition0.28 ms0.40 ms5 locations in 4 files, 879 B
FatouFind references0.54 ms0.68 ms135 locations in 17 files, 23 KiB
FatouRename0.61 ms0.69 ms135 edits in 17 files, 17 KiB
LanguageServer.jlDocument symbols5.42 ms7.96 ms372–626 symbols (median 519), 117 KiB
LanguageServer.jlHover0.09 ms0.35 ms1 result, 709 B
LanguageServer.jlGo to definition1.30 ms1.38 ms5 locations in 4 files, 884 B
LanguageServer.jlFind references54.30 ms84.15 ms135 locations in 17 files, 23 KiB
LanguageServer.jlRename54.57 ms59.51 ms135 edits in 17 files, 18 KiB
JETLSDocument symbols2.08 ms5.84 ms348–578 symbols (median 477), 131 KiB
JETLSHover1.28 ms4.23 ms1 result, 1 KiB
JETLSGo to definition0.62 ms0.87 ms2 locations in 1 file, 761 B
JETLSFind references1.07 ms4.38 ms135 locations in 17 files, 23 KiB
JETLSRename22.36 ms41.46 ms135 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

Resident memory across the whole process tree, in MB on a linear scale (left uses less memory). Each dot is one server at one milestone. Hover a dot for the exact figure.
Memory data
ServerBaselineSettledPeakvs FatouSettled afterDoing
Fatou79 MB84 MB93 MBbaseline9 sstatic analysis, no Julia runtime
LanguageServer.jl965 MB1104 MB1567 MB13.1x41 sJulia runtime plus a SymbolServer pass over the environment
JETLS792 MB1998 MB2111 MB23.7x33 sJulia 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.

CommandOverInputPeak RSSWall
fatou format --checksrc tree850 KiB39.2 MB20 ms
fatou lintsrc tree850 KiB26.6 MB30 ms
fatou format --checkone file98 KiB10.3 MB10 ms
fatou lintone file98 KiB9.3 MB10 ms
fatou parseone file98 KiB7.7 MB10 ms
fatou --versionprocess floor-4.6 MB0 ms
trueharness floor-2.2 MB0 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.