From 608d57b3ae930138a65e85b64edf2ba1b3450b06 Mon Sep 17 00:00:00 2001 From: Alan Donovan Date: Fri, 12 Feb 2016 15:39:35 -0500 Subject: [PATCH] cmd/guru: call{ers,stack}: avoid pointer analysis where possible As an optimization, the callers and callstack queries now avoid the relatively costly pointer analysis in some common cases: The callers of a function that is never address-taken can be enumerated directly from the SSA representation. Similarly, the callstack can be computed initially by ignoring dynamic call edges; we run the pointer analysis only if no path is found in this partial callgraph. As a bonus, this also causes the tool to preferentially report all-static callpaths. A callers query on fmt.Errorf now completes in 3 seconds instead of 8, and a callstack query completes in 2 seconds instead of 8. The new code is covered by the existing tests. Change-Id: I777ea07a1cdb6cadcc2a94952f553b6b036e7382 Reviewed-on: https://go-review.googlesource.com/19496 Reviewed-by: Michael Matloob --- cmd/guru/callers.go | 97 +++++++++++++++++++++++++++++++++++++++---- cmd/guru/callstack.go | 34 +++++++++++---- cmd/guru/main.go | 2 +- 3 files changed, 115 insertions(+), 18 deletions(-) diff --git a/cmd/guru/callers.go b/cmd/guru/callers.go index b0edc4d3..0dd8c144 100644 --- a/cmd/guru/callers.go +++ b/cmd/guru/callers.go @@ -7,6 +7,7 @@ package main import ( "fmt" "go/token" + "go/types" "golang.org/x/tools/cmd/guru/serial" "golang.org/x/tools/go/callgraph" @@ -60,16 +61,20 @@ func callers(q *Query) error { return fmt.Errorf("no SSA function built for this location (dead code?)") } - // TODO(adonovan): opt: if function is never address-taken, skip - // the pointer analysis. Just look for direct calls. This can - // be done in a single pass over the SSA. - - // Run the pointer analysis, recording each - // call found to originate from target. - ptaConfig.BuildCallGraph = true - cg := ptrAnalysis(ptaConfig).CallGraph + // If the function is never address-taken, all calls are direct + // and can be found quickly by inspecting the whole SSA program. + cg := directCallsTo(target, entryPoints(ptaConfig.Mains)) + if cg == nil { + // Run the pointer analysis, recording each + // call found to originate from target. + // (Pointer analysis may return fewer results than + // directCallsTo because it ignores dead code.) + ptaConfig.BuildCallGraph = true + cg = ptrAnalysis(ptaConfig).CallGraph + } cg.DeleteSyntheticNodes() edges := cg.CreateNode(target).In + // TODO(adonovan): sort + dedup calls to ensure test determinism. q.result = &callersResult{ @@ -80,6 +85,82 @@ func callers(q *Query) error { return nil } +// directCallsTo inspects the whole program and returns a callgraph +// containing edges for all direct calls to the target function. +// directCallsTo returns nil if the function is ever address-taken. +func directCallsTo(target *ssa.Function, entrypoints []*ssa.Function) *callgraph.Graph { + cg := callgraph.New(nil) // use nil as root *Function + targetNode := cg.CreateNode(target) + + // Is the function a program entry point? + // If so, add edge from callgraph root. + for _, f := range entrypoints { + if f == target { + callgraph.AddEdge(cg.Root, nil, targetNode) + } + } + + // Find receiver type (for methods). + var recvType types.Type + if recv := target.Signature.Recv(); recv != nil { + recvType = recv.Type() + } + + // Find all direct calls to function, + // or a place where its address is taken. + var space [32]*ssa.Value // preallocate + for fn := range ssautil.AllFunctions(target.Prog) { + for _, b := range fn.Blocks { + for _, instr := range b.Instrs { + // Is this a method (T).f of a concrete type T + // whose runtime type descriptor is address-taken? + // (To be fully sound, we would have to check that + // the type doesn't make it to reflection as a + // subelement of some other address-taken type.) + if recvType != nil { + if mi, ok := instr.(*ssa.MakeInterface); ok { + if types.Identical(mi.X.Type(), recvType) { + return nil // T is address-taken + } + if ptr, ok := mi.X.Type().(*types.Pointer); ok && + types.Identical(ptr.Elem(), recvType) { + return nil // *T is address-taken + } + } + } + + // Direct call to target? + rands := instr.Operands(space[:0]) + if site, ok := instr.(ssa.CallInstruction); ok && + site.Common().Value == target { + callgraph.AddEdge(cg.CreateNode(fn), site, targetNode) + rands = rands[1:] // skip .Value (rands[0]) + } + + // Address-taken? + for _, rand := range rands { + if rand != nil && *rand == target { + return nil + } + } + } + } + } + + return cg +} + +func entryPoints(mains []*ssa.Package) []*ssa.Function { + var entrypoints []*ssa.Function + for _, pkg := range mains { + entrypoints = append(entrypoints, pkg.Func("init")) + if main := pkg.Func("main"); main != nil && pkg.Pkg.Name() == "main" { + entrypoints = append(entrypoints, main) + } + } + return entrypoints +} + type callersResult struct { target *ssa.Function callgraph *callgraph.Graph diff --git a/cmd/guru/callstack.go b/cmd/guru/callstack.go index 3b0bf5f4..b2160b9f 100644 --- a/cmd/guru/callstack.go +++ b/cmd/guru/callstack.go @@ -10,6 +10,7 @@ import ( "golang.org/x/tools/cmd/guru/serial" "golang.org/x/tools/go/callgraph" + "golang.org/x/tools/go/callgraph/static" "golang.org/x/tools/go/loader" "golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa/ssautil" @@ -68,16 +69,31 @@ func callstack(q *Query) error { return fmt.Errorf("no SSA function built for this location (dead code?)") } - // Run the pointer analysis and build the complete call graph. - ptaConfig.BuildCallGraph = true - cg := ptrAnalysis(ptaConfig).CallGraph - cg.DeleteSyntheticNodes() - - // Search for an arbitrary path from a root to the target function. + var callpath []*callgraph.Edge isEnd := func(n *callgraph.Node) bool { return n.Func == target } - callpath := callgraph.PathSearch(cg.Root, isEnd) - if callpath != nil { - callpath = callpath[1:] // remove synthetic edge from + + // First, build a callgraph containing only static call edges, + // and search for an arbitrary path from a root to the target function. + // This is quick, and the user wants a static path if one exists. + cg := static.CallGraph(prog) + cg.DeleteSyntheticNodes() + for _, ep := range entryPoints(ptaConfig.Mains) { + callpath = callgraph.PathSearch(cg.CreateNode(ep), isEnd) + if callpath != nil { + break + } + } + + // No fully static path found. + // Run the pointer analysis and build a complete call graph. + if callpath == nil { + ptaConfig.BuildCallGraph = true + cg := ptrAnalysis(ptaConfig).CallGraph + cg.DeleteSyntheticNodes() + callpath = callgraph.PathSearch(cg.Root, isEnd) + if callpath != nil { + callpath = callpath[1:] // remove synthetic edge from + } } q.Fset = fset diff --git a/cmd/guru/main.go b/cmd/guru/main.go index d0bd0e34..0b85f03e 100644 --- a/cmd/guru/main.go +++ b/cmd/guru/main.go @@ -76,7 +76,7 @@ User manual: http://golang.org/s/oracle-user-manual Example: describe syntax at offset 530 in this file (an import spec): - $ guru describe src/golang.org/x/tools/cmd/guru/main.go:#530 + $ guru describe src/golang.org/x/tools/cmd/guru/main.go:#530 ` func printHelp() {