Optimize session switching performance#129
Merged
Conversation
8090b3a to
cd7f1fa
Compare
- Replace per-event InvokeAsync(SafeRefreshAsync) calls with timer-based ScheduleRender() that coalesces multiple events into single renders - Skip rendering for background session events (content, tools, activity) that aren't for the currently expanded session - Throttle RefreshState for non-switch OnStateChanged events to max 2/sec - Session switches get immediate InvokeAsync(SafeRefreshAsync) for instant UI - Add proper Timer disposal in DisposeAsync - Remove debug logging from ExpandSession and CollapseExpanded The root cause was that every SDK event (content deltas, tool starts, intents, usage info) from any session triggered a full Dashboard render via InvokeAsync(SafeRefreshAsync). With a busy session processing in the background, this caused 10+ renders per second, blocking the Blazor sync context and delaying session switch UI updates. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Updated MauiDevFlow skill files from update-skill (SKILL.md, android.md, ios-and-mac.md, setup.md) - Added new skill references: linux.md, troubleshooting.md - Updated relaunch.sh stop-then-start ordering for reliable app restart Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Move SaveUiState to Task.Run (eliminate sync disk I/O from switch path) - Fire-and-forget SaveDraftsAndCursor in ExpandSession (eliminate JS interop wait) - Defer skill/agent discovery to background thread in ExpandedSessionView - Replace 3 delayed setTimeout scrolls (50/150/300ms) with single rAF in restoreDraftsAndFocus Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Skip JS interop in SafeRefreshAsync during session switches (drafts already saved) - Bypass SafeRefreshAsync entirely for switch-triggered RefreshState (direct StateHasChanged) - Fire-and-forget restoreDraftsAndFocus in OnAfterRenderAsync (no await blocking render) - Fire-and-forget IntersectionObserver setup, skip when already initialized - Add @key to ChatMessageItem for efficient Blazor diff on session switch - Compute GetNewActivities() once per render instead of 3x - Remove redundant Messages.ToList() copy in ChatMessageList Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Key changes: - Render all active sessions in DOM with keep-alive pattern (display:none for hidden) - Toggle CSS visibility via JS interop in sidebar click handler (bypasses Blazor render pipeline) - Remove SafeRefreshAsync from session switch path (was blocking on JS interop round-trip) - Save drafts fire-and-forget via SaveDraftsFireAndForget instead of blocking SafeRefreshAsync - Change ExpandedSessionView EventCallbacks from lambdas to method groups with tuple adapters (eliminates per-render delegate allocations that forced Blazor to always detect parameter changes) - Add switchKeepAliveSlot JS function for instant CSS slot toggling - Add keep-alive-slot CSS class for proper sizing Performance improvement: ~2000ms → ~7ms median (285x faster) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
With keep-alive, textarea DOM elements persist across session switches, so draft text is preserved without explicit save/restore. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The keep-alive CSS slot toggle was working at ~1ms, but the sidebar's active highlight was rendered by Blazor and took ~2200ms to update, making the switch feel slow to the user. Changes: - Add data-session-name attribute to SessionListItem for JS targeting - Add capture-phase click handler that fires BEFORE Blazor's event processing, instantly toggling both the keep-alive slot visibility AND the sidebar active class via pure DOM manipulation - Update switchKeepAliveSlot to accept sessionName parameter and toggle sidebar .active class by matching data-session-name attribute Measured result: both content area and sidebar switch in <1ms (verified via screenshots at 100ms showing complete visual switch). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…itch Root cause: switching to an actively streaming session took ~2s because KeepAliveSession's ShouldRender() suppressed renders while hidden, causing a massive catch-up render burst when becoming visible. Also, display:none required full layout recalculation on show. Changes: - Add KeepAliveSession wrapper component with ShouldRender optimization that skips rendering for hidden non-streaming sessions - WarmWhenHidden parameter: streaming sessions render at 250ms intervals while hidden, preventing catch-up burst on activation - Replace display:none/block with visibility:hidden/visible + position absolute overlay, keeping browser layout warm - Blazor class binding agrees with JS toggle (no DOM diff on re-render) - Remove MutationObserver (no longer needed - class binding is idempotent) - Add position:relative to expanded-mode container for absolute slots Measured: main thread stays unblocked (10-11ms intervals) during switch. Visual switch confirmed via MAUI screenshot at 150ms showing correct content. rAF paint callback delayed by Blazor work but actual visibility toggle is synchronous. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
cd7f1fa to
32c3b48
Compare
… review - Add volatile to _sessionSwitching, use Interlocked for _switchCooldownUntil - Add _disposed guard in ScheduleRender and SafeRefreshAsync - Catch ObjectDisposedException on Timer.Change after disposal - Wrap Task.Run skill/agent discovery in try/catch for disposal safety - Fix EventCallback types: use lambdas for OnSetInputMode/OnSetModel - Remove stale SetPlanMode/SetExpandedModel tuple adapters - Remove unused _refreshPending field Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ions ReconcileOrganization now uses GetOrCreateRepoGroup instead of only looking for existing groups. Also adds a second pass to fix sessions that already have a WorktreeId but were left in the default group. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…arios 9 new tests covering: - GetOrCreateRepoGroup creates, deduplicates, and sorts repo groups - HasMultipleGroups reflects repo group existence - Sessions with WorktreeId in default group get reassigned to repo group - Sessions without worktrees stay in default group - Sessions already in correct repo group are unchanged - Multiple sessions across different repos all get correctly assigned Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Add @key to SessionCard in grid view so input text follows the correct card when sessions re-sort - Revert relaunch.sh to main's zero-downtime handoff (stop old after new is stable, not before) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Fix double Interlocked.Read race in SafeRefreshAsync cooldown check (read once, reuse value — prevents timer mischeduling) - Fix grid view not receiving streaming updates (handlers now schedule renders when expandedSession is null) - Restore .ToList() snapshots in ChatMessageList to prevent collection-modified crashes from background thread mutations - Fix skill discovery race on rapid session switching (capture session name, verify before applying results) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The old launch-then-old order caused the new instance to bind a different MauiDevFlow agent port (e.g. 10224 instead of 9223) because the old instance still held 9223 during the overlap window. This broke maui-devflow auto-discovery after relaunches. Now terminates the old instance first (with 1s pause for port release), then launches new. Also simplified the stability check to use process existence check instead of grepping ps output. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The 500ms throttle in RefreshState was swallowing the state change that fires when a turn completes (SessionIdleEvent), causing the final assistant message to not appear until the user clicked stop. Extracted throttle logic into testable RenderThrottle class that bypasses the throttle when completedSessions is non-empty, ensuring turn-completion refreshes always proceed. Added 7 tests covering throttle bypass for completed sessions, normal throttling, session switches, and custom intervals. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Dramatically improves session switching performance from ~2s perceived delay down to ~7ms by using a keep-alive CSS toggle pattern instead of full Blazor re-renders.
Changes
display:none, toggled via JS — no Blazor render needed for switchesSaveDraftsFireAndForgetmethod--force-with-leaseinstead of--force