summaryrefslogtreecommitdiff
path: root/internal/tui/flamegraph/renderer.go
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-03-05 22:29:11 +0200
committerPaul Buetow <paul@buetow.org>2026-03-05 22:29:11 +0200
commit63b9ad7c7692b3bedb4d0051c080946e38e058f9 (patch)
tree3285479bdee924913ff622476ac74125232aae56 /internal/tui/flamegraph/renderer.go
parent270c4b422cfc5e7588b7045276588e9f043f85e3 (diff)
task 355: add terminal flamegraph layout renderer
Diffstat (limited to 'internal/tui/flamegraph/renderer.go')
-rw-r--r--internal/tui/flamegraph/renderer.go99
1 files changed, 99 insertions, 0 deletions
diff --git a/internal/tui/flamegraph/renderer.go b/internal/tui/flamegraph/renderer.go
new file mode 100644
index 0000000..6cace44
--- /dev/null
+++ b/internal/tui/flamegraph/renderer.go
@@ -0,0 +1,99 @@
+package flamegraph
+
+import (
+ "hash/fnv"
+ "image/color"
+ "math"
+ "strings"
+)
+
+const pathSeparator = "\x1f"
+
+// BuildTerminalLayout converts a live trie snapshot into terminal frame cells.
+func BuildTerminalLayout(snapshot *snapshotNode, width, height int) []tuiFrame {
+ if snapshot == nil || width <= 0 || height <= 0 {
+ return nil
+ }
+ rootTotal := snapshotTotal(snapshot)
+ if rootTotal == 0 {
+ return nil
+ }
+
+ rootName := frameName(snapshot.Name, 0)
+ frames := make([]tuiFrame, 0, len(snapshot.Children)+1)
+ collectTerminalLayout(&frames, snapshot, rootTotal, width, height, 0, 0, rootName)
+ return frames
+}
+
+func collectTerminalLayout(out *[]tuiFrame, node *snapshotNode, rootTotal uint64, width, height, depth, col int, path string) {
+ if node == nil || depth >= height {
+ return
+ }
+ total := snapshotTotal(node)
+ frameWidth := int(math.Floor(float64(width) * (float64(total) / float64(rootTotal))))
+ if frameWidth < 1 {
+ return
+ }
+
+ name := frameName(node.Name, depth)
+ *out = append(*out, tuiFrame{
+ Name: name,
+ Col: col,
+ Row: depth,
+ Width: frameWidth,
+ Total: total,
+ Percent: 100 * float64(total) / float64(rootTotal),
+ Fill: terminalFrameColor(name),
+ Depth: depth,
+ Path: path,
+ })
+
+ cursor := col
+ for _, child := range node.Children {
+ childTotal := snapshotTotal(child)
+ childWidth := int(math.Floor(float64(width) * (float64(childTotal) / float64(rootTotal))))
+ if childWidth < 1 {
+ continue
+ }
+ childName := frameName(child.Name, depth+1)
+ childPath := strings.Join([]string{path, childName}, pathSeparator)
+ collectTerminalLayout(out, child, rootTotal, width, height, depth+1, cursor, childPath)
+ cursor += childWidth
+ }
+}
+
+func snapshotTotal(node *snapshotNode) uint64 {
+ if node == nil {
+ return 0
+ }
+ total := node.Value
+ for _, child := range node.Children {
+ total += snapshotTotal(child)
+ }
+ if node.Total > total {
+ return node.Total
+ }
+ return total
+}
+
+func frameName(name string, depth int) string {
+ if name != "" {
+ return name
+ }
+ if depth == 0 {
+ return "root"
+ }
+ return "(unknown)"
+}
+
+func terminalFrameColor(name string) color.Color {
+ hasher := fnv.New32a()
+ _, _ = hasher.Write([]byte(name))
+ h := hasher.Sum32()
+ return color.RGBA{
+ R: uint8(200 + int(h%35)),
+ G: uint8(80 + int((h>>8)%120)),
+ B: uint8(40 + int((h>>16)%90)),
+ A: 255,
+ }
+}