You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On windows-latest GitHub Actions runners the SQLite-backed proptests in crates/db/tests/ run roughly 8–12x slower than on ubuntu-latest and macos-latest, for identical test code and identical case counts. That gap is large enough to suspect a specific, fixable cause rather than generic "Windows is slower", and it is already costing CI reliability: it is what pushed fts_consistent_under_tag_churn past nextest's timeout budget in #186.
This issue tracks the profiling work. The immediate symptom in #186 has been mitigated by halving the Windows case count — that is a coverage trade, not an explanation.
Evidence
CI run 30666964446, one commit across the whole just ci matrix:
The 40.75s figure was already SLOW-flagged by nextest. The very next run, 30668291291, terminated the same test at 80.3s under .config/nextest.toml's slow-timeout = { period = "40s", terminate-after = 2 }. The only source changes between the two runs were to scripts/fetch-ffmpeg-sidecar.sh, so the ~2x swing is ordinary runner variance landing on top of an already-marginal baseline.
Slowest tests on windows-latest in that run — the SQLite proptests take three of the top four slots, and nothing else is close except one image-resize benchmark:
Nearly identical, even though the second proptest does substantially more SQL per operation: it runs compute_ground_truth (up to 8 per-hash SELECTs plus a full search_content scan) after every op, against the first one's single attach/detach plus a fixed set of FTS MATCH queries.
On ubuntu the ratio is not flat — 5.283/64 = 0.083 s/case versus 1.848/32 = 0.058 s/case — which is roughly what the op counts predict.
Reading: on Windows the cost is dominated by per-case fixture construction, not by the SQL executed inside the case. Each proptest case builds a fresh fixture via test_db() in crates/db/tests/common/mod.rs, which does:
let td = tempfile::tempdir()?;// new directorylet db_path = td.path().join("test.db");let writer = SqliteWriter::start(&db_path, bus)?;// migrations + spawn OS threadlet reads = ReadPool::open(&db_path)?;// r2d2 pool of read connections
plus a raw seed_conn opened per case. At 96 cases across the two tests that is ~96 tempdirs, ~96 writer threads, ~96 migration sweeps, and several hundred SQLite file handles (.db, -wal, -shm) created and torn down per run. That is the surface to profile.
Candidate hypotheses
Tempdir volume placement. GitHub's hosted Windows images expose more than one volume, and where TEMP points decides which disk every per-case SQLite file lands on. Worth confirming what std::env::temp_dir() actually resolves to on the runner and whether pinning the test temp root to the faster volume moves the number.
Microsoft Defender real-time scanning. Defender is on by default on the hosted Windows images and scans newly created files. This fixture creates thousands of fresh files per run in fresh directories — close to a worst case for on-access scanning. A scoped, CI-only exclusion for the test temp path is the standard mitigation, and measuring with it applied is a cheap way to confirm or kill the hypothesis.
busy_timeout / file-locking interaction. The fixture keeps a writer-actor connection, a pooled reader, and a raw seed connection open against the same WAL-mode file. Windows uses a different SQLite VFS with different locking primitives than the unix VFS, so contention that is free on Linux could serialize here. proptest harness hangs after writer-actor migration: two racing SQLite write-connections #124 tested and discarded the lock-race theory — but only on Linux. It has never been re-tested on Windows.
Thread and handle churn. Windows thread creation is more expensive than Linux's, and handle teardown on a scanned filesystem would compound with (2).
These are not mutually exclusive; (1) and (2) in particular would multiply.
What would close this
Either a measured explanation with numbers attached, or a change that brings windows-latest per-case fixture cost within ~3x of ubuntu.
"Hosted Windows runners are simply this slow at creating and scanning files" is a legitimate outcome — but write it down with the measurements so nobody re-derives it, and then treat the fixture shape as the lever instead. #124's post-mortem already lists candidates for that: a shared in-memory database (file::memory:?cache=shared) across the writer, pool, and seed connection, or a pure-Rust oracle for the trigger semantics with SQLite reserved for shape-level regression tests.
Explicitly out of scope
Raising slow-timeout in .config/nextest.toml. That budget exists to catch the SQLite deadlock class tracked in #122 — the config comment records the reasoning — and widening it to accommodate slow-but-healthy tests would blind CI to genuine hangs. If a Windows-specific timeout ever becomes unavoidable, nextest supports [[profile.default.overrides]] with platform = "cfg(windows)", but that should follow a measurement, not replace one.
Summary
On
windows-latestGitHub Actions runners the SQLite-backed proptests incrates/db/tests/run roughly 8–12x slower than onubuntu-latestandmacos-latest, for identical test code and identical case counts. That gap is large enough to suspect a specific, fixable cause rather than generic "Windows is slower", and it is already costing CI reliability: it is what pushedfts_consistent_under_tag_churnpast nextest's timeout budget in #186.This issue tracks the profiling work. The immediate symptom in #186 has been mitigated by halving the Windows case count — that is a coverage trade, not an explanation.
Evidence
CI run
30666964446, one commit across the wholejust cimatrix:search_proptests::fts_consistent_under_tag_churnsearch_proptests::fts_matches_ground_truth_under_soft_delete_churnThe 40.75s figure was already SLOW-flagged by nextest. The very next run,
30668291291, terminated the same test at 80.3s under.config/nextest.toml'sslow-timeout = { period = "40s", terminate-after = 2 }. The only source changes between the two runs were toscripts/fetch-ffmpeg-sidecar.sh, so the ~2x swing is ordinary runner variance landing on top of an already-marginal baseline.Slowest tests on windows-latest in that run — the SQLite proptests take three of the top four slots, and nothing else is close except one image-resize benchmark:
The strongest clue: cost tracks cases, not SQL
Per-case cost on windows-latest:
fts_consistent_under_tag_churn— 40.748s / 64 cases = 0.637 s/casefts_matches_ground_truth_under_soft_delete_churn— 22.210s / 32 cases = 0.694 s/caseNearly identical, even though the second proptest does substantially more SQL per operation: it runs
compute_ground_truth(up to 8 per-hash SELECTs plus a fullsearch_contentscan) after every op, against the first one's single attach/detach plus a fixed set of FTSMATCHqueries.On ubuntu the ratio is not flat — 5.283/64 = 0.083 s/case versus 1.848/32 = 0.058 s/case — which is roughly what the op counts predict.
Reading: on Windows the cost is dominated by per-case fixture construction, not by the SQL executed inside the case. Each proptest case builds a fresh fixture via
test_db()incrates/db/tests/common/mod.rs, which does:plus a raw
seed_connopened per case. At 96 cases across the two tests that is ~96 tempdirs, ~96 writer threads, ~96 migration sweeps, and several hundred SQLite file handles (.db,-wal,-shm) created and torn down per run. That is the surface to profile.Candidate hypotheses
TEMPpoints decides which disk every per-case SQLite file lands on. Worth confirming whatstd::env::temp_dir()actually resolves to on the runner and whether pinning the test temp root to the faster volume moves the number.busy_timeout/ file-locking interaction. The fixture keeps a writer-actor connection, a pooled reader, and a raw seed connection open against the same WAL-mode file. Windows uses a different SQLite VFS with different locking primitives than the unix VFS, so contention that is free on Linux could serialize here. proptest harness hangs after writer-actor migration: two racing SQLite write-connections #124 tested and discarded the lock-race theory — but only on Linux. It has never been re-tested on Windows.These are not mutually exclusive; (1) and (2) in particular would multiply.
What would close this
Either a measured explanation with numbers attached, or a change that brings windows-latest per-case fixture cost within ~3x of ubuntu.
"Hosted Windows runners are simply this slow at creating and scanning files" is a legitimate outcome — but write it down with the measurements so nobody re-derives it, and then treat the fixture shape as the lever instead. #124's post-mortem already lists candidates for that: a shared in-memory database (
file::memory:?cache=shared) across the writer, pool, and seed connection, or a pure-Rust oracle for the trigger semantics with SQLite reserved for shape-level regression tests.Explicitly out of scope
Raising
slow-timeoutin.config/nextest.toml. That budget exists to catch the SQLite deadlock class tracked in #122 — the config comment records the reasoning — and widening it to accommodate slow-but-healthy tests would blind CI to genuine hangs. If a Windows-specific timeout ever becomes unavoidable, nextest supports[[profile.default.overrides]]withplatform = "cfg(windows)", but that should follow a measurement, not replace one.Related
fts_consistent_under_tag_churnfrom 64 to 32; the coverage trade is documented inline incrates/db/tests/search_proptests.rs.C:\.perima\manifest.dbvolume-root manifest. Different root cause, same "only on Windows" shape.