mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-04-25 12:24:47 +02:00
Improve render performance (#3686)
- **PR Description** Fix a performance regression that I introduced with v0.41: when entering or leaving staging mode for a file, or when switching from a file that has only unstaged changes to one that has both staged and unstaged changes, there was a noticeable lag of about 500ms on my machine. With the improvements in this PR we get this back down to about 20ms.
This commit is contained in:
commit
cf40a5b077
@ -23,7 +23,7 @@ type BaseContext struct {
|
|||||||
focusable bool
|
focusable bool
|
||||||
transient bool
|
transient bool
|
||||||
hasControlledBounds bool
|
hasControlledBounds bool
|
||||||
needsRerenderOnWidthChange bool
|
needsRerenderOnWidthChange types.NeedsRerenderOnWidthChangeLevel
|
||||||
needsRerenderOnHeightChange bool
|
needsRerenderOnHeightChange bool
|
||||||
highlightOnFocus bool
|
highlightOnFocus bool
|
||||||
|
|
||||||
@ -46,7 +46,7 @@ type NewBaseContextOpts struct {
|
|||||||
Transient bool
|
Transient bool
|
||||||
HasUncontrolledBounds bool // negating for the sake of making false the default
|
HasUncontrolledBounds bool // negating for the sake of making false the default
|
||||||
HighlightOnFocus bool
|
HighlightOnFocus bool
|
||||||
NeedsRerenderOnWidthChange bool
|
NeedsRerenderOnWidthChange types.NeedsRerenderOnWidthChangeLevel
|
||||||
NeedsRerenderOnHeightChange bool
|
NeedsRerenderOnHeightChange bool
|
||||||
|
|
||||||
OnGetOptionsMap func() map[string]string
|
OnGetOptionsMap func() map[string]string
|
||||||
@ -201,7 +201,7 @@ func (self *BaseContext) HasControlledBounds() bool {
|
|||||||
return self.hasControlledBounds
|
return self.hasControlledBounds
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *BaseContext) NeedsRerenderOnWidthChange() bool {
|
func (self *BaseContext) NeedsRerenderOnWidthChange() types.NeedsRerenderOnWidthChangeLevel {
|
||||||
return self.needsRerenderOnWidthChange
|
return self.needsRerenderOnWidthChange
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ func NewBranchesContext(c *ContextCommon) *BranchesContext {
|
|||||||
Key: LOCAL_BRANCHES_CONTEXT_KEY,
|
Key: LOCAL_BRANCHES_CONTEXT_KEY,
|
||||||
Kind: types.SIDE_CONTEXT,
|
Kind: types.SIDE_CONTEXT,
|
||||||
Focusable: true,
|
Focusable: true,
|
||||||
NeedsRerenderOnWidthChange: true,
|
NeedsRerenderOnWidthChange: types.NEEDS_RERENDER_ON_WIDTH_CHANGE_WHEN_WIDTH_CHANGES,
|
||||||
})),
|
})),
|
||||||
ListRenderer: ListRenderer{
|
ListRenderer: ListRenderer{
|
||||||
list: viewModel,
|
list: viewModel,
|
||||||
|
@ -77,7 +77,7 @@ func NewLocalCommitsContext(c *ContextCommon) *LocalCommitsContext {
|
|||||||
Key: LOCAL_COMMITS_CONTEXT_KEY,
|
Key: LOCAL_COMMITS_CONTEXT_KEY,
|
||||||
Kind: types.SIDE_CONTEXT,
|
Kind: types.SIDE_CONTEXT,
|
||||||
Focusable: true,
|
Focusable: true,
|
||||||
NeedsRerenderOnWidthChange: true,
|
NeedsRerenderOnWidthChange: types.NEEDS_RERENDER_ON_WIDTH_CHANGE_WHEN_SCREEN_MODE_CHANGES,
|
||||||
NeedsRerenderOnHeightChange: true,
|
NeedsRerenderOnHeightChange: true,
|
||||||
})),
|
})),
|
||||||
ListRenderer: ListRenderer{
|
ListRenderer: ListRenderer{
|
||||||
|
@ -48,7 +48,7 @@ func NewReflogCommitsContext(c *ContextCommon) *ReflogCommitsContext {
|
|||||||
Key: REFLOG_COMMITS_CONTEXT_KEY,
|
Key: REFLOG_COMMITS_CONTEXT_KEY,
|
||||||
Kind: types.SIDE_CONTEXT,
|
Kind: types.SIDE_CONTEXT,
|
||||||
Focusable: true,
|
Focusable: true,
|
||||||
NeedsRerenderOnWidthChange: true,
|
NeedsRerenderOnWidthChange: types.NEEDS_RERENDER_ON_WIDTH_CHANGE_WHEN_SCREEN_MODE_CHANGES,
|
||||||
})),
|
})),
|
||||||
ListRenderer: ListRenderer{
|
ListRenderer: ListRenderer{
|
||||||
list: viewModel,
|
list: viewModel,
|
||||||
|
@ -43,7 +43,6 @@ func NewRemoteBranchesContext(
|
|||||||
Kind: types.SIDE_CONTEXT,
|
Kind: types.SIDE_CONTEXT,
|
||||||
Focusable: true,
|
Focusable: true,
|
||||||
Transient: true,
|
Transient: true,
|
||||||
NeedsRerenderOnWidthChange: true,
|
|
||||||
NeedsRerenderOnHeightChange: true,
|
NeedsRerenderOnHeightChange: true,
|
||||||
})),
|
})),
|
||||||
ListRenderer: ListRenderer{
|
ListRenderer: ListRenderer{
|
||||||
|
@ -121,7 +121,7 @@ func NewSubCommitsContext(
|
|||||||
Kind: types.SIDE_CONTEXT,
|
Kind: types.SIDE_CONTEXT,
|
||||||
Focusable: true,
|
Focusable: true,
|
||||||
Transient: true,
|
Transient: true,
|
||||||
NeedsRerenderOnWidthChange: true,
|
NeedsRerenderOnWidthChange: types.NEEDS_RERENDER_ON_WIDTH_CHANGE_WHEN_SCREEN_MODE_CHANGES,
|
||||||
NeedsRerenderOnHeightChange: true,
|
NeedsRerenderOnHeightChange: true,
|
||||||
})),
|
})),
|
||||||
ListRenderer: ListRenderer{
|
ListRenderer: ListRenderer{
|
||||||
|
@ -8,7 +8,6 @@ import (
|
|||||||
"github.com/jesseduffield/lazygit/pkg/config"
|
"github.com/jesseduffield/lazygit/pkg/config"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
"github.com/mattn/go-runewidth"
|
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -272,7 +271,7 @@ func infoSectionChildren(args WindowArrangementArgs) []*boxlayout.Box {
|
|||||||
return []*boxlayout.Box{
|
return []*boxlayout.Box{
|
||||||
{
|
{
|
||||||
Window: "searchPrefix",
|
Window: "searchPrefix",
|
||||||
Size: runewidth.StringWidth(args.SearchPrefix),
|
Size: utils.StringWidth(args.SearchPrefix),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
Window: "search",
|
Window: "search",
|
||||||
@ -325,7 +324,7 @@ func infoSectionChildren(args WindowArrangementArgs) []*boxlayout.Box {
|
|||||||
// app status appears very briefly in demos and dislodges the caption,
|
// app status appears very briefly in demos and dislodges the caption,
|
||||||
// so better not to show it at all
|
// so better not to show it at all
|
||||||
if args.AppStatus != "" {
|
if args.AppStatus != "" {
|
||||||
result = append(result, &boxlayout.Box{Window: "appStatus", Size: runewidth.StringWidth(args.AppStatus)})
|
result = append(result, &boxlayout.Box{Window: "appStatus", Size: utils.StringWidth(args.AppStatus)})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -338,7 +337,7 @@ func infoSectionChildren(args WindowArrangementArgs) []*boxlayout.Box {
|
|||||||
&boxlayout.Box{
|
&boxlayout.Box{
|
||||||
Window: "information",
|
Window: "information",
|
||||||
// unlike appStatus, informationStr has various colors so we need to decolorise before taking the length
|
// unlike appStatus, informationStr has various colors so we need to decolorise before taking the length
|
||||||
Size: runewidth.StringWidth(utils.Decolorise(args.InformationStr)),
|
Size: utils.StringWidth(utils.Decolorise(args.InformationStr)),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package controllers
|
package controllers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/jesseduffield/gocui"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -16,7 +17,7 @@ func (self *ScreenModeActions) Next() error {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
return nil
|
return self.rerenderViewsWithScreenModeDependentContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (self *ScreenModeActions) Prev() error {
|
func (self *ScreenModeActions) Prev() error {
|
||||||
@ -27,9 +28,33 @@ func (self *ScreenModeActions) Prev() error {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return self.rerenderViewsWithScreenModeDependentContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// these views need to be re-rendered when the screen mode changes. The commits view,
|
||||||
|
// for example, will show authorship information in half and full screen mode.
|
||||||
|
func (self *ScreenModeActions) rerenderViewsWithScreenModeDependentContent() error {
|
||||||
|
for _, context := range self.c.Context().AllList() {
|
||||||
|
if context.NeedsRerenderOnWidthChange() == types.NEEDS_RERENDER_ON_WIDTH_CHANGE_WHEN_SCREEN_MODE_CHANGES {
|
||||||
|
if err := self.rerenderView(context.GetView()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (self *ScreenModeActions) rerenderView(view *gocui.View) error {
|
||||||
|
context, ok := self.c.Helpers().View.ContextForView(view.Name())
|
||||||
|
if !ok {
|
||||||
|
self.c.Log.Errorf("no context found for view %s", view.Name())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return context.HandleRender()
|
||||||
|
}
|
||||||
|
|
||||||
func nextIntInCycle(sl []types.WindowMaximisation, current types.WindowMaximisation) types.WindowMaximisation {
|
func nextIntInCycle(sl []types.WindowMaximisation, current types.WindowMaximisation) types.WindowMaximisation {
|
||||||
for i, val := range sl {
|
for i, val := range sl {
|
||||||
if val == current {
|
if val == current {
|
||||||
|
@ -6,7 +6,6 @@ import (
|
|||||||
"github.com/jesseduffield/lazygit/pkg/constants"
|
"github.com/jesseduffield/lazygit/pkg/constants"
|
||||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||||
"github.com/mattn/go-runewidth"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (gui *Gui) informationStr() string {
|
func (gui *Gui) informationStr() string {
|
||||||
@ -34,7 +33,7 @@ func (gui *Gui) handleInfoClick() error {
|
|||||||
width, _ := view.Size()
|
width, _ := view.Size()
|
||||||
|
|
||||||
if activeMode, ok := gui.helpers.Mode.GetActiveMode(); ok {
|
if activeMode, ok := gui.helpers.Mode.GetActiveMode(); ok {
|
||||||
if width-cx > runewidth.StringWidth(gui.c.Tr.ResetInParentheses) {
|
if width-cx > utils.StringWidth(gui.c.Tr.ResetInParentheses) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return activeMode.Reset()
|
return activeMode.Reset()
|
||||||
@ -43,10 +42,10 @@ func (gui *Gui) handleInfoClick() error {
|
|||||||
var title, url string
|
var title, url string
|
||||||
|
|
||||||
// if we're not in an active mode we show the donate button
|
// if we're not in an active mode we show the donate button
|
||||||
if cx <= runewidth.StringWidth(gui.c.Tr.Donate) {
|
if cx <= utils.StringWidth(gui.c.Tr.Donate) {
|
||||||
url = constants.Links.Donate
|
url = constants.Links.Donate
|
||||||
title = gui.c.Tr.Donate
|
title = gui.c.Tr.Donate
|
||||||
} else if cx <= runewidth.StringWidth(gui.c.Tr.Donate)+1+runewidth.StringWidth(gui.c.Tr.AskQuestion) {
|
} else if cx <= utils.StringWidth(gui.c.Tr.Donate)+1+utils.StringWidth(gui.c.Tr.AskQuestion) {
|
||||||
url = constants.Links.Discussions
|
url = constants.Links.Discussions
|
||||||
title = gui.c.Tr.AskQuestion
|
title = gui.c.Tr.AskQuestion
|
||||||
}
|
}
|
||||||
|
@ -73,7 +73,7 @@ func (gui *Gui) layout(g *gocui.Gui) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mustRerender := false
|
mustRerender := false
|
||||||
if context.NeedsRerenderOnWidthChange() {
|
if context.NeedsRerenderOnWidthChange() == types.NEEDS_RERENDER_ON_WIDTH_CHANGE_WHEN_WIDTH_CHANGES {
|
||||||
// view.Width() returns the width -1 for some reason
|
// view.Width() returns the width -1 for some reason
|
||||||
oldWidth := view.Width() + 1
|
oldWidth := view.Width() + 1
|
||||||
newWidth := dimensionsObj.X1 - dimensionsObj.X0 + 2*frameOffset
|
newWidth := dimensionsObj.X1 - dimensionsObj.X0 + 2*frameOffset
|
||||||
|
@ -56,7 +56,7 @@ func getBranchDisplayStrings(
|
|||||||
// Recency is always three characters, plus one for the space
|
// Recency is always three characters, plus one for the space
|
||||||
availableWidth := viewWidth - 4
|
availableWidth := viewWidth - 4
|
||||||
if len(branchStatus) > 0 {
|
if len(branchStatus) > 0 {
|
||||||
availableWidth -= runewidth.StringWidth(utils.Decolorise(branchStatus)) + 1
|
availableWidth -= utils.StringWidth(utils.Decolorise(branchStatus)) + 1
|
||||||
}
|
}
|
||||||
if icons.IsIconEnabled() {
|
if icons.IsIconEnabled() {
|
||||||
availableWidth -= 2 // one for the icon, one for the space
|
availableWidth -= 2 // one for the icon, one for the space
|
||||||
@ -65,7 +65,7 @@ func getBranchDisplayStrings(
|
|||||||
availableWidth -= utils.COMMIT_HASH_SHORT_SIZE + 1
|
availableWidth -= utils.COMMIT_HASH_SHORT_SIZE + 1
|
||||||
}
|
}
|
||||||
if checkedOutByWorkTree {
|
if checkedOutByWorkTree {
|
||||||
availableWidth -= runewidth.StringWidth(worktreeIcon) + 1
|
availableWidth -= utils.StringWidth(worktreeIcon) + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
displayName := b.Name
|
displayName := b.Name
|
||||||
@ -79,7 +79,7 @@ func getBranchDisplayStrings(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Don't bother shortening branch names that are already 3 characters or less
|
// Don't bother shortening branch names that are already 3 characters or less
|
||||||
if runewidth.StringWidth(displayName) > max(availableWidth, 3) {
|
if utils.StringWidth(displayName) > max(availableWidth, 3) {
|
||||||
// Never shorten the branch name to less then 3 characters
|
// Never shorten the branch name to less then 3 characters
|
||||||
len := max(availableWidth, 4)
|
len := max(availableWidth, 4)
|
||||||
displayName = runewidth.Truncate(displayName, len, "…")
|
displayName = runewidth.Truncate(displayName, len, "…")
|
||||||
|
@ -39,6 +39,18 @@ type ParentContexter interface {
|
|||||||
GetParentContext() (Context, bool)
|
GetParentContext() (Context, bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NeedsRerenderOnWidthChangeLevel int
|
||||||
|
|
||||||
|
const (
|
||||||
|
// view doesn't render differently when its width changes
|
||||||
|
NEEDS_RERENDER_ON_WIDTH_CHANGE_NONE NeedsRerenderOnWidthChangeLevel = iota
|
||||||
|
// view renders differently when its width changes. An example is a view
|
||||||
|
// that truncates long lines to the view width, e.g. the branches view
|
||||||
|
NEEDS_RERENDER_ON_WIDTH_CHANGE_WHEN_WIDTH_CHANGES
|
||||||
|
// view renders differently only when the screen mode changes
|
||||||
|
NEEDS_RERENDER_ON_WIDTH_CHANGE_WHEN_SCREEN_MODE_CHANGES
|
||||||
|
)
|
||||||
|
|
||||||
type IBaseContext interface {
|
type IBaseContext interface {
|
||||||
HasKeybindings
|
HasKeybindings
|
||||||
ParentContexter
|
ParentContexter
|
||||||
@ -60,8 +72,8 @@ type IBaseContext interface {
|
|||||||
// determined independently.
|
// determined independently.
|
||||||
HasControlledBounds() bool
|
HasControlledBounds() bool
|
||||||
|
|
||||||
// true if the view needs to be rerendered when its width changes
|
// to what extent the view needs to be rerendered when its width changes
|
||||||
NeedsRerenderOnWidthChange() bool
|
NeedsRerenderOnWidthChange() NeedsRerenderOnWidthChangeLevel
|
||||||
|
|
||||||
// true if the view needs to be rerendered when its height changes
|
// true if the view needs to be rerendered when its height changes
|
||||||
NeedsRerenderOnHeightChange() bool
|
NeedsRerenderOnHeightChange() bool
|
||||||
|
@ -3,6 +3,7 @@ package utils
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"github.com/mattn/go-runewidth"
|
"github.com/mattn/go-runewidth"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
@ -21,10 +22,22 @@ type ColumnConfig struct {
|
|||||||
Alignment Alignment
|
Alignment Alignment
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func StringWidth(s string) int {
|
||||||
|
// We are intentionally not using a range loop here, because that would
|
||||||
|
// convert the characters to runes, which is unnecessary work in this case.
|
||||||
|
for i := 0; i < len(s); i++ {
|
||||||
|
if s[i] > unicode.MaxASCII {
|
||||||
|
return runewidth.StringWidth(s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(s)
|
||||||
|
}
|
||||||
|
|
||||||
// WithPadding pads a string as much as you want
|
// WithPadding pads a string as much as you want
|
||||||
func WithPadding(str string, padding int, alignment Alignment) string {
|
func WithPadding(str string, padding int, alignment Alignment) string {
|
||||||
uncoloredStr := Decolorise(str)
|
uncoloredStr := Decolorise(str)
|
||||||
width := runewidth.StringWidth(uncoloredStr)
|
width := StringWidth(uncoloredStr)
|
||||||
if padding < width {
|
if padding < width {
|
||||||
return str
|
return str
|
||||||
}
|
}
|
||||||
@ -144,7 +157,7 @@ func getPadWidths(stringArrays [][]string) []int {
|
|||||||
return MaxFn(stringArrays, func(stringArray []string) int {
|
return MaxFn(stringArrays, func(stringArray []string) int {
|
||||||
uncoloredStr := Decolorise(stringArray[i])
|
uncoloredStr := Decolorise(stringArray[i])
|
||||||
|
|
||||||
return runewidth.StringWidth(uncoloredStr)
|
return StringWidth(uncoloredStr)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -161,7 +174,7 @@ func MaxFn[T any](items []T, fn func(T) int) int {
|
|||||||
|
|
||||||
// TruncateWithEllipsis returns a string, truncated to a certain length, with an ellipsis
|
// TruncateWithEllipsis returns a string, truncated to a certain length, with an ellipsis
|
||||||
func TruncateWithEllipsis(str string, limit int) string {
|
func TruncateWithEllipsis(str string, limit int) string {
|
||||||
if runewidth.StringWidth(str) > limit && limit <= 2 {
|
if StringWidth(str) > limit && limit <= 2 {
|
||||||
return strings.Repeat(".", limit)
|
return strings.Repeat(".", limit)
|
||||||
}
|
}
|
||||||
return runewidth.Truncate(str, limit, "…")
|
return runewidth.Truncate(str, limit, "…")
|
||||||
|
@ -4,6 +4,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mattn/go-runewidth"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -250,3 +251,27 @@ func TestRenderDisplayStrings(t *testing.T) {
|
|||||||
assert.EqualValues(t, test.expectedColumnPositions, columnPositions)
|
assert.EqualValues(t, test.expectedColumnPositions, columnPositions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkStringWidthAsciiOriginal(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
runewidth.StringWidth("some ASCII string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkStringWidthAsciiOptimized(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
StringWidth("some ASCII string")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkStringWidthNonAsciiOriginal(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
runewidth.StringWidth("some non-ASCII string 🍉")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkStringWidthNonAsciiOptimized(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
StringWidth("some non-ASCII string 🍉")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user