Skip to content

Commit

Permalink
fix: resolve performance issues (#140)
Browse files Browse the repository at this point in the history
  • Loading branch information
leg100 authored Dec 24, 2024
1 parent 6705dd1 commit 30a3793
Show file tree
Hide file tree
Showing 10 changed files with 100 additions and 46 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ Pug automatically loads variables from a .tfvars file. It looks for a file named

### Explorer

The explorer pane a tree of [modules](#module) and [workspaces](#workspace) discovered on your filesystem. From here, terraform commands can be carried out on both modules and workspaces.
The explorer pane shows a tree of [modules](#module) and [workspaces](#workspace) discovered on your filesystem. From here, terraform commands can be carried out on both modules and workspaces.

You can select multiple modules or workspaces; you cannot select a combination of the two. Any terraform commands are then carried out on the selection.

Expand Down
2 changes: 1 addition & 1 deletion internal/pubsub/broker.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/leg100/pug/internal/resource"
)

const bufferSize = 1024
const bufferSize = 1024 * 1024

type Logger interface {
Debug(msg string, args ...any)
Expand Down
6 changes: 3 additions & 3 deletions internal/tui/border.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,8 @@ func borderize(content string, active bool, embeddedText map[BorderPosition]stri
// Add the corners
return style.Render(leftCorner) + s + style.Render(rightCorner)
}
// Stack top border onto remaining borders
return lipgloss.JoinVertical(lipgloss.Top,
// Stack top border, content and horizontal borders, and bottom border.
return strings.Join([]string{
buildHorizontalBorder(
embeddedText[TopLeftBorder],
embeddedText[TopMiddleBorder],
Expand All @@ -111,5 +111,5 @@ func borderize(content string, active bool, embeddedText map[BorderPosition]stri
border.Bottom,
border.BottomRight,
),
)
}, "\n")
}
5 changes: 4 additions & 1 deletion internal/tui/explorer/messages.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
package explorer

type builtTreeMsg *tree
type builtTreeMsg struct {
tree *tree
rendered string
}
59 changes: 39 additions & 20 deletions internal/tui/explorer/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import (
"github.com/charmbracelet/bubbles/textinput"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
lgtree "github.com/charmbracelet/lipgloss/tree"
"github.com/leg100/pug/internal"
"github.com/leg100/pug/internal/module"
"github.com/leg100/pug/internal/resource"
Expand All @@ -34,16 +33,17 @@ func (mm *Maker) Make(id resource.ID, width, height int) (tui.ChildModel, error)
moduleService: mm.ModuleService,
workspaceService: mm.WorkspaceService,
}
tree := builder.newTree("")
filter := textinput.New()
filter.Prompt = "Filter: "
tree, lgtree := builder.newTree("")
m := &model{
Helpers: mm.Helpers,
Workdir: mm.Workdir,
treeBuilder: builder,
tree: tree,
tracker: newTracker(tree, height),
filter: filter,
Helpers: mm.Helpers,
Workdir: mm.Workdir,
treeBuilder: builder,
tree: tree,
renderedTree: lgtree,
tracker: newTracker(tree, height),
filter: filter,
}
m.common = &tui.ActionHandler{
Helpers: mm.Helpers,
Expand All @@ -61,11 +61,21 @@ type model struct {
treeBuilder *treeBuilder
tree *tree
tracker *tracker
renderedTree string
width, height int
filter textinput.Model
status buildTreeStatus
}

func (m model) Init() tea.Cmd {
type buildTreeStatus int

const (
notBuildingTree buildTreeStatus = iota
buildingTree
queueBuildTree
)

func (m *model) Init() tea.Cmd {
return tea.Batch(
m.buildTree,
reload(true, m.Modules),
Expand Down Expand Up @@ -136,10 +146,15 @@ func (m *model) Update(msg tea.Msg) tea.Cmd {
return m.common.Update(msg)
}
case builtTreeMsg:
m.tree = (*tree)(msg)
m.tree = msg.tree
m.renderedTree = msg.rendered
// TODO: perform this in a cmd
m.tracker.reindex(m.tree, m.treeHeight())
return nil
if m.status == queueBuildTree {
return m.buildTree
} else {
m.status = notBuildingTree
}
case resource.Event[*module.Module]:
return m.buildTree
case resource.Event[*workspace.Workspace]:
Expand Down Expand Up @@ -209,12 +224,7 @@ func (m model) View() string {
Width(m.width - tui.ScrollbarWidth).
MaxWidth(m.width - tui.ScrollbarWidth).
Inline(true)
to := lgtree.New().
Enumerator(enumerator).
Indenter(indentor)
m.tree.render(true, to)
s := to.String()
lines := strings.Split(s, "\n")
lines := strings.Split(m.renderedTree, "\n")
numVisibleLines := clamp(m.treeHeight(), 0, len(lines))
visibleLines := lines[m.tracker.start : m.tracker.start+numVisibleLines]
for i := range visibleLines {
Expand Down Expand Up @@ -273,9 +283,18 @@ func (m model) BorderText() map[tui.BorderPosition]string {
}
}

func (m model) buildTree() tea.Msg {
tree := m.treeBuilder.newTree(m.filter.Value())
return builtTreeMsg(tree)
func (m *model) buildTree() tea.Msg {
switch m.status {
case notBuildingTree:
tree, rendered := m.treeBuilder.newTree(m.filter.Value())
return builtTreeMsg{
tree: tree,
rendered: rendered,
}
case buildingTree:
m.status = queueBuildTree
}
return nil
}

func (m model) filterVisible() bool {
Expand Down
23 changes: 18 additions & 5 deletions internal/tui/explorer/nodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import (

type node interface {
fmt.Stringer

// ID uniquely identifies the node
ID() any
// Value returns a value for lexicographic sorting
Value() string
}

type dirNode struct {
Expand All @@ -26,14 +27,18 @@ func (d dirNode) ID() any {
return d.path
}

func (d dirNode) String() string {
func (d dirNode) Value() string {
if d.root {
return fmt.Sprintf("%s %s", tui.DirIcon, d.path)
return d.path
} else {
return fmt.Sprintf("%s %s", tui.DirIcon, filepath.Base(d.path))
return filepath.Base(d.path)
}
}

func (d dirNode) String() string {
return fmt.Sprintf("%s %s", tui.DirIcon, d.Value())
}

type moduleNode struct {
id resource.ID
path string
Expand All @@ -43,8 +48,12 @@ func (m moduleNode) ID() any {
return m.id
}

func (m moduleNode) Value() string {
return filepath.Base(m.path)
}

func (m moduleNode) String() string {
return tui.ModulePathWithIcon(filepath.Base(m.path), false)
return tui.ModulePathWithIcon(m.Value(), false)
}

type workspaceNode struct {
Expand All @@ -59,6 +68,10 @@ func (w workspaceNode) ID() any {
return w.id
}

func (w workspaceNode) Value() string {
return w.name
}

func (w workspaceNode) String() string {
name := lipgloss.NewStyle().
Render(w.name)
Expand Down
29 changes: 25 additions & 4 deletions internal/tui/explorer/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type treeBuilderHelpers interface {
WorkspaceCost(ws *workspace.Workspace) string
}

func (b *treeBuilder) newTree(filter string) *tree {
func (b *treeBuilder) newTree(filter string) (*tree, string) {
t := &tree{
value: dirNode{root: true, path: b.wd.PrettyString()},
}
Expand Down Expand Up @@ -78,7 +78,16 @@ func (b *treeBuilder) newTree(filter string) *tree {
modTree.addChild(ws)
}
}
return t.filter(filter)
// Apply filter if there is one.
filtered := t.filter(filter)
// Now render the corresponding lipgloss tree. We do this here rather than
// in View() because it's quite expensive and thus best kept out of the
// bubble event loop.
to := lgtree.New().
Enumerator(enumerator).
Indenter(indentor)
filtered.render(true, to)
return filtered, to.String()
}

func (t *tree) filter(text string) *tree {
Expand Down Expand Up @@ -133,9 +142,21 @@ func (t *tree) addChild(child node) *tree {
}
newTree := &tree{value: child}
t.children = append(t.children, newTree)
// keep children lexicographically ordered
// keep children ordered
slices.SortFunc(t.children, func(a, b *tree) int {
if internal.StripAnsi(a.value.String()) < internal.StripAnsi(b.value.String()) {
// directories come before modules
if _, ok := a.value.(dirNode); ok {
if _, ok := b.value.(moduleNode); ok {
return -1
}
}
if _, ok := a.value.(moduleNode); ok {
if _, ok := b.value.(dirNode); ok {
return 1
}
}
// otherwise order lexicographically.
if a.value.Value() < b.value.Value() {
return -1
}
return 1
Expand Down
2 changes: 1 addition & 1 deletion internal/tui/explorer/tree_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func TestTree(t *testing.T) {
helpers: &fakeTreeBuilderHelpers{},
}

got := builder.newTree("")
got, _ := builder.newTree("")

want := &tree{
value: dirNode{path: builder.wd.String(), root: true},
Expand Down
2 changes: 1 addition & 1 deletion internal/tui/top/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,7 @@ func (m model) View() string {
Width(m.width).
Render(footer),
)
return lipgloss.JoinVertical(lipgloss.Top, components...)
return strings.Join(components, "\n")
}

var (
Expand Down
16 changes: 7 additions & 9 deletions internal/tui/workspace/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,19 +125,17 @@ func (m *resourceModel) View() string {
}

func (m *resourceModel) BorderText() map[tui.BorderPosition]string {
var tainted string
topLeft := fmt.Sprintf("%s %s",
tui.Bold.Render("resource"),
m.resource,
)
if m.resource.Tainted {
tainted = lipgloss.NewStyle().
topLeft += lipgloss.NewStyle().
Foreground(tui.Red).
Render("(tainted)")
Render(" (tainted)")
}
return map[tui.BorderPosition]string{
tui.TopLeftBorder: fmt.Sprintf(
"%s %s %s",
tui.Bold.Render("resource"),
m.resource,
tainted,
),
tui.TopLeftBorder: topLeft,
}
}

Expand Down

0 comments on commit 30a3793

Please sign in to comment.