T-M1VQ-backup-changes-lazy-line-counts
State: planning/draft · Impact: medium · Complexity: medium
Opening a snapshot’s Changes tab is slow on a large store. The per-file line counts need
git diff-tree --numstat, which reads and diffs every changed blob. The snapshot list already
dropped numstat for --raw and went from 21.8 s to 0.09 s cold on a 5 GB store. This task gives
the Changes view the same speed: files first, counts when they arrive.
history::list_snapshotsrunsgit log --raw. It compares tree entries only.history::changes_forruns twodiff-treecalls at once,--name-statusand--numstat, and joins them by path injoin_changes. The response waits for the slower call, which is numstat.BackupPort::get_changesreturns oneChangesReport { sha, changes }. EachChangecarrieslinesAddedandlinesDeleted. Null means a binary file.api/v1/backup.rs::get_changespasses it through.BackupChangesList.vuecallstransport.backupGetChangesonce, showsDDataStateuntil it returns, then renders each row with its counts. A null count renders as “binary”.- Parity cases
get-changes-*undercrates/determined-parity/fixtures/cases/backup/assert the counts in the one response.
Proposed
Section titled “Proposed”The Changes view lists files at once and fills counts in later.
| Request | Returns | Cost |
|---|---|---|
get_changes(sha) | { sha, changes: [{ path, status }] } from diff-tree --name-status only | Tree compare, fast |
get_change_counts(sha) (new) | { sha, counts: [{ path, linesAdded, linesDeleted }] } from diff-tree --numstat | Reads blobs, slow on large stores |
- A binary file has null counts in
get_change_counts, as today. - A row shows a small placeholder where counts go until the counts arrive. It must not show “binary” while counts are loading. That label means a loaded null.
- A failed or slow count load leaves the list usable. Rows, the restore buttons and the Browse tab work throughout. A failed load shows a quiet “line counts unavailable” note with a retry. It does not replace the list with the error state.
- Per snapshot or per file is open. Start with one request per snapshot, since it keeps one git call. Per file only pays off if one huge file stalls the rest. Decide in the first step.
- Switching snapshots while counts load drops the stale reply, so counts never land on the wrong snapshot.
Approach
Section titled “Approach”- Decide the request grain with a timing run on a large store. Default: per snapshot.
- In
history.rs, splitchanges_forinto a name-status read and achange_counts_fornumstat read. Droprun_pairthere. The client applies counts by path, sojoin_changesgoes away or shrinks to the status parse. - Add
ChangeCountsReporttowire.rs. Remove the count fields fromChange. - Add
BackupPort::get_change_counts, theapi/v1/backup.rsendpoint and the ontogen-generated transport. Validateshaasget_changesdoes. - In
BackupChangesList.vue, load files first, then callbackupGetChangeCountsand merge counts by path. Track a separate counts status (pending,error,success) and a request token to drop stale replies. UpdateBackupChangeintypes.ts. - Update the parity cases
get-changes-*and addget-change-counts-*cases (follow-up, first snapshot, unknown sha, invalid sha). - Add a component test for the split.
| Area | Note |
|---|---|
solutions/determined/crates/determined-backup/src/history.rs | Split changes_for; add change_counts_for |
solutions/determined/crates/determined-backup/src/wire.rs | Change loses counts; new ChangeCountsReport |
solutions/determined/crates/determined-backup/src/port.rs | get_change_counts |
solutions/determined/crates/determined-core/src/ports/backup.rs | BackupPort::get_change_counts |
solutions/determined/crates/determined-platform/src/api/v1/backup.rs | New endpoint |
solutions/determined/crates/determined-parity/fixtures/cases/backup/ | Updated and new cases |
solutions/determined/experiences/backup/app/components/BackupChangesList.vue | Two-stage load |
solutions/determined/experiences/backup/app/types.ts | BackupChange counts become optional |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1: The file list appears without waiting on numstat.
get_changesruns no--numstatcall. - AC-2: Counts fill in when
get_change_countsreturns, and match whatget_changesreturned before the split, including null for a binary file. - AC-3: While counts load, rows show a placeholder, not “binary”.
- AC-4: A slow or failed count load leaves the list usable. Rows and restore buttons work, and a failed load shows a retry for counts only.
- AC-5: Switching snapshots while counts load never shows the old snapshot’s counts.
- AC-6: A regression case covers the split: a parity case for each request, and a component test that delays or fails the counts request and checks the list.
Out of scope
Section titled “Out of scope”- Line counts in the snapshot list. It keeps
--raw. - Caching counts across sessions.
- The boundary snapshot’s all-added list (T-A97O-backup-prune-operation).
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”list_snapshotsmoved from--numstatto--raw: 21.8 s to 0.09 s cold on a 5 GB store.changes_forkept numstat because the Changes view shows counts, so opening a snapshot is still slow there.