Repository navigation
fix: stop a long-running app spawning git around the clock - #81
Merged
Merged
Conversation
A user on macOS reported lag after the app had run for a day. A process sample and a perf log showed about 65 git processes a second, roughly 115 per repo scan. On macOS each one is a fork(), which holds the allocator and Objective-C locks the UI thread needs, so the UI froze in plain allocations for up to 3.6s. Three changes: - Re-checking every sibling worktree on each scan was most of those processes, and linked worktrees are usually tracked as repos of their own, so scanning any of them repeated it for all the others. Whether a tree's branch is contained in mainline, ahead of it or merged is now remembered by the tree's HEAD and mainline's commit. An unchanged sibling costs status and log, not six to ten commands. - Read-only git runs with GIT_OPTIONAL_LOCKS=0. git status rewrote .git/index after refreshing its stat cache, and the recursive watch saw that as a change, so a scan could schedule the next one. - Nucleus's single-instance lock lives in its own directory instead of the temp dir. On macOS the JDK watch service polls, so it was stat'ing every file in $TMPDIR every few seconds. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Raman Gupta <rocketraman@gmail.com>
The perf log could say how often git ran and for how long, but not how much it handed back. A field report shows about 1GB/s allocated, and a 44-repo fixture at the same scan rate allocates 1-2MB/s, on the JVM and in the native image alike. What real repos return is the remaining difference, so each subcommand now also records its output size as git.<subcommand>.chars while collection is on. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Raman Gupta <rocketraman@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A macOS user (Apple Silicon, macOS 26.1) reported the app getting laggy after running for about a day. Their evidence:
sampleof the process: about 10 IO threads were infork()or reading git output in nearly every sample. The main thread spent 10–15% of the window blocked on locks thatfork()holds: malloc zones and the Objective-C weak table (the "AppKit lock" in the report). AFileSystemWatcherthread was polling$TMPDIR.Paint_nMakeblocked 1.28s). The Java heap held steady at ~55 MB used, so the 1.7 GB footprint is not a Java leak.The native image starts git with a real
fork(), so on macOS every git run briefly takes the allocator and Objective-C runtime locks away from the UI thread.What changed
WorktreeOps). Each scan of a repo with linked worktrees re-checks every sibling, and linked worktrees are usually tracked as repos themselves, so a scan of any of them redid the work for all the others. "Contained in mainline", "ahead by N", "squash-merged" and "merged" now come from a cache keyed on the tree's path, its HEAD, its own branch, and mainline's ref and commit. The uncommitted-file count, last-commit date and remote state are still read every scan. After the first scan an unchanged sibling costsstatuspluslog -1instead of 6–10 commands.GIT_OPTIONAL_LOCKS=0(Git.read/readOrNull/exitCode). Plaingit statusrewrites.git/indexwhen it refreshes its stat cache. The repo watch is recursive and includes.git, so a scan could trigger the next scan. Mutations throughGit.runare unchanged.Main.kt), next toregistry.json. By default it watchesjava.io.tmpdir. On macOS the JDK'sWatchServicepolls, which meant callinglstaton everything in$TMPDIRevery few seconds. This is fixed upstream (NucleusFramework/Nucleus@76d76c68b7), but only on the 3.0 line, not in any 2.5.x release.For review
hasOwnWorkreads the branch reflog, which only gains entries when the branch moves. The branch points at HEAD, so a move is a new key.scan.repo,watch.rescans, GC counts) will show the effect.Tests
Three new
WorktreeOpsTestcases cover the cache: it updates on a new commit in the tree and on mainline merging the branch, and the uncommitted-file count is read fresh while commits stand still. All 260 tests pass, confirmed in the result XML.Diagnostics: git output size
Each git subcommand now also records how much it returned, as
git.<subcommand>.charsin the perf log (only while collection is on). This is to answer what the reporter's GC rate is made of: about 2.5 young collections a second against a ~410 MB young generation, roughly 1 GB/s. A 44-repo, 20-worktree fixture churning at the same scan rate allocates 1–2 MB/s, on the JVM and in the 1.4.0 native image alike. On that fixture, git output totalled ~120 K characters over 3 minutes.The same fixture run confirms the worktree cache: in a minute of 36 scans,
diff,merge-baseandrev-listwent from 288/192/96 runs (pre-fix) to 0, and the mean scan time from 45 ms to 24 ms.