perf(sync): pace backfill with an inter-slice cooldown and per-run cap

Backfill chained bounded slices back-to-back with no gap, and on a large
mailbox `moreWork` never clears, so a single BackfillWorker run paged
flat-out for its whole session and kept the account's IMAP connection
saturated -- the background load that starves interactive message-opens
(#355) and worsens provider throttling (#360).

Add BackfillPacer, a small in-process primitive that bounds one run:
- inter-slice cooldown: a fixed 30s idle between chained slices;
- per-run slice cap: at most 4 slices per run, then defer to the 30-min
  periodic cadence so one run cannot monopolise the account.

Composes with the two sibling mechanisms instead of duplicating them:
- #355 (InteractiveImapGate): the cooldown is SKIPPED while an interactive
  fetch is active -- the next slice already parks at its per-page yield
  point, so a fixed delay on top would only double the idle (no pathological
  double-delay);
- #360 (AccountThrottleGate): a slice whose only outstanding work is a
  throttled account returns moreWork=false, so the loop stops and no
  cooldown is spent spinning on a backed-off provider.

The cooldown is a cancellable delay and the loop rechecks !isStopped before
each slice, so a WorkManager stop / teardown ends a run promptly. All
breadcrumbs are PII-free (durations/counts only).

Tests: BackfillPacerTest (JVM, virtual time) covers cooldown timing, cap,
cancellation, and the #355/#360 composition; BackfillWorkerTest asserts the
worker caps a flat-out run; BackfillPacerInstrumentedTest proves forward
progress across paced runs, the interactive skip, and prompt cancellation on
the real Android runtime.

Closes #356
This commit is contained in:
2026-07-08 16:57:15 -05:00
parent 2cd282fedc
commit 5c564ebeda
8 changed files with 480 additions and 9 deletions
+2
View File
@@ -73,6 +73,8 @@ style:
# #360 throttle gate: onThrottle/onSuccess breadcrumb through AppLog, which forwards to
# android.util.Log (a throwing JVM stub), so this suite mockkStatic(Log) too.
- '**/data/sync/AccountThrottleGateTest.kt'
# #356 backfill pacer: cooldown/cap/skip breadcrumbs through AppLog, so this suite mockkStatic(Log) too.
- '**/data/sync/BackfillPacerTest.kt'
# Reader-path perf logging (issue #358): the repository's openMessage and the reader ViewModel
# log via AppLog, so their unit tests mockkStatic(Log) too.
- '**/data/repository/MailRepositoryImplTest.kt'