diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-31 11:26:52 +0100 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-31 11:26:52 +0100 |
| commit | 467fa84fd851459d5d9c9e5297e2500e938f0a18 (patch) | |
| tree | 392d0237cac273a6a07db11b120a8a453e8e8ff6 /docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md | |
| parent | 9ae9473acdb7cb3310c3028765cc4b30fedfce27 (diff) | |
| download | qtmaildir-467fa84fd851459d5d9c9e5297e2500e938f0a18.tar.gz qtmaildir-467fa84fd851459d5d9c9e5297e2500e938f0a18.zip | |
fix: keep bulk tag actions linear, and say when they land
Select all on a 10k-row search, then Archive, froze the window with one
CPU pinned. gdb on the live process showed the UI thread cubic before the
write was ever sent: each optimistic row write emitted dataChanged, whose
handler refreshed the action labels by walking the whole selection, and
each step of that walk looked the message up with a linear scan of every
thread.
ThreadListModel now answers id lookups from a lazily rebuilt hash,
invalidated by its own structural signals and by hand at the two in-place
id changes. MainWindow::batchModelWrites() holds the label refresh while a
loop of optimistic writes runs, then refreshes once. Two list membership
tests became sets.
The announcement also used to fade on a timer whether or not the write
had landed. It now reads "applying" until a round trip through the
worker's queue says the write has run, then "done". An error takes the
label back from a late "done".
Item 211.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md')
| -rw-r--r-- | docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md | 42 |
1 files changed, 42 insertions, 0 deletions
diff --git a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md index f5ae9f0..c83e41c 100644 --- a/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md +++ b/docs/superpowers/plans/2026-08-03-post-0.1.0-usability-closed.md @@ -10870,3 +10870,45 @@ not a backlog item picked up on a Tuesday. to, since there are three and nothing in the files names a preference. **Shares the write-during-sync question with 205.** Answer it once. + +## 211. Archiving thousands of selected rows freezes the window + +**Observed.** From the user's notes: a `tag:promo` search returned about 10k +rows; Select all, then Archive, froze the window with one CPU pinned and no +sign of progress. + +**Cause.** Measured on the live process, not read: three gdb samples of the +UI thread, all identical, worker thread idle. Nothing had reached the worker, +because the optimistic model update runs before the write is sent. The loop in +`sendMessageTagChange` calls `applyMessageTagChange` once per message, which +walked every thread to find it and emitted `dataChanged`. `MainWindow`'s +`dataChanged` handler ran `refreshUnreadAction`, whose `selectionTagPresence` +walked the whole selection calling `messageById`, itself another walk of +every thread. N writes times N selected rows times N threads is about 10^12 +string compares at 10k. `QStringList::contains` in the membership code added +smaller quadratic terms on top. + +**Approach.** `ThreadListModel` answers id lookups from a lazily rebuilt hash +(`rowForThread`, `rowForMessage`). The model's own structural signals mark it +stale, and so do the two in-place id changes (`reconcile` replacing a summary, +`setThreadMessages` setting `first`). The first row wins, which matches the +walks it replaces. `MainWindow::batchModelWrites()` holds the +`dataChanged`-driven label refresh while a loop of optimistic writes runs and +refreshes once afterwards. All nine loops go through it. Thread-id membership +tests use a `QSet`. The whole path is now linear. + +**Constraints.** Correctness now depends on the index being invalidated. +`idLookupsFollowEveryChangeToTheRows` asks a question whose answer moved after +each kind of change, and it was mutation-checked against dropping the +`reconcile` mark and the `rowsRemoved` hook. Both failed it. + +**Feedback.** First skipped as unneeded, then asked for after the hand test, +where the archive worked (0 promo messages left in the inbox) and the user +could not tell. The announcement used to vanish on a timer. It now reads +"<action>, applying…" until the worker has run the write, then a transient +"<action>, done". The trigger is a queued round trip through the worker's +queue, not `tagsApplied()`, which stays silent for a write that moved nothing, +while moves report through another signal. An error bumps the same ticket, so +a late "done" cannot cover the error message. Tested by +`aWriteSaysItIsApplyingUntilTheWorkerHasRunIt`, which fails when the reply is +dropped. |
