Skip to content

Optimize session switching performance#129

Merged
PureWeen merged 17 commits intomainfrom
perf-session-switching
Feb 17, 2026
Merged

Optimize session switching performance#129
PureWeen merged 17 commits intomainfrom
perf-session-switching

Conversation

@PureWeen
Copy link
Owner

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

  • Keep-alive CSS toggle: All sessions rendered in DOM, hidden with display:none, toggled via JS — no Blazor render needed for switches
  • Instant JS sidebar toggle: Eliminates perceived 2s delay by doing sidebar selection via direct JS DOM manipulation
  • Visibility:hidden + warm renders: Ensures streaming sessions continue updating even when hidden
  • Render cooldown: Prevents redundant JS interop calls during rapid switching
  • Cleanup: Removed unused SaveDraftsFireAndForget method
  • Git workflow: Updated to use --force-with-lease instead of --force

@PureWeen PureWeen force-pushed the perf-session-switching branch from 8090b3a to cd7f1fa Compare February 17, 2026 05:30
PureWeen and others added 10 commits February 16, 2026 23:33
- 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>
@PureWeen PureWeen force-pushed the perf-session-switching branch from cd7f1fa to 32c3b48 Compare February 17, 2026 05:34
PureWeen and others added 7 commits February 16, 2026 23:52
… 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>
@PureWeen PureWeen merged commit f56b080 into main Feb 17, 2026
@PureWeen PureWeen deleted the perf-session-switching branch February 22, 2026 00:15
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