Watcher: Speed up initial scan, parallelize uploads, eliminate StateDB lock contention - #69
Merged
Merged
Conversation
…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>
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)Path.rglob+is_file+ doublestat()with anos.scandir-based walk against cachedDirEntry.stat(follow_symlinks=False)._EventHandler._load_dedup_indexbuilds an in-memorydict[str, list[(size, mtime)]]from one bulkSELECTper table, replacing two SQL round trips per scanned file with one Python dict lookup.StateDB thread-local connections (
state.py)sqlite3.Connectionviathreading.local()opened lazily through a_connproperty (existingstate_db._conntest access keeps working)._lockreplaced by a smaller_write_lockonly around mutations; reads run unlocked under WAL.iter_uploaded_stat_keys/iter_detected_stat_keysbulk loaders for the rewritten scan.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)HASH_CHUNK_SIZE = 1 << 20(1 MiB) constant replaces the 8 KiB streaming buffer (~128x fewerread()syscalls on multi-GiB instrument files).Uploader._upload_singlerunsfile_sha256andclient.request_upload_urlon a 2-threadThreadPoolExecutorso the hash and the presign network round trip overlap.Parallel uploads (
models.py,runtime.py,uploader.py)InstrumentConfig.upload_parallelism: int = Field(default=4, ge=1, le=32). Defaulted, so existing config YAMLs need no migration.runtime.build_runtimeinto theUploaderconstructor.Uploader.upload_filesnow uses a per-batchThreadPoolExecutorsized bymin(parallelism, len(files)), with a fast path whenparallelism == 1.requests.Sessionshared across S3 PUTs for TLS keep-alive across parallel uploads._counters_lock.Test plan
make check-allclean (ruff format/lint, pyright, frontend formatter/lint/typecheck)TestUploadFilesParallelism(4 tests) — serial fast path, true concurrency proven viathreading.Barrier(3), partial-failure handling, empty-list behaviour,parallelism=0rejectionTestBulkLoadIterators,TestStatInDedupIndex,TestInitialScanBulkLoad— last asserts exactly oneSELECTper table and zero per-rowhas_stat_matchcalls in the scantests/test_state_db_concurrency.py— 40-thread × 25-write contention, concurrent reads during writes, per-thread connection isolation,close()shutdown coverageTestUploadParallelismcovers default/range/rejection cases for the new fieldTestPutToPresignedUrl.test_successful_putto use anUploaderinstance and patch the shared sessionOut of scope (deferred)
The remaining items from the wider perf review (heartbeat thread split,
_persist_detected_filesdelta-only writes, parallel_pendingsnapshot,RunState.filesset membership, mimetype caching) are intentionally not in this PR. Easier to land + measure these four hot paths first.Made with Cursor