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