Skip to content
Merged
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
84 changes: 84 additions & 0 deletions pkg/proc/heap.go
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,7 @@ func (s *HeapScope) readHeap() error {
// read typed pointers when enabled alloc header
s.readTypePointers(spans, spanInfos)
}
s.readQueuedFinalizers()

// read firstmoduledata
return s.readModuleData()
Expand Down Expand Up @@ -716,6 +717,89 @@ func specialCleanupFnAddr(spf *region) (Address, bool) {
return cleanupField.Field("fn").a, true
}

// readQueuedFinalizers adds queued (not yet executed) finalizers from runtime finalizer blocks.
func (s *HeapScope) readQueuedFinalizers() {
// allfin tracks all finalizer blocks with active entries (cnt),
// including blocks currently being drained by runFinalizers.
if tmp, err := s.scope.EvalExpression("runtime.allfin", loadSingleValue); err == nil && tmp != nil && tmp.RealType != nil {
s.addQueuedFinalizersFromBlockList(toRegion(tmp, s.bi), "alllink")
return
}
// fallback for runtimes where allfin is unavailable in debug info
if tmp, err := s.scope.EvalExpression("runtime.finq", loadSingleValue); err == nil && tmp != nil && tmp.RealType != nil {
s.addQueuedFinalizersFromBlockList(toRegion(tmp, s.bi), "next")
}
}

func (s *HeapScope) addQueuedFinalizersFromBlockList(head *region, linkField string) {
seen := map[Address]struct{}{}
to := &region{}
for fb := head; fb.Address() != 0; {
fbAddr := fb.Address()
if _, ok := seen[fbAddr]; ok {
// avoid infinite loop on corrupted list
break
}
seen[fbAddr] = struct{}{}

block := fb.Deref()
if !block.IsStruct() || !block.HasField("cnt") || !block.HasField("fin") {
break
}
next := nextFinalizerBlock(block, linkField)
cnt := uint64(block.Field("cnt").Uint32())
if cnt == 0 {
if next == nil {
break
}
fb = next
continue
}
finArr := block.Field("fin")
n := finArr.ArrayLen()
if cnt > uint64(n) {
cnt = uint64(n)
}
for i := int64(0); i < int64(cnt); i++ {
finArr.ArrayIndex(i, to)
if !to.IsStruct() || !to.HasField("arg") || !to.HasField("fn") {
continue
}
argField := to.Field("arg")
var arg Address
switch argField.typ.(type) {
case *godwarf.PtrType:
arg = argField.Address()
case *godwarf.UintType:
arg = Address(argField.Uintptr())
default:
continue
}
if arg == 0 {
continue
}
s.finalizers = append(s.finalizers, finalizer{
p: arg,
fn: to.Field("fn").a,
})
}
if next == nil {
break
}
fb = next
}
}

func nextFinalizerBlock(block *region, linkField string) *region {
if block.HasField(linkField) {
return block.Field(linkField)
}
if block.HasField("next") {
return block.Field("next")
}
return nil
}

func (s *HeapScope) addSpecial(sp *region, spi *spanInfo, fintyp, clutyp godwarf.Type, kindSpecialFinalizer, kindSpecialCleanup uint8) error {
// Process special records.
for special := sp.Field("specials"); special.Address() != 0; special = special.Field("next") {
Expand Down
52 changes: 33 additions & 19 deletions test/framework.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ type TestScenario struct {
Code string
Expected *MemoryNode
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
}

// TestFramework manages integration test execution
Expand Down Expand Up @@ -94,7 +100,7 @@ func (tf *TestFramework) runScenario(scenario TestScenario) {
tf.t.Fatalf("Failed to attach and analyze: %v", err)
}

if err := tf.validateResults(scope, scenario.Expected); err != nil {
if err := tf.validateResults(scope, scenario); err != nil {
tf.t.Errorf("Memory tree validation failed: %v", err)
}
}
Expand Down Expand Up @@ -164,7 +170,7 @@ func (tf *TestFramework) attachAndAnalyze(pid int, outputFile, binary string) (*
}

// validateResults validates the analysis results against expectations using memory node comparison
func (tf *TestFramework) validateResults(scope *gorefproc.ObjRefScope, expectedNode *MemoryNode) error {
func (tf *TestFramework) validateResults(scope *gorefproc.ObjRefScope, scenario TestScenario) error {
tf.t.Logf("Analysis results:")

// Get the real profile data from goref
Expand All @@ -179,19 +185,19 @@ func (tf *TestFramework) validateResults(scope *gorefproc.ObjRefScope, expectedN
for k, v := range nodes {
nodeInterfaces[k] = ProfileNodeInterface(v)
}
actualNode := tf.buildMemoryTreeFromNodes(nodeInterfaces, stringTable)
actualNode := tf.buildMemoryTreeFromNodes(nodeInterfaces, stringTable, scenario.RootPrefixes)

// Compare nodes
if err := tf.compareNodes(expectedNode, actualNode); err != nil {
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")
return nil
}

// compareNodes recursively compares two memory nodes
func (tf *TestFramework) compareNodes(expected, actual *MemoryNode) error {
// compareNodes recursively compares two memory nodes.
func (tf *TestFramework) compareNodes(expected, actual *MemoryNode, allowExtraChildren bool) error {
// Compare Size
if expected.Size != nil && actual.Size != nil {
if !expected.Size.Matches(*actual.Size.Exact) {
Expand Down Expand Up @@ -242,16 +248,18 @@ func (tf *TestFramework) compareNodes(expected, actual *MemoryNode) error {
}

// Recursively compare child nodes
if err := tf.compareNodes(expectedChild, actualChild); err != nil {
if err := tf.compareNodes(expectedChild, actualChild, allowExtraChildren); err != nil {
return err
}
}

// Check for unexpected children
for name := range actualChildren {
if _, found := expectedChildren[name]; !found {
tf.t.Logf(" ✗ Unexpected child node: %s.%s", expected.Name, name)
return fmt.Errorf("unexpected child node: %s.%s", expected.Name, name)
if !allowExtraChildren {
for name := range actualChildren {
if _, found := expectedChildren[name]; !found {
tf.t.Logf(" ✗ Unexpected child node: %s.%s", expected.Name, name)
return fmt.Errorf("unexpected child node: %s.%s", expected.Name, name)
}
}
}

Expand Down Expand Up @@ -352,11 +360,14 @@ type MemoryNode struct {
Children []*MemoryNode `json:"children,omitempty"` // Child nodes
}

// buildMemoryTreeFromNodes builds a memory reference node from goref profile nodes
func (tf *TestFramework) buildMemoryTreeFromNodes(nodes map[string]ProfileNodeInterface, stringTable []string) *MemoryNode {
// 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{}}
if len(rootPrefixes) == 0 {
rootPrefixes = []string{"main."}
}

mainPackageNodes := 0
matchedRootNodes := 0

// Process each node to build the tree structure
for key, node := range nodes {
Expand All @@ -365,14 +376,17 @@ func (tf *TestFramework) buildMemoryTreeFromNodes(nodes map[string]ProfileNodeIn
continue // Skip empty paths
}

// Only include nodes from main package or system nodes for debugging
if strings.HasPrefix(nodePath[len(nodePath)-1], "main.") {
mainPackageNodes++
tf.createOrUpdateNode(root, nodePath, node.GetCount(), node.GetSize())
leaf := nodePath[len(nodePath)-1]
for _, prefix := range rootPrefixes {
if strings.HasPrefix(leaf, prefix) {
matchedRootNodes++
tf.createOrUpdateNode(root, nodePath, node.GetCount(), node.GetSize())
break
}
}
}

tf.t.Logf(" Found %d main package nodes", mainPackageNodes)
tf.t.Logf(" Found %d matched root nodes", matchedRootNodes)

return root
}
Expand Down
1 change: 1 addition & 0 deletions test/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ var testCases = []TestScenario{
FieldLockScenario,
NestedStructScenario,
FinalizerScenario,
FinalizerQueuedScenario,
InterfaceScenario,
ChannelScenario,
MallocHeaderHiddenTypeScenario,
Expand Down
85 changes: 85 additions & 0 deletions test/scenarios.go
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,91 @@ func main() {
Timeout: 30 * time.Second,
}

// FinalizerQueuedScenario validates objects/functions retained by finalizers that
// have been queued but not completed yet.
var FinalizerQueuedScenario = TestScenario{
Name: "finalizer queued but not executed",
Code: `package main
import (
"fmt"
"runtime"
"time"
)

type ToFin struct {
data [100]int64
}

func forceGC(rounds int) {
for i := 0; i < rounds; i++ {
runtime.GC()
runtime.Gosched()
time.Sleep(10 * time.Millisecond)
}
}

func startBlockingFinalizer(started chan struct{}) {
blockerObj := &ToFin{data: [100]int64{1, 2, 3, 4}}
runtime.SetFinalizer(blockerObj, func(*ToFin) {
close(started)
select {}
})
}

func enqueuePendingFinalizer() {
target := &ToFin{data: [100]int64{9, 8, 7, 6}}
obj := &ToFin{data: [100]int64{5, 4, 3, 2}}
runtime.SetFinalizer(obj, func(*ToFin) {
// Keep target alive only through finalizer closure.
_ = target.data[0]
})
}

func waitBlockingFinalizerStarted(started chan struct{}) {
deadline := time.Now().Add(5 * time.Second)
for {
forceGC(1)
select {
case <-started:
return
default:
if time.Now().After(deadline) {
panic("blocking finalizer did not start")
}
}
}
}

func main() {
started := make(chan struct{})
startBlockingFinalizer(started)
waitBlockingFinalizerStarted(started)

// While the finalizer goroutine is blocked, enqueue another finalizer.
enqueuePendingFinalizer()
forceGC(4)

fmt.Println("READY")
time.Sleep(100 * time.Second)
}
`,
Expected: &MemoryNode{
Children: []*MemoryNode{
{
Name: "runtime.SetFinalizer.obj",
Count: MinValue(1),
},
{
Name: "runtime.SetFinalizer.fn",
Count: MinValue(1),
},
},
},
Timeout: 30 * time.Second,
RootPrefixes: []string{"runtime.SetFinalizer."},
AllowExtraChildren: true,
}

// InterfaceScenario tests interface variable references
var InterfaceScenario = TestScenario{
Name: "interface variable references",
Expand Down