diff options
Diffstat (limited to 'internal/flamegraph')
| -rw-r--r-- | internal/flamegraph/livetrie.go | 8 | ||||
| -rw-r--r-- | internal/flamegraph/livetrie_test.go | 64 | ||||
| -rw-r--r-- | internal/flamegraph/testfixture.go | 120 |
3 files changed, 190 insertions, 2 deletions
diff --git a/internal/flamegraph/livetrie.go b/internal/flamegraph/livetrie.go index db46af5..0d42b6b 100644 --- a/internal/flamegraph/livetrie.go +++ b/internal/flamegraph/livetrie.go @@ -88,6 +88,12 @@ func (lt *LiveTrie) invalidateCache() { // Ingest adds one event pair into the live trie and recycles the pair. func (lt *LiveTrie) Ingest(ep *event.Pair) { record := eventPairToRecord(ep) + lt.AddRecord(record) + ep.Recycle() +} + +// AddRecord adds one already-decoded flamegraph record into the live trie. +func (lt *LiveTrie) AddRecord(record IterRecord) { value := record.Cnt.ValueByName(lt.countField) lt.mu.Lock() @@ -95,8 +101,6 @@ func (lt *LiveTrie) Ingest(ep *event.Pair) { lt.addLocked(frames, value) lt.version.Add(1) lt.mu.Unlock() - - ep.Recycle() } // Reset clears the trie so live snapshots start from a new baseline. diff --git a/internal/flamegraph/livetrie_test.go b/internal/flamegraph/livetrie_test.go index e569e00..632f668 100644 --- a/internal/flamegraph/livetrie_test.go +++ b/internal/flamegraph/livetrie_test.go @@ -61,6 +61,70 @@ func TestLiveTrieVersionIncrementsPerIngest(t *testing.T) { } } +func TestLiveTrieAddRecordIncrementsVersion(t *testing.T) { + lt := NewLiveTrie([]string{"comm", "path", "tracepoint"}, "count") + lt.AddRecord(IterRecord{ + Path: "/tmp/demo/read", + TraceID: types.SYS_ENTER_READ, + Comm: "demo", + Pid: 1001, + Tid: 1001, + Cnt: Counter{Count: 7, Duration: 70, DurationToPrev: 14, Bytes: 28}, + }) + + if got := lt.Version(); got != 1 { + t.Fatalf("version = %d, want 1", got) + } + snap := decodeLiveSnapshot(t, lt) + if snap.Total != 7 { + t.Fatalf("root total = %d, want 7", snap.Total) + } +} + +func TestSeedTestFlameDataBuildsStaticFixture(t *testing.T) { + lt := NewLiveTrie([]string{"comm", "path", "tracepoint"}, "count") + SeedTestFlameData(lt) + + if got := lt.Version(); got == 0 { + t.Fatalf("expected seed fixture to add records") + } + snap := decodeLiveSnapshot(t, lt) + if snap.Total == 0 { + t.Fatalf("expected non-empty seeded snapshot") + } + if findSnapshotChild(&snap, "api") == nil { + t.Fatalf("expected seeded snapshot to include api branch") + } + if findSnapshotChild(&snap, "worker") == nil { + t.Fatalf("expected seeded snapshot to include worker branch") + } +} + +func TestSeedTestLiveFlameDataVariesByTick(t *testing.T) { + lt := NewLiveTrie([]string{"comm", "path", "tracepoint"}, "count") + + SeedTestLiveFlameData(lt, 0) + snapTick0 := decodeLiveSnapshot(t, lt) + apiTick0 := findSnapshotPath(t, &snapTick0, "api").Total + workerTick0 := findSnapshotPath(t, &snapTick0, "worker").Total + + lt.Reset() + SeedTestLiveFlameData(lt, 1) + snapTick1 := decodeLiveSnapshot(t, lt) + apiTick1 := findSnapshotPath(t, &snapTick1, "api").Total + workerTick1 := findSnapshotPath(t, &snapTick1, "worker").Total + + if apiTick0 == apiTick1 && workerTick0 == workerTick1 { + t.Fatalf("expected phase shift to alter branch totals, got api=%d worker=%d for both ticks", apiTick0, workerTick0) + } + if apiTick0 <= workerTick0 { + t.Fatalf("expected api to dominate at tick 0, got api=%d worker=%d", apiTick0, workerTick0) + } + if workerTick1 <= apiTick1 { + t.Fatalf("expected worker to dominate at tick 1, got worker=%d api=%d", workerTick1, apiTick1) + } +} + func TestLiveTrieResetClearsDataAndAdvancesVersion(t *testing.T) { lt := NewLiveTrie([]string{"comm"}, "count") lt.Ingest(newTestPair("svc", 42, 1001, "/tmp/a", 1, 1, 1)) diff --git a/internal/flamegraph/testfixture.go b/internal/flamegraph/testfixture.go new file mode 100644 index 0000000..2774925 --- /dev/null +++ b/internal/flamegraph/testfixture.go @@ -0,0 +1,120 @@ +package flamegraph + +import ( + "ior/internal/types" + "strings" +) + +// SeedTestFlameData populates a deterministic static flamegraph fixture. +// Intended for keyboard-navigation validation in TUI test-flame mode. +func SeedTestFlameData(liveTrie *LiveTrie) { + if liveTrie == nil { + return + } + for _, record := range testFlameRecords() { + liveTrie.AddRecord(record) + } +} + +// SeedTestLiveFlameData populates deterministic synthetic data for a given live tick. +// The data shape stays navigable while branch weights shift by phase so the +// terminal flamegraph visibly changes over time. +func SeedTestLiveFlameData(liveTrie *LiveTrie, tick uint64) { + if liveTrie == nil { + return + } + phase := tick % 4 + for _, base := range testFlameRecords() { + weight := liveTestWeight(base, phase) + liveTrie.AddRecord(withTestFlameWeight(base, weight)) + } +} + +func testFlameRecords() []IterRecord { + return []IterRecord{ + newTestFlameRecord("api", "/srv/api/lib/http/client/read", 2001, 2201, types.SYS_ENTER_READ, 180), + newTestFlameRecord("api", "/srv/api/lib/json/encode/write", 2001, 2201, types.SYS_ENTER_WRITE, 120), + newTestFlameRecord("api", "/srv/api/storage/postgres/query/read", 2001, 2201, types.SYS_ENTER_READ, 240), + newTestFlameRecord("api", "/srv/api/storage/postgres/commit/fsync", 2001, 2201, types.SYS_ENTER_FSYNC, 70), + newTestFlameRecord("worker", "/srv/worker/queue/pop/read", 2002, 2202, types.SYS_ENTER_READ, 160), + newTestFlameRecord("worker", "/srv/worker/queue/push/write", 2002, 2202, types.SYS_ENTER_WRITE, 145), + newTestFlameRecord("worker", "/srv/worker/cache/redis/get/read", 2002, 2202, types.SYS_ENTER_READ, 95), + newTestFlameRecord("worker", "/srv/worker/cache/redis/set/write", 2002, 2202, types.SYS_ENTER_WRITE, 90), + newTestFlameRecord("ingest", "/srv/ingest/parser/csv/read", 2003, 2203, types.SYS_ENTER_READ, 110), + newTestFlameRecord("ingest", "/srv/ingest/parser/csv/normalize/write", 2003, 2203, types.SYS_ENTER_WRITE, 80), + newTestFlameRecord("ingest", "/srv/ingest/uploader/s3/put/writev", 2003, 2203, types.SYS_ENTER_WRITEV, 75), + newTestFlameRecord("batch", "/srv/batch/jobs/report/open", 2004, 2204, types.SYS_ENTER_OPENAT, 55), + newTestFlameRecord("batch", "/srv/batch/jobs/report/close", 2004, 2204, types.SYS_ENTER_CLOSE, 35), + newTestFlameRecord("batch", "/srv/batch/jobs/report/rename", 2004, 2204, types.SYS_ENTER_RENAMEAT, 20), + } +} + +func newTestFlameRecord(comm, path string, pid, tid uint32, traceID types.TraceId, weight uint64) IterRecord { + return IterRecord{ + Path: path, + TraceID: traceID, + Comm: comm, + Pid: pid, + Tid: tid, + Cnt: Counter{ + Count: weight, + Duration: weight * 1000, + DurationToPrev: weight * 350, + Bytes: weight * 4096, + }, + } +} + +func withTestFlameWeight(record IterRecord, weight uint64) IterRecord { + record.Cnt = Counter{ + Count: weight, + Duration: weight * 1000, + DurationToPrev: weight * 350, + Bytes: weight * 4096, + } + return record +} + +func liveTestWeight(record IterRecord, phase uint64) uint64 { + base := record.Cnt.Count + multiplier := uint64(1) + + switch phase { + case 0: + if record.Comm == "api" { + multiplier += 4 + } + if strings.Contains(record.Path, "/lib/") { + multiplier += 2 + } + case 1: + if record.Comm == "worker" { + multiplier += 4 + } + if strings.Contains(record.Path, "/queue/") { + multiplier += 2 + } + case 2: + if record.Comm == "ingest" { + multiplier += 4 + } + if strings.Contains(record.Path, "/uploader/") || strings.Contains(record.Path, "/parser/") { + multiplier += 2 + } + case 3: + if record.Comm == "batch" { + multiplier += 4 + } + if strings.Contains(record.Path, "/report/") { + multiplier += 2 + } + } + + if strings.Contains(record.Path, "/storage/") && phase%2 == 0 { + multiplier++ + } + if strings.Contains(record.Path, "/cache/") && phase%2 == 1 { + multiplier++ + } + return base * multiplier +} |
