| 1 | // go-callvis: a tool to help visualize the call graph of a Go program. |
|---|---|
| 2 | // |
| 3 | package main |
| 4 | |
| 5 | import ( |
| 6 | "flag" |
| 7 | "fmt" |
| 8 | "go/build" |
| 9 | "io/ioutil" |
| 10 | "log" |
| 11 | "net" |
| 12 | "net/http" |
| 13 | "net/url" |
| 14 | "os" |
| 15 | "time" |
| 16 | |
| 17 | "github.com/pkg/browser" |
| 18 | "golang.org/x/tools/go/buildutil" |
| 19 | ) |
| 20 | |
| 21 | const Usage = `go-callvis: visualize call graph of a Go program. |
| 22 | |
| 23 | Usage: |
| 24 | |
| 25 | go-callvis [flags] package |
| 26 | |
| 27 | Package should be main package, otherwise -tests flag must be used. |
| 28 | |
| 29 | Flags: |
| 30 | |
| 31 | ` |
| 32 | |
| 33 | var ( |
| 34 | focusFlag = flag.String("focus", "main", "Focus specific package using name or import path.") |
| 35 | groupFlag = flag.String("group", "pkg", "Grouping functions by packages and/or types [pkg, type] (separated by comma)") |
| 36 | limitFlag = flag.String("limit", "", "Limit package paths to given prefixes (separated by comma)") |
| 37 | ignoreFlag = flag.String("ignore", "", "Ignore package paths containing given prefixes (separated by comma)") |
| 38 | includeFlag = flag.String("include", "", "Include package paths with given prefixes (separated by comma)") |
| 39 | nostdFlag = flag.Bool("nostd", false, "Omit calls to/from packages in standard library.") |
| 40 | nointerFlag = flag.Bool("nointer", false, "Omit calls to unexported functions.") |
| 41 | testFlag = flag.Bool("tests", false, "Include test code.") |
| 42 | graphvizFlag = flag.Bool("graphviz", false, "Use Graphviz's dot program to render images.") |
| 43 | httpFlag = flag.String("http", ":7878", "HTTP service address.") |
| 44 | skipBrowser = flag.Bool("skipbrowser", false, "Skip opening browser.") |
| 45 | outputFile = flag.String("file", "", "output filename - omit to use server mode") |
| 46 | outputFormat = flag.String("format", "svg", "output file format [svg | png | jpg | ...]") |
| 47 | cacheDir = flag.String("cacheDir", "", "Enable caching to avoid unnecessary re-rendering, you can force rendering by adding 'refresh=true' to the URL query or emptying the cache directory") |
| 48 | callgraphAlgo = flag.String("algo", "pointer", fmt.Sprintf("The algorithm used to construct the call graph. Possible values inlcude: %q, %q, %q, %q", |
| 49 | "static", "cha", "rta", "pointer")) |
| 50 | |
| 51 | debugFlag = flag.Bool("debug", false, "Enable verbose log.") |
| 52 | versionFlag = flag.Bool("version", false, "Show version and exit.") |
| 53 | ) |
| 54 | |
| 55 | func init() { |
| 56 | flag.Var((*buildutil.TagsFlag)(&build.Default.BuildTags), "tags", buildutil.TagsFlagDoc) |
| 57 | // Graphviz options |
| 58 | flag.UintVar(&minlen, "minlen", 2, "Minimum edge length (for wider output).") |
| 59 | flag.Float64Var(&nodesep, "nodesep", 0.35, "Minimum space between two adjacent nodes in the same rank (for taller output).") |
| 60 | flag.StringVar(&nodeshape, "nodeshape", "box", "graph node shape (see graphvis manpage for valid values)") |
| 61 | flag.StringVar(&nodestyle, "nodestyle", "filled,rounded", "graph node style (see graphvis manpage for valid values)") |
| 62 | flag.StringVar(&rankdir, "rankdir", "LR", "Direction of graph layout [LR | RL | TB | BT]") |
| 63 | } |
| 64 | |
| 65 | func logf(f string, a ...interface{}) { |
| 66 | if *debugFlag { |
| 67 | log.Printf(f, a...) |
| 68 | } |
| 69 | } |
| 70 | |
| 71 | func parseHTTPAddr(addr string) string { |
| 72 | host, port, _ := net.SplitHostPort(addr) |
| 73 | if host == "" { |
| 74 | host = "localhost" |
| 75 | } |
| 76 | if port == "" { |
| 77 | port = "80" |
| 78 | } |
| 79 | u := url.URL{ |
| 80 | Scheme: "http", |
| 81 | Host: fmt.Sprintf("%s:%s", host, port), |
| 82 | } |
| 83 | return u.String() |
| 84 | } |
| 85 | |
| 86 | func openBrowser(url string) { |
| 87 | time.Sleep(time.Millisecond * 100) |
| 88 | if err := browser.OpenURL(url); err != nil { |
| 89 | log.Printf("OpenURL error: %v", err) |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | func outputDot(fname string, outputFormat string) { |
| 94 | // get cmdline default for analysis |
| 95 | Analysis.OptsSetup() |
| 96 | |
| 97 | if e := Analysis.ProcessListArgs(); e != nil { |
| 98 | log.Fatalf("%v\n", e) |
| 99 | } |
| 100 | |
| 101 | output, err := Analysis.Render() |
| 102 | if err != nil { |
| 103 | log.Fatalf("%v\n", err) |
| 104 | } |
| 105 | |
| 106 | log.Println("writing dot output..") |
| 107 | |
| 108 | writeErr := ioutil.WriteFile(fmt.Sprintf("%s.gv", fname), output, 0755) |
| 109 | if writeErr != nil { |
| 110 | log.Fatalf("%v\n", writeErr) |
| 111 | } |
| 112 | |
| 113 | log.Printf("converting dot to %s..\n", outputFormat) |
| 114 | |
| 115 | _, err = dotToImage(fname, outputFormat, output) |
| 116 | if err != nil { |
| 117 | log.Fatalf("%v\n", err) |
| 118 | } |
| 119 | } |
| 120 | |
| 121 | //noinspection GoUnhandledErrorResult |
| 122 | func main() { |
| 123 | flag.Parse() |
| 124 | |
| 125 | if *versionFlag { |
| 126 | fmt.Fprintln(os.Stderr, Version()) |
| 127 | os.Exit(0) |
| 128 | } |
| 129 | if *debugFlag { |
| 130 | log.SetFlags(log.Lmicroseconds) |
| 131 | } |
| 132 | |
| 133 | if flag.NArg() != 1 { |
| 134 | fmt.Fprint(os.Stderr, Usage) |
| 135 | flag.PrintDefaults() |
| 136 | os.Exit(2) |
| 137 | } |
| 138 | |
| 139 | args := flag.Args() |
| 140 | tests := *testFlag |
| 141 | httpAddr := *httpFlag |
| 142 | urlAddr := parseHTTPAddr(httpAddr) |
| 143 | |
| 144 | Analysis = new(analysis) |
| 145 | if err := Analysis.DoAnalysis(CallGraphType(*callgraphAlgo), "", tests, args); err != nil { |
| 146 | log.Fatal(err) |
| 147 | } |
| 148 | |
| 149 | http.HandleFunc("/", handler) |
| 150 | |
| 151 | if *outputFile == "" { |
| 152 | *outputFile = "output" |
| 153 | if !*skipBrowser { |
| 154 | go openBrowser(urlAddr) |
| 155 | } |
| 156 | |
| 157 | log.Printf("http serving at %s", urlAddr) |
| 158 | |
| 159 | if err := http.ListenAndServe(httpAddr, nil); err != nil { |
| 160 | log.Fatal(err) |
| 161 | } |
| 162 | } else { |
| 163 | outputDot(*outputFile, *outputFormat) |
| 164 | } |
| 165 | } |
| 166 |
Members