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.
## 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)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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.withStorewraps every operation in its own short-livedStore, so each folder-open pays a fullCONNECT + TLS + LOGIN + EXAMINE + FETCH + LOGOUT. OnlyEXAMINE + FETCHis 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 —selectFolderrenders 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 NLOGINs; 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