Skip to content

Commit 8dc3110

Browse files
committed
Continue iterating
1 parent ba441a6 commit 8dc3110

4 files changed

Lines changed: 3088 additions & 2965 deletions

File tree

callgraphutil/graph.go

Lines changed: 79 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,75 @@
11
package callgraphutil
22

33
import (
4-
"bytes"
5-
"context"
6-
"fmt"
7-
"go/token"
8-
"go/types"
9-
"sync"
4+
"bytes"
5+
"context"
6+
"fmt"
7+
"go/token"
8+
"go/types"
9+
"sync"
1010

1111
"golang.org/x/tools/go/callgraph"
1212
"golang.org/x/tools/go/ssa"
1313
"golang.org/x/tools/go/ssa/ssautil"
1414
)
1515

16-
// Global cache for AllFunctions results to avoid repeated expensive computation
16+
// Global caches with lock-free reads and per-key initialization
17+
type allFunctionsEntry struct {
18+
once sync.Once
19+
value map[*ssa.Function]bool
20+
}
21+
22+
type syntheticMethodEntry struct {
23+
once sync.Once
24+
fn *ssa.Function
25+
}
26+
1727
var (
18-
allFunctionsCache = make(map[*ssa.Program]map[*ssa.Function]bool)
19-
allFunctionsMutex sync.RWMutex
28+
// Cache of ssautil.AllFunctions(prog) results keyed by *ssa.Program
29+
// Uses sync.Map for lock-free reads; each entry initializes once.
30+
allFunctionsCache sync.Map // map[*ssa.Program]*allFunctionsEntry
31+
32+
// Cache of synthetic method functions keyed by receiver+method string
33+
// Ensures only one synthetic *ssa.Function is created per key.
34+
syntheticMethodCache sync.Map // map[string]*syntheticMethodEntry
2035
)
2136

2237
// getAllFunctionsCached returns cached AllFunctions result for significant performance boost.
2338
// AllFunctions is expensive (6+ms on large codebases) but result is identical for same program.
2439
func getAllFunctionsCached(prog *ssa.Program) map[*ssa.Function]bool {
25-
allFunctionsMutex.RLock()
26-
if cached, exists := allFunctionsCache[prog]; exists {
27-
allFunctionsMutex.RUnlock()
28-
return cached
29-
}
30-
allFunctionsMutex.RUnlock()
40+
// Fast-path: try to load existing entry
41+
if v, ok := allFunctionsCache.Load(prog); ok {
42+
e := v.(*allFunctionsEntry)
43+
e.once.Do(func() { /* already initialized or will be */ })
44+
return e.value
45+
}
3146

32-
// Cache miss - compute and store
33-
allFunctionsMutex.Lock()
34-
defer allFunctionsMutex.Unlock()
47+
// Create an entry placeholder; LoadOrStore to avoid races
48+
e := &allFunctionsEntry{}
49+
actual, _ := allFunctionsCache.LoadOrStore(prog, e)
50+
entry := actual.(*allFunctionsEntry)
51+
entry.once.Do(func() {
52+
entry.value = ssautil.AllFunctions(prog)
53+
})
54+
return entry.value
55+
}
3556

36-
// Double-check after acquiring write lock
37-
if cached, exists := allFunctionsCache[prog]; exists {
38-
return cached
39-
}
57+
// getOrCreateSyntheticMethod returns a stable synthetic method function for a
58+
// given key, creating it exactly once per key.
59+
func getOrCreateSyntheticMethod(prog *ssa.Program, key, methodName string, sig *types.Signature) *ssa.Function {
60+
if v, ok := syntheticMethodCache.Load(key); ok {
61+
e := v.(*syntheticMethodEntry)
62+
e.once.Do(func() { /* already initialized or will be */ })
63+
return e.fn
64+
}
4065

41-
result := ssautil.AllFunctions(prog)
42-
allFunctionsCache[prog] = result
43-
return result
66+
e := &syntheticMethodEntry{}
67+
actual, _ := syntheticMethodCache.LoadOrStore(key, e)
68+
entry := actual.(*syntheticMethodEntry)
69+
entry.once.Do(func() {
70+
entry.fn = prog.NewFunction(methodName, sig, "synthetic")
71+
})
72+
return entry.fn
4473
}
4574

4675
// GraphString returns a string representation of the call graph,
@@ -408,28 +437,20 @@ func checkBlockInstructionOptimized(root *ssa.Function, allFns map[*ssa.Function
408437
instrCall = calltFn
409438
}
410439

411-
case *ssa.Parameter:
412-
// Method calls via interface - more complex case
413-
if !cc.IsInvoke() || cc.Method == nil {
414-
return nil
415-
}
416-
417-
methodPkg := cc.Method.Pkg()
418-
if methodPkg == nil {
419-
// Universe scope method like error.Error - skip early
420-
return nil
421-
}
422-
423-
pkg := root.Prog.ImportedPackage(methodPkg.Path())
424-
if pkg == nil {
425-
return nil
426-
}
440+
case *ssa.Parameter:
441+
// Method calls via interface - more complex case
442+
if !cc.IsInvoke() || cc.Method == nil {
443+
return nil
444+
}
427445

428-
fn := pkg.Func(cc.Method.Name())
429-
if fn == nil {
430-
fn = pkg.Prog.NewFunction(cc.Method.Name(), cc.Signature(), "callgraph")
431-
}
432-
instrCall = fn
446+
// Create or reuse a synthetic function for the invoked method to avoid duplicates
447+
recv := cc.Signature().Recv()
448+
if recv == nil {
449+
return nil
450+
}
451+
recvStr := types.TypeString(recv.Type(), nil)
452+
key := fmt.Sprintf("(%s).%s", recvStr, cc.Method.Name())
453+
instrCall = getOrCreateSyntheticMethod(root.Prog, key, cc.Method.Name(), cc.Signature())
433454

434455
case *ssa.UnOp:
435456
// Dereference operations - less common
@@ -523,28 +544,18 @@ func checkBlockInstruction(root *ssa.Function, allFns map[*ssa.Function]bool, g
523544
}
524545
}
525546

526-
case *ssa.Parameter:
527-
// Handle method calls with early exits for performance
528-
if !cc.IsInvoke() || cc.Method == nil {
529-
return nil
530-
}
531-
532-
methodPkg := cc.Method.Pkg()
533-
if methodPkg == nil {
534-
// Universe scope method like error.Error - skip
535-
return nil
536-
}
537-
538-
pkg := root.Prog.ImportedPackage(methodPkg.Path())
539-
if pkg == nil {
540-
return nil
541-
}
542-
543-
fn := pkg.Func(cc.Method.Name())
544-
if fn == nil {
545-
fn = pkg.Prog.NewFunction(cc.Method.Name(), cc.Signature(), "callgraph")
546-
}
547-
instrCall = fn
547+
case *ssa.Parameter:
548+
// Handle method calls with early exits for performance
549+
if !cc.IsInvoke() || cc.Method == nil {
550+
return nil
551+
}
552+
recv := cc.Signature().Recv()
553+
if recv == nil {
554+
return nil
555+
}
556+
recvStr := types.TypeString(recv.Type(), nil)
557+
key := fmt.Sprintf("(%s).%s", recvStr, cc.Method.Name())
558+
instrCall = getOrCreateSyntheticMethod(root.Prog, key, cc.Method.Name(), cc.Signature())
548559
}
549560

550561
// Early exit if no function was determined

0 commit comments

Comments
 (0)