Skip to content

Watcher: Speed up initial scan, parallelize uploads, eliminate StateDB lock contention - #69

Merged
wasimxyz merged 2 commits into
stagingfrom
cursor/watcher-perf-optimizations
May 14, 2026
Merged

Watcher: Speed up initial scan, parallelize uploads, eliminate StateDB lock contention#69
wasimxyz merged 2 commits into
stagingfrom
cursor/watcher-perf-optimizations

Conversation

@wasimxyz

Copy link
Copy Markdown
Member

Summary

Four hot-path performance optimizations on the lab-PC watcher loop, scoped to watcher/src/data_hub_watcher. Targets the symptoms operators actually feel: minutes-long startup scans, sequential uploads bottlenecking large runs, and a process-wide SQLite lock that defeated WAL mode.

Initial scan rewrite (monitor.py)

  • Replaced Path.rglob + is_file + double stat() with an os.scandir-based walk against cached DirEntry.stat(follow_symlinks=False).
  • Pre-compiled fnmatch patterns into a single anchored regex shared between the scan loop and the watchdog _EventHandler.
  • New _load_dedup_index builds an in-memory dict[str, list[(size, mtime)]] from one bulk SELECT per table, replacing two SQL round trips per scanned file with one Python dict lookup.

StateDB thread-local connections (state.py)

  • Per-thread sqlite3.Connection via threading.local() opened lazily through a _conn property (existing state_db._conn test access keeps working).
  • Process-wide _lock replaced by a smaller _write_lock only around mutations; reads run unlocked under WAL.
  • New iter_uploaded_stat_keys / iter_detected_stat_keys bulk loaders for the rewritten scan.
  • Per-connection PRAGMAs added: cache_size=-65536, temp_store=MEMORY, mmap_size=268435456, busy_timeout=5000.
  • close() now reaches every thread's connection.

SHA-256 chunking + overlap (util.py, uploader.py)

  • New HASH_CHUNK_SIZE = 1 << 20 (1 MiB) constant replaces the 8 KiB streaming buffer (~128x fewer read() syscalls on multi-GiB instrument files).
  • Uploader._upload_single runs file_sha256 and client.request_upload_url on a 2-thread ThreadPoolExecutor so the hash and the presign network round trip overlap.

Parallel uploads (models.py, runtime.py, uploader.py)

  • New InstrumentConfig.upload_parallelism: int = Field(default=4, ge=1, le=32). Defaulted, so existing config YAMLs need no migration.
  • Plumbed through runtime.build_runtime into the Uploader constructor.
  • Uploader.upload_files now uses a per-batch ThreadPoolExecutor sized by min(parallelism, len(files)), with a fast path when parallelism == 1.
  • Single requests.Session shared across S3 PUTs for TLS keep-alive across parallel uploads.
  • Counter mutations protected by a new _counters_lock.

Test plan

  • make check-all clean (ruff format/lint, pyright, frontend formatter/lint/typecheck)
  • All 360 watcher unit tests pass
  • New TestUploadFilesParallelism (4 tests) — serial fast path, true concurrency proven via threading.Barrier(3), partial-failure handling, empty-list behaviour, parallelism=0 rejection
  • New TestBulkLoadIterators, TestStatInDedupIndex, TestInitialScanBulkLoad — last asserts exactly one SELECT per table and zero per-row has_stat_match calls in the scan
  • New tests/test_state_db_concurrency.py — 40-thread × 25-write contention, concurrent reads during writes, per-thread connection isolation, close() shutdown coverage
  • New TestUploadParallelism covers default/range/rejection cases for the new field
  • Updated TestPutToPresignedUrl.test_successful_put to use an Uploader instance and patch the shared session

Out of scope (deferred)

The remaining items from the wider perf review (heartbeat thread split, _persist_detected_files delta-only writes, parallel _pending snapshot, RunState.files set membership, mimetype caching) are intentionally not in this PR. Easier to land + measure these four hot paths first.

Made with Cursor

…B lock contention

Four hot-path optimizations on the lab-PC watcher loop:

- Initial scan: replace `Path.rglob` + double-stat per file with an
  `os.scandir` walk against cached `DirEntry.stat()`, pre-compile
  fnmatch globs into a single anchored regex shared with the watchdog
  handler, and bulk-load the dedup index from one `SELECT` per table
  instead of two SQL round trips per scanned file.

- StateDB: switch to per-thread `sqlite3.Connection`s (lazy via
  `threading.local`) so reads run unlocked under WAL and writes
  serialize through a smaller `_write_lock` plus `busy_timeout`.
  Adds `iter_uploaded_stat_keys` / `iter_detected_stat_keys` bulk
  loaders and tunes per-connection PRAGMAs (cache_size, mmap_size,
  temp_store).

- SHA-256: bump the streaming chunk size from 8 KiB to 1 MiB and
  overlap `file_sha256` with `request_upload_url` on a 2-thread pool
  so the hash and presign network round trip run concurrently.

- Parallel uploads: new `InstrumentConfig.upload_parallelism` field
  (default 4, range 1-32) drives a per-batch `ThreadPoolExecutor` in
  `Uploader.upload_files`. A single `requests.Session` is shared
  across S3 PUTs for TLS keep-alive; counter mutations are guarded by
  a dedicated lock.

Co-authored-by: Cursor <cursoragent@cursor.com>
@vercel

vercel Bot commented May 13, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
data-hub Ready Ready Preview, Comment May 14, 2026 6:31pm

Request Review

@wasimxyz wasimxyz self-assigned this May 14, 2026
@wasimxyz
wasimxyz marked this pull request as ready for review May 14, 2026 18:17
- StateDB: tie each thread's sqlite handle to a weakref.finalize so
  short-lived ThreadPoolExecutor workers don't accumulate sqlite
  handles for the watcher's lifetime.
- Uploader: mount an HTTPAdapter sized to upload_parallelism so the
  shared session stops discarding TLS connections above the urllib3
  default of 10.
- Uploader: route poll_upload_queue's error bumps through
  _bump_errors so every _counters mutation is symmetric under
  _counters_lock.
- FileMonitor: log per-entry OSError from is_dir() instead of
  silently skipping unreadable subdirectories.

Co-authored-by: Cursor <cursoragent@cursor.com>
@wasimxyz
wasimxyz merged commit 207cbd2 into staging May 14, 2026
3 checks passed
@wasimxyz
wasimxyz deleted the cursor/watcher-perf-optimizations branch May 14, 2026 18:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant