Skip to content

Commit a171ec4

Browse files
authored
Reduce memory consumption when loading large number of commits (#3687)
(Github decided to auto-close #2533, and I don't see any way to reopen it, so opening a new one here. Please see there for discussion and review.) When pressing `>` in the commits panel to trigger loading all the remaining commits past the initial 300, memory consumption is a pretty big problem for larger repositories. The two main causes seem to be 1. the cell memory from rendering the entire list of commits into the gocui view 2. the pipe sets when git.log.showGraph is on This PR addresses only the first of these problems, by not rendering the entire view, but only the visible portion of it. Since we already re-render the visible portion of the view on every layout call, this was relatively easy to do. Below are some measurements for our repository at work (261.985 commits): | | master | this PR | | ------------- | ------ | ------- | | without Graph | 855 MB | 360 MB | | with Graph | 3.1 GB | 770 MB | And for the linux kernel repo (1.170.387 commits): | | master | this PR | | ------------- | ----------------------------------------- | ------- | | without Graph | 5.8 GB | 1.2G | | with Graph | Killed by the OS after it reached 86.9 GB | 39.9 GB | The measurements were taken after scrolling all the way down in the list of commits. They have to be taken with a grain of salt, as memory consumption fluctuates quite a bit in ways that I find hard to make sense of. As you can see, there's more work to do to reduce the memory usage for the graph, but for our repo at work this PR makes it usable already, which it wasn't really before.
2 parents 5a5cd84 + deee5fa commit a171ec4

File tree

15 files changed

+218
-96
lines changed

15 files changed

+218
-96
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ require (
1616
github.com/integrii/flaggy v1.4.0
1717
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68
1818
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d
19-
github.com/jesseduffield/gocui v0.3.1-0.20240623092910-a42926c14fc9
19+
github.com/jesseduffield/gocui v0.3.1-0.20240623095254-05e1204c2454
2020
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10
2121
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5
2222
github.com/jesseduffield/minimal/gitignore v0.3.3-0.20211018110810-9cde264e6b1e

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,8 +188,8 @@ github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68 h1:EQP2Tv8T
188188
github.com/jesseduffield/generics v0.0.0-20220320043834-727e535cbe68/go.mod h1:+LLj9/WUPAP8LqCchs7P+7X0R98HiFujVFANdNaxhGk=
189189
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d h1:bO+OmbreIv91rCe8NmscRwhFSqkDJtzWCPV4Y+SQuXE=
190190
github.com/jesseduffield/go-git/v5 v5.1.2-0.20221018185014-fdd53fef665d/go.mod h1:nGNEErzf+NRznT+N2SWqmHnDnF9aLgANB1CUNEan09o=
191-
github.com/jesseduffield/gocui v0.3.1-0.20240623092910-a42926c14fc9 h1:JJ0DrXgAUpGBGV5w8nzrQLMWTgcTvf745IKAk08qjcM=
192-
github.com/jesseduffield/gocui v0.3.1-0.20240623092910-a42926c14fc9/go.mod h1:XtEbqCbn45keRXEu+OMZkjN5gw6AEob59afsgHjokZ8=
191+
github.com/jesseduffield/gocui v0.3.1-0.20240623095254-05e1204c2454 h1:rTPA5WiPM1SPUA3r2kSb3RiILC93am6irMvOLjO7JNA=
192+
github.com/jesseduffield/gocui v0.3.1-0.20240623095254-05e1204c2454/go.mod h1:XtEbqCbn45keRXEu+OMZkjN5gw6AEob59afsgHjokZ8=
193193
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10 h1:jmpr7KpX2+2GRiE91zTgfq49QvgiqB0nbmlwZ8UnOx0=
194194
github.com/jesseduffield/kill v0.0.0-20220618033138-bfbe04675d10/go.mod h1:aA97kHeNA+sj2Hbki0pvLslmE4CbDyhBeSSTUUnOuVo=
195195
github.com/jesseduffield/lazycore v0.0.0-20221012050358-03d2e40243c5 h1:CDuQmfOjAtb1Gms6a1p5L2P8RhbLUq5t8aL7PiQd2uY=

pkg/gui/background.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package gui
22

33
import (
4+
"fmt"
5+
"runtime"
46
"strings"
57
"time"
68

@@ -46,6 +48,29 @@ func (self *BackgroundRoutineMgr) startBackgroundRoutines() {
4648
refreshInterval)
4749
}
4850
}
51+
52+
if self.gui.Config.GetDebug() {
53+
self.goEvery(time.Second*time.Duration(10), self.gui.stopChan, func() error {
54+
formatBytes := func(b uint64) string {
55+
const unit = 1000
56+
if b < unit {
57+
return fmt.Sprintf("%d B", b)
58+
}
59+
div, exp := uint64(unit), 0
60+
for n := b / unit; n >= unit; n /= unit {
61+
div *= unit
62+
exp++
63+
}
64+
return fmt.Sprintf("%.1f %cB",
65+
float64(b)/float64(div), "kMGTPE"[exp])
66+
}
67+
68+
m := runtime.MemStats{}
69+
runtime.ReadMemStats(&m)
70+
self.gui.c.Log.Infof("Heap memory in use: %s", formatBytes(m.HeapAlloc))
71+
return nil
72+
})
73+
}
4974
}
5075

5176
func (self *BackgroundRoutineMgr) startBackgroundFetch() {

pkg/gui/context/base_context.go

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,12 @@ type BaseContext struct {
2020
onFocusFn onFocusFn
2121
onFocusLostFn onFocusLostFn
2222

23-
focusable bool
24-
transient bool
25-
hasControlledBounds bool
26-
needsRerenderOnWidthChange bool
27-
highlightOnFocus bool
23+
focusable bool
24+
transient bool
25+
hasControlledBounds bool
26+
needsRerenderOnWidthChange bool
27+
needsRerenderOnHeightChange bool
28+
highlightOnFocus bool
2829

2930
*ParentContextMgr
3031
}
@@ -37,15 +38,16 @@ type (
3738
var _ types.IBaseContext = &BaseContext{}
3839

3940
type NewBaseContextOpts struct {
40-
Kind types.ContextKind
41-
Key types.ContextKey
42-
View *gocui.View
43-
WindowName string
44-
Focusable bool
45-
Transient bool
46-
HasUncontrolledBounds bool // negating for the sake of making false the default
47-
HighlightOnFocus bool
48-
NeedsRerenderOnWidthChange bool
41+
Kind types.ContextKind
42+
Key types.ContextKey
43+
View *gocui.View
44+
WindowName string
45+
Focusable bool
46+
Transient bool
47+
HasUncontrolledBounds bool // negating for the sake of making false the default
48+
HighlightOnFocus bool
49+
NeedsRerenderOnWidthChange bool
50+
NeedsRerenderOnHeightChange bool
4951

5052
OnGetOptionsMap func() map[string]string
5153
}
@@ -56,18 +58,19 @@ func NewBaseContext(opts NewBaseContextOpts) *BaseContext {
5658
hasControlledBounds := !opts.HasUncontrolledBounds
5759

5860
return &BaseContext{
59-
kind: opts.Kind,
60-
key: opts.Key,
61-
view: opts.View,
62-
windowName: opts.WindowName,
63-
onGetOptionsMap: opts.OnGetOptionsMap,
64-
focusable: opts.Focusable,
65-
transient: opts.Transient,
66-
hasControlledBounds: hasControlledBounds,
67-
highlightOnFocus: opts.HighlightOnFocus,
68-
needsRerenderOnWidthChange: opts.NeedsRerenderOnWidthChange,
69-
ParentContextMgr: &ParentContextMgr{},
70-
viewTrait: viewTrait,
61+
kind: opts.Kind,
62+
key: opts.Key,
63+
view: opts.View,
64+
windowName: opts.WindowName,
65+
onGetOptionsMap: opts.OnGetOptionsMap,
66+
focusable: opts.Focusable,
67+
transient: opts.Transient,
68+
hasControlledBounds: hasControlledBounds,
69+
highlightOnFocus: opts.HighlightOnFocus,
70+
needsRerenderOnWidthChange: opts.NeedsRerenderOnWidthChange,
71+
needsRerenderOnHeightChange: opts.NeedsRerenderOnHeightChange,
72+
ParentContextMgr: &ParentContextMgr{},
73+
viewTrait: viewTrait,
7174
}
7275
}
7376

@@ -197,6 +200,10 @@ func (self *BaseContext) NeedsRerenderOnWidthChange() bool {
197200
return self.needsRerenderOnWidthChange
198201
}
199202

203+
func (self *BaseContext) NeedsRerenderOnHeightChange() bool {
204+
return self.needsRerenderOnHeightChange
205+
}
206+
200207
func (self *BaseContext) Title() string {
201208
return ""
202209
}

pkg/gui/context/list_context_trait.go

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,18 @@ type ListContextTrait struct {
1818
// we should find out exactly which lines are now part of the path and refresh those.
1919
// We should also keep track of the previous path and refresh those lines too.
2020
refreshViewportOnChange bool
21+
// If this is true, we only render the visible lines of the list. Useful for lists that can
22+
// get very long, because it can save a lot of memory
23+
renderOnlyVisibleLines bool
2124
}
2225

2326
func (self *ListContextTrait) IsListContext() {}
2427

2528
func (self *ListContextTrait) FocusLine() {
2629
// Doing this at the end of the layout function because we need the view to be
2730
// resized before we focus the line, otherwise if we're in accordion mode
28-
// the view could be squashed and won't how to adjust the cursor/origin
31+
// the view could be squashed and won't how to adjust the cursor/origin.
32+
// Also, refreshing the viewport needs to happen after the view has been resized.
2933
self.c.AfterLayout(func() error {
3034
oldOrigin, _ := self.GetViewTrait().ViewPortYBounds()
3135

@@ -40,22 +44,18 @@ func (self *ListContextTrait) FocusLine() {
4044
self.GetViewTrait().CancelRangeSelect()
4145
}
4246

43-
// If FocusPoint() caused the view to scroll (because the selected line
44-
// was out of view before), we need to rerender the view port again.
45-
// This can happen when pressing , or . to scroll by pages, or < or > to
46-
// jump to the top or bottom.
47-
newOrigin, _ := self.GetViewTrait().ViewPortYBounds()
48-
if self.refreshViewportOnChange && oldOrigin != newOrigin {
47+
if self.refreshViewportOnChange {
4948
self.refreshViewport()
49+
} else if self.renderOnlyVisibleLines {
50+
newOrigin, _ := self.GetViewTrait().ViewPortYBounds()
51+
if oldOrigin != newOrigin {
52+
return self.HandleRender()
53+
}
5054
}
5155
return nil
5256
})
5357

5458
self.setFooter()
55-
56-
if self.refreshViewportOnChange {
57-
self.refreshViewport()
58-
}
5959
}
6060

6161
func (self *ListContextTrait) refreshViewport() {
@@ -93,8 +93,21 @@ func (self *ListContextTrait) HandleFocusLost(opts types.OnFocusLostOpts) error
9393
// OnFocus assumes that the content of the context has already been rendered to the view. OnRender is the function which actually renders the content to the view
9494
func (self *ListContextTrait) HandleRender() error {
9595
self.list.ClampSelection()
96-
content := self.renderLines(-1, -1)
97-
self.GetViewTrait().SetContent(content)
96+
if self.renderOnlyVisibleLines {
97+
// Rendering only the visible area can save a lot of cell memory for
98+
// those views that support it.
99+
totalLength := self.list.Len()
100+
if self.getNonModelItems != nil {
101+
totalLength += len(self.getNonModelItems())
102+
}
103+
self.GetViewTrait().SetContentLineCount(totalLength)
104+
startIdx, length := self.GetViewTrait().ViewPortYBounds()
105+
content := self.renderLines(startIdx, startIdx+length)
106+
self.GetViewTrait().SetViewPortContentAndClearEverythingElse(content)
107+
} else {
108+
content := self.renderLines(-1, -1)
109+
self.GetViewTrait().SetContent(content)
110+
}
98111
self.c.Render()
99112
self.setFooter()
100113

@@ -123,3 +136,7 @@ func (self *ListContextTrait) IsItemVisible(item types.HasUrn) bool {
123136
func (self *ListContextTrait) RangeSelectEnabled() bool {
124137
return true
125138
}
139+
140+
func (self *ListContextTrait) RenderOnlyVisibleLines() bool {
141+
return self.renderOnlyVisibleLines
142+
}

pkg/gui/context/local_commits_context.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -72,19 +72,21 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
7272
SearchTrait: NewSearchTrait(c),
7373
ListContextTrait: &ListContextTrait{
7474
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
75-
View: c.Views().Commits,
76-
WindowName: "commits",
77-
Key: LOCAL_COMMITS_CONTEXT_KEY,
78-
Kind: types.SIDE_CONTEXT,
79-
Focusable: true,
80-
NeedsRerenderOnWidthChange: true,
75+
View: c.Views().Commits,
76+
WindowName: "commits",
77+
Key: LOCAL_COMMITS_CONTEXT_KEY,
78+
Kind: types.SIDE_CONTEXT,
79+
Focusable: true,
80+
NeedsRerenderOnWidthChange: true,
81+
NeedsRerenderOnHeightChange: true,
8182
})),
8283
ListRenderer: ListRenderer{
8384
list: viewModel,
8485
getDisplayStrings: getDisplayStrings,
8586
},
8687
c: c,
8788
refreshViewportOnChange: true,
89+
renderOnlyVisibleLines: true,
8890
},
8991
}
9092

pkg/gui/context/remote_branches_context.go

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,14 @@ func NewRemoteBranchesContext(
3737
DynamicTitleBuilder: NewDynamicTitleBuilder(c.Tr.RemoteBranchesDynamicTitle),
3838
ListContextTrait: &ListContextTrait{
3939
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
40-
View: c.Views().RemoteBranches,
41-
WindowName: "branches",
42-
Key: REMOTE_BRANCHES_CONTEXT_KEY,
43-
Kind: types.SIDE_CONTEXT,
44-
Focusable: true,
45-
Transient: true,
46-
NeedsRerenderOnWidthChange: true,
40+
View: c.Views().RemoteBranches,
41+
WindowName: "branches",
42+
Key: REMOTE_BRANCHES_CONTEXT_KEY,
43+
Kind: types.SIDE_CONTEXT,
44+
Focusable: true,
45+
Transient: true,
46+
NeedsRerenderOnWidthChange: true,
47+
NeedsRerenderOnHeightChange: true,
4748
})),
4849
ListRenderer: ListRenderer{
4950
list: viewModel,

pkg/gui/context/sub_commits_context.go

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,14 @@ func NewSubCommitsContext(
115115
DynamicTitleBuilder: NewDynamicTitleBuilder(c.Tr.SubCommitsDynamicTitle),
116116
ListContextTrait: &ListContextTrait{
117117
Context: NewSimpleContext(NewBaseContext(NewBaseContextOpts{
118-
View: c.Views().SubCommits,
119-
WindowName: "branches",
120-
Key: SUB_COMMITS_CONTEXT_KEY,
121-
Kind: types.SIDE_CONTEXT,
122-
Focusable: true,
123-
Transient: true,
124-
NeedsRerenderOnWidthChange: true,
118+
View: c.Views().SubCommits,
119+
WindowName: "branches",
120+
Key: SUB_COMMITS_CONTEXT_KEY,
121+
Kind: types.SIDE_CONTEXT,
122+
Focusable: true,
123+
Transient: true,
124+
NeedsRerenderOnWidthChange: true,
125+
NeedsRerenderOnHeightChange: true,
125126
})),
126127
ListRenderer: ListRenderer{
127128
list: viewModel,
@@ -130,6 +131,7 @@ func NewSubCommitsContext(
130131
},
131132
c: c,
132133
refreshViewportOnChange: true,
134+
renderOnlyVisibleLines: true,
133135
},
134136
}
135137

pkg/gui/context/view_trait.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ func (self *ViewTrait) SetViewPortContent(content string) {
3434
self.view.OverwriteLines(y, content)
3535
}
3636

37+
func (self *ViewTrait) SetViewPortContentAndClearEverythingElse(content string) {
38+
_, y := self.view.Origin()
39+
self.view.OverwriteLinesAndClearEverythingElse(y, content)
40+
}
41+
42+
func (self *ViewTrait) SetContentLineCount(lineCount int) {
43+
self.view.SetContentLineCount(lineCount)
44+
}
45+
3746
func (self *ViewTrait) SetContent(content string) {
3847
self.view.SetContent(content)
3948
}

pkg/gui/controllers/list_controller.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,19 @@ func (self *ListController) HandleScrollRight() error {
5353
func (self *ListController) HandleScrollUp() error {
5454
scrollHeight := self.c.UserConfig.Gui.ScrollHeight
5555
self.context.GetViewTrait().ScrollUp(scrollHeight)
56+
if self.context.RenderOnlyVisibleLines() {
57+
return self.context.HandleRender()
58+
}
5659

5760
return nil
5861
}
5962

6063
func (self *ListController) HandleScrollDown() error {
6164
scrollHeight := self.c.UserConfig.Gui.ScrollHeight
6265
self.context.GetViewTrait().ScrollDown(scrollHeight)
66+
if self.context.RenderOnlyVisibleLines() {
67+
return self.context.HandleRender()
68+
}
6369

6470
return nil
6571
}

0 commit comments

Comments
 (0)