summaryrefslogtreecommitdiff
path: root/internal/tui/flamegraph
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-13 14:41:18 +0300
committerPaul Buetow <paul@buetow.org>2026-05-13 14:41:18 +0300
commitd392eebe5bd127e1573734321b0cabaad4182d7c (patch)
treee6e0b38ba26110411d80e00b224640c26b8110ae /internal/tui/flamegraph
parentde6b9c4741dea87ce66e0309bac580030490dc30 (diff)
perf: replace string += concatenation with strings.Builder in TUI render hot paths
Swap out ad-hoc += string concatenation in the flamegraph toolbar/status lines, dashboard filter summary, bubble/treemap status lines, eventstream view, processes tab, and probes list for strings.Builder, eliminating redundant allocations on every render tick. Also update dashboard/model_test.go fake SnapshotSource implementations to match the updated interface signature. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/tui/flamegraph')
-rw-r--r--internal/tui/flamegraph/controls.go27
-rw-r--r--internal/tui/flamegraph/model.go12
-rw-r--r--internal/tui/flamegraph/renderer.go10
3 files changed, 33 insertions, 16 deletions
diff --git a/internal/tui/flamegraph/controls.go b/internal/tui/flamegraph/controls.go
index bd588b3..8ec1051 100644
--- a/internal/tui/flamegraph/controls.go
+++ b/internal/tui/flamegraph/controls.go
@@ -90,21 +90,27 @@ func (m Model) toolbarLine() string {
state = lipgloss.NewStyle().Foreground(common.ColorDanger).Bold(true).Render("[PAUSED]")
}
order := m.currentFieldPresetLabel()
- line := fmt.Sprintf("%s | view:%s | o:order(%s) | b:metric(%s) | /:search | enter/click:zoom | click ancestor:undo | u/esc:undo | r:reset | space:pause", state, compactFramePath(m.currentRootPath()), order, m.countFieldLabel())
+ // Use a Builder to avoid repeated allocations for the optional suffix segments.
+ var b strings.Builder
+ b.WriteString(fmt.Sprintf("%s | view:%s | o:order(%s) | b:metric(%s) | /:search | enter/click:zoom | click ancestor:undo | u/esc:undo | r:reset | space:pause",
+ state, compactFramePath(m.currentRootPath()), order, m.countFieldLabel()))
if m.searchQuery != "" {
- line += " | filter:" + m.searchQuery
+ b.WriteString(" | filter:")
+ b.WriteString(m.searchQuery)
}
if m.statusMessage != "" {
- line += " | " + m.statusMessage
+ b.WriteString(" | ")
+ b.WriteString(m.statusMessage)
}
if flameKeyDebugEnabled && m.lastKeyDebug != "" {
- line += " | " + m.lastKeyDebug
+ b.WriteString(" | ")
+ b.WriteString(m.lastKeyDebug)
}
width := m.width
if width <= 0 {
width = 80
}
- return padOrTrim(line, width)
+ return padOrTrim(b.String(), width)
}
func (m Model) helpOverlay() string {
@@ -148,12 +154,15 @@ func (m Model) selectionStatusLine() string {
shareLabel = fmt.Sprintf("%.2f%% of filtered %s", filterShare, metric)
}
}
- line := fmt.Sprintf("[%s] sel:%d/%d %s | path:%s | depth:%d | total(%s):%d | %s",
- mode, selIdx+1, len(m.frames), frame.Name, compactFramePath(frame.Path), frame.Depth, m.countFieldLabel(), frame.Total, shareLabel)
+ // Use a Builder to avoid a separate allocation for the optional filter suffix.
+ var b strings.Builder
+ b.WriteString(fmt.Sprintf("[%s] sel:%d/%d %s | path:%s | depth:%d | total(%s):%d | %s",
+ mode, selIdx+1, len(m.frames), frame.Name, compactFramePath(frame.Path), frame.Depth, m.countFieldLabel(), frame.Total, shareLabel))
if m.searchQuery != "" {
- line += " | filter:" + m.searchQuery
+ b.WriteString(" | filter:")
+ b.WriteString(m.searchQuery)
}
- return common.HelpBarStyle.Width(width).Render(padOrTrim(line, width))
+ return common.HelpBarStyle.Width(width).Render(padOrTrim(b.String(), width))
}
func (m Model) currentFieldPresetLabel() string {
diff --git a/internal/tui/flamegraph/model.go b/internal/tui/flamegraph/model.go
index c188323..3c63c3e 100644
--- a/internal/tui/flamegraph/model.go
+++ b/internal/tui/flamegraph/model.go
@@ -506,11 +506,17 @@ func (m Model) renderViewContent() string {
if m.snapshot != nil && len(m.frames) == 0 {
content = common.PanelStyle.Render(fmt.Sprintf("Flame: snapshot v%d has no visible frames", m.lastVersion))
}
- content += "\n" + m.selectionStatusLine()
+ // Assemble the final output using a Builder to avoid repeated string copies
+ // for the optional help-overlay suffix.
+ var b strings.Builder
+ b.WriteString(content)
+ b.WriteString("\n")
+ b.WriteString(m.selectionStatusLine())
if m.showHelp {
- content += "\n" + m.helpOverlay()
+ b.WriteString("\n")
+ b.WriteString(m.helpOverlay())
}
- return content
+ return b.String()
}
// currentViewCacheKey snapshots every Model field that influences View()
diff --git a/internal/tui/flamegraph/renderer.go b/internal/tui/flamegraph/renderer.go
index 24b99ed..361feb2 100644
--- a/internal/tui/flamegraph/renderer.go
+++ b/internal/tui/flamegraph/renderer.go
@@ -246,14 +246,16 @@ func computeRenderParams(frames []tuiFrame, height int) renderViewParams {
// buildToolbar assembles the top-of-view toolbar string and pads/trims it to
// width. The toolbar is replaced by the caller via replaceHeaderLine.
+// A Builder is used to avoid an extra allocation for the optional truncation suffix.
func buildToolbar(frames []tuiFrame, width int, params renderViewParams) string {
viewPath := compactFramePath(frames[0].Path)
- toolbar := fmt.Sprintf("Flame | view:%s | frames:%d | rows:%d",
- viewPath, params.visibleFrames, params.availableRows)
+ var b strings.Builder
+ b.WriteString(fmt.Sprintf("Flame | view:%s | frames:%d | rows:%d",
+ viewPath, params.visibleFrames, params.availableRows))
if params.truncated {
- toolbar += " | showing deepest levels"
+ b.WriteString(" | showing deepest levels")
}
- return padOrTrim(toolbar, width)
+ return padOrTrim(b.String(), width)
}
// buildFilteredStatus builds the per-selection status line when a search filter