Skip to content

Redraw only the spinner when nothing else has changed - #5571

Merged
stefanhaller merged 6 commits into
jesseduffield:masterfrom
AntoineGS:spinner-cpu-usage
May 6, 2026
Merged

stefanhaller merged 6 commits into
jesseduffield:masterfrom
AntoineGS:spinner-cpu-usage

Conversation

@AntoineGS

Copy link
Copy Markdown
Contributor

PR Description

This fixes #4734, where lazygit would use a lot of CPU when the spinner was shown due to redrawing the entire screen every time the spinner needed a redraw.
The change adds the ability to request a UI redraw without invalidating the full UI, thus only redrawing the spinner, re-routing the spinner to this new flow.
CPU usage on a chromebook went from 60% to 5% when the spinner is shown.

Changing the refresh rate of the spinner from 50ms to 200ms brings it down from 5% to 1% if ever we would like to change the default, personally I prefer the slower moving spinner, but that's subjective.

Please check if the PR fulfills these requirements

  • Cheatsheets are up-to-date (run go generate ./...)
  • Code has been formatted (see here)
  • Tests have been added/updated (see here for the integration test guide)
  • Text is internationalised (see here)
  • If a new UserConfig entry was added, make sure it can be hot-reloaded (see here)
  • Docs have been updated if necessary
  • You've read through your own file changes for silly mistakes etc

@AntoineGS
AntoineGS force-pushed the spinner-cpu-usage branch from 88c40e0 to 84a9210 Compare May 1, 2026 18:23
@AntoineGS
AntoineGS marked this pull request as draft May 1, 2026 18:47
@AntoineGS
AntoineGS force-pushed the spinner-cpu-usage branch from 84a9210 to 7270d5a Compare May 1, 2026 18:49
@AntoineGS

Copy link
Copy Markdown
Contributor Author

Went in a rabbit hole so it'll take some time to wrap up a working solution, my original approach introduced a bug where the full redraw event could be lost while the spinner was shown. I'll let you know when I am done!

@AntoineGS
AntoineGS force-pushed the spinner-cpu-usage branch from 7270d5a to 050ff91 Compare May 1, 2026 20:46
@AntoineGS
AntoineGS marked this pull request as ready for review May 1, 2026 20:50
@AntoineGS

Copy link
Copy Markdown
Contributor Author

Alright so the problem was that some processes that update the layout do not themselves request a redraw and assumed it would be done through the ticker loop, adding a single full redraw after the ticker has completed (aka whatever job it was doing is done) fixed my issue.
Ready for review :)

@stefanhaller stefanhaller left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Great work!

This only fixes it for the bottom line status view; there's also the inline status that is used e.g. for fast-forwarding a branch, would be nice if this could be improved too. The code is in InlineStatusHelper.renderContext; it seems safe to call OnUIThreadContentOnly there, too. When I quickly tested it though, it didn't make as much of a difference as I would have expected, so maybe there's something else going on in that case. (Found it: it ends up calling Gui.render further down the call chain which undoes the optimization.) I'd be ok with solving this in a followup.

One quirk: if a user sets their gui.spinner.frames to a bunch of strings of unequal lengths, this used to push the rest of the status line over as necessary with every frame, and now it no longer does because we skip the layout. We could ignore this since it's unlikely that users do this, but we could also not let them; I pushed a commit (8a7e1fe) that rejects configs that try this.

Comment thread pkg/gui/controllers/helpers/app_status_helper.go
Comment thread pkg/gui/controllers/helpers/app_status_helper.go Outdated
Comment thread pkg/gocui/flush_test.go Outdated
@stefanhaller

Copy link
Copy Markdown
Collaborator

Also, were you planning on looking into the WithWaitingStatusSync issue? (either as part of this PR, or as a followup)

@AntoineGS
AntoineGS force-pushed the spinner-cpu-usage branch 3 times, most recently from f1d70cf to 6b846f7 Compare May 4, 2026 19:53
@AntoineGS

Copy link
Copy Markdown
Contributor Author

I have now added it to sync events too, this one takes the views as parameters otherwise some of the UI would update before the task is actually done. For example if moving a commit down, the UI would show the commit moved down at the same time as the spinner starts to spin, but the highlight and control would only be given back to the user when truly done.
(This is how it was prior to this change so no UI change there)
The performance improvement will not be as interesting with this one since it was already targeting specific views.
I also realized after force pushing that I should have used fixup commits instead of doing the actual fixup to facilitate reviewing the changes, sorry!

I added a commit which should probably be another PR, let me know if that's the case and I will happily do so.
I noticed the *Sync call was not honoring the tick rate config so I fixed it.

@stefanhaller

Copy link
Copy Markdown
Collaborator

I have now added it to sync events too,

Ah, good, but that's actually not what I meant. I mistyped in my last comment, I meant WithInlineStatus, not WithWaitingStatusSync, sorry. The WithInlineStatus change needs to happen in InlineStatusHelper.renderContext as I said earlier.

Also, for this change the commit structure doesn't work. You can't make an API-breaking change in one commit and then adapt callers in a later commit. In this case I'd suggest to leave the commits as you had them before (59766d3 and e9a4650), and then add a new commit after that for changing ForceRedrawViews to ForceFlushViewsContentOnly, and adapting the caller in the same commit.

The performance improvement will not be as interesting with this one since it was already targeting specific views.

But it called Layout with every redraw, so it should still make a difference.

I added a commit which should probably be another PR, let me know if that's the case and I will happily do so. I noticed the *Sync call was not honoring the tick rate config so I fixed it.

That's fine, happy to include that here.

@AntoineGS

Copy link
Copy Markdown
Contributor Author

Ah that makes more sense, might as well make the changes in this PR for the in-line spinner.
I will review the commit structure too so that each commit is build-able on its own.

@AntoineGS
AntoineGS force-pushed the spinner-cpu-usage branch from 6b846f7 to b3f6e49 Compare May 5, 2026 22:39
@AntoineGS

Copy link
Copy Markdown
Contributor Author

I took a conservative approach with the in-line spinner, I did not want to change the Context interface as it has a lot more implementations than just this one, and instead opted for type assertion while leaving a fallback to the original branch. While I believe the original branch is now dead code, it felt wrong to suddenly enforce a type to the generic Context interface.
Let me know what you think.

PS: Thank you for taking the time with the code reviews, it is very much appreciated :)

@stefanhaller

Copy link
Copy Markdown
Collaborator

I did not want to change the Context interface as it has a lot more implementations than just this one,

I don't think it's so bad to extend Context; it has only two implementations really, SimpleContext and ListContextTrait. I pushed dfc3593 that adds HandleContentOnlyRender to the Context interface and implements it in SimpleContext to simply call through to HandleRender (it's never called right now though). I really don't think that a type assertion is an appropriate tool in such a case.

I'm still not super happy with it, because there's an asymmetry: HandleRender takes care of calling Gui.render itself, but HandleContentOnlyRender doesn't, and relies on the caller to put it inside a OnUIThreadContentOnly call. We could have HandleContentOnlyRender do the OnUIThreadContentOnly inside I guess to make it more similar, but then it wouldn't be as flexible any more; a future caller might want to do a OnUIThreadContentOnly and then render the content of several views inside. Or do it the other way round and require callers of HandleRender to do the OnUiThread at the call site, but that would be a much bigger change, as there are so many callers. Thoughts welcome, but maybe we'll just live with the inconsistency.

Apart from that, I pushed two more trivial fixups.

Now the only remaining problem is that the commit "Support to request a content-only UI refresh" still doesn't compile.

@stefanhaller

Copy link
Copy Markdown
Collaborator

Wait, none of this HandleContentOnlyRender stuff makes any sense; I didn't think.

Currently the only difference between HandleRender and HandleContentOnlyRender is that the former calls Gui.render. We should totally move this out to the call sites (many of which don't actually need it even, as far as I can see), then we don't need two different methods on Context. Since this is a bigger change and requires careful examination of each call site, I suggest we do this in a followup and simply drop the inline status commit (and my fixups) from this branch.

@AntoineGS
AntoineGS force-pushed the spinner-cpu-usage branch 2 times, most recently from efcd664 to 8b69e1e Compare May 6, 2026 15:36
@AntoineGS

Copy link
Copy Markdown
Contributor Author

Done, I also ensured all commits can be built and tests pass for each.
I will open another PR to discuss the in-line changes.

@stefanhaller stefanhaller left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Looks good, thanks. There's one more little style thing that I pushed a fixup for, but I'm happy with this state now.

I will open another PR to discuss the in-line changes.

I'm not really sure this will save me time. Reviewing such a PR will probably take as much time as making the change myself, so I think I'd prefer to do that in this case.

@stefanhaller

Copy link
Copy Markdown
Collaborator

For some reason Github swallowed my inline comment for why I made that last fixup. I'm too lazy to type that again, let me know if you want to discuss any of that.

This didn't cause a bug so far because switching repos always happens from
within an OnWaitingStatus, so the spinner would take care of calling layout and
draw. However, later in this branch we are going to optimize the spinner so that
it no longer calls layout, at which point this would break, so make sure we
rerender at the point where it's needed.
AntoineGS and others added 4 commits May 6, 2026 18:53
This skips the whole-UI layout calculations, and lets tcell's dirty cell
handling redraw only changed cells.
The spinner looks weird if they don't. While we're at it, validate that frames
must not be empty, which would have crashed with a division by zero.
@AntoineGS

AntoineGS commented May 6, 2026 •

Copy link
Copy Markdown
Contributor Author

I'm not really sure this will save me time. Reviewing such a PR will probably take as much time as making the change myself, so I think I'd prefer to do that in this case.

Sounds good!

For some reason Github swallowed my inline comment for why I made that last fixup. I'm too lazy to type that again, let me know if you want to discuss any of that.

I can see how making the argument explicit have benefits and is less ambiguous to callers, all good.

Instead of redrawing the two views on each tick, only redraw the spinner
@stefanhaller
stefanhaller enabled auto-merge May 6, 2026 18:39
@stefanhaller
stefanhaller merged commit 49bb072 into jesseduffield:master May 6, 2026
13 checks passed
@stefanhaller

Copy link
Copy Markdown
Collaborator

@AntoineGS Here's the followup-PR for the inline spinner: #5592. Code review would be appreciated, but especially testing; I feel this is a bit of a risky change, there might be redraw glitches where a layout is missing now.

stefanhaller added a commit that referenced this pull request May 9, 2026
As a followup to #5571, improve performance for inline spinners too.

This requires a bit of preparation; in particular, we change
ListContextTrait.HandleRender to no longer force a UI layout/redraw, so
callers need to take care of this themselves where needed. This requires
careful testing to see that we didn't miss any situation where redrawing
only worked by accident; see the first commit of the branch for an
example.
social4hyq pushed a commit to social4hyq/homebrew-core that referenced this pull request Sep 20, 2026
lazygit 0.62.0

Created-by: HarmonybrewBot
Commit-by: HarmonybrewBot
Merged-by: HarmonybrewBot
Description: Created by `brew bump`

---

Created with `brew bump-formula-pr`.<details>
  <summary>release notes</summary>
  <pre>The big change in this release is what I have been referring to as the "keybinding revamp": it allows you to use richer keybindings than the few that were available before; see [Custom_Keybindings.md](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md) for details. It also describes a new keybinding syntax that is a bit more intuitive than the previous `<c-x>` notation (but the old one is still supported for backwards compatibility).

Note the section about Terminal compatibility in that document; not all terminals support the new protocol equally well, so it may be a good idea to switch to one that does.

Breaking change: The default keybinding for submitting a commit from the commit description editor has changed from alt-enter to command-enter on Mac, or ctrl-enter on Linux and Windows; these are the same bindings that are used in many multi-line edit field situations, e.g. in GitHub comments. Unfortunately these are not supported by all terminals; see the above-mentioned [terminal compatibility](https://github.com/jesseduffield/lazygit/blob/master/docs/keybindings/Custom_Keybindings.md#terminal-compatibility) for more on that. If you want to revert this change, you can do so by adding the following to your config:

```yml
keybinding:
  universal:
    confirmInEditor: [<alt+enter>, <ctrl+s>]
```

The complete list of changes follows:



<!-- Release notes generated using configuration in .github/release.yml at v0.62.0 -->

## What's Changed
### Enhancements 🔥
* Retry on ref lock errors during fetch/pull by @majiayu000 in jesseduffield/lazygit#5161
* Preserve whitespace when remembering commit message by @stefanhaller in jesseduffield/lazygit#5528
* Define PR colors as hex colors to make them the same as GitHub's by @stefanhaller in jesseduffield/lazygit#5480
* Revamp keybinding mechanism to support richer keybindings (e.g. `ctrl-alt-shift-x `) by @stefanhaller in jesseduffield/lazygit#5563
* Platform-specific edit bindings for move-by-word and backspace-word by @stefanhaller in jesseduffield/lazygit#5564
* Change canonical keybinding syntax (`<ctrl+a>` instead of `<c-a>`) by @stefanhaller in jesseduffield/lazygit#5565
* Use more widely-supported Unicode symbols for the commit graph by @stefanhaller in jesseduffield/lazygit#5573
* Show push URLs for remotes that have any by @stefanhaller in jesseduffield/lazygit#5576
* Support GitHub Enterprise for the Pull Requests feature by @stefanhaller in jesseduffield/lazygit#5596
* Allow keybindings to configure more than one key by @stefanhaller in jesseduffield/lazygit#5634
* Add `<alt+up>`/`<alt+down>` as alternate keybindings for moving commits up/down by @stefanhaller in jesseduffield/lazygit#5637
* Add support for git flow using the git-flow-next tool by @hmaddocks in jesseduffield/lazygit#5288
### Fixes 🔧
* Fix copying a file's absolute path when running in a linked worktree by @stefanhaller in jesseduffield/lazygit#5523
* Preserve commit message when quitting while the commit message panel is open by @stefanhaller in jesseduffield/lazygit#5524
* Stop showing cached PRs when we no longer have a base remote by @stefanhaller in jesseduffield/lazygit#5543
* If multiple remotes exist but only one is a Github remote, pick it without prompting by @stefanhaller in jesseduffield/lazygit#5544
* Fix cherry-picking a range selection of commits by @stefanhaller in jesseduffield/lazygit#5599
* Dont log GitHub PR fetching errors to the Command log by @stefanhaller in jesseduffield/lazygit#5600
* Avoid auto-forwarding main branches checked out in other worktrees by @stefanhaller in jesseduffield/lazygit#5621
* Fix cycling pagers when main view is focused by @stefanhaller in jesseduffield/lazygit#5639
### Maintenance ⚙️
* Bump samber.lo to latest version by @stefanhaller in jesseduffield/lazygit#5545
* Bump github.com/kyokomi/emoji/v2 from 2.2.8 to 2.2.13 by @dependabot[bot] in jesseduffield/lazygit#5481
* Bump github.com/gookit/color from 1.4.2 to 1.6.0 by @dependabot[bot] in jesseduffield/lazygit#5482
* Bump github.com/sirupsen/logrus from 1.9.3 to 1.9.4 by @dependabot[bot] in jesseduffield/lazygit#5483
* Bump dario.cat/mergo from 1.0.1 to 1.0.2 by @dependabot[bot] in jesseduffield/lazygit#5484
* Bump peter-evans/create-pull-request from 8.1.0 to 8.1.1 by @dependabot[bot] in jesseduffield/lazygit#5514
* Bump goreleaser/goreleaser-action from 7.0.0 to 7.1.0 by @dependabot[bot] in jesseduffield/lazygit#5529
* Bump actions/github-script from 8 to 9 by @dependabot[bot] in jesseduffield/lazygit#5515
* Bump golang.org/x/sys from 0.42.0 to 0.43.0 by @dependabot[bot] in jesseduffield/lazygit#5548
* Copy gocui into lazygit repo by @stefanhaller in jesseduffield/lazygit#5561
* Update to tcell v3 by @stefanhaller in jesseduffield/lazygit#5562
* Clear LC_* env vars when running LANG tests by @AntoineGS in jesseduffield/lazygit#5568
* Fix failing windows tests by @stefanhaller in jesseduffield/lazygit#5574
* Fix new gopls linter warnings by @stefanhaller in jesseduffield/lazygit#5580
* Disable gh telemetry when running integration tests by @stefanhaller in jesseduffield/lazygit#5587
* Fix the check_for_fixups.sh script again by @stefanhaller in jesseduffield/lazygit#5590
* Add script to preview release notes by @stefanhaller in jesseduffield/lazygit#5620
* Bump github.com/gookit/color from 1.6.0 to 1.6.1 by @dependabot[bot] in jesseduffield/lazygit#5586
* Bump github.com/sahilm/fuzzy from 0.1.1 to 0.1.2 by @dependabot[bot] in jesseduffield/lazygit#5606
* Bump golang.org/x/sys from 0.43.0 to 0.45.0 by @dependabot[bot] in jesseduffield/lazygit#5607
* Bump github.com/gdamore/tcell/v3 from 3.3.0 to 3.4.0 by @dependabot[bot] in jesseduffield/lazygit#5618
* Bump goreleaser/goreleaser-action from 7.1.0 to 7.2.2 by @dependabot[bot] in jesseduffield/lazygit#5619
### Docs 📖
* Fix undo shortcut in Undoing.md by @stefanhaller in jesseduffield/lazygit#5640
* Update docs and schema for release by @stefanhaller in jesseduffield/lazygit#5645
### I18n 🌎
* Update translations from Crowdin by @stefanhaller in jesseduffield/lazygit#5644
### Performance Improvements 📊
* Improve performance of showDivergenceFromBaseBranch by @AntoineGS in jesseduffield/lazygit#5536
* Redraw only the spinner when nothing else has changed by @AntoineGS in jesseduffield/lazygit#5571
* Improve inline spinner performance by @stefanhaller in jesseduffield/lazygit#5592

## New Contributors
* @majiayu000 made their first contribution in jesseduffield/lazygit#5161
* @AntoineGS made their first contribution in jesseduffield/lazygit#5536
* @hmaddocks made their first contribution in jesseduffield/lazygit#5288

**Full Changelog**: jesseduffield/lazygit@v0.61.1...v0.62.0

</pre>
  <p>View the full release notes at <a href="https://github.com/jesseduffield/lazygit/releases/tag/v0.62.0">https://github.com/jesseduffield/lazygit/releases/tag/v0.62.0</a>.</p>
</details>
<hr>

See merge request: Harmonybrew/homebrew-core!8880
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

High 'idle' CPU usage caused by rendering spinner of background fetch

2 participants