How Chessalyzer works
The pipeline from PGN file to tracker results, and the design choices that make it fast.
This page is the optional tour under the hood. You don't need any of it to use the library — but if you're pushing millions of games through it, or you're just curious, here's what's actually happening.
The four stages
Every PGN file flows through the same four stages:
1. I/O → 2. PGN parse → 3. Replay → 4. Analyze- I/O — the file is streamed from disk, never loaded whole. In multithreaded mode it's cut into byte-sized chunks aligned to game boundaries; single-threaded it's read line by line.
- PGN parse — each game is split into tag-pair headers, mainline move strings (SAN), and the result. This stage is purely textual: no board, no legality, just structure.
- Replay — each SAN string is decoded and played on an internal board. This is the expensive part, and it's what move trackers need: they care where pieces stand, not just what was played.
- Analyze — your trackers run over the results and accumulate state.
A subtlety worth knowing: when people say "PGN parser" they usually mean stage 2. Turning a file into moves on a board takes stages 2 + 3, which is why the parser module and analyzePGN are separate tools.
Replay modes: pay only for what you need
Stage 3 comes in three flavors, and choosing the cheapest sufficient one is the single biggest speed lever:
| Mode | What happens | Who needs it |
|---|---|---|
'skip' | No board at all — moves are just counted | Count-only runs (no move trackers) or standalone PGN parsing |
'board' | Moves are played, positions tracked | Trackers that need the board, not per-move details |
'actions' | Each half-move is decoded into Action objects | Move trackers (defineMoveTracker) |
You normally never touch this: analyzePGN infers the mode from your trackers (a move tracker implies 'actions'; no move trackers means replay is skipped by default). The replay option exists for overrides — e.g. replay: 'board' to force decoding so broken moves surface as errors.
Headers work the same way: headers: 'auto' (the default) parses tag pairs when a game tracker or filter needs them and skips them otherwise.
Why it's fast
A few deliberate choices, in case you wonder why the internals look the way they do:
- Workers parse, not just analyze. In the default multithreaded mode, the main thread only cuts the file into chunks; parsing, replaying, and tracking all happen on worker threads, and chunks travel as transferable byte buffers to avoid copying.
- Strings on the hot path. Internally, moves stay plain strings (
'e4') as long as possible; the{ san }objects you see inParsedGameare only materialized at public boundaries. Millions of tiny object allocations would otherwise pressure the garbage collector during board replay. - No dependencies. The library is zero-dependency, so install and audit surfaces stay minimal.
- State merges once. Trackers accumulate locally per worker and merge into your instances a single time at the end — see Multithreading for the mechanics and the one pitfall this creates.
The repository's bench/ folder has the benchmarks behind these decisions if you ever want numbers.