feat(core): adaptive upload parallelism with throughput probe and disk-saturation gate - #143
Merged
Merged
Conversation
pmaxhogan
force-pushed
the
feat/adaptive-parallelism
branch
from
July 20, 2026 20:20
12517ae to
d0df17f
Compare
Contributor
Coverage
Gate: passed - no coverage regression (epsilon 0.1 pp). |
pmaxhogan
force-pushed
the
feat/adaptive-parallelism
branch
from
July 20, 2026 21:08
d0df17f to
198ba1d
Compare
pmaxhogan
enabled auto-merge (squash)
July 20, 2026 21:10
…k-saturation gate Close the control loop around the in-flight-file count (DESIGN 11.4.7 / 18.2). A 30s-window ThroughputProbe fed by the executor drives a resizable UploadPool within [1, 32]: shrink when a window's throughput collapses below 50% of the prior one (and neither the pacer nor the disk explains the drop), grow while lifting the pool still improves throughput. A per-OS disk-busy gate (new driven-diskstat crate: PDH on Windows, /proc/diskstats on Linux, IOKit on macOS; fail-open to not-saturated so a broken reader never strangles uploads) blocks growth when the disk is the bound. Default-on with an adaptive_parallelism_enabled kill-switch; off = today's fixed pool at default_concurrent_uploads (now wired as the start size). The control law is a pure, exhaustively unit-tested decide(); an e2e_fake test asserts a real shrink-then-regrow transition through the shared executor/probe/pool wiring. Refs #34 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QZQVP2tUuTLh8oL31D8heC
pmaxhogan
force-pushed
the
feat/adaptive-parallelism
branch
from
July 20, 2026 21:15
198ba1d to
9971dc3
Compare
This was referenced Jul 20, 2026
pmaxhogan
added a commit
that referenced
this pull request
Jul 20, 2026
The FS mtime-granularity write-stat probe ran inline on a source's FIRST
scan. That first scan is the source's initial backup cycle, where every file
is new and each path's first upload must commit its file_state row cleanly.
The probe's stat/sleep I/O (up to ~2.2s on a coarse FS) plus temp-file churn
in the source root added latency/contention to that hot path; on the
windows-latest hermetic chaos runner it tipped a pre-existing
create-during-upload race - the executor's SkipPostUpload on a CREATE leaves
a live orphan object the once-per-boot reconcile never adopts mid-run - into
a duplicate upload, failing the `frequent-edits` scenario ("expected exactly
1 object after edit soak, found 2"). The underlying orphan race is pre-existing
and tracked separately (#144).
Defer the probe: the first scan (no completed scan yet -> last_full_scan_at
is None) trusts mtime with no probe and no coarse fallback, persisting
nothing; the probe runs on the next scan, once a completed scan exists. This
mirrors the existing probe-failure degradation ("trust mtime this cycle,
re-probe next") and costs at most a one-cycle delay before the coarse
fallback first engages on a coarse FS (DESIGN s5.2). Adds a scanner-level
regression test that scan #1 performs no probe and no coarse-fallback re-hash
(and that the probe runs from scan #2).
Also rebases onto origin/main (#142 shared-drive, #143 adaptive parallelism)
and renumbers the migration 0009_mtime_granularity.sql ->
0012_mtime_granularity.sql (contiguous above the applied 0011; sqlx
auto-discovers by filename). Regenerates the .sqlx query cache for the merged
backup_sources INSERTs that now carry both drive_id and mtime_granularity_ns.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QZQVP2tUuTLh8oL31D8heC
pmaxhogan
added a commit
that referenced
this pull request
Jul 24, 2026
🤖 I have created a release *beep* *boop* --- ## [2.1.0](v2.0.1...v2.1.0) (2026-07-24) ### Features * **core:** adaptive upload parallelism with throughput probe and disk-saturation gate ([#143](#143)) ([8ecced6](8ecced6)) * **core:** filesystem timestamp-granularity probe with ctime fallback and per-directory gitignore cascade ([#141](#141)) ([344262c](344262c)) * **drive:** support Google Shared Drive destinations end-to-end ([#142](#142)) ([d9c3161](d9c3161)) * **net:** native OS reachability backends with automatic fallback ([#138](#138)) ([319e85f](319e85f)) * **net:** SOCKS5 and PAC proxy support for all outbound connections ([#145](#145)) ([2f0b7d1](2f0b7d1)) * **net:** support a custom corporate root CA for all outbound connections ([#134](#134)) ([929e93d](929e93d)) * per-source toggle to back up OneDrive cloud-only placeholder files ([#133](#133)) ([6863ea3](6863ea3)) * **telemetry:** capture latency percentiles and add rollup query endpoint ([#132](#132)) ([4e9fde6](4e9fde6)) * **telemetry:** preview exactly what a telemetry ping sends ([#139](#139)) ([95fbd9a](95fbd9a)) ### Bug Fixes * **core:** commit file_state for a create that skipped post-upload so the next scan updates instead of re-creating ([#146](#146)) ([f5230d1](f5230d1)) * **deps:** bump tauri-winrt-notification to drop vulnerable quick-xml (closes [#89](#89)) ([#129](#129)) ([232fd8f](232fd8f)) * **telemetry:** exclude pre-schema rows from latency rollup ([#137](#137)) ([1ae6220](1ae6220)) * **ui:** add cursor pointer to buttons and link-buttons ([#136](#136)) ([dbd4809](dbd4809)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
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.
Adaptive upload parallelism (DESIGN §11.4.7 / §18.2), default-on with a kill-switch.
Closes the control loop around the one Drive concurrency knob that matters - how many files are in flight at once. A fixed pool is a guess: too few wastes the link, too many overloads Drive's edge so each upload takes longer and net throughput falls (§11.4.7's pathological case). The pool now floats within
[1, 32], driven by measured throughput and a per-OS disk-busy gate.DESIGN conformance
> 80 %busy = saturated. Gates growth (and blocks a shrink from firing on a disk-bound drop).default_concurrent_uploads(finally wired), elsemin(available_parallelism*2, 16).adaptive_parallelism_enabled, default true. Off = today's exact fixed-pool behavior (no controller is built; the pool stays pinned at the start size).Controller rules (pure
decide, exhaustively unit-tested)Additive-increase / multiplicative-decrease: growth continues only while it pays off and settles at a plateau. A pacer throttle or disk saturation explains a throughput drop, so neither triggers a shrink.
Resolved ambiguities
Per-OS disk-busy backends (
driven-diskstat, mirrorsdriven-power's cfg-gated shape)\PhysicalDisk(_Total)\% Disk Time, added viaPdhAddEnglishCounterW(locale-independent),PDH_FMT_NOCAP100so a genuinely-over-100%_Totalreads honestly,/100→ fraction. Compiles and passesclippy -D warningson a real Windows host; the CItauri-compileWindows matrix compiles it as well./proc/diskstatsfield 10 ("time spent doing I/Os", ms) delta for the device backing the source root (resolved viastat(2)st_dev→ major:minor), over the wall-clock interval.IOBlockStorageDriverStatisticsTotal Time (Read/Write)ns deltas summed across drivers (best-effort; can over-report, which is safe - it only holds the pool at its start size, never strangles it).Unknown, which maps to not saturated. A broken disk reader must never pin the pool small.Known limitation: the disk-busy reader binds to the first source's root, so a multi-source account spanning different physical disks monitors only the first. Fail-open keeps this harmless (the worst case degrades to the fixed-pool behavior); a per-source disk gate is a future refinement.
Resize mechanism
The pool is a resizable
tokio::sync::Semaphore. Grow =add_permits(1). Shrink = acquire one permit andforget()it, done inline under a 100 ms timeout (never a detached task - the repo forbids orphanable spawns); on timeout the size is left honest and the next window retries. The executor and controller share the sameArc<UploadPool>, so a resize is immediately seen by the executor's per-file acquire path.Tests
Full workspace suite green on a Windows host:
clippy --workspace --all-targets -D warningsclean,cargo test --workspaceall-pass (both adaptive e2e tests included),cargo fmt --checkclean; UIlint/vue-tsc/ 266 vitest specs green. The Linux/proc/diskstatsparser and the macOS IOKit backend compile per-target under CI's Linux test job + the 3-OStauri-compilematrix.decide: every branch (hold/shrink/grow, each gate, floor/cap, bootstrap) covered.UploadPoolgrow-to-cap / shrink-to-floor / contention counting.AdaptiveControllerend-to-end on aFakeClock+FakeDiskBusyProbe: shrinks under induced latency then recovers (the §11.4.7 acceptance behavior, deterministically, no real time); holds below a full window; does not shrink on a throttle-explained drop.e2e_fakewiring seam: a real executor running a real multi-file plan feeds the injectedThroughputProbethe full uploaded byte total and passes uploads through the injected pool (the ROADMAP M3 adaptive-parallelism acceptance row).Refs #34
🤖 Generated with Claude Code