diff options
| author | Paul Buetow <paul@buetow.org> | 2026-05-27 08:17:33 +0300 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-05-27 08:17:33 +0300 |
| commit | 891e31f845d5a1d9fc7426eb8351a05c42fb1cd5 (patch) | |
| tree | af55bed456075ef23a6233f2120b9f824bb9faf3 /internal/tui/flamegraph | |
| parent | 45b8e82f18cadbedb1c57156b9580b199033be3c (diff) | |
flamegraph: dedupe layout math for hit mapping (8p)
Diffstat (limited to 'internal/tui/flamegraph')
| -rw-r--r-- | internal/tui/flamegraph/frame_animator.go | 106 | ||||
| -rw-r--r-- | internal/tui/flamegraph/frame_animator_test.go | 6 | ||||
| -rw-r--r-- | internal/tui/flamegraph/model.go | 7 | ||||
| -rw-r--r-- | internal/tui/flamegraph/renderer.go | 95 |
4 files changed, 102 insertions, 112 deletions
diff --git a/internal/tui/flamegraph/frame_animator.go b/internal/tui/flamegraph/frame_animator.go index 86e52ec..80b594a 100644 --- a/internal/tui/flamegraph/frame_animator.go +++ b/internal/tui/flamegraph/frame_animator.go @@ -79,112 +79,6 @@ func (fa *FrameAnimator) reset() { fa.ancestry = frameAncestry{} } -// frameIndexAt returns the index of the frame rendered at terminal coordinates -// (x, y), or -1 if no frame occupies that cell. showHelp adds one extra line -// to the UI chrome so the frame area row calculations account for it. -func frameIndexAt(frames []tuiFrame, x, y, width, height int, showHelp, heightMetricActive bool) int { - if len(frames) == 0 || width <= 0 || height <= 0 { - return -1 - } - if x < 0 || x >= width || y < 0 { - return -1 - } - extraLines := 1 // selection status line - if showHelp { - extraLines++ - } - renderHeight := height - extraLines - if renderHeight < 3 { - renderHeight = 3 - } - availableRows := renderHeight - 2 // flame toolbar + frame-status line - if availableRows < 1 { - return -1 - } - // Row 0 is flame toolbar, rows 1..availableRows are bars, last row is status. - if y < 1 || y > availableRows { - return -1 - } - targetRow := frameCoordToTargetRow(frames, y-1, availableRows, heightMetricActive) - if targetRow < 0 { - return -1 - } - return findFrameAtRow(frames, targetRow, x, width) -} - -// frameCoordToTargetRow converts a data-area row offset (0-based, after -// stripping the toolbar row) into the logical frame row index. Returns -1 when -// the coordinate falls in the top padding above the first visible row. -func frameCoordToTargetRow(frames []tuiFrame, dataRow, availableRows int, heightMetricActive bool) int { - maxRow := maxFrameRowForSet(frames, nil) - barHeight := computeBarHeight(availableRows, maxRow+1, maxBarVisualHeight) - leafBarHeight := barHeight - visibleDepthRows := availableRows / barHeight - if heightMetricActive { - barHeight = 1 - visibleDepthRows = availableRows - } - if visibleDepthRows < 1 { - visibleDepthRows = 1 - } - rowOffset := 0 - if maxRow+1 > visibleDepthRows { - rowOffset = maxRow + 1 - visibleDepthRows - } - if heightMetricActive { - visibleNonLeafRows := max(0, maxRow-rowOffset) - leafBarHeight = availableRows - visibleNonLeafRows - if leafBarHeight < 1 { - leafBarHeight = 1 - } - } - renderedRows := (maxRow - rowOffset + 1) * barHeight - if heightMetricActive { - renderedRows = leafBarHeight + max(0, maxRow-rowOffset)*barHeight - } - padTop := 0 - if renderedRows < availableRows { - padTop = availableRows - renderedRows - } - if dataRow < padTop { - return -1 - } - rowInRender := dataRow - padTop - for row := maxRow; row >= rowOffset; row-- { - rowHeight := barHeight - if heightMetricActive && row == maxRow { - rowHeight = leafBarHeight - } - if rowInRender < rowHeight { - return row - } - rowInRender -= rowHeight - } - return -1 -} - -// findFrameAtRow scans frames for the narrowest one that occupies logical row -// targetRow and contains pixel column x within [0, width). Returning the -// narrowest frame resolves overlap between wide parent and narrow child bars. -func findFrameAtRow(frames []tuiFrame, targetRow, x, width int) int { - best := -1 - bestWidth := int(^uint(0) >> 1) - for idx, frame := range frames { - if frame.Row != targetRow || frame.Col >= width { - continue - } - right := min(width, frame.Col+frame.Width) - if x < frame.Col || x >= right { - continue - } - if frame.Width < bestWidth { - best = idx - bestWidth = frame.Width - } - } - return best -} - // driveWindowActive reports whether lastKeyAt falls within the active drive // window where the user is considered to be actively pressing keys. func driveWindowActive(lastKeyAt time.Time) bool { diff --git a/internal/tui/flamegraph/frame_animator_test.go b/internal/tui/flamegraph/frame_animator_test.go index 6b4d4f7..acc4f26 100644 --- a/internal/tui/flamegraph/frame_animator_test.go +++ b/internal/tui/flamegraph/frame_animator_test.go @@ -10,9 +10,10 @@ func TestFrameCoordToTargetRowKeepsUniformBarMapping(t *testing.T) { {Name: "leaf", Row: 3, Col: 0, Width: 20, Path: "root" + pathSeparator + "a" + pathSeparator + "b" + pathSeparator + "leaf"}, } availableRows := 8 + params := computeRenderParamsForAvailableRows(frames, availableRows, false) want := []int{3, 3, 2, 2, 1, 1, 0, 0} for dataRow, expected := range want { - if got := frameCoordToTargetRow(frames, dataRow, availableRows, false); got != expected { + if got := frameCoordToTargetRow(dataRow, params); got != expected { t.Fatalf("dataRow=%d: got row=%d want=%d", dataRow, got, expected) } } @@ -24,9 +25,10 @@ func TestFrameCoordToTargetRowHeightMetricMapsExpandedLeafBand(t *testing.T) { {Name: "leaf", Row: 1, Col: 0, Width: 20, Path: "root" + pathSeparator + "leaf", HeightTotal: 100}, } availableRows := 6 + params := computeRenderParamsForAvailableRows(frames, availableRows, true) want := []int{1, 1, 1, 1, 1, 0} for dataRow, expected := range want { - if got := frameCoordToTargetRow(frames, dataRow, availableRows, true); got != expected { + if got := frameCoordToTargetRow(dataRow, params); got != expected { t.Fatalf("dataRow=%d: got row=%d want=%d", dataRow, got, expected) } } diff --git a/internal/tui/flamegraph/model.go b/internal/tui/flamegraph/model.go index b9a8734..9f14a70 100644 --- a/internal/tui/flamegraph/model.go +++ b/internal/tui/flamegraph/model.go @@ -1098,15 +1098,16 @@ func (m Model) rootSnapshotPath() string { return m.ZoomNavigator.rootSnapshotPath(m.snapshot, m.frames) } -// frameIndexAt delegates to the FrameAnimator package-level helper to convert +// frameIndexAt delegates to the renderer package-level helper to convert // terminal coordinates (x, y) to a frame index, accounting for UI chrome. func (m Model) frameIndexAt(x, y int) int { return frameIndexAt(m.frames, x, y, m.width, m.height, m.showHelp, strings.TrimSpace(m.heightField) != "") } -// frameCoordToTargetRow delegates to the FrameAnimator package-level helper. +// frameCoordToTargetRow delegates to the renderer package-level helper. func (m Model) frameCoordToTargetRow(dataRow, availableRows int) int { - return frameCoordToTargetRow(m.frames, dataRow, availableRows, strings.TrimSpace(m.heightField) != "") + params := computeRenderParamsForAvailableRows(m.frames, availableRows, strings.TrimSpace(m.heightField) != "") + return frameCoordToTargetRow(dataRow, params) } func (m Model) withZoomLineage(frames []tuiFrame) []tuiFrame { diff --git a/internal/tui/flamegraph/renderer.go b/internal/tui/flamegraph/renderer.go index a38792e..6d5a494 100644 --- a/internal/tui/flamegraph/renderer.go +++ b/internal/tui/flamegraph/renderer.go @@ -231,6 +231,7 @@ type renderViewParams struct { availableRows int visibleFrames int truncated bool + heightMetric bool } // RenderContext bundles flamegraph render inputs to avoid long positional @@ -270,7 +271,15 @@ type renderRowsContext struct { // computeRenderParams derives the row-layout parameters for a given frame set // and viewport height. func computeRenderParams(frames []tuiFrame, height int, heightMetricActive bool) renderViewParams { - availableRows := height - 2 // toolbar + frame-status line + return computeRenderParamsForAvailableRows(frames, height-2, heightMetricActive) +} + +// computeRenderParamsForAvailableRows derives row-layout parameters for a +// frame set and pre-computed data-area row budget. +func computeRenderParamsForAvailableRows(frames []tuiFrame, availableRows int, heightMetricActive bool) renderViewParams { + if availableRows < 1 { + availableRows = 1 + } maxRow := maxFrameRowForSet(frames, nil) totalDepthRows := maxRow + 1 barHeight := computeBarHeight(availableRows, totalDepthRows, maxBarVisualHeight) @@ -304,7 +313,91 @@ func computeRenderParams(frames []tuiFrame, height int, heightMetricActive bool) availableRows: availableRows, visibleFrames: countVisibleFrames(frames, nil), truncated: truncated, + heightMetric: heightMetricActive, + } +} + +// frameIndexAt returns the index of the frame rendered at terminal coordinates +// (x, y), or -1 if no frame occupies that cell. showHelp adds one extra line +// to the UI chrome so the frame area row calculations account for it. +func frameIndexAt(frames []tuiFrame, x, y, width, height int, showHelp, heightMetricActive bool) int { + if len(frames) == 0 || width <= 0 || height <= 0 { + return -1 + } + if x < 0 || x >= width || y < 0 { + return -1 + } + extraLines := 1 // selection status line + if showHelp { + extraLines++ + } + renderHeight := height - extraLines + if renderHeight < 3 { + renderHeight = 3 + } + params := computeRenderParamsForAvailableRows(frames, renderHeight-2, heightMetricActive) + if y < 1 || y > params.availableRows { + return -1 + } + targetRow := frameCoordToTargetRow(y-1, params) + if targetRow < 0 { + return -1 + } + return findFrameAtRow(frames, targetRow, x, width) +} + +// frameCoordToTargetRow converts a data-area row offset (0-based, after +// stripping the toolbar row) into the logical frame row index. Returns -1 when +// the coordinate falls in the top padding above the first visible row. +func frameCoordToTargetRow(dataRow int, params renderViewParams) int { + if params.visibleFrames == 0 || params.availableRows < 1 || dataRow < 0 || dataRow >= params.availableRows { + return -1 + } + renderedRows := (params.maxRow - params.rowOffset + 1) * params.barHeight + if params.heightMetric { + renderedRows = params.leafBarHeight + max(0, params.maxRow-params.rowOffset)*params.barHeight + } + padTop := 0 + if renderedRows < params.availableRows { + padTop = params.availableRows - renderedRows + } + if dataRow < padTop { + return -1 + } + rowInRender := dataRow - padTop + for row := params.maxRow; row >= params.rowOffset; row-- { + rowHeight := params.barHeight + if params.heightMetric && row == params.maxRow { + rowHeight = params.leafBarHeight + } + if rowInRender < rowHeight { + return row + } + rowInRender -= rowHeight + } + return -1 +} + +// findFrameAtRow scans frames for the narrowest one that occupies logical row +// targetRow and contains pixel column x within [0, width). Returning the +// narrowest frame resolves overlap between wide parent and narrow child bars. +func findFrameAtRow(frames []tuiFrame, targetRow, x, width int) int { + best := -1 + bestWidth := int(^uint(0) >> 1) + for idx, frame := range frames { + if frame.Row != targetRow || frame.Col >= width { + continue + } + right := min(width, frame.Col+frame.Width) + if x < frame.Col || x >= right { + continue + } + if frame.Width < bestWidth { + best = idx + bestWidth = frame.Width + } } + return best } // buildToolbar assembles the top-of-view toolbar string and pads/trims it to |
