Skip to content

Parallelize PGN parsing during import (parallel producer → single writer) #243

Description

@jozef2svrcek

Motivation

Import is single-core-bound: the bottleneck is our Rust-side PGN parsing (read text, replay moves, compute zobrist hashes), which runs in one loop. Observed directly on Linux during a first-run import — one core pinned at ~98%, the other 21 logical cores mostly idle (Core Ultra 7 165H). DuckDB's own thread pool can't help here because the work isn't inside a DuckDB query.

This is separate from — and stacks with — the WAL checkpoint_threshold fix (v0.14.11), which stops the writer from stalling on fsync. That fix keeps the pipeline flowing; this one lets the producer side use many cores instead of one, speeding up both Linux and Windows beyond the current single-core ceiling.

Key point: single writer ≠ single core

A single DuckDB write connection still gives two kinds of parallelism:

  1. Intra-query (already have it): DuckDB runs each statement multi-threaded (morsel-driven), governed by SET threads. The set-based passes — dedup UPDATEs, CREATE INDEX (position index build), dedup_games joins — already fan out across cores. Note we currently hardcode threads=4 in chess-db/src/db/mod.rs; scaling that toward the core count is a cheap, separate win for those steps.
  2. Application-side (missing): the PGN parse, which sits upstream of the writer, is single-threaded today.

Proposed design

Parallelize the parse, keep the write serialized:

parse workers (N cores) → bounded channel → one append thread → single writer

Each game is independent to parse, so fan the parsing across a worker pool (rayon or a fixed worker set), then funnel finished rows into the single DuckDB Appender from one thread. The DB write stays fully serialized (respects the single-writer model and DuckDB's one-writer rule); only the CPU-heavy parsing goes wide.

  • Applies to both flush_games and position generation (flush_positions) — both are per-game CPU.
  • Keep it behind the existing fast/bulk path the wizard uses.

Caveat to get right

The importer assigns sequential game IDs, so parallel parsing needs either order-preserving chunking or ID assignment on the serial (append) side. Very doable — just the part to handle carefully.

What the single writer genuinely rules out (fine)

Two independent write transactions at once (e.g. importing two files into the same DB concurrently). Not a real loss: DuckDB only allows one writer anyway, and the append itself isn't the bottleneck — the parse is, and it's before the writer.

Files

  • chess-db/src/importer/mod.rs — parse loop, flush_games, flush_positions (BATCH_SIZE)
  • chess-db/src/db/mod.rsthreads=4 cap

Related

  • v0.14.11 import checkpoint_threshold fix (fsync stalls) — orthogonal, already done.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions