|
1 | 1 | package callgraphutil |
2 | 2 |
|
3 | 3 | 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" |
10 | 10 |
|
11 | 11 | "golang.org/x/tools/go/callgraph" |
12 | 12 | "golang.org/x/tools/go/ssa" |
13 | 13 | "golang.org/x/tools/go/ssa/ssautil" |
14 | 14 | ) |
15 | 15 |
|
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 | + |
17 | 27 | 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 |
20 | 35 | ) |
21 | 36 |
|
22 | 37 | // getAllFunctionsCached returns cached AllFunctions result for significant performance boost. |
23 | 38 | // AllFunctions is expensive (6+ms on large codebases) but result is identical for same program. |
24 | 39 | 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 | + } |
31 | 46 |
|
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 | +} |
35 | 56 |
|
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 | + } |
40 | 65 |
|
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 |
44 | 73 | } |
45 | 74 |
|
46 | 75 | // GraphString returns a string representation of the call graph, |
@@ -408,28 +437,20 @@ func checkBlockInstructionOptimized(root *ssa.Function, allFns map[*ssa.Function |
408 | 437 | instrCall = calltFn |
409 | 438 | } |
410 | 439 |
|
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 | + } |
427 | 445 |
|
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()) |
433 | 454 |
|
434 | 455 | case *ssa.UnOp: |
435 | 456 | // Dereference operations - less common |
@@ -523,28 +544,18 @@ func checkBlockInstruction(root *ssa.Function, allFns map[*ssa.Function]bool, g |
523 | 544 | } |
524 | 545 | } |
525 | 546 |
|
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()) |
548 | 559 | } |
549 | 560 |
|
550 | 561 | // Early exit if no function was determined |
|
0 commit comments