diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-13 14:41:18 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-13 14:41:18 +0300 |
| commit | d392eebe5bd127e1573734321b0cabaad4182d7c (patch) | |
| tree | e6e0b38ba26110411d80e00b224640c26b8110ae /internal/tui/flamegraph | |
| parent | de6b9c4741dea87ce66e0309bac580030490dc30 (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.go | 27 | ||||
| -rw-r--r-- | internal/tui/flamegraph/model.go | 12 | ||||
| -rw-r--r-- | internal/tui/flamegraph/renderer.go | 10 |
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 |
