Skip to content

perf(messages): measure read-tracking rows once per frame - #1527

Merged
loganj merged 3 commits into
mainfrom
perf/reading-frame-coalesce
Oct 22, 2026
Merged

loganj merged 3 commits into
mainfrom
perf/reading-frame-coalesce

Conversation

@loganj

@loganj loganj commented Oct 22, 2026

Copy link
Copy Markdown
Contributor

🤖

Summary

  • While a conversation has focus, the app decides which messages you have read. Before this change it measured the position of every message row each time the list scrolled, changed or resized. Each of those events forced the browser to lay out the page again first. A fast scroll or a burst of new messages could run that full measurement many times in one frame.
  • Now scroll, list changes and resizes measure the rows at most once per animation frame. Focus, clicks, key presses and tab visibility still measure at once, so the 300 ms read delay starts from those actions as before.

Details

  • Read results do not change. A message still counts as read only after it stays fully visible for 300 ms while the list has focus. Any scroll or list change still cancels a running 300 ms wait at once. Only the measurement waits for the next frame, so a wait can start up to one frame (about 16 ms) later.
  • Ordering with Mark unread is kept. The read permission ("lease") is still taken when the event arrives, not when the frame runs. If you mark a message unread between the scroll and the frame, the deferred measurement cannot mark it read again. A later scroll or click can still read it as normal.
  • Stop paths cancel the queued frame and its lease: blur, a hidden tab, a channel change and unmount.
  • New unit tests cover a burst of scrolls and list changes (one measurement), a queued frame after blur (no read), and the Mark unread ordering above.

Every scroll, list mutation and resize measured every row of the timeline synchronously, which forced a fresh layout per event. These high-rate triggers now cancel dwell at once and measure rows in the next animation frame. Discrete actions such as focus, pointer, key and visibility still start dwell at once.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <[email protected]>
@loganj
loganj marked this pull request as ready for review October 22, 2026 14:51
@loganj
loganj requested a review from a team as a code owner October 22, 2026 14:51
wesbillman

This comment was marked as resolved.

A scroll, mutation or resize used to drop the current view at once and
re-measure in the next frame. A pending Notify-while-viewing alert whose
permission probe resolved in that gap saw a still-visible row as not
viewed and was dropped for good.

The current view now stays until the frame replaces it. Dwell is still
cancelled at the event. While a frame is pending, the view confirms a
row only when a consumer asks, so a row scrolled out of view is not
reported as viewed. ReadingHandle.view's predicate now takes the
message ID.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <[email protected]>
@loganj

This comment has been minimized.

wesbillman

This comment was marked as resolved.

The previous fix kept the last view until the next frame, but that view
only covered the rows it had measured. A row scrolled into view before
the frame still counted as not viewed. With Notify while viewing off, a
pending alert for that row could be shown.

Each deferred measurement's lease now answers for the list on demand:
until its frame runs, any row of this list is checked when a consumer
asks. The old view is again released at the event. The view predicate
now receives whether the row was among the measured IDs, and unread
matches views by channel.

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <[email protected]>
@loganj

This comment has been minimized.

@wesbillman wesbillman left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 Star Lord’s automated source review via Wes’s account, integrating independent Rocket (systems/lifecycle), Mantis (correctness/concurrency), and Groot (product/accessibility) reviews.

No actionable findings in this follow-up. Both previously reported notification defects are addressed in source: the deferred lease now checks any requested row in its channel before the next frame, preserving still-visible notifications and suppressing alerts for newly visible rows when Notify while viewing is off. Event-time manual-unread fencing, dwell cancellation and lifecycle cleanup remain intact. Source assessment: 9/10.

Reviewed a9fe637 → 1e5e230 against base 3d1938a (merge-base f941667), including the changed view contract and its notification/timeline/thread consumers. Public PR description and three commit messages inspected; no attached images or disclosure findings.

The new held-permission regression test exercises the newly-visible-row case through the real reading hook and unread service, with mocked geometry; hook assertions cover measured and deferred views. No browser cases were added or removed. Tests and fail-before/pass-after behavior were not executed in this review.

Source-only: no tests, builds or app execution; CI not assessed. Per-message attention queries may still scan row IDs and read geometry outside the batched frame, so actual performance gains need browser measurement. Browser/native timing, rendered accessibility, error/retry focus behavior and human acceptance remain unverified. This COMMENT review is not approval or merge authorization.

@loganj
loganj merged commit 6dd1d4d into main Oct 22, 2026
37 checks passed
@loganj
loganj deleted the perf/reading-frame-coalesce branch October 22, 2026 17:46
johnmatthewtennant added a commit that referenced this pull request Oct 22, 2026
Co-authored-by: Aaron <f425b86c3b780d43f8d9cf1802429d04dd985bf92eac44fca9468b5ad3887c5e@buzz.block.builderlab.xyz>

* origin/main:
  fix(channels): confirm a requested member when the relay stores bot (#1519)
  test(browser): settle dialog motion and startup errors before strict checks (#1492)
  perf(messages): measure read-tracking rows once per frame (#1527)
  fix(settings): keep the chosen plugin row in place when details open or close (#1551)
  fix(messages): polish reaction pills and mention badges (#1506)

Signed-off-by: John Tennant <[email protected]>
morgmart added a commit that referenced this pull request Oct 23, 2026
Signed-off-by: morgmart <[email protected]>
Co-authored-by: Carl <c217fe6b9d958f41c3a5e030dccc7f626775a923089cb6491305eade75ea1f1b@buzz.block.builderlab.xyz>
Co-Authored-By: Claude Opus 5.5 <[email protected]>

* origin/main: (24 commits)
  Unify chat and channel access, tidy the Solo chat menu (#1446)
  fix(todos): render nested TODOs with collapsible hierarchy and Markdown (#1483)
  fix(agents): Allow agents to propose updates to agents (#1478)
  fix(composer): prevent plugins from updating the recipient on a WIP draft message (#1497)
  fix(agents): can't delete an agent belonging to an archived channel (#1560)
  Bump tokio to 1.53.3 for RUSTSEC-2026-0386 (#1581)
  Unify attachment cards with neutral icon tiles (#1548)
  Polish edit context, channel join and profile actions (#1545)
  fix(claude-code): keep context across channel threads (#1516)
  fix(builderlab): let Create agent cancel a pending browser sign-in (#1526)
  test(sidebar-filters): start the focus journey on General (#1555)
  Add opt-in native MCP for macOS tests (#1528)
  Show unreads for channels after access changes (#1550)
  Deliver artifact changes through shared live routes (#1480)
  condense photo and video attachments into a compact grid (#1549)
  Add page-owned sidebar contributions (#844)
  fix(channels): confirm a requested member when the relay stores bot (#1519)
  test(browser): settle dialog motion and startup errors before strict checks (#1492)
  perf(messages): measure read-tracking rows once per frame (#1527)
  fix(settings): keep the chosen plugin row in place when details open or close (#1551)
  ...
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.

3 participants