Skip to content

fix(chat): stop the context row re-laying out on every frame - #59

Merged
sorrycc merged 2 commits into
mainfrom
fm/qoderian-tab-cpu-k4
Sep 12, 2026
Merged

fix(chat): stop the context row re-laying out on every frame#59
sorrycc merged 2 commits into
mainfrom
fm/qoderian-tab-cpu-k4

Conversation

@sorrycc

@sorrycc sorrycc commented Sep 11, 2026

Copy link
Copy Markdown
Contributor

Intent

With a Qoderian chat tab open, the Obsidian renderer process (Obsidian Helper (Renderer) in macOS Activity Monitor) stays at roughly 24-30% CPU while Obsidian is visible, with elevated GPU helper activity and idle wake-ups. Find the cause and fix it so an open Qoderian tab no longer causes that CPU use, keeping this change limited to the fix and its tests.

What Changed

  • ContextRowOverflowController now calls toggleClass for the row's qoderian-context-row--expanded class only when the value actually changes. Obsidian's toggleClass rewrites the class attribute even when nothing changes. That write fired the controller's own MutationObserver, which requested another animation frame, so every open Qoderian tab re-ran its context-row layout on every frame, even with no chips attached.
  • After each layout pass, the controller calls mutationObserver.takeRecords() to drop records for DOM it has already read, so the row settles in one frame instead of measuring again a frame later. It also keeps the pending requestAnimationFrame handle and cancels it in destroy().
  • The test DOM mock's toggleClass now always calls classList.add/remove, the same way Obsidian's does. New unit tests check that blank and collapsed rows stop requesting animation frames once they settle, and that destroy() cancels the pending frame. The Unreleased section of CHANGELOG.md gets a Fixed entry.

Risk Assessment

✅ Low: The fix is small and contained. It guards the one class write that ran every time and that the MutationObserver reported, which fed the requestAnimationFrame loop running every frame (about 60 times a second). The regression tests check behavior, and the other paths that write into the context row still settle after the fix: the selection pollers, the has-content updater, the file and image chips, and the vault events.

Testing

  • ⏭️ Test - skipped

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 warning
  • ⚠️ src/features/chat/controllers/context-row-overflow.ts:56 - Simplification: the change adds a cancel-on-destroy path. It replaces the layoutScheduled boolean with a layoutFrame handle (:14, :67-69), adds a cancelAnimationFrame branch to destroy() (:56-59), and adds a new test, 'cancels its pending layout frame on destroy' (test :413-426). The CPU goal in the intent doesn't need any of it. Before this change, the existing if (!this.destroyed) check at :70 already turned a frame left pending after destroy into one no-op callback, not a repeating cost. With the cancel added, nothing can run that callback after destroy: both observers are disconnected, the pill is removed, and toggleExpanded calls layout() directly. So the change now has two guards for the same condition, and the destroyed check at :70 can never be reached. Remedy: remove the cancel branch, the layoutFrame handle, and that test, and go back to the boolean flag, unless the author wants frame cancellation on purpose. Runtime impact is negligible either way.
⏭️ **Test** - skipped

Step was skipped.

✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

ContextRowOverflowController observes class changes on the context row and
ends every layout pass with rowEl.toggleClass(expanded). Obsidian's
toggleClass calls classList.add or remove unconditionally, which rewrites
the class attribute even when nothing changes, so each pass queued a
mutation record that scheduled the next pass. Every chat tab ran this loop
once per display frame while Obsidian was visible. The renderer stayed busy,
and the cost grew with the number of tabs and with attached note chips.

Write the expanded class only when it changes, drop the records a layout
pass produced itself, and cancel the pending frame on destroy. The test
double for toggleClass now writes unconditionally like Obsidian, and new
tests check that a blank or collapsed row stops scheduling frames once it
settles and that destroy cancels the pending frame.

Add AGENTS.md with this class-helper sharp edge, plus the CLAUDE.md pointer.
@sorrycc
sorrycc merged commit acfa39c into main Sep 12, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant