4 Commits

Author SHA1 Message Date
codinget cd4ab3d582 Add README
Covers project layout, setup, import tooling, and a summary of the bulk
import performance work, with an upfront disclaimer that this was built
with Claude Code (Sonnet 5).
2026-07-01 23:19:17 +00:00
codinget 26c5b4eb9a Add COPY-based bulk importer to bypass incremental B-tree update cost
The incremental importer was measured at 24.72 min wall / 186s user time for
rockyou (14.3M rows), and Postgres logs showed checkpoints firing every ~22s
due to WAL pressure from random-order B-tree maintenance and full-page-write
amplification. bulkImport.ts (`npm run import:bulk`) avoids all of that:

- Loads raw rows via COPY into a TEMP staging table (no WAL, no index).
- Merges with the existing table's contents into a fresh, unindexed shadow
  table via one INSERT ... SELECT ... GROUP BY.
- Adds the primary key and prefix index only after the table is populated,
  so Postgres builds them via a single sorted bulk pass instead of 14M
  random-order incremental inserts.
- Swaps the shadow table into place at the end.
- The entire run is one transaction: since pwned_passwords_new doesn't exist
  outside that transaction, Postgres skips WAL-logging its data entirely,
  and any failure rolls back to the exact starting state instead of leaving
  a half-migrated table.
- Wordlist hashing runs across a worker_threads pool (--jobs, shared
  resolveWorkerUrl helper with the incremental importer's hashWorker) instead
  of blocking the single thread that also drives the COPY stream.
- Each phase (hash+copy, merge, reindex, swap) reports its own wall time.

Net effect measured against the same rockyou file/DB: 42 min -> 24.72 min
(fsync fix) -> ~7.3 min (WAL-skip transaction) -> 186.17s (parallel hashing).
2026-07-01 23:11:38 +00:00
codinget 23a59c216d Reduce commit overhead in bulk import: async commit + bigger batches
rockyou import was ~195s of actual CPU work spread across 42 minutes wall
clock: Postgres was fsync-bound, not CPU/parallelism-bound (each batch was
its own implicit-commit transaction). Wrapping each batch in an explicit
transaction with synchronous_commit=off, and raising the default batch size
20000 (also used for wordlist worker chunking, previously hardcoded to 2000),
brought the same import down to ~25 minutes wall clock / ~186s user time.

Next bottleneck to address: B-tree index maintenance cost as it outgrows
shared_buffers (confirmed via pg_stat_user_tables: 14.3M live tuples, zero
dead tuples on a fresh DB, so it's not autovacuum/bloat). Random-order
inserts into the (prefix, suffix) index cause disk-bound page faults once the
index no longer fits in cache, which matched the observed end-of-run
slowdown and the "N-at-a-time" stalls lining up with --jobs concurrency.
2026-07-01 22:18:07 +00:00
codinget fee254e7e6 Scaffold Pwned Passwords clone: React/wouter frontend, Koa/pg backend, import CLI
- Backend: Koa + @koa/router serving the HIBP-style /range/:prefix k-anonymity
  API, backed by Postgres via pg, with migrate/seed scripts run through tsx.
- Frontend: React + wouter, bundled with webpack (dev server + prod build),
  thin client that hashes passwords locally and only sends the hash prefix.
- Import package: CLI to bulk-load a wordlist or hash list into the database,
  plus a separate script to pull the full dataset from the HIBP range API.
  Both support parallelism (worker threads for wordlist hashing, concurrency
  limiter for DB/HTTP fan-out).
- Dev tooling: docker compose for a local Postgres, .env.local/.env.example,
  DATABASE_URL support as an alternative to discrete PG* vars.
2026-07-01 21:35:19 +00:00