Skip to content

perf(desktop): reduce session-switch latency for active sessions #5627

Closed

Description

@me2seeks

What happened

Switching between Desktop sessions feels noticeably slow when the destination session is actively generating a response, especially when it is backed by a remote Runtime Host. In one instrumented development run, the previous transcript remained visible for about 1.5 seconds, new transcript rows began appearing around 2 seconds after selection, and the view settled after roughly 5 seconds. The sampling was approximate (about 200 ms), and this is an observation from one dev session, not a production benchmark.

A read-only replay of the transcript paging path against an isolated database copy took about 260 ms for a larger active-session sample. That makes SQLite alone an unlikely explanation for the full observed delay, but it does not identify the actual bottleneck: Runtime Host transport, Main/Renderer IPC, React work, streaming updates, and development-mode overhead still need to be measured separately.

How to reproduce

  1. Run Maka Desktop from source in development mode.
  2. Start a response in a session hosted by a remote Runtime Host and let it continue generating.
  3. Select another session, then switch back to the still-running session.
  4. Observe the delay before the selected session's transcript appears and becomes stable. Compare with switching to an idle session.

The delay was readily noticeable in the observed run. Packaged-build behavior has not yet been verified.

Environment

  • Maka version or commit: b004473ed (from source)
  • OS: Windows 11 with WSL2-backed remote Runtime Host
  • Surface: Desktop
  • Node.js: v26.2.0 (Windows dev process)
  • Mode: npm run dev (Electron 43 / Chromium 150)

Logs, screenshots, or additional context

No session names, transcript content, local paths, credentials, or account identifiers are included. The timings above are approximate and were collected with renderer instrumentation; instrumentation and dev mode may affect results. Please profile the switch in stages (selection/loading feedback, transcript-open response, IPC delivery, first paint, and stable render) and compare against a packaged build before assigning a root cause.

Related broader incremental-transcript work: #2913. This report is specifically about the user-visible latency when switching to an active session; it does not assume a particular implementation or duplicate the broader tracker.

Activity

  1. jackeyfaker77 commented on Sep 23, 2026

    @jackeyfaker77
    Contributor

    One interval worth measuring is the time between the first transcript batch arriving in the renderer and snapshot.ready becoming true.

    The publication path waits for readiness before replacing the displayed transcript, while history loading considers whole-Turn boundaries. Could we measure this interval separately from the Host subscription and React rendering timings? This might help distinguish data-loading latency from rendering latency.

    I haven’t reproduced the reported delay yet, so this is a profiling suggestion rather than a root-cause finding.

    中文对照

    一个值得测量的时间段是:从 Renderer 收到第一批聊天记录,到 snapshot.ready 变为 true,中间花了多久。

    当前逻辑会等待快照就绪后才替换界面上显示的记录,而历史记录加载还需要考虑完整的 Turn 边界。能否把这段耗时与 Host 订阅耗时、React 渲染耗时分别测量?这样可能有助于区分延迟来自数据加载还是界面渲染。

    我还没有复现报告中的延迟,因此这只是一个性能测量建议,并非对根因的判断。

  2. testikun commented on Sep 24, 2026

    @testikun
    Contributor

    Update 2026-09-24: the reproduction investigation produced the A/B/C measurements below. I am following up on those three measured workloads in #5680, starting with C, and have requested assignment there using the repository's take workflow. Product optimization will follow design review.

    The original report is partially reproduced, not disproved: C verifies a continuously active remote A → B → A switch with delayed content appearance, while the exact Windows/WSL behavior, approximately five-second settling milestone, active-specific penalty, and original bottleneck remain unconfirmed. Work on #5680 does not by itself establish a fix for this report.

    2026-09-24 更新:A/B/C 的实测场景统一在 #5680 跟进,优先讨论 C,方案评审后再实施优化。本 issue 的结论是“部分复现,原报告的完整表现与瓶颈尚未确认”,不表述为“主链路未复现”,也不据此否定原报告。

    Original investigation plan:

    I’d like to take this. I’ll first reproduce the active-session switch delay using the usage-faithful fixture retained from #5556, with an idle-session control. I’ll measure the path in stages (selection → transcript open → Host/Main/Renderer delivery → first transcript batch → snapshot.ready → first/stable paint) and compare development mode with a production build before proposing a fix.

    My reproduction environment is macOS rather than the originally reported Windows 11 + WSL2 remote-Host setup, so this first pass is specifically checking whether a similar user-visible delay can be reproduced on macOS. A positive result would show the symptom is not limited to Windows/WSL; a negative result would only be a platform comparison and would not invalidate the original report.

    我来跟进这个问题。先复用 #5556 留存的保真数据复现,并以 idle session 作为对照;确认各阶段耗时和开发/生产构建差异后,再基于证据提出修复。本轮环境是 macOS,不是原报告的 Windows 11 + WSL2 远端 Host,因此这里只尝试复现相似现象;未复现不能据此否定原报告。

    Prepared with Codex assistance at the contributor's direction.

  3. testikun commented on Sep 24, 2026

    @testikun
    Contributor

    Follow-up: #5680 now tracks the measured A/B/C workloads together, with C first. The current scope is reproduction and design discussion before product optimization. This original report is partially reproduced: C exercises a continuously active remote A → B → A switch and shows similar appearance delay, but the exact Windows/WSL behavior, approximately five-second settling milestone, active-specific penalty, and original bottleneck remain unconfirmed. Completing the follow-up will not by itself establish that #5627 is fixed.

    Updated 2026-09-24: clarified the timing boundaries in A/B and added C, which exercises a continuously generating session through a Linux Runtime Host and compares development mode with an actual packaged application.

    The 13–14 s values in B measure a return to a previously visited session after application startup has completed. The old column label “ready” was ambiguous: it meant a DOM observation, not application/Host startup readiness, transcript snapshot.ready, or stable rendering. The older timer also started before the automation click, not at the actual input event; that distinction is corrected below.

    Scope and baseline

    The original report concerns an already-running Windows/WSL development app: start generation in remote session A, switch to B, then return to A while generation continues. Its approximate 1.5 s old-content / 2 s new-rows / 5 s settled milestones concern that return.

    These tests use fb9df6c3dbbc449b8ff54dd4b608cc04381d3d81, Apple M2 Pro / 16 GiB, macOS Darwin 25.6.0 arm64, and Electron 43.4.1. They are reproduction measurements, with no product optimization applied.

    A/B: what the earlier measurements actually timed

    The setup launches Electron, waits for the UI and Host, opens the destination, and switches to the source. Only then does it arm the Renderer timer and call the automation click on the destination row. Launch, initial Host setup, and the destination's first visit are outside the measured interval. A previously visited idle transcript can still require a new subscription when revisited; “already running” does not imply its transcript remains resident.

    The tables below preserve the original numbers but rename the endpoints:

    • Idle historical DOM: all five target historical turns observed in A; the target's last historical anchor observed in B.
    • Streaming DOM: target selected and a matching streaming bubble observed. This does not establish that all history has appeared or that the view has settled.
    • Both intervals begin at pre-click probe arming and include automation click preparation. The old metadata's “click dispatch” wording was too precise. These are not directly interchangeable with C's actual-pointerdown measurements.

    A/B use a real isolated local macOS Host, with deterministic E2E FakeBackend output. Their “production” run uses a built Vite renderer in an unpackaged Electron launch; it is not the packaged-build comparison requested by the issue. There is only one sample per state per renderer. The earlier streaming check also did not verify continuous Host-side generation throughout A → B → A; C adds that verification.

    A. Five-turn local fixture: preliminary appearance measurements

    The retained #5556-shaped synthetic fixture has a source with 5 turns / 229 tools and a target with 5 turns / 789 tools / 6.95 MB logical payload. It is not the original reporter's workload.

    Renderer, unpackaged Idle historical DOM Streaming DOM
    Production renderer 1.497 s 1.351 s
    Vite development renderer 2.313 s 2.308 s

    These are preliminary content-appearance observations. In the dev samples, the automation click call itself took 0.83–1.09 s, so the 2.3 s values should not be presented as precise input-to-paint reproduction of the original 2 s symptom. The old-content detector also reported near-zero in A, which does not establish the reported old-transcript persistence.

    B. Very large local histories: a separate scaling stress case

    • Source: 100 turns / 15,780 tools / 139.0 MB logical payload.
    • Target: 300 turns / 6,448 tools / 51.4 MB logical payload.
    • Entire fixture: 302 sessions / 1,230 turns / 40,435 tools / 185,926 runtime events; SQLite 1.15 GB, schema 20, integrity check passed.
    Renderer, unpackaged Idle historical DOM Streaming DOM
    Production renderer 13.964 s 14.156 s
    Vite development renderer 14.125 s 13.736 s

    Here the automation click call returned in 0.29–0.58 s; the old source transcript was still observed until approximately 13.7–14.0 s after probe arming. Thus the long delay occurs during the same-process session return, rather than application startup. The dev streaming endpoint did not require the historical anchor to appear, so the two columns are not equivalent full-history readiness measurements.

    This demonstrates severe scaling trouble with unusually large local histories, including a production renderer. It does not identify the cause of the original active remote-session report.

    C. Matched histories, continuously active Linux Host, dev and packaged

    This run uses macOS Desktop → authenticated WebSocket → an independent Linux Runtime Host in Docker. Source and target use the same synthetic history recipe: either 30 turns / 300 tools each, or 100 turns / 1,000 tools each. The latter has about 4.64 MB payload per session and a 42.7 MB two-session database, substantially smaller than B.

    A short prompt starts a controlled FakeBackend response, approximately nine characters every 45 ms. Before leaving A, before returning, and after returning, the Host must report the same non-terminated turn with increasing output counters; the UI must resume displaying growing output. The control connection does not subscribe to the transcript. Each return starts after B's last historical turn is visible and at least 1.3 s of dwell.

    The timer starts at the Renderer's actual pointerdown, with 50 ms DOM sampling. “Content frame” below means target historical or streaming content observed followed by two animation frames: a paint-opportunity proxy, not a compositor timestamp or a stable-page measurement. Protocol readiness is measured separately.

    The packaged runs use a locally built .app / app.asar, with app.isPackaged === true verified at runtime. Values are milliseconds, median of three returns per state/configuration. “Added RTT” is an injected sensitivity test: +20 ms means 10 ms each direction; +0 means no injected delay, not zero actual latency and not a claim about WSL2 latency.

    Turns/tools per session Build Added RTT Idle old content gone Idle content frame Active old content gone Active content frame
    30 / 300 Dev 0 726.5 825.2 504.9 579.4
    30 / 300 Dev 20 963.7 1,067.1 769.7 854.5
    30 / 300 Packaged 0 375.0 446.8 322.0 376.7
    30 / 300 Packaged 20 620.1 676.5 481.3 544.9
    100 / 1,000 Dev 0 1,295.1 1,517.2 1,147.4 1,204.5
    100 / 1,000 Dev 20 1,919.4 2,027.6 1,632.5 1,718.3
    100 / 1,000 Packaged 0 755.7 819.9 678.0 713.6
    100 / 1,000 Packaged 20 1,407.2 1,488.2 1,270.3 1,310.0

    For the 100-turn active dev/+20 case, old-content removal ranged from 1,572.5–1,745.1 ms, and the content-frame proxy from 1,629.7–1,843.2 ms. The first historical row appeared at a median 1,745.1 ms; streaming content can appear before that row. This partially reproduces the visible delay when returning to a genuinely active remote session. Idle was not faster in this sample; states ran in blocks while active output grew, so the differences are observations, not an isolated causal estimate of “active overhead.”

    For the 100-turn active case, separate stage timestamps were:

    Milliseconds after pointerdown, median of 3 Dev / +0 Dev / +20 Packaged / +0 Packaged / +20
    Preload invokes transcript open 99.7 104.7 36.5 34.4
    Main sends first transcript batch 199.8 225.6 83.2 102.3
    Preload receives first batch 208.4 233.0 86.8 102.8
    Preload receives batch marked ready 776.3 1,258.2 586.7 1,117.7
    Preload open promise resolves 1,147.3 1,632.2 646.3 1,188.4
    Target content-frame proxy 1,204.5 1,718.3 713.6 1,310.0

    Each metric is summarized independently; differences between medians are not an additive CPU breakdown. Main→preload per-batch transfer medians were about 0.6–1.3 ms, with 23–29 ms aggregate synchronous transcript-handler time. Those measurements do not cover all response scheduling, ACKs, or streaming-event costs and do not exonerate the entire IPC path.

    Across C's remote active returns, the target required zero new subscription.open requests, versus one for idle. Nevertheless, the 100-turn active return still issued 20 history-page requests and delivered 41 Renderer batches / approximately 2.65 MB. This supports retention of the active subscription while showing that returning still transfers history. It does not justify carrying the idle subscription/reseed explanation over to the active case.

    C comprises 48 primary returns, six local-Host controls, 12 detailed-probe on/off checks, and six longer observations: 72 valid measurements. Detailed-probe on/off ranges overlapped; the small sample does not establish zero overhead. CPU profilers were off.

    Remaining differences: Docker/LinuxKit rather than Windows/WSL2, SQLite on a macOS bind mount, synthetic text streaming rather than real LLM/tool execution, Node 24.20.0 on the Mac / 24.21.0 on Linux rather than the reported Windows Node 26.2.0, and baseline fb9df6c3d rather than b004473ed. Dev uses Vite plus a tsc-built Main/preload, not the exact npm run dev launcher. The local package is unsigned/unnotarized and omits the optional unbuilt Direct peer addon; these measurements use WebSocket.

    What remains unverified

    The original approximately 5 s “settled” milestone remains unaligned. Six extra active runs continued observing for six seconds after the content frame. Streaming progressed, but the chosen historical-layout signature kept changing and never stayed constant for 500 ms. That is a limitation of this stability criterion during streaming/scrolling; it does not mean content needed seven seconds to load or establish a reproduction of the reported five-second settling time.

    C strengthens the reproduction of delayed old-content removal and target-content appearance on an active cross-system Host, and adds the requested real packaged comparison. The exact Windows/WSL workload, an active-specific penalty, the original stable-render milestone, and the final bottleneck remain unproven. A/B remain separately labeled local preliminary/stress evidence.

    Prepared with Codex assistance at the contributor's direction.

  4. added a commit that references this issue on Oct 16, 2026
    53751e4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions