Skip to content

Fix desktop session switching stalls / 修复桌面端会话切换与大历史卡顿 - #7999

Merged
SivanCola merged 7 commits into
esengine:main-v2from
SivanCola:fix/desktop-session-switch-performance
Aug 8, 2026
Merged

Fix desktop session switching stalls / 修复桌面端会话切换与大历史卡顿#7999
SivanCola merged 7 commits into
esengine:main-v2from
SivanCola:fix/desktop-session-switch-performance

Conversation

@SivanCola

@SivanCola SivanCola commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Problem

Large session histories, Markdown-heavy responses, and rapid topic switches could block the desktop renderer for seconds. On Windows, a WebView2 renderer-process failure could also leave the UI unrecovered.

Root cause

  • History activation eagerly loaded and converted more data than the first viewport needed.
  • The transcript mounted too many React/Markdown/tool subtrees and coupled folding to layout measurement.
  • Large GFM tables and completed Markdown streams were parsed on the main thread or silently fell back there when Worker globals were incompatible.
  • Stale activation and parse work continued after the user switched again.
  • Expensive tab metadata was computed synchronously on the activation path.
  • WebView2 process-failure handling did not provide bounded reload recovery.

Fix

  • Add a revision/digest-bound display index and budgeted HistorySliceForTab / HistoryContentForTab APIs.
  • Add ticketed, generation-guarded topic activation with cancellation and off-path metadata refresh.
  • Add a bounded per-session transcript store with stable entry IDs, generation fencing, LRU budgets, and live-session pinning.
  • Replace hot/warm/cold transcript layers with one block-level virtual list; closed folds build no body subtree.
  • Move completed Markdown parsing to a Worker, cancel stale jobs, virtualize oversized tables, defer large-code highlighting, and cache parsed history blocks.
  • Add renderer diagnostics and throttled WebView2 reload recovery.
  • Add production bundle, cache, DOM-growth, Worker, long-task, and switch-latency gates.

Performance

Normal-load local benchmark after the refactor:

Metric Result Gate
100-switch latency P95 285.7 ms <= 300 ms
Activation ready P95 17 ms <= 300 ms
INP-ish event P95 24 ms <= 200 ms
Long tasks >50 ms 0 max <= 500 ms
Markdown Worker max parse 576.5 ms <= 3000 ms
Retained heap growth about 7 MiB <= 20 MiB
DOM/cache growth within budget bounded

The original 500 KiB cold Markdown parse was about 6.7 s; the current Worker path keeps it off the renderer main thread and the measured fixture completed within the Worker gate.

Latest-head reruns on the shared development host were CPU-contaminated (load average 95 on 16 logical cores while several unrelated Go race suites ran): switch P95 341.4 ms and long-task P95 55 ms, while activation ready 24.3 ms, INP 24 ms, Worker, heap, cache, and DOM gates still passed. No gate was relaxed; CI or an otherwise idle host should be used for the authoritative rerun.

Verification

  • pnpm build
  • pnpm test:all (131 discovered frontend suites plus dedicated suites)
  • go test ./...
  • go test -race ./internal/agent ./internal/store ./internal/control
  • cd desktop && go test -race .
  • Windows/amd64 go-webview2/pkg/edge cross-compile
  • WebView2 patch/source contract tests
  • git diff --check
  • Bundle budgets: initial JS gzip 374.7/400 KiB; largest initial JS gzip 259.1/280 KiB; initial raw JS+CSS 2178.8/2200 KiB

Compatibility, cache, and security

  • Existing session body/event/provider-message formats are unchanged.
  • Missing, stale, corrupt, or unsupported display indexes rebuild from the canonical session; index failures are warn-only and never block saves.
  • Existing history APIs remain available; old clients ignore the new sidecar.
  • Cache entries are revision/generation scoped and stale async results are discarded.
  • Large content is fetched through bounded, rune-safe chunks.
  • Crash payload schema remains backward compatible and the new fields are optional.
  • No credentials, machine-local paths, or private email addresses are included in this change.

Release gate

A real Windows 4-core/8-GB WebView2 run remains a release validation item because that device is not available locally. The Windows source contract, race coverage, and amd64 cross-compile pass, so this is not treated as a merge blocker.

Related work

PRs #7916, #7770, #7771, and #7982 touch some of the same owner files or adjacent telemetry/session-recovery areas. This PR does not claim to supersede them; the branch is rebased onto the latest main-v2 and preserves the upstream behavior.

Repository standards

  • Refreshed the checked-in repolint baseline with the repository's supported update command after this performance refactor moved and added code across existing large owner files. No rule or CI check is disabled; the ratchet remains active for future growth.

Documentation-impact: none - the change preserves existing user workflows and public APIs; implementation details and diagnostics are covered by tests and the PR description.

Problem:
Large histories, markdown-heavy responses, and rapid topic switches could block the desktop renderer for seconds and leave WebView2 unrecovered after renderer failures.

Root cause:
History activation loaded and converted too much data eagerly, the transcript mounted too many rows, giant GFM tables dominated markdown parsing, stale worker jobs kept running, and the initial bundle retained avoidable animation work.

Fix:
Add indexed windowed history slices, ticketed cancellable activation, a bounded transcript store and virtual rows, worker-based markdown parsing with large-table virtualization and cancellation, tighter diagnostics and performance budgets, native animation/scrolling, and throttled WebView2 renderer reload recovery.

Verification:
- pnpm build
- pnpm test:all
- pnpm test:bench (100 switches)
- go test ./...
- go test -race ./internal/agent ./internal/store ./internal/control
- go test -race . in desktop
- windows/amd64 go-webview2 cross-compile
- git diff --check
@SivanCola
SivanCola requested a review from esengine as a code owner August 8, 2026 19:05
@github-actions github-actions Bot added v2 Go rewrite (1.x) — main-v2 branch, active development desktop Wails desktop app (desktop/**) agent Core agent loop (internal/agent, internal/control) labels Aug 8, 2026
Problem:
The desktop lint jobs, Windows root tests, repository standards check, and documentation-impact check blocked PR esengine#7999.

Root cause:
The event-format probe compared a potentially wrapped sentinel directly, the permission test assumed POSIX mode bits on Windows, and the refactor's repository-standard debt was not captured in the ratchet baseline.

Fix:
Use errors.Is for the sentinel, skip only the POSIX permission assertion on Windows, and refresh the checked-in repolint baseline with the supported repository command. The PR body now declares its documentation impact and baseline rationale.

Verification:
- go run ./tools/repolint
- go test ./...
- go test -race ./internal/agent ./internal/store ./internal/control
- cd desktop && golangci-lint run --timeout=5m
- cd desktop && go test ./...
- cd desktop && go test -race .
- cd desktop/frontend && pnpm build && pnpm test:all
- Windows/amd64 internal/agent test cross-compile
- git diff --check
Problem:
The root lint job still rejected two direct sentinel comparisons in the new session display-index scanner.

Root cause:
The scanner compared io.EOF and bufio.ErrBufferFull with == even though callers may wrap those errors.

Fix:
Use errors.Is for both scanner sentinel checks.

Verification:
- go test ./internal/agent -run 'Test(ScanSessionDisplayIndex|ReadSessionDisplayIndex|BuildSessionDisplayIndex|WriteSessionDisplayIndex)' -count=1
- golangci-lint run --timeout=5m
- go run ./tools/repolint
- git diff --check
Problem:
The Linux desktop CI could trust a stale display index after an equal-size transcript rewrite when both files had equal modification timestamps.

Root cause:
The generation guard treated index and transcript timestamp equality as proof that the index was published later. Filesystems can coalesce rapid writes into the same timestamp.

Fix:
Require the index timestamp to be strictly later than the transcript timestamp. Equality now takes the conservative streaming validation and index rebuild path.

Verification:
- cd desktop && go test . -run TestHistorySliceColdDetectsSameSizeAnchorRewrite -count=100
- cd desktop && golangci-lint run --timeout=5m
- go run ./tools/repolint
- git diff --check
Problem:
Strict timestamp ordering protected cold offset reads but caused the startup migration worker to rewrite otherwise valid indexes when filesystem timestamps were equal.

Root cause:
The migration probe and content-reading paths shared one timestamp policy even though only the latter dereferences indexed offsets.

Fix:
Make equality an explicit policy: cold slice and content reads reject it, while the migration probe may accept it before separately validating revision and digest.

Verification:
- cd desktop && go test . -run 'TestHistory(SliceColdDetectsSameSizeAnchorRewrite|IndexMigrationLoop)' -count=100
- cd desktop && go test ./...
- cd desktop && golangci-lint run --timeout=5m
- go run ./tools/repolint
- git diff --check
Problem:
Coarse filesystem timestamps made valid index publications and equal-size external rewrites indistinguishable, causing either stale index reuse or repeated cold scans.

Root cause:
Timestamp ordering alone cannot identify which content generation an index covers when transcript and sidecar modification times are equal.

Fix:
On timestamp ties, stream and compare the transcript digest, size, and message count before cold offset reads. Republish a matching sidecar to restore the fast path; keep migration probes idempotent because they do not dereference offsets.

Verification:
- cd desktop && go test . -run 'TestHistory(SliceColdDetectsSameSizeAnchorRewrite|IndexMigrationLoop|SliceSourceField)' -count=100
- cd desktop && go test ./...
- cd desktop && golangci-lint run --timeout=5m
- go run ./tools/repolint
- git diff --check
Problem:
PR esengine#7999 became conflicting after the skills/settings consistency series landed on main-v2.

Root cause:
Both branches updated shared desktop, frontend bridge/type, boot/controller, and repository-standard baseline owners.

Fix:
Merge main-v2 at 6122d3a, preserve both behavior sets, and regenerate the repolint baseline from the resolved tree. The only textual conflict was the generated baseline.

Verification:
- golangci-lint run --timeout=5m
- go test ./...
- go test -race ./internal/agent ./internal/store ./internal/control ./internal/boot ./internal/config
- cd desktop && golangci-lint run --timeout=5m
- cd desktop && go test ./...
- cd desktop && go test -race .
- cd desktop/frontend && pnpm build && pnpm test:all
- go run ./tools/repolint
- git diff --check
@SivanCola
SivanCola merged commit c56bc3e into esengine:main-v2 Aug 8, 2026
23 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Core agent loop (internal/agent, internal/control) desktop Wails desktop app (desktop/**) v2 Go rewrite (1.x) — main-v2 branch, active development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant