test(imap): investigate folder-open round-trip latency (#125) #126

Merged
JMR-dev merged 1 commits from investigate-imap-folder-latency into main 2026-07-02 13:35:41 +00:00
JMR-dev commented 2026-07-02 13:24:53 +00:00 (Migrated from github.com)

Summary

Investigation/spike for #125 (IMAP folder-open latency, follow-up to #86). Localhost GreenMail runs at ~0 RTT, so real wall-clock folder-open latency cannot be measured in this environment. Instead this delivers a deterministic structural analysis: it counts the IMAP round-trips a folder-open pays, proves them with real-in-process-IMAP tests, and recommends a mitigation — without fabricating latency numbers.

Finding. ImapClient.withStore wraps every operation in its own short-lived Store, so each folder-open pays a full CONNECT + TLS + LOGIN + EXAMINE + FETCH + LOGOUT. Only EXAMINE + FETCH is intrinsic to opening a folder; the entire connection-setup group (the majority of the round-trips) is avoidable on the 2nd+ operation if a connection were reused. Optimistic render-from-cache already exists — selectFolder renders cached rows instantly and the network sync is a background refresh — so #125's "render while the network catches up" is already satisfied; only connection reuse remains.

What's here

  • docs/perf/issue-125-imap-folder-open.md — the folder-open path trace, the per-open round-trip sequence (with the imaps/STARTTLS breakdown), which round-trips are avoidable vs. necessary, the recommended per-account connection-reuse / keep-alive mitigation and its constraints (must not disturb IMAP IDLE #90; thread-safety vs. the unserialized UI ops and prefetch-outside-syncMutex; per-account connection limits; battery #88/#89/#90; stale-connection handling), and a concrete real-device/real-account measurement plan.
  • CountingImapProxy — a localhost TCP proxy that forwards a cleartext IMAP session to GreenMail while counting TCP connections and parsing IMAP command words.
  • ImapFolderOpenLatencyTest — GreenMail tests asserting the current no-reuse structure (N opens ⇒ N connections and N LOGINs; a single open ⇒ CONNECT-LOGIN-EXAMINE-FETCH-LOGOUT; list-then-read ⇒ 2 connections). These double as the validation harness for a future fix: when a connection is reused, flip the expected counts to assert reuse and the same real-IMAP tests confirm the win.

Why analysis-only (fix deferred)

A connection-reuse pool is the clear structural win, but its knobs — single mutex-guarded connection vs. bounded pool, idle-eviction timeout, stale-probe strategy, low-battery posture — are latency/battery trade-offs that can only be tuned with a real network + real device, which this environment can't provide, and a naïve version risks regressing the deliberate concurrency design (prefetch runs outside syncMutex) or the IDLE connection budget. Per #125's "spike first, measure before committing," this ships the harness + analysis and defers the pool. References #125 without closing it; the doc lists exactly what real-device/real-account measurement is needed to close it.

Testing

Fast gate green on JDK 21: :app:assembleDebug, :app:testDebugUnitTest, :app:lintDebug, :app:ktlintCheck, :app:detekt, plus :app:compileDebugAndroidTestKotlin. No production code changed.

🤖 Generated with Claude Code

## Summary Investigation/spike for #125 (IMAP folder-open latency, follow-up to #86). Localhost GreenMail runs at ~0 RTT, so real wall-clock folder-open latency **cannot** be measured in this environment. Instead this delivers a deterministic **structural** analysis: it counts the IMAP round-trips a folder-open pays, proves them with real-in-process-IMAP tests, and recommends a mitigation — without fabricating latency numbers. **Finding.** `ImapClient.withStore` wraps *every* operation in its own short-lived `Store`, so each folder-open pays a full `CONNECT + TLS + LOGIN + EXAMINE + FETCH + LOGOUT`. Only `EXAMINE + FETCH` is intrinsic to opening a folder; the entire connection-setup group (the majority of the round-trips) is **avoidable on the 2nd+ operation** if a connection were reused. Optimistic render-from-cache already exists — `selectFolder` renders cached rows instantly and the network sync is a background refresh — so #125's "render while the network catches up" is already satisfied; only connection reuse remains. ## What's here - **`docs/perf/issue-125-imap-folder-open.md`** — the folder-open path trace, the per-open round-trip sequence (with the imaps/STARTTLS breakdown), which round-trips are avoidable vs. necessary, the recommended per-account **connection-reuse / keep-alive** mitigation and its constraints (must not disturb IMAP IDLE #90; thread-safety vs. the unserialized UI ops and prefetch-outside-`syncMutex`; per-account connection limits; battery #88/#89/#90; stale-connection handling), and a concrete real-device/real-account measurement plan. - **`CountingImapProxy`** — a localhost TCP proxy that forwards a cleartext IMAP session to GreenMail while counting TCP connections and parsing IMAP command words. - **`ImapFolderOpenLatencyTest`** — GreenMail tests asserting the current no-reuse structure (N opens ⇒ N connections and N `LOGIN`s; a single open ⇒ `CONNECT-LOGIN-EXAMINE-FETCH-LOGOUT`; list-then-read ⇒ 2 connections). These double as the **validation harness for a future fix**: when a connection is reused, flip the expected counts to assert reuse and the same real-IMAP tests confirm the win. ## Why analysis-only (fix deferred) A connection-reuse pool is the clear structural win, but its knobs — single mutex-guarded connection vs. bounded pool, idle-eviction timeout, stale-probe strategy, low-battery posture — are latency/battery trade-offs that can only be tuned with a **real network + real device**, which this environment can't provide, and a naïve version risks regressing the deliberate concurrency design (prefetch runs outside `syncMutex`) or the IDLE connection budget. Per #125's "spike first, measure before committing," this ships the harness + analysis and **defers the pool**. References #125 without closing it; the doc lists exactly what real-device/real-account measurement is needed to close it. ## Testing Fast gate green on JDK 21: `:app:assembleDebug`, `:app:testDebugUnitTest`, `:app:lintDebug`, `:app:ktlintCheck`, `:app:detekt`, plus `:app:compileDebugAndroidTestKotlin`. No production code changed. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.