From e9db062554d5fad3370794d3ae5e37882c38ce7c Mon Sep 17 00:00:00 2001 From: "lqw@128C" Date: Fri, 19 Jun 2026 21:04:05 +0800 Subject: [PATCH 01/15] fix(pkg/proc): add mapping id in location in protobuf --- pkg/proc/protobuf.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/proc/protobuf.go b/pkg/proc/protobuf.go index ba64cae..3de864e 100644 --- a/pkg/proc/protobuf.go +++ b/pkg/proc/protobuf.go @@ -321,10 +321,12 @@ func (b *profileBuilder) pbMapping(tag int, id, base, limit, offset uint64, file func (b *profileBuilder) flush() { b.flushReference() + const dummyMappingID = uint64(1) for i := uint64(5); i < uint64(len(b.strings)); i++ { // write location start := b.pb.startMessage() b.pb.uint64Opt(tagLocation_ID, i) + b.pb.uint64Opt(tagLocation_MappingID, dummyMappingID) b.pbLine(tagLocation_Line, i, 0) b.pb.endMessage(tagProfile_Location, start) @@ -335,7 +337,7 @@ func (b *profileBuilder) flush() { b.pb.endMessage(tagProfile_Function, start) } // just avoid error msg from pprof tool - b.pbMapping(tagProfile_Mapping, uint64(1), uint64(0), uint64(0xff), 0, "-", "", false) + b.pbMapping(tagProfile_Mapping, dummyMappingID, uint64(0), uint64(0xff), 0, "-", "", false) b.pb.strings(tagProfile_StringTable, b.strings) b.zw.Write(b.pb.data) b.zw.Close() From 49eca3ba67eb4aee73ea0b44f6a058d18c74895b Mon Sep 17 00:00:00 2001 From: "lqw@128C" Date: Sat, 20 Jun 2026 21:49:27 +0800 Subject: [PATCH 02/15] simply add stacktrace to pprofIndex --- pkg/proc/protobuf.go | 105 +++++++++++++++++++++++++++++++++++++----- pkg/proc/reference.go | 3 +- 2 files changed, 95 insertions(+), 13 deletions(-) diff --git a/pkg/proc/protobuf.go b/pkg/proc/protobuf.go index 3de864e..fc81bd7 100644 --- a/pkg/proc/protobuf.go +++ b/pkg/proc/protobuf.go @@ -11,7 +11,11 @@ package proc import ( "compress/gzip" + "fmt" "io" + "log" + + "github.com/go-delve/delve/pkg/proc" ) // A protobuf is a simple protocol buffer encoder. @@ -217,9 +221,11 @@ type profileBuilder struct { w io.Writer zw *gzip.Writer - pb protobuf - strings []string - stringMap map[string]int + pb protobuf + strings []string + stringMap map[string]int + funcID uint64 + stkStrIdxMap map[uint64]*pprofIndex // key: indexes, val: *profileNode nodes map[string]*profileNode @@ -247,11 +253,13 @@ func (n *profileNode) GetSize() int64 { func newProfileBuilder(w io.Writer) *profileBuilder { zw, _ := gzip.NewWriterLevel(w, gzip.BestSpeed) b := &profileBuilder{ - w: w, - zw: zw, - strings: []string{""}, - stringMap: map[string]int{"": 0}, - nodes: make(map[string]*profileNode), + w: w, + zw: zw, + strings: []string{""}, + stringMap: map[string]int{"": 0}, + funcID: 5, + stkStrIdxMap: make(map[uint64]*pprofIndex), + nodes: make(map[string]*profileNode), } b.pbValueType(tagProfile_SampleType, "inuse_objects", "count") b.pbValueType(tagProfile_SampleType, "inuse_space", "bytes") @@ -319,23 +327,28 @@ func (b *profileBuilder) pbMapping(tag int, id, base, limit, offset uint64, file b.pb.endMessage(tag, start) } +const dummyMappingID = uint64(1) + func (b *profileBuilder) flush() { - b.flushReference() - const dummyMappingID = uint64(1) for i := uint64(5); i < uint64(len(b.strings)); i++ { + if _, ok := b.stkStrIdxMap[i]; ok { + continue + } // write location start := b.pb.startMessage() b.pb.uint64Opt(tagLocation_ID, i) b.pb.uint64Opt(tagLocation_MappingID, dummyMappingID) - b.pbLine(tagLocation_Line, i, 0) + b.pbLine(tagLocation_Line, b.funcID, 0) b.pb.endMessage(tagProfile_Location, start) // write function start = b.pb.startMessage() - b.pb.uint64Opt(tagFunction_ID, i) + b.pb.uint64Opt(tagFunction_ID, b.funcID) b.pb.int64Opt(tagFunction_Name, int64(i)) b.pb.endMessage(tagProfile_Function, start) + b.funcID++ } + b.flushReference() // just avoid error msg from pprof tool b.pbMapping(tagProfile_Mapping, dummyMappingID, uint64(0), uint64(0xff), 0, "-", "", false) b.pb.strings(tagProfile_StringTable, b.strings) @@ -343,12 +356,80 @@ func (b *profileBuilder) flush() { b.zw.Close() } +func (b *profileBuilder) pbFunc(name, systemName, fileName string, startLine int64) uint64 { + funcID := b.funcID + b.funcID++ + start := b.pb.startMessage() + b.pb.uint64Opt(tagFunction_ID, funcID) + b.pb.int64Opt(tagFunction_Name, b.stringIndex(name)) + if systemName != "" { + b.pb.int64Opt(tagFunction_SystemName, b.stringIndex(systemName)) + } + if fileName != "" { + b.pb.int64Opt(tagFunction_Filename, b.stringIndex(fileName)) + } + if startLine != 0 { + b.pb.int64Opt(tagFunction_StartLine, startLine) + } + b.pb.endMessage(tagProfile_Function, start) + return funcID +} + type pprofIndex struct { idx uint64 prev *pprofIndex depth int } +func createStackTrace(b *profileBuilder, sf []proc.Stackframe, t *proc.Target, g *proc.G) *pprofIndex { + if len(sf) == 0 { + log.Panicf("unable to create pprofIndex for len == 0 stacktrace") + } + var prev *pprofIndex = &pprofIndex{ // the top frame is Goroutine ID + idx: uint64(b.stringIndex(fmt.Sprintf("G%d", g.ID))), + prev: nil, + } + for i := len(sf) - 1; i >= 0; i-- { + currentFn := sf[i].Current.Fn + idx := uint64(b.stringIndex(currentFn.Name)) + cur, ok := b.stkStrIdxMap[idx] + if ok { + prev = cur + continue + } + + _, startLine, _ := t.BinInfo().PCToLine(currentFn.Entry) + funcid := b.pbFunc(currentFn.Name, currentFn.Name, sf[i].Current.File, int64(startLine)) + var inlinefuncid uint64 + if sf[i].Inlined { + inlinefuncid = b.pbFunc(sf[i].Call.Fn.Name, sf[i].Call.Fn.Name, sf[i].Call.File, tagFunction_StartLine) + } + // write location + start := b.pb.startMessage() + b.pb.uint64Opt(tagLocation_ID, idx) + b.pb.uint64Opt(tagLocation_MappingID, dummyMappingID) + b.pb.uint64Opt(tagLocation_Address, sf[i].Current.PC) + b.pbLine(tagLocation_Line, funcid, int64(sf[i].Current.Line)) + if sf[i].Inlined { + b.pbLine(tagLocation_Line, inlinefuncid, int64(sf[i].Call.Line)) + } + b.pb.endMessage(tagProfile_Location, start) + + cur = &pprofIndex{ + idx: idx, + prev: prev, + } + if prev == nil { + cur.depth = 0 + } else { + cur.depth = prev.depth + 1 + } + b.stkStrIdxMap[idx] = cur + prev = cur + } + return prev +} + func (i *pprofIndex) pushHead(pb *profileBuilder, name string) *pprofIndex { if name == "" { return i diff --git a/pkg/proc/reference.go b/pkg/proc/reference.go index ee7dc0e..d7a5dac 100644 --- a/pkg/proc/reference.go +++ b/pkg/proc/reference.go @@ -555,6 +555,7 @@ func ObjectReference(t *proc.Target, filename string) (*ObjRefScope, error) { if err != nil { return nil, err } + defer f.Close() s := &ObjRefScope{ HeapScope: heapScope, @@ -608,7 +609,7 @@ func ObjectReference(t *proc.Target, filename string) (*ObjRefScope, error) { } l.Name = sf[i].Current.Fn.Name + "." + l.Name rv := ToReferenceVariable(l) - s.findRef(rv, nil) + s.findRef(rv, createStackTrace(s.pb, sf[i:], t, gr)) rvpool.Put(rv) } } From 6e9aaadd72a29d076914607c91ddfeb4329609ae Mon Sep 17 00:00:00 2001 From: "lqw@128C" Date: Tue, 23 Jun 2026 16:09:16 +0800 Subject: [PATCH 03/15] fix: avoid sharing pprofIndex node in stacktrace forest --- pkg/proc/protobuf.go | 86 ++++++++++++++++-------------------- test/testdata/manygo/main.go | 72 ++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 48 deletions(-) create mode 100644 test/testdata/manygo/main.go diff --git a/pkg/proc/protobuf.go b/pkg/proc/protobuf.go index fc81bd7..4c87829 100644 --- a/pkg/proc/protobuf.go +++ b/pkg/proc/protobuf.go @@ -221,11 +221,11 @@ type profileBuilder struct { w io.Writer zw *gzip.Writer - pb protobuf - strings []string - stringMap map[string]int - funcID uint64 - stkStrIdxMap map[uint64]*pprofIndex + pb protobuf + strings []string + stringMap map[string]int + funcID uint64 + funcNameStrIdxSet map[uint64]bool // key: indexes, val: *profileNode nodes map[string]*profileNode @@ -253,13 +253,13 @@ func (n *profileNode) GetSize() int64 { func newProfileBuilder(w io.Writer) *profileBuilder { zw, _ := gzip.NewWriterLevel(w, gzip.BestSpeed) b := &profileBuilder{ - w: w, - zw: zw, - strings: []string{""}, - stringMap: map[string]int{"": 0}, - funcID: 5, - stkStrIdxMap: make(map[uint64]*pprofIndex), - nodes: make(map[string]*profileNode), + w: w, + zw: zw, + strings: []string{""}, + stringMap: map[string]int{"": 0}, + funcID: 5, + funcNameStrIdxSet: make(map[uint64]bool), + nodes: make(map[string]*profileNode), } b.pbValueType(tagProfile_SampleType, "inuse_objects", "count") b.pbValueType(tagProfile_SampleType, "inuse_space", "bytes") @@ -331,7 +331,7 @@ const dummyMappingID = uint64(1) func (b *profileBuilder) flush() { for i := uint64(5); i < uint64(len(b.strings)); i++ { - if _, ok := b.stkStrIdxMap[i]; ok { + if _, ok := b.funcNameStrIdxSet[i]; ok { continue } // write location @@ -386,45 +386,35 @@ func createStackTrace(b *profileBuilder, sf []proc.Stackframe, t *proc.Target, g log.Panicf("unable to create pprofIndex for len == 0 stacktrace") } var prev *pprofIndex = &pprofIndex{ // the top frame is Goroutine ID - idx: uint64(b.stringIndex(fmt.Sprintf("G%d", g.ID))), - prev: nil, + idx: uint64(b.stringIndex(fmt.Sprintf("G%d", g.ID))), + prev: nil, + depth: 0, } for i := len(sf) - 1; i >= 0; i-- { currentFn := sf[i].Current.Fn - idx := uint64(b.stringIndex(currentFn.Name)) - cur, ok := b.stkStrIdxMap[idx] - if ok { - prev = cur - continue - } - - _, startLine, _ := t.BinInfo().PCToLine(currentFn.Entry) - funcid := b.pbFunc(currentFn.Name, currentFn.Name, sf[i].Current.File, int64(startLine)) - var inlinefuncid uint64 - if sf[i].Inlined { - inlinefuncid = b.pbFunc(sf[i].Call.Fn.Name, sf[i].Call.Fn.Name, sf[i].Call.File, tagFunction_StartLine) - } - // write location - start := b.pb.startMessage() - b.pb.uint64Opt(tagLocation_ID, idx) - b.pb.uint64Opt(tagLocation_MappingID, dummyMappingID) - b.pb.uint64Opt(tagLocation_Address, sf[i].Current.PC) - b.pbLine(tagLocation_Line, funcid, int64(sf[i].Current.Line)) - if sf[i].Inlined { - b.pbLine(tagLocation_Line, inlinefuncid, int64(sf[i].Call.Line)) - } - b.pb.endMessage(tagProfile_Location, start) - - cur = &pprofIndex{ - idx: idx, - prev: prev, - } - if prev == nil { - cur.depth = 0 - } else { - cur.depth = prev.depth + 1 + cur := prev.pushHead(b, currentFn.Name) + idx := cur.idx + if _, ok := b.funcNameStrIdxSet[idx]; !ok { + // Do following things if function appears for the first time + // write function + _, startLine, _ := t.BinInfo().PCToLine(currentFn.Entry) + funcid := b.pbFunc(currentFn.Name, currentFn.Name, sf[i].Current.File, int64(startLine)) + var inlinefuncid uint64 + if sf[i].Inlined { + inlinefuncid = b.pbFunc(sf[i].Call.Fn.Name, sf[i].Call.Fn.Name, sf[i].Call.File, tagFunction_StartLine) + } + // write location + start := b.pb.startMessage() + b.pb.uint64Opt(tagLocation_ID, idx) + b.pb.uint64Opt(tagLocation_MappingID, dummyMappingID) + b.pb.uint64Opt(tagLocation_Address, sf[i].Current.PC) + b.pbLine(tagLocation_Line, funcid, int64(sf[i].Current.Line)) + if sf[i].Inlined { + b.pbLine(tagLocation_Line, inlinefuncid, int64(sf[i].Call.Line)) + } + b.pb.endMessage(tagProfile_Location, start) + b.funcNameStrIdxSet[idx] = true } - b.stkStrIdxMap[idx] = cur prev = cur } return prev diff --git a/test/testdata/manygo/main.go b/test/testdata/manygo/main.go new file mode 100644 index 0000000..34948dc --- /dev/null +++ b/test/testdata/manygo/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "fmt" + "os" + "time" +) + +type LargeObjT struct { + data [100]int + id int +} + +type FnType func(count int, done chan bool, lastIntCh chan int) + +var dynFuncs [6]FnType = [6]FnType{ + f1, f2, f3, f1, f2, f3, +} + +func main() { + fmt.Println("pid", os.Getpid()) + done := make(chan bool) + lastIntCh := make(chan int) + ch := time.After(1 * time.Minute) + for i := range 6 { + go dynFuncs[i](10, done, lastIntCh) + } + sum := 0 + for { + select { + case <-ch: + for range 6 { + done <- true + } + case n := <-lastIntCh: + sum += n + for range 5 { + sum += <-lastIntCh + } + break + default: + } + } + fmt.Println(sum) +} + +func f1(count int, done chan bool, lastIntCh chan int) { + s := make([]LargeObjT, count) + for i := range count { + s[i].id = i + } + <-done + lastIntCh <- s[count-1].id +} + +func f2(count int, done chan bool, lastIntCh chan int) { + s := make([]LargeObjT, count*2) + for i := range count * 2 { + s[i].id = i + } + <-done + lastIntCh <- s[count*2-1].id +} + +func f3(count int, done chan bool, lastIntCh chan int) { + s := make([]LargeObjT, count*3) + for i := range count * 3 { + s[i].id = i + } + <-done + lastIntCh <- s[count*3-1].id +} From ec948c41345f9b4064df67d4e00c81be152e0142 Mon Sep 17 00:00:00 2001 From: "lqw@wsl" Date: Wed, 8 Jul 2026 11:38:23 +0800 Subject: [PATCH 04/15] feat: add stk_obj_split dummy frame to split stacktrace and object reference graph --- pkg/proc/protobuf.go | 16 +++++++++++++++- test/framework.go | 12 +++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/pkg/proc/protobuf.go b/pkg/proc/protobuf.go index 4c87829..2594e61 100644 --- a/pkg/proc/protobuf.go +++ b/pkg/proc/protobuf.go @@ -350,7 +350,7 @@ func (b *profileBuilder) flush() { } b.flushReference() // just avoid error msg from pprof tool - b.pbMapping(tagProfile_Mapping, dummyMappingID, uint64(0), uint64(0xff), 0, "-", "", false) + b.pbMapping(tagProfile_Mapping, dummyMappingID, uint64(0), uint64(0xff), 0, "", "", false) b.pb.strings(tagProfile_StringTable, b.strings) b.zw.Write(b.pb.data) b.zw.Close() @@ -381,6 +381,8 @@ type pprofIndex struct { depth int } +const StackTraceObjSplitLine string = "stk_obj_split" + func createStackTrace(b *profileBuilder, sf []proc.Stackframe, t *proc.Target, g *proc.G) *pprofIndex { if len(sf) == 0 { log.Panicf("unable to create pprofIndex for len == 0 stacktrace") @@ -417,6 +419,18 @@ func createStackTrace(b *profileBuilder, sf []proc.Stackframe, t *proc.Target, g } prev = cur } + // add stk_obj_split to separate the stack trace of object reference from the stack trace of goroutine + prev = prev.pushHead(b, StackTraceObjSplitLine) + idx := prev.idx + if _, ok := b.funcNameStrIdxSet[idx]; !ok { + funcid := b.pbFunc(StackTraceObjSplitLine, StackTraceObjSplitLine, "", 0) + start := b.pb.startMessage() + b.pb.uint64Opt(tagLocation_ID, idx) + b.pb.uint64Opt(tagLocation_MappingID, dummyMappingID) + b.pbLine(tagLocation_Line, funcid, 0) + b.pb.endMessage(tagProfile_Location, start) + b.funcNameStrIdxSet[idx] = true + } return prev } diff --git a/test/framework.go b/test/framework.go index c2458ca..c4d40c6 100644 --- a/test/framework.go +++ b/test/framework.go @@ -21,6 +21,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "sort" "strings" "testing" @@ -376,11 +377,16 @@ func (tf *TestFramework) buildMemoryTreeFromNodes(nodes map[string]ProfileNodeIn continue // Skip empty paths } - leaf := nodePath[len(nodePath)-1] + leafIdx := len(nodePath) - 1 + // omit stacktrace if exists, and use the last node before the stacktrace as the leaf node + if idx := slices.Index(nodePath, gorefproc.StackTraceObjSplitLine); idx != -1 { + leafIdx = idx - 1 + } + leaf := nodePath[leafIdx] for _, prefix := range rootPrefixes { if strings.HasPrefix(leaf, prefix) { - matchedRootNodes++ - tf.createOrUpdateNode(root, nodePath, node.GetCount(), node.GetSize()) + matchedRootNodes++ // only objects whose name starts with the specified root prefixes are counted + tf.createOrUpdateNode(root, nodePath[:leafIdx+1], node.GetCount(), node.GetSize()) break } } From 6d2661379adb87951fb2b1b4942d281612763486 Mon Sep 17 00:00:00 2001 From: "lqw@wsl" Date: Tue, 14 Jul 2026 20:18:17 +0800 Subject: [PATCH 05/15] remove G in the top of flamegraph remove G, the bottom of stacktrace --- pkg/proc/protobuf.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/pkg/proc/protobuf.go b/pkg/proc/protobuf.go index 2594e61..1dd9a80 100644 --- a/pkg/proc/protobuf.go +++ b/pkg/proc/protobuf.go @@ -11,7 +11,6 @@ package proc import ( "compress/gzip" - "fmt" "io" "log" @@ -387,11 +386,7 @@ func createStackTrace(b *profileBuilder, sf []proc.Stackframe, t *proc.Target, g if len(sf) == 0 { log.Panicf("unable to create pprofIndex for len == 0 stacktrace") } - var prev *pprofIndex = &pprofIndex{ // the top frame is Goroutine ID - idx: uint64(b.stringIndex(fmt.Sprintf("G%d", g.ID))), - prev: nil, - depth: 0, - } + var prev *pprofIndex for i := len(sf) - 1; i >= 0; i-- { currentFn := sf[i].Current.Fn cur := prev.pushHead(b, currentFn.Name) From 34d9fd7fdc02c692222b097826ced8000b12f7f8 Mon Sep 17 00:00:00 2001 From: "lqw@wsl" Date: Tue, 14 Jul 2026 22:26:47 +0800 Subject: [PATCH 06/15] test: add StackTraceScenario --- test/framework.go | 119 +++++++++++++++++++++++++++++++++++++-- test/integration_test.go | 1 + test/scenarios.go | 90 +++++++++++++++++++++++++++++ 3 files changed, 205 insertions(+), 5 deletions(-) diff --git a/test/framework.go b/test/framework.go index c4d40c6..5dd869f 100644 --- a/test/framework.go +++ b/test/framework.go @@ -34,16 +34,19 @@ import ( // TestScenario defines a complete test scenario type TestScenario struct { - Name string - Code string - Expected *MemoryNode - Timeout time.Duration + Name string + Code string + Expected *MemoryNode + ExpectedStackTrace *StackTraceNode + Timeout time.Duration // RootPrefixes limits tree roots to nodes whose leaf name has one of these prefixes. // If empty, the framework defaults to []string{"main."}. RootPrefixes []string // AllowExtraChildren relaxes strict tree matching by allowing actual nodes // to have children not listed in Expected. AllowExtraChildren bool + // MatchStackTraceExactly enforces strict stack trace matching, requiring the actual stack trace to match the expected one exactly. + MatchStackTraceExactly bool } // TestFramework manages integration test execution @@ -187,12 +190,18 @@ func (tf *TestFramework) validateResults(scope *gorefproc.ObjRefScope, scenario nodeInterfaces[k] = ProfileNodeInterface(v) } actualNode := tf.buildMemoryTreeFromNodes(nodeInterfaces, stringTable, scenario.RootPrefixes) + actualStackTrace := tf.buildStackTraceFromNodes(nodeInterfaces, stringTable) // Compare nodes if err := tf.compareNodes(scenario.Expected, actualNode, scenario.AllowExtraChildren); err != nil { return fmt.Errorf("node comparison failed: %v", err) } + // Compare stacktrace + if err := tf.compareStackTraceNodes(scenario.ExpectedStackTrace, actualStackTrace, true); err != nil { + return fmt.Errorf("stacktrace comparison failed: %v", err) + } + tf.t.Logf(" ✓ Memory node validation passed") return nil } @@ -267,7 +276,64 @@ func (tf *TestFramework) compareNodes(expected, actual *MemoryNode, allowExtraCh return nil } -func sortedNodeNames(children map[string]*MemoryNode) []string { +func (tf *TestFramework) compareStackTraceNodes(expected, actual *StackTraceNode, allowExtraStackTraceChildren bool) error { + if expected == nil { + if actual == nil { + return nil + } + if allowExtraStackTraceChildren { + return nil + } + } + if actual == nil { + tf.t.Logf(" ✗ StackTrace mismatch: expected %v, actual ", expected) + return fmt.Errorf("stacktrace mismatch, actual is nil when expected is %v", expected.FuncName) + } + if expected.FuncName != actual.FuncName { + tf.t.Logf(" ✗ StackTrace FuncName mismatch: expected %s, actual %s", expected.FuncName, actual.FuncName) + return fmt.Errorf("stacktrace funcname mismatch") + } + + // Compare children + expectedChildren := make(map[string]*StackTraceNode) + actualChildren := make(map[string]*StackTraceNode) + + for _, child := range expected.Children { + expectedChildren[child.FuncName] = child + } + for _, child := range actual.Children { + actualChildren[child.FuncName] = child + } + + // Check for missing children + for name, expectedChild := range expectedChildren { + actualChild, found := actualChildren[name] + if !found { + tf.t.Logf(" ✗ Missing stacktrace child node: %s.%s", expected.FuncName, name) + tf.t.Logf(" Actual stacktrace children: %v", sortedNodeNames(actualChildren)) + return fmt.Errorf("missing stacktrace child node: %s.%s", expected.FuncName, name) + } + + // Recursively compare child nodes + if err := tf.compareStackTraceNodes(expectedChild, actualChild, allowExtraStackTraceChildren); err != nil { + return err + } + } + + // Check for unexpected children + if !allowExtraStackTraceChildren { + for name := range actualChildren { + if _, found := expectedChildren[name]; !found { + tf.t.Logf(" ✗ Unexpected stacktrace child node: %s.%s", expected.FuncName, name) + return fmt.Errorf("unexpected stacktrace child node: %s.%s", expected.FuncName, name) + } + } + } + + return nil +} + +func sortedNodeNames[T any](children map[string]T) []string { names := make([]string, 0, len(children)) for name := range children { names = append(names, name) @@ -361,6 +427,11 @@ type MemoryNode struct { Children []*MemoryNode `json:"children,omitempty"` // Child nodes } +type StackTraceNode struct { + FuncName string `json:"func_name,omitempty"` // Function name (e.g., "main.main", "runtime.main") + Children []*StackTraceNode `json:"children,omitempty"` // Child nodes representing stack trace hierarchy +} + // buildMemoryTreeFromNodes builds a memory reference node from goref profile nodes. func (tf *TestFramework) buildMemoryTreeFromNodes(nodes map[string]ProfileNodeInterface, stringTable, rootPrefixes []string) *MemoryNode { root := &MemoryNode{Children: []*MemoryNode{}} @@ -454,6 +525,44 @@ func (tf *TestFramework) createOrUpdateNode(node *MemoryNode, path []string, cou tf.createOrUpdateNode(child, path[:len(path)-1], count, size) } +func (tf *TestFramework) buildStackTraceFromNodes(nodes map[string]ProfileNodeInterface, stringTable []string) *StackTraceNode { + root := &StackTraceNode{Children: []*StackTraceNode{}} + + for key := range nodes { + nodePath := tf.extractNodePathFromKey(key, stringTable) + if nodePath == nil { + continue + } + + var splitLineIdx int + if splitLineIdx = slices.Index(nodePath, gorefproc.StackTraceObjSplitLine); splitLineIdx == -1 { + continue + } + tf.createOrUpdateStackTraceNode(root, nodePath[splitLineIdx+1:len(nodePath)-1]) + } + return root +} + +func (tf *TestFramework) createOrUpdateStackTraceNode(node *StackTraceNode, path []string) { + if len(path) == 0 { + return + } + funcName := path[len(path)-1] + for _, child := range node.Children { + if child.FuncName == funcName { + tf.createOrUpdateStackTraceNode(child, path[:len(path)-1]) + return + } + } + // Create new node + child := &StackTraceNode{ + FuncName: funcName, + } + + node.Children = append(node.Children, child) + tf.createOrUpdateStackTraceNode(child, path[:len(path)-1]) +} + // extractTypeFromKey extracts name and type information from the key path func (tf *TestFramework) extractNameAndTypeFromPath(path string) (string, string) { // Simple type extraction - can be enhanced later diff --git a/test/integration_test.go b/test/integration_test.go index 740c9f0..dde7888 100644 --- a/test/integration_test.go +++ b/test/integration_test.go @@ -33,6 +33,7 @@ var testCases = []TestScenario{ ChannelScenario, MallocHeaderHiddenTypeScenario, CircularReferenceScenario, + StackTraceScenario, } // TestScenarios runs individual test scenarios using table-driven approach diff --git a/test/scenarios.go b/test/scenarios.go index 7293e3b..b46a962 100644 --- a/test/scenarios.go +++ b/test/scenarios.go @@ -47,6 +47,18 @@ func main() { }, }, }, + ExpectedStackTrace: &StackTraceNode{ + Children: []*StackTraceNode{ + { + FuncName: "runtime.main", + Children: []*StackTraceNode{ + { + FuncName: "main.main", + }, + }, + }, + }, + }, Timeout: 30 * time.Second, } @@ -809,3 +821,81 @@ func main() { }, Timeout: 30 * time.Second, } + +var StackTraceScenario = TestScenario{ + Name: "stack trace validation", + Code: `package main + +import ( + "fmt" + "os" + "runtime" + "time" +) + +type Node struct { + data int + next *Node +} + +func bar(ch chan *int, a int) { + n := &Node{ + data: a, + } + ch <- &n.data +} + +func foo(ch chan *int, a int) { + bar(ch, a) +} + +func main() { + ch := make(chan *int) + go foo(ch, 1) + go bar(ch, 2) + fmt.Println("READY") + fmt.Println(os.Getpid()) + time.Sleep(100 * time.Second) + sum := 0 + for pa := range ch { + sum += *pa + } + fmt.Println(sum) + runtime.KeepAlive(ch) +}`, + Expected: &MemoryNode{ + Children: []*MemoryNode{ + { + Name: "main.bar.n", + Size: ExactValue(32), + }, + }, + }, + ExpectedStackTrace: &StackTraceNode{ + Children: []*StackTraceNode{ + { + FuncName: "main.main.gowrap1", + Children: []*StackTraceNode{ + { + FuncName: "main.foo", + Children: []*StackTraceNode{ + { + FuncName: "main.bar", + }, + }, + }, + }, + }, + { + FuncName: "main.main.gowrap2", + Children: []*StackTraceNode{ + { + FuncName: "main.bar", + }, + }, + }, + }, + }, + AllowExtraChildren: true, + Timeout: 30 * time.Second, +} From 6176ded901e9f3d174ea5af9180061a59533eca8 Mon Sep 17 00:00:00 2001 From: "lqw@wsl" Date: Wed, 15 Jul 2026 14:28:24 +0800 Subject: [PATCH 07/15] refactor: rename StackTraceObjSplitLine with ReferenceStackBoundary --- pkg/proc/protobuf.go | 6 +++--- test/framework.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/proc/protobuf.go b/pkg/proc/protobuf.go index 1dd9a80..0fb9ac2 100644 --- a/pkg/proc/protobuf.go +++ b/pkg/proc/protobuf.go @@ -380,7 +380,7 @@ type pprofIndex struct { depth int } -const StackTraceObjSplitLine string = "stk_obj_split" +const ReferenceStackBoundary string = "[goref:reference-stack-boundary]" func createStackTrace(b *profileBuilder, sf []proc.Stackframe, t *proc.Target, g *proc.G) *pprofIndex { if len(sf) == 0 { @@ -415,10 +415,10 @@ func createStackTrace(b *profileBuilder, sf []proc.Stackframe, t *proc.Target, g prev = cur } // add stk_obj_split to separate the stack trace of object reference from the stack trace of goroutine - prev = prev.pushHead(b, StackTraceObjSplitLine) + prev = prev.pushHead(b, ReferenceStackBoundary) idx := prev.idx if _, ok := b.funcNameStrIdxSet[idx]; !ok { - funcid := b.pbFunc(StackTraceObjSplitLine, StackTraceObjSplitLine, "", 0) + funcid := b.pbFunc(ReferenceStackBoundary, ReferenceStackBoundary, "", 0) start := b.pb.startMessage() b.pb.uint64Opt(tagLocation_ID, idx) b.pb.uint64Opt(tagLocation_MappingID, dummyMappingID) diff --git a/test/framework.go b/test/framework.go index 5dd869f..a48b99e 100644 --- a/test/framework.go +++ b/test/framework.go @@ -450,7 +450,7 @@ func (tf *TestFramework) buildMemoryTreeFromNodes(nodes map[string]ProfileNodeIn leafIdx := len(nodePath) - 1 // omit stacktrace if exists, and use the last node before the stacktrace as the leaf node - if idx := slices.Index(nodePath, gorefproc.StackTraceObjSplitLine); idx != -1 { + if idx := slices.Index(nodePath, gorefproc.ReferenceStackBoundary); idx != -1 { leafIdx = idx - 1 } leaf := nodePath[leafIdx] @@ -535,7 +535,7 @@ func (tf *TestFramework) buildStackTraceFromNodes(nodes map[string]ProfileNodeIn } var splitLineIdx int - if splitLineIdx = slices.Index(nodePath, gorefproc.StackTraceObjSplitLine); splitLineIdx == -1 { + if splitLineIdx = slices.Index(nodePath, gorefproc.ReferenceStackBoundary); splitLineIdx == -1 { continue } tf.createOrUpdateStackTraceNode(root, nodePath[splitLineIdx+1:len(nodePath)-1]) From 8ae7f8e5e251243324ba908d178417d23a938ae8 Mon Sep 17 00:00:00 2001 From: "lqw@wsl" Date: Wed, 15 Jul 2026 15:56:30 +0800 Subject: [PATCH 08/15] simplify stacktrace to function-level 1. remove file/line/PC metadata. 2. init stacktrace for each goroutine and reuse the stacktrace --- pkg/proc/protobuf.go | 128 ++++++++++++------------------------------ pkg/proc/reference.go | 3 +- 2 files changed, 38 insertions(+), 93 deletions(-) diff --git a/pkg/proc/protobuf.go b/pkg/proc/protobuf.go index 0fb9ac2..033605e 100644 --- a/pkg/proc/protobuf.go +++ b/pkg/proc/protobuf.go @@ -12,7 +12,6 @@ package proc import ( "compress/gzip" "io" - "log" "github.com/go-delve/delve/pkg/proc" ) @@ -220,11 +219,9 @@ type profileBuilder struct { w io.Writer zw *gzip.Writer - pb protobuf - strings []string - stringMap map[string]int - funcID uint64 - funcNameStrIdxSet map[uint64]bool + pb protobuf + strings []string + stringMap map[string]int // key: indexes, val: *profileNode nodes map[string]*profileNode @@ -252,13 +249,11 @@ func (n *profileNode) GetSize() int64 { func newProfileBuilder(w io.Writer) *profileBuilder { zw, _ := gzip.NewWriterLevel(w, gzip.BestSpeed) b := &profileBuilder{ - w: w, - zw: zw, - strings: []string{""}, - stringMap: map[string]int{"": 0}, - funcID: 5, - funcNameStrIdxSet: make(map[uint64]bool), - nodes: make(map[string]*profileNode), + w: w, + zw: zw, + strings: []string{""}, + stringMap: map[string]int{"": 0}, + nodes: make(map[string]*profileNode), } b.pbValueType(tagProfile_SampleType, "inuse_objects", "count") b.pbValueType(tagProfile_SampleType, "inuse_space", "bytes") @@ -330,22 +325,18 @@ const dummyMappingID = uint64(1) func (b *profileBuilder) flush() { for i := uint64(5); i < uint64(len(b.strings)); i++ { - if _, ok := b.funcNameStrIdxSet[i]; ok { - continue - } // write location start := b.pb.startMessage() b.pb.uint64Opt(tagLocation_ID, i) b.pb.uint64Opt(tagLocation_MappingID, dummyMappingID) - b.pbLine(tagLocation_Line, b.funcID, 0) + b.pbLine(tagLocation_Line, i, 0) b.pb.endMessage(tagProfile_Location, start) // write function start = b.pb.startMessage() - b.pb.uint64Opt(tagFunction_ID, b.funcID) + b.pb.uint64Opt(tagFunction_ID, i) b.pb.int64Opt(tagFunction_Name, int64(i)) b.pb.endMessage(tagProfile_Function, start) - b.funcID++ } b.flushReference() // just avoid error msg from pprof tool @@ -355,25 +346,6 @@ func (b *profileBuilder) flush() { b.zw.Close() } -func (b *profileBuilder) pbFunc(name, systemName, fileName string, startLine int64) uint64 { - funcID := b.funcID - b.funcID++ - start := b.pb.startMessage() - b.pb.uint64Opt(tagFunction_ID, funcID) - b.pb.int64Opt(tagFunction_Name, b.stringIndex(name)) - if systemName != "" { - b.pb.int64Opt(tagFunction_SystemName, b.stringIndex(systemName)) - } - if fileName != "" { - b.pb.int64Opt(tagFunction_Filename, b.stringIndex(fileName)) - } - if startLine != 0 { - b.pb.int64Opt(tagFunction_StartLine, startLine) - } - b.pb.endMessage(tagProfile_Function, start) - return funcID -} - type pprofIndex struct { idx uint64 prev *pprofIndex @@ -382,68 +354,40 @@ type pprofIndex struct { const ReferenceStackBoundary string = "[goref:reference-stack-boundary]" -func createStackTrace(b *profileBuilder, sf []proc.Stackframe, t *proc.Target, g *proc.G) *pprofIndex { - if len(sf) == 0 { - log.Panicf("unable to create pprofIndex for len == 0 stacktrace") - } +// initStackTrace create the stackframe pprofIndexes from right(bottom of stack) to left(top of stack) with ReferenceStackBoundary as the new top frame. +func initStackTrace(b *profileBuilder, sfs []proc.Stackframe) (frameIndexes []*pprofIndex) { var prev *pprofIndex - for i := len(sf) - 1; i >= 0; i-- { - currentFn := sf[i].Current.Fn - cur := prev.pushHead(b, currentFn.Name) - idx := cur.idx - if _, ok := b.funcNameStrIdxSet[idx]; !ok { - // Do following things if function appears for the first time - // write function - _, startLine, _ := t.BinInfo().PCToLine(currentFn.Entry) - funcid := b.pbFunc(currentFn.Name, currentFn.Name, sf[i].Current.File, int64(startLine)) - var inlinefuncid uint64 - if sf[i].Inlined { - inlinefuncid = b.pbFunc(sf[i].Call.Fn.Name, sf[i].Call.Fn.Name, sf[i].Call.File, tagFunction_StartLine) - } - // write location - start := b.pb.startMessage() - b.pb.uint64Opt(tagLocation_ID, idx) - b.pb.uint64Opt(tagLocation_MappingID, dummyMappingID) - b.pb.uint64Opt(tagLocation_Address, sf[i].Current.PC) - b.pbLine(tagLocation_Line, funcid, int64(sf[i].Current.Line)) - if sf[i].Inlined { - b.pbLine(tagLocation_Line, inlinefuncid, int64(sf[i].Call.Line)) - } - b.pb.endMessage(tagProfile_Location, start) - b.funcNameStrIdxSet[idx] = true - } - prev = cur - } - // add stk_obj_split to separate the stack trace of object reference from the stack trace of goroutine - prev = prev.pushHead(b, ReferenceStackBoundary) - idx := prev.idx - if _, ok := b.funcNameStrIdxSet[idx]; !ok { - funcid := b.pbFunc(ReferenceStackBoundary, ReferenceStackBoundary, "", 0) - start := b.pb.startMessage() - b.pb.uint64Opt(tagLocation_ID, idx) - b.pb.uint64Opt(tagLocation_MappingID, dummyMappingID) - b.pbLine(tagLocation_Line, funcid, 0) - b.pb.endMessage(tagProfile_Location, start) - b.funcNameStrIdxSet[idx] = true + frameIndexes = make([]*pprofIndex, len(sfs)) + for i := len(sfs) - 1; i >= 0; i-- { + prev = newHeadWithDepth(b, prev, sfs[i].Current.Fn.Name, -1) + frameIndexes[i] = prev } - return prev + return +} + +func (i *pprofIndex) pushReferenceStackBoundary(pb *profileBuilder) *pprofIndex { + return newHeadWithDepth(pb, i, ReferenceStackBoundary, -1) } func (i *pprofIndex) pushHead(pb *profileBuilder, name string) *pprofIndex { - if name == "" { - return i - } - idx := uint64(pb.stringIndex(name)) - pi := &pprofIndex{ - prev: i, - idx: idx, - } + var depth int if i == nil { - pi.depth = 0 + depth = 0 } else { - pi.depth = i.depth + 1 + depth = i.depth + 1 + } + return newHeadWithDepth(pb, i, name, depth) +} + +func newHeadWithDepth(pb *profileBuilder, prev *pprofIndex, name string, depth int) *pprofIndex { + if name == "" { + return prev + } + return &pprofIndex{ + prev: prev, + idx: uint64(pb.stringIndex(name)), + depth: depth, } - return pi } func (i *pprofIndex) indexes() (res []uint64) { diff --git a/pkg/proc/reference.go b/pkg/proc/reference.go index d7a5dac..855cb5e 100644 --- a/pkg/proc/reference.go +++ b/pkg/proc/reference.go @@ -596,6 +596,7 @@ func ObjectReference(t *proc.Target, filename string) (*ObjRefScope, error) { sf, _ := proc.GoroutineStacktrace(t, gr, 1024, 0) s.g.init(Address(lo), Address(hi), s.stackPtrMask(Address(lo), Address(hi), sf)) if len(sf) > 0 { + sfIndexes := initStackTrace(s.pb, sf) for i := range sf { es := proc.FrameToScope(t, t.Memory(), gr, threadID, sf[i:]...) locals, err := es.Locals(0, "") @@ -609,7 +610,7 @@ func ObjectReference(t *proc.Target, filename string) (*ObjRefScope, error) { } l.Name = sf[i].Current.Fn.Name + "." + l.Name rv := ToReferenceVariable(l) - s.findRef(rv, createStackTrace(s.pb, sf[i:], t, gr)) + s.findRef(rv, sfIndexes[i].pushReferenceStackBoundary(s.pb)) rvpool.Put(rv) } } From e1cc2262c264aadf9cbc976dc19a8446fd731276 Mon Sep 17 00:00:00 2001 From: "lqw@wsl" Date: Wed, 15 Jul 2026 16:34:31 +0800 Subject: [PATCH 09/15] refactor: replace depth -1 with stackTracePprofIndexDepth const --- pkg/proc/protobuf.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/proc/protobuf.go b/pkg/proc/protobuf.go index 033605e..196b49f 100644 --- a/pkg/proc/protobuf.go +++ b/pkg/proc/protobuf.go @@ -352,6 +352,8 @@ type pprofIndex struct { depth int } +const stackTracePprofIndexDepth int = -1 + const ReferenceStackBoundary string = "[goref:reference-stack-boundary]" // initStackTrace create the stackframe pprofIndexes from right(bottom of stack) to left(top of stack) with ReferenceStackBoundary as the new top frame. @@ -359,14 +361,14 @@ func initStackTrace(b *profileBuilder, sfs []proc.Stackframe) (frameIndexes []*p var prev *pprofIndex frameIndexes = make([]*pprofIndex, len(sfs)) for i := len(sfs) - 1; i >= 0; i-- { - prev = newHeadWithDepth(b, prev, sfs[i].Current.Fn.Name, -1) + prev = newHeadWithDepth(b, prev, sfs[i].Current.Fn.Name, stackTracePprofIndexDepth) frameIndexes[i] = prev } return } func (i *pprofIndex) pushReferenceStackBoundary(pb *profileBuilder) *pprofIndex { - return newHeadWithDepth(pb, i, ReferenceStackBoundary, -1) + return newHeadWithDepth(pb, i, ReferenceStackBoundary, stackTracePprofIndexDepth) } func (i *pprofIndex) pushHead(pb *profileBuilder, name string) *pprofIndex { From 79e268610367c16964a76038ccf63d195082a209 Mon Sep 17 00:00:00 2001 From: "lqw@wsl" Date: Thu, 16 Jul 2026 20:45:31 +0800 Subject: [PATCH 10/15] refactor: remove unused field --- test/framework.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/framework.go b/test/framework.go index a48b99e..f49d8ed 100644 --- a/test/framework.go +++ b/test/framework.go @@ -45,8 +45,6 @@ type TestScenario struct { // AllowExtraChildren relaxes strict tree matching by allowing actual nodes // to have children not listed in Expected. AllowExtraChildren bool - // MatchStackTraceExactly enforces strict stack trace matching, requiring the actual stack trace to match the expected one exactly. - MatchStackTraceExactly bool } // TestFramework manages integration test execution From 340330252f90e0d03348dfafb05badac8e2bac13 Mon Sep 17 00:00:00 2001 From: "lqw@wsl" Date: Sat, 25 Jul 2026 22:02:28 +0800 Subject: [PATCH 11/15] fix: add nil-safe helper to get function name of stackframe --- pkg/proc/protobuf.go | 13 ++++++++++++- pkg/proc/reference.go | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/pkg/proc/protobuf.go b/pkg/proc/protobuf.go index 196b49f..732a69d 100644 --- a/pkg/proc/protobuf.go +++ b/pkg/proc/protobuf.go @@ -356,12 +356,23 @@ const stackTracePprofIndexDepth int = -1 const ReferenceStackBoundary string = "[goref:reference-stack-boundary]" +// getStackFrameFuncName returns the function name of the stack frame. If the function is nil, it returns "". +func getStackFrameFuncName(sf proc.Stackframe) string { + if sf.Call.Fn != nil { + return sf.Call.Fn.Name + } + if sf.Current.Fn != nil { + return sf.Current.Fn.Name + } + return "" +} + // initStackTrace create the stackframe pprofIndexes from right(bottom of stack) to left(top of stack) with ReferenceStackBoundary as the new top frame. func initStackTrace(b *profileBuilder, sfs []proc.Stackframe) (frameIndexes []*pprofIndex) { var prev *pprofIndex frameIndexes = make([]*pprofIndex, len(sfs)) for i := len(sfs) - 1; i >= 0; i-- { - prev = newHeadWithDepth(b, prev, sfs[i].Current.Fn.Name, stackTracePprofIndexDepth) + prev = newHeadWithDepth(b, prev, getStackFrameFuncName(sfs[i]), stackTracePprofIndexDepth) frameIndexes[i] = prev } return diff --git a/pkg/proc/reference.go b/pkg/proc/reference.go index 855cb5e..d2896b6 100644 --- a/pkg/proc/reference.go +++ b/pkg/proc/reference.go @@ -608,7 +608,7 @@ func ObjectReference(t *proc.Target, filename string) (*ObjRefScope, error) { if l.Addr == 0 { continue } - l.Name = sf[i].Current.Fn.Name + "." + l.Name + l.Name = getStackFrameFuncName(sf[i]) + "." + l.Name rv := ToReferenceVariable(l) s.findRef(rv, sfIndexes[i].pushReferenceStackBoundary(s.pb)) rvpool.Put(rv) From 038b25d49645a8abcdbe4dfae10cdf126737f3d2 Mon Sep 17 00:00:00 2001 From: "lqw@wsl" Date: Sat, 25 Jul 2026 22:09:46 +0800 Subject: [PATCH 12/15] refactor: remove dummy mapping id --- pkg/proc/protobuf.go | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/pkg/proc/protobuf.go b/pkg/proc/protobuf.go index 732a69d..ac6038d 100644 --- a/pkg/proc/protobuf.go +++ b/pkg/proc/protobuf.go @@ -321,14 +321,11 @@ func (b *profileBuilder) pbMapping(tag int, id, base, limit, offset uint64, file b.pb.endMessage(tag, start) } -const dummyMappingID = uint64(1) - func (b *profileBuilder) flush() { for i := uint64(5); i < uint64(len(b.strings)); i++ { // write location start := b.pb.startMessage() b.pb.uint64Opt(tagLocation_ID, i) - b.pb.uint64Opt(tagLocation_MappingID, dummyMappingID) b.pbLine(tagLocation_Line, i, 0) b.pb.endMessage(tagProfile_Location, start) @@ -340,7 +337,7 @@ func (b *profileBuilder) flush() { } b.flushReference() // just avoid error msg from pprof tool - b.pbMapping(tagProfile_Mapping, dummyMappingID, uint64(0), uint64(0xff), 0, "", "", false) + b.pbMapping(tagProfile_Mapping, 1, uint64(0), uint64(0xff), 0, "", "", false) b.pb.strings(tagProfile_StringTable, b.strings) b.zw.Write(b.pb.data) b.zw.Close() From bb0106af3140091cdd515c159e59be65fe7a55b1 Mon Sep 17 00:00:00 2001 From: "lqw@wsl" Date: Sat, 25 Jul 2026 22:14:44 +0800 Subject: [PATCH 13/15] refactor: recover order of flushReference and location, function writing --- pkg/proc/protobuf.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/proc/protobuf.go b/pkg/proc/protobuf.go index ac6038d..2b5bb0d 100644 --- a/pkg/proc/protobuf.go +++ b/pkg/proc/protobuf.go @@ -322,6 +322,7 @@ func (b *profileBuilder) pbMapping(tag int, id, base, limit, offset uint64, file } func (b *profileBuilder) flush() { + b.flushReference() for i := uint64(5); i < uint64(len(b.strings)); i++ { // write location start := b.pb.startMessage() @@ -335,7 +336,6 @@ func (b *profileBuilder) flush() { b.pb.int64Opt(tagFunction_Name, int64(i)) b.pb.endMessage(tagProfile_Function, start) } - b.flushReference() // just avoid error msg from pprof tool b.pbMapping(tagProfile_Mapping, 1, uint64(0), uint64(0xff), 0, "", "", false) b.pb.strings(tagProfile_StringTable, b.strings) From c3566f0a38f41d4e4b2df5aa820aba653fad5df8 Mon Sep 17 00:00:00 2001 From: "lqw@wsl" Date: Sat, 25 Jul 2026 22:29:37 +0800 Subject: [PATCH 14/15] refactor: replace nil stackframe's name to --- pkg/proc/protobuf.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/proc/protobuf.go b/pkg/proc/protobuf.go index 2b5bb0d..0259271 100644 --- a/pkg/proc/protobuf.go +++ b/pkg/proc/protobuf.go @@ -353,7 +353,7 @@ const stackTracePprofIndexDepth int = -1 const ReferenceStackBoundary string = "[goref:reference-stack-boundary]" -// getStackFrameFuncName returns the function name of the stack frame. If the function is nil, it returns "". +// getStackFrameFuncName returns the function name of the stack frame. If the function is nil, it returns "". func getStackFrameFuncName(sf proc.Stackframe) string { if sf.Call.Fn != nil { return sf.Call.Fn.Name @@ -361,7 +361,7 @@ func getStackFrameFuncName(sf proc.Stackframe) string { if sf.Current.Fn != nil { return sf.Current.Fn.Name } - return "" + return "" } // initStackTrace create the stackframe pprofIndexes from right(bottom of stack) to left(top of stack) with ReferenceStackBoundary as the new top frame. From 882d634935ffb268baf10e48c39bbbbc6c69f326 Mon Sep 17 00:00:00 2001 From: "lqw@wsl" Date: Sun, 26 Jul 2026 21:57:13 +0800 Subject: [PATCH 15/15] test: add sample count/size comparison --- test/framework.go | 80 ++++++++++++++++++++++++++++++++++------------- test/scenarios.go | 36 +++++++++++++++------ 2 files changed, 85 insertions(+), 31 deletions(-) diff --git a/test/framework.go b/test/framework.go index f49d8ed..980e52e 100644 --- a/test/framework.go +++ b/test/framework.go @@ -34,11 +34,11 @@ import ( // TestScenario defines a complete test scenario type TestScenario struct { - Name string - Code string - Expected *MemoryNode - ExpectedStackTrace *StackTraceNode - Timeout time.Duration + Name string + Code string + Expected *MemoryNode + ExpectedStackSample *StackSampleNode + Timeout time.Duration // RootPrefixes limits tree roots to nodes whose leaf name has one of these prefixes. // If empty, the framework defaults to []string{"main."}. RootPrefixes []string @@ -194,13 +194,14 @@ func (tf *TestFramework) validateResults(scope *gorefproc.ObjRefScope, scenario if err := tf.compareNodes(scenario.Expected, actualNode, scenario.AllowExtraChildren); err != nil { return fmt.Errorf("node comparison failed: %v", err) } + tf.t.Logf(" ✓ Memory node validation passed") // Compare stacktrace - if err := tf.compareStackTraceNodes(scenario.ExpectedStackTrace, actualStackTrace, true); err != nil { + if err := tf.compareStackTraceNodes(scenario.ExpectedStackSample, actualStackTrace, true); err != nil { return fmt.Errorf("stacktrace comparison failed: %v", err) } + tf.t.Logf(" ✓ Stack trace validation passed") - tf.t.Logf(" ✓ Memory node validation passed") return nil } @@ -274,7 +275,7 @@ func (tf *TestFramework) compareNodes(expected, actual *MemoryNode, allowExtraCh return nil } -func (tf *TestFramework) compareStackTraceNodes(expected, actual *StackTraceNode, allowExtraStackTraceChildren bool) error { +func (tf *TestFramework) compareStackTraceNodes(expected, actual *StackSampleNode, allowExtraStackTraceChildren bool) error { if expected == nil { if actual == nil { return nil @@ -287,14 +288,37 @@ func (tf *TestFramework) compareStackTraceNodes(expected, actual *StackTraceNode tf.t.Logf(" ✗ StackTrace mismatch: expected %v, actual ", expected) return fmt.Errorf("stacktrace mismatch, actual is nil when expected is %v", expected.FuncName) } + // Compare function name if expected.FuncName != actual.FuncName { tf.t.Logf(" ✗ StackTrace FuncName mismatch: expected %s, actual %s", expected.FuncName, actual.FuncName) return fmt.Errorf("stacktrace funcname mismatch") } + // Compare Count + if expected.Count != nil && actual.Count != nil { + if !expected.Count.Matches(*actual.Count.Exact) { + tf.t.Logf(" ✗ StackTrace Count mismatch for %s: expected %s, actual %d", expected.FuncName, expected.Count.String(), *actual.Count.Exact) + return fmt.Errorf("stacktrace count mismatch for %s", expected.FuncName) + } + } else if expected.Count != nil && actual.Count == nil { + tf.t.Logf(" ✗ StackTrace Count mismatch for %s: expected %s, actual ", expected.FuncName, expected.Count.String()) + return fmt.Errorf("stacktrace count mismatch for %s", expected.FuncName) + } + + // Compare Size + if expected.Size != nil && actual.Size != nil { + if !expected.Size.Matches(*actual.Size.Exact) { + tf.t.Logf(" ✗ StackTrace Size mismatch for %s: expected %s, actual %d", expected.FuncName, expected.Size.String(), *actual.Size.Exact) + return fmt.Errorf("stacktrace size mismatch for %s", expected.FuncName) + } + } else if expected.Size != nil && actual.Size == nil { + tf.t.Logf(" ✗ StackTrace Size mismatch for %s: expected %s, actual ", expected.FuncName, expected.Size.String()) + return fmt.Errorf("stacktrace size mismatch for %s", expected.FuncName) + } + // Compare children - expectedChildren := make(map[string]*StackTraceNode) - actualChildren := make(map[string]*StackTraceNode) + expectedChildren := make(map[string]*StackSampleNode) + actualChildren := make(map[string]*StackSampleNode) for _, child := range expected.Children { expectedChildren[child.FuncName] = child @@ -425,9 +449,11 @@ type MemoryNode struct { Children []*MemoryNode `json:"children,omitempty"` // Child nodes } -type StackTraceNode struct { - FuncName string `json:"func_name,omitempty"` // Function name (e.g., "main.main", "runtime.main") - Children []*StackTraceNode `json:"children,omitempty"` // Child nodes representing stack trace hierarchy +type StackSampleNode struct { + FuncName string `json:"func_name,omitempty"` // Function name (e.g., "main.main", "runtime.main") + Count *ValueRange `json:"count,omitempty"` // Number of samples with flexible validation + Size *ValueRange `json:"size,omitempty"` // Memory size in bytes with flexible validation + Children []*StackSampleNode `json:"children,omitempty"` // Child nodes representing stack trace hierarchy } // buildMemoryTreeFromNodes builds a memory reference node from goref profile nodes. @@ -523,10 +549,10 @@ func (tf *TestFramework) createOrUpdateNode(node *MemoryNode, path []string, cou tf.createOrUpdateNode(child, path[:len(path)-1], count, size) } -func (tf *TestFramework) buildStackTraceFromNodes(nodes map[string]ProfileNodeInterface, stringTable []string) *StackTraceNode { - root := &StackTraceNode{Children: []*StackTraceNode{}} +func (tf *TestFramework) buildStackTraceFromNodes(nodes map[string]ProfileNodeInterface, stringTable []string) *StackSampleNode { + root := &StackSampleNode{Children: []*StackSampleNode{}} - for key := range nodes { + for key, node := range nodes { nodePath := tf.extractNodePathFromKey(key, stringTable) if nodePath == nil { continue @@ -536,29 +562,41 @@ func (tf *TestFramework) buildStackTraceFromNodes(nodes map[string]ProfileNodeIn if splitLineIdx = slices.Index(nodePath, gorefproc.ReferenceStackBoundary); splitLineIdx == -1 { continue } - tf.createOrUpdateStackTraceNode(root, nodePath[splitLineIdx+1:len(nodePath)-1]) + tf.createOrUpdateStackSampleNode(root, nodePath[splitLineIdx+1:len(nodePath)-1], node.GetCount(), node.GetSize()) } return root } -func (tf *TestFramework) createOrUpdateStackTraceNode(node *StackTraceNode, path []string) { +func (tf *TestFramework) createOrUpdateStackSampleNode(node *StackSampleNode, path []string, count, size int64) { + // accumulate count and size for each stack trace node + if node.Size == nil { + node.Size = ExactValue(size) + } else if node.Size.Exact != nil { + *node.Size.Exact += size + } + + if node.Count == nil { + node.Count = ExactValue(count) + } else if node.Count.Exact != nil { + *node.Count.Exact += count + } if len(path) == 0 { return } funcName := path[len(path)-1] for _, child := range node.Children { if child.FuncName == funcName { - tf.createOrUpdateStackTraceNode(child, path[:len(path)-1]) + tf.createOrUpdateStackSampleNode(child, path[:len(path)-1], count, size) return } } // Create new node - child := &StackTraceNode{ + child := &StackSampleNode{ FuncName: funcName, } node.Children = append(node.Children, child) - tf.createOrUpdateStackTraceNode(child, path[:len(path)-1]) + tf.createOrUpdateStackSampleNode(child, path[:len(path)-1], count, size) } // extractTypeFromKey extracts name and type information from the key path diff --git a/test/scenarios.go b/test/scenarios.go index b46a962..b80493c 100644 --- a/test/scenarios.go +++ b/test/scenarios.go @@ -47,13 +47,17 @@ func main() { }, }, }, - ExpectedStackTrace: &StackTraceNode{ - Children: []*StackTraceNode{ + ExpectedStackSample: &StackSampleNode{ + Children: []*StackSampleNode{ { FuncName: "runtime.main", - Children: []*StackTraceNode{ + Count: ExactValue(2), + Size: ExactValue(80), + Children: []*StackSampleNode{ { FuncName: "main.main", + Count: ExactValue(2), + Size: ExactValue(80), }, }, }, @@ -851,7 +855,9 @@ func foo(ch chan *int, a int) { func main() { ch := make(chan *int) - go foo(ch, 1) + for i := 0; i < 2; i++ { + go foo(ch, 1) + } go bar(ch, 2) fmt.Println("READY") fmt.Println(os.Getpid()) @@ -867,20 +873,26 @@ func main() { Children: []*MemoryNode{ { Name: "main.bar.n", - Size: ExactValue(32), + Size: ExactValue(48), }, }, }, - ExpectedStackTrace: &StackTraceNode{ - Children: []*StackTraceNode{ + ExpectedStackSample: &StackSampleNode{ + Children: []*StackSampleNode{ { FuncName: "main.main.gowrap1", - Children: []*StackTraceNode{ + Count: ExactValue(2), + Size: ExactValue(32), + Children: []*StackSampleNode{ { FuncName: "main.foo", - Children: []*StackTraceNode{ + Count: ExactValue(2), + Size: ExactValue(32), + Children: []*StackSampleNode{ { FuncName: "main.bar", + Count: ExactValue(2), + Size: ExactValue(32), }, }, }, @@ -888,9 +900,13 @@ func main() { }, { FuncName: "main.main.gowrap2", - Children: []*StackTraceNode{ + Count: ExactValue(1), + Size: ExactValue(16), + Children: []*StackSampleNode{ { FuncName: "main.bar", + Count: ExactValue(1), + Size: ExactValue(16), }, }, },