summaryrefslogtreecommitdiff
path: root/internal/tui/flamegraph/renderer.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-05-12 22:30:32 +0300
committerPaul Buetow <paul@buetow.org>2026-05-12 22:30:32 +0300
commit235eb7541d6396e860b23aad63ed44c734cdf767 (patch)
tree274d163f00605433174fa89ece82a28d4978c991 /internal/tui/flamegraph/renderer.go
parent8a4cb57703845c1d8ffbc9318a4125818a72a545 (diff)
refactor flamegraph TUI model into focused sub-controllers
Break the god-class Model in internal/tui/flamegraph/model.go into four focused sub-controllers that each own a single concern: - ZoomNavigator : zoom path, stack, root node, and reset/undo logic - SelectionManager: selected frame index, clamp, traversal, navigation - FrameAnimator : spring animation, frame layout swap, ancestry index - SearchController: search query, match indices, filter-visible set All four are embedded in Model so existing field access (m.selectedIdx, m.zoomPath, etc.) is promoted unchanged, keeping tests and callers intact. Model methods now delegate to the sub-controllers rather than holding all logic inline. Also split RenderTerminalView (88 lines) into computeRenderParams, buildToolbar, buildFilteredStatus, and buildNormalStatus helpers, and extracted buildSnapshotMsg from RefreshFromLiveTrieCmd. All functions are now ≤ 50 lines. All tests pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'internal/tui/flamegraph/renderer.go')
-rw-r--r--internal/tui/flamegraph/renderer.go156
1 files changed, 98 insertions, 58 deletions
diff --git a/internal/tui/flamegraph/renderer.go b/internal/tui/flamegraph/renderer.go
index 12e5f8e..8f0d09b 100644
--- a/internal/tui/flamegraph/renderer.go
+++ b/internal/tui/flamegraph/renderer.go
@@ -206,7 +206,96 @@ func semanticFrameColor(name string) (color.Color, bool) {
}
}
+// renderViewParams bundles the pre-computed layout parameters used by
+// RenderTerminalView helpers to avoid threading many individual arguments.
+type renderViewParams struct {
+ rowOffset int
+ maxRow int
+ barHeight int
+ availableRows int
+ visibleFrames int
+ truncated bool
+}
+
+// computeRenderParams derives the row-layout parameters for a given frame set
+// and viewport height.
+func computeRenderParams(frames []tuiFrame, height int) renderViewParams {
+ availableRows := height - 2 // toolbar + frame-status line
+ maxRow := maxFrameRowForSet(frames, nil)
+ totalDepthRows := maxRow + 1
+ barHeight := computeBarHeight(availableRows, totalDepthRows, maxBarVisualHeight)
+ visibleDepthRows := availableRows / barHeight
+ if visibleDepthRows < 1 {
+ visibleDepthRows = 1
+ }
+ rowOffset := 0
+ truncated := false
+ if maxRow+1 > visibleDepthRows {
+ rowOffset = maxRow + 1 - visibleDepthRows
+ truncated = true
+ }
+ return renderViewParams{
+ rowOffset: rowOffset,
+ maxRow: maxRow,
+ barHeight: barHeight,
+ availableRows: availableRows,
+ visibleFrames: countVisibleFrames(frames, nil),
+ truncated: truncated,
+ }
+}
+
+// buildToolbar assembles the top-of-view toolbar string and pads/trims it to
+// width. The toolbar is replaced by the caller via replaceHeaderLine.
+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)
+ if params.truncated {
+ toolbar += " | showing deepest levels"
+ }
+ return padOrTrim(toolbar, width)
+}
+
+// buildFilteredStatus builds the per-selection status line when a search filter
+// is active. The searchQuery is embedded in the status so the user can see
+// which pattern is applied.
+func buildFilteredStatus(frames []tuiFrame, selected tuiFrame, selectedIdx int, matchSet map[int]bool, metricLabel, searchQuery string, globalTotal uint64, visibleFrames int) string {
+ filterCoveredTotal, filterBaseTotal := filterCoverageTotals(frames, matchSet, globalTotal)
+ filterSystemShare := percentOfTotal(filterCoveredTotal, filterBaseTotal)
+ selectedFilterShare := 0.0
+ if filterCoveredTotal > 0 {
+ selectedMatchTotal := filterCoverageTotalForPath(frames, matchSet, selected.Path)
+ selectedFilterShare = percentOfTotal(selectedMatchTotal, filterCoveredTotal)
+ }
+ matches := orderedMatchIndices(matchSet)
+ pos := 0
+ if len(matches) > 0 {
+ if idx := indexOf(matches, selectedIdx); idx >= 0 {
+ pos = idx + 1
+ }
+ }
+ frameCoverage := 0.0
+ if len(frames) > 0 {
+ frameCoverage = 100 * float64(visibleFrames) / float64(len(frames))
+ }
+ return fmt.Sprintf("Filter %q: %.1f%% %s (%d/%d matches, %.1f%% frames shown) | Selected: %s total(%s)=%d depth=%d %.2f%% filtered %s",
+ searchQuery, filterSystemShare, metricLabel, pos, len(matches), frameCoverage,
+ selected.Name, metricLabel, selected.Total, selected.Depth, selectedFilterShare, metricLabel)
+}
+
+// buildNormalStatus builds the per-selection status line when no filter is active.
+func buildNormalStatus(selected tuiFrame, metricLabel string, globalTotal uint64) string {
+ selectedSystemShare := selected.Percent
+ if globalTotal > 0 {
+ selectedSystemShare = percentOfTotal(selected.Total, globalTotal)
+ }
+ return fmt.Sprintf("Selected: %s [%s] total(%s)=%d depth=%d col=%d width=%d share=%.2f%% %s",
+ selected.Name, compactFramePath(selected.Path), metricLabel, selected.Total, selected.Depth, selected.Col, selected.Width, selectedSystemShare, metricLabel)
+}
+
// RenderTerminalView renders a terminal flamegraph viewport from laid out frames.
+// The function is split into helpers (computeRenderParams, buildToolbar,
+// buildFilteredStatus, buildNormalStatus) to keep each piece under 50 lines.
func RenderTerminalView(frames []tuiFrame, width, height, selectedIdx int, subtreeSet, matchSet, filterSet map[int]bool, globalTotal uint64, metricLabel string, isDark, searchActive bool, searchQuery string) string {
if width < minFlameWidth {
return common.PanelStyle.Render("Flame: terminal too narrow (need >= 60 columns)")
@@ -220,9 +309,8 @@ func RenderTerminalView(frames []tuiFrame, width, height, selectedIdx int, subtr
if strings.TrimSpace(metricLabel) == "" {
metricLabel = "events"
}
-
- filterActive := strings.TrimSpace(searchQuery) != ""
- if filterActive {
+ filterIsActive := strings.TrimSpace(searchQuery) != ""
+ if filterIsActive {
if filterSet == nil {
filterSet = computeFilterVisibleSetInto(frames, matchSet, nil)
}
@@ -232,68 +320,20 @@ func RenderTerminalView(frames []tuiFrame, width, height, selectedIdx int, subtr
} else {
filterSet = nil
}
-
selectedIdx = normalizeSelectedIndex(frames, selectedIdx, filterSet)
selected := frames[selectedIdx]
- viewPath := compactFramePath(frames[0].Path)
if subtreeSet == nil {
subtreeSet = computeSubtreeSet(frames, selectedIdx)
}
-
- availableRows := height - 2 // toolbar + status
- maxRow := maxFrameRowForSet(frames, nil)
- totalDepthRows := maxRow + 1
- barHeight := computeBarHeight(availableRows, totalDepthRows, maxBarVisualHeight)
- visibleDepthRows := availableRows / barHeight
- if visibleDepthRows < 1 {
- visibleDepthRows = 1
- }
- rowOffset := 0
- truncated := false
- if maxRow+1 > visibleDepthRows {
- rowOffset = maxRow + 1 - visibleDepthRows
- truncated = true
- }
-
- visibleFrames := countVisibleFrames(frames, nil)
- toolbar := fmt.Sprintf("Flame | view:%s | frames:%d", viewPath, visibleFrames)
- toolbar += fmt.Sprintf(" | rows:%d", availableRows)
- if truncated {
- toolbar += " | showing deepest levels"
- }
- toolbar = padOrTrim(toolbar, width)
- selectedSystemShare := selected.Percent
- if globalTotal > 0 {
- selectedSystemShare = percentOfTotal(selected.Total, globalTotal)
- }
- if filterActive {
- filterCoveredTotal, filterBaseTotal := filterCoverageTotals(frames, matchSet, globalTotal)
- filterSystemShare := percentOfTotal(filterCoveredTotal, filterBaseTotal)
- selectedFilterShare := 0.0
- if filterCoveredTotal > 0 {
- selectedMatchTotal := filterCoverageTotalForPath(frames, matchSet, selected.Path)
- selectedFilterShare = percentOfTotal(selectedMatchTotal, filterCoveredTotal)
- }
- matches := orderedMatchIndices(matchSet)
- pos := 0
- if len(matches) > 0 {
- if idx := indexOf(matches, selectedIdx); idx >= 0 {
- pos = idx + 1
- }
- }
- frameCoverage := 0.0
- if len(frames) > 0 {
- frameCoverage = 100 * float64(visibleFrames) / float64(len(frames))
- }
- status := fmt.Sprintf("Filter %q: %.1f%% %s (%d/%d matches, %.1f%% frames shown) | Selected: %s total(%s)=%d depth=%d %.2f%% filtered %s",
- searchQuery, filterSystemShare, metricLabel, pos, len(matches), frameCoverage,
- selected.Name, metricLabel, selected.Total, selected.Depth, selectedFilterShare, metricLabel)
- return renderViewRows(toolbar, status, rowsForRender(frames, width, rowOffset, maxRow, barHeight, availableRows, selected.Path, subtreeSet, matchSet, selectedIdx, isDark, searchActive, filterActive), width)
+ params := computeRenderParams(frames, height)
+ toolbar := buildToolbar(frames, width, params)
+ var status string
+ if filterIsActive {
+ status = buildFilteredStatus(frames, selected, selectedIdx, matchSet, metricLabel, searchQuery, globalTotal, params.visibleFrames)
} else {
- status := fmt.Sprintf("Selected: %s [%s] total(%s)=%d depth=%d col=%d width=%d share=%.2f%% %s",
- selected.Name, compactFramePath(selected.Path), metricLabel, selected.Total, selected.Depth, selected.Col, selected.Width, selectedSystemShare, metricLabel)
- return renderViewRows(toolbar, status, rowsForRender(frames, width, rowOffset, maxRow, barHeight, availableRows, selected.Path, subtreeSet, matchSet, selectedIdx, isDark, searchActive, filterActive), width)
+ status = buildNormalStatus(selected, metricLabel, globalTotal)
}
+ return renderViewRows(toolbar, status, rowsForRender(frames, width, params.rowOffset, params.maxRow, params.barHeight, params.availableRows, selected.Path, subtreeSet, matchSet, selectedIdx, isDark, searchActive, filterIsActive), width)
}
func rowsForRender(frames []tuiFrame, width, rowOffset, maxRow, barHeight, availableRows int, selectedPath string, subtreeSet, matchSet map[int]bool, selectedIdx int, isDark, searchActive, filterActive bool) []string {