jesseduffield / jesseduffield/lazygit
Proposed Architectural Improvements for Efficiency
- Dominant language
- Go
- Stars
- 82.4k
- Forks
- 3k
- Avg merge
- 2d 18h
- Merged PRs (30d)
- 19
Description
The other day, I put my laptop away and woke up to a dead battery. Activity Monitor reported a 37% idle CPU usage by lazygit with a 12-hour energy score in the 10,000's. I am on a M3 Max MacBook Pro, but standby time has never been an issue in the past. I found one of my zellij tabs had an instance of lazygit that was stuck in a fetching state. My best assumption is that the gitea server this instance was accessing could not complete a tcp handshake, and got stuck in a fetching state.
I (admittedly) conversed with Claude Code about this to figure out best solutions for how to potentially fix this functionality, as well as to understand better how the codebase works. I am not a Go developer, but I do love this app and wanted to see if I could make any major improvements to it. Claude and I did reach a solution that drastically lowered CPU usage (back under 0.1%) and Energy consumption (12-hour in the mid 20's), but rather than submit an AI slop PR I figured I would start an architecture discussion instead.
## Architectural Changes
### 1. Fail-open TCP dial
Currently, backgroundFetch() calls git fetch --all on a fixed 60-second timer with no timeout or reachability check. If the remote is unreachable, the process hangs for 75 seconds and then immediately retries on the next tick.
Instead, I suggest a 2-second TCP dial is implemented that checks if the remote can be reached. This ensures we don't try to fetch a remote that is not accessible. Additionally, I tried adding a 30 second timeout to the fetch command to kill the fetch if it does not finish within 30 seconds. Finally, I added exponential backoffs that stretch the retry interval from 60 seconds to 8 minutes on consecutive failures. This value resets on either success OR a manual trigger.
In order of importance, I would argue:
1. TCP dial
2. Reduce timeout to 30 seconds from 75
3. Exponential backoff
As TCP dial will already reduce idling fetch time from ~100% down to about 3%. I could see 30 second fetch timeouts being an issue for REALLY large fetches, but I would wonder how large. Exponential backoff I think is a good feature, but I understand if it could add tech debt.
### 2. fsnotify
Currently, a time ticker triggers a git status check every 10 seconds, regardless of if files have changed. Instead, I recommend we try an fsnotify watcher that monitors the working dir for filesystem events. Additionally, this can be adjusted on a per-dir level to trigger different pane refreshes:
1. .git/refs/heads/ - changes triggers a BRANCHES refresh
2. working tree - changes triggers a FILES refresh
The implementation I created also runs polling as a safety net, but every 5 minutes rather than every 10-seconds. Additionally, if the fsnotify watcher fails to start, polling continues at the original 10-second interval.
## Final Notes
I certainly believe there are things about my Claude implementation that need refinement. For example, Claude made a new goEveryWithBackoff function, rather than just keeping goEvery and passing the backoff amount as an argument. Things like these could certainly be adjusted. However, from anecdotal evidence, I have noticed a massive difference in CPU usage based on these changes. I would be very interested to hear what suggestions people have and/or discuss best ways to move forward to avoid AI-slopifying such a great app, especially with my limited Go experience. Thanks!
Contributor guide
Assessment
This issue has not been assessed yet.