Environment
- Lanes 0.45.1 (brew cask), macOS 26.5.2 (arm64)
- git 2.50.1 (Apple Git-155)
- Affected project: a work monorepo with ~250,000 tracked files (I can't share it — see "Steps to reproduce" for a synthetic equivalent)
- Unaffected projects, same machine/session: repos with ~250 and ~13,000 files
Summary
While a project's board/lane view is displayed, Lanes spawns git -C <project> ls-files --others [--exclude-standard] roughly every 2 seconds (plus periodic git diff HEAD --name-status / --numstat). Each invocation walks the full working tree. On a ~250k-file repo one walk takes 0.5–1s+, so the scans run essentially back-to-back, and — this is the actual bug — the main thread blocks waiting for the result while servicing the webview's IPC (custom-scheme) request. Since that same thread pumps AppKit events, keyboard/scroll/window events stall in 0.5–3.4s chunks: the UI stutters, typing lags, scrolling is janky. Switching to a small-repo project is instantly smooth (identical code path, millisecond walks), switching back immediately stutters again.
Average CPU of all Lanes processes stays low throughout (the thread is blocked, not busy), so this is invisible to casual CPU-based profiling.
Impact
Any project backed by a large working tree (big monorepos are common in enterprise setups) is effectively unusable: permanent input latency and dropped frames while that project is displayed, regardless of whether any agent sessions are running (reproduces with zero sessions, just navigating the board).
Steps to reproduce
- Create a synthetic large repo (~250k tracked files):
git init bigrepo && cd bigrepo
for i in $(seq 1 500); do
mkdir -p "dir$i"
for j in $(seq 1 500); do echo x > "dir$i/f$j.txt"; done
done
git add -A && git commit -m "250k files"
- Add it as a project in Lanes and open its board tab. No sessions needed.
- Type in any text field / scroll the board / drag a card.
- In a terminal, watch the polling:
while true; do ps -eo pid,etime,args | grep '[l]s-files'; sleep 1; done
Expected: board interaction stays at 60 fps; typed characters echo immediately; any git state refresh happens off the UI thread, debounced, and its cost does not scale into the event loop.
Actual: the entire project UI is laggy for as long as the project is displayed, in sub-second to multi-second freeze bursts:
- Typing: characters appear noticeably later than typed. Holding backspace deletes smoothly for a few characters, then stops entirely, then "jumps" and removes 4–6 characters at once (classic key-repeat events queuing behind a blocked event loop and being coalesced on release).
- Scrolling: scrolling inside an open session lags visibly behind the gesture and takes a moment to "catch up" to where the user actually scrolled.
- Navigation/clicks: every interaction stutters — clicking into the project, opening past conversations/sessions, scrolling the session list, creating a new session — and clicks on UI controls (labels, state changes, etc.) take much longer to take effect than in other projects. All of it feels delayed and janky.
- Meanwhile
ls-files processes spawn every ~2s, each alive ~1s+, sometimes overlapping.
Three properties that narrow the cause:
- No session needs to be open. It reproduces with zero agent sessions — merely displaying and navigating the large-repo project's board is enough.
- Switching away is instantly smooth. Moving to a parallel session/board in a different (small-repo) project makes Lanes fully responsive again, even while sessions in the large-repo project keep running in the background.
- Switching back is instantly laggy again. Returning to the large-repo project restores the slowness immediately — it tracks which project is displayed, not session count, session age, or app uptime.
Evidence
Collected with a 1 Hz process logger and sample(1) (1 ms interval, 15 s windows) on both the main process and the WebContent process, correlated with which project was focused.
1. The polling loop (process log during a 2-minute stutter window; paths redacted):
00:03:04 pid 15004 ppid <lanes> git -C /path/to/bigrepo ls-files --others ... (etime 00:01)
00:03:07 pid 15105 ppid <lanes> git -C /path/to/bigrepo ls-files --others ... (etime 00:01)
00:03:09 pid 15194 ppid <lanes> git -C /path/to/bigrepo ls-files --others ... (etime 00:01)
00:03:11 pid 15285 ppid <lanes> git -C /path/to/bigrepo ls-files --others ... (etime 00:01, 70% CPU)
00:03:20 pid 15708/15709/15711 git diff HEAD --name-status | ls-files --others | git diff HEAD --numstat
00:03:23 pid 15791 ... git -C /path/to/bigrepo ls-files --others ... (etime 00:01)
00:03:25 pid 15892 ... (continues every ~2s for as long as the project is displayed)
The scan bursts also fire immediately on switching into the project's tab (timestamps match the tab switch to the second).
2. Main-thread blocking (15 s sample of the main app process, 1 ms interval ≈ 12.8k samples of the main thread):
|
large-repo project focused (stutter) |
small-repo project focused (smooth) |
samples inside WebURLSchemeHandlerCocoa::platformStartTask → app handler |
4,013 (31% of wall time) |
274 (2%) |
of which blocked in _pthread_join |
3,816 |
85 |
| longest contiguous block |
~3.4 s (3,449 consecutive 1 ms samples) |
~0.08 s |
The join sits under __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ inside the _DPSNextEvent run loop — i.e., the thread that delivers NSEvents is the one waiting. (App symbols are stripped, so the join target is inferred from timing correlation with the git children: block duration ≈ scan duration, both scale with repo size, spawns interleave exactly with the blocks.)
3. Scan cost scales with tree size, and caches don't save it:
| repo size |
git ls-files --others --exclude-standard (warm) |
| ~250 files |
~0.01 s |
| ~13k files |
~0.07 s |
| ~250k files |
0.45–0.55 s (longer cold; the untracked result was 4 files — the cost is the walk itself) |
Enabling core.untrackedCache + core.fsmonitor on the large repo made git diff HEAD drop 0.42 s → 0.05 s, but ls-files --others is unaffected (it doesn't use the untracked cache), and the user-felt stutter did not improve — consistent with ls-files being the dominant blocker. The WebContent process was ruled out: layout/paint/JS profiles are essentially identical between stutter and smooth windows.
Suggested directions
Any one of these would likely resolve it; together they'd make the git integration scale-proof:
- Never await the scan in the IPC handler. Reply to the webview asynchronously; the scheme-handler path currently ties repo-scan latency directly into the AppKit event loop.
- Debounce + cache per project. A 2 s unconditional poll re-pays the full walk even when nothing changed; an FSEvents-driven invalidation (or reusing git's fsmonitor) would reduce steady-state cost to ~zero.
- Prefer a cache-aware command.
git status --porcelain=v2 benefits from core.untrackedCache/core.fsmonitor; plumbing ls-files --others never does. Or skip untracked enumeration entirely where only tracked-change state is shown (--untracked-files=no).
- Scale guard. Above some working-tree size, lengthen the interval or degrade gracefully (on-demand refresh), and/or expose the poll interval in Settings.
Workarounds attempted
core.untrackedCache=true + core.fsmonitor=true on the repo: helps diff HEAD, does not help ls-files --others; no perceptible improvement.
- No setting found in Lanes to disable or throttle the git polling.
Happy to provide the full sample files / logs (scrubbed) or test a fix build.
Environment
Summary
While a project's board/lane view is displayed, Lanes spawns
git -C <project> ls-files --others [--exclude-standard]roughly every 2 seconds (plus periodicgit diff HEAD --name-status/--numstat). Each invocation walks the full working tree. On a ~250k-file repo one walk takes 0.5–1s+, so the scans run essentially back-to-back, and — this is the actual bug — the main thread blocks waiting for the result while servicing the webview's IPC (custom-scheme) request. Since that same thread pumps AppKit events, keyboard/scroll/window events stall in 0.5–3.4s chunks: the UI stutters, typing lags, scrolling is janky. Switching to a small-repo project is instantly smooth (identical code path, millisecond walks), switching back immediately stutters again.Average CPU of all Lanes processes stays low throughout (the thread is blocked, not busy), so this is invisible to casual CPU-based profiling.
Impact
Any project backed by a large working tree (big monorepos are common in enterprise setups) is effectively unusable: permanent input latency and dropped frames while that project is displayed, regardless of whether any agent sessions are running (reproduces with zero sessions, just navigating the board).
Steps to reproduce
while true; do ps -eo pid,etime,args | grep '[l]s-files'; sleep 1; doneExpected: board interaction stays at 60 fps; typed characters echo immediately; any git state refresh happens off the UI thread, debounced, and its cost does not scale into the event loop.
Actual: the entire project UI is laggy for as long as the project is displayed, in sub-second to multi-second freeze bursts:
ls-filesprocesses spawn every ~2s, each alive ~1s+, sometimes overlapping.Three properties that narrow the cause:
Evidence
Collected with a 1 Hz process logger and
sample(1)(1 ms interval, 15 s windows) on both the main process and the WebContent process, correlated with which project was focused.1. The polling loop (process log during a 2-minute stutter window; paths redacted):
The scan bursts also fire immediately on switching into the project's tab (timestamps match the tab switch to the second).
2. Main-thread blocking (15 s
sampleof the main app process, 1 ms interval ≈ 12.8k samples of the main thread):WebURLSchemeHandlerCocoa::platformStartTask→ app handler_pthread_joinThe join sits under
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__inside the_DPSNextEventrun loop — i.e., the thread that delivers NSEvents is the one waiting. (App symbols are stripped, so the join target is inferred from timing correlation with the git children: block duration ≈ scan duration, both scale with repo size, spawns interleave exactly with the blocks.)3. Scan cost scales with tree size, and caches don't save it:
git ls-files --others --exclude-standard(warm)Enabling
core.untrackedCache+core.fsmonitoron the large repo madegit diff HEADdrop 0.42 s → 0.05 s, butls-files --othersis unaffected (it doesn't use the untracked cache), and the user-felt stutter did not improve — consistent withls-filesbeing the dominant blocker. The WebContent process was ruled out: layout/paint/JS profiles are essentially identical between stutter and smooth windows.Suggested directions
Any one of these would likely resolve it; together they'd make the git integration scale-proof:
git status --porcelain=v2benefits fromcore.untrackedCache/core.fsmonitor; plumbingls-files --othersnever does. Or skip untracked enumeration entirely where only tracked-change state is shown (--untracked-files=no).Workarounds attempted
core.untrackedCache=true+core.fsmonitor=trueon the repo: helpsdiff HEAD, does not helpls-files --others; no perceptible improvement.Happy to provide the full sample files / logs (scrubbed) or test a fix build.