Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 47 additions & 11 deletions pkg/proc/protobuf.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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()
Expand All @@ -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 "<nil>".
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 "<nil>"
}

// 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) {
Expand Down
6 changes: 4 additions & 2 deletions pkg/proc/reference.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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, "")
Expand All @@ -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)
}
}
Expand Down
169 changes: 160 additions & 9 deletions test/framework.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"os"
"os/exec"
"path/filepath"
"slices"
"sort"
"strings"
"testing"
Expand All @@ -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
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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 <nil>", 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 <nil>", 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 <nil>", 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)
Expand Down Expand Up @@ -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{}}
Expand All @@ -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
}
}
Expand Down Expand Up @@ -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
}
Comment on lines +570 to +585

@Lslightly Lslightly Jul 26, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow comment above.

Take main.foo -call-> main.bar as an example. The count/size of samples is recorded in both main.foo and main.bar StackSampleNode.

The other alternative is to remain main.foo's Count/Size nil while keeping main.bar's Count/Size the sample's count/value.

For testing purpose, both are acceptable in my opinion. The second option only simplifies the testcase without the need to write the expected count/size for each stack frame node.

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
Expand Down
1 change: 1 addition & 0 deletions test/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ var testCases = []TestScenario{
ChannelScenario,
MallocHeaderHiddenTypeScenario,
CircularReferenceScenario,
StackTraceScenario,
}

// TestScenarios runs individual test scenarios using table-driven approach
Expand Down
Loading