Do a performance and memory consumption optimization pass #91
Labels
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Grandiras/Ponente#91
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
🤖 On it — follow along in the action run; the reply will link the full session transcript.
📜 Live transcript — updating every ~5 minutes while the task runs.
Pushed to
claude/issue-91.What I found
There was no toolchain in the container, so I installed .NET 10 and PostgreSQL, seeded a board the size the group might actually run (4 columns, 120 cards, 2 tags + 2 assignees + 5 comments a card), and measured before touching anything. One thing dominated everything else.
CardService.GetColumnsAsynchung three collections offCardin one query, and collection includes multiply. That query came back 2,400 rows wide, each repeating that card's encrypted title and description — 2.85 MiB of ciphertext for 120 cards' worth of text. The third collection bought a single number: a card tile shows how many comments there are and never a word of them, so 600 comment bodies were fetched and AES-decrypted to be thrown away. And this runs on every board load and again in every connected circuit on everyBoardNotifierevent — one member dragging a card paid for it once per person looking.Changes
Count the comments, don't load them.
Card.CommentCountcomes from aGroupByreturning two integers a card;Card.Commentsis documented as not populated by the board read. Tags and assignees deliberately stay in the one query — four rows a card is cheaper than the two extra round tripsAsSplitQueryspends avoiding it (~18 ms against ~25 ms). It was the third collection, not the join.AesGcmDataEncryptor's string paths no longer route throughEncryptBytes/DecryptBytes, which forced an array per call that existed only to be copied out of again. The envelope is unchanged — the new tests carry envelopes written by the old implementation to prove it. Buffers are pooled, and the ones holding plaintext are returned cleared.BoardService.ResolveAsyncasks "the board, if it is theirs" in one query instead of "the board" then "may they see it", which always cost two round trips on two connections.CardPreviewmoved out ofCardView.razor(untestable there, asBoardFilterwas) and stops flattening 16,000 characters to render 200.Allocation figures are deterministic; the timings varied run to run on this shared box (the board load measured 12–24 ms after, 64–85 ms before), so treat those as approximate.
I measured and rejected one option: holding a single
AesGcminstead of building one per call is ~25% faster per call, butAesGcmisn't documented thread-safe and this is a singleton every circuit shares — so it needs thread-affine caching of a native handle for the life of the process, to buy ~3% of a board load. Not worth it in that class.Verification
AesGcmDataEncryptorhas ever had, with hard-coded legacy envelopes so a change to the bytes fails there rather than in production)._**_loses**and then vanishes as__). Both are now pinned by name inCardPreviewTests.denied, nonexistent →missing); the calendar renders.dotnet ef migrations has-pending-model-changes→ no changes.CommentCountis[NotMapped], so there's no migration.What I could not run: the Playwright suite needs
podman/docker, and this CI image has neither. I extendedboard.spec.js's comment test to assert the badge (the one user-visible thing the query change could break, and nothing covered it) and made it create its own card — it mutates one, so by the repo's own rule it always should have. I then ran that test's exact flow by hand against the live app and it passes; it still deserves a real run in the compose stack.Also added: a
2.4.1changelog entry in both locales (a board that opens noticeably faster is member-visible, and "dragging is smoother" set that precedent in 2.4.0), and notes inCLAUDE.mdrecording the two new invariants — never a third collection include onCard, and the encryptor's allocation rules. One trap worth flagging that's now written down there: EF caches its compiled model, value converters and the encryptor they close over included, process-wide and keyed on the options rather than the DI container — so my first attempt at an in-process A/B of two encryptors silently measured the same one twice.⏱ 44m 31s · 126 turns · tokens: 250.6k in / 100.2k out (+21360.9k cache-read)
📜 Full session transcript
→ Opened PR: #92