diff --git a/pkg/proc/protobuf.go b/pkg/proc/protobuf.go index ba64cae..0259271 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. @@ -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, 1, uint64(0), uint64(0xff), 0, "", "", false) b.pb.strings(tagProfile_StringTable, b.strings) b.zw.Write(b.pb.data) b.zw.Close() @@ -347,21 +349,55 @@ type pprofIndex struct { depth int } -func (i *pprofIndex) pushHead(pb *profileBuilder, name string) *pprofIndex { - if name == "" { - return i +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 } - idx := uint64(pb.stringIndex(name)) - pi := &pprofIndex{ - prev: i, - idx: idx, + 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, getStackFrameFuncName(sfs[i]), 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..d2896b6 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, "") @@ -606,9 +608,9 @@ 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, nil) + s.findRef(rv, sfIndexes[i].pushReferenceStackBoundary(s.pb)) rvpool.Put(rv) } } diff --git a/test/framework.go b/test/framework.go index c2458ca..980e52e 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 + 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 @@ -186,13 +188,20 @@ 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) } - tf.t.Logf(" ✓ Memory node validation passed") + + // Compare stacktrace + 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") + return nil } @@ -266,7 +275,87 @@ func (tf *TestFramework) compareNodes(expected, actual *MemoryNode, allowExtraCh return nil } -func sortedNodeNames(children map[string]*MemoryNode) []string { +func (tf *TestFramework) compareStackTraceNodes(expected, actual *StackSampleNode, 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) + } + // 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]*StackSampleNode) + actualChildren := make(map[string]*StackSampleNode) + + 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 +449,13 @@ type MemoryNode struct { Children []*MemoryNode `json:"children,omitempty"` // Child nodes } +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. func (tf *TestFramework) buildMemoryTreeFromNodes(nodes map[string]ProfileNodeInterface, stringTable, rootPrefixes []string) *MemoryNode { root := &MemoryNode{Children: []*MemoryNode{}} @@ -376,11 +472,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 +549,56 @@ 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) *StackSampleNode { + root := &StackSampleNode{Children: []*StackSampleNode{}} + + for key, node := 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.createOrUpdateStackSampleNode(root, nodePath[splitLineIdx+1:len(nodePath)-1], node.GetCount(), node.GetSize()) + } + return root +} + +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.createOrUpdateStackSampleNode(child, path[:len(path)-1], count, size) + return + } + } + // Create new node + child := &StackSampleNode{ + FuncName: funcName, + } + + node.Children = append(node.Children, child) + tf.createOrUpdateStackSampleNode(child, path[:len(path)-1], count, size) +} + // 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..b80493c 100644 --- a/test/scenarios.go +++ b/test/scenarios.go @@ -47,6 +47,22 @@ func main() { }, }, }, + ExpectedStackSample: &StackSampleNode{ + Children: []*StackSampleNode{ + { + FuncName: "runtime.main", + Count: ExactValue(2), + Size: ExactValue(80), + Children: []*StackSampleNode{ + { + FuncName: "main.main", + Count: ExactValue(2), + Size: ExactValue(80), + }, + }, + }, + }, + }, Timeout: 30 * time.Second, } @@ -809,3 +825,93 @@ 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) + for i := 0; i < 2; i++ { + 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(48), + }, + }, + }, + ExpectedStackSample: &StackSampleNode{ + Children: []*StackSampleNode{ + { + FuncName: "main.main.gowrap1", + Count: ExactValue(2), + Size: ExactValue(32), + Children: []*StackSampleNode{ + { + FuncName: "main.foo", + Count: ExactValue(2), + Size: ExactValue(32), + Children: []*StackSampleNode{ + { + FuncName: "main.bar", + Count: ExactValue(2), + Size: ExactValue(32), + }, + }, + }, + }, + }, + { + FuncName: "main.main.gowrap2", + Count: ExactValue(1), + Size: ExactValue(16), + Children: []*StackSampleNode{ + { + FuncName: "main.bar", + Count: ExactValue(1), + Size: ExactValue(16), + }, + }, + }, + }, + }, + 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 +}