Commit 85f6d6a
feat: real-world benchmark suite comparing driven with rclone (#178)
Adds a benchmark suite that measures Driven's **real backup engine**
against
`rclone` on a live Google Drive account, across the two workloads that
dominate
real backup sets and both a cold upload and an incremental re-run.
The suite exists to answer one question honestly: is Driven constrained
by
hardware (CPU, disk, network, the Drive API) or by its own algorithms? A
competitor doing roughly the same work is the cheapest way to tell those
apart,
and it catches regressions a synthetic microbenchmark never would.
## One deliberate deviation from the brief, up front
The brief said to drive `driven-cli sync`. **I did not**, because
reading it
showed it is a debug driver, not the engine: it walks only the *top
level* of the
source folder (`crates/driven-cli/src/main.rs:394` - "V1 debug driver:
top-level
files only (no recursion)"), reads whole files into memory, keeps no
state
database, and uploads sequentially. Pointed at the `tiny-deep` fixture
it would
have uploaded **zero files**, and on multi-gigabyte files it would
exhaust memory.
Benchmarking it would have measured a debugging tool and published the
number as
Driven's.
Instead the harness assembles the same stack `src-tauri/src/assembly.rs`
does -
`SqliteStateRepo` -> `DefaultExecutor` (adaptive upload pool, AIMD
pacer) ->
`SyncOrchestrator` - and calls `run_cycle` against a live
`GoogleDriveStore`, so
the real scan -> plan -> execute -> verify pipeline is what gets timed.
This is
new wiring: nothing in the repo previously ran the headless core against
real
Drive.
## What's here
- **`crates/driven-bench`** - a single `driven-bench` binary with three
subcommands: `run` (the matrix), `fixture` (build/clean trees without
uploading), and a hidden `agent-sync` that runs **one** engine cycle and
prints
a JSON metrics line.
- **`bench/README.md`** - prerequisites, scales, costs, safety rails,
and an
explicit "what is and is not apples-to-apples" section.
- **`bench/run.ps1`** and `just bench` / `just bench-fixture` /
`just bench-fixture-clean`.
- **`.github/workflows/bench.yml`** - `workflow_dispatch` (scale, tools)
plus
`v*` tag pushes at the `smoke` scale. Never on `pull_request`, never on
a plain
push. Time-boxed at 180 minutes; clean skip when secrets are absent.
### Design choices worth reviewing
**Both tools run as child processes.** Driven's engine could have run
in-process,
but then its CPU-time and peak-memory columns would silently include
fixture
generation and the harness's own Drive calls, and would not be
comparable to
rclone's. The harness re-invokes itself with `agent-sync`, so both tools
are
measured by identical OS accounting (`GetProcessTimes` /
`GetProcessMemoryInfo`
on Windows, `getrusage(RUSAGE_CHILDREN)` on Unix).
**API-call counts come from a `RemoteStore` decorator**, the same seam
the
executor already uses for `BreakerReportingStore` - no core change.
rclone
exposes no request counter, so that cell renders as `-`, meaning "not
measurable", never `0`.
**rclone auth needs no token-minting request.** Its config carries a
*non-empty*
placeholder access token that is already expired, and rclone refreshes
it from
the same refresh token Driven uses. (An *empty* `access_token` makes
rclone treat
the whole token as unparseable and report "there's no refresh token" -
verified
empirically before building anything on it.)
**The report separates scan time from upload time.** A total cannot
answer the
question the suite exists for - on the million-tiny-files shape, is a
slow cold
pass bound by the local walk and hashing, or by Drive round-trips? The
agent
consumes the orchestrator's event stream while the cycle runs (a
timestamp
cannot be recovered from a buffered event, and this also stops losing
events to
broadcast lag) and reports the boundary. rclone interleaves listing with
transferring, so its cell stays blank rather than invented.
**restic was considered and deliberately excluded.** It stores a
chunked,
deduplicating repository rather than a mirror, so its "upload" is a
different
operation, and on a re-run its deduplication would flatter it on exactly
the
workload this suite measures. Adding it would produce a bigger table,
not a more
honest one. The rationale is in the README, not just here.
**Fixtures are seeded and incompressible** (SplitMix64), so both tools
see
byte-identical input and no tool scores on test-data entropy it would
never see
on photos and archives. The mutation step changes content only - no
creates, no
deletes - which keeps `rclone copy` a fair match rather than requiring
the
riskier `rclone sync`.
### Safety rails
- The destination folder id must be explicit (`--dest` or
`DRIVEN_E2E_DEST_FOLDER_ID`); no default, no discovery. Checked before a
byte
is generated.
- All writes go under one `driven-bench-<uuid>` folder, with a subfolder
per
scenario. Cleanup trashes **that folder by the id it was created with**
- the
suite never lists the destination and never matches by name, so it
cannot touch
anything it did not create. A cleanup failure prints the id to trash by
hand.
- Uploads are capped at 2 GiB by default; exceeding it is an error
telling you to
pass `--full` or lower `--scale`.
- Credentials are read from the environment only (never the keychain)
and never
printed.
## Smoke run (real Drive, dedicated automation account)
Release build, Windows, 20 logical CPUs, rclone v1.74.4. Both tools,
both shapes,
both phases; run folder trashed afterwards.
**huge** - 2 files, 16.0 MiB
| Tool | Phase | Wall s | MiB/s | Files | API calls | CPU s | Peak RSS |
Conc |
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| driven | cold | 3.4 | 4.72 | 2 | 6 | 0.6 | 49.8 MiB | 16 |
| driven | incremental | 2.4 | 3.37 | 1 | 4 | 0.6 | 37.3 MiB | 16 |
| rclone | cold | 4.4 | 3.63 | 2 | - | 0.8 | 83.3 MiB | 4 |
| rclone | incremental | 1.8 | 4.48 | 1 | - | 0.4 | 67.0 MiB | 4 |
**tiny-deep** - 300 files, 591.7 KiB, nested 5 deep
| Tool | Phase | Wall s | files/s | Files | API calls | CPU s | Peak RSS
| Conc |
| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| driven | cold | 78.6 | 3.8 | 300 | 489 | 2.0 | 28.4 MiB | 16 |
| driven | incremental | 2.4 | 0.4 | 1 | 7 | 0.4 | 26.1 MiB | 16 |
| rclone | cold | 152.5 | 2.0 | 300 | - | 1.3 | 59.9 MiB | 4 |
| rclone | incremental | 10.6 | 0.1 | 1 | - | 0.6 | 56.6 MiB | 4 |
Reading it: the incremental rows are the point. Driven re-detects a
single
changed file in **2.4 s and 7 API calls**; rclone takes **10.6 s**
because it
re-lists the remote every time. The state database is doing its job. The
concurrency columns differ because each tool runs at its **stock**
settings -
`--rclone-transfers` equalises them if you want to isolate the
algorithms.
These numbers are a pipeline proof, not a verdict: 300 files is far too
small to
conclude anything from, and cross-host comparisons are meaningless. Run
`just bench` (~610 MiB per tool) for numbers worth quoting.
## Gates
- `cargo fmt --all -- --check` - clean
- `cargo clippy --workspace --all-targets -- -D warnings` - clean (exit
0)
- `cargo test --workspace` - clean (exit 0); every crate green,
including driven-bench's 49 tests
- `driven-bench` ships 49 unit tests: fixture determinism and depth,
mutate /
restore round-trips, crash-left-mutated recovery, rclone stats parsing,
the
metrics marker line, upload-cap arithmetic, dest-id refusal, dotenv
precedence,
and report rendering (including that an unmeasurable cell is a dash, not
a
zero).
## Notes for the reviewer
- The coverage gate auto-includes any new crate via `--workspace
--exclude`, so
`driven-bench` is added to the exclusion list alongside `src-tauri` and
`driven-chaos` in `coverage.yml`, `scripts/coverage.sh` and the `just
coverage`
recipe. A harness that mostly spawns processes and needs live
credentials would
otherwise drag the gate down for no signal.
- `cargo test --workspace` does build and test the new crate. Its tests
are fast
and need no credentials or network; the credential-dependent paths
simply are
not exercised there.
- Nothing here runs on PR CI or dev-branch builds, by design.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01JLB3E2Jm7knNJd37fVpH8X
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>1 parent af8f048 commit 85f6d6a
19 files changed
Lines changed: 3769 additions & 5 deletions
File tree
- .github/workflows
- bench
- results
- crates/driven-bench
- src
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
12 | 12 | | |
13 | 13 | | |
14 | 14 | | |
15 | | - | |
16 | | - | |
17 | | - | |
18 | | - | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
19 | 22 | | |
20 | 23 | | |
21 | 24 | | |
| |||
88 | 91 | | |
89 | 92 | | |
90 | 93 | | |
| 94 | + | |
91 | 95 | | |
92 | 96 | | |
93 | 97 | | |
| |||
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| 16 | + | |
16 | 17 | | |
17 | 18 | | |
18 | 19 | | |
| |||
0 commit comments