summaryrefslogtreecommitdiff
path: root/internal/tui/flamegraph/ancestry.go
blob: 4065909ff9b7705b194b513d39886f3d66fa49a0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package flamegraph

import "strings"

// frameAncestry stores parent/child relationships between frames keyed by their
// slice index. It is rebuilt whenever the frames slice is rebuilt, and reused
// across keystrokes so subtree and filter-visibility lookups run in O(subtree)
// time instead of O(frames) string scans.
type frameAncestry struct {
	parent   []int
	children [][]int
}

// buildFrameAncestry derives parent/child links for the supplied frame slice
// using each frame's Path string. Robust to any ordering — the layout pass
// emits depth-first frames, but withZoomLineage appends ancestor frames at the
// tail, so contiguous-range tricks are not safe.
func buildFrameAncestry(frames []tuiFrame) frameAncestry {
	n := len(frames)
	ancestry := frameAncestry{
		parent:   make([]int, n),
		children: make([][]int, n),
	}
	if n == 0 {
		return ancestry
	}

	byPath := make(map[string]int, n)
	for idx, frame := range frames {
		byPath[frame.Path] = idx
		ancestry.parent[idx] = -1
	}
	for idx, frame := range frames {
		sep := strings.LastIndexByte(frame.Path, pathSeparatorByte)
		if sep < 0 {
			continue
		}
		parentPath := frame.Path[:sep]
		parentIdx, ok := byPath[parentPath]
		if !ok {
			continue
		}
		ancestry.parent[idx] = parentIdx
		ancestry.children[parentIdx] = append(ancestry.children[parentIdx], idx)
	}
	return ancestry
}

// subtreeSetUsingAncestry fills `set` with the selected frame, every descendant
// reachable through the child adjacency list, and every ancestor reachable
// through the parent chain. Matches the visibility contract of the legacy
// path-prefix walk in computeSubtreeSetInto.
//
// If `ancestry` is stale relative to `frames` (different length), it is rebuilt
// in place. The hot path through the model keeps ancestry warm via
// rebuildFrames, so the fallback only fires in tests that mutate frames
// directly.
func subtreeSetUsingAncestry(frames []tuiFrame, selectedIdx int, ancestry frameAncestry, set map[int]bool) map[int]bool {
	if set == nil {
		set = make(map[int]bool)
	} else {
		for idx := range set {
			delete(set, idx)
		}
	}
	if selectedIdx < 0 || selectedIdx >= len(frames) {
		return set
	}
	if len(ancestry.parent) != len(frames) {
		ancestry = buildFrameAncestry(frames)
	}
	markSubtree(ancestry, selectedIdx, set)
	for cur := ancestry.parent[selectedIdx]; cur >= 0; cur = ancestry.parent[cur] {
		set[cur] = true
	}
	return set
}

// filterVisibleSetUsingAncestry fills `set` with every match plus its full
// descendant subtree and ancestor chain — same semantics as the legacy
// computeFilterVisibleSetInto walk, but in O(sum-of-subtree-sizes) instead of
// O(frames × matches). Falls back to building ancestry on the fly when the
// supplied index is stale (see subtreeSetUsingAncestry).
func filterVisibleSetUsingAncestry(frames []tuiFrame, matchSet map[int]bool, ancestry frameAncestry, set map[int]bool) map[int]bool {
	if set == nil {
		set = make(map[int]bool)
	} else {
		for idx := range set {
			delete(set, idx)
		}
	}
	if len(matchSet) == 0 {
		return set
	}
	if len(ancestry.parent) != len(frames) {
		ancestry = buildFrameAncestry(frames)
	}
	for matchIdx := range matchSet {
		if matchIdx < 0 || matchIdx >= len(frames) {
			continue
		}
		markSubtree(ancestry, matchIdx, set)
		for cur := ancestry.parent[matchIdx]; cur >= 0; cur = ancestry.parent[cur] {
			if set[cur] {
				break
			}
			set[cur] = true
		}
	}
	return set
}

// markSubtree marks a frame and every descendant. Uses an iterative stack
// to keep allocations bounded for deep trees.
func markSubtree(ancestry frameAncestry, root int, set map[int]bool) {
	stack := []int{root}
	for len(stack) > 0 {
		last := len(stack) - 1
		idx := stack[last]
		stack = stack[:last]
		if set[idx] {
			continue
		}
		set[idx] = true
		stack = append(stack, ancestry.children[idx]...)
	}
}