diff --git a/pkg/proc/protobuf.go b/pkg/proc/protobuf.go index ba64cae..196b49f 100644 --- a/pkg/proc/protobuf.go +++ b/pkg/proc/protobuf.go @@ -12,6 +12,8 @@ package proc import ( "compress/gzip" "io" + + "github.com/go-delve/delve/pkg/proc" ) // A protobuf is a simple protocol buffer encoder. @@ -319,12 +321,14 @@ 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() 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) @@ -334,8 +338,9 @@ 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, 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() @@ -347,21 +352,44 @@ type pprofIndex struct { depth int } -func (i *pprofIndex) pushHead(pb *profileBuilder, name string) *pprofIndex { - if name == "" { - return i - } - idx := uint64(pb.stringIndex(name)) - pi := &pprofIndex{ - prev: i, - idx: idx, +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. +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) + frameIndexes[i] = prev } + return +} + +func (i *pprofIndex) pushReferenceStackBoundary(pb *profileBuilder) *pprofIndex { + return newHeadWithDepth(pb, i, ReferenceStackBoundary, stackTracePprofIndexDepth) +} + +func (i *pprofIndex) pushHead(pb *profileBuilder, name string) *pprofIndex { + 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 ee7dc0e..855cb5e 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, @@ -595,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, "") @@ -608,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, nil) + s.findRef(rv, sfIndexes[i].pushReferenceStackBoundary(s.pb)) rvpool.Put(rv) } } diff --git a/test/framework.go b/test/framework.go index c2458ca..f49d8ed 100644 --- a/test/framework.go +++ b/test/framework.go @@ -21,6 +21,7 @@ import ( "os" "os/exec" "path/filepath" + "slices" "sort" "strings" "testing" @@ -33,10 +34,11 @@ 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 @@ -186,12 +188,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 } @@ -266,7 +274,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) @@ -360,6 +425,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{}} @@ -376,11 +446,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.ReferenceStackBoundary); 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 } } @@ -448,6 +523,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.ReferenceStackBoundary); 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, +} 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 +}