Skip to content
54 changes: 41 additions & 13 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 @@ -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)

Expand All @@ -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()
Expand All @@ -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) {
Expand Down
4 changes: 3 additions & 1 deletion 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 @@ -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)
}
}
Expand Down
129 changes: 121 additions & 8 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
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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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 <nil>", 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)
Expand Down Expand Up @@ -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{}}
Expand All @@ -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
}
}
Expand Down Expand Up @@ -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
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