summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/tui/dashboard/bubbles.go13
-rw-r--r--internal/tui/dashboard/model.go27
-rw-r--r--internal/tui/dashboard/model_test.go8
-rw-r--r--internal/tui/dashboard/processes.go6
-rw-r--r--internal/tui/dashboard/tabs.go7
-rw-r--r--internal/tui/dashboard/treemap.go12
-rw-r--r--internal/tui/eventstream/model.go12
-rw-r--r--internal/tui/flamegraph/controls.go27
-rw-r--r--internal/tui/flamegraph/model.go12
-rw-r--r--internal/tui/flamegraph/renderer.go10
-rw-r--r--internal/tui/help.go14
-rw-r--r--internal/tui/probes/model.go10
12 files changed, 110 insertions, 48 deletions
diff --git a/internal/tui/dashboard/bubbles.go b/internal/tui/dashboard/bubbles.go
index f4fa6d5..8f9b745 100644
--- a/internal/tui/dashboard/bubbles.go
+++ b/internal/tui/dashboard/bubbles.go
@@ -542,14 +542,19 @@ func (c *bubbleChart) statusLine(width int) string {
}
node := c.nodes[c.selected]
metricText := fmt.Sprintf("%s=%s", c.metricLabel(), c.formatMetricValue(node))
- base := fmt.Sprintf("sel:%d/%d %s | %s | bytes=%s", c.selected+1, len(c.nodes), node.Label, metricText, formatBytes(float64(node.Bytes)))
+ // Use a Builder to avoid extra allocations for the optional hint/detail suffixes
+ // that are appended conditionally on every render.
+ var b strings.Builder
+ b.WriteString(fmt.Sprintf("sel:%d/%d %s | %s | bytes=%s", c.selected+1, len(c.nodes), node.Label, metricText, formatBytes(float64(node.Bytes))))
if c.statusHint != "" {
- base += " | " + c.statusHint
+ b.WriteString(" | ")
+ b.WriteString(c.statusHint)
}
if node.Detail != "" {
- base += " | " + node.Detail
+ b.WriteString(" | ")
+ b.WriteString(node.Detail)
}
- return padOrTrim(base, width)
+ return padOrTrim(b.String(), width)
}
func (c *bubbleChart) metricLabel() string {
diff --git a/internal/tui/dashboard/model.go b/internal/tui/dashboard/model.go
index 2535a90..8a3c5d4 100644
--- a/internal/tui/dashboard/model.go
+++ b/internal/tui/dashboard/model.go
@@ -27,14 +27,18 @@ const dashboardHelpHintRows = 1
const dashboardExpandedHelpRows = 2
const dashboardTabBarRows = 1
-// SnapshotSource is the dashboard data source.
+// SnapshotSource is the dashboard data source. Snapshot returns nil, nil when
+// the engine is nil. A non-nil error indicates that snapshot construction
+// failed and the caller should discard the result.
type SnapshotSource interface {
- Snapshot() *statsengine.Snapshot
+ Snapshot() (*statsengine.Snapshot, error)
}
+// resettableSnapshotSource extends SnapshotSource with a Reset method that
+// clears accumulated state and restarts the series baselines.
type resettableSnapshotSource interface {
Reset()
- Snapshot() *statsengine.Snapshot
+ Snapshot() (*statsengine.Snapshot, error)
}
type refreshTickMsg struct{}
@@ -1105,15 +1109,22 @@ func (m Model) View() tea.View {
}
func (m Model) filterSummary() string {
- summary := "filter: " + presenter.FilterSummary(m.globalFilter)
+ // Use a Builder to avoid repeated string copies for the optional suffix segments
+ // (filter stack, recording status, auto-reset label) on every render tick.
+ var b strings.Builder
+ b.WriteString("filter: ")
+ b.WriteString(presenter.FilterSummary(m.globalFilter))
if len(m.filterStack) > 0 {
- summary += " | stack: " + strings.Join(m.filterStack, " | ")
+ b.WriteString(" | stack: ")
+ b.WriteString(strings.Join(m.filterStack, " | "))
}
if m.recordingStatus != "" {
- summary += " | " + m.recordingStatus
+ b.WriteString(" | ")
+ b.WriteString(m.recordingStatus)
}
- summary += " | " + m.autoResetStatus()
- return summary
+ b.WriteString(" | ")
+ b.WriteString(m.autoResetStatus())
+ return b.String()
}
// autoResetStatus is the human-readable label for the current
diff --git a/internal/tui/dashboard/model_test.go b/internal/tui/dashboard/model_test.go
index 4ca10c9..59c2155 100644
--- a/internal/tui/dashboard/model_test.go
+++ b/internal/tui/dashboard/model_test.go
@@ -24,9 +24,9 @@ type fakeSnapshotSource struct {
snap *statsengine.Snapshot
}
-func (f *fakeSnapshotSource) Snapshot() *statsengine.Snapshot {
+func (f *fakeSnapshotSource) Snapshot() (*statsengine.Snapshot, error) {
f.snapshots++
- return f.snap
+ return f.snap, nil
}
type fakeResettableSnapshotSource struct {
@@ -39,9 +39,9 @@ func (f *fakeResettableSnapshotSource) Reset() {
f.resetCount++
}
-func (f *fakeResettableSnapshotSource) Snapshot() *statsengine.Snapshot {
+func (f *fakeResettableSnapshotSource) Snapshot() (*statsengine.Snapshot, error) {
f.snapCount++
- return f.snap
+ return f.snap, nil
}
func stripANSIEscape(value string) string {
diff --git a/internal/tui/dashboard/processes.go b/internal/tui/dashboard/processes.go
index 34fdbc8..f4eedec 100644
--- a/internal/tui/dashboard/processes.go
+++ b/internal/tui/dashboard/processes.go
@@ -42,7 +42,11 @@ func renderProcessesWithSort(snap *statsengine.Snapshot, width, height, offset,
columns := processColumns()
out := renderSelectableTable(columns, rows, height, offset, selectedCol, "enter:filter", "s/S:sort", processSortHint(sortState), "v:mode", "b:metric")
if pidFilter > 0 {
- out += "\n" + "Note: this tab is most useful with All PIDs."
+ // Use a Builder to avoid an extra allocation for the PID-filter note suffix.
+ var b strings.Builder
+ b.WriteString(out)
+ b.WriteString("\nNote: this tab is most useful with All PIDs.")
+ return b.String()
}
return out
}
diff --git a/internal/tui/dashboard/tabs.go b/internal/tui/dashboard/tabs.go
index 0e9d924..8fc5132 100644
--- a/internal/tui/dashboard/tabs.go
+++ b/internal/tui/dashboard/tabs.go
@@ -228,7 +228,12 @@ func renderTabBarPlain(active Tab, width int) string {
text = truncatePlain(text, width)
padding := width - utf8.RuneCountInString(text)
if padding > 0 {
- text += strings.Repeat(" ", padding)
+ // Use a Builder to avoid a redundant allocation when right-padding to width.
+ var b strings.Builder
+ b.Grow(len(text) + padding)
+ b.WriteString(text)
+ b.WriteString(strings.Repeat(" ", padding))
+ return b.String()
}
}
return text
diff --git a/internal/tui/dashboard/treemap.go b/internal/tui/dashboard/treemap.go
index 03c2917..4d5486a 100644
--- a/internal/tui/dashboard/treemap.go
+++ b/internal/tui/dashboard/treemap.go
@@ -438,7 +438,10 @@ func treemapStatusLine(items []syscallTreemapItem, selected int, metric bubbleMe
default:
metricText = fmt.Sprintf("%d", item.Count)
}
- status := fmt.Sprintf(
+ // Use a Builder to avoid a redundant allocation for the optional detail suffix
+ // appended conditionally on every render call.
+ var b strings.Builder
+ b.WriteString(fmt.Sprintf(
"sel:%d/%d %s | %s=%s | bytes=%s",
selected+1,
len(items),
@@ -446,11 +449,12 @@ func treemapStatusLine(items []syscallTreemapItem, selected int, metric bubbleMe
treemapMetricLabel(metric),
metricText,
formatBytes(float64(item.Bytes)),
- )
+ ))
if detail := strings.TrimSpace(item.Detail); detail != "" {
- status += " | " + detail
+ b.WriteString(" | ")
+ b.WriteString(detail)
}
- return status
+ return b.String()
}
func treemapMetricLabel(metric bubbleMetric) string {
diff --git a/internal/tui/eventstream/model.go b/internal/tui/eventstream/model.go
index 2780524..a8f399c 100644
--- a/internal/tui/eventstream/model.go
+++ b/internal/tui/eventstream/model.go
@@ -500,9 +500,15 @@ func (m *Model) View(width, height int) string {
if m.paused && m.selectedIdx >= 0 {
status = fmt.Sprintf("Row %d/%d | Sel %d/%d Col %d/%d | Enter push-filter | Esc/F undo", rowNumber(start, len(m.filtered)), len(m.filtered), rowNumber(m.selectedIdx, len(m.filtered)), len(m.filtered), m.selectedCol+1, streamColumnCount)
}
- out := base + "\n" + status
+ // Use a Builder to avoid a redundant allocation for the optional status-message
+ // line appended conditionally on every render call.
+ var b strings.Builder
+ b.WriteString(base)
+ b.WriteString("\n")
+ b.WriteString(status)
if m.statusMessage != "" {
- out += "\n" + m.statusMessage
+ b.WriteString("\n")
+ b.WriteString(m.statusMessage)
}
if m.exportModal.Visible() {
@@ -511,7 +517,7 @@ func (m *Model) View(width, height int) string {
if m.searchModal.Visible() {
return m.searchModal.View(width, height)
}
- return out
+ return b.String()
}
func (m *Model) Refresh() {
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
diff --git a/internal/tui/help.go b/internal/tui/help.go
index ba4ed02..5a343cb 100644
--- a/internal/tui/help.go
+++ b/internal/tui/help.go
@@ -47,12 +47,18 @@ type helpSection struct {
}
func (m Model) helpSections() []helpSection {
+ line1 := "f filter p pid picker t tid picker o probes R parquet rec"
+ if help := m.keys.Export.Help(); help.Key != "" || help.Desc != "" {
+ // Use a Builder to append the optional export hint without reallocating
+ // the base string on each render when help is visible.
+ var b strings.Builder
+ b.WriteString(line1)
+ b.WriteString(" e stream export")
+ line1 = b.String()
+ }
globalLines := []string{
"H help esc/? close help q quit",
- "f filter p pid picker t tid picker o probes R parquet rec",
- }
- if help := m.keys.Export.Help(); help.Key != "" || help.Desc != "" {
- globalLines[1] += " e stream export"
+ line1,
}
return []helpSection{
diff --git a/internal/tui/probes/model.go b/internal/tui/probes/model.go
index b7e694f..986b697 100644
--- a/internal/tui/probes/model.go
+++ b/internal/tui/probes/model.go
@@ -276,11 +276,15 @@ func (m Model) View(width, height int) string {
if p.Active {
check = "[x]"
}
- line := fmt.Sprintf("%s%s %-24s", prefix, check, p.Syscall)
+ // Use a Builder to avoid an extra allocation for the optional error suffix
+ // emitted per probe row on every render call.
+ var lb strings.Builder
+ lb.WriteString(fmt.Sprintf("%s%s %-24s", prefix, check, p.Syscall))
if p.Error != "" {
- line += " ! " + truncateText(sanitizeOneLine(p.Error), 28)
+ lb.WriteString(" ! ")
+ lb.WriteString(truncateText(sanitizeOneLine(p.Error), 28))
}
- lines = append(lines, line)
+ lines = append(lines, lb.String())
}
if len(items) == 0 {
lines = append(lines, " (no probes)")