Measured, not asserted
Benchmark added in b1a69d3a (service/mail/bench_test.go):
go test ./service/mail/ -run xxx -bench AsTheStoreGrows -benchtime 20x
| messages already stored |
one delivery |
one mail_inbox |
| 100 |
1.4 ms |
0.46 ms |
| 1,000 |
10.4 ms |
4.4 ms |
| 5,000 |
71.3 ms |
20.6 ms |
Worse than linear — 5x the messages costs 6.9x the time.
Why
SendMessageTo does two things over the whole store, not over the message:
rebuildInboxes() — throws away inboxes and reconstructs every account's threads from every message, on every delivery. Called from 8 places.
save() — marshals, encrypts and rewrites the entire mail.json.
Both run holding the package's write mutex, so every reader and every other writer on the instance waits behind them. At 5,000 messages that is a 71ms global stall per message delivered. At 50,000 it is closer to a second.
Reads are linear too: messages is one flat slice with no index, so ListMessages, MessageUnlocked, byMessageIDUnlocked, imapFolder and about twenty other loops each scan all of it.
Why it is invisible until it isn't
Nothing gets slower in a way anyone notices — it degrades by a millisecond a week. A test store never reaches the size where it matters, and the instance that does has no benchmark watching it. The numbers above exist so this argues from measurement.
Directions, cheapest first
Related
Measured, not asserted
Benchmark added in
b1a69d3a(service/mail/bench_test.go):mail_inboxWorse than linear — 5x the messages costs 6.9x the time.
Why
SendMessageTodoes two things over the whole store, not over the message:rebuildInboxes()— throws awayinboxesand reconstructs every account's threads from every message, on every delivery. Called from 8 places.save()— marshals, encrypts and rewrites the entiremail.json.Both run holding the package's write mutex, so every reader and every other writer on the instance waits behind them. At 5,000 messages that is a 71ms global stall per message delivered. At 50,000 it is closer to a second.
Reads are linear too:
messagesis one flat slice with no index, soListMessages,MessageUnlocked,byMessageIDUnlocked,imapFolderand about twenty other loops each scan all of it.Why it is invisible until it isn't
Nothing gets slower in a way anyone notices — it degrades by a millisecond a week. A test store never reaches the size where it matters, and the instance that does has no benchmark watching it. The numbers above exist so this argues from measurement.
Directions, cheapest first
addMessageToInboxalready exists and takes a single message; delivery can update the affected inboxes incrementally instead of discarding the map. This is the single biggest win and does not change the storage format.flushUIDsalready does for the IMAP numbering — the pattern is in the package with a comment explaining exactly this reasoning.id → *Message,Message-ID → *Message,account → []*Message. Turns the common lookups from O(n) into O(1) and removes the ~20 hand-written scans.Related