summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--internal/tui/flamegraph/model.go15
-rw-r--r--internal/tui/flamegraph/renderer.go103
-rw-r--r--internal/tui/flamegraph/renderer_test.go95
3 files changed, 171 insertions, 42 deletions
diff --git a/internal/tui/flamegraph/model.go b/internal/tui/flamegraph/model.go
index 81fcb7b..b9a8734 100644
--- a/internal/tui/flamegraph/model.go
+++ b/internal/tui/flamegraph/model.go
@@ -510,7 +510,20 @@ func (m Model) renderViewContent() string {
renderHeight = 3
}
- content := RenderTerminalView(m.frames, m.width, renderHeight, m.selectedIdx, m.subtreeSet, m.matchIndices, m.filterVisible, m.globalTotal, m.countFieldLabel(), strings.TrimSpace(m.heightField) != "", m.isDark, m.searchActive, m.searchQuery)
+ content := RenderTerminalView(RenderContext{
+ Frames: m.frames,
+ Width: m.width,
+ Height: renderHeight,
+ SelectedIdx: m.selectedIdx,
+ SubtreeSet: m.subtreeSet,
+ MatchSet: m.matchIndices,
+ FilterSet: m.filterVisible,
+ GlobalTotal: m.globalTotal,
+ MetricLabel: m.countFieldLabel(),
+ HeightMetricActive: strings.TrimSpace(m.heightField) != "",
+ IsDark: m.isDark,
+ SearchQuery: m.searchQuery,
+ })
content = replaceHeaderLine(content, m.toolbarLine())
if m.searchActive {
content = replaceFooterLine(content, m.searchFooter())
diff --git a/internal/tui/flamegraph/renderer.go b/internal/tui/flamegraph/renderer.go
index e5bbae2..a38792e 100644
--- a/internal/tui/flamegraph/renderer.go
+++ b/internal/tui/flamegraph/renderer.go
@@ -233,6 +233,40 @@ type renderViewParams struct {
truncated bool
}
+// RenderContext bundles flamegraph render inputs to avoid long positional
+// parameter lists at call sites.
+type RenderContext struct {
+ Frames []tuiFrame
+ Width int
+ Height int
+ SelectedIdx int
+ SubtreeSet map[int]bool
+ MatchSet map[int]bool
+ FilterSet map[int]bool
+ GlobalTotal uint64
+ MetricLabel string
+ HeightMetricActive bool
+ IsDark bool
+ SearchQuery string
+}
+
+type renderRowsContext struct {
+ frames []tuiFrame
+ width int
+ rowOffset int
+ maxRow int
+ barHeight int
+ leafBarHeight int
+ availableRows int
+ selectedPath string
+ subtreeSet map[int]bool
+ matchSet map[int]bool
+ selectedIdx int
+ heightMetricActive bool
+ isDark bool
+ filterActive bool
+}
+
// computeRenderParams derives the row-layout parameters for a given frame set
// and viewport height.
func computeRenderParams(frames []tuiFrame, height int, heightMetricActive bool) renderViewParams {
@@ -327,7 +361,20 @@ func buildNormalStatus(selected tuiFrame, metricLabel string, globalTotal uint64
// 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, heightMetricActive, isDark, searchActive bool, searchQuery string) string {
+func RenderTerminalView(ctx RenderContext) string {
+ frames := ctx.Frames
+ width := ctx.Width
+ height := ctx.Height
+ selectedIdx := ctx.SelectedIdx
+ subtreeSet := ctx.SubtreeSet
+ matchSet := ctx.MatchSet
+ filterSet := ctx.FilterSet
+ globalTotal := ctx.GlobalTotal
+ metricLabel := ctx.MetricLabel
+ heightMetricActive := ctx.HeightMetricActive
+ isDark := ctx.IsDark
+ searchQuery := ctx.SearchQuery
+
if width < minFlameWidth {
return common.PanelStyle.Render("Flame: terminal too narrow (need >= 60 columns)")
}
@@ -364,11 +411,23 @@ func RenderTerminalView(frames []tuiFrame, width, height, selectedIdx int, subtr
} else {
status = buildNormalStatus(selected, metricLabel, globalTotal)
}
- return renderViewRows(toolbar, status, rowsForRender(frames, width, params.rowOffset, params.maxRow, params.barHeight, params.leafBarHeight, params.availableRows, selected.Path, subtreeSet, matchSet, selectedIdx, heightMetricActive, isDark, searchActive, filterIsActive), width)
-}
-
-func rowsForRender(frames []tuiFrame, width, rowOffset, maxRow, barHeight, leafBarHeight, availableRows int, selectedPath string, subtreeSet, matchSet map[int]bool, selectedIdx int, heightMetricActive, isDark, searchActive, filterActive bool) []string {
- return buildRenderRows(frames, width, rowOffset, maxRow, barHeight, leafBarHeight, availableRows, selectedPath, subtreeSet, matchSet, selectedIdx, heightMetricActive, isDark, searchActive, filterActive)
+ rows := buildRenderRows(renderRowsContext{
+ frames: frames,
+ width: width,
+ rowOffset: params.rowOffset,
+ maxRow: params.maxRow,
+ barHeight: params.barHeight,
+ leafBarHeight: params.leafBarHeight,
+ availableRows: params.availableRows,
+ selectedPath: selected.Path,
+ subtreeSet: subtreeSet,
+ matchSet: matchSet,
+ selectedIdx: selectedIdx,
+ heightMetricActive: heightMetricActive,
+ isDark: isDark,
+ filterActive: filterIsActive,
+ })
+ return renderViewRows(toolbar, status, rows, width)
}
func renderViewRows(toolbar, status string, rows []string, width int) string {
@@ -390,7 +449,22 @@ type indexedFrame struct {
frame tuiFrame
}
-func buildRenderRows(frames []tuiFrame, width, rowOffset, maxRow, barHeight, leafBarHeight, availableRows int, selectedPath string, subtreeSet, matchSet map[int]bool, selectedIdx int, heightMetricActive, isDark, searchActive, filterActive bool) []string {
+func buildRenderRows(ctx renderRowsContext) []string {
+ frames := ctx.frames
+ width := ctx.width
+ rowOffset := ctx.rowOffset
+ maxRow := ctx.maxRow
+ barHeight := ctx.barHeight
+ leafBarHeight := ctx.leafBarHeight
+ availableRows := ctx.availableRows
+ selectedPath := ctx.selectedPath
+ subtreeSet := ctx.subtreeSet
+ matchSet := ctx.matchSet
+ selectedIdx := ctx.selectedIdx
+ heightMetricActive := ctx.heightMetricActive
+ isDark := ctx.isDark
+ filterActive := ctx.filterActive
+
rowsByDepth := make(map[int][]indexedFrame)
for idx, frame := range frames {
if frame.Row < rowOffset || frame.Row > maxRow {
@@ -413,13 +487,13 @@ func buildRenderRows(frames []tuiFrame, width, rowOffset, maxRow, barHeight, lea
frameHeights := leafFrameHeights(framesAtRow, leafBarHeight)
for h := leafBarHeight - 1; h >= 0; h-- {
showLabels := h == 0
- rows = append(rows, renderLeafRowBand(framesAtRow, frameHeights, h, width, selectedPath, subtreeSet, matchSet, selectedIdx, isDark, searchActive, filterActive, showLabels))
+ rows = append(rows, renderLeafRowBand(framesAtRow, frameHeights, h, width, selectedPath, subtreeSet, matchSet, selectedIdx, isDark, filterActive, showLabels))
}
continue
}
for repeat := 0; repeat < barHeight; repeat++ {
showLabels := repeat == barHeight/2
- rows = append(rows, renderRow(framesAtRow, width, selectedPath, subtreeSet, matchSet, selectedIdx, isDark, searchActive, filterActive, showLabels))
+ rows = append(rows, renderRow(framesAtRow, width, selectedPath, subtreeSet, matchSet, selectedIdx, isDark, filterActive, showLabels))
}
}
@@ -464,17 +538,17 @@ func leafFrameHeights(frames []indexedFrame, leafBarHeight int) map[int]int {
return heights
}
-func renderLeafRowBand(frames []indexedFrame, frameHeights map[int]int, band, width int, selectedPath string, subtreeSet, matchSet map[int]bool, selectedIdx int, isDark, searchActive, filterActive, showLabels bool) string {
+func renderLeafRowBand(frames []indexedFrame, frameHeights map[int]int, band, width int, selectedPath string, subtreeSet, matchSet map[int]bool, selectedIdx int, isDark, filterActive, showLabels bool) string {
visible := make([]indexedFrame, 0, len(frames))
for _, item := range frames {
if frameHeights[item.idx] > band {
visible = append(visible, item)
}
}
- return renderRow(visible, width, selectedPath, subtreeSet, matchSet, selectedIdx, isDark, searchActive, filterActive, showLabels)
+ return renderRow(visible, width, selectedPath, subtreeSet, matchSet, selectedIdx, isDark, filterActive, showLabels)
}
-func renderRow(frames []indexedFrame, width int, selectedPath string, subtreeSet, matchSet map[int]bool, selectedIdx int, isDark, searchActive, filterActive, showLabels bool) string {
+func renderRow(frames []indexedFrame, width int, selectedPath string, subtreeSet, matchSet map[int]bool, selectedIdx int, isDark, filterActive, showLabels bool) string {
if len(frames) == 0 {
return strings.Repeat(" ", width)
}
@@ -503,7 +577,7 @@ func renderRow(frames []indexedFrame, width int, selectedPath string, subtreeSet
if showLabels {
label = frameLabel(frame.Name, cellWidth, item.idx == selectedIdx, matchSet != nil && matchSet[item.idx])
}
- style := styleForFrame(item.idx, frame, selectedPath, subtreeSet, matchSet, selectedIdx, isDark, searchActive, filterActive)
+ style := styleForFrame(item.idx, frame, selectedPath, subtreeSet, matchSet, selectedIdx, isDark, filterActive)
cell := style.Render(label)
b.WriteString(cell)
cursor = frame.Col + cellWidth
@@ -582,8 +656,7 @@ func computeFilterVisibleSetInto(frames []tuiFrame, matchSet, visible map[int]bo
return visible
}
-func styleForFrame(idx int, frame tuiFrame, selectedPath string, subtreeSet, matchSet map[int]bool, selectedIdx int, isDark, searchActive, filterActive bool) lipgloss.Style {
- _ = searchActive
+func styleForFrame(idx int, frame tuiFrame, selectedPath string, subtreeSet, matchSet map[int]bool, selectedIdx int, isDark, filterActive bool) lipgloss.Style {
base := lipgloss.NewStyle().
Foreground(common.ColorBackground).
Background(frame.Fill)
diff --git a/internal/tui/flamegraph/renderer_test.go b/internal/tui/flamegraph/renderer_test.go
index 9361686..9b9ed88 100644
--- a/internal/tui/flamegraph/renderer_test.go
+++ b/internal/tui/flamegraph/renderer_test.go
@@ -152,7 +152,12 @@ func TestTerminalFrameColorSemanticPalette(t *testing.T) {
}
func TestRenderTerminalViewShowsNarrowMessage(t *testing.T) {
- out := RenderTerminalView(nil, 50, 10, 0, nil, nil, nil, 0, "events", false, true, false, "")
+ out := RenderTerminalView(RenderContext{
+ Width: 50,
+ Height: 10,
+ MetricLabel: "events",
+ IsDark: true,
+ })
if !strings.Contains(out, "terminal too narrow") {
t.Fatalf("expected narrow terminal warning, got %q", out)
}
@@ -177,7 +182,14 @@ func TestRenderTerminalViewIncludesToolbarAndStatus(t *testing.T) {
}
frames := BuildTerminalLayout(snapshot, 80, 6)
- out := RenderTerminalView(frames, 80, 6, 1, nil, nil, nil, 0, "events", false, true, false, "")
+ out := RenderTerminalView(RenderContext{
+ Frames: frames,
+ Width: 80,
+ Height: 6,
+ SelectedIdx: 1,
+ MetricLabel: "events",
+ IsDark: true,
+ })
if !strings.Contains(out, "Flame | view:root | frames:2") {
t.Fatalf("expected toolbar to include frame count, got %q", out)
}
@@ -196,7 +208,14 @@ func TestRenderTerminalViewFillsAvailableHeightForShallowTree(t *testing.T) {
}
frames := BuildTerminalLayout(snapshot, 100, 20)
- out := RenderTerminalView(frames, 100, 20, 1, nil, nil, nil, 0, "events", false, true, false, "")
+ out := RenderTerminalView(RenderContext{
+ Frames: frames,
+ Width: 100,
+ Height: 20,
+ SelectedIdx: 1,
+ MetricLabel: "events",
+ IsDark: true,
+ })
lines := strings.Split(out, "\n")
if got, want := len(lines), 20; got != want {
t.Fatalf("expected render to fill viewport height (%d lines), got %d", want, got)
@@ -226,7 +245,6 @@ func TestSelectedFrameStyleDoesNotUnderline(t *testing.T) {
1,
true,
false,
- false,
)
rendered := style.Render(" child ")
if strings.Contains(rendered, "\x1b[4m") || strings.Contains(rendered, "[4;") || strings.Contains(rendered, ";4m") {
@@ -245,7 +263,16 @@ func TestRenderTerminalViewShowsPersistentFilterContext(t *testing.T) {
frames := BuildTerminalLayout(snapshot, 80, 6)
matchSet := map[int]bool{1: true}
- out := RenderTerminalView(frames, 140, 6, 1, nil, matchSet, nil, 0, "events", false, true, false, "child")
+ out := RenderTerminalView(RenderContext{
+ Frames: frames,
+ Width: 140,
+ Height: 6,
+ SelectedIdx: 1,
+ MatchSet: matchSet,
+ MetricLabel: "events",
+ IsDark: true,
+ SearchQuery: "child",
+ })
if !strings.Contains(out, `Filter "child"`) {
t.Fatalf("expected filter context in status line, got %q", out)
}
@@ -279,7 +306,17 @@ func TestRenderTerminalViewFilterKeepsNonMatchingBranchesVisible(t *testing.T) {
}
matchSet := map[int]bool{needleIdx: true}
- out := RenderTerminalView(frames, 180, 8, needleIdx, nil, matchSet, nil, 100, "bytes", false, true, false, "needle")
+ out := RenderTerminalView(RenderContext{
+ Frames: frames,
+ Width: 180,
+ Height: 8,
+ SelectedIdx: needleIdx,
+ MatchSet: matchSet,
+ GlobalTotal: 100,
+ MetricLabel: "bytes",
+ IsDark: true,
+ SearchQuery: "needle",
+ })
if !strings.Contains(out, `Filter "needle": 60.0% bytes`) {
t.Fatalf("expected filter status to report 60.0%% bytes share, got %q", out)
}
@@ -368,7 +405,14 @@ func TestRenderTerminalViewShowsDeepLevelTruncationHint(t *testing.T) {
},
}
frames := BuildTerminalLayout(snapshot, 80, 10)
- out := RenderTerminalView(frames, 80, 4, 0, nil, nil, nil, 0, "events", false, true, false, "")
+ out := RenderTerminalView(RenderContext{
+ Frames: frames,
+ Width: 80,
+ Height: 4,
+ SelectedIdx: 0,
+ MetricLabel: "events",
+ IsDark: true,
+ })
if !strings.Contains(out, "showing deepest levels") {
t.Fatalf("expected truncation hint in toolbar, got %q", out)
}
@@ -435,7 +479,7 @@ func TestRenderLeafRowBandFiltersFramesByBand(t *testing.T) {
1: 2,
}
- topBand := renderLeafRowBand(frames, heights, 3, 10, "root"+pathSeparator+"A", nil, nil, 0, true, false, false, true)
+ topBand := renderLeafRowBand(frames, heights, 3, 10, "root"+pathSeparator+"A", nil, nil, 0, true, false, true)
if !strings.Contains(topBand, "A") {
t.Fatalf("expected top band to render taller frame A, got %q", topBand)
}
@@ -443,7 +487,7 @@ func TestRenderLeafRowBandFiltersFramesByBand(t *testing.T) {
t.Fatalf("expected top band to hide shorter frame B, got %q", topBand)
}
- lowerBand := renderLeafRowBand(frames, heights, 1, 10, "root"+pathSeparator+"A", nil, nil, 0, true, false, false, true)
+ lowerBand := renderLeafRowBand(frames, heights, 1, 10, "root"+pathSeparator+"A", nil, nil, 0, true, false, true)
if !strings.Contains(lowerBand, "A") || !strings.Contains(lowerBand, "B") {
t.Fatalf("expected lower band to render both frames, got %q", lowerBand)
}
@@ -456,23 +500,22 @@ func TestBuildRenderRowsHeightMetricUsesLeafBandsAndViewportRows(t *testing.T) {
{Name: "B", Row: 1, Col: 6, Width: 6, Path: "root" + pathSeparator + "B", HeightTotal: 50, Fill: color.RGBA{R: 80, G: 120, B: 180, A: 255}},
}
- rows := buildRenderRows(
- frames,
- 12, // width
- 0, // rowOffset
- 1, // maxRow
- 1, // barHeight
- 4, // leafBarHeight
- 5, // availableRows
- "root",
- map[int]bool{0: true, 1: true, 2: true},
- nil,
- 0,
- true, // heightMetricActive
- true, // isDark
- false, // searchActive
- false, // filterActive
- )
+ rows := buildRenderRows(renderRowsContext{
+ frames: frames,
+ width: 12, // width
+ rowOffset: 0, // rowOffset
+ maxRow: 1, // maxRow
+ barHeight: 1, // barHeight
+ leafBarHeight: 4, // leafBarHeight
+ availableRows: 5, // availableRows
+ selectedPath: "root",
+ subtreeSet: map[int]bool{0: true, 1: true, 2: true},
+ matchSet: nil,
+ selectedIdx: 0,
+ heightMetricActive: true, // heightMetricActive
+ isDark: true, // isDark
+ filterActive: false, // filterActive
+ })
if got, want := len(rows), 5; got != want {
t.Fatalf("row count = %d, want %d", got, want)