├── .gitignore ├── .travis.yml ├── LICENSE ├── NOTICE ├── README.md ├── build.properties ├── build.xml ├── callgraph.png ├── cmd ├── analyze-callgrind.go ├── analyze-xhprof.go ├── commands.go ├── compare-callgrind.go ├── compare-xhprof.go ├── generate-xhprof-diff-graphviz.go ├── generate-xhprof-graphviz.go └── utils.go ├── tests ├── data │ ├── cachegrind.out │ ├── cachet.xhprof │ ├── oxid.xhprof │ ├── wp-index.xhprof │ ├── wp-index2.xhprof │ └── wp-post.xhprof └── integration_test.go ├── toolkit.go └── xhprof ├── call.go ├── call_test.go ├── callgrind.go ├── callgrind_test.go ├── file.go ├── file_test.go ├── graphviz.go ├── paircall.go ├── paircall_test.go ├── profile.go ├── profile_test.go └── testdata ├── callgrind-simple.out └── simple.xhprof /.gitignore: -------------------------------------------------------------------------------- 1 | toolkit 2 | tk 3 | build/ 4 | toolkit_* 5 | callgraph.dot 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.8 5 | - 1.9 6 | 7 | before_script: 8 | - go get -t ./... 9 | 10 | script: go test -v ./ ./xhprof/ 11 | 12 | notifications: 13 | email: false 14 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Tideways Toolkit (tk) 2 | Copyright (c) 2018 Tideways GmbH 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tideways Toolkit (tk) 2 | 3 | The Tideways Toolkit (tk) is a collection of commandline tools to interact with 4 | PHP and perform various debugging, profiling and introspection jobs by 5 | interacting with PHP or with debugging extensions for PHP. 6 | 7 | ## Installing 8 | 9 | We recommend installing Tideways Toolkit by using one of the binary downloads 10 | provided on each tagged release: 11 | 12 | https://github.com/tideways/toolkit/releases 13 | 14 | If you want to build from source, Tideways Toolkit is written in Go and you can 15 | install it with the Go compiler 16 | 17 | go get github.com/tideways/toolkit 18 | 19 | This requires a `$GOPATH` to be setup as environment variable for your user ([docs](https://github.com/golang/go/wiki/GOPATH)). 20 | If you don't have this, select something like `/home/$USER/code/golang` and create 21 | this directory, putting the environment varible into `.bashrc`: 22 | 23 | export GOPATH="/home/$USER/code/golang" 24 | 25 | You will then find the compiled binary in `/home/$USER/code/golang/bin/toolkit` and can copy 26 | or symlink it to `/usr/local/bin/tk`. 27 | 28 | ## Tools 29 | 30 | ### analyze-xhprof - Parse and view JSON-serialized XHProf dumps 31 | 32 | XHProf data format can be viewed in various Web-based viewers, but often times 33 | a simple CLI view is all that you need and `analyze-xhprof` provides just that. 34 | 35 | $ tk analyze-xhprof filepath 36 | 37 | Getting this data requires the [tideways_xhprof](https://github.com/tideways/php-profiler-extension) PHP extension 38 | and some instrumentation code: 39 | 40 | ```php 41 | = 0 69 MS) | 95 | +-----------------------------+-------+-----------+------------------------------+ 96 | | mysqli_query | 25 | 6.93 ms | 6.93 ms | 97 | | preg_replace | 914 | 2.15 ms | 2.15 ms | 98 | | main() | 1 | 60.57 ms | 1.90 ms | 99 | | get_option | 363 | 10.54 ms | 1.74 ms | 100 | | translate | 1302 | 3.46 ms | 1.46 ms | 101 | | WP_Hook::apply_filters | 106 | 30.27 ms | 1.05 ms | 102 | | get_translations_for_domain | 1739 | 1.64 ms | 1.04 ms | 103 | | apply_filters | 1699 | 8.07 ms | 0.99 ms | 104 | | WP_Hook::add_filter | 590 | 1.71 ms | 0.97 ms | 105 | | WP_Object_Cache::get | 842 | 1.25 ms | 0.81 ms | 106 | | apply_filters@1 | 1561 | 1.49 ms | 0.75 ms | 107 | | preg_match | 541 | 0.74 ms | 0.74 ms | 108 | | __ | 1290 | 4.11 ms | 0.74 ms | 109 | | add_filter | 590 | 2.41 ms | 0.70 ms | 110 | +-----------------------------+-------+-----------+------------------------------+ 111 | ``` 112 | 113 | ## compare-xhprof - Compare performance of two traces 114 | 115 | To compare if changes made to the code base had a positive or negative effect 116 | you can use this command to compare two profiles. If you are using averaging 117 | and then writing the result to an outfile with `analyze-xhprof` then you can even compare 118 | averaged profiles including multiple requests with each other. 119 | 120 | $ tk compare-xhprof file1 file2 121 | 122 | ## generate-xhprof-graphviz - Convert profile to graphviz for rendering 123 | 124 | If you want to render an image with the callgraph, then the best way for this 125 | is to convert it into graphviz file format. 126 | 127 | $ tk generate-xhprof-graphviz file 128 | 129 | ``` 130 | Usage: 131 | tk generate-xhprof-graphviz filepaths... [flags] 132 | 133 | Flags: 134 | --critical-path If present, the critical path will be highlighted 135 | -f, --function string If provided, the graph will be generated only for functions directly related to this one 136 | -h, --help help for generate-xhprof-graphviz 137 | -o, --out-file string The path to store the resulting graph 138 | -t, --threshold float32 Display items having greater ratio of excl_wt (default 1%) with respect to main() (default 1) 139 | ``` 140 | 141 | Or to make a graph of two compared profiles: 142 | 143 | ``` 144 | Usage: 145 | tk generate-xhprof-diff-graphviz filepaths... [flags] 146 | 147 | Flags: 148 | -h, --help help for generate-xhprof-diff-graphviz 149 | -o, --out-file string The path to store the resulting graph (default "callgraph.dot") 150 | -t, --threshold float32 Display items having greater ratio of excl_wt (default 1%) with respect to main() (default 1) 151 | ``` 152 | 153 | To convert the output to a viewable image install the `graphviz` package that includes the `dot` 154 | command and then run it: 155 | 156 | $ dot -Tpng callgraph.dot > callgraph.png 157 | 158 | A sample callgraph with the testdata in this repository: 159 | 160 | ![](https://github.com/tideways/toolkit/blob/master/callgraph.png) 161 | -------------------------------------------------------------------------------- /build.properties: -------------------------------------------------------------------------------- 1 | build.version=0.2.0 2 | -------------------------------------------------------------------------------- /build.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | -------------------------------------------------------------------------------- /callgraph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tideways/toolkit/5c0f65c62f5c1bb666286705c34fe351ce298cad/callgraph.png -------------------------------------------------------------------------------- /cmd/analyze-callgrind.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/tideways/toolkit/xhprof" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func init() { 13 | RootCmd.AddCommand(analyzeCallgrindCmd) 14 | analyzeCallgrindCmd.Flags().StringVarP(&field, "dimension", "d", "excl_wt", "Dimension to view/sort (wt, excl_wt)") 15 | analyzeCallgrindCmd.Flags().Float32VarP(&minPercent, "min", "m", 1, "Display items having minimum percentage (default 1%) of --dimension, with respect to max value") 16 | } 17 | 18 | var analyzeCallgrindCmd = &cobra.Command{ 19 | Use: "analyze-callgrind filepaths...", 20 | Short: "Parse the output of callgrind outputs into a sorted tabular output.", 21 | Long: `Parse the output of callgrind outputs into a sorted tabular output.`, 22 | Args: cobra.MinimumNArgs(1), 23 | RunE: analyzeCallgrind, 24 | } 25 | 26 | func analyzeCallgrind(cmd *cobra.Command, args []string) error { 27 | maps := make([]*xhprof.PairCallMap, 0, len(args)) 28 | for _, arg := range args { 29 | f := xhprof.NewFile(arg, "callgrind") 30 | m, err := f.GetPairCallMap() 31 | if err != nil { 32 | return err 33 | } 34 | 35 | maps = append(maps, m) 36 | } 37 | 38 | avgMap := xhprof.AvgPairCallMaps(maps) 39 | profile := avgMap.Flatten() 40 | 41 | fieldInfo, ok := fieldsMap[field] 42 | if !ok { 43 | fmt.Printf("Provided field (%s) is not valid, defaulting to excl_wt\n", field) 44 | field = "excl_wt" 45 | fieldInfo = fieldsMap[field] 46 | } 47 | 48 | profile.SortBy(fieldInfo.Name) 49 | 50 | // Change default to 10 for exclusive fields, only when user 51 | // hasn't manually provided 1% 52 | if strings.HasPrefix(field, "excl_") && !cmd.Flags().Changed("min") { 53 | minPercent = float32(10) 54 | } 55 | minPercent = minPercent / 100.0 56 | minValue := minPercent * profile.Calls[0].GetFloat32Field(fieldInfo.Name) 57 | profile = profile.SelectGreater(fieldInfo.Name, minValue) 58 | err := renderProfile(profile, field, fieldInfo, minValue) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /cmd/analyze-xhprof.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/tideways/toolkit/xhprof" 9 | 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | func init() { 14 | RootCmd.AddCommand(xhprofCmd) 15 | xhprofCmd.Flags().StringVarP(&field, "dimension", "d", "excl_wt", "Dimension to view/sort (wt, excl_wt, cpu, excl_cpu, memory, excl_memory, io, excl_io, num_alloc, num_free, alloc_amt)") 16 | xhprofCmd.Flags().Float32VarP(&minPercent, "min", "m", 1, "Display items having minimum percentage (default 1% for inclusive, and 10% for exclusive dimensions) of --dimension, with respect to max value") 17 | xhprofCmd.Flags().StringVarP(&outFile, "out-file", "o", "", "If provided, the path to store the resulting profile (e.g. after averaging)") 18 | xhprofCmd.Flags().StringVarP(&function, "function", "", "", "If provided, one table for parents, and one for children of this function will be displayed") 19 | } 20 | 21 | var ( 22 | field string 23 | minPercent float32 24 | outFile string 25 | function string 26 | ) 27 | 28 | var xhprofCmd = &cobra.Command{ 29 | Use: "analyze-xhprof filepaths...", 30 | Short: "Parse the output of JSON serialized XHProf outputs into a sorted tabular output.", 31 | Long: `Parse the output of JSON serialized XHProf outputs into a sorted tabular output.`, 32 | Args: cobra.MinimumNArgs(1), 33 | RunE: analyzeXhprof, 34 | } 35 | 36 | func analyzeXhprof(cmd *cobra.Command, args []string) error { 37 | maps := make([]*xhprof.PairCallMap, 0, len(args)) 38 | for _, arg := range args { 39 | f := xhprof.NewFile(arg, "xhprof") 40 | m, err := f.GetPairCallMap() 41 | if err != nil { 42 | return err 43 | } 44 | 45 | maps = append(maps, m) 46 | } 47 | 48 | avgMap := xhprof.AvgPairCallMaps(maps) 49 | if outFile != "" { 50 | fmt.Printf("Writing profile to %s\n", outFile) 51 | f := xhprof.NewFile(outFile, "xhprof") 52 | err := f.WritePairCallMap(avgMap) 53 | if err != nil { 54 | return err 55 | } 56 | } 57 | 58 | profile := avgMap.Flatten() 59 | 60 | // Change default to 10 for exclusive fields, only when user 61 | // hasn't manually provided 1% 62 | if strings.HasPrefix(field, "excl_") && !cmd.Flags().Changed("min") { 63 | minPercent = float32(10) 64 | } 65 | minPercent = minPercent / 100.0 66 | 67 | if function == "" { 68 | fieldInfo, ok := fieldsMap[field] 69 | if !ok { 70 | fmt.Printf("Provided field (%s) is not valid, defaulting to excl_wt\n", field) 71 | field = "excl_wt" 72 | fieldInfo = fieldsMap[field] 73 | } 74 | 75 | profile.SortBy(fieldInfo.Name) 76 | minValue := minPercent * profile.Calls[0].GetFloat32Field(fieldInfo.Name) 77 | profile = profile.SelectGreater(fieldInfo.Name, minValue) 78 | err := renderProfile(profile, field, fieldInfo, minValue) 79 | if err != nil { 80 | return err 81 | } 82 | } else { 83 | family := avgMap.ComputeNearestFamily(function) 84 | parentsProfile := family.Parents.Flatten() 85 | childrenProfile := family.Children.Flatten() 86 | 87 | field = "wt" 88 | fieldInfo := fieldsMap[field] 89 | minPercent = 0.1 90 | 91 | functionCall := profile.GetCall(function) 92 | if functionCall == nil { 93 | return errors.New("Profile doesn't contain function") 94 | } 95 | minValue := minPercent * functionCall.GetFloat32Field(fieldInfo.Name) 96 | profile.SortBy(fieldInfo.Name) 97 | profile = profile.SelectGreater(fieldInfo.Name, minValue) 98 | 99 | fmt.Printf("Parents of %s:\n", function) 100 | err := renderProfile(parentsProfile, field, fieldInfo, minValue) 101 | if err != nil { 102 | return err 103 | } 104 | 105 | fmt.Printf("Children of %s:\n", function) 106 | err = renderProfile(childrenProfile, field, fieldInfo, minValue) 107 | if err != nil { 108 | return err 109 | } 110 | } 111 | 112 | fmt.Printf("Looking for a Web UI and SQL Profiling Support? Try our SaaS: https://tideways.io\n") 113 | 114 | return nil 115 | } 116 | -------------------------------------------------------------------------------- /cmd/commands.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | var RootCmd = &cobra.Command{ 11 | Use: "tk", 12 | Short: "Tideways Toolkit is a collection of tools to interact with PHP", 13 | Long: `The Tideways Toolkit (tk) is a collection of commandline tools to interact with 14 | PHP and perform various debugging, profiling and introspection jobs by 15 | interacting with PHP or with debugging extensions for PHP. 16 | 17 | Are you looking for a production profiler for your team with Web UI, SQL and 18 | HTTP profiling, monitoring, exception tracking and more? 19 | 20 | Start a Tideways Profiler 30 days trial @ https://tideways.io`, 21 | } 22 | 23 | var version string 24 | 25 | func Execute(v string) { 26 | version = v 27 | 28 | if err := RootCmd.Execute(); err != nil { 29 | fmt.Println(err) 30 | if cerr, ok := err.(*CommandError); ok { 31 | os.Exit(cerr.ExitStatus()) 32 | } 33 | 34 | os.Exit(1) 35 | } 36 | } 37 | 38 | type CommandError struct { 39 | s string 40 | exitStatus int 41 | } 42 | 43 | func NewCommandError(exitStatus int, s string) *CommandError { 44 | err := new(CommandError) 45 | err.s = s 46 | err.exitStatus = exitStatus 47 | 48 | return err 49 | } 50 | 51 | func (err *CommandError) Error() string { 52 | return err.s 53 | } 54 | 55 | func (err *CommandError) ExitStatus() int { 56 | return err.exitStatus 57 | } 58 | -------------------------------------------------------------------------------- /cmd/compare-callgrind.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/tideways/toolkit/xhprof" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func init() { 10 | RootCmd.AddCommand(compareCallgrindCmd) 11 | compareCallgrindCmd.Flags().IntVarP(&limit, "limit", "n", 10, "Number of rows to display") 12 | } 13 | 14 | var compareCallgrindCmd = &cobra.Command{ 15 | Use: "compare-callgrind filepaths...", 16 | Short: "Compare two callgrind outputs and display them in a sorted table.", 17 | Long: `Compare two callgrind outputs and display them in a sorted table.`, 18 | Args: cobra.ExactArgs(2), 19 | RunE: compareCallgrind, 20 | } 21 | 22 | func compareCallgrind(cmd *cobra.Command, args []string) error { 23 | profiles := make([]*xhprof.Profile, 0, len(args)) 24 | for _, arg := range args { 25 | f := xhprof.NewFile(arg, "callgrind") 26 | profile, err := f.GetProfile() 27 | if err != nil { 28 | return err 29 | } 30 | 31 | profiles = append(profiles, profile) 32 | } 33 | 34 | diff := profiles[0].Subtract(profiles[1]) 35 | 36 | err := renderProfileDiff(diff, limit) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /cmd/compare-xhprof.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "github.com/tideways/toolkit/xhprof" 5 | 6 | "github.com/spf13/cobra" 7 | ) 8 | 9 | func init() { 10 | RootCmd.AddCommand(compareXhprofCmd) 11 | compareXhprofCmd.Flags().IntVarP(&limit, "limit", "n", 10, "Number of rows to display") 12 | } 13 | 14 | var ( 15 | limit int 16 | ) 17 | 18 | var compareXhprofCmd = &cobra.Command{ 19 | Use: "compare-xhprof filepaths...", 20 | Short: "Compare two JSON serialized XHProf outputs and display them in a sorted table.", 21 | Long: `Compare two JSON serialized XHProf outputs and display them in a sorted table.`, 22 | Args: cobra.ExactArgs(2), 23 | RunE: compareXhprof, 24 | } 25 | 26 | func compareXhprof(cmd *cobra.Command, args []string) error { 27 | profiles := make([]*xhprof.Profile, 0, len(args)) 28 | for _, arg := range args { 29 | f := xhprof.NewFile(arg, "xhprof") 30 | profile, err := f.GetProfile() 31 | if err != nil { 32 | return err 33 | } 34 | 35 | profiles = append(profiles, profile) 36 | } 37 | 38 | diff := profiles[0].Subtract(profiles[1]) 39 | 40 | err := renderProfileDiff(diff, limit) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /cmd/generate-xhprof-diff-graphviz.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | 7 | "github.com/tideways/toolkit/xhprof" 8 | 9 | "github.com/spf13/cobra" 10 | ) 11 | 12 | func init() { 13 | RootCmd.AddCommand(generateXhprofDiffGraphvizCmd) 14 | generateXhprofDiffGraphvizCmd.Flags().Float32VarP(&threshold, "threshold", "t", 1, "Display items having greater ratio of excl_wt (default 1%) with respect to main()") 15 | generateXhprofDiffGraphvizCmd.Flags().StringVarP(&outFile, "out-file", "o", "callgraph.dot", "The path to store the resulting graph") 16 | } 17 | 18 | var generateXhprofDiffGraphvizCmd = &cobra.Command{ 19 | Use: "generate-xhprof-diff-graphviz filepaths...", 20 | Short: "Parse the output of two JSON serialized XHProf outputs, and generate a dot script out of their diff.", 21 | Long: `Parse the output of two JSON serialized XHProf outputs, and generate a dot script out of their diff.`, 22 | Args: cobra.ExactArgs(2), 23 | RunE: generateXhprofDiffGraphviz, 24 | } 25 | 26 | func generateXhprofDiffGraphviz(cmd *cobra.Command, args []string) error { 27 | f := xhprof.NewFile(args[0], "xhprof") 28 | m1, err := f.GetPairCallMap() 29 | if err != nil { 30 | return err 31 | } 32 | 33 | f = xhprof.NewFile(args[1], "xhprof") 34 | m2, err := f.GetPairCallMap() 35 | if err != nil { 36 | return err 37 | } 38 | 39 | threshold /= 100 40 | dot, err := xhprof.GenerateDiffDotScript(m1, m2, threshold) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | err = ioutil.WriteFile(outFile, []byte(dot), 0755) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | fmt.Printf("Written callgraph to graphviz dotfile: %s", outFile) 51 | fmt.Printf("Looking for interactive Web-based Callgraph? Try our SaaS: https://tideways.io\n") 52 | 53 | return nil 54 | } 55 | -------------------------------------------------------------------------------- /cmd/generate-xhprof-graphviz.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "io/ioutil" 5 | 6 | "github.com/tideways/toolkit/xhprof" 7 | 8 | "github.com/spf13/cobra" 9 | ) 10 | 11 | func init() { 12 | RootCmd.AddCommand(generateXhprofGraphvizCmd) 13 | generateXhprofGraphvizCmd.Flags().Float32VarP(&threshold, "threshold", "t", 1, "Display items having greater ratio of excl_wt (default 1%) with respect to main()") 14 | generateXhprofGraphvizCmd.Flags().StringVarP(&function, "function", "f", "", "If provided, the graph will be generated only for functions directly related to this one") 15 | generateXhprofGraphvizCmd.Flags().BoolVarP(&criticalPath, "critical-path", "", false, "If present, the critical path will be highlighted") 16 | generateXhprofGraphvizCmd.Flags().StringVarP(&outFile, "out-file", "o", "", "The path to store the resulting graph") 17 | } 18 | 19 | var ( 20 | threshold float32 21 | criticalPath bool 22 | ) 23 | 24 | var generateXhprofGraphvizCmd = &cobra.Command{ 25 | Use: "generate-xhprof-graphviz filepaths...", 26 | Short: "Parse the output of JSON serialized XHProf outputs into a dot script for graphviz.", 27 | Long: `Parse the output of JSON serialized XHProf outputs into a dot script for graphviz.`, 28 | Args: cobra.MinimumNArgs(1), 29 | RunE: generateXhprofGraphviz, 30 | } 31 | 32 | func generateXhprofGraphviz(cmd *cobra.Command, args []string) error { 33 | maps := make([]*xhprof.PairCallMap, 0, len(args)) 34 | for _, arg := range args { 35 | f := xhprof.NewFile(arg, "xhprof") 36 | m, err := f.GetPairCallMap() 37 | if err != nil { 38 | return err 39 | } 40 | 41 | maps = append(maps, m) 42 | } 43 | 44 | avgMap := xhprof.AvgPairCallMaps(maps) 45 | 46 | threshold /= 100 47 | dot, err := xhprof.GenerateDotScript(avgMap, threshold, function, criticalPath, nil, nil) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | if len(outFile) == 0 { 53 | outFile = "callgraph.dot" 54 | } 55 | 56 | err = ioutil.WriteFile(outFile, []byte(dot), 0755) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /cmd/utils.go: -------------------------------------------------------------------------------- 1 | package cmd 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "strings" 7 | 8 | "github.com/tideways/toolkit/xhprof" 9 | 10 | "github.com/olekukonko/tablewriter" 11 | ) 12 | 13 | type Unit struct { 14 | Name string 15 | Divisor float32 16 | } 17 | 18 | var ( 19 | ms Unit = Unit{Name: "ms", Divisor: 1000.0} 20 | kb Unit = Unit{Name: "KB", Divisor: 1024.0} 21 | plain Unit = Unit{Name: "", Divisor: 1.0} 22 | ) 23 | 24 | type FieldInfo struct { 25 | Name string 26 | Label string 27 | Header string 28 | Unit Unit 29 | } 30 | 31 | var fieldsMap map[string]FieldInfo = map[string]FieldInfo{ 32 | "wt": FieldInfo{ 33 | Name: "WallTime", 34 | Label: "Inclusive Wall-Time", 35 | Header: "Wall-Time", 36 | Unit: ms, 37 | }, 38 | "excl_wt": FieldInfo{ 39 | Name: "ExclusiveWallTime", 40 | Label: "Exclusive Wall-Time", 41 | Header: "Wall-Time", 42 | Unit: ms, 43 | }, 44 | "cpu": FieldInfo{ 45 | Name: "CpuTime", 46 | Label: "Inclusive CPU-Time", 47 | Header: "CPU-Time", 48 | Unit: ms, 49 | }, 50 | "excl_cpu": FieldInfo{ 51 | Name: "ExclusiveCpuTime", 52 | Label: "Exclusive CPU-Time", 53 | Header: "CPU-Time", 54 | Unit: ms, 55 | }, 56 | "memory": FieldInfo{ 57 | Name: "Memory", 58 | Label: "Inclusive Memory", 59 | Header: "Memory", 60 | Unit: kb, 61 | }, 62 | "excl_memory": FieldInfo{ 63 | Name: "ExclusiveMemory", 64 | Label: "Exclusive Memory", 65 | Header: "Memory", 66 | Unit: kb, 67 | }, 68 | "io": FieldInfo{ 69 | Name: "IoTime", 70 | Label: "Inclusive I/O-Time", 71 | Header: "I/O-Time", 72 | Unit: ms, 73 | }, 74 | "excl_io": FieldInfo{ 75 | Name: "ExclusiveIoTime", 76 | Label: "Exclusive I/O-Time", 77 | Header: "I/O-Time", 78 | Unit: ms, 79 | }, 80 | "num_alloc": FieldInfo{ 81 | Name: "NumAlloc", 82 | Label: "Number of Allocations", 83 | Header: "Num. Alloc.", 84 | Unit: plain, 85 | }, 86 | "excl_num_alloc": FieldInfo{ 87 | Name: "ExclusiveNumAlloc", 88 | Label: "Exclusive Number of Allocations", 89 | Header: "Num. Alloc.", 90 | Unit: plain, 91 | }, 92 | "alloc_amt": FieldInfo{ 93 | Name: "AllocAmount", 94 | Label: "Amount of allocated Memory", 95 | Header: "Alloc. Amount", 96 | Unit: kb, 97 | }, 98 | "excl_alloc_amt": FieldInfo{ 99 | Name: "ExclusiveAllocAmount", 100 | Label: "Exclusive Amount of allocated Memory", 101 | Header: "Alloc. Amount", 102 | Unit: kb, 103 | }, 104 | "num_free": FieldInfo{ 105 | Name: "NumFree", 106 | Label: "Number of Frees", 107 | Header: "Num. Frees", 108 | Unit: plain, 109 | }, 110 | "excl_num_free": FieldInfo{ 111 | Name: "ExclusiveNumFree", 112 | Label: "Exclusive Number of Frees", 113 | Header: "Num. Frees", 114 | Unit: plain, 115 | }, 116 | } 117 | 118 | func renderProfile(profile *xhprof.Profile, field string, fieldInfo FieldInfo, minValue float32) error { 119 | header := fieldInfo.Header 120 | exclHeader := "Excl. " + fieldInfo.Header 121 | var fields []FieldInfo 122 | var headers []string 123 | if strings.HasPrefix(field, "excl_") { 124 | fields = []FieldInfo{fieldsMap[strings.TrimPrefix(field, "excl_")], fieldInfo} 125 | exclHeader = fmt.Sprintf("%s (>= %2.2f %s)", exclHeader, minValue/fieldInfo.Unit.Divisor, fieldInfo.Unit.Name) 126 | headers = []string{"Function", "Count", header, exclHeader} 127 | } else { 128 | fields = []FieldInfo{fieldInfo} 129 | header = fmt.Sprintf("%s (>= %2.2f %s)", header, minValue/fieldInfo.Unit.Divisor, fieldInfo.Unit.Name) 130 | headers = []string{"Function", "Count", header} 131 | } 132 | 133 | table := tablewriter.NewWriter(os.Stdout) 134 | table.SetHeader(headers) 135 | for _, call := range profile.Calls { 136 | table.Append(getRow(call, fields)) 137 | } 138 | 139 | fmt.Printf("Showing XHProf data by %s\n", fieldInfo.Label) 140 | table.Render() 141 | 142 | return nil 143 | } 144 | 145 | func getRow(call *xhprof.Call, fields []FieldInfo) []string { 146 | res := []string{ 147 | fmt.Sprintf("%.90s", call.Name), 148 | fmt.Sprintf("%d", call.Count), 149 | } 150 | 151 | for _, field := range fields { 152 | var col string 153 | if field.Unit == plain { 154 | col = fmt.Sprintf("%2.0f %s", call.GetFloat32Field(field.Name)/field.Unit.Divisor, field.Unit.Name) 155 | } else { 156 | col = fmt.Sprintf("%2.2f %s", call.GetFloat32Field(field.Name)/field.Unit.Divisor, field.Unit.Name) 157 | } 158 | 159 | res = append(res, col) 160 | } 161 | 162 | return res 163 | } 164 | 165 | func renderProfileDiff(diff *xhprof.ProfileDiff, limit int) error { 166 | diff.Sort() 167 | 168 | table := tablewriter.NewWriter(os.Stdout) 169 | table.SetHeader([]string{"Function", "Count", "Wall-Time", "Fraction Wall-Time From", "Fraction Wall-Time To"}) 170 | for i, call := range diff.Calls { 171 | if i >= limit { 172 | break 173 | } 174 | 175 | row := []string{ 176 | fmt.Sprintf("%.90s", call.Name), 177 | fmt.Sprintf("%d", call.Count), 178 | fmt.Sprintf("%2.2f ms", call.WallTime/1000), 179 | fmt.Sprintf("%2.2f", call.FractionWtFrom), 180 | fmt.Sprintf("%2.2f", call.FractionWtTo), 181 | } 182 | 183 | table.Append(row) 184 | } 185 | 186 | fmt.Printf("Showing XHProf data by the difference of fractions\n") 187 | table.Render() 188 | 189 | return nil 190 | } 191 | -------------------------------------------------------------------------------- /tests/integration_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/tideways/toolkit/xhprof" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestParseWPIndexXhprof(t *testing.T) { 13 | f := xhprof.NewFile("data/wp-index.xhprof", "xhprof") 14 | m, err := f.GetPairCallMap() 15 | require.Nil(t, err) 16 | require.IsType(t, m, new(xhprof.PairCallMap)) 17 | require.NotEmpty(t, m.M) 18 | 19 | assert.Equal(t, m.M["main()"].WallTime, float32(60572)) 20 | assert.Equal(t, m.M["main()"].Count, 1) 21 | assert.Equal(t, m.M["main()"].CpuTime, float32(54683)) 22 | assert.Equal(t, m.M["main()"].Memory, float32(2738112)) 23 | assert.Equal(t, m.M["main()"].PeakMemory, float32(2596544)) 24 | assert.Equal(t, m.M["wp_set_current_user==>setup_userdata"].WallTime, float32(74)) 25 | assert.Equal(t, m.M["wp_set_current_user==>setup_userdata"].Count, 1) 26 | assert.Equal(t, m.M["wp_set_current_user==>setup_userdata"].CpuTime, float32(74)) 27 | assert.Equal(t, m.M["wp_set_current_user==>setup_userdata"].Memory, float32(4408)) 28 | assert.Equal(t, m.M["wp_set_current_user==>setup_userdata"].PeakMemory, float32(328)) 29 | 30 | p := m.Flatten() 31 | require.IsType(t, p, new(xhprof.Profile)) 32 | require.NotEmpty(t, p.Calls) 33 | 34 | assert.Equal(t, p.GetMain().WallTime, float32(60572)) 35 | 36 | c := p.GetCall("is_search") 37 | require.IsType(t, c, new(xhprof.Call)) 38 | 39 | assert.Equal(t, c.Count, 5) 40 | assert.Equal(t, c.WallTime, float32(5)) 41 | assert.Equal(t, c.ExclusiveWallTime, float32(4)) 42 | assert.Equal(t, c.CpuTime, float32(5)) 43 | assert.Equal(t, c.ExclusiveCpuTime, float32(3)) 44 | assert.Equal(t, c.IoTime, float32(1)) 45 | assert.Equal(t, c.ExclusiveIoTime, float32(1)) 46 | assert.Equal(t, c.Memory, float32(672)) 47 | assert.Equal(t, c.ExclusiveMemory, float32(560)) 48 | 49 | c = p.GetCall("vsprintf") 50 | require.IsType(t, c, new(xhprof.Call)) 51 | 52 | assert.Equal(t, c.Count, 14) 53 | assert.Equal(t, c.WallTime, float32(18)) 54 | assert.Equal(t, c.ExclusiveWallTime, float32(18)) 55 | assert.Equal(t, c.CpuTime, float32(17)) 56 | assert.Equal(t, c.ExclusiveCpuTime, float32(17)) 57 | assert.Equal(t, c.IoTime, float32(2)) 58 | assert.Equal(t, c.ExclusiveIoTime, float32(2)) 59 | assert.Equal(t, c.Memory, float32(4704)) 60 | assert.Equal(t, c.ExclusiveMemory, float32(4704)) 61 | } 62 | 63 | func TestParseWPIndexCallgrind(t *testing.T) { 64 | expected := xhprof.Profile{ 65 | Calls: []*xhprof.Call{ 66 | &xhprof.Call{ 67 | Name: "main()", 68 | Count: 1, 69 | WallTime: 305039, 70 | ExclusiveWallTime: 54, 71 | IoTime: 305039, 72 | ExclusiveIoTime: 54, 73 | }, 74 | &xhprof.Call{ 75 | Name: "require::/var/www/wordpress/wp-blog-header.php", 76 | Count: 1, 77 | WallTime: 304981, 78 | ExclusiveWallTime: 86, 79 | IoTime: 304981, 80 | ExclusiveIoTime: 86, 81 | }, 82 | }, 83 | } 84 | 85 | f := xhprof.NewFile("data/cachegrind.out", "callgrind") 86 | profile, err := f.GetProfile() 87 | require.Nil(t, err) 88 | require.NotNil(t, profile) 89 | 90 | require.NotNil(t, profile.Main) 91 | assert.EqualValues(t, expected.Calls[0], profile.Main) 92 | 93 | for _, c := range profile.Calls { 94 | if c.Name == expected.Calls[1].Name { 95 | assert.EqualValues(t, expected.Calls[1], c) 96 | } 97 | } 98 | } 99 | 100 | func TestComputeNearestFamilyWPIndexXhprof(t *testing.T) { 101 | expected := &xhprof.NearestFamily{ 102 | Children: &xhprof.PairCallMap{ 103 | M: map[string]*xhprof.PairCall{}, 104 | }, 105 | Parents: &xhprof.PairCallMap{ 106 | M: map[string]*xhprof.PairCall{ 107 | "wpdb::prepare": &xhprof.PairCall{ 108 | WallTime: float32(17), 109 | Count: 11, 110 | }, 111 | "get_custom_header": &xhprof.PairCall{ 112 | WallTime: float32(1), 113 | Count: 3, 114 | }, 115 | }, 116 | }, 117 | ChildrenCount: 0, 118 | ParentsCount: 14, 119 | } 120 | 121 | f := xhprof.NewFile("data/wp-index.xhprof", "xhprof") 122 | m, err := f.GetPairCallMap() 123 | require.Nil(t, err) 124 | require.IsType(t, m, new(xhprof.PairCallMap)) 125 | require.NotEmpty(t, m.M) 126 | 127 | family := m.ComputeNearestFamily("vsprintf") 128 | require.IsType(t, family, new(xhprof.NearestFamily)) 129 | assert.EqualValues(t, expected, family) 130 | } 131 | -------------------------------------------------------------------------------- /toolkit.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/tideways/toolkit/cmd" 5 | ) 6 | 7 | const VERSION = "0.1.0" 8 | 9 | func main() { 10 | cmd.Execute(VERSION) 11 | } 12 | -------------------------------------------------------------------------------- /xhprof/call.go: -------------------------------------------------------------------------------- 1 | package xhprof 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | type Call struct { 8 | Name string 9 | Count int 10 | WallTime float32 11 | CpuTime float32 12 | IoTime float32 13 | Memory float32 14 | PeakMemory float32 15 | ExclusiveWallTime float32 16 | ExclusiveCpuTime float32 17 | ExclusiveMemory float32 18 | ExclusiveIoTime float32 19 | NumAlloc float32 20 | ExclusiveNumAlloc float32 21 | NumFree float32 22 | ExclusiveNumFree float32 23 | AllocAmount float32 24 | ExclusiveAllocAmount float32 25 | 26 | graphvizId int 27 | } 28 | 29 | func (c *Call) GetFloat32Field(field string) float32 { 30 | cVal := reflect.Indirect(reflect.ValueOf(c)) 31 | return float32(cVal.FieldByName(field).Float()) 32 | } 33 | 34 | func (c *Call) Add(o *Call) *Call { 35 | c.Count += o.Count 36 | c.WallTime += o.WallTime 37 | c.ExclusiveWallTime += o.ExclusiveWallTime 38 | c.CpuTime += o.CpuTime 39 | c.ExclusiveCpuTime += o.ExclusiveCpuTime 40 | c.Memory += o.Memory 41 | c.PeakMemory += o.PeakMemory 42 | c.ExclusiveMemory += o.ExclusiveMemory 43 | c.IoTime += o.IoTime 44 | c.ExclusiveIoTime += o.ExclusiveIoTime 45 | c.NumAlloc += o.NumAlloc 46 | c.NumFree += o.NumFree 47 | c.AllocAmount += o.AllocAmount 48 | c.ExclusiveNumAlloc += o.ExclusiveNumAlloc 49 | c.ExclusiveNumFree += o.ExclusiveNumFree 50 | c.ExclusiveAllocAmount += o.ExclusiveAllocAmount 51 | 52 | return c 53 | } 54 | 55 | func (c *Call) AddPairCall(p *PairCall) *Call { 56 | c.Count += p.Count 57 | c.WallTime += p.WallTime 58 | c.ExclusiveWallTime += p.WallTime 59 | c.CpuTime += p.CpuTime 60 | c.ExclusiveCpuTime += p.CpuTime 61 | 62 | io := p.WallTime - p.CpuTime 63 | if io < 0 { 64 | io = 0 65 | } 66 | 67 | c.IoTime += io 68 | c.ExclusiveIoTime += io 69 | 70 | c.Memory += p.Memory 71 | c.PeakMemory += p.PeakMemory 72 | c.ExclusiveMemory += p.Memory 73 | c.NumAlloc += p.NumAlloc 74 | c.NumFree += p.NumFree 75 | c.AllocAmount += p.AllocAmount 76 | c.ExclusiveNumAlloc += p.NumAlloc 77 | c.ExclusiveNumFree += p.NumFree 78 | c.ExclusiveAllocAmount += p.AllocAmount 79 | return c 80 | } 81 | 82 | func (c *Call) SubtractExcl(p *PairCall) *Call { 83 | c.ExclusiveWallTime -= p.WallTime 84 | c.ExclusiveCpuTime -= p.CpuTime 85 | c.ExclusiveMemory -= p.Memory 86 | 87 | c.ExclusiveNumAlloc -= p.NumAlloc 88 | c.ExclusiveNumFree -= p.NumFree 89 | c.ExclusiveAllocAmount -= p.AllocAmount 90 | 91 | io := p.WallTime - p.CpuTime 92 | if io < 0 { 93 | io = 0 94 | } 95 | 96 | c.ExclusiveIoTime -= io 97 | 98 | return c 99 | } 100 | 101 | func (c *Call) Divide(d float32) *Call { 102 | c.Count /= int(d) 103 | c.WallTime /= d 104 | c.ExclusiveWallTime /= d 105 | c.CpuTime /= d 106 | c.ExclusiveCpuTime /= d 107 | c.Memory /= d 108 | c.PeakMemory /= d 109 | c.ExclusiveMemory /= d 110 | c.IoTime /= d 111 | c.ExclusiveIoTime /= d 112 | c.NumAlloc /= d 113 | c.NumFree /= d 114 | c.AllocAmount /= d 115 | c.ExclusiveNumAlloc /= d 116 | c.ExclusiveNumFree /= d 117 | c.ExclusiveAllocAmount /= d 118 | 119 | return c 120 | } 121 | 122 | type CallDiff struct { 123 | Name string 124 | WallTime float32 125 | Count int 126 | FractionWtFrom float32 127 | FractionWtTo float32 128 | } 129 | -------------------------------------------------------------------------------- /xhprof/call_test.go: -------------------------------------------------------------------------------- 1 | package xhprof 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestAdd(t *testing.T) { 10 | expected := &Call{ 11 | Count: 7, 12 | WallTime: 1000, 13 | ExclusiveWallTime: 600, 14 | CpuTime: 500, 15 | ExclusiveCpuTime: 300, 16 | IoTime: 500, 17 | ExclusiveIoTime: 300, 18 | Memory: 1024, 19 | ExclusiveMemory: 512, 20 | } 21 | 22 | c1 := &Call{ 23 | Count: 2, 24 | WallTime: 300, 25 | ExclusiveWallTime: 200, 26 | CpuTime: 100, 27 | ExclusiveCpuTime: 50, 28 | IoTime: 200, 29 | ExclusiveIoTime: 150, 30 | Memory: 256, 31 | ExclusiveMemory: 128, 32 | } 33 | 34 | c2 := &Call{ 35 | Count: 3, 36 | WallTime: 400, 37 | ExclusiveWallTime: 200, 38 | CpuTime: 200, 39 | ExclusiveCpuTime: 150, 40 | IoTime: 200, 41 | ExclusiveIoTime: 50, 42 | Memory: 256, 43 | ExclusiveMemory: 128, 44 | } 45 | 46 | c3 := &Call{ 47 | Count: 2, 48 | WallTime: 300, 49 | ExclusiveWallTime: 200, 50 | CpuTime: 200, 51 | ExclusiveCpuTime: 100, 52 | IoTime: 100, 53 | ExclusiveIoTime: 100, 54 | Memory: 512, 55 | ExclusiveMemory: 256, 56 | } 57 | 58 | c1.Add(c2).Add(c3) 59 | 60 | assert.EqualValues(t, expected, c1) 61 | } 62 | 63 | func TestAddPairCall(t *testing.T) { 64 | expected := &Call{ 65 | Count: 5, 66 | WallTime: 700, 67 | ExclusiveWallTime: 600, 68 | CpuTime: 300, 69 | ExclusiveCpuTime: 250, 70 | IoTime: 400, 71 | ExclusiveIoTime: 350, 72 | Memory: 512, 73 | PeakMemory: 300, 74 | ExclusiveMemory: 384, 75 | } 76 | 77 | c := &Call{ 78 | Count: 2, 79 | WallTime: 300, 80 | ExclusiveWallTime: 200, 81 | CpuTime: 100, 82 | ExclusiveCpuTime: 50, 83 | IoTime: 200, 84 | ExclusiveIoTime: 150, 85 | Memory: 256, 86 | ExclusiveMemory: 128, 87 | } 88 | 89 | p := &PairCall{ 90 | Count: 3, 91 | WallTime: 400, 92 | CpuTime: 200, 93 | Memory: 256, 94 | PeakMemory: 300, 95 | } 96 | 97 | c.AddPairCall(p) 98 | 99 | assert.EqualValues(t, expected, c) 100 | } 101 | 102 | func TestSubtractExcl(t *testing.T) { 103 | expected := &Call{ 104 | Count: 4, 105 | WallTime: 500, 106 | ExclusiveWallTime: 200, 107 | CpuTime: 200, 108 | ExclusiveCpuTime: 100, 109 | IoTime: 300, 110 | ExclusiveIoTime: 100, 111 | Memory: 512, 112 | ExclusiveMemory: 128, 113 | } 114 | 115 | c := &Call{ 116 | Count: 4, 117 | WallTime: 500, 118 | ExclusiveWallTime: 300, 119 | CpuTime: 200, 120 | ExclusiveCpuTime: 150, 121 | IoTime: 300, 122 | ExclusiveIoTime: 150, 123 | Memory: 512, 124 | ExclusiveMemory: 256, 125 | } 126 | 127 | p := &PairCall{ 128 | Count: 1, 129 | WallTime: 100, 130 | CpuTime: 50, 131 | Memory: 128, 132 | } 133 | 134 | c.SubtractExcl(p) 135 | 136 | assert.EqualValues(t, expected, c) 137 | } 138 | 139 | func TestDivide(t *testing.T) { 140 | expected := &Call{ 141 | Count: 3, 142 | WallTime: 900, 143 | ExclusiveWallTime: 690, 144 | CpuTime: 400, 145 | ExclusiveCpuTime: 300, 146 | IoTime: 500, 147 | ExclusiveIoTime: 390, 148 | Memory: 1024, 149 | ExclusiveMemory: 512, 150 | } 151 | 152 | c1 := &Call{ 153 | Count: 10, 154 | WallTime: 2700, 155 | ExclusiveWallTime: 2070, 156 | CpuTime: 1200, 157 | ExclusiveCpuTime: 900, 158 | IoTime: 1500, 159 | ExclusiveIoTime: 1170, 160 | Memory: 3072, 161 | ExclusiveMemory: 1536, 162 | } 163 | 164 | c1.Divide(3) 165 | 166 | assert.Equal(t, expected, c1) 167 | } 168 | -------------------------------------------------------------------------------- /xhprof/callgrind.go: -------------------------------------------------------------------------------- 1 | package xhprof 2 | 3 | import ( 4 | "bufio" 5 | "errors" 6 | "io" 7 | "regexp" 8 | "strconv" 9 | "strings" 10 | ) 11 | 12 | var ( 13 | formatSpecPattern = regexp.MustCompile(`^# callgrind format$`) 14 | formatVersionPattern = regexp.MustCompile(`^version: 1$`) 15 | creatorPattern = regexp.MustCompile(`^creator: .*$`) 16 | headerPattern = regexp.MustCompile(`^(\w+):\s*([[:graph:]]+)$`) 17 | costsPattern = regexp.MustCompile(`^(?:\d+\s*)+$`) 18 | positionPattern = regexp.MustCompile(`^(fl|fi|fn|cfi|cfl|cfn)=\s*(?:\((\d+)\))?\s*([[:graph:]]*)?$`) 19 | callsPattern = regexp.MustCompile(`^calls=\s*(\d+)\s*(\d+\s*)+$`) 20 | emptyPattern = regexp.MustCompile(`^\s*$`) 21 | ) 22 | 23 | func ParseCallgrind(rd io.Reader) (*PairCallMap, error) { 24 | p := NewCallgrindParser(rd) 25 | return p.parseFile() 26 | } 27 | 28 | type CallgrindParser struct { 29 | scanner *bufio.Scanner 30 | headers map[string]string 31 | positions map[string]string 32 | pcMap *PairCallMap 33 | lastFn string 34 | lastCfn string 35 | } 36 | 37 | func NewCallgrindParser(rd io.Reader) *CallgrindParser { 38 | p := new(CallgrindParser) 39 | p.scanner = bufio.NewScanner(rd) 40 | p.headers = make(map[string]string) 41 | p.positions = make(map[string]string) 42 | p.pcMap = NewPairCallMap() 43 | p.pcMap.NewPairCall("main()") 44 | p.pcMap.M["main()"].Count = 1 45 | 46 | return p 47 | } 48 | 49 | func (p *CallgrindParser) setHeader(k, v string) { 50 | p.headers[k] = v 51 | } 52 | 53 | func (p *CallgrindParser) getOrSetPosName(kind, num, posName string) (name string, err error) { 54 | name = posName 55 | if num == "" && name == "" { 56 | err = errors.New("A position must be defined either with a name or a reference number") 57 | return 58 | } 59 | 60 | if name == "" { 61 | var ok bool 62 | name, ok = p.positions["fn:"+num] 63 | if !ok { 64 | err = errors.New("Position referenced without being defined") 65 | } 66 | } else { 67 | if name == "{main}" { 68 | name = "main()" 69 | } 70 | 71 | p.positions["fn:"+num] = name 72 | } 73 | 74 | return 75 | } 76 | 77 | func (p *CallgrindParser) readLine() (text string, eof bool, err error) { 78 | ok := p.scanner.Scan() 79 | for ok { 80 | text = p.scanner.Text() 81 | if !emptyPattern.MatchString(text) { 82 | break 83 | } 84 | 85 | ok = p.scanner.Scan() 86 | } 87 | 88 | err = p.scanner.Err() 89 | if !ok { 90 | eof = true 91 | } 92 | 93 | return 94 | } 95 | 96 | func (p *CallgrindParser) parseFile() (pcMap *PairCallMap, err error) { 97 | var text string 98 | var eof bool 99 | text, eof, err = p.readLine() 100 | if eof || err != nil { 101 | return 102 | } 103 | 104 | if formatSpecPattern.MatchString(text) { 105 | text, eof, err = p.readLine() 106 | if eof || err != nil { 107 | return 108 | } 109 | } 110 | 111 | if formatVersionPattern.MatchString(text) { 112 | text, eof, err = p.readLine() 113 | if eof || err != nil { 114 | return 115 | } 116 | } 117 | 118 | if creatorPattern.MatchString(text) { 119 | text, eof, err = p.readLine() 120 | if eof || err != nil { 121 | return 122 | } 123 | } 124 | 125 | err = p.parsePartData() 126 | if err != nil { 127 | return 128 | } 129 | 130 | if sum, ok := p.headers["summary"]; ok && p.pcMap.M["main()"].WallTime == 0 { 131 | var wt float64 132 | wt, err = strconv.ParseFloat(sum, 32) 133 | if err != nil { 134 | return 135 | } 136 | 137 | p.pcMap.M["main()"].WallTime = float32(wt) 138 | } 139 | 140 | pcMap = p.pcMap 141 | 142 | return 143 | } 144 | 145 | func (p *CallgrindParser) parsePartData() (err error) { 146 | eof := false 147 | text := p.scanner.Text() 148 | for !eof && err == nil { 149 | if headerPattern.MatchString(text) { 150 | err = p.parseHeader() 151 | } else if positionPattern.MatchString(text) { 152 | err = p.parsePosition() 153 | } else if callsPattern.MatchString(text) { 154 | err = p.parseCalls() 155 | } else if costsPattern.MatchString(text) { 156 | err = p.parseCosts(false) 157 | } else { 158 | err = errors.New("PartData is not valid: " + text) 159 | } 160 | 161 | if err != nil { 162 | break 163 | } 164 | 165 | text, eof, err = p.readLine() 166 | } 167 | 168 | return 169 | } 170 | 171 | func (p *CallgrindParser) parseHeader() (err error) { 172 | text := p.scanner.Text() 173 | submatches := headerPattern.FindStringSubmatch(text) 174 | k := strings.TrimSpace(submatches[1]) 175 | v := strings.TrimSpace(submatches[2]) 176 | 177 | if k == "events" && v != "Time" { 178 | err = errors.New("Only Time event is supported") 179 | } else { 180 | p.setHeader(submatches[1], submatches[2]) 181 | } 182 | 183 | return 184 | } 185 | 186 | func (p *CallgrindParser) parsePosition() (err error) { 187 | text := p.scanner.Text() 188 | submatches := positionPattern.FindStringSubmatch(text) 189 | posType := strings.TrimSpace(submatches[1]) 190 | posNum := strings.TrimSpace(submatches[2]) 191 | posName := strings.TrimSpace(submatches[3]) 192 | 193 | // Ignore file information 194 | if posType != "fn" && posType != "cfn" { 195 | return 196 | } 197 | 198 | posName, err = p.getOrSetPosName(posType, posNum, posName) 199 | 200 | if posType == "fn" { 201 | p.lastFn = posName 202 | p.lastCfn = "" 203 | } else if posType == "cfn" { 204 | p.lastCfn = posName 205 | } 206 | 207 | if p.lastFn != "" && p.lastCfn != "" { 208 | p.pcMap.NewPairCall(pairName(p.lastFn, p.lastCfn)) 209 | } 210 | 211 | return nil 212 | } 213 | 214 | func (p *CallgrindParser) parseCalls() (err error) { 215 | text := p.scanner.Text() 216 | submatches := callsPattern.FindStringSubmatch(text) 217 | count, err := strconv.Atoi(strings.TrimSpace(submatches[1])) 218 | if err != nil { 219 | return 220 | } 221 | 222 | if p.lastCfn == "" { 223 | return errors.New("Calls expression encountered without called function being defined") 224 | } 225 | 226 | p.pcMap.M[pairName(p.lastFn, p.lastCfn)].Count += count 227 | eof := false 228 | text, eof, err = p.readLine() 229 | if eof || err != nil { 230 | return errors.New("Expected inclusive cost of function call after calls expression") 231 | } 232 | 233 | if !costsPattern.MatchString(text) { 234 | return errors.New("Expected inclusive cost of function call after calls expression") 235 | } 236 | 237 | err = p.parseCosts(true) 238 | 239 | return 240 | } 241 | 242 | func (p *CallgrindParser) parseCosts(callCosts bool) (err error) { 243 | if !callCosts && !(p.lastFn == "main()" && p.lastCfn == "") { 244 | return 245 | } 246 | 247 | text := p.scanner.Text() 248 | match := costsPattern.FindString(text) 249 | cost, err := strconv.ParseFloat(strings.TrimSpace(strings.Split(match, " ")[1]), 32) 250 | if err != nil { 251 | return 252 | } 253 | 254 | if p.lastFn == "" { 255 | err = errors.New("Costs expression encountered without function being defined") 256 | return 257 | } 258 | 259 | p.pcMap.M[pairName(p.lastFn, p.lastCfn)].WallTime += float32(cost) 260 | if p.lastFn == "main()" && p.lastCfn != "" { 261 | p.pcMap.M["main()"].WallTime += float32(cost) 262 | } 263 | 264 | return 265 | } 266 | -------------------------------------------------------------------------------- /xhprof/callgrind_test.go: -------------------------------------------------------------------------------- 1 | package xhprof 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | func TestParseCallgrind(t *testing.T) { 12 | expected := &PairCallMap{ 13 | M: map[string]*PairCall{ 14 | "main()": &PairCall{ 15 | Count: 1, 16 | WallTime: 820, 17 | }, 18 | "main()==>func2": &PairCall{ 19 | Count: 3, 20 | WallTime: 400, 21 | }, 22 | "main()==>func1": &PairCall{ 23 | Count: 1, 24 | WallTime: 400, 25 | }, 26 | "func1==>func2": &PairCall{ 27 | Count: 2, 28 | WallTime: 300, 29 | }, 30 | }, 31 | } 32 | 33 | f, err := os.Open("testdata/callgrind-simple.out") 34 | require.Nil(t, err) 35 | 36 | m, err := ParseCallgrind(f) 37 | require.Nil(t, err) 38 | 39 | assert.EqualValues(t, expected, m) 40 | } 41 | -------------------------------------------------------------------------------- /xhprof/file.go: -------------------------------------------------------------------------------- 1 | package xhprof 2 | 3 | import ( 4 | "encoding/json" 5 | "io/ioutil" 6 | "os" 7 | ) 8 | 9 | type File struct { 10 | Path string 11 | Format string 12 | } 13 | 14 | func NewFile(path, format string) (f *File) { 15 | f = new(File) 16 | f.Path = path 17 | f.Format = format 18 | 19 | return 20 | } 21 | 22 | func (f *File) GetProfile() (*Profile, error) { 23 | m, err := f.GetPairCallMap() 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return m.Flatten(), nil 29 | } 30 | 31 | func (f *File) GetPairCallMap() (m *PairCallMap, err error) { 32 | if f.Format == "callgrind" { 33 | fh, err := os.Open(f.Path) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | return ParseCallgrind(fh) 39 | } 40 | 41 | var rawData []byte 42 | if rawData, err = ioutil.ReadFile(f.Path); err != nil { 43 | return 44 | } 45 | 46 | m = new(PairCallMap) 47 | if err = json.Unmarshal(rawData, &m.M); err != nil { 48 | return 49 | } 50 | 51 | return 52 | } 53 | 54 | func (f *File) WritePairCallMap(m *PairCallMap) error { 55 | data, err := json.Marshal(m.M) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | return ioutil.WriteFile(f.Path, data, 0755) 61 | } 62 | -------------------------------------------------------------------------------- /xhprof/file_test.go: -------------------------------------------------------------------------------- 1 | package xhprof 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestGetPairCallMap(t *testing.T) { 11 | expected := &PairCallMap{ 12 | M: map[string]*PairCall{ 13 | "main()": &PairCall{ 14 | WallTime: 1000, 15 | Count: 1, 16 | CpuTime: 400, 17 | Memory: 1500, 18 | }, 19 | "main()==>foo": &PairCall{ 20 | WallTime: 500, 21 | Count: 2, 22 | CpuTime: 200, 23 | Memory: 700, 24 | }, 25 | "foo==>bar": &PairCall{ 26 | WallTime: 200, 27 | Count: 10, 28 | CpuTime: 100, 29 | Memory: 300, 30 | }, 31 | }, 32 | } 33 | 34 | f := NewFile("testdata/simple.xhprof", "xhprof") 35 | m, err := f.GetPairCallMap() 36 | require.Nil(t, err) 37 | assert.EqualValues(t, expected, m) 38 | } 39 | -------------------------------------------------------------------------------- /xhprof/graphviz.go: -------------------------------------------------------------------------------- 1 | package xhprof 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "math" 7 | ) 8 | 9 | func GenerateDotScript(m *PairCallMap, threshold float32, function string, criticalPath bool, right map[string]*Call, left map[string]*Call) (string, error) { 10 | result := "digraph call_graph {\n" 11 | 12 | maxWidth := float32(5) 13 | maxHeight := float32(3.5) 14 | maxFontSize := 35 15 | maxSizingRatio := float32(20) 16 | 17 | callMap := m.GetCallMap() 18 | main, ok := callMap["main()"] 19 | if !ok { 20 | return "", errors.New("Call map has no main()") 21 | } 22 | mainWt := main.WallTime 23 | 24 | var path map[string]bool 25 | var pathEdges map[string]bool 26 | 27 | if criticalPath { 28 | path, pathEdges = getCriticalPath(m) 29 | } 30 | 31 | if function != "" { 32 | relatedFuncs := getRelatedFuncs(m, function) 33 | for name, _ := range callMap { 34 | if _, ok := relatedFuncs[name]; !ok { 35 | delete(relatedFuncs, name) 36 | } 37 | } 38 | } 39 | 40 | curId := 0 41 | maxWt := float32(0) 42 | for name, c := range callMap { 43 | if function == "" && float32(math.Abs(float64(c.WallTime/mainWt))) < threshold { 44 | delete(callMap, name) 45 | continue 46 | } 47 | 48 | if maxWt == float32(0) || maxWt < float32(math.Abs(float64(c.ExclusiveWallTime))) { 49 | maxWt = float32(math.Abs(float64(c.ExclusiveWallTime))) 50 | } 51 | 52 | c.graphvizId = curId 53 | curId += 1 54 | } 55 | 56 | sizingFactor := float32(0) 57 | for name, c := range callMap { 58 | if c.ExclusiveWallTime == 0 { 59 | sizingFactor = maxSizingRatio 60 | } else { 61 | sizingFactor = float32(math.Min(float64(maxWt)/math.Abs(float64(c.ExclusiveWallTime)), float64(maxSizingRatio))) 62 | } 63 | 64 | fillColor := "" 65 | if sizingFactor < 1.5 { 66 | fillColor = ", style=filled, fillcolor=red" 67 | } 68 | 69 | if _, ok := path[name]; criticalPath && fillColor == "" && ok { 70 | fillColor = ", style=filled, fillcolor=yellow" 71 | } 72 | 73 | fontSize := fmt.Sprintf(", fontsize=%d", int(float32(maxFontSize)/((sizingFactor-1)/10+1))) 74 | width := fmt.Sprintf(", width=%.1f", maxWidth/sizingFactor) 75 | height := fmt.Sprintf(", height=%.1f", maxHeight/sizingFactor) 76 | 77 | shape := "box" 78 | n := "" 79 | if name == "main()" { 80 | shape = "octagon" 81 | n = fmt.Sprintf("Total: %2.2f ms \\nmain()", mainWt/1000) 82 | } else { 83 | n = fmt.Sprintf("%s\\nInc: %.3f ms (%.1f%%)", name, c.WallTime/1000, 100*c.WallTime/mainWt) 84 | } 85 | 86 | var label string 87 | if left == nil { 88 | label = fmt.Sprintf(", label=\"%s\\nExcl: %.3f ms (%.1f%%)\\n%d total calls\"", n, c.ExclusiveWallTime/1000, 100*c.ExclusiveWallTime/mainWt, c.Count) 89 | } else { 90 | leftC, lOk := left[name] 91 | rightC, rOk := right[name] 92 | 93 | if lOk && rOk { 94 | label = fmt.Sprintf( 95 | ", label=\"%s\\nInc: %.3f ms - %.3f ms = %.3f ms\\nExcl: %.3f ms - %.3f ms = %.3f ms\\nCalls: %d - %d = %d\"", 96 | name, 97 | leftC.WallTime/1000, rightC.WallTime/1000, c.WallTime/1000, 98 | leftC.ExclusiveWallTime/1000, rightC.ExclusiveWallTime/1000, c.ExclusiveWallTime/1000, 99 | leftC.Count, rightC.Count, c.Count, 100 | ) 101 | } else if lOk { 102 | label = fmt.Sprintf( 103 | ", label=\"%s\\nInc: %.3f ms - %.3f ms = %.3f ms\\nExcl: %.3f ms - %.3f ms = %.3f ms\\nCalls: %d - %d = %d\"", 104 | name, 105 | leftC.WallTime/1000, 0, c.WallTime/1000, 106 | leftC.ExclusiveWallTime/1000, 0, c.ExclusiveWallTime/1000, 107 | leftC.Count, 0, c.Count, 108 | ) 109 | } else { 110 | label = fmt.Sprintf( 111 | ", label=\"%s\\nInc: %.3f ms - %.3f ms = %.3f ms\\nExcl: %.3f ms - %.3f ms = %.3f ms\\nCalls: %d - %d = %d\"", 112 | name, 113 | 0, rightC.WallTime/1000, c.WallTime/1000, 114 | 0, rightC.ExclusiveWallTime/1000, c.ExclusiveWallTime/1000, 115 | 0, rightC.Count, c.Count, 116 | ) 117 | } 118 | } 119 | result += fmt.Sprintf("N%d[shape=%s %s%s%s%s%s];\n", c.graphvizId, shape, label, width, height, fontSize, fillColor) 120 | } 121 | 122 | for name, c := range m.M { 123 | parent, child := parsePairName(name) 124 | 125 | parentC, ok := callMap[parent] 126 | if !ok { 127 | continue 128 | } 129 | 130 | childC, ok := callMap[child] 131 | if !ok { 132 | continue 133 | } 134 | 135 | if function != "" && parent != function && child != function { 136 | continue 137 | } 138 | 139 | label := "1 call" 140 | if c.Count != 1 { 141 | label = fmt.Sprintf("%d calls", c.Count) 142 | } 143 | 144 | headLabel := "0.0%" 145 | if childC.WallTime > 0 { 146 | headLabel = fmt.Sprintf("%.1f%%", 100*c.WallTime/childC.WallTime) 147 | } 148 | 149 | tailLabel := "0.0%" 150 | if parentC.WallTime > 0 { 151 | tailLabel = fmt.Sprintf("%.1f%%", 100*c.WallTime/(parentC.WallTime-parentC.ExclusiveWallTime)) 152 | } 153 | 154 | lineWidth := 1 155 | arrowSize := 1 156 | if _, ok := pathEdges[name]; criticalPath && ok { 157 | lineWidth = 10 158 | arrowSize = 2 159 | } 160 | 161 | result += fmt.Sprintf( 162 | "N%d -> N%d[arrowsize=%d, color=grey, style=\"setlinewidth(%d)\", label=\"%s\", headlabel=\"%s\", taillabel=\"%s\" ];\n", 163 | parentC.graphvizId, childC.graphvizId, arrowSize, lineWidth, label, headLabel, tailLabel, 164 | ) 165 | } 166 | 167 | result += "\n}" 168 | 169 | return result, nil 170 | } 171 | 172 | func GenerateDiffDotScript(m1, m2 *PairCallMap, threshold float32) (string, error) { 173 | right := m1.GetCallMap() 174 | left := m2.GetCallMap() 175 | diff := m2.Subtract(m1) 176 | 177 | return GenerateDotScript(diff, threshold, "", true, right, left) 178 | } 179 | 180 | func getCriticalPath(m *PairCallMap) (map[string]bool, map[string]bool) { 181 | path := make(map[string]bool) 182 | pathEdges := make(map[string]bool) 183 | visited := make(map[string]bool) 184 | childrenMap := m.GetChildrenMap() 185 | node := "main()" 186 | 187 | for node != "" { 188 | visited[node] = true 189 | if children, ok := childrenMap[node]; ok { 190 | maxChild := "" 191 | for _, child := range children { 192 | if _, ok := visited[child]; ok { 193 | continue 194 | } 195 | 196 | if maxChild == "" || float32(math.Abs(float64(m.M[pairName(node, child)].WallTime))) > float32(math.Abs(float64(m.M[pairName(node, maxChild)].WallTime))) { 197 | maxChild = child 198 | } 199 | } 200 | 201 | if maxChild != "" { 202 | path[maxChild] = true 203 | pathEdges[pairName(node, maxChild)] = true 204 | } 205 | 206 | node = maxChild 207 | } else { 208 | node = "" 209 | } 210 | } 211 | 212 | return path, pathEdges 213 | } 214 | 215 | func getRelatedFuncs(m *PairCallMap, f string) map[string]bool { 216 | r := make(map[string]bool) 217 | 218 | for name, _ := range m.M { 219 | parent, child := parsePairName(name) 220 | if parent == f || child == f { 221 | r[parent] = true 222 | r[child] = true 223 | } 224 | } 225 | 226 | return r 227 | } 228 | -------------------------------------------------------------------------------- /xhprof/paircall.go: -------------------------------------------------------------------------------- 1 | package xhprof 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | type PairCall struct { 8 | Count int `json:"ct"` 9 | WallTime float32 `json:"wt"` 10 | CpuTime float32 `json:"cpu"` 11 | Memory float32 `json:"mu"` 12 | PeakMemory float32 `json:"pmu"` 13 | NumAlloc float32 `json:"mem.na"` 14 | NumFree float32 `json:"mem.nf"` 15 | AllocAmount float32 `json:"mem.aa"` 16 | } 17 | 18 | func (p *PairCall) Add(o *PairCall) *PairCall { 19 | p.Count += o.Count 20 | p.WallTime += o.WallTime 21 | p.CpuTime += o.CpuTime 22 | p.Memory += o.Memory 23 | p.PeakMemory += o.PeakMemory 24 | p.NumAlloc += o.NumAlloc 25 | p.NumFree += o.NumFree 26 | p.AllocAmount += o.AllocAmount 27 | 28 | return p 29 | } 30 | 31 | func (p *PairCall) Divide(d float32) *PairCall { 32 | p.Count /= int(d) 33 | p.WallTime /= d 34 | p.CpuTime /= d 35 | p.Memory /= d 36 | p.PeakMemory /= d 37 | p.NumAlloc /= d 38 | p.NumFree /= d 39 | p.AllocAmount /= d 40 | 41 | return p 42 | } 43 | 44 | func (p *PairCall) Subtract(o *PairCall) *PairCall { 45 | p.Count -= o.Count 46 | p.WallTime -= o.WallTime 47 | p.CpuTime -= o.CpuTime 48 | p.Memory -= o.Memory 49 | p.PeakMemory -= o.PeakMemory 50 | p.NumAlloc -= o.NumAlloc 51 | p.NumFree -= o.NumFree 52 | p.AllocAmount -= o.AllocAmount 53 | 54 | return p 55 | } 56 | 57 | type NearestFamily struct { 58 | Children *PairCallMap 59 | Parents *PairCallMap 60 | ChildrenCount int 61 | ParentsCount int 62 | } 63 | 64 | func NewNearestFamily() *NearestFamily { 65 | f := new(NearestFamily) 66 | f.Children = NewPairCallMap() 67 | f.Parents = NewPairCallMap() 68 | 69 | return f 70 | } 71 | 72 | type PairCallMap struct { 73 | M map[string]*PairCall 74 | } 75 | 76 | func NewPairCallMap() *PairCallMap { 77 | m := new(PairCallMap) 78 | m.M = make(map[string]*PairCall) 79 | 80 | return m 81 | } 82 | 83 | func (m *PairCallMap) NewPairCall(name string) *PairCall { 84 | pc, ok := m.M[name] 85 | if ok { 86 | return pc 87 | } 88 | 89 | pc = new(PairCall) 90 | m.M[name] = pc 91 | 92 | return pc 93 | } 94 | 95 | func (m *PairCallMap) GetCallMap() map[string]*Call { 96 | symbols := make(map[string]*Call) 97 | for name, info := range m.M { 98 | parent, child := parsePairName(name) 99 | 100 | call, ok := symbols[child] 101 | if !ok { 102 | call = &Call{Name: child} 103 | } 104 | 105 | call.AddPairCall(info) 106 | symbols[child] = call 107 | 108 | if len(parent) == 0 { 109 | continue 110 | } 111 | 112 | if call, ok = symbols[parent]; !ok { 113 | call = &Call{Name: parent} 114 | } 115 | 116 | call.SubtractExcl(info) 117 | symbols[parent] = call 118 | } 119 | 120 | return symbols 121 | } 122 | 123 | func (m *PairCallMap) Flatten() *Profile { 124 | symbols := m.GetCallMap() 125 | 126 | profile := new(Profile) 127 | calls := make([]*Call, 0, len(symbols)) 128 | for _, call := range symbols { 129 | calls = append(calls, call) 130 | } 131 | profile.Calls = calls 132 | 133 | main, ok := symbols["main()"] 134 | if ok { 135 | profile.Main = main 136 | } 137 | 138 | return profile 139 | } 140 | 141 | func (m *PairCallMap) ComputeNearestFamily(f string) *NearestFamily { 142 | family := NewNearestFamily() 143 | 144 | for name, info := range m.M { 145 | parent, child := parsePairName(name) 146 | if parent == f { 147 | c, ok := family.Children.M[child] 148 | if !ok { 149 | c = new(PairCall) 150 | family.Children.M[child] = c 151 | } 152 | 153 | c.WallTime += info.WallTime 154 | c.Count += info.Count 155 | family.ChildrenCount += info.Count 156 | } 157 | 158 | if child == f && parent != "" { 159 | p, ok := family.Parents.M[parent] 160 | if !ok { 161 | p = new(PairCall) 162 | family.Parents.M[parent] = p 163 | } 164 | 165 | p.WallTime += info.WallTime 166 | p.Count += info.Count 167 | family.ParentsCount += info.Count 168 | } 169 | } 170 | 171 | return family 172 | } 173 | 174 | func (m *PairCallMap) GetChildrenMap() map[string][]string { 175 | r := make(map[string][]string) 176 | 177 | for name, _ := range m.M { 178 | parent, child := parsePairName(name) 179 | if _, ok := r[parent]; !ok { 180 | r[parent] = make([]string, 0, 1) 181 | } 182 | 183 | r[parent] = append(r[parent], child) 184 | } 185 | 186 | return r 187 | } 188 | 189 | func (m *PairCallMap) Copy() *PairCallMap { 190 | r := NewPairCallMap() 191 | 192 | for name, info := range m.M { 193 | c := new(PairCall) 194 | *c = *info 195 | r.M[name] = c 196 | } 197 | 198 | return r 199 | } 200 | 201 | func (m *PairCallMap) Subtract(o *PairCallMap) *PairCallMap { 202 | r := m.Copy() 203 | 204 | for name, info := range o.M { 205 | p, ok := r.M[name] 206 | if !ok { 207 | p = new(PairCall) 208 | r.M[name] = p 209 | } 210 | 211 | p.Subtract(info) 212 | } 213 | 214 | return r 215 | } 216 | 217 | func AvgPairCallMaps(maps []*PairCallMap) *PairCallMap { 218 | if len(maps) == 1 { 219 | return maps[0] 220 | } 221 | 222 | res := NewPairCallMap() 223 | 224 | for _, m := range maps { 225 | for k, v := range m.M { 226 | pairCall, ok := res.M[k] 227 | if !ok { 228 | pairCall = new(PairCall) 229 | *pairCall = *v 230 | res.M[k] = pairCall 231 | continue 232 | } 233 | 234 | pairCall.Add(v) 235 | } 236 | } 237 | 238 | num := float32(len(maps)) 239 | for _, v := range res.M { 240 | v.Divide(num) 241 | } 242 | 243 | return res 244 | } 245 | 246 | func parsePairName(name string) (parent string, child string) { 247 | fns := strings.Split(name, "==>") 248 | if len(fns) == 2 { 249 | parent = fns[0] 250 | child = fns[1] 251 | } else { 252 | child = fns[0] 253 | } 254 | 255 | return 256 | } 257 | 258 | func pairName(parent, child string) string { 259 | if parent == "" { 260 | return child 261 | } else if child == "" { 262 | return parent 263 | } 264 | 265 | return parent + "==>" + child 266 | } 267 | -------------------------------------------------------------------------------- /xhprof/paircall_test.go: -------------------------------------------------------------------------------- 1 | package xhprof 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestParsePairName(t *testing.T) { 11 | cases := []struct { 12 | Name string 13 | Parent string 14 | Child string 15 | }{ 16 | { 17 | Name: "main()", 18 | Parent: "", 19 | Child: "main()", 20 | }, 21 | { 22 | Name: "main()==>foo", 23 | Parent: "main()", 24 | Child: "foo", 25 | }, 26 | } 27 | 28 | for _, c := range cases { 29 | parent, child := parsePairName(c.Name) 30 | assert.Equal(t, c.Parent, parent) 31 | assert.Equal(t, c.Child, child) 32 | } 33 | } 34 | 35 | func TestFlatten(t *testing.T) { 36 | expected := &Profile{ 37 | Calls: []*Call{ 38 | &Call{ 39 | Name: "main()", 40 | Count: 1, 41 | WallTime: 1000, 42 | ExclusiveWallTime: 500, 43 | CpuTime: 400, 44 | ExclusiveCpuTime: 200, 45 | Memory: 1500, 46 | ExclusiveMemory: 800, 47 | IoTime: 600, 48 | ExclusiveIoTime: 300, 49 | }, 50 | &Call{ 51 | Name: "foo", 52 | Count: 2, 53 | WallTime: 500, 54 | ExclusiveWallTime: 300, 55 | CpuTime: 200, 56 | ExclusiveCpuTime: 100, 57 | Memory: 700, 58 | ExclusiveMemory: 400, 59 | IoTime: 300, 60 | ExclusiveIoTime: 200, 61 | }, 62 | &Call{ 63 | Name: "bar", 64 | Count: 10, 65 | WallTime: 200, 66 | ExclusiveWallTime: 200, 67 | CpuTime: 100, 68 | ExclusiveCpuTime: 100, 69 | Memory: 300, 70 | ExclusiveMemory: 300, 71 | IoTime: 100, 72 | ExclusiveIoTime: 100, 73 | }, 74 | }, 75 | } 76 | 77 | m := &PairCallMap{ 78 | M: map[string]*PairCall{ 79 | "main()": &PairCall{ 80 | WallTime: 1000, 81 | Count: 1, 82 | CpuTime: 400, 83 | Memory: 1500, 84 | }, 85 | "main()==>foo": &PairCall{ 86 | WallTime: 500, 87 | Count: 2, 88 | CpuTime: 200, 89 | Memory: 700, 90 | }, 91 | "foo==>bar": &PairCall{ 92 | WallTime: 200, 93 | Count: 10, 94 | CpuTime: 100, 95 | Memory: 300, 96 | }, 97 | }, 98 | } 99 | 100 | profile := m.Flatten() 101 | require.IsType(t, profile, expected) 102 | 103 | profile.SortBy("WallTime") 104 | 105 | assert.Equal(t, float32(1000), profile.Main.WallTime) 106 | assert.EqualValues(t, expected.Calls, profile.Calls) 107 | } 108 | 109 | func TestAvgPairCallMaps(t *testing.T) { 110 | expected := &PairCallMap{ 111 | M: map[string]*PairCall{ 112 | "main()": &PairCall{ 113 | WallTime: 600, 114 | Count: 1, 115 | CpuTime: 300, 116 | Memory: 700, 117 | }, 118 | "main()==>foo": &PairCall{ 119 | WallTime: 300, 120 | Count: 2, 121 | CpuTime: 170, 122 | Memory: 500, 123 | }, 124 | "foo==>bar": &PairCall{ 125 | WallTime: 100, 126 | Count: 3, 127 | CpuTime: 50, 128 | Memory: 100, 129 | }, 130 | }, 131 | } 132 | m1 := &PairCallMap{ 133 | M: map[string]*PairCall{ 134 | "main()": &PairCall{ 135 | WallTime: 800, 136 | Count: 1, 137 | CpuTime: 400, 138 | Memory: 1000, 139 | }, 140 | "main()==>foo": &PairCall{ 141 | WallTime: 600, 142 | Count: 2, 143 | CpuTime: 300, 144 | Memory: 900, 145 | }, 146 | "foo==>bar": &PairCall{ 147 | WallTime: 300, 148 | Count: 10, 149 | CpuTime: 150, 150 | Memory: 300, 151 | }, 152 | }, 153 | } 154 | m2 := &PairCallMap{ 155 | M: map[string]*PairCall{ 156 | "main()": &PairCall{ 157 | WallTime: 300, 158 | Count: 1, 159 | CpuTime: 100, 160 | Memory: 200, 161 | }, 162 | }, 163 | } 164 | m3 := &PairCallMap{ 165 | M: map[string]*PairCall{ 166 | "main()": &PairCall{ 167 | WallTime: 700, 168 | Count: 1, 169 | CpuTime: 400, 170 | Memory: 900, 171 | }, 172 | "main()==>foo": &PairCall{ 173 | WallTime: 300, 174 | Count: 4, 175 | CpuTime: 210, 176 | Memory: 600, 177 | }, 178 | }, 179 | } 180 | 181 | res := AvgPairCallMaps([]*PairCallMap{m1, m2, m3}) 182 | assert.EqualValues(t, expected, res) 183 | } 184 | 185 | func TestComputeNearestFamily(t *testing.T) { 186 | expected := &NearestFamily{ 187 | Children: &PairCallMap{ 188 | M: map[string]*PairCall{ 189 | "bar": &PairCall{ 190 | WallTime: 200, 191 | Count: 10, 192 | }, 193 | }, 194 | }, 195 | Parents: &PairCallMap{ 196 | M: map[string]*PairCall{ 197 | "main()": &PairCall{ 198 | WallTime: 500, 199 | Count: 2, 200 | }, 201 | }, 202 | }, 203 | ChildrenCount: 10, 204 | ParentsCount: 2, 205 | } 206 | 207 | m := &PairCallMap{ 208 | M: map[string]*PairCall{ 209 | "main()": &PairCall{ 210 | WallTime: 1000, 211 | Count: 1, 212 | CpuTime: 400, 213 | Memory: 1500, 214 | }, 215 | "main()==>foo": &PairCall{ 216 | WallTime: 500, 217 | Count: 2, 218 | CpuTime: 200, 219 | Memory: 700, 220 | }, 221 | "foo==>bar": &PairCall{ 222 | WallTime: 200, 223 | Count: 10, 224 | CpuTime: 100, 225 | Memory: 300, 226 | }, 227 | }, 228 | } 229 | 230 | f := m.ComputeNearestFamily("foo") 231 | 232 | assert.EqualValues(t, expected, f) 233 | } 234 | 235 | func TestSubtractPairCallMaps(t *testing.T) { 236 | expected := &PairCallMap{ 237 | M: map[string]*PairCall{ 238 | "main()": &PairCall{ 239 | WallTime: 500, 240 | Count: 0, 241 | CpuTime: 300, 242 | Memory: 800, 243 | }, 244 | "main()==>foo": &PairCall{ 245 | WallTime: 600, 246 | Count: 2, 247 | CpuTime: 300, 248 | Memory: 900, 249 | }, 250 | "foo==>bar": &PairCall{ 251 | WallTime: -300, 252 | Count: -10, 253 | CpuTime: -150, 254 | Memory: -300, 255 | }, 256 | }, 257 | } 258 | m1 := &PairCallMap{ 259 | M: map[string]*PairCall{ 260 | "main()": &PairCall{ 261 | WallTime: 800, 262 | Count: 1, 263 | CpuTime: 400, 264 | Memory: 1000, 265 | }, 266 | "main()==>foo": &PairCall{ 267 | WallTime: 600, 268 | Count: 2, 269 | CpuTime: 300, 270 | Memory: 900, 271 | }, 272 | }, 273 | } 274 | m2 := &PairCallMap{ 275 | M: map[string]*PairCall{ 276 | "main()": &PairCall{ 277 | WallTime: 300, 278 | Count: 1, 279 | CpuTime: 100, 280 | Memory: 200, 281 | }, 282 | "foo==>bar": &PairCall{ 283 | WallTime: 300, 284 | Count: 10, 285 | CpuTime: 150, 286 | Memory: 300, 287 | }, 288 | }, 289 | } 290 | 291 | diff := m1.Subtract(m2) 292 | assert.EqualValues(t, expected, diff) 293 | } 294 | -------------------------------------------------------------------------------- /xhprof/profile.go: -------------------------------------------------------------------------------- 1 | package xhprof 2 | 3 | import ( 4 | "sort" 5 | ) 6 | 7 | type Profile struct { 8 | Calls []*Call 9 | Main *Call 10 | } 11 | 12 | func NewProfile() *Profile { 13 | p := new(Profile) 14 | p.Calls = make([]*Call, 0, 5) 15 | 16 | return p 17 | } 18 | 19 | func (p *Profile) GetMain() *Call { 20 | return p.Main 21 | } 22 | 23 | func (p *Profile) GetCall(name string) *Call { 24 | for _, c := range p.Calls { 25 | if c.Name == name { 26 | return c 27 | } 28 | } 29 | 30 | return nil 31 | } 32 | 33 | type ProfileByField struct { 34 | Profile *Profile 35 | Field string 36 | } 37 | 38 | func (p ProfileByField) Len() int { return len(p.Profile.Calls) } 39 | func (p ProfileByField) Swap(i, j int) { 40 | p.Profile.Calls[i], p.Profile.Calls[j] = p.Profile.Calls[j], p.Profile.Calls[i] 41 | } 42 | func (p ProfileByField) Less(i, j int) bool { 43 | return p.Profile.Calls[i].GetFloat32Field(p.Field) > p.Profile.Calls[j].GetFloat32Field(p.Field) 44 | } 45 | 46 | func (p *Profile) SortBy(field string) error { 47 | params := ProfileByField{Profile: p, Field: field} 48 | sort.Sort(params) 49 | return nil 50 | } 51 | 52 | func (p *Profile) SelectGreater(field string, min float32) *Profile { 53 | r := NewProfile() 54 | for _, c := range p.Calls { 55 | if c.GetFloat32Field(field) >= min { 56 | r.Calls = append(r.Calls, c) 57 | } 58 | } 59 | 60 | return r 61 | } 62 | 63 | func (p *Profile) Subtract(o *Profile) *ProfileDiff { 64 | d := new(ProfileDiff) 65 | diff := make(map[string]*CallDiff) 66 | 67 | oCalls := make(map[string]*Call) 68 | for _, c := range o.Calls { 69 | oCalls[c.Name] = c 70 | } 71 | 72 | for _, c := range p.Calls { 73 | if c == p.Main { 74 | continue 75 | } 76 | 77 | oCall, ok := oCalls[c.Name] 78 | if !ok { 79 | callDiff := &CallDiff{ 80 | Name: c.Name, 81 | WallTime: c.WallTime, 82 | Count: c.Count, 83 | FractionWtFrom: c.WallTime / p.Main.WallTime, 84 | FractionWtTo: 0, 85 | } 86 | diff[c.Name] = callDiff 87 | continue 88 | } 89 | 90 | var wtChange float32 91 | var ctChange int 92 | if c.WallTime != oCall.WallTime { 93 | wtChange = oCall.WallTime - c.WallTime 94 | } 95 | if c.Count != oCall.Count { 96 | ctChange = oCall.Count - c.Count 97 | } 98 | 99 | if wtChange != 0 || ctChange != 0 { 100 | callDiff := &CallDiff{ 101 | Name: c.Name, 102 | WallTime: wtChange, 103 | Count: ctChange, 104 | FractionWtFrom: c.WallTime / p.Main.WallTime, 105 | FractionWtTo: oCall.WallTime / o.Main.WallTime, 106 | } 107 | diff[c.Name] = callDiff 108 | } 109 | 110 | delete(oCalls, c.Name) 111 | } 112 | 113 | for _, c := range oCalls { 114 | diff[c.Name] = &CallDiff{ 115 | Name: c.Name, 116 | WallTime: c.WallTime, 117 | Count: c.Count, 118 | FractionWtFrom: 0, 119 | FractionWtTo: c.WallTime / o.Main.WallTime, 120 | } 121 | } 122 | 123 | d.Calls = make([]*CallDiff, 0, len(diff)) 124 | for _, c := range diff { 125 | d.Calls = append(d.Calls, c) 126 | } 127 | 128 | return d 129 | } 130 | 131 | type ProfileDiff struct { 132 | Calls []*CallDiff 133 | } 134 | 135 | type ProfileDiffRelative ProfileDiff 136 | 137 | func (d ProfileDiffRelative) Len() int { return len(d.Calls) } 138 | func (d ProfileDiffRelative) Swap(i, j int) { 139 | d.Calls[i], d.Calls[j] = d.Calls[j], d.Calls[i] 140 | } 141 | func (d ProfileDiffRelative) Less(i, j int) bool { 142 | iFractionDiff := d.Calls[i].FractionWtFrom - d.Calls[i].FractionWtTo 143 | jFractionDiff := d.Calls[j].FractionWtFrom - d.Calls[j].FractionWtTo 144 | 145 | return iFractionDiff > jFractionDiff 146 | } 147 | 148 | func (d *ProfileDiff) Sort() { 149 | params := ProfileDiffRelative(*d) 150 | sort.Sort(params) 151 | } 152 | 153 | func AvgProfiles(profiles []*Profile) *Profile { 154 | if len(profiles) == 1 { 155 | return profiles[0] 156 | } 157 | 158 | callMap := make(map[string]*Call) 159 | for _, p := range profiles { 160 | for _, c := range p.Calls { 161 | call, ok := callMap[c.Name] 162 | if !ok { 163 | call = new(Call) 164 | *call = *c 165 | callMap[call.Name] = call 166 | continue 167 | } 168 | 169 | call.Add(c) 170 | } 171 | } 172 | 173 | num := float32(len(profiles)) 174 | res := new(Profile) 175 | calls := make([]*Call, 0, len(callMap)) 176 | for _, call := range callMap { 177 | avgCall := call.Divide(num) 178 | if call.Name == "main()" { 179 | res.Main = avgCall 180 | } 181 | 182 | calls = append(calls, avgCall) 183 | } 184 | res.Calls = calls 185 | 186 | return res 187 | } 188 | -------------------------------------------------------------------------------- /xhprof/profile_test.go: -------------------------------------------------------------------------------- 1 | package xhprof 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestAvgProfiles(t *testing.T) { 11 | expected := Profile{ 12 | Calls: []*Call{ 13 | &Call{ 14 | Name: "main()", 15 | Count: 2, 16 | WallTime: 300, 17 | ExclusiveWallTime: 200, 18 | CpuTime: 150, 19 | ExclusiveCpuTime: 100, 20 | IoTime: 150, 21 | ExclusiveIoTime: 100, 22 | Memory: 256, 23 | ExclusiveMemory: 128, 24 | }, 25 | }, 26 | } 27 | 28 | p1 := &Profile{ 29 | Calls: []*Call{ 30 | &Call{ 31 | Name: "main()", 32 | Count: 2, 33 | WallTime: 200, 34 | ExclusiveWallTime: 200, 35 | CpuTime: 100, 36 | ExclusiveCpuTime: 50, 37 | IoTime: 100, 38 | ExclusiveIoTime: 150, 39 | Memory: 256, 40 | ExclusiveMemory: 128, 41 | }, 42 | }, 43 | } 44 | 45 | p2 := &Profile{ 46 | Calls: []*Call{ 47 | &Call{ 48 | Name: "main()", 49 | Count: 3, 50 | WallTime: 400, 51 | ExclusiveWallTime: 200, 52 | CpuTime: 200, 53 | ExclusiveCpuTime: 150, 54 | IoTime: 200, 55 | ExclusiveIoTime: 50, 56 | Memory: 256, 57 | ExclusiveMemory: 128, 58 | }, 59 | }, 60 | } 61 | 62 | p3 := &Profile{ 63 | Calls: []*Call{ 64 | &Call{ 65 | Name: "main()", 66 | Count: 2, 67 | WallTime: 300, 68 | ExclusiveWallTime: 200, 69 | CpuTime: 150, 70 | ExclusiveCpuTime: 100, 71 | IoTime: 150, 72 | ExclusiveIoTime: 100, 73 | Memory: 256, 74 | ExclusiveMemory: 128, 75 | }, 76 | }, 77 | } 78 | 79 | p := AvgProfiles([]*Profile{p1, p2, p3}) 80 | 81 | require.Len(t, p.Calls, 1) 82 | assert.EqualValues(t, expected.Calls, p.Calls) 83 | } 84 | 85 | func TestSelectGreater(t *testing.T) { 86 | expected := &Profile{ 87 | Calls: []*Call{ 88 | &Call{ 89 | Name: "main()", 90 | WallTime: 300, 91 | }, 92 | &Call{ 93 | Name: "f3", 94 | WallTime: 150, 95 | }, 96 | &Call{ 97 | Name: "f2", 98 | WallTime: 70, 99 | }, 100 | }, 101 | } 102 | 103 | p := &Profile{ 104 | Calls: []*Call{ 105 | &Call{ 106 | Name: "main()", 107 | WallTime: 300, 108 | }, 109 | &Call{ 110 | Name: "f1", 111 | WallTime: 20, 112 | }, 113 | &Call{ 114 | Name: "f2", 115 | WallTime: 70, 116 | }, 117 | &Call{ 118 | Name: "f3", 119 | WallTime: 150, 120 | }, 121 | &Call{ 122 | Name: "f4", 123 | WallTime: 29, 124 | }, 125 | }, 126 | } 127 | 128 | p.SortBy("WallTime") 129 | p = p.SelectGreater("WallTime", 30) 130 | 131 | assert.EqualValues(t, expected, p) 132 | } 133 | -------------------------------------------------------------------------------- /xhprof/testdata/callgrind-simple.out: -------------------------------------------------------------------------------- 1 | # callgrind format 2 | events: Time 3 | 4 | fl=file1.c 5 | fn={main} 6 | 16 20 7 | cfn=func1 8 | calls=1 50 9 | 16 400 10 | cfi=file2.c 11 | cfn=func2 12 | calls=3 20 13 | 16 400 14 | 15 | fn=func1 16 | 51 100 17 | cfi=file2.c 18 | cfn=func2 19 | calls=2 20 20 | 51 300 21 | 22 | fl=file2.c 23 | fn=func2 24 | 20 700 25 | -------------------------------------------------------------------------------- /xhprof/testdata/simple.xhprof: -------------------------------------------------------------------------------- 1 | { 2 | "main()": { 3 | "wt": 1000, 4 | "ct": 1, 5 | "cpu": 400, 6 | "mu": 1500 7 | }, 8 | "main()==>foo": { 9 | "wt": 500, 10 | "ct": 2, 11 | "cpu": 200, 12 | "mu": 700 13 | }, 14 | "foo==>bar": { 15 | "wt": 200, 16 | "ct": 10, 17 | "cpu": 100, 18 | "mu": 300 19 | } 20 | } 21 | --------------------------------------------------------------------------------