diff options
| author | Paul Buetow <paul@buetow.org> | 2026-03-05 23:09:31 +0200 |
|---|---|---|
| committer | Paul Buetow <paul@buetow.org> | 2026-03-05 23:09:31 +0200 |
| commit | 4953fd0200eef52f7e1547d5961a2e70e24e49d1 (patch) | |
| tree | 3f392865fc0255a32f2db02f781438d7aa182cd3 /internal/tui/flamegraph | |
| parent | 9744b3ce9e7b33b8843d1c260c3dfa3431391f43 (diff) | |
task 367: add flamegraph benchmark fixture generators
Diffstat (limited to 'internal/tui/flamegraph')
| -rw-r--r-- | internal/tui/flamegraph/testdata_fixture_test.go | 39 | ||||
| -rw-r--r-- | internal/tui/flamegraph/testdata_test.go | 185 |
2 files changed, 224 insertions, 0 deletions
diff --git a/internal/tui/flamegraph/testdata_fixture_test.go b/internal/tui/flamegraph/testdata_fixture_test.go new file mode 100644 index 0000000..1f22c26 --- /dev/null +++ b/internal/tui/flamegraph/testdata_fixture_test.go @@ -0,0 +1,39 @@ +package flamegraph + +import "testing" + +func TestFixtureSnapshotsHaveApproximateFrameCounts(t *testing.T) { + fixtures := []struct { + name string + depth int + breadth int + expect int + }{ + {name: "small", depth: fixtureSmallDepth, breadth: fixtureSmallBreadth, expect: 121}, + {name: "medium", depth: fixtureMediumDepth, breadth: fixtureMediumBreadth, expect: 2500}, + {name: "large", depth: fixtureLargeDepth, breadth: fixtureLargeBreadth, expect: 12000}, + {name: "deep", depth: fixtureDeepDepth, breadth: fixtureDeepBreadth, expect: 100}, + {name: "wide", depth: fixtureWideDepth, breadth: fixtureWideBreadth, expect: 5000}, + } + + for _, fixture := range fixtures { + t.Run(fixture.name, func(t *testing.T) { + snap := generateTestSnapshot(fixture.depth, fixture.breadth) + got := snapshotNodeCount(snap) + if !approxEqualCount(got, fixture.expect) { + t.Fatalf("%s fixture nodes=%d, expected approximately %d", fixture.name, got, fixture.expect) + } + }) + } +} + +func TestGenerateTestTrieProducesSnapshotData(t *testing.T) { + lt := generateTestTrie(fixtureSmallDepth, fixtureSmallBreadth) + snap, err := decodeTrieSnapshot(lt) + if err != nil { + t.Fatalf("decode trie snapshot: %v", err) + } + if snap.Total == 0 { + t.Fatalf("expected generated trie snapshot to contain data") + } +} diff --git a/internal/tui/flamegraph/testdata_test.go b/internal/tui/flamegraph/testdata_test.go new file mode 100644 index 0000000..c7d97b0 --- /dev/null +++ b/internal/tui/flamegraph/testdata_test.go @@ -0,0 +1,185 @@ +package flamegraph + +import ( + "encoding/json" + "fmt" + "math" + + "ior/internal/event" + "ior/internal/file" + coreflamegraph "ior/internal/flamegraph" + "ior/internal/types" +) + +const ( + fixtureSmallDepth = 5 + fixtureSmallBreadth = 3 + + fixtureMediumDepth = 10 + fixtureMediumBreadth = 5 + + fixtureLargeDepth = 15 + fixtureLargeBreadth = 8 + + fixtureDeepDepth = 50 + fixtureDeepBreadth = 2 + + fixtureWideDepth = 3 + fixtureWideBreadth = 50 +) + +func generateTestTrie(depth, breadthPerLevel int) *coreflamegraph.LiveTrie { + lt := coreflamegraph.NewLiveTrie([]string{"comm", "path", "tracepoint"}, "count") + comms := []string{"api", "db", "worker", "cache"} + traceIDs := []types.TraceId{ + types.SYS_ENTER_READ, + types.SYS_ENTER_WRITE, + types.SYS_ENTER_OPENAT, + types.SYS_ENTER_CLOSE, + } + + totalEvents := maxInt(100, fixtureTargetFrames(depth, breadthPerLevel)/2) + for i := 0; i < totalEvents; i++ { + comm := comms[i%len(comms)] + traceID := traceIDs[i%len(traceIDs)] + path := buildBenchmarkPath(depth, breadthPerLevel, i) + lt.Ingest(newBenchmarkPair(comm, traceID, uint32(1000+(i%256)), uint32(200000+i), path)) + } + return lt +} + +func generateTestSnapshot(depth, breadthPerLevel int) *snapshotNode { + targetFrames := fixtureTargetFrames(depth, breadthPerLevel) + if targetFrames < 1 { + targetFrames = 1 + } + + root := &snapshotNode{Name: "root", Value: 1} + type qItem struct { + node *snapshotNode + depth int + } + queue := []qItem{{node: root, depth: 0}} + created := 1 + + for len(queue) > 0 && created < targetFrames { + item := queue[0] + queue = queue[1:] + if item.depth >= depth { + continue + } + remaining := targetFrames - created + branchCount := breadthPerLevel + if branchCount > remaining { + branchCount = remaining + } + for i := 0; i < branchCount; i++ { + child := &snapshotNode{ + Name: fmt.Sprintf("d%d-n%d", item.depth+1, created+i), + Value: 1, + } + item.node.Children = append(item.node.Children, child) + queue = append(queue, qItem{node: child, depth: item.depth + 1}) + } + created += branchCount + } + + computeSnapshotTotals(root) + return root +} + +func fixtureTargetFrames(depth, breadth int) int { + switch { + case depth == fixtureSmallDepth && breadth == fixtureSmallBreadth: + return 121 + case depth == fixtureMediumDepth && breadth == fixtureMediumBreadth: + return 2500 + case depth == fixtureLargeDepth && breadth == fixtureLargeBreadth: + return 12000 + case depth == fixtureDeepDepth && breadth == fixtureDeepBreadth: + return 100 + case depth == fixtureWideDepth && breadth == fixtureWideBreadth: + return 5000 + default: + return maxInt(1, depth*breadth*10) + } +} + +func computeSnapshotTotals(node *snapshotNode) uint64 { + if node == nil { + return 0 + } + total := node.Value + for _, child := range node.Children { + total += computeSnapshotTotals(child) + } + node.Total = total + return total +} + +func buildBenchmarkPath(depth, breadth, seed int) string { + if depth < 1 { + depth = 1 + } + if breadth < 1 { + breadth = 1 + } + path := "/bench" + value := seed + for level := 0; level < depth; level++ { + slot := value % breadth + path += fmt.Sprintf("/l%d-b%d", level, slot) + value = value / breadth + } + return path +} + +func newBenchmarkPair(comm string, traceID types.TraceId, pid, tid uint32, path string) *event.Pair { + enter := &types.OpenEvent{ + TraceId: traceID, + Pid: pid, + Tid: tid, + } + exit := &types.RetEvent{ + TraceId: types.SYS_EXIT_OPENAT, + Pid: pid, + Tid: tid, + } + pair := event.NewPair(enter) + pair.ExitEv = exit + pair.File = file.NewFd(3, path, 0) + pair.Comm = comm + pair.Duration = 1 + pair.DurationToPrev = 1 + pair.Bytes = 64 + return pair +} + +func snapshotNodeCount(node *snapshotNode) int { + if node == nil { + return 0 + } + total := 1 + for _, child := range node.Children { + total += snapshotNodeCount(child) + } + return total +} + +func approxEqualCount(got, want int) bool { + if got == want { + return true + } + const tolerance = 0.2 + diff := math.Abs(float64(got-want)) / float64(want) + return diff <= tolerance +} + +func decodeTrieSnapshot(lt *coreflamegraph.LiveTrie) (*snapshotNode, error) { + payload, _ := lt.SnapshotJSON() + var snap snapshotNode + if err := json.Unmarshal(payload, &snap); err != nil { + return nil, err + } + return &snap, nil +} |
