Skip to content

CPLAT-12472: move the session rescan off the UI loop - #172

Merged
gavin-jeong merged 1 commit into
masterfrom
CPLAT-12472-async-rescan
Sep 30, 2026
Merged

gavin-jeong merged 1 commit into
masterfrom
CPLAT-12472-async-rescan

Conversation

@gavin-jeong

Copy link
Copy Markdown
Collaborator

JIRA: https://sendbird.atlassian.net/browse/CPLAT-12472

Reported as "list navigation locks up sometimes".

Navigation was never the problem

Profiled against a real 277-session directory. A keypress costs 11-23µs; the debounced preview update another ~20µs. Both are far inside a frame.

The stall is doRefresh, which ran two expensive calls inline on the UI loop:

Call Cost
session.ScanSessions ~308ms
tmux.MarkLiveSessions (shells out to tmux) ~127ms
os.Stat ×279 — the rest of the "lightweight" pass 0.5ms

That is ~420ms of frozen UI per refresh, and doRefresh runs on R, on the post-spawn delayedRefreshMsg, and — with live updates on — every 3s tick.

The comment calling that whole fallback "lightweight" was wrong about which part cost what: the stat pass really is cheap, the tmux call is not.

Fix

scanSessionsCmd runs the scan and the live-state marking off the loop, returning refreshScannedMsg. The handler does only the merge (carryOverRefState, injectRemoteSessions, rebuild) — 1.9ms p50 / 8.8ms max, inside a frame.

doRefresh keeps just the mtime pass inline so ordering stays current between scans, and dedups concurrent scans behind refreshScanInFlight: the tick is 3s but a large scan takes longer, so without it the walks stack up and compete for the same disk.

The startup path (sessionsScannedMsg) had the same 127ms MarkLiveSessions call on the loop; it moved into that command too.

420ms → 0.26ms p50, 3.2ms max.

The trade — worth a look

LIVE/BUSY badges now refresh when the scan lands rather than synchronously, so between scans a badge can lag by up to one tick. I think that is clearly the right trade against freezing the UI on every refresh, but it is a behaviour change and I would rather have it looked at than buried.

TestDoRefreshRebuildsFilteredSessionItemsWhenLiveStateChanges still holds — it just has to run the dispatched command first, which is what the runtime does.

Test plan

  • go build ./... && go vet ./... && go test ./... green
  • Profiled before and after against the real session directory; numbers above are measured, not estimated
  • Two guards, each verified by reverting the fix and confirming failure:
    • TestDoRefreshDoesNotScanOnTheUILoop — a session written to disk must not appear until the dispatched scan's result is handled; if the scan runs inline it shows up before the call returns
    • TestDoRefreshDedupsConcurrentScans — a second refresh while one is in flight must not start another walk
  • Both assert shape, not wall-clock time, so they cannot go flaky on a loaded machine

Note

Branched from master, not from #171 — the two are independent and I did not want the reviews entangled.

Security checklist

  • No SecurityGroup rule changes
  • No 0.0.0.0/0 inbound
  • No public subnet resources
  • No IAM user changes
  • No secrets in code, commits, or logs

@upwind-code-us

upwind-code-us Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Upwind Upwind Code Scan - ✅ Passed

0 newly introduced vulnerabilities · 0 resolved · 1 total in this PR vs master

Total breakdown: 🔶 1 High

View full analysis in Upwind Console

Scan completed in 19s

Scan history (2 scans)
Commit Scanned at New Resolved Net
96fa04a 2026-09-30 04:32 UTC 0 0 0
20bdb92 < 2026-09-30 06:21 UTC 0 0 0

Last scanned: 20bdb92 · 2026-09-30 06:21 UTC

@upwind-code-us

upwind-code-us Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Upwind Upwind IaC Scan - ✅ Passed

0 newly introduced misconfigurations · 0 resolved · 0 total in this PR vs master

View full analysis in Upwind Console →

Scan completed in 2s

Scan history (3 scans)
Commit Scanned at New Resolved Net
96fa04a 2026-09-30 04:33 UTC 0 0 0
96fa04a 2026-09-30 04:32 UTC — — —
20bdb92 < 2026-09-30 06:22 UTC 0 0 0

Last scanned: 20bdb92 · 2026-09-30 06:22 UTC

@Kairo-Kim Kairo-Kim added the auto-review/approved Auto-approved by the Slack auto-reviewer bot label Sep 30, 2026

@jinsekim jinsekim left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM!

Reported as "list navigation locks up sometimes".

Navigation itself was never the problem. Profiled against a real
277-session directory, a keypress costs 11-23µs and the debounced
preview update another ~20µs. The stall was doRefresh, which ran two
expensive calls inline on the UI loop:

  session.ScanSessions       ~308ms
  tmux.MarkLiveSessions      ~127ms   (shells out to tmux)
  os.Stat x279                 0.5ms

So ~420ms of frozen UI per refresh — on R, on the post-spawn
delayedRefreshMsg, and with live updates on, every 3s tick. The comment
calling that whole fallback "lightweight" was wrong about which part
cost what: the stat pass really is cheap, the tmux call is not.

scanSessionsCmd now runs the scan AND the live-state marking off the
loop and returns refreshScannedMsg; the handler does only the merge
(carryOverRefState, injectRemoteSessions, rebuild), measured at 1.9ms
p50 / 8.8ms max — inside a frame.

doRefresh keeps just the mtime pass inline so ordering stays current
between scans, and dedups concurrent scans behind refreshScanInFlight:
the tick is 3s but a large scan takes longer, so without it the walks
stack up and compete for the same disk.

The startup path (sessionsScannedMsg) had the same 127ms
MarkLiveSessions call on the loop; it moved into that command too.

420ms -> 0.26ms p50, 3.2ms max.

The trade is that LIVE/BUSY badges refresh when the scan lands rather
than synchronously, so between scans a badge can lag by up to one tick.
TestDoRefreshRebuildsFilteredSessionItemsWhenLiveStateChanges still
holds — it just has to run the dispatched command first, which is what
the runtime does.

Two guards, each verified by reverting the fix and confirming they fail.
They assert shape (a command is dispatched; the session slice does not
change until its result is handled) rather than wall-clock time, so they
cannot go flaky on a loaded machine.
@gavin-jeong
gavin-jeong force-pushed the CPLAT-12472-async-rescan branch from 96fa04a to 20bdb92 Compare September 30, 2026 06:21
@gavin-jeong
gavin-jeong merged commit b1c15ff into master Sep 30, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

auto-review/approved Auto-approved by the Slack auto-reviewer bot

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants