├── .travis.yml ├── LICENSE ├── README.md ├── cmd └── gom │ ├── gom.go │ ├── report.go │ └── stats.go ├── http ├── example_test.go └── http.go └── internal ├── commands └── commands.go ├── driver ├── driver.go └── interactive.go ├── fetch └── fetch.go ├── plugin └── plugin.go ├── profile ├── encode.go ├── filter.go ├── legacy_profile.go ├── profile.go ├── profile_test.go ├── proto.go └── prune.go ├── report ├── report.go ├── source.go └── source_html.go ├── svg ├── svg.go └── svgpan.go ├── symbolizer └── symbolizer.go ├── symbolz └── symbolz.go └── tempfile └── tempfile.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.5 5 | - tip 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2014 Google Inc. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gom [![GoDoc](https://godoc.org/github.com/rakyll/gom/http?status.svg)](https://godoc.org/github.com/rakyll/gom/http) [![Build Status](https://travis-ci.org/rakyll/gom.svg?branch=master)](https://travis-ci.org/rakyll/gom) 2 | 3 | A visual interface to work with runtime profiling data from Go programs. 4 | 5 | ## Installation 6 | 7 | ``` 8 | go get github.com/rakyll/gom/cmd/gom 9 | ``` 10 | 11 | The program you're willing to profile should import the 12 | github.com/rakyll/gom/http package. The http package will register several handlers to provide information about your program during runtime. 13 | 14 | ``` go 15 | import _ "github.com/rakyll/gom/http" 16 | 17 | // If your application is not already running an http server, 18 | // you need to start one. 19 | log.Println(http.ListenAndServe("localhost:6060", nil)) 20 | 21 | ``` 22 | 23 | If your HTTP server is not going to handle the http.DefaultServeMux, 24 | you need to manually register the gom handler to respond to "/debug/_gom". 25 | 26 | For example, gorilla/mux users can use the snippet below: 27 | 28 | ``` go 29 | import gomhttp "github.com/rakyll/gom/http" 30 | 31 | mux := http.NewServeMux() 32 | mux.HandleFunc("/debug/_gom", gomhttp.Handler()) 33 | log.Println(http.ListenAndServe("localhost:6060", nil)) 34 | ``` 35 | 36 | Now, you are ready to launch gom. 37 | 38 | ``` 39 | $ gom 40 | ``` 41 | 42 | - :c loads the CPU profile. 43 | - :h loads the heap profile (default profile on launch). 44 | - :r refreshes the current profile. 45 | - :s toggles the cumulative sort and resorts the items. 46 | - ↓ and ↑ to paginate. 47 | - :f=\ filters the profile with the provided regex. 48 | 49 | ## Goals 50 | 51 | * Building a lightweight tool that works well with runtime profiles is a necessity. Over the time, I recognized that a lot of people around me delayed to use the existing pprof tools because it's a tedious experience. 52 | * gom has no ambition to provide the features at the granularity of the features of the command line tools. Users should feel free to fallback to `go tool pprof` if they need more sophisticated features. 53 | * Allow users to filter, hide and ignore by symbol names. 54 | * Increase the awareness around profiling tools and packages in Go. 55 | * Provide additional lightweight stats where possible. 56 | 57 | ### Minor Goals 58 | * gom should provide interfaces to let the users to export their profile data and continue to work with the go tool. 59 | * Allow users to work with their custom user profiles. 60 | * Make it easier to generate pprof graphical output. 61 | -------------------------------------------------------------------------------- /cmd/gom/gom.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // Package main contains a program that helps to visualize performance-related 16 | // data from Go programs. 17 | // Read the instructions at https://github.com/rakyll/gom to learn more. 18 | package main 19 | 20 | import ( 21 | "flag" 22 | "fmt" 23 | "regexp" 24 | "strings" 25 | 26 | ui "github.com/gizak/termui" 27 | ) 28 | 29 | var ( 30 | target = flag.String("target", "http://localhost:6060", "the target process to profile; it has to enable pprof debug server") 31 | 32 | prompt *ui.Par 33 | ls *ui.List 34 | sp *ui.Sparklines 35 | display *ui.Par 36 | 37 | cpuProfile = &report{name: "profile"} 38 | heapProfile = &report{name: "heap"} 39 | currentProfile = heapProfile 40 | 41 | promptMsg string 42 | 43 | reportPage int 44 | reportItems []string 45 | cum bool 46 | filter string 47 | ) 48 | 49 | func main() { 50 | flag.Parse() 51 | if err := ui.Init(); err != nil { 52 | panic(err) 53 | } 54 | defer ui.Close() 55 | draw() 56 | ui.Handle("/sys/kbd", func(e ui.Event) { 57 | ev := e.Data.(ui.EvtKbd) 58 | switch ev.KeyStr { 59 | case ":": 60 | // TODO(jbd): enable input mode and disable after esc or enter. 61 | promptMsg = ":" 62 | case "C-8": 63 | if l := len(promptMsg); l != 0 { 64 | promptMsg = promptMsg[:l-1] 65 | } 66 | case "": 67 | handleInput() 68 | promptMsg = "" 69 | case "": 70 | if reportPage > 0 { 71 | reportPage-- 72 | } 73 | case "": 74 | reportPage++ 75 | case "": 76 | promptMsg = "" 77 | default: 78 | // TODO: filter irrelevant keys such as up, down, etc. 79 | promptMsg += ev.KeyStr 80 | } 81 | refresh() 82 | }) 83 | ui.Handle("/sys/kbd/C-c", func(ui.Event) { 84 | ui.StopLoop() 85 | }) 86 | ui.Handle("/timer/1s", func(ui.Event) { 87 | loadProfile(false) 88 | loadStats() 89 | refresh() 90 | }) 91 | ui.Handle("/sys/wnd/resize", func(e ui.Event) { 92 | ui.Body.Width = ui.TermWidth() 93 | refresh() 94 | }) 95 | 96 | ui.Body.Align() 97 | ui.Render(ui.Body) 98 | ui.Loop() 99 | } 100 | 101 | func draw() { 102 | display = ui.NewPar("") 103 | display.Height = 1 104 | display.Border = false 105 | 106 | prompt = ui.NewPar(promptMsg) 107 | prompt.Height = 1 108 | prompt.Border = false 109 | 110 | help := ui.NewPar(`:c, :h for profiles; :f to filter; ↓ and ↑ to paginate`) 111 | help.Height = 1 112 | help.Border = false 113 | help.TextBgColor = ui.ColorBlue 114 | help.Bg = ui.ColorBlue 115 | help.TextFgColor = ui.ColorWhite 116 | 117 | gs := ui.Sparkline{} 118 | gs.Title = "goroutines" 119 | gs.Height = 4 120 | gs.LineColor = ui.ColorCyan 121 | 122 | ts := ui.Sparkline{} 123 | ts.Title = "threads" 124 | ts.Height = 4 125 | ts.LineColor = ui.ColorCyan 126 | 127 | sp = ui.NewSparklines(gs, ts) 128 | sp.Height = 10 129 | sp.Border = false 130 | 131 | ls = ui.NewList() 132 | ls.Border = false 133 | ui.Body.AddRows( 134 | ui.NewRow(ui.NewCol(4, 0, prompt), ui.NewCol(8, 0, help)), 135 | ui.NewRow(ui.NewCol(12, 0, sp)), 136 | ui.NewRow(ui.NewCol(12, 0, display)), 137 | ui.NewRow(ui.NewCol(12, 0, ls)), 138 | ) 139 | } 140 | 141 | func loadStats() { 142 | var max = ui.TermWidth() 143 | s, err := fetchStats() 144 | if err != nil { 145 | displayMsg(fmt.Sprintf("error fetching stats: %v", err)) 146 | return 147 | } 148 | displayMsg("") 149 | var cnts = []struct { 150 | cnt int 151 | titleFmt string 152 | }{ 153 | {s.Goroutine, "goroutines (%d)"}, 154 | {s.Thread, "threads (%d)"}, 155 | } 156 | for i, v := range cnts { 157 | if n := len(sp.Lines[i].Data); n > max { 158 | sp.Lines[i].Data = sp.Lines[i].Data[n-max : n] 159 | } 160 | sp.Lines[i].Title = fmt.Sprintf(v.titleFmt, v.cnt) 161 | sp.Lines[i].Data = append(sp.Lines[i].Data, v.cnt) 162 | } 163 | } 164 | 165 | func loadProfile(force bool) { 166 | if err := currentProfile.fetch(force, 0); err != nil { 167 | displayMsg(err.Error()) 168 | return 169 | } 170 | re, _ := regexp.Compile(filter) 171 | reportItems = currentProfile.filter(cum, re) 172 | } 173 | 174 | func refresh() { 175 | prompt.Text = promptMsg 176 | 177 | nreport := ui.TermHeight() - 13 178 | ls.Height = nreport 179 | if len(reportItems) > nreport*reportPage { 180 | // can seek to the page 181 | ls.Items = reportItems[nreport*reportPage : len(reportItems)] 182 | } else { 183 | ls.Items = []string{} 184 | } 185 | 186 | ui.Body.Align() 187 | ui.Render(ui.Body) 188 | } 189 | 190 | func handleInput() { 191 | // TODO(jbd): disable input when handling input. 192 | switch promptMsg { 193 | case ":c": 194 | currentProfile = cpuProfile 195 | reportPage = 0 196 | filter = "" 197 | loadProfile(false) 198 | case ":h": 199 | currentProfile = heapProfile 200 | reportPage = 0 201 | filter = "" 202 | loadProfile(false) 203 | case ":r": 204 | reportPage = 0 205 | loadProfile(true) 206 | case ":s": 207 | cum = !cum 208 | reportPage = 0 209 | loadProfile(false) 210 | } 211 | // handle filtering 212 | if strings.HasPrefix(promptMsg, ":f=") { 213 | re := regexp.MustCompile(":f=(.*)") 214 | filter = re.FindStringSubmatch(promptMsg)[1] 215 | reportPage = 0 216 | loadProfile(false) 217 | } 218 | refresh() 219 | } 220 | 221 | func displayMsg(msg string) { 222 | // TODO(jbd): hide after n secs. 223 | display.Text = msg 224 | ui.Render(display) 225 | } 226 | -------------------------------------------------------------------------------- /cmd/gom/report.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "bytes" 19 | "fmt" 20 | "regexp" 21 | "strings" 22 | "sync" 23 | "time" 24 | 25 | "github.com/rakyll/gom/internal/fetch" 26 | "github.com/rakyll/gom/internal/profile" 27 | goreport "github.com/rakyll/gom/internal/report" 28 | "github.com/rakyll/gom/internal/symbolz" 29 | ) 30 | 31 | type report struct { 32 | mu sync.Mutex 33 | p *profile.Profile 34 | 35 | name string 36 | } 37 | 38 | // fetch fetches the current profile and the symbols from the target program. 39 | func (r *report) fetch(force bool, secs time.Duration) error { 40 | r.mu.Lock() 41 | defer r.mu.Unlock() 42 | if r.p != nil && !force { 43 | return nil 44 | } 45 | if secs == 0 { 46 | secs = 60 * time.Second 47 | } 48 | url := fmt.Sprintf("%s/debug/_gom?view=profile&name=%s", *target, r.name) 49 | p, err := fetch.FetchProfile(url, secs) 50 | if err != nil { 51 | return err 52 | } 53 | if err := symbolz.Symbolize(fmt.Sprintf("%s/debug/_gom?view=symbol", *target), fetch.PostURL, p); err != nil { 54 | return err 55 | } 56 | r.p = p 57 | return nil 58 | } 59 | 60 | // filter filters the report with a focus regex. If no focus is provided, 61 | // it reports back with the entire set of calls. 62 | // Focus regex works on the package, type and function names. Filtered 63 | // results will include parent samples from the call graph. 64 | func (r *report) filter(cum bool, focus *regexp.Regexp) []string { 65 | r.mu.Lock() 66 | defer r.mu.Unlock() 67 | if r.p == nil { 68 | return nil 69 | } 70 | c := r.p.Copy() 71 | c.FilterSamplesByName(focus, nil, nil) 72 | rpt := goreport.NewDefault(c, goreport.Options{ 73 | OutputFormat: goreport.Text, 74 | CumSort: cum, 75 | PrintAddresses: true, 76 | }) 77 | buf := bytes.NewBuffer(nil) 78 | goreport.Generate(buf, rpt, nil) 79 | return strings.Split(buf.String(), "\n") 80 | } 81 | -------------------------------------------------------------------------------- /cmd/gom/stats.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "net/http" 21 | ) 22 | 23 | type stats struct { 24 | Goroutine int `json:"goroutine"` 25 | Thread int `json:"thread"` 26 | Block int `json:"block"` 27 | Timestamp int64 `json:"timestamp"` 28 | } 29 | 30 | func fetchStats() (s stats, err error) { 31 | url := fmt.Sprintf("%s/debug/_gom", *target) 32 | resp, err := http.Get(url) 33 | if err != nil { 34 | return s, err 35 | } 36 | defer resp.Body.Close() 37 | 38 | d := json.NewDecoder(resp.Body) 39 | err = d.Decode(&s) 40 | return 41 | } 42 | -------------------------------------------------------------------------------- /http/example_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package http_test 16 | 17 | import ( 18 | "log" 19 | "net/http" 20 | 21 | gomhttp "github.com/rakyll/gom/http" 22 | ) 23 | 24 | func Example_gorillaMux() { 25 | mux := http.NewServeMux() 26 | mux.HandleFunc("/debug/_gom", gomhttp.Handler()) 27 | log.Println(http.ListenAndServe("localhost:6060", nil)) 28 | } 29 | -------------------------------------------------------------------------------- /http/http.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Google Inc. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package http 16 | 17 | import ( 18 | "encoding/json" 19 | "fmt" 20 | "net/http" 21 | "runtime/pprof" 22 | "time" 23 | 24 | httppprof "net/http/pprof" 25 | ) 26 | 27 | type stats struct { 28 | Goroutine int `json:"goroutine"` 29 | Thread int `json:"thread"` 30 | Block int `json:"block"` 31 | Timestamp int64 `json:"timestamp"` 32 | } 33 | 34 | func init() { 35 | http.HandleFunc("/debug/_gom", Handler()) 36 | } 37 | 38 | // Handler returns an http.HandlerFunc that returns pprof profiles 39 | // and additional metrics. 40 | // The handler must be accessible through the "/debug/_gom" route 41 | // in order for gom to display the stats from the debugged program. 42 | // See the godoc examples for usage. 43 | func Handler() http.HandlerFunc { 44 | // TODO(jbd): enable block profile. 45 | return func(w http.ResponseWriter, r *http.Request) { 46 | switch r.URL.Query().Get("view") { 47 | case "profile": 48 | name := r.URL.Query().Get("name") 49 | if name == "profile" { 50 | httppprof.Profile(w, r) 51 | return 52 | } 53 | httppprof.Handler(name).ServeHTTP(w, r) 54 | return 55 | case "symbol": 56 | httppprof.Symbol(w, r) 57 | return 58 | } 59 | n := &stats{ 60 | Goroutine: pprof.Lookup("goroutine").Count(), 61 | Thread: pprof.Lookup("threadcreate").Count(), 62 | Block: pprof.Lookup("block").Count(), 63 | Timestamp: time.Now().Unix(), 64 | } 65 | err := json.NewEncoder(w).Encode(n) 66 | if err != nil { 67 | w.WriteHeader(500) 68 | fmt.Fprint(w, err) 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /internal/commands/commands.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package commands defines and manages the basic pprof commands 6 | package commands 7 | 8 | import ( 9 | "bytes" 10 | "fmt" 11 | "io" 12 | "io/ioutil" 13 | "os" 14 | "os/exec" 15 | "runtime" 16 | "strings" 17 | "time" 18 | 19 | "github.com/rakyll/gom/internal/plugin" 20 | "github.com/rakyll/gom/internal/report" 21 | "github.com/rakyll/gom/internal/svg" 22 | "github.com/rakyll/gom/internal/tempfile" 23 | ) 24 | 25 | // Commands describes the commands accepted by pprof. 26 | type Commands map[string]*Command 27 | 28 | // Command describes the actions for a pprof command. Includes a 29 | // function for command-line completion, the report format to use 30 | // during report generation, any postprocessing functions, and whether 31 | // the command expects a regexp parameter (typically a function name). 32 | type Command struct { 33 | Complete Completer // autocomplete for interactive mode 34 | Format int // report format to generate 35 | PostProcess PostProcessor // postprocessing to run on report 36 | HasParam bool // Collect a parameter from the CLI 37 | Usage string // Help text 38 | } 39 | 40 | // Completer is a function for command-line autocompletion 41 | type Completer func(prefix string) string 42 | 43 | // PostProcessor is a function that applies post-processing to the report output 44 | type PostProcessor func(input *bytes.Buffer, output io.Writer, ui plugin.UI) error 45 | 46 | // PProf returns the basic pprof report-generation commands 47 | func PProf(c Completer, interactive **bool) Commands { 48 | return Commands{ 49 | // Commands that require no post-processing. 50 | "tags": {nil, report.Tags, nil, false, "Outputs all tags in the profile"}, 51 | "raw": {c, report.Raw, nil, false, "Outputs a text representation of the raw profile"}, 52 | "dot": {c, report.Dot, nil, false, "Outputs a graph in DOT format"}, 53 | "top": {c, report.Text, nil, false, "Outputs top entries in text form"}, 54 | "tree": {c, report.Tree, nil, false, "Outputs a text rendering of call graph"}, 55 | "text": {c, report.Text, nil, false, "Outputs top entries in text form"}, 56 | "disasm": {c, report.Dis, nil, true, "Output annotated assembly for functions matching regexp or address"}, 57 | "list": {c, report.List, nil, true, "Output annotated source for functions matching regexp"}, 58 | "peek": {c, report.Tree, nil, true, "Output callers/callees of functions matching regexp"}, 59 | 60 | // Save binary formats to a file 61 | "callgrind": {c, report.Callgrind, awayFromTTY("callgraph.out"), false, "Outputs a graph in callgrind format"}, 62 | "proto": {c, report.Proto, awayFromTTY("pb.gz"), false, "Outputs the profile in compressed protobuf format"}, 63 | 64 | // Generate report in DOT format and postprocess with dot 65 | "gif": {c, report.Dot, invokeDot("gif"), false, "Outputs a graph image in GIF format"}, 66 | "pdf": {c, report.Dot, invokeDot("pdf"), false, "Outputs a graph in PDF format"}, 67 | "png": {c, report.Dot, invokeDot("png"), false, "Outputs a graph image in PNG format"}, 68 | "ps": {c, report.Dot, invokeDot("ps"), false, "Outputs a graph in PS format"}, 69 | 70 | // Save SVG output into a file after including svgpan library 71 | "svg": {c, report.Dot, saveSVGToFile(), false, "Outputs a graph in SVG format"}, 72 | 73 | // Visualize postprocessed dot output 74 | "eog": {c, report.Dot, invokeVisualizer(interactive, invokeDot("svg"), "svg", []string{"eog"}), false, "Visualize graph through eog"}, 75 | "evince": {c, report.Dot, invokeVisualizer(interactive, invokeDot("pdf"), "pdf", []string{"evince"}), false, "Visualize graph through evince"}, 76 | "gv": {c, report.Dot, invokeVisualizer(interactive, invokeDot("ps"), "ps", []string{"gv --noantialias"}), false, "Visualize graph through gv"}, 77 | "web": {c, report.Dot, invokeVisualizer(interactive, saveSVGToFile(), "svg", browsers()), false, "Visualize graph through web browser"}, 78 | 79 | // Visualize HTML directly generated by report. 80 | "weblist": {c, report.WebList, invokeVisualizer(interactive, awayFromTTY("html"), "html", browsers()), true, "Output annotated source in HTML for functions matching regexp or address"}, 81 | } 82 | } 83 | 84 | // browsers returns a list of commands to attempt for web visualization 85 | // on the current platform 86 | func browsers() []string { 87 | var cmds []string 88 | if exe := os.Getenv("BROWSER"); exe != "" { 89 | cmds = append(cmds, exe) 90 | } 91 | switch runtime.GOOS { 92 | case "darwin": 93 | cmds = append(cmds, "/usr/bin/open") 94 | case "windows": 95 | cmds = append(cmds, "cmd /c start") 96 | default: 97 | cmds = append(cmds, "xdg-open") 98 | } 99 | cmds = append(cmds, "chrome", "google-chrome", "firefox") 100 | return cmds 101 | } 102 | 103 | // NewCompleter creates an autocompletion function for a set of commands. 104 | func NewCompleter(cs Commands) Completer { 105 | return func(line string) string { 106 | switch tokens := strings.Fields(line); len(tokens) { 107 | case 0: 108 | // Nothing to complete 109 | case 1: 110 | // Single token -- complete command name 111 | found := "" 112 | for c := range cs { 113 | if strings.HasPrefix(c, tokens[0]) { 114 | if found != "" { 115 | return line 116 | } 117 | found = c 118 | } 119 | } 120 | if found != "" { 121 | return found 122 | } 123 | default: 124 | // Multiple tokens -- complete using command completer 125 | if c, ok := cs[tokens[0]]; ok { 126 | if c.Complete != nil { 127 | lastTokenIdx := len(tokens) - 1 128 | lastToken := tokens[lastTokenIdx] 129 | if strings.HasPrefix(lastToken, "-") { 130 | lastToken = "-" + c.Complete(lastToken[1:]) 131 | } else { 132 | lastToken = c.Complete(lastToken) 133 | } 134 | return strings.Join(append(tokens[:lastTokenIdx], lastToken), " ") 135 | } 136 | } 137 | } 138 | return line 139 | } 140 | } 141 | 142 | // awayFromTTY saves the output in a file if it would otherwise go to 143 | // the terminal screen. This is used to avoid dumping binary data on 144 | // the screen. 145 | func awayFromTTY(format string) PostProcessor { 146 | return func(input *bytes.Buffer, output io.Writer, ui plugin.UI) error { 147 | if output == os.Stdout && ui.IsTerminal() { 148 | tempFile, err := tempfile.New("", "profile", "."+format) 149 | if err != nil { 150 | return err 151 | } 152 | ui.PrintErr("Generating report in ", tempFile.Name()) 153 | _, err = fmt.Fprint(tempFile, input) 154 | return err 155 | } 156 | _, err := fmt.Fprint(output, input) 157 | return err 158 | } 159 | } 160 | 161 | func invokeDot(format string) PostProcessor { 162 | divert := awayFromTTY(format) 163 | return func(input *bytes.Buffer, output io.Writer, ui plugin.UI) error { 164 | if _, err := exec.LookPath("dot"); err != nil { 165 | ui.PrintErr("Cannot find dot, have you installed Graphviz?") 166 | return err 167 | } 168 | cmd := exec.Command("dot", "-T"+format) 169 | var buf bytes.Buffer 170 | cmd.Stdin, cmd.Stdout, cmd.Stderr = input, &buf, os.Stderr 171 | if err := cmd.Run(); err != nil { 172 | return err 173 | } 174 | return divert(&buf, output, ui) 175 | } 176 | } 177 | 178 | func saveSVGToFile() PostProcessor { 179 | generateSVG := invokeDot("svg") 180 | divert := awayFromTTY("svg") 181 | return func(input *bytes.Buffer, output io.Writer, ui plugin.UI) error { 182 | baseSVG := &bytes.Buffer{} 183 | generateSVG(input, baseSVG, ui) 184 | massaged := &bytes.Buffer{} 185 | fmt.Fprint(massaged, svg.Massage(*baseSVG)) 186 | return divert(massaged, output, ui) 187 | } 188 | } 189 | 190 | var vizTmpDir string 191 | 192 | func makeVizTmpDir() error { 193 | if vizTmpDir != "" { 194 | return nil 195 | } 196 | name, err := ioutil.TempDir("", "pprof-") 197 | if err != nil { 198 | return err 199 | } 200 | vizTmpDir = name 201 | return nil 202 | } 203 | 204 | func invokeVisualizer(interactive **bool, format PostProcessor, suffix string, visualizers []string) PostProcessor { 205 | return func(input *bytes.Buffer, output io.Writer, ui plugin.UI) error { 206 | if err := makeVizTmpDir(); err != nil { 207 | return err 208 | } 209 | tempFile, err := tempfile.New(vizTmpDir, "pprof", "."+suffix) 210 | if err != nil { 211 | return err 212 | } 213 | tempfile.DeferDelete(tempFile.Name()) 214 | if err = format(input, tempFile, ui); err != nil { 215 | return err 216 | } 217 | tempFile.Close() // on windows, if the file is Open, start cannot access it. 218 | // Try visualizers until one is successful 219 | for _, v := range visualizers { 220 | // Separate command and arguments for exec.Command. 221 | args := strings.Split(v, " ") 222 | if len(args) == 0 { 223 | continue 224 | } 225 | viewer := exec.Command(args[0], append(args[1:], tempFile.Name())...) 226 | viewer.Stderr = os.Stderr 227 | if err = viewer.Start(); err == nil { 228 | // The viewer might just send a message to another program 229 | // to open the file. Give that program a little time to open the 230 | // file before we remove it. 231 | time.Sleep(1 * time.Second) 232 | 233 | if !**interactive { 234 | // In command-line mode, wait for the viewer to be closed 235 | // before proceeding 236 | return viewer.Wait() 237 | } 238 | return nil 239 | } 240 | } 241 | return err 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /internal/driver/driver.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package driver implements the core pprof functionality. It can be 6 | // parameterized with a flag implementation, fetch and symbolize 7 | // mechanisms. 8 | package driver 9 | 10 | import ( 11 | "bytes" 12 | "fmt" 13 | "io" 14 | "net/url" 15 | "os" 16 | "path/filepath" 17 | "regexp" 18 | "sort" 19 | "strconv" 20 | "strings" 21 | "sync" 22 | "time" 23 | 24 | "github.com/rakyll/gom/internal/commands" 25 | "github.com/rakyll/gom/internal/plugin" 26 | "github.com/rakyll/gom/internal/profile" 27 | "github.com/rakyll/gom/internal/report" 28 | "github.com/rakyll/gom/internal/tempfile" 29 | ) 30 | 31 | // PProf acquires a profile, and symbolizes it using a profile 32 | // manager. Then it generates a report formatted according to the 33 | // options selected through the flags package. 34 | func PProf(flagset plugin.FlagSet, fetch plugin.Fetcher, sym plugin.Symbolizer, obj plugin.ObjTool, ui plugin.UI, overrides commands.Commands) error { 35 | // Remove any temporary files created during pprof processing. 36 | defer tempfile.Cleanup() 37 | 38 | f, err := getFlags(flagset, overrides, ui) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | obj.SetConfig(*f.flagTools) 44 | 45 | sources := f.profileSource 46 | if len(sources) > 1 { 47 | source := sources[0] 48 | // If the first argument is a supported object file, treat as executable. 49 | if file, err := obj.Open(source, 0); err == nil { 50 | file.Close() 51 | f.profileExecName = source 52 | sources = sources[1:] 53 | } else if *f.flagBuildID == "" && isBuildID(source) { 54 | f.flagBuildID = &source 55 | sources = sources[1:] 56 | } 57 | } 58 | 59 | // errMu protects concurrent accesses to errset and err. errset is set if an 60 | // error is encountered by one of the goroutines grabbing a profile. 61 | errMu, errset := sync.Mutex{}, false 62 | 63 | // Fetch profiles. 64 | wg := sync.WaitGroup{} 65 | profs := make([]*profile.Profile, len(sources)) 66 | for i, source := range sources { 67 | wg.Add(1) 68 | go func(i int, src string) { 69 | defer wg.Done() 70 | p, grabErr := grabProfile(src, f.profileExecName, *f.flagBuildID, fetch, sym, obj, ui, f) 71 | if grabErr != nil { 72 | errMu.Lock() 73 | defer errMu.Unlock() 74 | errset, err = true, grabErr 75 | return 76 | } 77 | profs[i] = p 78 | }(i, source) 79 | } 80 | wg.Wait() 81 | if errset { 82 | return err 83 | } 84 | 85 | // Merge profiles. 86 | prof := profs[0] 87 | for _, p := range profs[1:] { 88 | if err = prof.Merge(p, 1); err != nil { 89 | return err 90 | } 91 | } 92 | 93 | if *f.flagBase != "" { 94 | // Fetch base profile and subtract from current profile. 95 | base, err := grabProfile(*f.flagBase, f.profileExecName, *f.flagBuildID, fetch, sym, obj, ui, f) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | if err = prof.Merge(base, -1); err != nil { 101 | return err 102 | } 103 | } 104 | 105 | if err := processFlags(prof, ui, f); err != nil { 106 | return err 107 | } 108 | 109 | if !*f.flagRuntime { 110 | prof.RemoveUninteresting() 111 | } 112 | 113 | if *f.flagInteractive { 114 | return interactive(prof, obj, ui, f) 115 | } 116 | 117 | return generate(false, prof, obj, ui, f) 118 | } 119 | 120 | // isBuildID determines if the profile may contain a build ID, by 121 | // checking that it is a string of hex digits. 122 | func isBuildID(id string) bool { 123 | return strings.Trim(id, "0123456789abcdefABCDEF") == "" 124 | } 125 | 126 | // adjustURL updates the profile source URL based on heuristics. It 127 | // will append ?seconds=sec for CPU profiles if not already 128 | // specified. Returns the hostname if the profile is remote. 129 | func adjustURL(source string, sec int, ui plugin.UI) (adjusted, host string, duration time.Duration) { 130 | // If there is a local file with this name, just use it. 131 | if _, err := os.Stat(source); err == nil { 132 | return source, "", 0 133 | } 134 | 135 | url, err := url.Parse(source) 136 | 137 | // Automatically add http:// to URLs of the form hostname:port/path. 138 | // url.Parse treats "hostname" as the Scheme. 139 | if err != nil || (url.Host == "" && url.Scheme != "" && url.Scheme != "file") { 140 | url, err = url.Parse("http://" + source) 141 | if err != nil { 142 | return source, url.Host, time.Duration(30) * time.Second 143 | } 144 | } 145 | if scheme := strings.ToLower(url.Scheme); scheme == "" || scheme == "file" { 146 | url.Scheme = "" 147 | return url.String(), "", 0 148 | } 149 | 150 | values := url.Query() 151 | if urlSeconds := values.Get("seconds"); urlSeconds != "" { 152 | if us, err := strconv.ParseInt(urlSeconds, 10, 32); err == nil { 153 | if sec >= 0 { 154 | ui.PrintErr("Overriding -seconds for URL ", source) 155 | } 156 | sec = int(us) 157 | } 158 | } 159 | 160 | switch strings.ToLower(url.Path) { 161 | case "", "/": 162 | // Apply default /profilez. 163 | url.Path = "/profilez" 164 | case "/protoz": 165 | // Rewrite to /profilez?type=proto 166 | url.Path = "/profilez" 167 | values.Set("type", "proto") 168 | } 169 | 170 | if hasDuration(url.Path) { 171 | if sec > 0 { 172 | duration = time.Duration(sec) * time.Second 173 | values.Set("seconds", fmt.Sprintf("%d", sec)) 174 | } else { 175 | // Assume default duration: 30 seconds 176 | duration = 30 * time.Second 177 | } 178 | } 179 | url.RawQuery = values.Encode() 180 | return url.String(), url.Host, duration 181 | } 182 | 183 | func hasDuration(path string) bool { 184 | for _, trigger := range []string{"profilez", "wallz", "/profile"} { 185 | if strings.Contains(path, trigger) { 186 | return true 187 | } 188 | } 189 | return false 190 | } 191 | 192 | // preprocess does filtering and aggregation of a profile based on the 193 | // requested options. 194 | func preprocess(prof *profile.Profile, ui plugin.UI, f *flags) error { 195 | if *f.flagFocus != "" || *f.flagIgnore != "" || *f.flagHide != "" { 196 | focus, ignore, hide, err := compileFocusIgnore(*f.flagFocus, *f.flagIgnore, *f.flagHide) 197 | if err != nil { 198 | return err 199 | } 200 | fm, im, hm := prof.FilterSamplesByName(focus, ignore, hide) 201 | 202 | warnNoMatches(fm, *f.flagFocus, "Focus", ui) 203 | warnNoMatches(im, *f.flagIgnore, "Ignore", ui) 204 | warnNoMatches(hm, *f.flagHide, "Hide", ui) 205 | } 206 | 207 | if *f.flagTagFocus != "" || *f.flagTagIgnore != "" { 208 | focus, err := compileTagFilter(*f.flagTagFocus, ui) 209 | if err != nil { 210 | return err 211 | } 212 | ignore, err := compileTagFilter(*f.flagTagIgnore, ui) 213 | if err != nil { 214 | return err 215 | } 216 | fm, im := prof.FilterSamplesByTag(focus, ignore) 217 | 218 | warnNoMatches(fm, *f.flagTagFocus, "TagFocus", ui) 219 | warnNoMatches(im, *f.flagTagIgnore, "TagIgnore", ui) 220 | } 221 | 222 | return aggregate(prof, f) 223 | } 224 | 225 | func compileFocusIgnore(focus, ignore, hide string) (f, i, h *regexp.Regexp, err error) { 226 | if focus != "" { 227 | if f, err = regexp.Compile(focus); err != nil { 228 | return nil, nil, nil, fmt.Errorf("parsing focus regexp: %v", err) 229 | } 230 | } 231 | 232 | if ignore != "" { 233 | if i, err = regexp.Compile(ignore); err != nil { 234 | return nil, nil, nil, fmt.Errorf("parsing ignore regexp: %v", err) 235 | } 236 | } 237 | 238 | if hide != "" { 239 | if h, err = regexp.Compile(hide); err != nil { 240 | return nil, nil, nil, fmt.Errorf("parsing hide regexp: %v", err) 241 | } 242 | } 243 | return 244 | } 245 | 246 | func compileTagFilter(filter string, ui plugin.UI) (f func(string, string, int64) bool, err error) { 247 | if filter == "" { 248 | return nil, nil 249 | } 250 | if numFilter := parseTagFilterRange(filter); numFilter != nil { 251 | ui.PrintErr("Interpreted '", filter, "' as range, not regexp") 252 | return func(key, val string, num int64) bool { 253 | if val != "" { 254 | return false 255 | } 256 | return numFilter(num, key) 257 | }, nil 258 | } 259 | fx, err := regexp.Compile(filter) 260 | if err != nil { 261 | return nil, err 262 | } 263 | 264 | return func(key, val string, num int64) bool { 265 | if val == "" { 266 | return false 267 | } 268 | return fx.MatchString(key + ":" + val) 269 | }, nil 270 | } 271 | 272 | var tagFilterRangeRx = regexp.MustCompile("([[:digit:]]+)([[:alpha:]]+)") 273 | 274 | // parseTagFilterRange returns a function to checks if a value is 275 | // contained on the range described by a string. It can recognize 276 | // strings of the form: 277 | // "32kb" -- matches values == 32kb 278 | // ":64kb" -- matches values <= 64kb 279 | // "4mb:" -- matches values >= 4mb 280 | // "12kb:64mb" -- matches values between 12kb and 64mb (both included). 281 | func parseTagFilterRange(filter string) func(int64, string) bool { 282 | ranges := tagFilterRangeRx.FindAllStringSubmatch(filter, 2) 283 | if len(ranges) == 0 { 284 | return nil // No ranges were identified 285 | } 286 | v, err := strconv.ParseInt(ranges[0][1], 10, 64) 287 | if err != nil { 288 | panic(fmt.Errorf("Failed to parse int %s: %v", ranges[0][1], err)) 289 | } 290 | value, unit := report.ScaleValue(v, ranges[0][2], ranges[0][2]) 291 | if len(ranges) == 1 { 292 | switch match := ranges[0][0]; filter { 293 | case match: 294 | return func(v int64, u string) bool { 295 | sv, su := report.ScaleValue(v, u, unit) 296 | return su == unit && sv == value 297 | } 298 | case match + ":": 299 | return func(v int64, u string) bool { 300 | sv, su := report.ScaleValue(v, u, unit) 301 | return su == unit && sv >= value 302 | } 303 | case ":" + match: 304 | return func(v int64, u string) bool { 305 | sv, su := report.ScaleValue(v, u, unit) 306 | return su == unit && sv <= value 307 | } 308 | } 309 | return nil 310 | } 311 | if filter != ranges[0][0]+":"+ranges[1][0] { 312 | return nil 313 | } 314 | if v, err = strconv.ParseInt(ranges[1][1], 10, 64); err != nil { 315 | panic(fmt.Errorf("Failed to parse int %s: %v", ranges[1][1], err)) 316 | } 317 | value2, unit2 := report.ScaleValue(v, ranges[1][2], unit) 318 | if unit != unit2 { 319 | return nil 320 | } 321 | return func(v int64, u string) bool { 322 | sv, su := report.ScaleValue(v, u, unit) 323 | return su == unit && sv >= value && sv <= value2 324 | } 325 | } 326 | 327 | func warnNoMatches(match bool, rx, option string, ui plugin.UI) { 328 | if !match && rx != "" && rx != "." { 329 | ui.PrintErr(option + " expression matched no samples: " + rx) 330 | } 331 | } 332 | 333 | // grabProfile fetches and symbolizes a profile. 334 | func grabProfile(source, exec, buildid string, fetch plugin.Fetcher, sym plugin.Symbolizer, obj plugin.ObjTool, ui plugin.UI, f *flags) (*profile.Profile, error) { 335 | source, host, duration := adjustURL(source, *f.flagSeconds, ui) 336 | remote := host != "" 337 | 338 | if remote { 339 | ui.Print("Fetching profile from ", source) 340 | if duration != 0 { 341 | ui.Print("Please wait... (" + duration.String() + ")") 342 | } 343 | } 344 | 345 | now := time.Now() 346 | // Fetch profile from source. 347 | // Give 50% slack on the timeout. 348 | p, err := fetch(source, duration+duration/2, ui) 349 | if err != nil { 350 | return nil, err 351 | } 352 | 353 | // Update the time/duration if the profile source doesn't include it. 354 | // TODO(rsilvera): Remove this when we remove support for legacy profiles. 355 | if remote { 356 | if p.TimeNanos == 0 { 357 | p.TimeNanos = now.UnixNano() 358 | } 359 | if duration != 0 && p.DurationNanos == 0 { 360 | p.DurationNanos = int64(duration) 361 | } 362 | } 363 | 364 | // Replace executable/buildID with the options provided in the 365 | // command line. Assume the executable is the first Mapping entry. 366 | if exec != "" || buildid != "" { 367 | if len(p.Mapping) == 0 { 368 | // Create a fake mapping to hold the user option, and associate 369 | // all samples to it. 370 | m := &profile.Mapping{ 371 | ID: 1, 372 | } 373 | for _, l := range p.Location { 374 | l.Mapping = m 375 | } 376 | p.Mapping = []*profile.Mapping{m} 377 | } 378 | if exec != "" { 379 | p.Mapping[0].File = exec 380 | } 381 | if buildid != "" { 382 | p.Mapping[0].BuildID = buildid 383 | } 384 | } 385 | 386 | if err := sym(*f.flagSymbolize, source, p, obj, ui); err != nil { 387 | return nil, err 388 | } 389 | 390 | // Save a copy of any remote profiles, unless the user is explicitly 391 | // saving it. 392 | if remote && !f.isFormat("proto") { 393 | prefix := "pprof." 394 | if len(p.Mapping) > 0 && p.Mapping[0].File != "" { 395 | prefix = prefix + filepath.Base(p.Mapping[0].File) + "." 396 | } 397 | if !strings.ContainsRune(host, os.PathSeparator) { 398 | prefix = prefix + host + "." 399 | } 400 | for _, s := range p.SampleType { 401 | prefix = prefix + s.Type + "." 402 | } 403 | 404 | dir := os.Getenv("PPROF_TMPDIR") 405 | tempFile, err := tempfile.New(dir, prefix, ".pb.gz") 406 | if err == nil { 407 | if err = p.Write(tempFile); err == nil { 408 | ui.PrintErr("Saved profile in ", tempFile.Name()) 409 | } 410 | } 411 | if err != nil { 412 | ui.PrintErr("Could not save profile: ", err) 413 | } 414 | } 415 | 416 | if err := p.Demangle(obj.Demangle); err != nil { 417 | ui.PrintErr("Failed to demangle profile: ", err) 418 | } 419 | 420 | if err := p.CheckValid(); err != nil { 421 | return nil, fmt.Errorf("Grab %s: %v", source, err) 422 | } 423 | 424 | return p, nil 425 | } 426 | 427 | type flags struct { 428 | flagInteractive *bool // Accept commands interactively 429 | flagCommands map[string]*bool // pprof commands without parameters 430 | flagParamCommands map[string]*string // pprof commands with parameters 431 | 432 | flagOutput *string // Output file name 433 | 434 | flagCum *bool // Sort by cumulative data 435 | flagCallTree *bool // generate a context-sensitive call tree 436 | 437 | flagAddresses *bool // Report at address level 438 | flagLines *bool // Report at source line level 439 | flagFiles *bool // Report at file level 440 | flagFunctions *bool // Report at function level [default] 441 | 442 | flagSymbolize *string // Symbolization options (=none to disable) 443 | flagBuildID *string // Override build if for first mapping 444 | 445 | flagNodeCount *int // Max number of nodes to show 446 | flagNodeFraction *float64 // Hide nodes below *total 447 | flagEdgeFraction *float64 // Hide edges below *total 448 | flagTrim *bool // Set to false to ignore NodeCount/*Fraction 449 | flagRuntime *bool // Show runtime call frames in memory profiles 450 | flagFocus *string // Restricts to paths going through a node matching regexp 451 | flagIgnore *string // Skips paths going through any nodes matching regexp 452 | flagHide *string // Skips sample locations matching regexp 453 | flagTagFocus *string // Restrict to samples tagged with key:value matching regexp 454 | flagTagIgnore *string // Discard samples tagged with key:value matching regexp 455 | flagDropNegative *bool // Skip negative values 456 | 457 | flagBase *string // Source for base profile to user for comparison 458 | 459 | flagSeconds *int // Length of time for dynamic profiles 460 | 461 | flagTotalDelay *bool // Display total delay at each region 462 | flagContentions *bool // Display number of delays at each region 463 | flagMeanDelay *bool // Display mean delay at each region 464 | 465 | flagInUseSpace *bool // Display in-use memory size 466 | flagInUseObjects *bool // Display in-use object counts 467 | flagAllocSpace *bool // Display allocated memory size 468 | flagAllocObjects *bool // Display allocated object counts 469 | flagDisplayUnit *string // Measurement unit to use on reports 470 | flagDivideBy *float64 // Ratio to divide sample values 471 | 472 | flagSampleIndex *int // Sample value to use in reports. 473 | flagMean *bool // Use mean of sample_index over count 474 | 475 | flagTools *string 476 | profileSource []string 477 | profileExecName string 478 | 479 | extraUsage string 480 | commands commands.Commands 481 | } 482 | 483 | func (f *flags) isFormat(format string) bool { 484 | if fl := f.flagCommands[format]; fl != nil { 485 | return *fl 486 | } 487 | if fl := f.flagParamCommands[format]; fl != nil { 488 | return *fl != "" 489 | } 490 | return false 491 | } 492 | 493 | // String provides a printable representation for the current set of flags. 494 | func (f *flags) String(p *profile.Profile) string { 495 | var ret string 496 | 497 | if ix := *f.flagSampleIndex; ix != -1 { 498 | ret += fmt.Sprintf(" %-25s : %d (%s)\n", "sample_index", ix, p.SampleType[ix].Type) 499 | } 500 | if ix := *f.flagMean; ix { 501 | ret += boolFlagString("mean") 502 | } 503 | if *f.flagDisplayUnit != "minimum" { 504 | ret += stringFlagString("unit", *f.flagDisplayUnit) 505 | } 506 | 507 | switch { 508 | case *f.flagInteractive: 509 | ret += boolFlagString("interactive") 510 | } 511 | for name, fl := range f.flagCommands { 512 | if *fl { 513 | ret += boolFlagString(name) 514 | } 515 | } 516 | 517 | if *f.flagCum { 518 | ret += boolFlagString("cum") 519 | } 520 | if *f.flagCallTree { 521 | ret += boolFlagString("call_tree") 522 | } 523 | 524 | switch { 525 | case *f.flagAddresses: 526 | ret += boolFlagString("addresses") 527 | case *f.flagLines: 528 | ret += boolFlagString("lines") 529 | case *f.flagFiles: 530 | ret += boolFlagString("files") 531 | case *f.flagFunctions: 532 | ret += boolFlagString("functions") 533 | } 534 | 535 | if *f.flagNodeCount != -1 { 536 | ret += intFlagString("nodecount", *f.flagNodeCount) 537 | } 538 | 539 | ret += floatFlagString("nodefraction", *f.flagNodeFraction) 540 | ret += floatFlagString("edgefraction", *f.flagEdgeFraction) 541 | 542 | if *f.flagFocus != "" { 543 | ret += stringFlagString("focus", *f.flagFocus) 544 | } 545 | if *f.flagIgnore != "" { 546 | ret += stringFlagString("ignore", *f.flagIgnore) 547 | } 548 | if *f.flagHide != "" { 549 | ret += stringFlagString("hide", *f.flagHide) 550 | } 551 | 552 | if *f.flagTagFocus != "" { 553 | ret += stringFlagString("tagfocus", *f.flagTagFocus) 554 | } 555 | if *f.flagTagIgnore != "" { 556 | ret += stringFlagString("tagignore", *f.flagTagIgnore) 557 | } 558 | 559 | return ret 560 | } 561 | 562 | func boolFlagString(label string) string { 563 | return fmt.Sprintf(" %-25s : true\n", label) 564 | } 565 | 566 | func stringFlagString(label, value string) string { 567 | return fmt.Sprintf(" %-25s : %s\n", label, value) 568 | } 569 | 570 | func intFlagString(label string, value int) string { 571 | return fmt.Sprintf(" %-25s : %d\n", label, value) 572 | } 573 | 574 | func floatFlagString(label string, value float64) string { 575 | return fmt.Sprintf(" %-25s : %f\n", label, value) 576 | } 577 | 578 | // Utility routines to set flag values. 579 | func newBool(b bool) *bool { 580 | return &b 581 | } 582 | 583 | func newString(s string) *string { 584 | return &s 585 | } 586 | 587 | func newFloat64(fl float64) *float64 { 588 | return &fl 589 | } 590 | 591 | func newInt(i int) *int { 592 | return &i 593 | } 594 | 595 | func (f *flags) usage(ui plugin.UI) { 596 | var commandMsg []string 597 | for name, cmd := range f.commands { 598 | if cmd.HasParam { 599 | name = name + "=p" 600 | } 601 | commandMsg = append(commandMsg, 602 | fmt.Sprintf(" -%-16s %s", name, cmd.Usage)) 603 | } 604 | 605 | sort.Strings(commandMsg) 606 | 607 | text := usageMsgHdr + strings.Join(commandMsg, "\n") + "\n" + usageMsg + "\n" 608 | if f.extraUsage != "" { 609 | text += f.extraUsage + "\n" 610 | } 611 | text += usageMsgVars 612 | ui.Print(text) 613 | } 614 | 615 | func getFlags(flag plugin.FlagSet, overrides commands.Commands, ui plugin.UI) (*flags, error) { 616 | f := &flags{ 617 | flagInteractive: flag.Bool("interactive", false, "Accepts commands interactively"), 618 | flagCommands: make(map[string]*bool), 619 | flagParamCommands: make(map[string]*string), 620 | 621 | // Filename for file-based output formats, stdout by default. 622 | flagOutput: flag.String("output", "", "Output filename for file-based outputs "), 623 | // Comparisons. 624 | flagBase: flag.String("base", "", "Source for base profile for comparison"), 625 | flagDropNegative: flag.Bool("drop_negative", false, "Ignore negative differences"), 626 | 627 | // Data sorting criteria. 628 | flagCum: flag.Bool("cum", false, "Sort by cumulative data"), 629 | // Graph handling options. 630 | flagCallTree: flag.Bool("call_tree", false, "Create a context-sensitive call tree"), 631 | // Granularity of output resolution. 632 | flagAddresses: flag.Bool("addresses", false, "Report at address level"), 633 | flagLines: flag.Bool("lines", false, "Report at source line level"), 634 | flagFiles: flag.Bool("files", false, "Report at source file level"), 635 | flagFunctions: flag.Bool("functions", false, "Report at function level [default]"), 636 | // Internal options. 637 | flagSymbolize: flag.String("symbolize", "", "Options for profile symbolization"), 638 | flagBuildID: flag.String("buildid", "", "Override build id for first mapping"), 639 | // Filtering options 640 | flagNodeCount: flag.Int("nodecount", -1, "Max number of nodes to show"), 641 | flagNodeFraction: flag.Float64("nodefraction", 0.005, "Hide nodes below *total"), 642 | flagEdgeFraction: flag.Float64("edgefraction", 0.001, "Hide edges below *total"), 643 | flagTrim: flag.Bool("trim", true, "Honor nodefraction/edgefraction/nodecount defaults"), 644 | flagRuntime: flag.Bool("runtime", false, "Show runtime call frames in memory profiles"), 645 | flagFocus: flag.String("focus", "", "Restricts to paths going through a node matching regexp"), 646 | flagIgnore: flag.String("ignore", "", "Skips paths going through any nodes matching regexp"), 647 | flagHide: flag.String("hide", "", "Skips nodes matching regexp"), 648 | flagTagFocus: flag.String("tagfocus", "", "Restrict to samples with tags in range or matched by regexp"), 649 | flagTagIgnore: flag.String("tagignore", "", "Discard samples with tags in range or matched by regexp"), 650 | // CPU profile options 651 | flagSeconds: flag.Int("seconds", -1, "Length of time for dynamic profiles"), 652 | // Heap profile options 653 | flagInUseSpace: flag.Bool("inuse_space", false, "Display in-use memory size"), 654 | flagInUseObjects: flag.Bool("inuse_objects", false, "Display in-use object counts"), 655 | flagAllocSpace: flag.Bool("alloc_space", false, "Display allocated memory size"), 656 | flagAllocObjects: flag.Bool("alloc_objects", false, "Display allocated object counts"), 657 | flagDisplayUnit: flag.String("unit", "minimum", "Measurement units to display"), 658 | flagDivideBy: flag.Float64("divide_by", 1.0, "Ratio to divide all samples before visualization"), 659 | flagSampleIndex: flag.Int("sample_index", -1, "Index of sample value to report"), 660 | flagMean: flag.Bool("mean", false, "Average sample value over first value (count)"), 661 | // Contention profile options 662 | flagTotalDelay: flag.Bool("total_delay", false, "Display total delay at each region"), 663 | flagContentions: flag.Bool("contentions", false, "Display number of delays at each region"), 664 | flagMeanDelay: flag.Bool("mean_delay", false, "Display mean delay at each region"), 665 | flagTools: flag.String("tools", os.Getenv("PPROF_TOOLS"), "Path for object tool pathnames"), 666 | extraUsage: flag.ExtraUsage(), 667 | } 668 | 669 | // Flags used during command processing 670 | interactive := &f.flagInteractive 671 | f.commands = commands.PProf(functionCompleter, interactive) 672 | 673 | // Override commands 674 | for name, cmd := range overrides { 675 | f.commands[name] = cmd 676 | } 677 | 678 | for name, cmd := range f.commands { 679 | if cmd.HasParam { 680 | f.flagParamCommands[name] = flag.String(name, "", "Generate a report in "+name+" format, matching regexp") 681 | } else { 682 | f.flagCommands[name] = flag.Bool(name, false, "Generate a report in "+name+" format") 683 | } 684 | } 685 | 686 | args := flag.Parse(func() { f.usage(ui) }) 687 | if len(args) == 0 { 688 | return nil, fmt.Errorf("no profile source specified") 689 | } 690 | 691 | f.profileSource = args 692 | 693 | // Instruct legacy heapz parsers to grab historical allocation data, 694 | // instead of the default in-use data. Not available with tcmalloc. 695 | if *f.flagAllocSpace || *f.flagAllocObjects { 696 | profile.LegacyHeapAllocated = true 697 | } 698 | 699 | if profileDir := os.Getenv("PPROF_TMPDIR"); profileDir == "" { 700 | profileDir = os.Getenv("HOME") + "/pprof" 701 | os.Setenv("PPROF_TMPDIR", profileDir) 702 | if err := os.MkdirAll(profileDir, 0755); err != nil { 703 | return nil, fmt.Errorf("failed to access temp dir %s: %v", profileDir, err) 704 | } 705 | } 706 | 707 | return f, nil 708 | } 709 | 710 | func processFlags(p *profile.Profile, ui plugin.UI, f *flags) error { 711 | flagDis := f.isFormat("disasm") 712 | flagPeek := f.isFormat("peek") 713 | flagWebList := f.isFormat("weblist") 714 | flagList := f.isFormat("list") 715 | 716 | if flagDis || flagWebList { 717 | // Collect all samples at address granularity for assembly 718 | // listing. 719 | f.flagNodeCount = newInt(0) 720 | f.flagAddresses = newBool(true) 721 | f.flagLines = newBool(false) 722 | f.flagFiles = newBool(false) 723 | f.flagFunctions = newBool(false) 724 | } 725 | 726 | if flagPeek { 727 | // Collect all samples at function granularity for peek command 728 | f.flagNodeCount = newInt(0) 729 | f.flagAddresses = newBool(false) 730 | f.flagLines = newBool(false) 731 | f.flagFiles = newBool(false) 732 | f.flagFunctions = newBool(true) 733 | } 734 | 735 | if flagList { 736 | // Collect all samples at fileline granularity for source 737 | // listing. 738 | f.flagNodeCount = newInt(0) 739 | f.flagAddresses = newBool(false) 740 | f.flagLines = newBool(true) 741 | f.flagFiles = newBool(false) 742 | f.flagFunctions = newBool(false) 743 | } 744 | 745 | if !*f.flagTrim { 746 | f.flagNodeCount = newInt(0) 747 | f.flagNodeFraction = newFloat64(0) 748 | f.flagEdgeFraction = newFloat64(0) 749 | } 750 | 751 | if oc := countFlagMap(f.flagCommands, f.flagParamCommands); oc == 0 { 752 | f.flagInteractive = newBool(true) 753 | } else if oc > 1 { 754 | f.usage(ui) 755 | return fmt.Errorf("must set at most one output format") 756 | } 757 | 758 | // Apply nodecount defaults for non-interactive mode. The 759 | // interactive shell will apply defaults for the interactive mode. 760 | if *f.flagNodeCount < 0 && !*f.flagInteractive { 761 | switch { 762 | default: 763 | f.flagNodeCount = newInt(80) 764 | case f.isFormat("text"): 765 | f.flagNodeCount = newInt(0) 766 | } 767 | } 768 | 769 | // Apply legacy options and diagnose conflicts. 770 | if rc := countFlags([]*bool{f.flagAddresses, f.flagLines, f.flagFiles, f.flagFunctions}); rc == 0 { 771 | f.flagFunctions = newBool(true) 772 | } else if rc > 1 { 773 | f.usage(ui) 774 | return fmt.Errorf("must set at most one granularity option") 775 | } 776 | 777 | var err error 778 | si, sm := *f.flagSampleIndex, *f.flagMean || *f.flagMeanDelay 779 | si, err = sampleIndex(p, &f.flagTotalDelay, si, 1, "delay", "-total_delay", err) 780 | si, err = sampleIndex(p, &f.flagMeanDelay, si, 1, "delay", "-mean_delay", err) 781 | si, err = sampleIndex(p, &f.flagContentions, si, 0, "contentions", "-contentions", err) 782 | 783 | si, err = sampleIndex(p, &f.flagInUseSpace, si, 1, "inuse_space", "-inuse_space", err) 784 | si, err = sampleIndex(p, &f.flagInUseObjects, si, 0, "inuse_objects", "-inuse_objects", err) 785 | si, err = sampleIndex(p, &f.flagAllocSpace, si, 1, "alloc_space", "-alloc_space", err) 786 | si, err = sampleIndex(p, &f.flagAllocObjects, si, 0, "alloc_objects", "-alloc_objects", err) 787 | 788 | if si == -1 { 789 | // Use last value if none is requested. 790 | si = len(p.SampleType) - 1 791 | } else if si < 0 || si >= len(p.SampleType) { 792 | err = fmt.Errorf("sample_index value %d out of range [0..%d]", si, len(p.SampleType)-1) 793 | } 794 | 795 | if err != nil { 796 | f.usage(ui) 797 | return err 798 | } 799 | f.flagSampleIndex, f.flagMean = newInt(si), newBool(sm) 800 | return nil 801 | } 802 | 803 | func sampleIndex(p *profile.Profile, flag **bool, 804 | sampleIndex int, 805 | newSampleIndex int, 806 | sampleType, option string, 807 | err error) (int, error) { 808 | if err != nil || !**flag { 809 | return sampleIndex, err 810 | } 811 | *flag = newBool(false) 812 | if sampleIndex != -1 { 813 | return 0, fmt.Errorf("set at most one sample value selection option") 814 | } 815 | if newSampleIndex >= len(p.SampleType) || 816 | p.SampleType[newSampleIndex].Type != sampleType { 817 | return 0, fmt.Errorf("option %s not valid for this profile", option) 818 | } 819 | return newSampleIndex, nil 820 | } 821 | 822 | func countFlags(bs []*bool) int { 823 | var c int 824 | for _, b := range bs { 825 | if *b { 826 | c++ 827 | } 828 | } 829 | return c 830 | } 831 | 832 | func countFlagMap(bms map[string]*bool, bmrxs map[string]*string) int { 833 | var c int 834 | for _, b := range bms { 835 | if *b { 836 | c++ 837 | } 838 | } 839 | for _, s := range bmrxs { 840 | if *s != "" { 841 | c++ 842 | } 843 | } 844 | return c 845 | } 846 | 847 | var usageMsgHdr = "usage: pprof [options] [binary] ...\n" + 848 | "Output format (only set one):\n" 849 | 850 | var usageMsg = "Output file parameters (for file-based output formats):\n" + 851 | " -output=f Generate output on file f (stdout by default)\n" + 852 | "Output granularity (only set one):\n" + 853 | " -functions Report at function level [default]\n" + 854 | " -files Report at source file level\n" + 855 | " -lines Report at source line level\n" + 856 | " -addresses Report at address level\n" + 857 | "Comparison options:\n" + 858 | " -base Show delta from this profile\n" + 859 | " -drop_negative Ignore negative differences\n" + 860 | "Sorting options:\n" + 861 | " -cum Sort by cumulative data\n\n" + 862 | "Dynamic profile options:\n" + 863 | " -seconds=N Length of time for dynamic profiles\n" + 864 | "Profile trimming options:\n" + 865 | " -nodecount=N Max number of nodes to show\n" + 866 | " -nodefraction=f Hide nodes below *total\n" + 867 | " -edgefraction=f Hide edges below *total\n" + 868 | "Sample value selection option (by index):\n" + 869 | " -sample_index Index of sample value to display\n" + 870 | " -mean Average sample value over first value\n" + 871 | "Sample value selection option (for heap profiles):\n" + 872 | " -inuse_space Display in-use memory size\n" + 873 | " -inuse_objects Display in-use object counts\n" + 874 | " -alloc_space Display allocated memory size\n" + 875 | " -alloc_objects Display allocated object counts\n" + 876 | "Sample value selection option (for contention profiles):\n" + 877 | " -total_delay Display total delay at each region\n" + 878 | " -contentions Display number of delays at each region\n" + 879 | " -mean_delay Display mean delay at each region\n" + 880 | "Filtering options:\n" + 881 | " -runtime Show runtime call frames in memory profiles\n" + 882 | " -focus=r Restricts to paths going through a node matching regexp\n" + 883 | " -ignore=r Skips paths going through any nodes matching regexp\n" + 884 | " -tagfocus=r Restrict to samples tagged with key:value matching regexp\n" + 885 | " Restrict to samples with numeric tags in range (eg \"32kb:1mb\")\n" + 886 | " -tagignore=r Discard samples tagged with key:value matching regexp\n" + 887 | " Avoid samples with numeric tags in range (eg \"1mb:\")\n" + 888 | "Miscellaneous:\n" + 889 | " -call_tree Generate a context-sensitive call tree\n" + 890 | " -unit=u Convert all samples to unit u for display\n" + 891 | " -divide_by=f Scale all samples by dividing them by f\n" + 892 | " -buildid=id Override build id for main binary in profile\n" + 893 | " -tools=path Search path for object-level tools\n" + 894 | " -help This message" 895 | 896 | var usageMsgVars = "Environment Variables:\n" + 897 | " PPROF_TMPDIR Location for saved profiles (default $HOME/pprof)\n" + 898 | " PPROF_TOOLS Search path for object-level tools\n" + 899 | " PPROF_BINARY_PATH Search path for local binary files\n" + 900 | " default: $HOME/pprof/binaries\n" + 901 | " finds binaries by $name and $buildid/$name" 902 | 903 | func aggregate(prof *profile.Profile, f *flags) error { 904 | switch { 905 | case f.isFormat("proto"), f.isFormat("raw"): 906 | // No aggregation for raw profiles. 907 | case f.isFormat("callgrind"): 908 | // Aggregate to file/line for callgrind. 909 | fallthrough 910 | case *f.flagLines: 911 | return prof.Aggregate(true, true, true, true, false) 912 | case *f.flagFiles: 913 | return prof.Aggregate(true, false, true, false, false) 914 | case *f.flagFunctions: 915 | return prof.Aggregate(true, true, false, false, false) 916 | case f.isFormat("weblist"), f.isFormat("disasm"): 917 | return prof.Aggregate(false, true, true, true, true) 918 | } 919 | return nil 920 | } 921 | 922 | // parseOptions parses the options into report.Options 923 | // Returns a function to postprocess the report after generation. 924 | func parseOptions(f *flags) (o *report.Options, p commands.PostProcessor, err error) { 925 | 926 | if *f.flagDivideBy == 0 { 927 | return nil, nil, fmt.Errorf("zero divisor specified") 928 | } 929 | 930 | o = &report.Options{ 931 | CumSort: *f.flagCum, 932 | CallTree: *f.flagCallTree, 933 | PrintAddresses: *f.flagAddresses, 934 | DropNegative: *f.flagDropNegative, 935 | Ratio: 1 / *f.flagDivideBy, 936 | 937 | NodeCount: *f.flagNodeCount, 938 | NodeFraction: *f.flagNodeFraction, 939 | EdgeFraction: *f.flagEdgeFraction, 940 | OutputUnit: *f.flagDisplayUnit, 941 | } 942 | 943 | for cmd, b := range f.flagCommands { 944 | if *b { 945 | pcmd := f.commands[cmd] 946 | o.OutputFormat = pcmd.Format 947 | return o, pcmd.PostProcess, nil 948 | } 949 | } 950 | 951 | for cmd, rx := range f.flagParamCommands { 952 | if *rx != "" { 953 | pcmd := f.commands[cmd] 954 | if o.Symbol, err = regexp.Compile(*rx); err != nil { 955 | return nil, nil, fmt.Errorf("parsing -%s regexp: %v", cmd, err) 956 | } 957 | o.OutputFormat = pcmd.Format 958 | return o, pcmd.PostProcess, nil 959 | } 960 | } 961 | 962 | return nil, nil, fmt.Errorf("no output format selected") 963 | } 964 | 965 | type sampleValueFunc func(*profile.Sample) int64 966 | 967 | // sampleFormat returns a function to extract values out of a profile.Sample, 968 | // and the type/units of those values. 969 | func sampleFormat(p *profile.Profile, f *flags) (sampleValueFunc, string, string) { 970 | valueIndex := *f.flagSampleIndex 971 | 972 | if *f.flagMean { 973 | return meanExtractor(valueIndex), "mean_" + p.SampleType[valueIndex].Type, p.SampleType[valueIndex].Unit 974 | } 975 | 976 | return valueExtractor(valueIndex), p.SampleType[valueIndex].Type, p.SampleType[valueIndex].Unit 977 | } 978 | 979 | func valueExtractor(ix int) sampleValueFunc { 980 | return func(s *profile.Sample) int64 { 981 | return s.Value[ix] 982 | } 983 | } 984 | 985 | func meanExtractor(ix int) sampleValueFunc { 986 | return func(s *profile.Sample) int64 { 987 | if s.Value[0] == 0 { 988 | return 0 989 | } 990 | return s.Value[ix] / s.Value[0] 991 | } 992 | } 993 | 994 | func generate(interactive bool, prof *profile.Profile, obj plugin.ObjTool, ui plugin.UI, f *flags) error { 995 | o, postProcess, err := parseOptions(f) 996 | if err != nil { 997 | return err 998 | } 999 | 1000 | var w io.Writer 1001 | if *f.flagOutput == "" { 1002 | w = os.Stdout 1003 | } else { 1004 | ui.PrintErr("Generating report in ", *f.flagOutput) 1005 | outputFile, err := os.Create(*f.flagOutput) 1006 | if err != nil { 1007 | return err 1008 | } 1009 | defer outputFile.Close() 1010 | w = outputFile 1011 | } 1012 | 1013 | if prof.Empty() { 1014 | return fmt.Errorf("profile is empty") 1015 | } 1016 | 1017 | value, stype, unit := sampleFormat(prof, f) 1018 | o.SampleType = stype 1019 | rpt := report.New(prof, *o, value, unit) 1020 | 1021 | // Do not apply filters if we're just generating a proto, so we 1022 | // still have all the data. 1023 | if o.OutputFormat != report.Proto { 1024 | // Delay applying focus/ignore until after creating the report so 1025 | // the report reflects the total number of samples. 1026 | if err := preprocess(prof, ui, f); err != nil { 1027 | return err 1028 | } 1029 | } 1030 | 1031 | if postProcess == nil { 1032 | return report.Generate(w, rpt, obj) 1033 | } 1034 | 1035 | var dot bytes.Buffer 1036 | if err = report.Generate(&dot, rpt, obj); err != nil { 1037 | return err 1038 | } 1039 | 1040 | return postProcess(&dot, w, ui) 1041 | } 1042 | -------------------------------------------------------------------------------- /internal/driver/interactive.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package driver 6 | 7 | import ( 8 | "fmt" 9 | "io" 10 | "regexp" 11 | "sort" 12 | "strconv" 13 | "strings" 14 | 15 | "github.com/rakyll/gom/internal/commands" 16 | "github.com/rakyll/gom/internal/plugin" 17 | "github.com/rakyll/gom/internal/profile" 18 | ) 19 | 20 | var profileFunctionNames = []string{} 21 | 22 | // functionCompleter replaces provided substring with a function 23 | // name retrieved from a profile if a single match exists. Otherwise, 24 | // it returns unchanged substring. It defaults to no-op if the profile 25 | // is not specified. 26 | func functionCompleter(substring string) string { 27 | found := "" 28 | for _, fName := range profileFunctionNames { 29 | if strings.Contains(fName, substring) { 30 | if found != "" { 31 | return substring 32 | } 33 | found = fName 34 | } 35 | } 36 | if found != "" { 37 | return found 38 | } 39 | return substring 40 | } 41 | 42 | // updateAutoComplete enhances autocompletion with information that can be 43 | // retrieved from the profile 44 | func updateAutoComplete(p *profile.Profile) { 45 | profileFunctionNames = nil // remove function names retrieved previously 46 | for _, fn := range p.Function { 47 | profileFunctionNames = append(profileFunctionNames, fn.Name) 48 | } 49 | } 50 | 51 | // splitCommand splits the command line input into tokens separated by 52 | // spaces. Takes care to separate commands of the form 'top10' into 53 | // two tokens: 'top' and '10' 54 | func splitCommand(input string) []string { 55 | fields := strings.Fields(input) 56 | if num := strings.IndexAny(fields[0], "0123456789"); num != -1 { 57 | inputNumber := fields[0][num:] 58 | fields[0] = fields[0][:num] 59 | fields = append([]string{fields[0], inputNumber}, fields[1:]...) 60 | } 61 | return fields 62 | } 63 | 64 | // interactive displays a prompt and reads commands for profile 65 | // manipulation/visualization. 66 | func interactive(p *profile.Profile, obj plugin.ObjTool, ui plugin.UI, f *flags) error { 67 | updateAutoComplete(p) 68 | 69 | // Enter command processing loop. 70 | ui.Print("Entering interactive mode (type \"help\" for commands)") 71 | ui.SetAutoComplete(commands.NewCompleter(f.commands)) 72 | 73 | for { 74 | input, err := readCommand(p, ui, f) 75 | if err != nil { 76 | if err != io.EOF { 77 | return err 78 | } 79 | if input == "" { 80 | return nil 81 | } 82 | } 83 | // Process simple commands. 84 | switch input { 85 | case "": 86 | continue 87 | case ":": 88 | f.flagFocus = newString("") 89 | f.flagIgnore = newString("") 90 | f.flagTagFocus = newString("") 91 | f.flagTagIgnore = newString("") 92 | f.flagHide = newString("") 93 | continue 94 | } 95 | 96 | fields := splitCommand(input) 97 | // Process report generation commands. 98 | if _, ok := f.commands[fields[0]]; ok { 99 | if err := generateReport(p, fields, obj, ui, f); err != nil { 100 | if err == io.EOF { 101 | return nil 102 | } 103 | ui.PrintErr(err) 104 | } 105 | continue 106 | } 107 | 108 | switch cmd := fields[0]; cmd { 109 | case "help": 110 | commandHelp(fields, ui, f) 111 | continue 112 | case "exit", "quit": 113 | return nil 114 | } 115 | 116 | // Process option settings. 117 | if of, err := optFlags(p, input, f); err == nil { 118 | f = of 119 | } else { 120 | ui.PrintErr("Error: ", err.Error()) 121 | } 122 | } 123 | } 124 | 125 | func generateReport(p *profile.Profile, cmd []string, obj plugin.ObjTool, ui plugin.UI, f *flags) error { 126 | prof := p.Copy() 127 | 128 | cf, err := cmdFlags(prof, cmd, ui, f) 129 | if err != nil { 130 | return err 131 | } 132 | 133 | return generate(true, prof, obj, ui, cf) 134 | } 135 | 136 | // validateRegex checks if a string is a valid regular expression. 137 | func validateRegex(v string) error { 138 | _, err := regexp.Compile(v) 139 | return err 140 | } 141 | 142 | // readCommand prompts for and reads the next command. 143 | func readCommand(p *profile.Profile, ui plugin.UI, f *flags) (string, error) { 144 | //ui.Print("Options:\n", f.String(p)) 145 | s, err := ui.ReadLine() 146 | return strings.TrimSpace(s), err 147 | } 148 | 149 | func commandHelp(_ []string, ui plugin.UI, f *flags) error { 150 | help := ` 151 | Commands: 152 | cmd [n] [--cum] [focus_regex]* [-ignore_regex]* 153 | Produce a text report with the top n entries. 154 | Include samples matching focus_regex, and exclude ignore_regex. 155 | Add --cum to sort using cumulative data. 156 | Available commands: 157 | ` 158 | var commands []string 159 | for name, cmd := range f.commands { 160 | commands = append(commands, fmt.Sprintf(" %-12s %s", name, cmd.Usage)) 161 | } 162 | sort.Strings(commands) 163 | 164 | help = help + strings.Join(commands, "\n") + ` 165 | peek func_regex 166 | Display callers and callees of functions matching func_regex. 167 | 168 | dot [n] [focus_regex]* [-ignore_regex]* [>file] 169 | Produce an annotated callgraph with the top n entries. 170 | Include samples matching focus_regex, and exclude ignore_regex. 171 | For other outputs, replace dot with: 172 | - Graphic formats: dot, svg, pdf, ps, gif, png (use > to name output file) 173 | - Graph viewer: gv, web, evince, eog 174 | 175 | callgrind [n] [focus_regex]* [-ignore_regex]* [>file] 176 | Produce a file in callgrind-compatible format. 177 | Include samples matching focus_regex, and exclude ignore_regex. 178 | 179 | weblist func_regex [-ignore_regex]* 180 | Show annotated source with interspersed assembly in a web browser. 181 | 182 | list func_regex [-ignore_regex]* 183 | Print source for routines matching func_regex, and exclude ignore_regex. 184 | 185 | disasm func_regex [-ignore_regex]* 186 | Disassemble routines matching func_regex, and exclude ignore_regex. 187 | 188 | tags tag_regex [-ignore_regex]* 189 | List tags with key:value matching tag_regex and exclude ignore_regex. 190 | 191 | quit/exit/^D 192 | Exit pprof. 193 | 194 | option=value 195 | The following options can be set individually: 196 | cum/flat: Sort entries based on cumulative or flat data 197 | call_tree: Build context-sensitive call trees 198 | nodecount: Max number of entries to display 199 | nodefraction: Min frequency ratio of nodes to display 200 | edgefraction: Min frequency ratio of edges to display 201 | focus/ignore: Regexp to include/exclude samples by name/file 202 | tagfocus/tagignore: Regexp or value range to filter samples by tag 203 | eg "1mb", "1mb:2mb", ":64kb" 204 | 205 | functions: Level of aggregation for sample data 206 | files: 207 | lines: 208 | addresses: 209 | 210 | unit: Measurement unit to use on reports 211 | 212 | Sample value selection by index: 213 | sample_index: Index of sample value to display 214 | mean: Average sample value over first value 215 | 216 | Sample value selection by name: 217 | alloc_space for heap profiles 218 | alloc_objects 219 | inuse_space 220 | inuse_objects 221 | 222 | total_delay for contention profiles 223 | mean_delay 224 | contentions 225 | 226 | : Clear focus/ignore/hide/tagfocus/tagignore` 227 | 228 | ui.Print(help) 229 | return nil 230 | } 231 | 232 | // cmdFlags parses the options of an interactive command and returns 233 | // an updated flags object. 234 | func cmdFlags(prof *profile.Profile, input []string, ui plugin.UI, f *flags) (*flags, error) { 235 | cf := *f 236 | 237 | var focus, ignore string 238 | output := *cf.flagOutput 239 | nodeCount := *cf.flagNodeCount 240 | cmd := input[0] 241 | 242 | // Update output flags based on parameters. 243 | tokens := input[1:] 244 | for p := 0; p < len(tokens); p++ { 245 | t := tokens[p] 246 | if t == "" { 247 | continue 248 | } 249 | if c, err := strconv.ParseInt(t, 10, 32); err == nil { 250 | nodeCount = int(c) 251 | continue 252 | } 253 | switch t[0] { 254 | case '>': 255 | if len(t) > 1 { 256 | output = t[1:] 257 | continue 258 | } 259 | // find next token 260 | for p++; p < len(tokens); p++ { 261 | if tokens[p] != "" { 262 | output = tokens[p] 263 | break 264 | } 265 | } 266 | case '-': 267 | if t == "--cum" || t == "-cum" { 268 | cf.flagCum = newBool(true) 269 | continue 270 | } 271 | ignore = catRegex(ignore, t[1:]) 272 | default: 273 | focus = catRegex(focus, t) 274 | } 275 | } 276 | 277 | pcmd, ok := f.commands[cmd] 278 | if !ok { 279 | return nil, fmt.Errorf("Unexpected parse failure: %v", input) 280 | } 281 | // Reset flags 282 | cf.flagCommands = make(map[string]*bool) 283 | cf.flagParamCommands = make(map[string]*string) 284 | 285 | if !pcmd.HasParam { 286 | cf.flagCommands[cmd] = newBool(true) 287 | 288 | switch cmd { 289 | case "tags": 290 | cf.flagTagFocus = newString(focus) 291 | cf.flagTagIgnore = newString(ignore) 292 | default: 293 | cf.flagFocus = newString(catRegex(*cf.flagFocus, focus)) 294 | cf.flagIgnore = newString(catRegex(*cf.flagIgnore, ignore)) 295 | } 296 | } else { 297 | if focus == "" { 298 | focus = "." 299 | } 300 | cf.flagParamCommands[cmd] = newString(focus) 301 | cf.flagIgnore = newString(catRegex(*cf.flagIgnore, ignore)) 302 | } 303 | 304 | if nodeCount < 0 { 305 | switch cmd { 306 | case "text", "top": 307 | // Default text/top to 10 nodes on interactive mode 308 | nodeCount = 10 309 | default: 310 | nodeCount = 80 311 | } 312 | } 313 | 314 | cf.flagNodeCount = newInt(nodeCount) 315 | cf.flagOutput = newString(output) 316 | 317 | // Do regular flags processing 318 | if err := processFlags(prof, ui, &cf); err != nil { 319 | cf.usage(ui) 320 | return nil, err 321 | } 322 | 323 | return &cf, nil 324 | } 325 | 326 | func catRegex(a, b string) string { 327 | if a == "" { 328 | return b 329 | } 330 | if b == "" { 331 | return a 332 | } 333 | return a + "|" + b 334 | } 335 | 336 | // optFlags parses an interactive option setting and returns 337 | // an updated flags object. 338 | func optFlags(p *profile.Profile, input string, f *flags) (*flags, error) { 339 | inputs := strings.SplitN(input, "=", 2) 340 | option := strings.ToLower(strings.TrimSpace(inputs[0])) 341 | var value string 342 | if len(inputs) == 2 { 343 | value = strings.TrimSpace(inputs[1]) 344 | } 345 | 346 | of := *f 347 | 348 | var err error 349 | var bv bool 350 | var uv uint64 351 | var fv float64 352 | 353 | switch option { 354 | case "cum": 355 | if bv, err = parseBool(value); err != nil { 356 | return nil, err 357 | } 358 | of.flagCum = newBool(bv) 359 | case "flat": 360 | if bv, err = parseBool(value); err != nil { 361 | return nil, err 362 | } 363 | of.flagCum = newBool(!bv) 364 | case "call_tree": 365 | if bv, err = parseBool(value); err != nil { 366 | return nil, err 367 | } 368 | of.flagCallTree = newBool(bv) 369 | case "unit": 370 | of.flagDisplayUnit = newString(value) 371 | case "sample_index": 372 | if uv, err = strconv.ParseUint(value, 10, 32); err != nil { 373 | return nil, err 374 | } 375 | if ix := int(uv); ix < 0 || ix >= len(p.SampleType) { 376 | return nil, fmt.Errorf("sample_index out of range [0..%d]", len(p.SampleType)-1) 377 | } 378 | of.flagSampleIndex = newInt(int(uv)) 379 | case "mean": 380 | if bv, err = parseBool(value); err != nil { 381 | return nil, err 382 | } 383 | of.flagMean = newBool(bv) 384 | case "nodecount": 385 | if uv, err = strconv.ParseUint(value, 10, 32); err != nil { 386 | return nil, err 387 | } 388 | of.flagNodeCount = newInt(int(uv)) 389 | case "nodefraction": 390 | if fv, err = strconv.ParseFloat(value, 64); err != nil { 391 | return nil, err 392 | } 393 | of.flagNodeFraction = newFloat64(fv) 394 | case "edgefraction": 395 | if fv, err = strconv.ParseFloat(value, 64); err != nil { 396 | return nil, err 397 | } 398 | of.flagEdgeFraction = newFloat64(fv) 399 | case "focus": 400 | if err = validateRegex(value); err != nil { 401 | return nil, err 402 | } 403 | of.flagFocus = newString(value) 404 | case "ignore": 405 | if err = validateRegex(value); err != nil { 406 | return nil, err 407 | } 408 | of.flagIgnore = newString(value) 409 | case "tagfocus": 410 | if err = validateRegex(value); err != nil { 411 | return nil, err 412 | } 413 | of.flagTagFocus = newString(value) 414 | case "tagignore": 415 | if err = validateRegex(value); err != nil { 416 | return nil, err 417 | } 418 | of.flagTagIgnore = newString(value) 419 | case "hide": 420 | if err = validateRegex(value); err != nil { 421 | return nil, err 422 | } 423 | of.flagHide = newString(value) 424 | case "addresses", "files", "lines", "functions": 425 | if bv, err = parseBool(value); err != nil { 426 | return nil, err 427 | } 428 | if !bv { 429 | return nil, fmt.Errorf("select one of addresses/files/lines/functions") 430 | } 431 | setGranularityToggle(option, &of) 432 | default: 433 | if ix := findSampleIndex(p, "", option); ix >= 0 { 434 | of.flagSampleIndex = newInt(ix) 435 | } else if ix := findSampleIndex(p, "total_", option); ix >= 0 { 436 | of.flagSampleIndex = newInt(ix) 437 | of.flagMean = newBool(false) 438 | } else if ix := findSampleIndex(p, "mean_", option); ix >= 1 { 439 | of.flagSampleIndex = newInt(ix) 440 | of.flagMean = newBool(true) 441 | } else { 442 | return nil, fmt.Errorf("unrecognized command: %s", input) 443 | } 444 | } 445 | return &of, nil 446 | } 447 | 448 | // parseBool parses a string as a boolean value. 449 | func parseBool(v string) (bool, error) { 450 | switch strings.ToLower(v) { 451 | case "true", "t", "yes", "y", "1", "": 452 | return true, nil 453 | case "false", "f", "no", "n", "0": 454 | return false, nil 455 | } 456 | return false, fmt.Errorf(`illegal input "%s" for bool value`, v) 457 | } 458 | 459 | func findSampleIndex(p *profile.Profile, prefix, sampleType string) int { 460 | if !strings.HasPrefix(sampleType, prefix) { 461 | return -1 462 | } 463 | sampleType = strings.TrimPrefix(sampleType, prefix) 464 | for i, r := range p.SampleType { 465 | if r.Type == sampleType { 466 | return i 467 | } 468 | } 469 | return -1 470 | } 471 | 472 | // setGranularityToggle manages the set of granularity options. These 473 | // operate as a toggle; turning one on turns the others off. 474 | func setGranularityToggle(o string, fl *flags) { 475 | t, f := newBool(true), newBool(false) 476 | fl.flagFunctions = f 477 | fl.flagFiles = f 478 | fl.flagLines = f 479 | fl.flagAddresses = f 480 | switch o { 481 | case "functions": 482 | fl.flagFunctions = t 483 | case "files": 484 | fl.flagFiles = t 485 | case "lines": 486 | fl.flagLines = t 487 | case "addresses": 488 | fl.flagAddresses = t 489 | default: 490 | panic(fmt.Errorf("unexpected option %s", o)) 491 | } 492 | } 493 | -------------------------------------------------------------------------------- /internal/fetch/fetch.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package fetch provides an extensible mechanism to fetch a profile 6 | // from a data source. 7 | package fetch 8 | 9 | import ( 10 | "fmt" 11 | "io" 12 | "io/ioutil" 13 | "net/http" 14 | "net/url" 15 | "os" 16 | "strings" 17 | "time" 18 | 19 | "github.com/rakyll/gom/internal/plugin" 20 | "github.com/rakyll/gom/internal/profile" 21 | ) 22 | 23 | // FetchProfile reads from a data source (network, file) and generates a 24 | // profile. 25 | func FetchProfile(source string, timeout time.Duration) (*profile.Profile, error) { 26 | return Fetcher(source, timeout, plugin.StandardUI()) 27 | } 28 | 29 | // Fetcher is the plugin.Fetcher version of FetchProfile. 30 | func Fetcher(source string, timeout time.Duration, ui plugin.UI) (*profile.Profile, error) { 31 | var f io.ReadCloser 32 | var err error 33 | 34 | url, err := url.Parse(source) 35 | if err == nil && url.Host != "" { 36 | f, err = FetchURL(source, timeout) 37 | } else { 38 | f, err = os.Open(source) 39 | } 40 | if err != nil { 41 | return nil, err 42 | } 43 | defer f.Close() 44 | return profile.Parse(f) 45 | } 46 | 47 | // FetchURL fetches a profile from a URL using HTTP. 48 | func FetchURL(source string, timeout time.Duration) (io.ReadCloser, error) { 49 | resp, err := httpGet(source, timeout) 50 | if err != nil { 51 | return nil, fmt.Errorf("http fetch %s: %v", source, err) 52 | } 53 | if resp.StatusCode != http.StatusOK { 54 | return nil, fmt.Errorf("server response: %s", resp.Status) 55 | } 56 | 57 | return resp.Body, nil 58 | } 59 | 60 | // PostURL issues a POST to a URL over HTTP. 61 | func PostURL(source, post string) ([]byte, error) { 62 | resp, err := http.Post(source, "application/octet-stream", strings.NewReader(post)) 63 | if err != nil { 64 | return nil, fmt.Errorf("http post %s: %v", source, err) 65 | } 66 | if resp.StatusCode != http.StatusOK { 67 | return nil, fmt.Errorf("server response: %s", resp.Status) 68 | } 69 | defer resp.Body.Close() 70 | return ioutil.ReadAll(resp.Body) 71 | } 72 | 73 | // httpGet is a wrapper around http.Get; it is defined as a variable 74 | // so it can be redefined during for testing. 75 | var httpGet = func(url string, timeout time.Duration) (*http.Response, error) { 76 | client := &http.Client{ 77 | Transport: &http.Transport{ 78 | ResponseHeaderTimeout: timeout + 5*time.Second, 79 | }, 80 | } 81 | return client.Get(url) 82 | } 83 | -------------------------------------------------------------------------------- /internal/plugin/plugin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package plugin defines the plugin implementations that the main pprof driver requires. 6 | package plugin 7 | 8 | import ( 9 | "bufio" 10 | "fmt" 11 | "os" 12 | "regexp" 13 | "strings" 14 | "time" 15 | 16 | "github.com/rakyll/gom/internal/profile" 17 | ) 18 | 19 | // A FlagSet creates and parses command-line flags. 20 | // It is similar to the standard flag.FlagSet. 21 | type FlagSet interface { 22 | // Bool, Int, Float64, and String define new flags, 23 | // like the functions of the same name in package flag. 24 | Bool(name string, def bool, usage string) *bool 25 | Int(name string, def int, usage string) *int 26 | Float64(name string, def float64, usage string) *float64 27 | String(name string, def string, usage string) *string 28 | 29 | // ExtraUsage returns any additional text that should be 30 | // printed after the standard usage message. 31 | // The typical use of ExtraUsage is to show any custom flags 32 | // defined by the specific pprof plugins being used. 33 | ExtraUsage() string 34 | 35 | // Parse initializes the flags with their values for this run 36 | // and returns the non-flag command line arguments. 37 | // If an unknown flag is encountered or there are no arguments, 38 | // Parse should call usage and return nil. 39 | Parse(usage func()) []string 40 | } 41 | 42 | // An ObjTool inspects shared libraries and executable files. 43 | type ObjTool interface { 44 | // Open opens the named object file. 45 | // If the object is a shared library, start is the address where 46 | // it is mapped into memory in the address space being inspected. 47 | Open(file string, start uint64) (ObjFile, error) 48 | 49 | // Demangle translates a batch of symbol names from mangled 50 | // form to human-readable form. 51 | Demangle(names []string) (map[string]string, error) 52 | 53 | // Disasm disassembles the named object file, starting at 54 | // the start address and stopping at (before) the end address. 55 | Disasm(file string, start, end uint64) ([]Inst, error) 56 | 57 | // SetConfig configures the tool. 58 | // The implementation defines the meaning of the string 59 | // and can ignore it entirely. 60 | SetConfig(config string) 61 | } 62 | 63 | // NoObjTool returns a trivial implementation of the ObjTool interface. 64 | // Open returns an error indicating that the requested file does not exist. 65 | // Demangle returns an empty map and a nil error. 66 | // Disasm returns an error. 67 | // SetConfig is a no-op. 68 | func NoObjTool() ObjTool { 69 | return noObjTool{} 70 | } 71 | 72 | type noObjTool struct{} 73 | 74 | func (noObjTool) Open(file string, start uint64) (ObjFile, error) { 75 | return nil, &os.PathError{Op: "open", Path: file, Err: os.ErrNotExist} 76 | } 77 | 78 | func (noObjTool) Demangle(name []string) (map[string]string, error) { 79 | return make(map[string]string), nil 80 | } 81 | 82 | func (noObjTool) Disasm(file string, start, end uint64) ([]Inst, error) { 83 | return nil, fmt.Errorf("disassembly not supported") 84 | } 85 | 86 | func (noObjTool) SetConfig(config string) { 87 | } 88 | 89 | // An ObjFile is a single object file: a shared library or executable. 90 | type ObjFile interface { 91 | // Name returns the underlyinf file name, if available 92 | Name() string 93 | 94 | // Base returns the base address to use when looking up symbols in the file. 95 | Base() uint64 96 | 97 | // BuildID returns the GNU build ID of the file, or an empty string. 98 | BuildID() string 99 | 100 | // SourceLine reports the source line information for a given 101 | // address in the file. Due to inlining, the source line information 102 | // is in general a list of positions representing a call stack, 103 | // with the leaf function first. 104 | SourceLine(addr uint64) ([]Frame, error) 105 | 106 | // Symbols returns a list of symbols in the object file. 107 | // If r is not nil, Symbols restricts the list to symbols 108 | // with names matching the regular expression. 109 | // If addr is not zero, Symbols restricts the list to symbols 110 | // containing that address. 111 | Symbols(r *regexp.Regexp, addr uint64) ([]*Sym, error) 112 | 113 | // Close closes the file, releasing associated resources. 114 | Close() error 115 | } 116 | 117 | // A Frame describes a single line in a source file. 118 | type Frame struct { 119 | Func string // name of function 120 | File string // source file name 121 | Line int // line in file 122 | } 123 | 124 | // A Sym describes a single symbol in an object file. 125 | type Sym struct { 126 | Name []string // names of symbol (many if symbol was dedup'ed) 127 | File string // object file containing symbol 128 | Start uint64 // start virtual address 129 | End uint64 // virtual address of last byte in sym (Start+size-1) 130 | } 131 | 132 | // An Inst is a single instruction in an assembly listing. 133 | type Inst struct { 134 | Addr uint64 // virtual address of instruction 135 | Text string // instruction text 136 | File string // source file 137 | Line int // source line 138 | } 139 | 140 | // A UI manages user interactions. 141 | type UI interface { 142 | // Read returns a line of text (a command) read from the user. 143 | ReadLine() (string, error) 144 | 145 | // Print shows a message to the user. 146 | // It formats the text as fmt.Print would and adds a final \n if not already present. 147 | // For line-based UI, Print writes to standard error. 148 | // (Standard output is reserved for report data.) 149 | Print(...interface{}) 150 | 151 | // PrintErr shows an error message to the user. 152 | // It formats the text as fmt.Print would and adds a final \n if not already present. 153 | // For line-based UI, PrintErr writes to standard error. 154 | PrintErr(...interface{}) 155 | 156 | // IsTerminal returns whether the UI is known to be tied to an 157 | // interactive terminal (as opposed to being redirected to a file). 158 | IsTerminal() bool 159 | 160 | // SetAutoComplete instructs the UI to call complete(cmd) to obtain 161 | // the auto-completion of cmd, if the UI supports auto-completion at all. 162 | SetAutoComplete(complete func(string) string) 163 | } 164 | 165 | // StandardUI returns a UI that reads from standard input, 166 | // prints messages to standard output, 167 | // prints errors to standard error, and doesn't use auto-completion. 168 | func StandardUI() UI { 169 | return &stdUI{r: bufio.NewReader(os.Stdin)} 170 | } 171 | 172 | type stdUI struct { 173 | r *bufio.Reader 174 | } 175 | 176 | func (ui *stdUI) ReadLine() (string, error) { 177 | os.Stdout.WriteString("(pprof) ") 178 | return ui.r.ReadString('\n') 179 | } 180 | 181 | func (ui *stdUI) Print(args ...interface{}) { 182 | ui.fprint(os.Stderr, args) 183 | } 184 | 185 | func (ui *stdUI) PrintErr(args ...interface{}) { 186 | ui.fprint(os.Stderr, args) 187 | } 188 | 189 | func (ui *stdUI) IsTerminal() bool { 190 | return false 191 | } 192 | 193 | func (ui *stdUI) SetAutoComplete(func(string) string) { 194 | } 195 | 196 | func (ui *stdUI) fprint(f *os.File, args []interface{}) { 197 | text := fmt.Sprint(args...) 198 | if !strings.HasSuffix(text, "\n") { 199 | text += "\n" 200 | } 201 | f.WriteString(text) 202 | } 203 | 204 | // A Fetcher reads and returns the profile named by src. 205 | // It gives up after the given timeout, unless src contains a timeout override 206 | // (as defined by the implementation). 207 | // It can print messages to ui. 208 | type Fetcher func(src string, timeout time.Duration, ui UI) (*profile.Profile, error) 209 | 210 | // A Symbolizer annotates a profile with symbol information. 211 | // The profile was fetch from src. 212 | // The meaning of mode is defined by the implementation. 213 | type Symbolizer func(mode, src string, prof *profile.Profile, obj ObjTool, ui UI) error 214 | -------------------------------------------------------------------------------- /internal/profile/encode.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package profile 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "sort" 11 | ) 12 | 13 | func (p *Profile) decoder() []decoder { 14 | return profileDecoder 15 | } 16 | 17 | // preEncode populates the unexported fields to be used by encode 18 | // (with suffix X) from the corresponding exported fields. The 19 | // exported fields are cleared up to facilitate testing. 20 | func (p *Profile) preEncode() { 21 | strings := make(map[string]int) 22 | addString(strings, "") 23 | 24 | for _, st := range p.SampleType { 25 | st.typeX = addString(strings, st.Type) 26 | st.unitX = addString(strings, st.Unit) 27 | } 28 | 29 | for _, s := range p.Sample { 30 | s.labelX = nil 31 | var keys []string 32 | for k := range s.Label { 33 | keys = append(keys, k) 34 | } 35 | sort.Strings(keys) 36 | for _, k := range keys { 37 | vs := s.Label[k] 38 | for _, v := range vs { 39 | s.labelX = append(s.labelX, 40 | Label{ 41 | keyX: addString(strings, k), 42 | strX: addString(strings, v), 43 | }, 44 | ) 45 | } 46 | } 47 | var numKeys []string 48 | for k := range s.NumLabel { 49 | numKeys = append(numKeys, k) 50 | } 51 | sort.Strings(numKeys) 52 | for _, k := range numKeys { 53 | vs := s.NumLabel[k] 54 | for _, v := range vs { 55 | s.labelX = append(s.labelX, 56 | Label{ 57 | keyX: addString(strings, k), 58 | numX: v, 59 | }, 60 | ) 61 | } 62 | } 63 | s.locationIDX = nil 64 | for _, l := range s.Location { 65 | s.locationIDX = append(s.locationIDX, l.ID) 66 | } 67 | } 68 | 69 | for _, m := range p.Mapping { 70 | m.fileX = addString(strings, m.File) 71 | m.buildIDX = addString(strings, m.BuildID) 72 | } 73 | 74 | for _, l := range p.Location { 75 | for i, ln := range l.Line { 76 | if ln.Function != nil { 77 | l.Line[i].functionIDX = ln.Function.ID 78 | } else { 79 | l.Line[i].functionIDX = 0 80 | } 81 | } 82 | if l.Mapping != nil { 83 | l.mappingIDX = l.Mapping.ID 84 | } else { 85 | l.mappingIDX = 0 86 | } 87 | } 88 | for _, f := range p.Function { 89 | f.nameX = addString(strings, f.Name) 90 | f.systemNameX = addString(strings, f.SystemName) 91 | f.filenameX = addString(strings, f.Filename) 92 | } 93 | 94 | p.dropFramesX = addString(strings, p.DropFrames) 95 | p.keepFramesX = addString(strings, p.KeepFrames) 96 | 97 | if pt := p.PeriodType; pt != nil { 98 | pt.typeX = addString(strings, pt.Type) 99 | pt.unitX = addString(strings, pt.Unit) 100 | } 101 | 102 | p.stringTable = make([]string, len(strings)) 103 | for s, i := range strings { 104 | p.stringTable[i] = s 105 | } 106 | } 107 | 108 | func (p *Profile) encode(b *buffer) { 109 | for _, x := range p.SampleType { 110 | encodeMessage(b, 1, x) 111 | } 112 | for _, x := range p.Sample { 113 | encodeMessage(b, 2, x) 114 | } 115 | for _, x := range p.Mapping { 116 | encodeMessage(b, 3, x) 117 | } 118 | for _, x := range p.Location { 119 | encodeMessage(b, 4, x) 120 | } 121 | for _, x := range p.Function { 122 | encodeMessage(b, 5, x) 123 | } 124 | encodeStrings(b, 6, p.stringTable) 125 | encodeInt64Opt(b, 7, p.dropFramesX) 126 | encodeInt64Opt(b, 8, p.keepFramesX) 127 | encodeInt64Opt(b, 9, p.TimeNanos) 128 | encodeInt64Opt(b, 10, p.DurationNanos) 129 | if pt := p.PeriodType; pt != nil && (pt.typeX != 0 || pt.unitX != 0) { 130 | encodeMessage(b, 11, p.PeriodType) 131 | } 132 | encodeInt64Opt(b, 12, p.Period) 133 | } 134 | 135 | var profileDecoder = []decoder{ 136 | nil, // 0 137 | // repeated ValueType sample_type = 1 138 | func(b *buffer, m message) error { 139 | x := new(ValueType) 140 | pp := m.(*Profile) 141 | pp.SampleType = append(pp.SampleType, x) 142 | return decodeMessage(b, x) 143 | }, 144 | // repeated Sample sample = 2 145 | func(b *buffer, m message) error { 146 | x := new(Sample) 147 | pp := m.(*Profile) 148 | pp.Sample = append(pp.Sample, x) 149 | return decodeMessage(b, x) 150 | }, 151 | // repeated Mapping mapping = 3 152 | func(b *buffer, m message) error { 153 | x := new(Mapping) 154 | pp := m.(*Profile) 155 | pp.Mapping = append(pp.Mapping, x) 156 | return decodeMessage(b, x) 157 | }, 158 | // repeated Location location = 4 159 | func(b *buffer, m message) error { 160 | x := new(Location) 161 | pp := m.(*Profile) 162 | pp.Location = append(pp.Location, x) 163 | return decodeMessage(b, x) 164 | }, 165 | // repeated Function function = 5 166 | func(b *buffer, m message) error { 167 | x := new(Function) 168 | pp := m.(*Profile) 169 | pp.Function = append(pp.Function, x) 170 | return decodeMessage(b, x) 171 | }, 172 | // repeated string string_table = 6 173 | func(b *buffer, m message) error { 174 | err := decodeStrings(b, &m.(*Profile).stringTable) 175 | if err != nil { 176 | return err 177 | } 178 | if *&m.(*Profile).stringTable[0] != "" { 179 | return errors.New("string_table[0] must be ''") 180 | } 181 | return nil 182 | }, 183 | // repeated int64 drop_frames = 7 184 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Profile).dropFramesX) }, 185 | // repeated int64 keep_frames = 8 186 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Profile).keepFramesX) }, 187 | // repeated int64 time_nanos = 9 188 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Profile).TimeNanos) }, 189 | // repeated int64 duration_nanos = 10 190 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Profile).DurationNanos) }, 191 | // optional string period_type = 11 192 | func(b *buffer, m message) error { 193 | x := new(ValueType) 194 | pp := m.(*Profile) 195 | pp.PeriodType = x 196 | return decodeMessage(b, x) 197 | }, 198 | // repeated int64 period = 12 199 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Profile).Period) }, 200 | } 201 | 202 | // postDecode takes the unexported fields populated by decode (with 203 | // suffix X) and populates the corresponding exported fields. 204 | // The unexported fields are cleared up to facilitate testing. 205 | func (p *Profile) postDecode() error { 206 | var err error 207 | 208 | mappings := make(map[uint64]*Mapping) 209 | for _, m := range p.Mapping { 210 | m.File, err = getString(p.stringTable, &m.fileX, err) 211 | m.BuildID, err = getString(p.stringTable, &m.buildIDX, err) 212 | mappings[m.ID] = m 213 | } 214 | 215 | functions := make(map[uint64]*Function) 216 | for _, f := range p.Function { 217 | f.Name, err = getString(p.stringTable, &f.nameX, err) 218 | f.SystemName, err = getString(p.stringTable, &f.systemNameX, err) 219 | f.Filename, err = getString(p.stringTable, &f.filenameX, err) 220 | functions[f.ID] = f 221 | } 222 | 223 | locations := make(map[uint64]*Location) 224 | for _, l := range p.Location { 225 | l.Mapping = mappings[l.mappingIDX] 226 | l.mappingIDX = 0 227 | for i, ln := range l.Line { 228 | if id := ln.functionIDX; id != 0 { 229 | l.Line[i].Function = functions[id] 230 | if l.Line[i].Function == nil { 231 | return fmt.Errorf("Function ID %d not found", id) 232 | } 233 | l.Line[i].functionIDX = 0 234 | } 235 | } 236 | locations[l.ID] = l 237 | } 238 | 239 | for _, st := range p.SampleType { 240 | st.Type, err = getString(p.stringTable, &st.typeX, err) 241 | st.Unit, err = getString(p.stringTable, &st.unitX, err) 242 | } 243 | 244 | for _, s := range p.Sample { 245 | labels := make(map[string][]string) 246 | numLabels := make(map[string][]int64) 247 | for _, l := range s.labelX { 248 | var key, value string 249 | key, err = getString(p.stringTable, &l.keyX, err) 250 | if l.strX != 0 { 251 | value, err = getString(p.stringTable, &l.strX, err) 252 | labels[key] = append(labels[key], value) 253 | } else { 254 | numLabels[key] = append(numLabels[key], l.numX) 255 | } 256 | } 257 | if len(labels) > 0 { 258 | s.Label = labels 259 | } 260 | if len(numLabels) > 0 { 261 | s.NumLabel = numLabels 262 | } 263 | s.Location = nil 264 | for _, lid := range s.locationIDX { 265 | s.Location = append(s.Location, locations[lid]) 266 | } 267 | s.locationIDX = nil 268 | } 269 | 270 | p.DropFrames, err = getString(p.stringTable, &p.dropFramesX, err) 271 | p.KeepFrames, err = getString(p.stringTable, &p.keepFramesX, err) 272 | 273 | if pt := p.PeriodType; pt == nil { 274 | p.PeriodType = &ValueType{} 275 | } 276 | 277 | if pt := p.PeriodType; pt != nil { 278 | pt.Type, err = getString(p.stringTable, &pt.typeX, err) 279 | pt.Unit, err = getString(p.stringTable, &pt.unitX, err) 280 | } 281 | p.stringTable = nil 282 | return nil 283 | } 284 | 285 | func (p *ValueType) decoder() []decoder { 286 | return valueTypeDecoder 287 | } 288 | 289 | func (p *ValueType) encode(b *buffer) { 290 | encodeInt64Opt(b, 1, p.typeX) 291 | encodeInt64Opt(b, 2, p.unitX) 292 | } 293 | 294 | var valueTypeDecoder = []decoder{ 295 | nil, // 0 296 | // optional int64 type = 1 297 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*ValueType).typeX) }, 298 | // optional int64 unit = 2 299 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*ValueType).unitX) }, 300 | } 301 | 302 | func (p *Sample) decoder() []decoder { 303 | return sampleDecoder 304 | } 305 | 306 | func (p *Sample) encode(b *buffer) { 307 | encodeUint64s(b, 1, p.locationIDX) 308 | for _, x := range p.Value { 309 | encodeInt64(b, 2, x) 310 | } 311 | for _, x := range p.labelX { 312 | encodeMessage(b, 3, x) 313 | } 314 | } 315 | 316 | var sampleDecoder = []decoder{ 317 | nil, // 0 318 | // repeated uint64 location = 1 319 | func(b *buffer, m message) error { return decodeUint64s(b, &m.(*Sample).locationIDX) }, 320 | // repeated int64 value = 2 321 | func(b *buffer, m message) error { return decodeInt64s(b, &m.(*Sample).Value) }, 322 | // repeated Label label = 3 323 | func(b *buffer, m message) error { 324 | s := m.(*Sample) 325 | n := len(s.labelX) 326 | s.labelX = append(s.labelX, Label{}) 327 | return decodeMessage(b, &s.labelX[n]) 328 | }, 329 | } 330 | 331 | func (p Label) decoder() []decoder { 332 | return labelDecoder 333 | } 334 | 335 | func (p Label) encode(b *buffer) { 336 | encodeInt64Opt(b, 1, p.keyX) 337 | encodeInt64Opt(b, 2, p.strX) 338 | encodeInt64Opt(b, 3, p.numX) 339 | } 340 | 341 | var labelDecoder = []decoder{ 342 | nil, // 0 343 | // optional int64 key = 1 344 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Label).keyX) }, 345 | // optional int64 str = 2 346 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Label).strX) }, 347 | // optional int64 num = 3 348 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Label).numX) }, 349 | } 350 | 351 | func (p *Mapping) decoder() []decoder { 352 | return mappingDecoder 353 | } 354 | 355 | func (p *Mapping) encode(b *buffer) { 356 | encodeUint64Opt(b, 1, p.ID) 357 | encodeUint64Opt(b, 2, p.Start) 358 | encodeUint64Opt(b, 3, p.Limit) 359 | encodeUint64Opt(b, 4, p.Offset) 360 | encodeInt64Opt(b, 5, p.fileX) 361 | encodeInt64Opt(b, 6, p.buildIDX) 362 | encodeBoolOpt(b, 7, p.HasFunctions) 363 | encodeBoolOpt(b, 8, p.HasFilenames) 364 | encodeBoolOpt(b, 9, p.HasLineNumbers) 365 | encodeBoolOpt(b, 10, p.HasInlineFrames) 366 | } 367 | 368 | var mappingDecoder = []decoder{ 369 | nil, // 0 370 | func(b *buffer, m message) error { return decodeUint64(b, &m.(*Mapping).ID) }, // optional uint64 id = 1 371 | func(b *buffer, m message) error { return decodeUint64(b, &m.(*Mapping).Start) }, // optional uint64 memory_offset = 2 372 | func(b *buffer, m message) error { return decodeUint64(b, &m.(*Mapping).Limit) }, // optional uint64 memory_limit = 3 373 | func(b *buffer, m message) error { return decodeUint64(b, &m.(*Mapping).Offset) }, // optional uint64 file_offset = 4 374 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Mapping).fileX) }, // optional int64 filename = 5 375 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Mapping).buildIDX) }, // optional int64 build_id = 6 376 | func(b *buffer, m message) error { return decodeBool(b, &m.(*Mapping).HasFunctions) }, // optional bool has_functions = 7 377 | func(b *buffer, m message) error { return decodeBool(b, &m.(*Mapping).HasFilenames) }, // optional bool has_filenames = 8 378 | func(b *buffer, m message) error { return decodeBool(b, &m.(*Mapping).HasLineNumbers) }, // optional bool has_line_numbers = 9 379 | func(b *buffer, m message) error { return decodeBool(b, &m.(*Mapping).HasInlineFrames) }, // optional bool has_inline_frames = 10 380 | } 381 | 382 | func (p *Location) decoder() []decoder { 383 | return locationDecoder 384 | } 385 | 386 | func (p *Location) encode(b *buffer) { 387 | encodeUint64Opt(b, 1, p.ID) 388 | encodeUint64Opt(b, 2, p.mappingIDX) 389 | encodeUint64Opt(b, 3, p.Address) 390 | for i := range p.Line { 391 | encodeMessage(b, 4, &p.Line[i]) 392 | } 393 | } 394 | 395 | var locationDecoder = []decoder{ 396 | nil, // 0 397 | func(b *buffer, m message) error { return decodeUint64(b, &m.(*Location).ID) }, // optional uint64 id = 1; 398 | func(b *buffer, m message) error { return decodeUint64(b, &m.(*Location).mappingIDX) }, // optional uint64 mapping_id = 2; 399 | func(b *buffer, m message) error { return decodeUint64(b, &m.(*Location).Address) }, // optional uint64 address = 3; 400 | func(b *buffer, m message) error { // repeated Line line = 4 401 | pp := m.(*Location) 402 | n := len(pp.Line) 403 | pp.Line = append(pp.Line, Line{}) 404 | return decodeMessage(b, &pp.Line[n]) 405 | }, 406 | } 407 | 408 | func (p *Line) decoder() []decoder { 409 | return lineDecoder 410 | } 411 | 412 | func (p *Line) encode(b *buffer) { 413 | encodeUint64Opt(b, 1, p.functionIDX) 414 | encodeInt64Opt(b, 2, p.Line) 415 | } 416 | 417 | var lineDecoder = []decoder{ 418 | nil, // 0 419 | // optional uint64 function_id = 1 420 | func(b *buffer, m message) error { return decodeUint64(b, &m.(*Line).functionIDX) }, 421 | // optional int64 line = 2 422 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Line).Line) }, 423 | } 424 | 425 | func (p *Function) decoder() []decoder { 426 | return functionDecoder 427 | } 428 | 429 | func (p *Function) encode(b *buffer) { 430 | encodeUint64Opt(b, 1, p.ID) 431 | encodeInt64Opt(b, 2, p.nameX) 432 | encodeInt64Opt(b, 3, p.systemNameX) 433 | encodeInt64Opt(b, 4, p.filenameX) 434 | encodeInt64Opt(b, 5, p.StartLine) 435 | } 436 | 437 | var functionDecoder = []decoder{ 438 | nil, // 0 439 | // optional uint64 id = 1 440 | func(b *buffer, m message) error { return decodeUint64(b, &m.(*Function).ID) }, 441 | // optional int64 function_name = 2 442 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Function).nameX) }, 443 | // optional int64 function_system_name = 3 444 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Function).systemNameX) }, 445 | // repeated int64 filename = 4 446 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Function).filenameX) }, 447 | // optional int64 start_line = 5 448 | func(b *buffer, m message) error { return decodeInt64(b, &m.(*Function).StartLine) }, 449 | } 450 | 451 | func addString(strings map[string]int, s string) int64 { 452 | i, ok := strings[s] 453 | if !ok { 454 | i = len(strings) 455 | strings[s] = i 456 | } 457 | return int64(i) 458 | } 459 | 460 | func getString(strings []string, strng *int64, err error) (string, error) { 461 | if err != nil { 462 | return "", err 463 | } 464 | s := int(*strng) 465 | if s < 0 || s >= len(strings) { 466 | return "", errMalformed 467 | } 468 | *strng = 0 469 | return strings[s], nil 470 | } 471 | -------------------------------------------------------------------------------- /internal/profile/filter.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Implements methods to filter samples from profiles. 6 | 7 | package profile 8 | 9 | import "regexp" 10 | 11 | // FilterSamplesByName filters the samples in a profile and only keeps 12 | // samples where at least one frame matches focus but none match ignore. 13 | // Returns true is the corresponding regexp matched at least one sample. 14 | func (p *Profile) FilterSamplesByName(focus, ignore, hide *regexp.Regexp) (fm, im, hm bool) { 15 | focusOrIgnore := make(map[uint64]bool) 16 | hidden := make(map[uint64]bool) 17 | for _, l := range p.Location { 18 | if ignore != nil && l.matchesName(ignore) { 19 | im = true 20 | focusOrIgnore[l.ID] = false 21 | } else if focus == nil || l.matchesName(focus) { 22 | fm = true 23 | focusOrIgnore[l.ID] = true 24 | } 25 | if hide != nil && l.matchesName(hide) { 26 | hm = true 27 | l.Line = l.unmatchedLines(hide) 28 | if len(l.Line) == 0 { 29 | hidden[l.ID] = true 30 | } 31 | } 32 | } 33 | 34 | s := make([]*Sample, 0, len(p.Sample)) 35 | for _, sample := range p.Sample { 36 | if focusedAndNotIgnored(sample.Location, focusOrIgnore) { 37 | if len(hidden) > 0 { 38 | var locs []*Location 39 | for _, loc := range sample.Location { 40 | if !hidden[loc.ID] { 41 | locs = append(locs, loc) 42 | } 43 | } 44 | if len(locs) == 0 { 45 | // Remove sample with no locations (by not adding it to s). 46 | continue 47 | } 48 | sample.Location = locs 49 | } 50 | s = append(s, sample) 51 | } 52 | } 53 | p.Sample = s 54 | 55 | return 56 | } 57 | 58 | // matchesName returns whether the function name or file in the 59 | // location matches the regular expression. 60 | func (loc *Location) matchesName(re *regexp.Regexp) bool { 61 | for _, ln := range loc.Line { 62 | if fn := ln.Function; fn != nil { 63 | if re.MatchString(fn.Name) { 64 | return true 65 | } 66 | if re.MatchString(fn.Filename) { 67 | return true 68 | } 69 | } 70 | } 71 | return false 72 | } 73 | 74 | // unmatchedLines returns the lines in the location that do not match 75 | // the regular expression. 76 | func (loc *Location) unmatchedLines(re *regexp.Regexp) []Line { 77 | var lines []Line 78 | for _, ln := range loc.Line { 79 | if fn := ln.Function; fn != nil { 80 | if re.MatchString(fn.Name) { 81 | continue 82 | } 83 | if re.MatchString(fn.Filename) { 84 | continue 85 | } 86 | } 87 | lines = append(lines, ln) 88 | } 89 | return lines 90 | } 91 | 92 | // focusedAndNotIgnored looks up a slice of ids against a map of 93 | // focused/ignored locations. The map only contains locations that are 94 | // explicitly focused or ignored. Returns whether there is at least 95 | // one focused location but no ignored locations. 96 | func focusedAndNotIgnored(locs []*Location, m map[uint64]bool) bool { 97 | var f bool 98 | for _, loc := range locs { 99 | if focus, focusOrIgnore := m[loc.ID]; focusOrIgnore { 100 | if focus { 101 | // Found focused location. Must keep searching in case there 102 | // is an ignored one as well. 103 | f = true 104 | } else { 105 | // Found ignored location. Can return false right away. 106 | return false 107 | } 108 | } 109 | } 110 | return f 111 | } 112 | 113 | // TagMatch selects tags for filtering 114 | type TagMatch func(key, val string, nval int64) bool 115 | 116 | // FilterSamplesByTag removes all samples from the profile, except 117 | // those that match focus and do not match the ignore regular 118 | // expression. 119 | func (p *Profile) FilterSamplesByTag(focus, ignore TagMatch) (fm, im bool) { 120 | samples := make([]*Sample, 0, len(p.Sample)) 121 | for _, s := range p.Sample { 122 | focused, ignored := focusedSample(s, focus, ignore) 123 | fm = fm || focused 124 | im = im || ignored 125 | if focused && !ignored { 126 | samples = append(samples, s) 127 | } 128 | } 129 | p.Sample = samples 130 | return 131 | } 132 | 133 | // focusedTag checks a sample against focus and ignore regexps. 134 | // Returns whether the focus/ignore regexps match any tags 135 | func focusedSample(s *Sample, focus, ignore TagMatch) (fm, im bool) { 136 | fm = focus == nil 137 | for key, vals := range s.Label { 138 | for _, val := range vals { 139 | if ignore != nil && ignore(key, val, 0) { 140 | im = true 141 | } 142 | if !fm && focus(key, val, 0) { 143 | fm = true 144 | } 145 | } 146 | } 147 | for key, vals := range s.NumLabel { 148 | for _, val := range vals { 149 | if ignore != nil && ignore(key, "", val) { 150 | im = true 151 | } 152 | if !fm && focus(key, "", val) { 153 | fm = true 154 | } 155 | } 156 | } 157 | return fm, im 158 | } 159 | -------------------------------------------------------------------------------- /internal/profile/legacy_profile.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // This file implements parsers to convert legacy profiles into the 6 | // profile.proto format. 7 | 8 | package profile 9 | 10 | import ( 11 | "bufio" 12 | "bytes" 13 | "fmt" 14 | "io" 15 | "math" 16 | "regexp" 17 | "strconv" 18 | "strings" 19 | ) 20 | 21 | var ( 22 | countStartRE = regexp.MustCompile(`\A(\w+) profile: total \d+\n\z`) 23 | countRE = regexp.MustCompile(`\A(\d+) @(( 0x[0-9a-f]+)+)\n\z`) 24 | 25 | heapHeaderRE = regexp.MustCompile(`heap profile: *(\d+): *(\d+) *\[ *(\d+): *(\d+) *\] *@ *(heap[_a-z0-9]*)/?(\d*)`) 26 | heapSampleRE = regexp.MustCompile(`(-?\d+): *(-?\d+) *\[ *(\d+): *(\d+) *] @([ x0-9a-f]*)`) 27 | 28 | contentionSampleRE = regexp.MustCompile(`(\d+) *(\d+) @([ x0-9a-f]*)`) 29 | 30 | hexNumberRE = regexp.MustCompile(`0x[0-9a-f]+`) 31 | 32 | growthHeaderRE = regexp.MustCompile(`heap profile: *(\d+): *(\d+) *\[ *(\d+): *(\d+) *\] @ growthz`) 33 | 34 | fragmentationHeaderRE = regexp.MustCompile(`heap profile: *(\d+): *(\d+) *\[ *(\d+): *(\d+) *\] @ fragmentationz`) 35 | 36 | threadzStartRE = regexp.MustCompile(`--- threadz \d+ ---`) 37 | threadStartRE = regexp.MustCompile(`--- Thread ([[:xdigit:]]+) \(name: (.*)/(\d+)\) stack: ---`) 38 | 39 | procMapsRE = regexp.MustCompile(`([[:xdigit:]]+)-([[:xdigit:]]+)\s+([-rwxp]+)\s+([[:xdigit:]]+)\s+([[:xdigit:]]+):([[:xdigit:]]+)\s+([[:digit:]]+)\s*(\S+)?`) 40 | 41 | briefMapsRE = regexp.MustCompile(`\s*([[:xdigit:]]+)-([[:xdigit:]]+):\s*(\S+)(\s.*@)?([[:xdigit:]]+)?`) 42 | 43 | // LegacyHeapAllocated instructs the heapz parsers to use the 44 | // allocated memory stats instead of the default in-use memory. Note 45 | // that tcmalloc doesn't provide all allocated memory, only in-use 46 | // stats. 47 | LegacyHeapAllocated bool 48 | ) 49 | 50 | func isSpaceOrComment(line string) bool { 51 | trimmed := strings.TrimSpace(line) 52 | return len(trimmed) == 0 || trimmed[0] == '#' 53 | } 54 | 55 | // parseGoCount parses a Go count profile (e.g., threadcreate or 56 | // goroutine) and returns a new Profile. 57 | func parseGoCount(b []byte) (*Profile, error) { 58 | r := bytes.NewBuffer(b) 59 | 60 | var line string 61 | var err error 62 | for { 63 | // Skip past comments and empty lines seeking a real header. 64 | line, err = r.ReadString('\n') 65 | if err != nil { 66 | return nil, err 67 | } 68 | if !isSpaceOrComment(line) { 69 | break 70 | } 71 | } 72 | 73 | m := countStartRE.FindStringSubmatch(line) 74 | if m == nil { 75 | return nil, errUnrecognized 76 | } 77 | profileType := string(m[1]) 78 | p := &Profile{ 79 | PeriodType: &ValueType{Type: profileType, Unit: "count"}, 80 | Period: 1, 81 | SampleType: []*ValueType{{Type: profileType, Unit: "count"}}, 82 | } 83 | locations := make(map[uint64]*Location) 84 | for { 85 | line, err = r.ReadString('\n') 86 | if err != nil { 87 | if err == io.EOF { 88 | break 89 | } 90 | return nil, err 91 | } 92 | if isSpaceOrComment(line) { 93 | continue 94 | } 95 | if strings.HasPrefix(line, "---") { 96 | break 97 | } 98 | m := countRE.FindStringSubmatch(line) 99 | if m == nil { 100 | return nil, errMalformed 101 | } 102 | n, err := strconv.ParseInt(string(m[1]), 0, 64) 103 | if err != nil { 104 | return nil, errMalformed 105 | } 106 | fields := strings.Fields(string(m[2])) 107 | locs := make([]*Location, 0, len(fields)) 108 | for _, stk := range fields { 109 | addr, err := strconv.ParseUint(stk, 0, 64) 110 | if err != nil { 111 | return nil, errMalformed 112 | } 113 | // Adjust all frames by -1 (except the leaf) to land on top of 114 | // the call instruction. 115 | if len(locs) > 0 { 116 | addr-- 117 | } 118 | loc := locations[addr] 119 | if loc == nil { 120 | loc = &Location{ 121 | Address: addr, 122 | } 123 | locations[addr] = loc 124 | p.Location = append(p.Location, loc) 125 | } 126 | locs = append(locs, loc) 127 | } 128 | p.Sample = append(p.Sample, &Sample{ 129 | Location: locs, 130 | Value: []int64{n}, 131 | }) 132 | } 133 | 134 | if err = parseAdditionalSections(strings.TrimSpace(line), r, p); err != nil { 135 | return nil, err 136 | } 137 | return p, nil 138 | } 139 | 140 | // remapLocationIDs ensures there is a location for each address 141 | // referenced by a sample, and remaps the samples to point to the new 142 | // location ids. 143 | func (p *Profile) remapLocationIDs() { 144 | seen := make(map[*Location]bool, len(p.Location)) 145 | var locs []*Location 146 | 147 | for _, s := range p.Sample { 148 | for _, l := range s.Location { 149 | if seen[l] { 150 | continue 151 | } 152 | l.ID = uint64(len(locs) + 1) 153 | locs = append(locs, l) 154 | seen[l] = true 155 | } 156 | } 157 | p.Location = locs 158 | } 159 | 160 | func (p *Profile) remapFunctionIDs() { 161 | seen := make(map[*Function]bool, len(p.Function)) 162 | var fns []*Function 163 | 164 | for _, l := range p.Location { 165 | for _, ln := range l.Line { 166 | fn := ln.Function 167 | if fn == nil || seen[fn] { 168 | continue 169 | } 170 | fn.ID = uint64(len(fns) + 1) 171 | fns = append(fns, fn) 172 | seen[fn] = true 173 | } 174 | } 175 | p.Function = fns 176 | } 177 | 178 | // remapMappingIDs matches location addresses with existing mappings 179 | // and updates them appropriately. This is O(N*M), if this ever shows 180 | // up as a bottleneck, evaluate sorting the mappings and doing a 181 | // binary search, which would make it O(N*log(M)). 182 | func (p *Profile) remapMappingIDs() { 183 | if len(p.Mapping) == 0 { 184 | return 185 | } 186 | 187 | // Some profile handlers will incorrectly set regions for the main 188 | // executable if its section is remapped. Fix them through heuristics. 189 | 190 | // Remove the initial mapping if named '/anon_hugepage' and has a 191 | // consecutive adjacent mapping. 192 | if m := p.Mapping[0]; strings.HasPrefix(m.File, "/anon_hugepage") { 193 | if len(p.Mapping) > 1 && m.Limit == p.Mapping[1].Start { 194 | p.Mapping = p.Mapping[1:] 195 | } 196 | } 197 | 198 | // Subtract the offset from the start of the main mapping if it 199 | // ends up at a recognizable start address. 200 | const expectedStart = 0x400000 201 | if m := p.Mapping[0]; m.Start-m.Offset == expectedStart { 202 | m.Start = expectedStart 203 | m.Offset = 0 204 | } 205 | 206 | for _, l := range p.Location { 207 | if a := l.Address; a != 0 { 208 | for _, m := range p.Mapping { 209 | if m.Start <= a && a < m.Limit { 210 | l.Mapping = m 211 | break 212 | } 213 | } 214 | } 215 | } 216 | 217 | // Reset all mapping IDs. 218 | for i, m := range p.Mapping { 219 | m.ID = uint64(i + 1) 220 | } 221 | } 222 | 223 | var cpuInts = []func([]byte) (uint64, []byte){ 224 | get32l, 225 | get32b, 226 | get64l, 227 | get64b, 228 | } 229 | 230 | func get32l(b []byte) (uint64, []byte) { 231 | if len(b) < 4 { 232 | return 0, nil 233 | } 234 | return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24, b[4:] 235 | } 236 | 237 | func get32b(b []byte) (uint64, []byte) { 238 | if len(b) < 4 { 239 | return 0, nil 240 | } 241 | return uint64(b[3]) | uint64(b[2])<<8 | uint64(b[1])<<16 | uint64(b[0])<<24, b[4:] 242 | } 243 | 244 | func get64l(b []byte) (uint64, []byte) { 245 | if len(b) < 8 { 246 | return 0, nil 247 | } 248 | return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 | uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56, b[8:] 249 | } 250 | 251 | func get64b(b []byte) (uint64, []byte) { 252 | if len(b) < 8 { 253 | return 0, nil 254 | } 255 | return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 | uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56, b[8:] 256 | } 257 | 258 | // ParseTracebacks parses a set of tracebacks and returns a newly 259 | // populated profile. It will accept any text file and generate a 260 | // Profile out of it with any hex addresses it can identify, including 261 | // a process map if it can recognize one. Each sample will include a 262 | // tag "source" with the addresses recognized in string format. 263 | func ParseTracebacks(b []byte) (*Profile, error) { 264 | r := bytes.NewBuffer(b) 265 | 266 | p := &Profile{ 267 | PeriodType: &ValueType{Type: "trace", Unit: "count"}, 268 | Period: 1, 269 | SampleType: []*ValueType{ 270 | {Type: "trace", Unit: "count"}, 271 | }, 272 | } 273 | 274 | var sources []string 275 | var sloc []*Location 276 | 277 | locs := make(map[uint64]*Location) 278 | for { 279 | l, err := r.ReadString('\n') 280 | if err != nil { 281 | if err != io.EOF { 282 | return nil, err 283 | } 284 | if l == "" { 285 | break 286 | } 287 | } 288 | if sectionTrigger(l) == memoryMapSection { 289 | break 290 | } 291 | if s, addrs := extractHexAddresses(l); len(s) > 0 { 292 | for _, addr := range addrs { 293 | // Addresses from stack traces point to the next instruction after 294 | // each call. Adjust by -1 to land somewhere on the actual call 295 | // (except for the leaf, which is not a call). 296 | if len(sloc) > 0 { 297 | addr-- 298 | } 299 | loc := locs[addr] 300 | if locs[addr] == nil { 301 | loc = &Location{ 302 | Address: addr, 303 | } 304 | p.Location = append(p.Location, loc) 305 | locs[addr] = loc 306 | } 307 | sloc = append(sloc, loc) 308 | } 309 | 310 | sources = append(sources, s...) 311 | } else { 312 | if len(sources) > 0 || len(sloc) > 0 { 313 | addTracebackSample(sloc, sources, p) 314 | sloc, sources = nil, nil 315 | } 316 | } 317 | } 318 | 319 | // Add final sample to save any leftover data. 320 | if len(sources) > 0 || len(sloc) > 0 { 321 | addTracebackSample(sloc, sources, p) 322 | } 323 | 324 | if err := p.ParseMemoryMap(r); err != nil { 325 | return nil, err 326 | } 327 | return p, nil 328 | } 329 | 330 | func addTracebackSample(l []*Location, s []string, p *Profile) { 331 | p.Sample = append(p.Sample, 332 | &Sample{ 333 | Value: []int64{1}, 334 | Location: l, 335 | Label: map[string][]string{"source": s}, 336 | }) 337 | } 338 | 339 | // parseCPU parses a profilez legacy profile and returns a newly 340 | // populated Profile. 341 | // 342 | // The general format for profilez samples is a sequence of words in 343 | // binary format. The first words are a header with the following data: 344 | // 1st word -- 0 345 | // 2nd word -- 3 346 | // 3rd word -- 0 if a c++ application, 1 if a java application. 347 | // 4th word -- Sampling period (in microseconds). 348 | // 5th word -- Padding. 349 | func parseCPU(b []byte) (*Profile, error) { 350 | var parse func([]byte) (uint64, []byte) 351 | var n1, n2, n3, n4, n5 uint64 352 | for _, parse = range cpuInts { 353 | var tmp []byte 354 | n1, tmp = parse(b) 355 | n2, tmp = parse(tmp) 356 | n3, tmp = parse(tmp) 357 | n4, tmp = parse(tmp) 358 | n5, tmp = parse(tmp) 359 | 360 | if tmp != nil && n1 == 0 && n2 == 3 && n3 == 0 && n4 > 0 && n5 == 0 { 361 | b = tmp 362 | return cpuProfile(b, int64(n4), parse) 363 | } 364 | } 365 | return nil, errUnrecognized 366 | } 367 | 368 | // cpuProfile returns a new Profile from C++ profilez data. 369 | // b is the profile bytes after the header, period is the profiling 370 | // period, and parse is a function to parse 8-byte chunks from the 371 | // profile in its native endianness. 372 | func cpuProfile(b []byte, period int64, parse func(b []byte) (uint64, []byte)) (*Profile, error) { 373 | p := &Profile{ 374 | Period: period * 1000, 375 | PeriodType: &ValueType{Type: "cpu", Unit: "nanoseconds"}, 376 | SampleType: []*ValueType{ 377 | {Type: "samples", Unit: "count"}, 378 | {Type: "cpu", Unit: "nanoseconds"}, 379 | }, 380 | } 381 | var err error 382 | if b, _, err = parseCPUSamples(b, parse, true, p); err != nil { 383 | return nil, err 384 | } 385 | 386 | // If all samples have the same second-to-the-bottom frame, it 387 | // strongly suggests that it is an uninteresting artifact of 388 | // measurement -- a stack frame pushed by the signal handler. The 389 | // bottom frame is always correct as it is picked up from the signal 390 | // structure, not the stack. Check if this is the case and if so, 391 | // remove. 392 | if len(p.Sample) > 1 && len(p.Sample[0].Location) > 1 { 393 | allSame := true 394 | id1 := p.Sample[0].Location[1].Address 395 | for _, s := range p.Sample { 396 | if len(s.Location) < 2 || id1 != s.Location[1].Address { 397 | allSame = false 398 | break 399 | } 400 | } 401 | if allSame { 402 | for _, s := range p.Sample { 403 | s.Location = append(s.Location[:1], s.Location[2:]...) 404 | } 405 | } 406 | } 407 | 408 | if err := p.ParseMemoryMap(bytes.NewBuffer(b)); err != nil { 409 | return nil, err 410 | } 411 | return p, nil 412 | } 413 | 414 | // parseCPUSamples parses a collection of profilez samples from a 415 | // profile. 416 | // 417 | // profilez samples are a repeated sequence of stack frames of the 418 | // form: 419 | // 1st word -- The number of times this stack was encountered. 420 | // 2nd word -- The size of the stack (StackSize). 421 | // 3rd word -- The first address on the stack. 422 | // ... 423 | // StackSize + 2 -- The last address on the stack 424 | // The last stack trace is of the form: 425 | // 1st word -- 0 426 | // 2nd word -- 1 427 | // 3rd word -- 0 428 | // 429 | // Addresses from stack traces may point to the next instruction after 430 | // each call. Optionally adjust by -1 to land somewhere on the actual 431 | // call (except for the leaf, which is not a call). 432 | func parseCPUSamples(b []byte, parse func(b []byte) (uint64, []byte), adjust bool, p *Profile) ([]byte, map[uint64]*Location, error) { 433 | locs := make(map[uint64]*Location) 434 | for len(b) > 0 { 435 | var count, nstk uint64 436 | count, b = parse(b) 437 | nstk, b = parse(b) 438 | if b == nil || nstk > uint64(len(b)/4) { 439 | return nil, nil, errUnrecognized 440 | } 441 | var sloc []*Location 442 | addrs := make([]uint64, nstk) 443 | for i := 0; i < int(nstk); i++ { 444 | addrs[i], b = parse(b) 445 | } 446 | 447 | if count == 0 && nstk == 1 && addrs[0] == 0 { 448 | // End of data marker 449 | break 450 | } 451 | for i, addr := range addrs { 452 | if adjust && i > 0 { 453 | addr-- 454 | } 455 | loc := locs[addr] 456 | if loc == nil { 457 | loc = &Location{ 458 | Address: addr, 459 | } 460 | locs[addr] = loc 461 | p.Location = append(p.Location, loc) 462 | } 463 | sloc = append(sloc, loc) 464 | } 465 | p.Sample = append(p.Sample, 466 | &Sample{ 467 | Value: []int64{int64(count), int64(count) * int64(p.Period)}, 468 | Location: sloc, 469 | }) 470 | } 471 | // Reached the end without finding the EOD marker. 472 | return b, locs, nil 473 | } 474 | 475 | // parseHeap parses a heapz legacy or a growthz profile and 476 | // returns a newly populated Profile. 477 | func parseHeap(b []byte) (p *Profile, err error) { 478 | r := bytes.NewBuffer(b) 479 | l, err := r.ReadString('\n') 480 | if err != nil { 481 | return nil, errUnrecognized 482 | } 483 | 484 | sampling := "" 485 | 486 | if header := heapHeaderRE.FindStringSubmatch(l); header != nil { 487 | p = &Profile{ 488 | SampleType: []*ValueType{ 489 | {Type: "objects", Unit: "count"}, 490 | {Type: "space", Unit: "bytes"}, 491 | }, 492 | PeriodType: &ValueType{Type: "objects", Unit: "bytes"}, 493 | } 494 | 495 | var period int64 496 | if len(header[6]) > 0 { 497 | if period, err = strconv.ParseInt(string(header[6]), 10, 64); err != nil { 498 | return nil, errUnrecognized 499 | } 500 | } 501 | 502 | switch header[5] { 503 | case "heapz_v2", "heap_v2": 504 | sampling, p.Period = "v2", period 505 | case "heapprofile": 506 | sampling, p.Period = "", 1 507 | case "heap": 508 | sampling, p.Period = "v2", period/2 509 | default: 510 | return nil, errUnrecognized 511 | } 512 | } else if header = growthHeaderRE.FindStringSubmatch(l); header != nil { 513 | p = &Profile{ 514 | SampleType: []*ValueType{ 515 | {Type: "objects", Unit: "count"}, 516 | {Type: "space", Unit: "bytes"}, 517 | }, 518 | PeriodType: &ValueType{Type: "heapgrowth", Unit: "count"}, 519 | Period: 1, 520 | } 521 | } else if header = fragmentationHeaderRE.FindStringSubmatch(l); header != nil { 522 | p = &Profile{ 523 | SampleType: []*ValueType{ 524 | {Type: "objects", Unit: "count"}, 525 | {Type: "space", Unit: "bytes"}, 526 | }, 527 | PeriodType: &ValueType{Type: "allocations", Unit: "count"}, 528 | Period: 1, 529 | } 530 | } else { 531 | return nil, errUnrecognized 532 | } 533 | 534 | if LegacyHeapAllocated { 535 | for _, st := range p.SampleType { 536 | st.Type = "alloc_" + st.Type 537 | } 538 | } else { 539 | for _, st := range p.SampleType { 540 | st.Type = "inuse_" + st.Type 541 | } 542 | } 543 | 544 | locs := make(map[uint64]*Location) 545 | for { 546 | l, err = r.ReadString('\n') 547 | if err != nil { 548 | if err != io.EOF { 549 | return nil, err 550 | } 551 | 552 | if l == "" { 553 | break 554 | } 555 | } 556 | 557 | if isSpaceOrComment(l) { 558 | continue 559 | } 560 | l = strings.TrimSpace(l) 561 | 562 | if sectionTrigger(l) != unrecognizedSection { 563 | break 564 | } 565 | 566 | value, blocksize, addrs, err := parseHeapSample(l, p.Period, sampling) 567 | if err != nil { 568 | return nil, err 569 | } 570 | var sloc []*Location 571 | for i, addr := range addrs { 572 | // Addresses from stack traces point to the next instruction after 573 | // each call. Adjust by -1 to land somewhere on the actual call 574 | // (except for the leaf, which is not a call). 575 | if i > 0 { 576 | addr-- 577 | } 578 | loc := locs[addr] 579 | if locs[addr] == nil { 580 | loc = &Location{ 581 | Address: addr, 582 | } 583 | p.Location = append(p.Location, loc) 584 | locs[addr] = loc 585 | } 586 | sloc = append(sloc, loc) 587 | } 588 | 589 | p.Sample = append(p.Sample, &Sample{ 590 | Value: value, 591 | Location: sloc, 592 | NumLabel: map[string][]int64{"bytes": {blocksize}}, 593 | }) 594 | } 595 | 596 | if err = parseAdditionalSections(l, r, p); err != nil { 597 | return nil, err 598 | } 599 | return p, nil 600 | } 601 | 602 | // parseHeapSample parses a single row from a heap profile into a new Sample. 603 | func parseHeapSample(line string, rate int64, sampling string) (value []int64, blocksize int64, addrs []uint64, err error) { 604 | sampleData := heapSampleRE.FindStringSubmatch(line) 605 | if len(sampleData) != 6 { 606 | return value, blocksize, addrs, fmt.Errorf("unexpected number of sample values: got %d, want 6", len(sampleData)) 607 | } 608 | 609 | // Use first two values by default; tcmalloc sampling generates the 610 | // same value for both, only the older heap-profile collect separate 611 | // stats for in-use and allocated objects. 612 | valueIndex := 1 613 | if LegacyHeapAllocated { 614 | valueIndex = 3 615 | } 616 | 617 | var v1, v2 int64 618 | if v1, err = strconv.ParseInt(sampleData[valueIndex], 10, 64); err != nil { 619 | return value, blocksize, addrs, fmt.Errorf("malformed sample: %s: %v", line, err) 620 | } 621 | if v2, err = strconv.ParseInt(sampleData[valueIndex+1], 10, 64); err != nil { 622 | return value, blocksize, addrs, fmt.Errorf("malformed sample: %s: %v", line, err) 623 | } 624 | 625 | if v1 == 0 { 626 | if v2 != 0 { 627 | return value, blocksize, addrs, fmt.Errorf("allocation count was 0 but allocation bytes was %d", v2) 628 | } 629 | } else { 630 | blocksize = v2 / v1 631 | if sampling == "v2" { 632 | v1, v2 = scaleHeapSample(v1, v2, rate) 633 | } 634 | } 635 | 636 | value = []int64{v1, v2} 637 | addrs = parseHexAddresses(sampleData[5]) 638 | 639 | return value, blocksize, addrs, nil 640 | } 641 | 642 | // extractHexAddresses extracts hex numbers from a string and returns 643 | // them, together with their numeric value, in a slice. 644 | func extractHexAddresses(s string) ([]string, []uint64) { 645 | hexStrings := hexNumberRE.FindAllString(s, -1) 646 | var ids []uint64 647 | for _, s := range hexStrings { 648 | if id, err := strconv.ParseUint(s, 0, 64); err == nil { 649 | ids = append(ids, id) 650 | } else { 651 | // Do not expect any parsing failures due to the regexp matching. 652 | panic("failed to parse hex value:" + s) 653 | } 654 | } 655 | return hexStrings, ids 656 | } 657 | 658 | // parseHexAddresses parses hex numbers from a string and returns them 659 | // in a slice. 660 | func parseHexAddresses(s string) []uint64 { 661 | _, ids := extractHexAddresses(s) 662 | return ids 663 | } 664 | 665 | // scaleHeapSample adjusts the data from a heapz Sample to 666 | // account for its probability of appearing in the collected 667 | // data. heapz profiles are a sampling of the memory allocations 668 | // requests in a program. We estimate the unsampled value by dividing 669 | // each collected sample by its probability of appearing in the 670 | // profile. heapz v2 profiles rely on a poisson process to determine 671 | // which samples to collect, based on the desired average collection 672 | // rate R. The probability of a sample of size S to appear in that 673 | // profile is 1-exp(-S/R). 674 | func scaleHeapSample(count, size, rate int64) (int64, int64) { 675 | if count == 0 || size == 0 { 676 | return 0, 0 677 | } 678 | 679 | if rate <= 1 { 680 | // if rate==1 all samples were collected so no adjustment is needed. 681 | // if rate<1 treat as unknown and skip scaling. 682 | return count, size 683 | } 684 | 685 | avgSize := float64(size) / float64(count) 686 | scale := 1 / (1 - math.Exp(-avgSize/float64(rate))) 687 | 688 | return int64(float64(count) * scale), int64(float64(size) * scale) 689 | } 690 | 691 | // parseContention parses a contentionz profile and returns a newly 692 | // populated Profile. 693 | func parseContention(b []byte) (p *Profile, err error) { 694 | r := bytes.NewBuffer(b) 695 | l, err := r.ReadString('\n') 696 | if err != nil { 697 | return nil, errUnrecognized 698 | } 699 | 700 | if !strings.HasPrefix(l, "--- contention") { 701 | return nil, errUnrecognized 702 | } 703 | 704 | p = &Profile{ 705 | PeriodType: &ValueType{Type: "contentions", Unit: "count"}, 706 | Period: 1, 707 | SampleType: []*ValueType{ 708 | {Type: "contentions", Unit: "count"}, 709 | {Type: "delay", Unit: "nanoseconds"}, 710 | }, 711 | } 712 | 713 | var cpuHz int64 714 | // Parse text of the form "attribute = value" before the samples. 715 | const delimiter = "=" 716 | for { 717 | l, err = r.ReadString('\n') 718 | if err != nil { 719 | if err != io.EOF { 720 | return nil, err 721 | } 722 | 723 | if l == "" { 724 | break 725 | } 726 | } 727 | 728 | if l = strings.TrimSpace(l); l == "" { 729 | continue 730 | } 731 | 732 | if strings.HasPrefix(l, "---") { 733 | break 734 | } 735 | 736 | attr := strings.SplitN(l, delimiter, 2) 737 | if len(attr) != 2 { 738 | break 739 | } 740 | key, val := strings.TrimSpace(attr[0]), strings.TrimSpace(attr[1]) 741 | var err error 742 | switch key { 743 | case "cycles/second": 744 | if cpuHz, err = strconv.ParseInt(val, 0, 64); err != nil { 745 | return nil, errUnrecognized 746 | } 747 | case "sampling period": 748 | if p.Period, err = strconv.ParseInt(val, 0, 64); err != nil { 749 | return nil, errUnrecognized 750 | } 751 | case "ms since reset": 752 | ms, err := strconv.ParseInt(val, 0, 64) 753 | if err != nil { 754 | return nil, errUnrecognized 755 | } 756 | p.DurationNanos = ms * 1000 * 1000 757 | case "format": 758 | // CPP contentionz profiles don't have format. 759 | return nil, errUnrecognized 760 | case "resolution": 761 | // CPP contentionz profiles don't have resolution. 762 | return nil, errUnrecognized 763 | case "discarded samples": 764 | default: 765 | return nil, errUnrecognized 766 | } 767 | } 768 | 769 | locs := make(map[uint64]*Location) 770 | for { 771 | if l = strings.TrimSpace(l); strings.HasPrefix(l, "---") { 772 | break 773 | } 774 | value, addrs, err := parseContentionSample(l, p.Period, cpuHz) 775 | if err != nil { 776 | return nil, err 777 | } 778 | var sloc []*Location 779 | for i, addr := range addrs { 780 | // Addresses from stack traces point to the next instruction after 781 | // each call. Adjust by -1 to land somewhere on the actual call 782 | // (except for the leaf, which is not a call). 783 | if i > 0 { 784 | addr-- 785 | } 786 | loc := locs[addr] 787 | if locs[addr] == nil { 788 | loc = &Location{ 789 | Address: addr, 790 | } 791 | p.Location = append(p.Location, loc) 792 | locs[addr] = loc 793 | } 794 | sloc = append(sloc, loc) 795 | } 796 | p.Sample = append(p.Sample, &Sample{ 797 | Value: value, 798 | Location: sloc, 799 | }) 800 | 801 | if l, err = r.ReadString('\n'); err != nil { 802 | if err != io.EOF { 803 | return nil, err 804 | } 805 | if l == "" { 806 | break 807 | } 808 | } 809 | } 810 | 811 | if err = parseAdditionalSections(l, r, p); err != nil { 812 | return nil, err 813 | } 814 | 815 | return p, nil 816 | } 817 | 818 | // parseContentionSample parses a single row from a contention profile 819 | // into a new Sample. 820 | func parseContentionSample(line string, period, cpuHz int64) (value []int64, addrs []uint64, err error) { 821 | sampleData := contentionSampleRE.FindStringSubmatch(line) 822 | if sampleData == nil { 823 | return value, addrs, errUnrecognized 824 | } 825 | 826 | v1, err := strconv.ParseInt(sampleData[1], 10, 64) 827 | if err != nil { 828 | return value, addrs, fmt.Errorf("malformed sample: %s: %v", line, err) 829 | } 830 | v2, err := strconv.ParseInt(sampleData[2], 10, 64) 831 | if err != nil { 832 | return value, addrs, fmt.Errorf("malformed sample: %s: %v", line, err) 833 | } 834 | 835 | // Unsample values if period and cpuHz are available. 836 | // - Delays are scaled to cycles and then to nanoseconds. 837 | // - Contentions are scaled to cycles. 838 | if period > 0 { 839 | if cpuHz > 0 { 840 | cpuGHz := float64(cpuHz) / 1e9 841 | v1 = int64(float64(v1) * float64(period) / cpuGHz) 842 | } 843 | v2 = v2 * period 844 | } 845 | 846 | value = []int64{v2, v1} 847 | addrs = parseHexAddresses(sampleData[3]) 848 | 849 | return value, addrs, nil 850 | } 851 | 852 | // parseThread parses a Threadz profile and returns a new Profile. 853 | func parseThread(b []byte) (*Profile, error) { 854 | r := bytes.NewBuffer(b) 855 | 856 | var line string 857 | var err error 858 | for { 859 | // Skip past comments and empty lines seeking a real header. 860 | line, err = r.ReadString('\n') 861 | if err != nil { 862 | return nil, err 863 | } 864 | if !isSpaceOrComment(line) { 865 | break 866 | } 867 | } 868 | 869 | if m := threadzStartRE.FindStringSubmatch(line); m != nil { 870 | // Advance over initial comments until first stack trace. 871 | for { 872 | line, err = r.ReadString('\n') 873 | if err != nil { 874 | if err != io.EOF { 875 | return nil, err 876 | } 877 | 878 | if line == "" { 879 | break 880 | } 881 | } 882 | if sectionTrigger(line) != unrecognizedSection || line[0] == '-' { 883 | break 884 | } 885 | } 886 | } else if t := threadStartRE.FindStringSubmatch(line); len(t) != 4 { 887 | return nil, errUnrecognized 888 | } 889 | 890 | p := &Profile{ 891 | SampleType: []*ValueType{{Type: "thread", Unit: "count"}}, 892 | PeriodType: &ValueType{Type: "thread", Unit: "count"}, 893 | Period: 1, 894 | } 895 | 896 | locs := make(map[uint64]*Location) 897 | // Recognize each thread and populate profile samples. 898 | for sectionTrigger(line) == unrecognizedSection { 899 | if strings.HasPrefix(line, "---- no stack trace for") { 900 | line = "" 901 | break 902 | } 903 | if t := threadStartRE.FindStringSubmatch(line); len(t) != 4 { 904 | return nil, errUnrecognized 905 | } 906 | 907 | var addrs []uint64 908 | line, addrs, err = parseThreadSample(r) 909 | if err != nil { 910 | return nil, errUnrecognized 911 | } 912 | if len(addrs) == 0 { 913 | // We got a --same as previous threads--. Bump counters. 914 | if len(p.Sample) > 0 { 915 | s := p.Sample[len(p.Sample)-1] 916 | s.Value[0]++ 917 | } 918 | continue 919 | } 920 | 921 | var sloc []*Location 922 | for i, addr := range addrs { 923 | // Addresses from stack traces point to the next instruction after 924 | // each call. Adjust by -1 to land somewhere on the actual call 925 | // (except for the leaf, which is not a call). 926 | if i > 0 { 927 | addr-- 928 | } 929 | loc := locs[addr] 930 | if locs[addr] == nil { 931 | loc = &Location{ 932 | Address: addr, 933 | } 934 | p.Location = append(p.Location, loc) 935 | locs[addr] = loc 936 | } 937 | sloc = append(sloc, loc) 938 | } 939 | 940 | p.Sample = append(p.Sample, &Sample{ 941 | Value: []int64{1}, 942 | Location: sloc, 943 | }) 944 | } 945 | 946 | if err = parseAdditionalSections(line, r, p); err != nil { 947 | return nil, err 948 | } 949 | 950 | return p, nil 951 | } 952 | 953 | // parseThreadSample parses a symbolized or unsymbolized stack trace. 954 | // Returns the first line after the traceback, the sample (or nil if 955 | // it hits a 'same-as-previous' marker) and an error. 956 | func parseThreadSample(b *bytes.Buffer) (nextl string, addrs []uint64, err error) { 957 | var l string 958 | sameAsPrevious := false 959 | for { 960 | if l, err = b.ReadString('\n'); err != nil { 961 | if err != io.EOF { 962 | return "", nil, err 963 | } 964 | if l == "" { 965 | break 966 | } 967 | } 968 | if l = strings.TrimSpace(l); l == "" { 969 | continue 970 | } 971 | 972 | if strings.HasPrefix(l, "---") { 973 | break 974 | } 975 | if strings.Contains(l, "same as previous thread") { 976 | sameAsPrevious = true 977 | continue 978 | } 979 | 980 | addrs = append(addrs, parseHexAddresses(l)...) 981 | } 982 | 983 | if sameAsPrevious { 984 | return l, nil, nil 985 | } 986 | return l, addrs, nil 987 | } 988 | 989 | // parseAdditionalSections parses any additional sections in the 990 | // profile, ignoring any unrecognized sections. 991 | func parseAdditionalSections(l string, b *bytes.Buffer, p *Profile) (err error) { 992 | for { 993 | if sectionTrigger(l) == memoryMapSection { 994 | break 995 | } 996 | // Ignore any unrecognized sections. 997 | if l, err := b.ReadString('\n'); err != nil { 998 | if err != io.EOF { 999 | return err 1000 | } 1001 | if l == "" { 1002 | break 1003 | } 1004 | } 1005 | } 1006 | return p.ParseMemoryMap(b) 1007 | } 1008 | 1009 | // ParseMemoryMap parses a memory map in the format of 1010 | // /proc/self/maps, and overrides the mappings in the current profile. 1011 | // It renumbers the samples and locations in the profile correspondingly. 1012 | func (p *Profile) ParseMemoryMap(rd io.Reader) error { 1013 | b := bufio.NewReader(rd) 1014 | 1015 | var attrs []string 1016 | var r *strings.Replacer 1017 | const delimiter = "=" 1018 | for { 1019 | l, err := b.ReadString('\n') 1020 | if err != nil { 1021 | if err != io.EOF { 1022 | return err 1023 | } 1024 | if l == "" { 1025 | break 1026 | } 1027 | } 1028 | if l = strings.TrimSpace(l); l == "" { 1029 | continue 1030 | } 1031 | 1032 | if r != nil { 1033 | l = r.Replace(l) 1034 | } 1035 | m, err := parseMappingEntry(l) 1036 | if err != nil { 1037 | if err == errUnrecognized { 1038 | // Recognize assignments of the form: attr=value, and replace 1039 | // $attr with value on subsequent mappings. 1040 | if attr := strings.SplitN(l, delimiter, 2); len(attr) == 2 { 1041 | attrs = append(attrs, "$"+strings.TrimSpace(attr[0]), strings.TrimSpace(attr[1])) 1042 | r = strings.NewReplacer(attrs...) 1043 | } 1044 | // Ignore any unrecognized entries 1045 | continue 1046 | } 1047 | return err 1048 | } 1049 | if m == nil || (m.File == "" && len(p.Mapping) != 0) { 1050 | // In some cases the first entry may include the address range 1051 | // but not the name of the file. It should be followed by 1052 | // another entry with the name. 1053 | continue 1054 | } 1055 | if len(p.Mapping) == 1 && p.Mapping[0].File == "" { 1056 | // Update the name if this is the entry following that empty one. 1057 | p.Mapping[0].File = m.File 1058 | continue 1059 | } 1060 | p.Mapping = append(p.Mapping, m) 1061 | } 1062 | p.remapLocationIDs() 1063 | p.remapFunctionIDs() 1064 | p.remapMappingIDs() 1065 | return nil 1066 | } 1067 | 1068 | func parseMappingEntry(l string) (*Mapping, error) { 1069 | mapping := &Mapping{} 1070 | var err error 1071 | if me := procMapsRE.FindStringSubmatch(l); len(me) == 9 { 1072 | if !strings.Contains(me[3], "x") { 1073 | // Skip non-executable entries. 1074 | return nil, nil 1075 | } 1076 | if mapping.Start, err = strconv.ParseUint(me[1], 16, 64); err != nil { 1077 | return nil, errUnrecognized 1078 | } 1079 | if mapping.Limit, err = strconv.ParseUint(me[2], 16, 64); err != nil { 1080 | return nil, errUnrecognized 1081 | } 1082 | if me[4] != "" { 1083 | if mapping.Offset, err = strconv.ParseUint(me[4], 16, 64); err != nil { 1084 | return nil, errUnrecognized 1085 | } 1086 | } 1087 | mapping.File = me[8] 1088 | return mapping, nil 1089 | } 1090 | 1091 | if me := briefMapsRE.FindStringSubmatch(l); len(me) == 6 { 1092 | if mapping.Start, err = strconv.ParseUint(me[1], 16, 64); err != nil { 1093 | return nil, errUnrecognized 1094 | } 1095 | if mapping.Limit, err = strconv.ParseUint(me[2], 16, 64); err != nil { 1096 | return nil, errUnrecognized 1097 | } 1098 | mapping.File = me[3] 1099 | if me[5] != "" { 1100 | if mapping.Offset, err = strconv.ParseUint(me[5], 16, 64); err != nil { 1101 | return nil, errUnrecognized 1102 | } 1103 | } 1104 | return mapping, nil 1105 | } 1106 | 1107 | return nil, errUnrecognized 1108 | } 1109 | 1110 | type sectionType int 1111 | 1112 | const ( 1113 | unrecognizedSection sectionType = iota 1114 | memoryMapSection 1115 | ) 1116 | 1117 | var memoryMapTriggers = []string{ 1118 | "--- Memory map: ---", 1119 | "MAPPED_LIBRARIES:", 1120 | } 1121 | 1122 | func sectionTrigger(line string) sectionType { 1123 | for _, trigger := range memoryMapTriggers { 1124 | if strings.Contains(line, trigger) { 1125 | return memoryMapSection 1126 | } 1127 | } 1128 | return unrecognizedSection 1129 | } 1130 | 1131 | func (p *Profile) addLegacyFrameInfo() { 1132 | switch { 1133 | case isProfileType(p, heapzSampleTypes) || 1134 | isProfileType(p, heapzInUseSampleTypes) || 1135 | isProfileType(p, heapzAllocSampleTypes): 1136 | p.DropFrames, p.KeepFrames = allocRxStr, allocSkipRxStr 1137 | case isProfileType(p, contentionzSampleTypes): 1138 | p.DropFrames, p.KeepFrames = lockRxStr, "" 1139 | default: 1140 | p.DropFrames, p.KeepFrames = cpuProfilerRxStr, "" 1141 | } 1142 | } 1143 | 1144 | var heapzSampleTypes = []string{"allocations", "size"} // early Go pprof profiles 1145 | var heapzInUseSampleTypes = []string{"inuse_objects", "inuse_space"} 1146 | var heapzAllocSampleTypes = []string{"alloc_objects", "alloc_space"} 1147 | var contentionzSampleTypes = []string{"contentions", "delay"} 1148 | 1149 | func isProfileType(p *Profile, t []string) bool { 1150 | st := p.SampleType 1151 | if len(st) != len(t) { 1152 | return false 1153 | } 1154 | 1155 | for i := range st { 1156 | if st[i].Type != t[i] { 1157 | return false 1158 | } 1159 | } 1160 | return true 1161 | } 1162 | 1163 | var allocRxStr = strings.Join([]string{ 1164 | // POSIX entry points. 1165 | `calloc`, 1166 | `cfree`, 1167 | `malloc`, 1168 | `free`, 1169 | `memalign`, 1170 | `do_memalign`, 1171 | `(__)?posix_memalign`, 1172 | `pvalloc`, 1173 | `valloc`, 1174 | `realloc`, 1175 | 1176 | // TC malloc. 1177 | `tcmalloc::.*`, 1178 | `tc_calloc`, 1179 | `tc_cfree`, 1180 | `tc_malloc`, 1181 | `tc_free`, 1182 | `tc_memalign`, 1183 | `tc_posix_memalign`, 1184 | `tc_pvalloc`, 1185 | `tc_valloc`, 1186 | `tc_realloc`, 1187 | `tc_new`, 1188 | `tc_delete`, 1189 | `tc_newarray`, 1190 | `tc_deletearray`, 1191 | `tc_new_nothrow`, 1192 | `tc_newarray_nothrow`, 1193 | 1194 | // Memory-allocation routines on OS X. 1195 | `malloc_zone_malloc`, 1196 | `malloc_zone_calloc`, 1197 | `malloc_zone_valloc`, 1198 | `malloc_zone_realloc`, 1199 | `malloc_zone_memalign`, 1200 | `malloc_zone_free`, 1201 | 1202 | // Go runtime 1203 | `runtime\..*`, 1204 | 1205 | // Other misc. memory allocation routines 1206 | `BaseArena::.*`, 1207 | `(::)?do_malloc_no_errno`, 1208 | `(::)?do_malloc_pages`, 1209 | `(::)?do_malloc`, 1210 | `DoSampledAllocation`, 1211 | `MallocedMemBlock::MallocedMemBlock`, 1212 | `_M_allocate`, 1213 | `__builtin_(vec_)?delete`, 1214 | `__builtin_(vec_)?new`, 1215 | `__gnu_cxx::new_allocator::allocate`, 1216 | `__libc_malloc`, 1217 | `__malloc_alloc_template::allocate`, 1218 | `allocate`, 1219 | `cpp_alloc`, 1220 | `operator new(\[\])?`, 1221 | `simple_alloc::allocate`, 1222 | }, `|`) 1223 | 1224 | var allocSkipRxStr = strings.Join([]string{ 1225 | // Preserve Go runtime frames that appear in the middle/bottom of 1226 | // the stack. 1227 | `runtime\.panic`, 1228 | }, `|`) 1229 | 1230 | var cpuProfilerRxStr = strings.Join([]string{ 1231 | `ProfileData::Add`, 1232 | `ProfileData::prof_handler`, 1233 | `CpuProfiler::prof_handler`, 1234 | `__pthread_sighandler`, 1235 | `__restore`, 1236 | }, `|`) 1237 | 1238 | var lockRxStr = strings.Join([]string{ 1239 | `RecordLockProfileData`, 1240 | `(base::)?RecordLockProfileData.*`, 1241 | `(base::)?SubmitMutexProfileData.*`, 1242 | `(base::)?SubmitSpinLockProfileData.*`, 1243 | `(Mutex::)?AwaitCommon.*`, 1244 | `(Mutex::)?Unlock.*`, 1245 | `(Mutex::)?UnlockSlow.*`, 1246 | `(Mutex::)?ReaderUnlock.*`, 1247 | `(MutexLock::)?~MutexLock.*`, 1248 | `(SpinLock::)?Unlock.*`, 1249 | `(SpinLock::)?SlowUnlock.*`, 1250 | `(SpinLockHolder::)?~SpinLockHolder.*`, 1251 | }, `|`) 1252 | -------------------------------------------------------------------------------- /internal/profile/profile.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package profile provides a representation of profile.proto and 6 | // methods to encode/decode profiles in this format. 7 | package profile 8 | 9 | import ( 10 | "bytes" 11 | "compress/gzip" 12 | "fmt" 13 | "io" 14 | "io/ioutil" 15 | "regexp" 16 | "strings" 17 | "time" 18 | ) 19 | 20 | // Profile is an in-memory representation of profile.proto. 21 | type Profile struct { 22 | SampleType []*ValueType 23 | Sample []*Sample 24 | Mapping []*Mapping 25 | Location []*Location 26 | Function []*Function 27 | 28 | DropFrames string 29 | KeepFrames string 30 | 31 | TimeNanos int64 32 | DurationNanos int64 33 | PeriodType *ValueType 34 | Period int64 35 | 36 | dropFramesX int64 37 | keepFramesX int64 38 | stringTable []string 39 | } 40 | 41 | // ValueType corresponds to Profile.ValueType 42 | type ValueType struct { 43 | Type string // cpu, wall, inuse_space, etc 44 | Unit string // seconds, nanoseconds, bytes, etc 45 | 46 | typeX int64 47 | unitX int64 48 | } 49 | 50 | // Sample corresponds to Profile.Sample 51 | type Sample struct { 52 | Location []*Location 53 | Value []int64 54 | Label map[string][]string 55 | NumLabel map[string][]int64 56 | 57 | locationIDX []uint64 58 | labelX []Label 59 | } 60 | 61 | // Label corresponds to Profile.Label 62 | type Label struct { 63 | keyX int64 64 | // Exactly one of the two following values must be set 65 | strX int64 66 | numX int64 // Integer value for this label 67 | } 68 | 69 | // Mapping corresponds to Profile.Mapping 70 | type Mapping struct { 71 | ID uint64 72 | Start uint64 73 | Limit uint64 74 | Offset uint64 75 | File string 76 | BuildID string 77 | HasFunctions bool 78 | HasFilenames bool 79 | HasLineNumbers bool 80 | HasInlineFrames bool 81 | 82 | fileX int64 83 | buildIDX int64 84 | } 85 | 86 | // Location corresponds to Profile.Location 87 | type Location struct { 88 | ID uint64 89 | Mapping *Mapping 90 | Address uint64 91 | Line []Line 92 | 93 | mappingIDX uint64 94 | } 95 | 96 | // Line corresponds to Profile.Line 97 | type Line struct { 98 | Function *Function 99 | Line int64 100 | 101 | functionIDX uint64 102 | } 103 | 104 | // Function corresponds to Profile.Function 105 | type Function struct { 106 | ID uint64 107 | Name string 108 | SystemName string 109 | Filename string 110 | StartLine int64 111 | 112 | nameX int64 113 | systemNameX int64 114 | filenameX int64 115 | } 116 | 117 | // Parse parses a profile and checks for its validity. The input 118 | // may be a gzip-compressed encoded protobuf or one of many legacy 119 | // profile formats which may be unsupported in the future. 120 | func Parse(r io.Reader) (*Profile, error) { 121 | orig, err := ioutil.ReadAll(r) 122 | if err != nil { 123 | return nil, err 124 | } 125 | 126 | var p *Profile 127 | if len(orig) >= 2 && orig[0] == 0x1f && orig[1] == 0x8b { 128 | gz, err := gzip.NewReader(bytes.NewBuffer(orig)) 129 | if err != nil { 130 | return nil, fmt.Errorf("decompressing profile: %v", err) 131 | } 132 | data, err := ioutil.ReadAll(gz) 133 | if err != nil { 134 | return nil, fmt.Errorf("decompressing profile: %v", err) 135 | } 136 | orig = data 137 | } 138 | if p, err = parseUncompressed(orig); err != nil { 139 | if p, err = parseLegacy(orig); err != nil { 140 | return nil, fmt.Errorf("parsing profile: %v", err) 141 | } 142 | } 143 | 144 | if err := p.CheckValid(); err != nil { 145 | return nil, fmt.Errorf("malformed profile: %v", err) 146 | } 147 | return p, nil 148 | } 149 | 150 | var errUnrecognized = fmt.Errorf("unrecognized profile format") 151 | var errMalformed = fmt.Errorf("malformed profile format") 152 | 153 | func parseLegacy(data []byte) (*Profile, error) { 154 | parsers := []func([]byte) (*Profile, error){ 155 | parseCPU, 156 | parseHeap, 157 | parseGoCount, // goroutine, threadcreate 158 | parseThread, 159 | parseContention, 160 | } 161 | 162 | for _, parser := range parsers { 163 | p, err := parser(data) 164 | if err == nil { 165 | p.setMain() 166 | p.addLegacyFrameInfo() 167 | return p, nil 168 | } 169 | if err != errUnrecognized { 170 | return nil, err 171 | } 172 | } 173 | return nil, errUnrecognized 174 | } 175 | 176 | func parseUncompressed(data []byte) (*Profile, error) { 177 | p := &Profile{} 178 | if err := unmarshal(data, p); err != nil { 179 | return nil, err 180 | } 181 | 182 | if err := p.postDecode(); err != nil { 183 | return nil, err 184 | } 185 | 186 | return p, nil 187 | } 188 | 189 | var libRx = regexp.MustCompile(`([.]so$|[.]so[._][0-9]+)`) 190 | 191 | // setMain scans Mapping entries and guesses which entry is main 192 | // because legacy profiles don't obey the convention of putting main 193 | // first. 194 | func (p *Profile) setMain() { 195 | for i := 0; i < len(p.Mapping); i++ { 196 | file := strings.TrimSpace(strings.Replace(p.Mapping[i].File, "(deleted)", "", -1)) 197 | if len(file) == 0 { 198 | continue 199 | } 200 | if len(libRx.FindStringSubmatch(file)) > 0 { 201 | continue 202 | } 203 | if strings.HasPrefix(file, "[") { 204 | continue 205 | } 206 | // Swap what we guess is main to position 0. 207 | tmp := p.Mapping[i] 208 | p.Mapping[i] = p.Mapping[0] 209 | p.Mapping[0] = tmp 210 | break 211 | } 212 | } 213 | 214 | // Write writes the profile as a gzip-compressed marshaled protobuf. 215 | func (p *Profile) Write(w io.Writer) error { 216 | p.preEncode() 217 | b := marshal(p) 218 | zw := gzip.NewWriter(w) 219 | defer zw.Close() 220 | _, err := zw.Write(b) 221 | return err 222 | } 223 | 224 | // CheckValid tests whether the profile is valid. Checks include, but are 225 | // not limited to: 226 | // - len(Profile.Sample[n].value) == len(Profile.value_unit) 227 | // - Sample.id has a corresponding Profile.Location 228 | func (p *Profile) CheckValid() error { 229 | // Check that sample values are consistent 230 | sampleLen := len(p.SampleType) 231 | if sampleLen == 0 && len(p.Sample) != 0 { 232 | return fmt.Errorf("missing sample type information") 233 | } 234 | for _, s := range p.Sample { 235 | if len(s.Value) != sampleLen { 236 | return fmt.Errorf("mismatch: sample has: %d values vs. %d types", len(s.Value), len(p.SampleType)) 237 | } 238 | } 239 | 240 | // Check that all mappings/locations/functions are in the tables 241 | // Check that there are no duplicate ids 242 | mappings := make(map[uint64]*Mapping, len(p.Mapping)) 243 | for _, m := range p.Mapping { 244 | if m.ID == 0 { 245 | return fmt.Errorf("found mapping with reserved ID=0") 246 | } 247 | if mappings[m.ID] != nil { 248 | return fmt.Errorf("multiple mappings with same id: %d", m.ID) 249 | } 250 | mappings[m.ID] = m 251 | } 252 | functions := make(map[uint64]*Function, len(p.Function)) 253 | for _, f := range p.Function { 254 | if f.ID == 0 { 255 | return fmt.Errorf("found function with reserved ID=0") 256 | } 257 | if functions[f.ID] != nil { 258 | return fmt.Errorf("multiple functions with same id: %d", f.ID) 259 | } 260 | functions[f.ID] = f 261 | } 262 | locations := make(map[uint64]*Location, len(p.Location)) 263 | for _, l := range p.Location { 264 | if l.ID == 0 { 265 | return fmt.Errorf("found location with reserved id=0") 266 | } 267 | if locations[l.ID] != nil { 268 | return fmt.Errorf("multiple locations with same id: %d", l.ID) 269 | } 270 | locations[l.ID] = l 271 | if m := l.Mapping; m != nil { 272 | if m.ID == 0 || mappings[m.ID] != m { 273 | return fmt.Errorf("inconsistent mapping %p: %d", m, m.ID) 274 | } 275 | } 276 | for _, ln := range l.Line { 277 | if f := ln.Function; f != nil { 278 | if f.ID == 0 || functions[f.ID] != f { 279 | return fmt.Errorf("inconsistent function %p: %d", f, f.ID) 280 | } 281 | } 282 | } 283 | } 284 | return nil 285 | } 286 | 287 | // Aggregate merges the locations in the profile into equivalence 288 | // classes preserving the request attributes. It also updates the 289 | // samples to point to the merged locations. 290 | func (p *Profile) Aggregate(inlineFrame, function, filename, linenumber, address bool) error { 291 | for _, m := range p.Mapping { 292 | m.HasInlineFrames = m.HasInlineFrames && inlineFrame 293 | m.HasFunctions = m.HasFunctions && function 294 | m.HasFilenames = m.HasFilenames && filename 295 | m.HasLineNumbers = m.HasLineNumbers && linenumber 296 | } 297 | 298 | // Aggregate functions 299 | if !function || !filename { 300 | for _, f := range p.Function { 301 | if !function { 302 | f.Name = "" 303 | f.SystemName = "" 304 | } 305 | if !filename { 306 | f.Filename = "" 307 | } 308 | } 309 | } 310 | 311 | // Aggregate locations 312 | if !inlineFrame || !address || !linenumber { 313 | for _, l := range p.Location { 314 | if !inlineFrame && len(l.Line) > 1 { 315 | l.Line = l.Line[len(l.Line)-1:] 316 | } 317 | if !linenumber { 318 | for i := range l.Line { 319 | l.Line[i].Line = 0 320 | } 321 | } 322 | if !address { 323 | l.Address = 0 324 | } 325 | } 326 | } 327 | 328 | return p.CheckValid() 329 | } 330 | 331 | // Print dumps a text representation of a profile. Intended mainly 332 | // for debugging purposes. 333 | func (p *Profile) String() string { 334 | 335 | ss := make([]string, 0, len(p.Sample)+len(p.Mapping)+len(p.Location)) 336 | if pt := p.PeriodType; pt != nil { 337 | ss = append(ss, fmt.Sprintf("PeriodType: %s %s", pt.Type, pt.Unit)) 338 | } 339 | ss = append(ss, fmt.Sprintf("Period: %d", p.Period)) 340 | if p.TimeNanos != 0 { 341 | ss = append(ss, fmt.Sprintf("Time: %v", time.Unix(0, p.TimeNanos))) 342 | } 343 | if p.DurationNanos != 0 { 344 | ss = append(ss, fmt.Sprintf("Duration: %v", time.Duration(p.DurationNanos))) 345 | } 346 | 347 | ss = append(ss, "Samples:") 348 | var sh1 string 349 | for _, s := range p.SampleType { 350 | sh1 = sh1 + fmt.Sprintf("%s/%s ", s.Type, s.Unit) 351 | } 352 | ss = append(ss, strings.TrimSpace(sh1)) 353 | for _, s := range p.Sample { 354 | var sv string 355 | for _, v := range s.Value { 356 | sv = fmt.Sprintf("%s %10d", sv, v) 357 | } 358 | sv = sv + ": " 359 | for _, l := range s.Location { 360 | sv = sv + fmt.Sprintf("%d ", l.ID) 361 | } 362 | ss = append(ss, sv) 363 | const labelHeader = " " 364 | if len(s.Label) > 0 { 365 | ls := labelHeader 366 | for k, v := range s.Label { 367 | ls = ls + fmt.Sprintf("%s:%v ", k, v) 368 | } 369 | ss = append(ss, ls) 370 | } 371 | if len(s.NumLabel) > 0 { 372 | ls := labelHeader 373 | for k, v := range s.NumLabel { 374 | ls = ls + fmt.Sprintf("%s:%v ", k, v) 375 | } 376 | ss = append(ss, ls) 377 | } 378 | } 379 | 380 | ss = append(ss, "Locations") 381 | for _, l := range p.Location { 382 | locStr := fmt.Sprintf("%6d: %#x ", l.ID, l.Address) 383 | if m := l.Mapping; m != nil { 384 | locStr = locStr + fmt.Sprintf("M=%d ", m.ID) 385 | } 386 | if len(l.Line) == 0 { 387 | ss = append(ss, locStr) 388 | } 389 | for li := range l.Line { 390 | lnStr := "??" 391 | if fn := l.Line[li].Function; fn != nil { 392 | lnStr = fmt.Sprintf("%s %s:%d s=%d", 393 | fn.Name, 394 | fn.Filename, 395 | l.Line[li].Line, 396 | fn.StartLine) 397 | if fn.Name != fn.SystemName { 398 | lnStr = lnStr + "(" + fn.SystemName + ")" 399 | } 400 | } 401 | ss = append(ss, locStr+lnStr) 402 | // Do not print location details past the first line 403 | locStr = " " 404 | } 405 | } 406 | 407 | ss = append(ss, "Mappings") 408 | for _, m := range p.Mapping { 409 | bits := "" 410 | if m.HasFunctions { 411 | bits = bits + "[FN]" 412 | } 413 | if m.HasFilenames { 414 | bits = bits + "[FL]" 415 | } 416 | if m.HasLineNumbers { 417 | bits = bits + "[LN]" 418 | } 419 | if m.HasInlineFrames { 420 | bits = bits + "[IN]" 421 | } 422 | ss = append(ss, fmt.Sprintf("%d: %#x/%#x/%#x %s %s %s", 423 | m.ID, 424 | m.Start, m.Limit, m.Offset, 425 | m.File, 426 | m.BuildID, 427 | bits)) 428 | } 429 | 430 | return strings.Join(ss, "\n") + "\n" 431 | } 432 | 433 | // Merge adds profile p adjusted by ratio r into profile p. Profiles 434 | // must be compatible (same Type and SampleType). 435 | // TODO(rsilvera): consider normalizing the profiles based on the 436 | // total samples collected. 437 | func (p *Profile) Merge(pb *Profile, r float64) error { 438 | if err := p.Compatible(pb); err != nil { 439 | return err 440 | } 441 | 442 | pb = pb.Copy() 443 | 444 | // Keep the largest of the two periods. 445 | if pb.Period > p.Period { 446 | p.Period = pb.Period 447 | } 448 | 449 | p.DurationNanos += pb.DurationNanos 450 | 451 | p.Mapping = append(p.Mapping, pb.Mapping...) 452 | for i, m := range p.Mapping { 453 | m.ID = uint64(i + 1) 454 | } 455 | p.Location = append(p.Location, pb.Location...) 456 | for i, l := range p.Location { 457 | l.ID = uint64(i + 1) 458 | } 459 | p.Function = append(p.Function, pb.Function...) 460 | for i, f := range p.Function { 461 | f.ID = uint64(i + 1) 462 | } 463 | 464 | if r != 1.0 { 465 | for _, s := range pb.Sample { 466 | for i, v := range s.Value { 467 | s.Value[i] = int64((float64(v) * r)) 468 | } 469 | } 470 | } 471 | p.Sample = append(p.Sample, pb.Sample...) 472 | return p.CheckValid() 473 | } 474 | 475 | // Compatible determines if two profiles can be compared/merged. 476 | // returns nil if the profiles are compatible; otherwise an error with 477 | // details on the incompatibility. 478 | func (p *Profile) Compatible(pb *Profile) error { 479 | if !compatibleValueTypes(p.PeriodType, pb.PeriodType) { 480 | return fmt.Errorf("incompatible period types %v and %v", p.PeriodType, pb.PeriodType) 481 | } 482 | 483 | if len(p.SampleType) != len(pb.SampleType) { 484 | return fmt.Errorf("incompatible sample types %v and %v", p.SampleType, pb.SampleType) 485 | } 486 | 487 | for i := range p.SampleType { 488 | if !compatibleValueTypes(p.SampleType[i], pb.SampleType[i]) { 489 | return fmt.Errorf("incompatible sample types %v and %v", p.SampleType, pb.SampleType) 490 | } 491 | } 492 | 493 | return nil 494 | } 495 | 496 | // HasFunctions determines if all locations in this profile have 497 | // symbolized function information. 498 | func (p *Profile) HasFunctions() bool { 499 | for _, l := range p.Location { 500 | if l.Mapping == nil || !l.Mapping.HasFunctions { 501 | return false 502 | } 503 | } 504 | return true 505 | } 506 | 507 | // HasFileLines determines if all locations in this profile have 508 | // symbolized file and line number information. 509 | func (p *Profile) HasFileLines() bool { 510 | for _, l := range p.Location { 511 | if l.Mapping == nil || (!l.Mapping.HasFilenames || !l.Mapping.HasLineNumbers) { 512 | return false 513 | } 514 | } 515 | return true 516 | } 517 | 518 | func compatibleValueTypes(v1, v2 *ValueType) bool { 519 | if v1 == nil || v2 == nil { 520 | return true // No grounds to disqualify. 521 | } 522 | return v1.Type == v2.Type && v1.Unit == v2.Unit 523 | } 524 | 525 | // Copy makes a fully independent copy of a profile. 526 | func (p *Profile) Copy() *Profile { 527 | p.preEncode() 528 | b := marshal(p) 529 | 530 | pp := &Profile{} 531 | if err := unmarshal(b, pp); err != nil { 532 | panic(err) 533 | } 534 | if err := pp.postDecode(); err != nil { 535 | panic(err) 536 | } 537 | 538 | return pp 539 | } 540 | 541 | // Demangler maps symbol names to a human-readable form. This may 542 | // include C++ demangling and additional simplification. Names that 543 | // are not demangled may be missing from the resulting map. 544 | type Demangler func(name []string) (map[string]string, error) 545 | 546 | // Demangle attempts to demangle and optionally simplify any function 547 | // names referenced in the profile. It works on a best-effort basis: 548 | // it will silently preserve the original names in case of any errors. 549 | func (p *Profile) Demangle(d Demangler) error { 550 | // Collect names to demangle. 551 | var names []string 552 | for _, fn := range p.Function { 553 | names = append(names, fn.SystemName) 554 | } 555 | 556 | // Update profile with demangled names. 557 | demangled, err := d(names) 558 | if err != nil { 559 | return err 560 | } 561 | for _, fn := range p.Function { 562 | if dd, ok := demangled[fn.SystemName]; ok { 563 | fn.Name = dd 564 | } 565 | } 566 | return nil 567 | } 568 | 569 | // Empty returns true if the profile contains no samples. 570 | func (p *Profile) Empty() bool { 571 | return len(p.Sample) == 0 572 | } 573 | -------------------------------------------------------------------------------- /internal/profile/profile_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package profile 6 | 7 | import ( 8 | "bytes" 9 | "testing" 10 | ) 11 | 12 | func TestEmptyProfile(t *testing.T) { 13 | var buf bytes.Buffer 14 | p, err := Parse(&buf) 15 | if err != nil { 16 | t.Error("Want no error, got", err) 17 | } 18 | if p == nil { 19 | t.Fatal("Want a valid profile, got ") 20 | } 21 | if !p.Empty() { 22 | t.Errorf("Profile should be empty, got %#v", p) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /internal/profile/proto.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // This file is a simple protocol buffer encoder and decoder. 6 | // 7 | // A protocol message must implement the message interface: 8 | // decoder() []decoder 9 | // encode(*buffer) 10 | // 11 | // The decode method returns a slice indexed by field number that gives the 12 | // function to decode that field. 13 | // The encode method encodes its receiver into the given buffer. 14 | // 15 | // The two methods are simple enough to be implemented by hand rather than 16 | // by using a protocol compiler. 17 | // 18 | // See profile.go for examples of messages implementing this interface. 19 | // 20 | // There is no support for groups, message sets, or "has" bits. 21 | 22 | package profile 23 | 24 | import "errors" 25 | 26 | type buffer struct { 27 | field int 28 | typ int 29 | u64 uint64 30 | data []byte 31 | tmp [16]byte 32 | } 33 | 34 | type decoder func(*buffer, message) error 35 | 36 | type message interface { 37 | decoder() []decoder 38 | encode(*buffer) 39 | } 40 | 41 | func marshal(m message) []byte { 42 | var b buffer 43 | m.encode(&b) 44 | return b.data 45 | } 46 | 47 | func encodeVarint(b *buffer, x uint64) { 48 | for x >= 128 { 49 | b.data = append(b.data, byte(x)|0x80) 50 | x >>= 7 51 | } 52 | b.data = append(b.data, byte(x)) 53 | } 54 | 55 | func encodeLength(b *buffer, tag int, len int) { 56 | encodeVarint(b, uint64(tag)<<3|2) 57 | encodeVarint(b, uint64(len)) 58 | } 59 | 60 | func encodeUint64(b *buffer, tag int, x uint64) { 61 | // append varint to b.data 62 | encodeVarint(b, uint64(tag)<<3|0) 63 | encodeVarint(b, x) 64 | } 65 | 66 | func encodeUint64s(b *buffer, tag int, x []uint64) { 67 | for _, u := range x { 68 | encodeUint64(b, tag, u) 69 | } 70 | } 71 | 72 | func encodeUint64Opt(b *buffer, tag int, x uint64) { 73 | if x == 0 { 74 | return 75 | } 76 | encodeUint64(b, tag, x) 77 | } 78 | 79 | func encodeInt64(b *buffer, tag int, x int64) { 80 | u := uint64(x) 81 | encodeUint64(b, tag, u) 82 | } 83 | 84 | func encodeInt64Opt(b *buffer, tag int, x int64) { 85 | if x == 0 { 86 | return 87 | } 88 | encodeInt64(b, tag, x) 89 | } 90 | 91 | func encodeString(b *buffer, tag int, x string) { 92 | encodeLength(b, tag, len(x)) 93 | b.data = append(b.data, x...) 94 | } 95 | 96 | func encodeStrings(b *buffer, tag int, x []string) { 97 | for _, s := range x { 98 | encodeString(b, tag, s) 99 | } 100 | } 101 | 102 | func encodeStringOpt(b *buffer, tag int, x string) { 103 | if x == "" { 104 | return 105 | } 106 | encodeString(b, tag, x) 107 | } 108 | 109 | func encodeBool(b *buffer, tag int, x bool) { 110 | if x { 111 | encodeUint64(b, tag, 1) 112 | } else { 113 | encodeUint64(b, tag, 0) 114 | } 115 | } 116 | 117 | func encodeBoolOpt(b *buffer, tag int, x bool) { 118 | if x == false { 119 | return 120 | } 121 | encodeBool(b, tag, x) 122 | } 123 | 124 | func encodeMessage(b *buffer, tag int, m message) { 125 | n1 := len(b.data) 126 | m.encode(b) 127 | n2 := len(b.data) 128 | encodeLength(b, tag, n2-n1) 129 | n3 := len(b.data) 130 | copy(b.tmp[:], b.data[n2:n3]) 131 | copy(b.data[n1+(n3-n2):], b.data[n1:n2]) 132 | copy(b.data[n1:], b.tmp[:n3-n2]) 133 | } 134 | 135 | func unmarshal(data []byte, m message) (err error) { 136 | b := buffer{data: data, typ: 2} 137 | return decodeMessage(&b, m) 138 | } 139 | 140 | func le64(p []byte) uint64 { 141 | return uint64(p[0]) | uint64(p[1])<<8 | uint64(p[2])<<16 | uint64(p[3])<<24 | uint64(p[4])<<32 | uint64(p[5])<<40 | uint64(p[6])<<48 | uint64(p[7])<<56 142 | } 143 | 144 | func le32(p []byte) uint32 { 145 | return uint32(p[0]) | uint32(p[1])<<8 | uint32(p[2])<<16 | uint32(p[3])<<24 146 | } 147 | 148 | func decodeVarint(data []byte) (uint64, []byte, error) { 149 | var i int 150 | var u uint64 151 | for i = 0; ; i++ { 152 | if i >= 10 || i >= len(data) { 153 | return 0, nil, errors.New("bad varint") 154 | } 155 | u |= uint64(data[i]&0x7F) << uint(7*i) 156 | if data[i]&0x80 == 0 { 157 | return u, data[i+1:], nil 158 | } 159 | } 160 | } 161 | 162 | func decodeField(b *buffer, data []byte) ([]byte, error) { 163 | x, data, err := decodeVarint(data) 164 | if err != nil { 165 | return nil, err 166 | } 167 | b.field = int(x >> 3) 168 | b.typ = int(x & 7) 169 | b.data = nil 170 | b.u64 = 0 171 | switch b.typ { 172 | case 0: 173 | b.u64, data, err = decodeVarint(data) 174 | if err != nil { 175 | return nil, err 176 | } 177 | case 1: 178 | if len(data) < 8 { 179 | return nil, errors.New("not enough data") 180 | } 181 | b.u64 = le64(data[:8]) 182 | data = data[8:] 183 | case 2: 184 | var n uint64 185 | n, data, err = decodeVarint(data) 186 | if err != nil { 187 | return nil, err 188 | } 189 | if n > uint64(len(data)) { 190 | return nil, errors.New("too much data") 191 | } 192 | b.data = data[:n] 193 | data = data[n:] 194 | case 5: 195 | if len(data) < 4 { 196 | return nil, errors.New("not enough data") 197 | } 198 | b.u64 = uint64(le32(data[:4])) 199 | data = data[4:] 200 | default: 201 | return nil, errors.New("unknown type: " + string(b.typ)) 202 | } 203 | 204 | return data, nil 205 | } 206 | 207 | func checkType(b *buffer, typ int) error { 208 | if b.typ != typ { 209 | return errors.New("type mismatch") 210 | } 211 | return nil 212 | } 213 | 214 | func decodeMessage(b *buffer, m message) error { 215 | if err := checkType(b, 2); err != nil { 216 | return err 217 | } 218 | dec := m.decoder() 219 | data := b.data 220 | for len(data) > 0 { 221 | // pull varint field# + type 222 | var err error 223 | data, err = decodeField(b, data) 224 | if err != nil { 225 | return err 226 | } 227 | if b.field >= len(dec) || dec[b.field] == nil { 228 | continue 229 | } 230 | if err := dec[b.field](b, m); err != nil { 231 | return err 232 | } 233 | } 234 | return nil 235 | } 236 | 237 | func decodeInt64(b *buffer, x *int64) error { 238 | if err := checkType(b, 0); err != nil { 239 | return err 240 | } 241 | *x = int64(b.u64) 242 | return nil 243 | } 244 | 245 | func decodeInt64s(b *buffer, x *[]int64) error { 246 | var i int64 247 | if err := decodeInt64(b, &i); err != nil { 248 | return err 249 | } 250 | *x = append(*x, i) 251 | return nil 252 | } 253 | 254 | func decodeUint64(b *buffer, x *uint64) error { 255 | if err := checkType(b, 0); err != nil { 256 | return err 257 | } 258 | *x = b.u64 259 | return nil 260 | } 261 | 262 | func decodeUint64s(b *buffer, x *[]uint64) error { 263 | var u uint64 264 | if err := decodeUint64(b, &u); err != nil { 265 | return err 266 | } 267 | *x = append(*x, u) 268 | return nil 269 | } 270 | 271 | func decodeString(b *buffer, x *string) error { 272 | if err := checkType(b, 2); err != nil { 273 | return err 274 | } 275 | *x = string(b.data) 276 | return nil 277 | } 278 | 279 | func decodeStrings(b *buffer, x *[]string) error { 280 | var s string 281 | if err := decodeString(b, &s); err != nil { 282 | return err 283 | } 284 | *x = append(*x, s) 285 | return nil 286 | } 287 | 288 | func decodeBool(b *buffer, x *bool) error { 289 | if err := checkType(b, 0); err != nil { 290 | return err 291 | } 292 | if int64(b.u64) == 0 { 293 | *x = false 294 | } else { 295 | *x = true 296 | } 297 | return nil 298 | } 299 | -------------------------------------------------------------------------------- /internal/profile/prune.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Implements methods to remove frames from profiles. 6 | 7 | package profile 8 | 9 | import ( 10 | "fmt" 11 | "regexp" 12 | ) 13 | 14 | // Prune removes all nodes beneath a node matching dropRx, and not 15 | // matching keepRx. If the root node of a Sample matches, the sample 16 | // will have an empty stack. 17 | func (p *Profile) Prune(dropRx, keepRx *regexp.Regexp) { 18 | prune := make(map[uint64]bool) 19 | pruneBeneath := make(map[uint64]bool) 20 | 21 | for _, loc := range p.Location { 22 | var i int 23 | for i = len(loc.Line) - 1; i >= 0; i-- { 24 | if fn := loc.Line[i].Function; fn != nil && fn.Name != "" { 25 | funcName := fn.Name 26 | // Account for leading '.' on the PPC ELF v1 ABI. 27 | if funcName[0] == '.' { 28 | funcName = funcName[1:] 29 | } 30 | if dropRx.MatchString(funcName) { 31 | if keepRx == nil || !keepRx.MatchString(funcName) { 32 | break 33 | } 34 | } 35 | } 36 | } 37 | 38 | if i >= 0 { 39 | // Found matching entry to prune. 40 | pruneBeneath[loc.ID] = true 41 | 42 | // Remove the matching location. 43 | if i == len(loc.Line)-1 { 44 | // Matched the top entry: prune the whole location. 45 | prune[loc.ID] = true 46 | } else { 47 | loc.Line = loc.Line[i+1:] 48 | } 49 | } 50 | } 51 | 52 | // Prune locs from each Sample 53 | for _, sample := range p.Sample { 54 | // Scan from the root to the leaves to find the prune location. 55 | // Do not prune frames before the first user frame, to avoid 56 | // pruning everything. 57 | foundUser := false 58 | for i := len(sample.Location) - 1; i >= 0; i-- { 59 | id := sample.Location[i].ID 60 | if !prune[id] && !pruneBeneath[id] { 61 | foundUser = true 62 | continue 63 | } 64 | if !foundUser { 65 | continue 66 | } 67 | if prune[id] { 68 | sample.Location = sample.Location[i+1:] 69 | break 70 | } 71 | if pruneBeneath[id] { 72 | sample.Location = sample.Location[i:] 73 | break 74 | } 75 | } 76 | } 77 | } 78 | 79 | // RemoveUninteresting prunes and elides profiles using built-in 80 | // tables of uninteresting function names. 81 | func (p *Profile) RemoveUninteresting() error { 82 | var keep, drop *regexp.Regexp 83 | var err error 84 | 85 | if p.DropFrames != "" { 86 | if drop, err = regexp.Compile("^(" + p.DropFrames + ")$"); err != nil { 87 | return fmt.Errorf("failed to compile regexp %s: %v", p.DropFrames, err) 88 | } 89 | if p.KeepFrames != "" { 90 | if keep, err = regexp.Compile("^(" + p.KeepFrames + ")$"); err != nil { 91 | return fmt.Errorf("failed to compile regexp %s: %v", p.KeepFrames, err) 92 | } 93 | } 94 | p.Prune(drop, keep) 95 | } 96 | return nil 97 | } 98 | -------------------------------------------------------------------------------- /internal/report/source.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package report 6 | 7 | // This file contains routines related to the generation of annotated 8 | // source listings. 9 | 10 | import ( 11 | "bufio" 12 | "fmt" 13 | "html/template" 14 | "io" 15 | "os" 16 | "path/filepath" 17 | "sort" 18 | "strconv" 19 | "strings" 20 | 21 | "github.com/rakyll/gom/internal/plugin" 22 | ) 23 | 24 | // printSource prints an annotated source listing, include all 25 | // functions with samples that match the regexp rpt.options.symbol. 26 | // The sources are sorted by function name and then by filename to 27 | // eliminate potential nondeterminism. 28 | func printSource(w io.Writer, rpt *Report) error { 29 | o := rpt.options 30 | g, err := newGraph(rpt) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | // Identify all the functions that match the regexp provided. 36 | // Group nodes for each matching function. 37 | var functions nodes 38 | functionNodes := make(map[string]nodes) 39 | for _, n := range g.ns { 40 | if !o.Symbol.MatchString(n.info.name) { 41 | continue 42 | } 43 | if functionNodes[n.info.name] == nil { 44 | functions = append(functions, n) 45 | } 46 | functionNodes[n.info.name] = append(functionNodes[n.info.name], n) 47 | } 48 | functions.sort(nameOrder) 49 | 50 | fmt.Fprintf(w, "Total: %s\n", rpt.formatValue(rpt.total)) 51 | for _, fn := range functions { 52 | name := fn.info.name 53 | 54 | // Identify all the source files associated to this function. 55 | // Group nodes for each source file. 56 | var sourceFiles nodes 57 | fileNodes := make(map[string]nodes) 58 | for _, n := range functionNodes[name] { 59 | if n.info.file == "" { 60 | continue 61 | } 62 | if fileNodes[n.info.file] == nil { 63 | sourceFiles = append(sourceFiles, n) 64 | } 65 | fileNodes[n.info.file] = append(fileNodes[n.info.file], n) 66 | } 67 | 68 | if len(sourceFiles) == 0 { 69 | fmt.Printf("No source information for %s\n", name) 70 | continue 71 | } 72 | 73 | sourceFiles.sort(fileOrder) 74 | 75 | // Print each file associated with this function. 76 | for _, fl := range sourceFiles { 77 | filename := fl.info.file 78 | fns := fileNodes[filename] 79 | flatSum, cumSum := sumNodes(fns) 80 | 81 | fnodes, path, err := getFunctionSource(name, filename, fns, 0, 0) 82 | fmt.Fprintf(w, "ROUTINE ======================== %s in %s\n", name, path) 83 | fmt.Fprintf(w, "%10s %10s (flat, cum) %s of Total\n", 84 | rpt.formatValue(flatSum), rpt.formatValue(cumSum), 85 | percentage(cumSum, rpt.total)) 86 | 87 | if err != nil { 88 | fmt.Fprintf(w, " Error: %v\n", err) 89 | continue 90 | } 91 | 92 | for _, fn := range fnodes { 93 | fmt.Fprintf(w, "%10s %10s %6d:%s\n", valueOrDot(fn.flat, rpt), valueOrDot(fn.cum, rpt), fn.info.lineno, fn.info.name) 94 | } 95 | } 96 | } 97 | return nil 98 | } 99 | 100 | // printWebSource prints an annotated source listing, include all 101 | // functions with samples that match the regexp rpt.options.symbol. 102 | func printWebSource(w io.Writer, rpt *Report, obj plugin.ObjTool) error { 103 | o := rpt.options 104 | g, err := newGraph(rpt) 105 | if err != nil { 106 | return err 107 | } 108 | 109 | // If the regexp source can be parsed as an address, also match 110 | // functions that land on that address. 111 | var address *uint64 112 | if hex, err := strconv.ParseUint(o.Symbol.String(), 0, 64); err == nil { 113 | address = &hex 114 | } 115 | 116 | // Extract interesting symbols from binary files in the profile and 117 | // classify samples per symbol. 118 | symbols := symbolsFromBinaries(rpt.prof, g, o.Symbol, address, obj) 119 | symNodes := nodesPerSymbol(g.ns, symbols) 120 | 121 | // Sort symbols for printing. 122 | var syms objSymbols 123 | for s := range symNodes { 124 | syms = append(syms, s) 125 | } 126 | sort.Sort(syms) 127 | 128 | if len(syms) == 0 { 129 | return fmt.Errorf("no samples found on routines matching: %s", o.Symbol.String()) 130 | } 131 | 132 | printHeader(w, rpt) 133 | for _, s := range syms { 134 | name := s.sym.Name[0] 135 | // Identify sources associated to a symbol by examining 136 | // symbol samples. Classify samples per source file. 137 | var sourceFiles nodes 138 | fileNodes := make(map[string]nodes) 139 | for _, n := range symNodes[s] { 140 | if n.info.file == "" { 141 | continue 142 | } 143 | if fileNodes[n.info.file] == nil { 144 | sourceFiles = append(sourceFiles, n) 145 | } 146 | fileNodes[n.info.file] = append(fileNodes[n.info.file], n) 147 | } 148 | 149 | if len(sourceFiles) == 0 { 150 | fmt.Printf("No source information for %s\n", name) 151 | continue 152 | } 153 | 154 | sourceFiles.sort(fileOrder) 155 | 156 | // Print each file associated with this function. 157 | for _, fl := range sourceFiles { 158 | filename := fl.info.file 159 | fns := fileNodes[filename] 160 | 161 | asm := assemblyPerSourceLine(symbols, fns, filename, obj) 162 | start, end := sourceCoordinates(asm) 163 | 164 | fnodes, path, err := getFunctionSource(name, filename, fns, start, end) 165 | if err != nil { 166 | fnodes, path = getMissingFunctionSource(filename, asm, start, end) 167 | } 168 | 169 | flatSum, cumSum := sumNodes(fnodes) 170 | printFunctionHeader(w, name, path, flatSum, cumSum, rpt) 171 | for _, fn := range fnodes { 172 | printFunctionSourceLine(w, fn, asm[fn.info.lineno], rpt) 173 | } 174 | printFunctionClosing(w) 175 | } 176 | } 177 | printPageClosing(w) 178 | return nil 179 | } 180 | 181 | // sourceCoordinates returns the lowest and highest line numbers from 182 | // a set of assembly statements. 183 | func sourceCoordinates(asm map[int]nodes) (start, end int) { 184 | for l := range asm { 185 | if start == 0 || l < start { 186 | start = l 187 | } 188 | if end == 0 || l > end { 189 | end = l 190 | } 191 | } 192 | return start, end 193 | } 194 | 195 | // assemblyPerSourceLine disassembles the binary containing a symbol 196 | // and classifies the assembly instructions according to its 197 | // corresponding source line, annotating them with a set of samples. 198 | func assemblyPerSourceLine(objSyms []*objSymbol, rs nodes, src string, obj plugin.ObjTool) map[int]nodes { 199 | assembly := make(map[int]nodes) 200 | // Identify symbol to use for this collection of samples. 201 | o := findMatchingSymbol(objSyms, rs) 202 | if o == nil { 203 | return assembly 204 | } 205 | 206 | // Extract assembly for matched symbol 207 | insns, err := obj.Disasm(o.sym.File, o.sym.Start, o.sym.End) 208 | if err != nil { 209 | return assembly 210 | } 211 | 212 | srcBase := filepath.Base(src) 213 | anodes := annotateAssembly(insns, rs, o.base) 214 | var lineno = 0 215 | for _, an := range anodes { 216 | if filepath.Base(an.info.file) == srcBase { 217 | lineno = an.info.lineno 218 | } 219 | if lineno != 0 { 220 | assembly[lineno] = append(assembly[lineno], an) 221 | } 222 | } 223 | 224 | return assembly 225 | } 226 | 227 | // findMatchingSymbol looks for the symbol that corresponds to a set 228 | // of samples, by comparing their addresses. 229 | func findMatchingSymbol(objSyms []*objSymbol, ns nodes) *objSymbol { 230 | for _, n := range ns { 231 | for _, o := range objSyms { 232 | if filepath.Base(o.sym.File) == n.info.objfile && 233 | o.sym.Start <= n.info.address-o.base && 234 | n.info.address-o.base <= o.sym.End { 235 | return o 236 | } 237 | } 238 | } 239 | return nil 240 | } 241 | 242 | // printHeader prints the page header for a weblist report. 243 | func printHeader(w io.Writer, rpt *Report) { 244 | fmt.Fprintln(w, weblistPageHeader) 245 | 246 | var labels []string 247 | for _, l := range legendLabels(rpt) { 248 | labels = append(labels, template.HTMLEscapeString(l)) 249 | } 250 | 251 | fmt.Fprintf(w, `
%s
Total: %s
`, 252 | strings.Join(labels, "
\n"), 253 | rpt.formatValue(rpt.total), 254 | ) 255 | } 256 | 257 | // printFunctionHeader prints a function header for a weblist report. 258 | func printFunctionHeader(w io.Writer, name, path string, flatSum, cumSum int64, rpt *Report) { 259 | fmt.Fprintf(w, `

%s

%s 260 |
261 |   Total:  %10s %10s (flat, cum) %s
262 | `,
263 | 		template.HTMLEscapeString(name), template.HTMLEscapeString(path),
264 | 		rpt.formatValue(flatSum), rpt.formatValue(cumSum),
265 | 		percentage(cumSum, rpt.total))
266 | }
267 | 
268 | // printFunctionSourceLine prints a source line and the corresponding assembly.
269 | func printFunctionSourceLine(w io.Writer, fn *node, assembly nodes, rpt *Report) {
270 | 	if len(assembly) == 0 {
271 | 		fmt.Fprintf(w,
272 | 			" %6d   %10s %10s %s \n",
273 | 			fn.info.lineno,
274 | 			valueOrDot(fn.flat, rpt), valueOrDot(fn.cum, rpt),
275 | 			template.HTMLEscapeString(fn.info.name))
276 | 		return
277 | 	}
278 | 
279 | 	fmt.Fprintf(w,
280 | 		" %6d   %10s %10s %s ",
281 | 		fn.info.lineno,
282 | 		valueOrDot(fn.flat, rpt), valueOrDot(fn.cum, rpt),
283 | 		template.HTMLEscapeString(fn.info.name))
284 | 	fmt.Fprint(w, "")
285 | 	for _, an := range assembly {
286 | 		var fileline string
287 | 		class := "disasmloc"
288 | 		if an.info.file != "" {
289 | 			fileline = fmt.Sprintf("%s:%d", template.HTMLEscapeString(an.info.file), an.info.lineno)
290 | 			if an.info.lineno != fn.info.lineno {
291 | 				class = "unimportant"
292 | 			}
293 | 		}
294 | 		fmt.Fprintf(w, " %8s %10s %10s %8x: %-48s %s\n", "",
295 | 			valueOrDot(an.flat, rpt), valueOrDot(an.cum, rpt),
296 | 			an.info.address,
297 | 			template.HTMLEscapeString(an.info.name),
298 | 			class,
299 | 			template.HTMLEscapeString(fileline))
300 | 	}
301 | 	fmt.Fprintln(w, "")
302 | }
303 | 
304 | // printFunctionClosing prints the end of a function in a weblist report.
305 | func printFunctionClosing(w io.Writer) {
306 | 	fmt.Fprintln(w, "
") 307 | } 308 | 309 | // printPageClosing prints the end of the page in a weblist report. 310 | func printPageClosing(w io.Writer) { 311 | fmt.Fprintln(w, weblistPageClosing) 312 | } 313 | 314 | // getFunctionSource collects the sources of a function from a source 315 | // file and annotates it with the samples in fns. Returns the sources 316 | // as nodes, using the info.name field to hold the source code. 317 | func getFunctionSource(fun, file string, fns nodes, start, end int) (nodes, string, error) { 318 | f, file, err := adjustSourcePath(file) 319 | if err != nil { 320 | return nil, file, err 321 | } 322 | 323 | lineNodes := make(map[int]nodes) 324 | 325 | // Collect source coordinates from profile. 326 | const margin = 5 // Lines before first/after last sample. 327 | if start == 0 { 328 | if fns[0].info.startLine != 0 { 329 | start = fns[0].info.startLine 330 | } else { 331 | start = fns[0].info.lineno - margin 332 | } 333 | } else { 334 | start -= margin 335 | } 336 | if end == 0 { 337 | end = fns[0].info.lineno 338 | } 339 | end += margin 340 | for _, n := range fns { 341 | lineno := n.info.lineno 342 | nodeStart := n.info.startLine 343 | if nodeStart == 0 { 344 | nodeStart = lineno - margin 345 | } 346 | nodeEnd := lineno + margin 347 | if nodeStart < start { 348 | start = nodeStart 349 | } else if nodeEnd > end { 350 | end = nodeEnd 351 | } 352 | lineNodes[lineno] = append(lineNodes[lineno], n) 353 | } 354 | 355 | var src nodes 356 | buf := bufio.NewReader(f) 357 | lineno := 1 358 | for { 359 | line, err := buf.ReadString('\n') 360 | if err != nil { 361 | if err != io.EOF { 362 | return nil, file, err 363 | } 364 | if line == "" { 365 | // end was at or past EOF; that's okay 366 | break 367 | } 368 | } 369 | if lineno >= start { 370 | flat, cum := sumNodes(lineNodes[lineno]) 371 | 372 | src = append(src, &node{ 373 | info: nodeInfo{ 374 | name: strings.TrimRight(line, "\n"), 375 | lineno: lineno, 376 | }, 377 | flat: flat, 378 | cum: cum, 379 | }) 380 | } 381 | lineno++ 382 | if lineno > end { 383 | break 384 | } 385 | } 386 | return src, file, nil 387 | } 388 | 389 | // getMissingFunctionSource creates a dummy function body to point to 390 | // the source file and annotates it with the samples in asm. 391 | func getMissingFunctionSource(filename string, asm map[int]nodes, start, end int) (nodes, string) { 392 | var fnodes nodes 393 | for i := start; i <= end; i++ { 394 | lrs := asm[i] 395 | if len(lrs) == 0 { 396 | continue 397 | } 398 | flat, cum := sumNodes(lrs) 399 | fnodes = append(fnodes, &node{ 400 | info: nodeInfo{ 401 | name: "???", 402 | lineno: i, 403 | }, 404 | flat: flat, 405 | cum: cum, 406 | }) 407 | } 408 | return fnodes, filename 409 | } 410 | 411 | // adjustSourcePath adjusts the pathe for a source file by trimmming 412 | // known prefixes and searching for the file on all parents of the 413 | // current working dir. 414 | func adjustSourcePath(path string) (*os.File, string, error) { 415 | path = trimPath(path) 416 | f, err := os.Open(path) 417 | if err == nil { 418 | return f, path, nil 419 | } 420 | 421 | if dir, wderr := os.Getwd(); wderr == nil { 422 | for { 423 | parent := filepath.Dir(dir) 424 | if parent == dir { 425 | break 426 | } 427 | if f, err := os.Open(filepath.Join(parent, path)); err == nil { 428 | return f, filepath.Join(parent, path), nil 429 | } 430 | 431 | dir = parent 432 | } 433 | } 434 | 435 | return nil, path, err 436 | } 437 | 438 | // trimPath cleans up a path by removing prefixes that are commonly 439 | // found on profiles. 440 | func trimPath(path string) string { 441 | basePaths := []string{ 442 | "/proc/self/cwd/./", 443 | "/proc/self/cwd/", 444 | } 445 | 446 | sPath := filepath.ToSlash(path) 447 | 448 | for _, base := range basePaths { 449 | if strings.HasPrefix(sPath, base) { 450 | return filepath.FromSlash(sPath[len(base):]) 451 | } 452 | } 453 | return path 454 | } 455 | -------------------------------------------------------------------------------- /internal/report/source_html.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package report 6 | 7 | const weblistPageHeader = ` 8 | 9 | 10 | 11 | Pprof listing 12 | 53 | 70 | 71 | 72 | ` 73 | 74 | const weblistPageClosing = ` 75 | 76 | 77 | ` 78 | -------------------------------------------------------------------------------- /internal/svg/svg.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package svg provides tools related to handling of SVG files 6 | package svg 7 | 8 | import ( 9 | "bytes" 10 | "regexp" 11 | "strings" 12 | ) 13 | 14 | var ( 15 | viewBox = regexp.MustCompile(``) 18 | ) 19 | 20 | // Massage enhances the SVG output from DOT to provide better 21 | // panning inside a web browser. It uses the SVGPan library, which is 22 | // included directly. 23 | func Massage(in bytes.Buffer) string { 24 | svg := string(in.Bytes()) 25 | 26 | // Work around for dot bug which misses quoting some ampersands, 27 | // resulting on unparsable SVG. 28 | svg = strings.Replace(svg, "&;", "&;", -1) 29 | 30 | //Dot's SVG output is 31 | // 32 | // 34 | // 35 | // ... 36 | // 37 | // 38 | // 39 | // Change it to 40 | // 41 | // 43 | // 44 | // 45 | // 46 | // ... 47 | // 48 | // 49 | // 50 | 51 | if loc := viewBox.FindStringIndex(svg); loc != nil { 52 | svg = svg[:loc[0]] + 53 | `` + svgPanJS + `` + 60 | `` + 61 | svg[loc[0]:] 62 | } 63 | 64 | if loc := svgClose.FindStringIndex(svg); loc != nil { 65 | svg = svg[:loc[0]] + 66 | `` + 67 | svg[loc[0]:] 68 | } 69 | 70 | return svg 71 | } 72 | -------------------------------------------------------------------------------- /internal/svg/svgpan.go: -------------------------------------------------------------------------------- 1 | // SVG pan and zoom library. 2 | // See copyright notice in string constant below. 3 | 4 | package svg 5 | 6 | // https://www.cyberz.org/projects/SVGPan/SVGPan.js 7 | 8 | const svgPanJS = ` 9 | /** 10 | * SVGPan library 1.2.1 11 | * ====================== 12 | * 13 | * Given an unique existing element with id "viewport" (or when missing, the first g 14 | * element), including the the library into any SVG adds the following capabilities: 15 | * 16 | * - Mouse panning 17 | * - Mouse zooming (using the wheel) 18 | * - Object dragging 19 | * 20 | * You can configure the behaviour of the pan/zoom/drag with the variables 21 | * listed in the CONFIGURATION section of this file. 22 | * 23 | * Known issues: 24 | * 25 | * - Zooming (while panning) on Safari has still some issues 26 | * 27 | * Releases: 28 | * 29 | * 1.2.1, Mon Jul 4 00:33:18 CEST 2011, Andrea Leofreddi 30 | * - Fixed a regression with mouse wheel (now working on Firefox 5) 31 | * - Working with viewBox attribute (#4) 32 | * - Added "use strict;" and fixed resulting warnings (#5) 33 | * - Added configuration variables, dragging is disabled by default (#3) 34 | * 35 | * 1.2, Sat Mar 20 08:42:50 GMT 2010, Zeng Xiaohui 36 | * Fixed a bug with browser mouse handler interaction 37 | * 38 | * 1.1, Wed Feb 3 17:39:33 GMT 2010, Zeng Xiaohui 39 | * Updated the zoom code to support the mouse wheel on Safari/Chrome 40 | * 41 | * 1.0, Andrea Leofreddi 42 | * First release 43 | * 44 | * This code is licensed under the following BSD license: 45 | * 46 | * Copyright 2009-2010 Andrea Leofreddi . All rights reserved. 47 | * 48 | * Redistribution and use in source and binary forms, with or without modification, are 49 | * permitted provided that the following conditions are met: 50 | * 51 | * 1. Redistributions of source code must retain the above copyright notice, this list of 52 | * conditions and the following disclaimer. 53 | * 54 | * 2. Redistributions in binary form must reproduce the above copyright notice, this list 55 | * of conditions and the following disclaimer in the documentation and/or other materials 56 | * provided with the distribution. 57 | * 58 | * THIS SOFTWARE IS PROVIDED BY Andrea Leofreddi ` + "``AS IS''" + ` AND ANY EXPRESS OR IMPLIED 59 | * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 60 | * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL Andrea Leofreddi OR 61 | * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 62 | * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 63 | * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 64 | * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 65 | * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 66 | * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 67 | * 68 | * The views and conclusions contained in the software and documentation are those of the 69 | * authors and should not be interpreted as representing official policies, either expressed 70 | * or implied, of Andrea Leofreddi. 71 | */ 72 | 73 | "use strict"; 74 | 75 | /// CONFIGURATION 76 | /// ====> 77 | 78 | var enablePan = 1; // 1 or 0: enable or disable panning (default enabled) 79 | var enableZoom = 1; // 1 or 0: enable or disable zooming (default enabled) 80 | var enableDrag = 0; // 1 or 0: enable or disable dragging (default disabled) 81 | 82 | /// <==== 83 | /// END OF CONFIGURATION 84 | 85 | var root = document.documentElement; 86 | 87 | var state = 'none', svgRoot, stateTarget, stateOrigin, stateTf; 88 | 89 | setupHandlers(root); 90 | 91 | /** 92 | * Register handlers 93 | */ 94 | function setupHandlers(root){ 95 | setAttributes(root, { 96 | "onmouseup" : "handleMouseUp(evt)", 97 | "onmousedown" : "handleMouseDown(evt)", 98 | "onmousemove" : "handleMouseMove(evt)", 99 | //"onmouseout" : "handleMouseUp(evt)", // Decomment this to stop the pan functionality when dragging out of the SVG element 100 | }); 101 | 102 | if(navigator.userAgent.toLowerCase().indexOf('webkit') >= 0) 103 | window.addEventListener('mousewheel', handleMouseWheel, false); // Chrome/Safari 104 | else 105 | window.addEventListener('DOMMouseScroll', handleMouseWheel, false); // Others 106 | } 107 | 108 | /** 109 | * Retrieves the root element for SVG manipulation. The element is then cached into the svgRoot global variable. 110 | */ 111 | function getRoot(root) { 112 | if(typeof(svgRoot) == "undefined") { 113 | var g = null; 114 | 115 | g = root.getElementById("viewport"); 116 | 117 | if(g == null) 118 | g = root.getElementsByTagName('g')[0]; 119 | 120 | if(g == null) 121 | alert('Unable to obtain SVG root element'); 122 | 123 | setCTM(g, g.getCTM()); 124 | 125 | g.removeAttribute("viewBox"); 126 | 127 | svgRoot = g; 128 | } 129 | 130 | return svgRoot; 131 | } 132 | 133 | /** 134 | * Instance an SVGPoint object with given event coordinates. 135 | */ 136 | function getEventPoint(evt) { 137 | var p = root.createSVGPoint(); 138 | 139 | p.x = evt.clientX; 140 | p.y = evt.clientY; 141 | 142 | return p; 143 | } 144 | 145 | /** 146 | * Sets the current transform matrix of an element. 147 | */ 148 | function setCTM(element, matrix) { 149 | var s = "matrix(" + matrix.a + "," + matrix.b + "," + matrix.c + "," + matrix.d + "," + matrix.e + "," + matrix.f + ")"; 150 | 151 | element.setAttribute("transform", s); 152 | } 153 | 154 | /** 155 | * Dumps a matrix to a string (useful for debug). 156 | */ 157 | function dumpMatrix(matrix) { 158 | var s = "[ " + matrix.a + ", " + matrix.c + ", " + matrix.e + "\n " + matrix.b + ", " + matrix.d + ", " + matrix.f + "\n 0, 0, 1 ]"; 159 | 160 | return s; 161 | } 162 | 163 | /** 164 | * Sets attributes of an element. 165 | */ 166 | function setAttributes(element, attributes){ 167 | for (var i in attributes) 168 | element.setAttributeNS(null, i, attributes[i]); 169 | } 170 | 171 | /** 172 | * Handle mouse wheel event. 173 | */ 174 | function handleMouseWheel(evt) { 175 | if(!enableZoom) 176 | return; 177 | 178 | if(evt.preventDefault) 179 | evt.preventDefault(); 180 | 181 | evt.returnValue = false; 182 | 183 | var svgDoc = evt.target.ownerDocument; 184 | 185 | var delta; 186 | 187 | if(evt.wheelDelta) 188 | delta = evt.wheelDelta / 3600; // Chrome/Safari 189 | else 190 | delta = evt.detail / -90; // Mozilla 191 | 192 | var z = 1 + delta; // Zoom factor: 0.9/1.1 193 | 194 | var g = getRoot(svgDoc); 195 | 196 | var p = getEventPoint(evt); 197 | 198 | p = p.matrixTransform(g.getCTM().inverse()); 199 | 200 | // Compute new scale matrix in current mouse position 201 | var k = root.createSVGMatrix().translate(p.x, p.y).scale(z).translate(-p.x, -p.y); 202 | 203 | setCTM(g, g.getCTM().multiply(k)); 204 | 205 | if(typeof(stateTf) == "undefined") 206 | stateTf = g.getCTM().inverse(); 207 | 208 | stateTf = stateTf.multiply(k.inverse()); 209 | } 210 | 211 | /** 212 | * Handle mouse move event. 213 | */ 214 | function handleMouseMove(evt) { 215 | if(evt.preventDefault) 216 | evt.preventDefault(); 217 | 218 | evt.returnValue = false; 219 | 220 | var svgDoc = evt.target.ownerDocument; 221 | 222 | var g = getRoot(svgDoc); 223 | 224 | if(state == 'pan' && enablePan) { 225 | // Pan mode 226 | var p = getEventPoint(evt).matrixTransform(stateTf); 227 | 228 | setCTM(g, stateTf.inverse().translate(p.x - stateOrigin.x, p.y - stateOrigin.y)); 229 | } else if(state == 'drag' && enableDrag) { 230 | // Drag mode 231 | var p = getEventPoint(evt).matrixTransform(g.getCTM().inverse()); 232 | 233 | setCTM(stateTarget, root.createSVGMatrix().translate(p.x - stateOrigin.x, p.y - stateOrigin.y).multiply(g.getCTM().inverse()).multiply(stateTarget.getCTM())); 234 | 235 | stateOrigin = p; 236 | } 237 | } 238 | 239 | /** 240 | * Handle click event. 241 | */ 242 | function handleMouseDown(evt) { 243 | if(evt.preventDefault) 244 | evt.preventDefault(); 245 | 246 | evt.returnValue = false; 247 | 248 | var svgDoc = evt.target.ownerDocument; 249 | 250 | var g = getRoot(svgDoc); 251 | 252 | if( 253 | evt.target.tagName == "svg" 254 | || !enableDrag // Pan anyway when drag is disabled and the user clicked on an element 255 | ) { 256 | // Pan mode 257 | state = 'pan'; 258 | 259 | stateTf = g.getCTM().inverse(); 260 | 261 | stateOrigin = getEventPoint(evt).matrixTransform(stateTf); 262 | } else { 263 | // Drag mode 264 | state = 'drag'; 265 | 266 | stateTarget = evt.target; 267 | 268 | stateTf = g.getCTM().inverse(); 269 | 270 | stateOrigin = getEventPoint(evt).matrixTransform(stateTf); 271 | } 272 | } 273 | 274 | /** 275 | * Handle mouse button release event. 276 | */ 277 | function handleMouseUp(evt) { 278 | if(evt.preventDefault) 279 | evt.preventDefault(); 280 | 281 | evt.returnValue = false; 282 | 283 | var svgDoc = evt.target.ownerDocument; 284 | 285 | if(state == 'pan' || state == 'drag') { 286 | // Quit pan mode 287 | state = ''; 288 | } 289 | } 290 | 291 | ` 292 | -------------------------------------------------------------------------------- /internal/symbolizer/symbolizer.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package symbolizer provides a routine to populate a profile with 6 | // symbol, file and line number information. It relies on the 7 | // addr2liner and demangler packages to do the actual work. 8 | package symbolizer 9 | 10 | import ( 11 | "fmt" 12 | "os" 13 | "path/filepath" 14 | "strings" 15 | 16 | "github.com/rakyll/gom/internal/plugin" 17 | "github.com/rakyll/gom/internal/profile" 18 | ) 19 | 20 | // Symbolize adds symbol and line number information to all locations 21 | // in a profile. mode enables some options to control 22 | // symbolization. Currently only recognizes "force", which causes it 23 | // to overwrite any existing data. 24 | func Symbolize(mode string, prof *profile.Profile, obj plugin.ObjTool, ui plugin.UI) error { 25 | force := false 26 | // Disable some mechanisms based on mode string. 27 | for _, o := range strings.Split(strings.ToLower(mode), ":") { 28 | switch o { 29 | case "force": 30 | force = true 31 | default: 32 | } 33 | } 34 | 35 | if len(prof.Mapping) == 0 { 36 | return fmt.Errorf("no known mappings") 37 | } 38 | 39 | mt, err := newMapping(prof, obj, ui, force) 40 | if err != nil { 41 | return err 42 | } 43 | defer mt.close() 44 | 45 | functions := make(map[profile.Function]*profile.Function) 46 | for _, l := range mt.prof.Location { 47 | m := l.Mapping 48 | segment := mt.segments[m] 49 | if segment == nil { 50 | // Nothing to do 51 | continue 52 | } 53 | 54 | stack, err := segment.SourceLine(l.Address) 55 | if err != nil || len(stack) == 0 { 56 | // No answers from addr2line 57 | continue 58 | } 59 | 60 | l.Line = make([]profile.Line, len(stack)) 61 | for i, frame := range stack { 62 | if frame.Func != "" { 63 | m.HasFunctions = true 64 | } 65 | if frame.File != "" { 66 | m.HasFilenames = true 67 | } 68 | if frame.Line != 0 { 69 | m.HasLineNumbers = true 70 | } 71 | f := &profile.Function{ 72 | Name: frame.Func, 73 | SystemName: frame.Func, 74 | Filename: frame.File, 75 | } 76 | if fp := functions[*f]; fp != nil { 77 | f = fp 78 | } else { 79 | functions[*f] = f 80 | f.ID = uint64(len(mt.prof.Function)) + 1 81 | mt.prof.Function = append(mt.prof.Function, f) 82 | } 83 | l.Line[i] = profile.Line{ 84 | Function: f, 85 | Line: int64(frame.Line), 86 | } 87 | } 88 | 89 | if len(stack) > 0 { 90 | m.HasInlineFrames = true 91 | } 92 | } 93 | return nil 94 | } 95 | 96 | // newMapping creates a mappingTable for a profile. 97 | func newMapping(prof *profile.Profile, obj plugin.ObjTool, ui plugin.UI, force bool) (*mappingTable, error) { 98 | mt := &mappingTable{ 99 | prof: prof, 100 | segments: make(map[*profile.Mapping]plugin.ObjFile), 101 | } 102 | 103 | // Identify used mappings 104 | mappings := make(map[*profile.Mapping]bool) 105 | for _, l := range prof.Location { 106 | mappings[l.Mapping] = true 107 | } 108 | 109 | for _, m := range prof.Mapping { 110 | if !mappings[m] { 111 | continue 112 | } 113 | // Do not attempt to re-symbolize a mapping that has already been symbolized. 114 | if !force && (m.HasFunctions || m.HasFilenames || m.HasLineNumbers) { 115 | continue 116 | } 117 | 118 | f, err := locateFile(obj, m.File, m.BuildID, m.Start) 119 | if err != nil { 120 | ui.PrintErr("Local symbolization failed for ", filepath.Base(m.File), ": ", err) 121 | // Move on to other mappings 122 | continue 123 | } 124 | 125 | if fid := f.BuildID(); m.BuildID != "" && fid != "" && fid != m.BuildID { 126 | // Build ID mismatch - ignore. 127 | f.Close() 128 | continue 129 | } 130 | 131 | mt.segments[m] = f 132 | } 133 | 134 | return mt, nil 135 | } 136 | 137 | // locateFile opens a local file for symbolization on the search path 138 | // at $PPROF_BINARY_PATH. Looks inside these directories for files 139 | // named $BUILDID/$BASENAME and $BASENAME (if build id is available). 140 | func locateFile(obj plugin.ObjTool, file, buildID string, start uint64) (plugin.ObjFile, error) { 141 | // Construct search path to examine 142 | searchPath := os.Getenv("PPROF_BINARY_PATH") 143 | if searchPath == "" { 144 | // Use $HOME/pprof/binaries as default directory for local symbolization binaries 145 | searchPath = filepath.Join(os.Getenv("HOME"), "pprof", "binaries") 146 | } 147 | 148 | // Collect names to search: {buildid/basename, basename} 149 | var fileNames []string 150 | if baseName := filepath.Base(file); buildID != "" { 151 | fileNames = []string{filepath.Join(buildID, baseName), baseName} 152 | } else { 153 | fileNames = []string{baseName} 154 | } 155 | for _, path := range filepath.SplitList(searchPath) { 156 | for nameIndex, name := range fileNames { 157 | file := filepath.Join(path, name) 158 | if f, err := obj.Open(file, start); err == nil { 159 | fileBuildID := f.BuildID() 160 | if buildID == "" || buildID == fileBuildID { 161 | return f, nil 162 | } 163 | f.Close() 164 | if nameIndex == 0 { 165 | // If this is the first name, the path includes the build id. Report inconsistency. 166 | return nil, fmt.Errorf("found file %s with inconsistent build id %s", file, fileBuildID) 167 | } 168 | } 169 | } 170 | } 171 | // Try original file name 172 | f, err := obj.Open(file, start) 173 | if err == nil && buildID != "" { 174 | if fileBuildID := f.BuildID(); fileBuildID != "" && fileBuildID != buildID { 175 | // Mismatched build IDs, ignore 176 | f.Close() 177 | return nil, fmt.Errorf("mismatched build ids %s != %s", fileBuildID, buildID) 178 | } 179 | } 180 | return f, err 181 | } 182 | 183 | // mappingTable contains the mechanisms for symbolization of a 184 | // profile. 185 | type mappingTable struct { 186 | prof *profile.Profile 187 | segments map[*profile.Mapping]plugin.ObjFile 188 | } 189 | 190 | // Close releases any external processes being used for the mapping. 191 | func (mt *mappingTable) close() { 192 | for _, segment := range mt.segments { 193 | segment.Close() 194 | } 195 | } 196 | -------------------------------------------------------------------------------- /internal/symbolz/symbolz.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package symbolz symbolizes a profile using the output from the symbolz 6 | // service. 7 | package symbolz 8 | 9 | import ( 10 | "bytes" 11 | "fmt" 12 | "io" 13 | "regexp" 14 | "strconv" 15 | "strings" 16 | 17 | "github.com/rakyll/gom/internal/profile" 18 | ) 19 | 20 | var ( 21 | symbolzRE = regexp.MustCompile(`(0x[[:xdigit:]]+)\s+(.*)`) 22 | ) 23 | 24 | // Symbolize symbolizes profile p by parsing data returned by a 25 | // symbolz handler. syms receives the symbolz query (hex addresses 26 | // separated by '+') and returns the symbolz output in a string. It 27 | // symbolizes all locations based on their addresses, regardless of 28 | // mapping. 29 | func Symbolize(source string, syms func(string, string) ([]byte, error), p *profile.Profile) error { 30 | if source == "" { 31 | // If the source is not a recognizable URL, do nothing. 32 | return nil 33 | } 34 | 35 | // Construct query of addresses to symbolize. 36 | var a []string 37 | for _, l := range p.Location { 38 | if l.Address != 0 && len(l.Line) == 0 { 39 | a = append(a, fmt.Sprintf("%#x", l.Address)) 40 | } 41 | } 42 | 43 | if len(a) == 0 { 44 | // No addresses to symbolize. 45 | return nil 46 | } 47 | lines := make(map[uint64]profile.Line) 48 | functions := make(map[string]*profile.Function) 49 | if b, err := syms(source, strings.Join(a, "+")); err == nil { 50 | buf := bytes.NewBuffer(b) 51 | for { 52 | l, err := buf.ReadString('\n') 53 | 54 | if err != nil { 55 | if err == io.EOF { 56 | break 57 | } 58 | return err 59 | } 60 | 61 | if symbol := symbolzRE.FindStringSubmatch(l); len(symbol) == 3 { 62 | addr, err := strconv.ParseUint(symbol[1], 0, 64) 63 | if err != nil { 64 | return fmt.Errorf("unexpected parse failure %s: %v", symbol[1], err) 65 | } 66 | 67 | name := symbol[2] 68 | fn := functions[name] 69 | if fn == nil { 70 | fn = &profile.Function{ 71 | ID: uint64(len(p.Function) + 1), 72 | Name: name, 73 | SystemName: name, 74 | } 75 | functions[name] = fn 76 | p.Function = append(p.Function, fn) 77 | } 78 | 79 | lines[addr] = profile.Line{Function: fn} 80 | } 81 | } 82 | } 83 | 84 | for _, l := range p.Location { 85 | if line, ok := lines[l.Address]; ok { 86 | l.Line = []profile.Line{line} 87 | if l.Mapping != nil { 88 | l.Mapping.HasFunctions = true 89 | } 90 | } 91 | } 92 | 93 | return nil 94 | } 95 | -------------------------------------------------------------------------------- /internal/tempfile/tempfile.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package tempfile provides tools to create and delete temporary files 6 | package tempfile 7 | 8 | import ( 9 | "fmt" 10 | "os" 11 | "path/filepath" 12 | "sync" 13 | ) 14 | 15 | // New returns an unused filename for output files. 16 | func New(dir, prefix, suffix string) (*os.File, error) { 17 | for index := 1; index < 10000; index++ { 18 | path := filepath.Join(dir, fmt.Sprintf("%s%03d%s", prefix, index, suffix)) 19 | if _, err := os.Stat(path); err != nil { 20 | return os.Create(path) 21 | } 22 | } 23 | // Give up 24 | return nil, fmt.Errorf("could not create file of the form %s%03d%s", prefix, 1, suffix) 25 | } 26 | 27 | var tempFiles []string 28 | var tempFilesMu = sync.Mutex{} 29 | 30 | // DeferDelete marks a file to be deleted by next call to Cleanup() 31 | func DeferDelete(path string) { 32 | tempFilesMu.Lock() 33 | tempFiles = append(tempFiles, path) 34 | tempFilesMu.Unlock() 35 | } 36 | 37 | // Cleanup removes any temporary files selected for deferred cleaning. 38 | func Cleanup() { 39 | tempFilesMu.Lock() 40 | for _, f := range tempFiles { 41 | os.Remove(f) 42 | } 43 | tempFiles = nil 44 | tempFilesMu.Unlock() 45 | } 46 | --------------------------------------------------------------------------------