├── .gitignore ├── LICENSE ├── README.md ├── agent.go ├── agent_test.go ├── examples ├── aws-lambda │ └── main.go ├── basic │ └── main.go ├── demo │ └── main.go ├── errors │ └── main.go ├── focused │ └── main.go ├── handlers │ └── main.go └── manual │ └── main.go ├── internal ├── agent.go ├── agent_test.go ├── allocation_profiler.go ├── allocation_profiler_test.go ├── api_request.go ├── api_request_test.go ├── block_profiler.go ├── block_profiler_test.go ├── caller_frames.go ├── caller_frames_1_6.go ├── config.go ├── config_loader.go ├── config_loader_test.go ├── cpu_profiler.go ├── cpu_profiler_labels_test.go ├── cpu_profiler_test.go ├── error_reporter.go ├── error_reporter_test.go ├── message_queue.go ├── message_queue_test.go ├── metric.go ├── metric_test.go ├── pprof │ └── profile │ │ ├── encode.go │ │ ├── filter.go │ │ ├── legacy_profile.go │ │ ├── profile.go │ │ ├── profile_test.go │ │ ├── proto.go │ │ ├── proto_test.go │ │ └── prune.go ├── process_reporter.go ├── process_reporter_test.go ├── profile_reporter.go ├── profile_reporter_test.go ├── span_reporter.go ├── span_reporter_test.go ├── symbolizer.go ├── symbolizer_1_9.go ├── system.go ├── system_appengine.go ├── system_darwin.go ├── system_linux.go └── system_windows.go ├── pprof_labels.go ├── pprof_labels_1_8.go ├── segment.go └── span.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.o 2 | *.a 3 | *.so 4 | *.exe 5 | *.test 6 | *.prof 7 | *.test 8 | *.out 9 | *.DS_Store 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, StackImpact GmbH. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | * Redistributions of source code must retain the above copyright 6 | notice, this list of conditions and the following disclaimer. 7 | * Redistributions in binary form must reproduce the above copyright 8 | notice, this list of conditions and the following disclaimer in the 9 | documentation and/or other materials provided with the distribution. 10 | * Neither the name of the StackImpact GmbH nor the 11 | names of its contributors may be used to endorse or promote products 12 | derived from this software without specific prior written permission. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY 18 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 21 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | 26 | 27 | The Agent also includes source code from the following open source 28 | projects under the following licenses: 29 | 30 | 31 | 32 | pprof package 33 | ============= 34 | 35 | 36 | Apache License 37 | Version 2.0, January 2004 38 | http://www.apache.org/licenses/ 39 | 40 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 41 | 42 | 1. Definitions. 43 | 44 | "License" shall mean the terms and conditions for use, reproduction, 45 | and distribution as defined by Sections 1 through 9 of this document. 46 | 47 | "Licensor" shall mean the copyright owner or entity authorized by 48 | the copyright owner that is granting the License. 49 | 50 | "Legal Entity" shall mean the union of the acting entity and all 51 | other entities that control, are controlled by, or are under common 52 | control with that entity. For the purposes of this definition, 53 | "control" means (i) the power, direct or indirect, to cause the 54 | direction or management of such entity, whether by contract or 55 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 56 | outstanding shares, or (iii) beneficial ownership of such entity. 57 | 58 | "You" (or "Your") shall mean an individual or Legal Entity 59 | exercising permissions granted by this License. 60 | 61 | "Source" form shall mean the preferred form for making modifications, 62 | including but not limited to software source code, documentation 63 | source, and configuration files. 64 | 65 | "Object" form shall mean any form resulting from mechanical 66 | transformation or translation of a Source form, including but 67 | not limited to compiled object code, generated documentation, 68 | and conversions to other media types. 69 | 70 | "Work" shall mean the work of authorship, whether in Source or 71 | Object form, made available under the License, as indicated by a 72 | copyright notice that is included in or attached to the work 73 | (an example is provided in the Appendix below). 74 | 75 | "Derivative Works" shall mean any work, whether in Source or Object 76 | form, that is based on (or derived from) the Work and for which the 77 | editorial revisions, annotations, elaborations, or other modifications 78 | represent, as a whole, an original work of authorship. For the purposes 79 | of this License, Derivative Works shall not include works that remain 80 | separable from, or merely link (or bind by name) to the interfaces of, 81 | the Work and Derivative Works thereof. 82 | 83 | "Contribution" shall mean any work of authorship, including 84 | the original version of the Work and any modifications or additions 85 | to that Work or Derivative Works thereof, that is intentionally 86 | submitted to Licensor for inclusion in the Work by the copyright owner 87 | or by an individual or Legal Entity authorized to submit on behalf of 88 | the copyright owner. For the purposes of this definition, "submitted" 89 | means any form of electronic, verbal, or written communication sent 90 | to the Licensor or its representatives, including but not limited to 91 | communication on electronic mailing lists, source code control systems, 92 | and issue tracking systems that are managed by, or on behalf of, the 93 | Licensor for the purpose of discussing and improving the Work, but 94 | excluding communication that is conspicuously marked or otherwise 95 | designated in writing by the copyright owner as "Not a Contribution." 96 | 97 | "Contributor" shall mean Licensor and any individual or Legal Entity 98 | on behalf of whom a Contribution has been received by Licensor and 99 | subsequently incorporated within the Work. 100 | 101 | 2. Grant of Copyright License. Subject to the terms and conditions of 102 | this License, each Contributor hereby grants to You a perpetual, 103 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 104 | copyright license to reproduce, prepare Derivative Works of, 105 | publicly display, publicly perform, sublicense, and distribute the 106 | Work and such Derivative Works in Source or Object form. 107 | 108 | 3. Grant of Patent License. Subject to the terms and conditions of 109 | this License, each Contributor hereby grants to You a perpetual, 110 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 111 | (except as stated in this section) patent license to make, have made, 112 | use, offer to sell, sell, import, and otherwise transfer the Work, 113 | where such license applies only to those patent claims licensable 114 | by such Contributor that are necessarily infringed by their 115 | Contribution(s) alone or by combination of their Contribution(s) 116 | with the Work to which such Contribution(s) was submitted. If You 117 | institute patent litigation against any entity (including a 118 | cross-claim or counterclaim in a lawsuit) alleging that the Work 119 | or a Contribution incorporated within the Work constitutes direct 120 | or contributory patent infringement, then any patent licenses 121 | granted to You under this License for that Work shall terminate 122 | as of the date such litigation is filed. 123 | 124 | 4. Redistribution. You may reproduce and distribute copies of the 125 | Work or Derivative Works thereof in any medium, with or without 126 | modifications, and in Source or Object form, provided that You 127 | meet the following conditions: 128 | 129 | (a) You must give any other recipients of the Work or 130 | Derivative Works a copy of this License; and 131 | 132 | (b) You must cause any modified files to carry prominent notices 133 | stating that You changed the files; and 134 | 135 | (c) You must retain, in the Source form of any Derivative Works 136 | that You distribute, all copyright, patent, trademark, and 137 | attribution notices from the Source form of the Work, 138 | excluding those notices that do not pertain to any part of 139 | the Derivative Works; and 140 | 141 | (d) If the Work includes a "NOTICE" text file as part of its 142 | distribution, then any Derivative Works that You distribute must 143 | include a readable copy of the attribution notices contained 144 | within such NOTICE file, excluding those notices that do not 145 | pertain to any part of the Derivative Works, in at least one 146 | of the following places: within a NOTICE text file distributed 147 | as part of the Derivative Works; within the Source form or 148 | documentation, if provided along with the Derivative Works; or, 149 | within a display generated by the Derivative Works, if and 150 | wherever such third-party notices normally appear. The contents 151 | of the NOTICE file are for informational purposes only and 152 | do not modify the License. You may add Your own attribution 153 | notices within Derivative Works that You distribute, alongside 154 | or as an addendum to the NOTICE text from the Work, provided 155 | that such additional attribution notices cannot be construed 156 | as modifying the License. 157 | 158 | You may add Your own copyright statement to Your modifications and 159 | may provide additional or different license terms and conditions 160 | for use, reproduction, or distribution of Your modifications, or 161 | for any such Derivative Works as a whole, provided Your use, 162 | reproduction, and distribution of the Work otherwise complies with 163 | the conditions stated in this License. 164 | 165 | 5. Submission of Contributions. Unless You explicitly state otherwise, 166 | any Contribution intentionally submitted for inclusion in the Work 167 | by You to the Licensor shall be under the terms and conditions of 168 | this License, without any additional terms or conditions. 169 | Notwithstanding the above, nothing herein shall supersede or modify 170 | the terms of any separate license agreement you may have executed 171 | with Licensor regarding such Contributions. 172 | 173 | 6. Trademarks. This License does not grant permission to use the trade 174 | names, trademarks, service marks, or product names of the Licensor, 175 | except as required for reasonable and customary use in describing the 176 | origin of the Work and reproducing the content of the NOTICE file. 177 | 178 | 7. Disclaimer of Warranty. Unless required by applicable law or 179 | agreed to in writing, Licensor provides the Work (and each 180 | Contributor provides its Contributions) on an "AS IS" BASIS, 181 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 182 | implied, including, without limitation, any warranties or conditions 183 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 184 | PARTICULAR PURPOSE. You are solely responsible for determining the 185 | appropriateness of using or redistributing the Work and assume any 186 | risks associated with Your exercise of permissions under this License. 187 | 188 | 8. Limitation of Liability. In no event and under no legal theory, 189 | whether in tort (including negligence), contract, or otherwise, 190 | unless required by applicable law (such as deliberate and grossly 191 | negligent acts) or agreed to in writing, shall any Contributor be 192 | liable to You for damages, including any direct, indirect, special, 193 | incidental, or consequential damages of any character arising as a 194 | result of this License or out of the use or inability to use the 195 | Work (including but not limited to damages for loss of goodwill, 196 | work stoppage, computer failure or malfunction, or any and all 197 | other commercial damages or losses), even if such Contributor 198 | has been advised of the possibility of such damages. 199 | 200 | 9. Accepting Warranty or Additional Liability. While redistributing 201 | the Work or Derivative Works thereof, You may choose to offer, 202 | and charge a fee for, acceptance of support, warranty, indemnity, 203 | or other liability obligations and/or rights consistent with this 204 | License. However, in accepting such obligations, You may act only 205 | on Your own behalf and on Your sole responsibility, not on behalf 206 | of any other Contributor, and only if You agree to indemnify, 207 | defend, and hold each Contributor harmless for any liability 208 | incurred by, or claims asserted against, such Contributor by reason 209 | of your accepting any such warranty or additional liability. 210 | 211 | END OF TERMS AND CONDITIONS 212 | 213 | APPENDIX: How to apply the Apache License to your work. 214 | 215 | To apply the Apache License to your work, attach the following 216 | boilerplate notice, with the fields enclosed by brackets "[]" 217 | replaced with your own identifying information. (Don't include 218 | the brackets!) The text should be enclosed in the appropriate 219 | comment syntax for the file format. We also recommend that a 220 | file or class name and description of purpose be included on the 221 | same "printed page" as the copyright notice for easier 222 | identification within third-party archives. 223 | 224 | Copyright [yyyy] [name of copyright owner] 225 | 226 | Licensed under the Apache License, Version 2.0 (the "License"); 227 | you may not use this file except in compliance with the License. 228 | You may obtain a copy of the License at 229 | 230 | http://www.apache.org/licenses/LICENSE-2.0 231 | 232 | Unless required by applicable law or agreed to in writing, software 233 | distributed under the License is distributed on an "AS IS" BASIS, 234 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 235 | See the License for the specific language governing permissions and 236 | limitations under the License. 237 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StackImpact Go Profiler 2 | 3 | ## Overview 4 | 5 | StackImpact is a production-grade performance profiler built for both production and development environments. It gives developers continuous and historical code-level view of application performance that is essential for locating CPU, memory allocation and I/O hot spots as well as latency bottlenecks. Included runtime metrics and error monitoring complement profiles for extensive performance analysis. Learn more at [stackimpact.com](https://stackimpact.com/). 6 | 7 | ![dashboard](https://stackimpact.com/img/readme/hotspots-cpu-1.5-go.png) 8 | 9 | #### Features 10 | 11 | * Continuous hot spot profiling of CPU usage, memory allocation and blocking calls. 12 | * Continuous latency bottleneck tracing. 13 | * Error and panic monitoring. 14 | * Health monitoring including CPU, memory, garbage collection and other runtime metrics. 15 | * Alerts on profile anomalies. 16 | * Team access. 17 | 18 | Learn more on the [features](https://stackimpact.com/features/) page (with screenshots). 19 | 20 | 21 | #### How it works 22 | 23 | The StackImpact profiler agent is imported into a program and used as a normal package. When the program runs, various sampling profilers are started and stopped automatically by the agent and/or programmatically using the agent methods. The agent periodically reports recorded profiles and metrics to the StackImpact Dashboard. The agent can also operate in manual mode, which should be used in development only. 24 | 25 | 26 | #### Documentation 27 | 28 | See full [documentation](https://stackimpact.com/docs/) for reference. 29 | 30 | 31 | 32 | ## Requirements 33 | 34 | Linux, OS X or Windows. Go version 1.5+. 35 | 36 | 37 | ## Getting started 38 | 39 | 40 | #### Create StackImpact account 41 | 42 | Sign up for a free trial account at [stackimpact.com](https://stackimpact.com) (also with GitHub login). 43 | 44 | 45 | #### Installing the agent 46 | 47 | Install the Go agent by running 48 | 49 | ``` 50 | go get github.com/stackimpact/stackimpact-go 51 | ``` 52 | 53 | And import the package `github.com/stackimpact/stackimpact-go` in your application. 54 | 55 | 56 | #### Configuring the agent 57 | 58 | Start the agent by specifying the agent key and application name. The agent key can be found in your account's Configuration section. 59 | 60 | ```go 61 | agent := stackimpact.Start(stackimpact.Options{ 62 | AgentKey: "agent key here", 63 | AppName: "MyGoApp", 64 | }) 65 | ``` 66 | 67 | All initialization options: 68 | 69 | * `AgentKey` (Required) The access key for communication with the StackImpact servers. 70 | * `AppName` (Required) A name to identify and group application data. Typically, a single codebase, deployable unit or executable module corresponds to one application. Sometimes also referred as a service. 71 | * `AppVersion` (Optional) Sets application version, which can be used to associate profiling information with the source code release. 72 | * `AppEnvironment` (Optional) Used to differentiate applications in different environments. 73 | * `HostName` (Optional) By default, host name will be the OS hostname. 74 | * `ProxyAddress` (Optional) Proxy server URL to use when connecting to the Dashboard servers. 75 | * `HTTPClient` (Optional) An `http.Client` instance to be used instead of the default client for reporting data to Dashboard servers. 76 | * `DisableAutoProfiling` (Optional) If set to `true`, disables the default automatic profiling and reporting. Focused or manual profiling should be used instead. Useful for environments without support for timers or background tasks. 77 | * `Debug` (Optional) Enables debug logging. 78 | * `Logger` (Optional) A `log.Logger` instance to be used instead of default `STDOUT` logger. 79 | 80 | 81 | #### Basic example 82 | 83 | ```go 84 | package main 85 | 86 | import ( 87 | "fmt" 88 | "net/http" 89 | 90 | "github.com/stackimpact/stackimpact-go" 91 | ) 92 | 93 | func handler(w http.ResponseWriter, r *http.Request) { 94 | fmt.Fprintf(w, "Hello world!") 95 | } 96 | 97 | func main() { 98 | agent := stackimpact.Start(stackimpact.Options{ 99 | AgentKey: "agent key here", 100 | AppName: "Basic Go Server", 101 | AppVersion: "1.0.0", 102 | AppEnvironment: "production", 103 | }) 104 | 105 | http.HandleFunc(agent.ProfileHandlerFunc("/", handler)) 106 | http.ListenAndServe(":8080", nil) 107 | } 108 | ``` 109 | 110 | 111 | #### Focused profiling 112 | 113 | Focused profiling is suitable for repeating code, such as request or event handlers. By default, the agent starts and stops profiling automatically. In order to make sure the agent profiles the most relevant execution intervals, the following methods can be used. In addition to more precise profiling, timing information will also be reported for the profiled spans. 114 | 115 | ```go 116 | // Use this method to instruct the agent to start and stop 117 | // profiling. It does not guarantee that any profiler will be 118 | // started. The decision is made by the agent based on the 119 | // overhead constraints. The method returns Span object, on 120 | // which the Stop() method should be called. 121 | span := agent.Profile(); 122 | defer span.Stop(); 123 | ``` 124 | 125 | ```go 126 | // This method is similar to the Profile() method. It additionally 127 | // allows to specify a span name to group span timing measurements. 128 | span := agent.ProfileWithName(name); 129 | defer span.Stop(); 130 | ``` 131 | 132 | ```go 133 | // A helper function to profile HTTP handler execution by wrapping 134 | // http.Handle method parameters. 135 | // Usage example: 136 | // http.Handle(agent.ProfileHandler("/some-path", someHandler)) 137 | pattern, wrappedHandler := agent.ProfileHandler(pattern, handler) 138 | ``` 139 | 140 | ```go 141 | // A helper function to profile HTTP handler function execution 142 | // by wrapping http.HandleFunc method parameters. 143 | // Usage example: 144 | // http.HandleFunc(agent.ProfileHandlerFunc("/some-path", someHandlerFunc)) 145 | pattern, wrappedHandlerFunc := agent.ProfileHandlerFunc(pattern, handlerFunc) 146 | ``` 147 | 148 | 149 | #### Error reporting 150 | 151 | To monitor exceptions and panics with stack traces, the error recording API can be used. 152 | 153 | Recording handled errors: 154 | 155 | ```go 156 | // Aggregates and reports errors with regular intervals. 157 | agent.RecordError(someError) 158 | ``` 159 | 160 | Recording panics without recovering: 161 | 162 | ```go 163 | // Aggregates and reports panics with regular intervals. 164 | defer agent.RecordPanic() 165 | ``` 166 | 167 | Recording and recovering from panics: 168 | 169 | ```go 170 | // Aggregates and reports panics with regular intervals. This function also 171 | // recovers from panics. 172 | defer agent.RecordAndRecoverPanic() 173 | ``` 174 | 175 | 176 | #### Manual profiling 177 | 178 | *Manual profiling should not be used in production!* 179 | 180 | By default, the agent starts and stops profiling automatically. Manual profiling allows to start and stop profilers directly. It is suitable for profiling short-lived programs and should not be used for long-running production applications. Automatic profiling should be disabled with `DisableAutoProfiling: true`. 181 | 182 | ```go 183 | // Start CPU profiler. 184 | agent.StartCPUProfiler(); 185 | ``` 186 | 187 | ```go 188 | // Stop CPU profiler and report the recorded profile to the Dashboard. 189 | // Automatic profiling should be disabled. 190 | agent.StopCPUProfiler(); 191 | ``` 192 | 193 | ```go 194 | // Start blocking call profiler. 195 | agent.StartBlockProfiler(); 196 | ``` 197 | 198 | ```go 199 | // Stop blocking call profiler and report the recorded profile to the Dashboard. 200 | agent.StopBlockProfiler(); 201 | ``` 202 | 203 | ```go 204 | // Report current allocation profile to the Dashboard. 205 | agent.ReportAllocationProfile(); 206 | ``` 207 | 208 | 209 | #### Analyzing performance data in the Dashboard 210 | 211 | Once your application is restarted, you can start observing continuously recorded CPU, memory, I/O, and other hot spot profiles, execution bottlenecks as well as process metrics in the [Dashboard](https://dashboard.stackimpact.com/). 212 | 213 | 214 | #### Troubleshooting 215 | 216 | To enable debug logging, add `Debug: true` to startup options. If the debug log doesn't give you any hints on how to fix a problem, please report it to our support team in your account's Support section. 217 | 218 | 219 | ## Overhead 220 | 221 | The agent overhead is measured to be less than 1% for applications under high load. 222 | -------------------------------------------------------------------------------- /agent.go: -------------------------------------------------------------------------------- 1 | package stackimpact 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | 8 | "github.com/stackimpact/stackimpact-go/internal" 9 | ) 10 | 11 | const ErrorGroupRecoveredPanics string = "Recovered panics" 12 | const ErrorGroupUnrecoveredPanics string = "Unrecovered panics" 13 | const ErrorGroupHandledExceptions string = "Handled exceptions" 14 | 15 | type Options struct { 16 | DashboardAddress string 17 | ProxyAddress string 18 | HTTPClient *http.Client 19 | AgentKey string 20 | AppName string 21 | AppVersion string 22 | AppEnvironment string 23 | HostName string 24 | DisableAutoProfiling bool 25 | Debug bool 26 | Logger *log.Logger 27 | ProfileAgent bool 28 | } 29 | 30 | type Agent struct { 31 | internalAgent *internal.Agent 32 | 33 | spanStarted int32 34 | 35 | // compatibility < 1.2.0 36 | DashboardAddress string 37 | AgentKey string 38 | AppName string 39 | HostName string 40 | Debug bool 41 | } 42 | 43 | // DEPRECATED. Kept for compatibility with <1.4.3. 44 | func NewAgent() *Agent { 45 | a := &Agent{ 46 | internalAgent: internal.NewAgent(), 47 | spanStarted: 0, 48 | } 49 | 50 | return a 51 | } 52 | 53 | // Agent instance 54 | var _agent *Agent = nil 55 | 56 | // Starts the agent with configuration options. 57 | // Required options are AgentKey and AppName. 58 | func Start(options Options) *Agent { 59 | if _agent == nil { 60 | _agent = &Agent{ 61 | internalAgent: internal.NewAgent(), 62 | } 63 | } 64 | 65 | _agent.Start(options) 66 | 67 | return _agent 68 | } 69 | 70 | // Starts the agent with configuration options. 71 | // Required options are AgentKey and AppName. 72 | func (a *Agent) Start(options Options) { 73 | a.internalAgent.AgentKey = options.AgentKey 74 | a.internalAgent.AppName = options.AppName 75 | 76 | if options.AppVersion != "" { 77 | a.internalAgent.AppVersion = options.AppVersion 78 | } 79 | 80 | if options.AppEnvironment != "" { 81 | a.internalAgent.AppEnvironment = options.AppEnvironment 82 | } 83 | 84 | if options.HostName != "" { 85 | a.internalAgent.HostName = options.HostName 86 | } 87 | 88 | if options.DisableAutoProfiling { 89 | a.internalAgent.AutoProfiling = false 90 | } 91 | 92 | if options.DashboardAddress != "" { 93 | a.internalAgent.DashboardAddress = options.DashboardAddress 94 | } 95 | 96 | if options.ProxyAddress != "" { 97 | a.internalAgent.ProxyAddress = options.ProxyAddress 98 | } 99 | 100 | if options.HTTPClient != nil { 101 | a.internalAgent.HTTPClient = options.HTTPClient 102 | } 103 | 104 | if options.Debug { 105 | a.internalAgent.Debug = true 106 | } 107 | 108 | if options.Logger != nil { 109 | a.internalAgent.Logger = options.Logger 110 | } 111 | 112 | if options.ProfileAgent { 113 | a.internalAgent.ProfileAgent = true 114 | } 115 | 116 | a.internalAgent.Start() 117 | } 118 | 119 | // Update some options after the agent has already been started. 120 | // Only ProxyAddress, HTTPClient and Debug options are updatable. 121 | func (a *Agent) UpdateOptions(options Options) { 122 | if options.ProxyAddress != "" { 123 | a.internalAgent.ProxyAddress = options.ProxyAddress 124 | } 125 | 126 | if options.HTTPClient != nil { 127 | a.internalAgent.HTTPClient = options.HTTPClient 128 | } 129 | 130 | if options.Debug { 131 | a.internalAgent.Debug = true 132 | } 133 | 134 | if options.Logger != nil { 135 | a.internalAgent.Logger = options.Logger 136 | } 137 | } 138 | 139 | // DEPRECATED. Kept for compatibility with <1.2.0. 140 | func (a *Agent) Configure(agentKey string, appName string) { 141 | a.Start(Options{ 142 | AgentKey: agentKey, 143 | AppName: appName, 144 | HostName: a.HostName, 145 | DashboardAddress: a.DashboardAddress, 146 | Debug: a.Debug, 147 | }) 148 | } 149 | 150 | // Start CPU profiler. Automatic profiling should be disabled. 151 | func (a *Agent) StartCPUProfiler() { 152 | a.internalAgent.StartCPUProfiler() 153 | } 154 | 155 | // Stop CPU profiler and report the recorded profile to the Dashboard. 156 | func (a *Agent) StopCPUProfiler() { 157 | a.internalAgent.StopCPUProfiler() 158 | } 159 | 160 | // Start blocking call profiler. Automatic profiling should be disabled. 161 | func (a *Agent) StartBlockProfiler() { 162 | a.internalAgent.StartBlockProfiler() 163 | } 164 | 165 | // Stop blocking call profiler and report the recorded profile to the Dashboard. 166 | func (a *Agent) StopBlockProfiler() { 167 | a.internalAgent.StopBlockProfiler() 168 | } 169 | 170 | // Report current allocation profile to the Dashboard. 171 | func (a *Agent) ReportAllocationProfile() { 172 | a.internalAgent.ReportAllocationProfile() 173 | } 174 | 175 | // Use this method to instruct the agent to start and stop 176 | // profiling. It does not guarantee that any profiler will be 177 | // started. The decision is made by the agent based on the 178 | // overhead constraints. The method returns Span object, on 179 | // which the Stop() method should be called. 180 | func (a *Agent) Profile() *Span { 181 | return a.ProfileWithName("Default") 182 | } 183 | 184 | // This method is similar to the Profile() method. It additionally 185 | // allows to specify a span name to group span timing measurements. 186 | func (a *Agent) ProfileWithName(name string) *Span { 187 | s := newSpan(a, name) 188 | s.start() 189 | 190 | return s 191 | } 192 | 193 | // A helper function to profile HTTP handler function execution 194 | // by wrapping http.HandleFunc method parameters. 195 | func (a *Agent) ProfileHandlerFunc(pattern string, handlerFunc func(http.ResponseWriter, *http.Request)) (string, func(http.ResponseWriter, *http.Request)) { 196 | return pattern, func(w http.ResponseWriter, r *http.Request) { 197 | span := newSpan(a, fmt.Sprintf("Handler %s", pattern)) 198 | span.start() 199 | defer span.Stop() 200 | 201 | if span.active { 202 | WithPprofLabel("workload", span.name, r, func() { 203 | handlerFunc(w, r) 204 | }) 205 | } else { 206 | handlerFunc(w, r) 207 | } 208 | } 209 | } 210 | 211 | // A helper function to profile HTTP handler execution 212 | // by wrapping http.Handle method parameters. 213 | func (a *Agent) ProfileHandler(pattern string, handler http.Handler) (string, http.Handler) { 214 | return pattern, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 215 | span := newSpan(a, fmt.Sprintf("Handler %s", pattern)) 216 | span.start() 217 | defer span.Stop() 218 | 219 | if span.active { 220 | WithPprofLabel("workload", span.name, r, func() { 221 | handler.ServeHTTP(w, r) 222 | }) 223 | } else { 224 | handler.ServeHTTP(w, r) 225 | } 226 | }) 227 | } 228 | 229 | // DEPRECATED. Starts measurement of execution time of a code segment. 230 | // To stop measurement call Stop on returned Segment object. 231 | // After calling Stop the segment is recorded, aggregated and 232 | // reported with regular intervals. 233 | func (a *Agent) MeasureSegment(segmentName string) *Segment { 234 | s := newSegment(a, segmentName) 235 | s.start() 236 | 237 | return s 238 | } 239 | 240 | // DEPRECATED. A helper function to measure HTTP handler function execution 241 | // by wrapping http.HandleFunc method parameters. 242 | func (a *Agent) MeasureHandlerFunc(pattern string, handlerFunc func(http.ResponseWriter, *http.Request)) (string, func(http.ResponseWriter, *http.Request)) { 243 | return pattern, func(w http.ResponseWriter, r *http.Request) { 244 | segment := a.MeasureSegment(fmt.Sprintf("Handler %s", pattern)) 245 | defer segment.Stop() 246 | 247 | handlerFunc(w, r) 248 | } 249 | } 250 | 251 | // DEPRECATED. A helper function to measure HTTP handler execution 252 | // by wrapping http.Handle method parameters. 253 | func (a *Agent) MeasureHandler(pattern string, handler http.Handler) (string, http.Handler) { 254 | return pattern, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 255 | segment := a.MeasureSegment(fmt.Sprintf("Handler %s", pattern)) 256 | defer segment.Stop() 257 | 258 | handler.ServeHTTP(w, r) 259 | }) 260 | } 261 | 262 | // Aggregates and reports errors with regular intervals. 263 | func (a *Agent) RecordError(err interface{}) { 264 | a.internalAgent.RecordError(ErrorGroupHandledExceptions, err, 1) 265 | } 266 | 267 | // Aggregates and reports panics with regular intervals. 268 | func (a *Agent) RecordPanic() { 269 | if err := recover(); err != nil { 270 | a.internalAgent.RecordError(ErrorGroupUnrecoveredPanics, err, 1) 271 | 272 | panic(err) 273 | } 274 | } 275 | 276 | // Aggregates and reports panics with regular intervals. This function also 277 | // recovers from panics 278 | func (a *Agent) RecordAndRecoverPanic() { 279 | if err := recover(); err != nil { 280 | a.internalAgent.RecordError(ErrorGroupRecoveredPanics, err, 1) 281 | } 282 | } 283 | 284 | // DEPRECATED. Kept for compatibility. 285 | func (a *Agent) Report() { 286 | } 287 | 288 | // DEPRECATED. Kept for compatibility. 289 | func (a *Agent) ReportWithHTTPClient(client *http.Client) { 290 | } 291 | -------------------------------------------------------------------------------- /agent_test.go: -------------------------------------------------------------------------------- 1 | package stackimpact 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "os" 9 | "testing" 10 | "time" 11 | ) 12 | 13 | func TestMeasureSegment(t *testing.T) { 14 | agent := NewAgent() 15 | 16 | done1 := make(chan bool) 17 | 18 | var seg1 *Segment 19 | go func() { 20 | seg1 = agent.MeasureSegment("seg1") 21 | defer seg1.Stop() 22 | 23 | time.Sleep(50 * time.Millisecond) 24 | 25 | done1 <- true 26 | }() 27 | 28 | <-done1 29 | 30 | time.Sleep(10 * time.Millisecond) 31 | 32 | if seg1.Duration < 50 { 33 | t.Errorf("Duration of seg1 is too low: %v", seg1.Duration) 34 | } 35 | } 36 | 37 | func TestMeasureHandler(t *testing.T) { 38 | agent := NewAgent() 39 | 40 | // start HTTP server 41 | go func() { 42 | http.Handle(agent.MeasureHandler("/test1", http.StripPrefix("/test1", http.FileServer(http.Dir("/tmp"))))) 43 | 44 | if err := http.ListenAndServe(":5010", nil); err != nil { 45 | t.Error(err) 46 | return 47 | } 48 | }() 49 | 50 | waitForServer("http://localhost:5010/test1") 51 | 52 | res, err := http.Get("http://localhost:5010/test1") 53 | if err != nil { 54 | t.Error(err) 55 | return 56 | } else if res.StatusCode != 200 { 57 | io.Copy(os.Stdout, res.Body) 58 | t.Error(err) 59 | return 60 | } else { 61 | defer res.Body.Close() 62 | } 63 | } 64 | 65 | func TestMeasureHandlerFunc(t *testing.T) { 66 | agent := NewAgent() 67 | 68 | // start HTTP server 69 | go func() { 70 | http.HandleFunc(agent.MeasureHandlerFunc("/test2", func(w http.ResponseWriter, r *http.Request) { 71 | time.Sleep(100 * time.Millisecond) 72 | fmt.Fprintf(w, "OK") 73 | })) 74 | 75 | if err := http.ListenAndServe(":5011", nil); err != nil { 76 | t.Error(err) 77 | return 78 | } 79 | }() 80 | 81 | waitForServer("http://localhost:5011/test2") 82 | 83 | res, err := http.Get("http://localhost:5011/test2") 84 | if err != nil { 85 | t.Error(err) 86 | return 87 | } else if res.StatusCode != 200 { 88 | t.Error(err) 89 | return 90 | } else { 91 | defer res.Body.Close() 92 | } 93 | } 94 | 95 | func TestRecoverPanic(t *testing.T) { 96 | agent := NewAgent() 97 | 98 | done := make(chan bool) 99 | 100 | go func() { 101 | defer func() { 102 | if err := recover(); err != nil { 103 | t.Error("panic1 unrecovered") 104 | } 105 | }() 106 | defer agent.RecordAndRecoverPanic() 107 | defer func() { 108 | done <- true 109 | }() 110 | 111 | panic("panic1") 112 | }() 113 | 114 | <-done 115 | } 116 | 117 | func waitForServer(url string) { 118 | for { 119 | if _, err := http.Get(url); err == nil { 120 | time.Sleep(100 * time.Millisecond) 121 | break 122 | } 123 | } 124 | } 125 | 126 | func BenchmarkProfile(b *testing.B) { 127 | agent := NewAgent() 128 | agent.Start(Options{ 129 | AgentKey: "key1", 130 | AppName: "app1", 131 | }) 132 | agent.internalAgent.Enable() 133 | 134 | b.ResetTimer() 135 | 136 | for i := 0; i < b.N; i++ { 137 | s := agent.Profile() 138 | s.Stop() 139 | } 140 | 141 | // go test -v -run=^$ -bench=BenchmarkProfile -cpuprofile=cpu.out 142 | // go tool pprof internal.test cpu.out 143 | } 144 | 145 | func BenchmarkRecordError(b *testing.B) { 146 | agent := NewAgent() 147 | agent.Start(Options{ 148 | AgentKey: "key1", 149 | AppName: "app1", 150 | }) 151 | agent.internalAgent.Enable() 152 | 153 | err := errors.New("error1") 154 | 155 | b.ResetTimer() 156 | 157 | for i := 0; i < b.N; i++ { 158 | agent.RecordError(err) 159 | } 160 | 161 | // go test -v -run=^$ -bench=BenchmarkRecordError -cpuprofile=cpu.out 162 | // go tool pprof internal.test cpu.out 163 | } 164 | -------------------------------------------------------------------------------- /examples/aws-lambda/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "time" 5 | "math/rand" 6 | 7 | "github.com/aws/aws-lambda-go/events" 8 | "github.com/aws/aws-lambda-go/lambda" 9 | "github.com/stackimpact/stackimpact-go" 10 | ) 11 | 12 | var agent *stackimpact.Agent 13 | 14 | var mem []string = make([]string, 0) 15 | 16 | func Handler(request events.APIGatewayProxyRequest) (events.APIGatewayProxyResponse, error) { 17 | // profile this handler 18 | span := agent.Profile() 19 | defer span.Stop() 20 | 21 | // simulate cpu work 22 | for i := 0; i < 1000000; i++ { 23 | rand.Intn(1000) 24 | } 25 | 26 | // simulate memory leak 27 | for i := 0; i < 1000; i++ { 28 | mem = append(mem, string(i)) 29 | } 30 | 31 | // simulate blocking call 32 | done := make(chan bool) 33 | go func() { 34 | time.Sleep(200 * time.Millisecond) 35 | done <- true 36 | }() 37 | <-done 38 | 39 | return events.APIGatewayProxyResponse{ 40 | Body: "Hello", 41 | StatusCode: 200, 42 | }, nil 43 | 44 | } 45 | 46 | func main() { 47 | agent = stackimpact.Start(stackimpact.Options{ 48 | AgentKey: "agent key here", 49 | AppName: "LambdaGo", 50 | DisableAutoProfiling: true, 51 | }) 52 | 53 | lambda.Start(Handler) 54 | } 55 | -------------------------------------------------------------------------------- /examples/basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/stackimpact/stackimpact-go" 8 | ) 9 | 10 | func handler(w http.ResponseWriter, r *http.Request) { 11 | fmt.Fprintf(w, "Hello world!") 12 | } 13 | 14 | func main() { 15 | agent := stackimpact.Start(stackimpact.Options{ 16 | AgentKey: "agent key here", 17 | AppName: "Basic Go Server", 18 | AppVersion: "1.0.0", 19 | AppEnvironment: "production", 20 | }) 21 | 22 | http.HandleFunc(agent.ProfileHandlerFunc("/", handler)) 23 | http.ListenAndServe(":8080", nil) 24 | } 25 | -------------------------------------------------------------------------------- /examples/demo/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "math" 8 | "math/rand" 9 | "net/http" 10 | "os" 11 | "os/exec" 12 | "strconv" 13 | "sync" 14 | "time" 15 | 16 | "github.com/stackimpact/stackimpact-go" 17 | ) 18 | 19 | var agent *stackimpact.Agent 20 | 21 | func useCPU(duration int, usage int) { 22 | for j := 0; j < duration; j++ { 23 | go func() { 24 | for i := 0; i < usage*80000; i++ { 25 | str := "str" + strconv.Itoa(i) 26 | str = str + "a" 27 | } 28 | }() 29 | 30 | time.Sleep(1 * time.Second) 31 | } 32 | } 33 | 34 | func simulateCPUUsage() { 35 | // sumulate CPU usage anomaly - every 45 minutes 36 | cpuAnomalyTicker := time.NewTicker(45 * time.Minute) 37 | go func() { 38 | for { 39 | select { 40 | case <-cpuAnomalyTicker.C: 41 | // for 60 seconds produce generate 50% CPU usage 42 | useCPU(60, 50) 43 | } 44 | } 45 | }() 46 | 47 | // generate constant ~10% CPU usage 48 | useCPU(math.MaxInt64, 10) 49 | } 50 | 51 | func leakMemory(duration int, size int) { 52 | mem := make([]string, 0) 53 | 54 | for j := 0; j < duration; j++ { 55 | go func() { 56 | for i := 0; i < size; i++ { 57 | mem = append(mem, string(i)) 58 | } 59 | }() 60 | 61 | time.Sleep(1 * time.Second) 62 | } 63 | } 64 | 65 | func simulateMemoryLeak() { 66 | // simulate memory leak - constantly 67 | constantTicker := time.NewTicker(2 * 3600 * time.Second) 68 | go func() { 69 | for { 70 | select { 71 | case <-constantTicker.C: 72 | leakMemory(2*3600, 1000) 73 | } 74 | } 75 | }() 76 | 77 | go leakMemory(2*3600, 1000) 78 | } 79 | 80 | func simulateChannelWait() { 81 | for { 82 | done := make(chan bool) 83 | 84 | go func() { 85 | wait := make(chan bool) 86 | 87 | go func() { 88 | time.Sleep(500 * time.Millisecond) 89 | 90 | wait <- true 91 | }() 92 | 93 | <-wait 94 | 95 | done <- true 96 | }() 97 | 98 | <-done 99 | 100 | time.Sleep(1000 * time.Millisecond) 101 | } 102 | } 103 | 104 | func simulateNetworkWait() { 105 | // start HTTP server 106 | go func() { 107 | http.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { 108 | done := make(chan bool) 109 | 110 | go func() { 111 | time.Sleep(time.Duration(200+rand.Intn(5)) * time.Millisecond) 112 | done <- true 113 | }() 114 | <-done 115 | 116 | fmt.Fprintf(w, "OK") 117 | }) 118 | 119 | if err := http.ListenAndServe(":5002", nil); err != nil { 120 | log.Fatal(err) 121 | return 122 | } 123 | }() 124 | 125 | requestTicker := time.NewTicker(500 * time.Millisecond) 126 | for { 127 | select { 128 | case <-requestTicker.C: 129 | res, err := http.Get("http://localhost:5002/test") 130 | if err == nil { 131 | res.Body.Close() 132 | } 133 | } 134 | } 135 | } 136 | 137 | func simulateSyscallWait() { 138 | for { 139 | done := make(chan bool) 140 | 141 | go func() { 142 | _, err := exec.Command("sleep", "1").Output() 143 | if err != nil { 144 | fmt.Println(err) 145 | } 146 | 147 | done <- true 148 | }() 149 | 150 | time.Sleep(1 * time.Second) 151 | 152 | <-done 153 | } 154 | } 155 | 156 | func simulateLockWait() { 157 | for { 158 | done := make(chan bool) 159 | 160 | lock := &sync.RWMutex{} 161 | lock.Lock() 162 | 163 | go func() { 164 | lock.RLock() 165 | lock.RUnlock() 166 | 167 | done <- true 168 | }() 169 | 170 | go func() { 171 | time.Sleep(200 * time.Millisecond) 172 | lock.Unlock() 173 | }() 174 | 175 | <-done 176 | 177 | time.Sleep(500 * time.Millisecond) 178 | } 179 | } 180 | 181 | func simulateFocusedProfiling() { 182 | for { 183 | span := agent.Profile() 184 | fmt.Println("Focused profile started") 185 | 186 | // wait 187 | time.Sleep(time.Duration(10+rand.Intn(10)) * time.Millisecond) 188 | 189 | // cpu 190 | for i := 0; i < 1000; i++ { 191 | rand.Intn(1000) 192 | } 193 | 194 | span.Stop() 195 | fmt.Println("Focused profile stopped") 196 | 197 | time.Sleep(20 * time.Second) 198 | } 199 | } 200 | 201 | func simulateWorkloadProfiling() { 202 | // start HTTP server 203 | go func() { 204 | http.HandleFunc(agent.ProfileHandlerFunc("/some-handler", func(w http.ResponseWriter, r *http.Request) { 205 | for i := 0; i < 500000; i++ { 206 | str := "str" + strconv.Itoa(i) 207 | str = str + "a" 208 | } 209 | 210 | fmt.Fprintf(w, "OK") 211 | })) 212 | 213 | if err := http.ListenAndServe(":5003", nil); err != nil { 214 | log.Fatal(err) 215 | return 216 | } 217 | }() 218 | 219 | requestTicker := time.NewTicker(1000 * time.Millisecond) 220 | for { 221 | select { 222 | case <-requestTicker.C: 223 | res, err := http.Get("http://localhost:5003/some-handler") 224 | if err == nil { 225 | res.Body.Close() 226 | } 227 | } 228 | } 229 | } 230 | 231 | func simulateErrors() { 232 | go func() { 233 | for { 234 | agent.RecordError(fmt.Sprintf("A handled exception %v", rand.Intn(10))) 235 | 236 | time.Sleep(2 * time.Second) 237 | } 238 | }() 239 | 240 | go func() { 241 | for { 242 | agent.RecordError(errors.New(fmt.Sprintf("A handled exception %v", rand.Intn(10)))) 243 | 244 | time.Sleep(10 * time.Second) 245 | } 246 | }() 247 | 248 | go func() { 249 | for { 250 | go func() { 251 | defer agent.RecordAndRecoverPanic() 252 | 253 | panic("A recovered panic") 254 | }() 255 | 256 | time.Sleep(5 * time.Second) 257 | } 258 | }() 259 | 260 | go func() { 261 | for { 262 | go func() { 263 | defer func() { 264 | if err := recover(); err != nil { 265 | // recover from unrecovered panic 266 | } 267 | }() 268 | defer agent.RecordPanic() 269 | 270 | panic("An unrecovered panic") 271 | }() 272 | 273 | time.Sleep(7 * time.Second) 274 | } 275 | }() 276 | 277 | } 278 | 279 | func main() { 280 | // StackImpact initialization 281 | agent = stackimpact.Start(stackimpact.Options{ 282 | AgentKey: os.Getenv("AGENT_KEY"), 283 | AppName: "ExampleGoApp", 284 | AppVersion: "1.0.0", 285 | DashboardAddress: os.Getenv("DASHBOARD_ADDRESS"), // test only 286 | Debug: true, 287 | }) 288 | // end StackImpact initialization 289 | 290 | go simulateCPUUsage() 291 | go simulateMemoryLeak() 292 | go simulateChannelWait() 293 | go simulateNetworkWait() 294 | go simulateSyscallWait() 295 | go simulateLockWait() 296 | go simulateFocusedProfiling() 297 | go simulateWorkloadProfiling() 298 | go simulateErrors() 299 | 300 | select {} 301 | } 302 | -------------------------------------------------------------------------------- /examples/errors/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/stackimpact/stackimpact-go" 10 | ) 11 | 12 | var agent *stackimpact.Agent 13 | 14 | func handlerA(w http.ResponseWriter, r *http.Request) { 15 | res, err := http.Get("https://nonexistingdomain") 16 | if err != nil { 17 | agent.RecordError(err) 18 | } else { 19 | defer res.Body.Close() 20 | } 21 | 22 | fmt.Fprintf(w, "Done") 23 | } 24 | 25 | func handlerB(w http.ResponseWriter, r *http.Request) { 26 | defer agent.RecordPanic() 27 | 28 | s := []string{"a", "b"} 29 | fmt.Println(s[2]) // this will cause panic 30 | 31 | fmt.Fprintf(w, "Done") 32 | } 33 | 34 | func handlerC(w http.ResponseWriter, r *http.Request) { 35 | defer agent.RecordAndRecoverPanic() 36 | 37 | s := []string{"a", "b"} 38 | fmt.Println(s[2]) // this will cause panic 39 | 40 | fmt.Fprintf(w, "Done") 41 | } 42 | 43 | func main() { 44 | // StackImpact initialization 45 | agent = stackimpact.Start(stackimpact.Options{ 46 | AgentKey: "0dac7f9b26b07a7c25328f7e567e4b89409e4ac3", 47 | AppName: "Some Go App", 48 | }) 49 | // end StackImpact initialization 50 | 51 | // Start server 52 | go func() { 53 | http.HandleFunc("/a", handlerA) 54 | http.HandleFunc("/b", handlerB) 55 | http.HandleFunc("/c", handlerC) 56 | http.ListenAndServe(":9000", nil) 57 | }() 58 | 59 | requestTicker := time.NewTicker(5 * time.Second) 60 | for { 61 | select { 62 | case <-requestTicker.C: 63 | if rand.Intn(2) == 0 { 64 | res, err := http.Get("http://localhost:9000/a") 65 | if err == nil { 66 | res.Body.Close() 67 | } 68 | } 69 | 70 | if rand.Intn(3) == 0 { 71 | res, err := http.Get("http://localhost:9000/b") 72 | if err == nil { 73 | res.Body.Close() 74 | } 75 | } 76 | 77 | if rand.Intn(4) == 0 { 78 | res, err := http.Get("http://localhost:9000/c") 79 | if err == nil { 80 | res.Body.Close() 81 | } 82 | } 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /examples/focused/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/stackimpact/stackimpact-go" 8 | ) 9 | 10 | var agent *stackimpact.Agent 11 | 12 | func helloHandler(w http.ResponseWriter, r *http.Request) { 13 | span := agent.ProfileWithName("Hello handler") 14 | defer span.Stop() 15 | 16 | fmt.Fprintf(w, "Hello world!") 17 | } 18 | 19 | func main() { 20 | agent = stackimpact.Start(stackimpact.Options{ 21 | AgentKey: "agent key here", 22 | AppName: "My Go App", 23 | }) 24 | 25 | http.HandleFunc("/", helloHandler) 26 | http.ListenAndServe(":8080", nil) 27 | } 28 | -------------------------------------------------------------------------------- /examples/handlers/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "net/http" 7 | 8 | "github.com/stackimpact/stackimpact-go" 9 | ) 10 | 11 | func requestHandler(w http.ResponseWriter, r *http.Request) { 12 | // simulate cpu work 13 | for i := 0; i < 10000000; i++ { 14 | rand.Intn(1000) 15 | } 16 | 17 | fmt.Fprintf(w, "Done") 18 | } 19 | 20 | func main() { 21 | // Initialize StackImpact agent 22 | agent := stackimpact.Start(stackimpact.Options{ 23 | AgentKey: "agent key yere", 24 | AppName: "Workload Profiling Example", 25 | }) 26 | 27 | // Serve 28 | http.HandleFunc(agent.ProfileHandlerFunc("/test", requestHandler)) 29 | http.ListenAndServe(":9000", nil) 30 | } 31 | -------------------------------------------------------------------------------- /examples/manual/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "math/rand" 5 | 6 | "github.com/stackimpact/stackimpact-go" 7 | ) 8 | 9 | 10 | func main() { 11 | agent := stackimpact.Start(stackimpact.Options{ 12 | AgentKey: "agent key here", 13 | AppName: "My Go App", 14 | AppEnvironment: "mydevenv", 15 | DisableAutoProfiling: true, 16 | }) 17 | 18 | agent.StartCPUProfiler() 19 | 20 | // simulate CPU work 21 | for i := 0; i < 100000000; i++ { 22 | rand.Intn(1000) 23 | } 24 | 25 | agent.StopCPUProfiler() 26 | } 27 | -------------------------------------------------------------------------------- /internal/agent.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/hex" 6 | "fmt" 7 | "io" 8 | "log" 9 | "math/rand" 10 | "net/http" 11 | "os" 12 | "path/filepath" 13 | "strconv" 14 | "sync" 15 | "sync/atomic" 16 | "time" 17 | ) 18 | 19 | const AgentVersion = "2.3.10" 20 | const SAASDashboardAddress = "https://agent-api.stackimpact.com" 21 | 22 | var agentPath = filepath.Join("github.com", "stackimpact", "stackimpact-go") 23 | var agentPathInternal = filepath.Join("github.com", "stackimpact", "stackimpact-go", "internal") 24 | var agentPathExamples = filepath.Join("github.com", "stackimpact", "stackimpact-go", "examples") 25 | 26 | var agentStarted bool = false 27 | 28 | type Agent struct { 29 | randSource *rand.Rand 30 | randLock *sync.Mutex 31 | nextId int64 32 | buildId string 33 | runId string 34 | runTs int64 35 | 36 | apiRequest *APIRequest 37 | config *Config 38 | configLoader *ConfigLoader 39 | messageQueue *MessageQueue 40 | processReporter *ProcessReporter 41 | cpuReporter *ProfileReporter 42 | allocationReporter *ProfileReporter 43 | blockReporter *ProfileReporter 44 | spanReporter *SpanReporter 45 | errorReporter *ErrorReporter 46 | 47 | profilerActive *Flag 48 | 49 | // Options 50 | DashboardAddress string 51 | ProxyAddress string 52 | AgentKey string 53 | AppName string 54 | AppVersion string 55 | AppEnvironment string 56 | HostName string 57 | AutoProfiling bool 58 | Debug bool 59 | Logger *log.Logger 60 | ProfileAgent bool 61 | HTTPClient *http.Client 62 | } 63 | 64 | func NewAgent() *Agent { 65 | a := &Agent{ 66 | randSource: rand.New(rand.NewSource(time.Now().UnixNano())), 67 | randLock: &sync.Mutex{}, 68 | nextId: 0, 69 | runId: "", 70 | buildId: "", 71 | runTs: time.Now().Unix(), 72 | 73 | apiRequest: nil, 74 | config: nil, 75 | configLoader: nil, 76 | messageQueue: nil, 77 | processReporter: nil, 78 | cpuReporter: nil, 79 | allocationReporter: nil, 80 | blockReporter: nil, 81 | spanReporter: nil, 82 | errorReporter: nil, 83 | 84 | profilerActive: &Flag{}, 85 | 86 | DashboardAddress: SAASDashboardAddress, 87 | ProxyAddress: "", 88 | AgentKey: "", 89 | AppName: "", 90 | AppVersion: "", 91 | AppEnvironment: "", 92 | HostName: "", 93 | AutoProfiling: true, 94 | Debug: false, 95 | Logger: log.New(os.Stdout, "", 0), 96 | ProfileAgent: false, 97 | HTTPClient: nil, 98 | } 99 | 100 | a.buildId = a.calculateProgramSHA1() 101 | a.runId = a.uuid() 102 | 103 | a.apiRequest = newAPIRequest(a) 104 | a.config = newConfig(a) 105 | a.configLoader = newConfigLoader(a) 106 | a.messageQueue = newMessageQueue(a) 107 | a.processReporter = newProcessReporter(a) 108 | 109 | cpuProfiler := newCPUProfiler(a) 110 | cpuProfilerConfig := &ProfilerConfig{ 111 | logPrefix: "CPU profiler", 112 | maxProfileDuration: 20, 113 | maxSpanDuration: 2, 114 | maxSpanCount: 30, 115 | spanInterval: 8, 116 | reportInterval: 120, 117 | } 118 | a.cpuReporter = newProfileReporter(a, cpuProfiler, cpuProfilerConfig) 119 | 120 | allocationProfiler := newAllocationProfiler(a) 121 | allocationProfilerConfig := &ProfilerConfig{ 122 | logPrefix: "Allocation profiler", 123 | reportOnly: true, 124 | reportInterval: 120, 125 | } 126 | a.allocationReporter = newProfileReporter(a, allocationProfiler, allocationProfilerConfig) 127 | 128 | blockProfiler := newBlockProfiler(a) 129 | blockProfilerConfig := &ProfilerConfig{ 130 | logPrefix: "Block profiler", 131 | maxProfileDuration: 20, 132 | maxSpanDuration: 4, 133 | maxSpanCount: 30, 134 | spanInterval: 16, 135 | reportInterval: 120, 136 | } 137 | a.blockReporter = newProfileReporter(a, blockProfiler, blockProfilerConfig) 138 | 139 | a.spanReporter = newSpanReporter(a) 140 | a.errorReporter = newErrorReporter(a) 141 | 142 | return a 143 | } 144 | 145 | func (a *Agent) Start() { 146 | if agentStarted { 147 | return 148 | } 149 | agentStarted = true 150 | 151 | if a.HostName == "" { 152 | hostName, err := os.Hostname() 153 | if err != nil { 154 | a.error(err) 155 | } 156 | 157 | if hostName != "" { 158 | a.HostName = hostName 159 | } else { 160 | a.HostName = "unknown" 161 | } 162 | } 163 | 164 | a.configLoader.start() 165 | a.messageQueue.start() 166 | 167 | a.log("Agent started.") 168 | 169 | return 170 | } 171 | 172 | func (a *Agent) Enable() { 173 | if !a.config.isAgentEnabled() { 174 | a.cpuReporter.start() 175 | a.allocationReporter.start() 176 | a.blockReporter.start() 177 | a.spanReporter.start() 178 | a.errorReporter.start() 179 | a.processReporter.start() 180 | a.config.setAgentEnabled(true) 181 | } 182 | } 183 | 184 | func (a *Agent) Disable() { 185 | if a.config.isAgentEnabled() { 186 | a.config.setAgentEnabled(false) 187 | a.cpuReporter.stop() 188 | a.allocationReporter.stop() 189 | a.blockReporter.stop() 190 | a.spanReporter.stop() 191 | a.errorReporter.stop() 192 | a.processReporter.stop() 193 | } 194 | } 195 | 196 | func (a *Agent) StartProfiling(workload string) bool { 197 | defer a.recoverAndLog() 198 | 199 | if rand.Intn(2) == 0 { 200 | return a.cpuReporter.startProfiling(true, true, workload) || a.blockReporter.startProfiling(true, true, workload) 201 | } else { 202 | return a.blockReporter.startProfiling(true, true, workload) || a.cpuReporter.startProfiling(true, true, workload) 203 | } 204 | } 205 | 206 | func (a *Agent) StopProfiling() { 207 | defer a.recoverAndLog() 208 | 209 | a.cpuReporter.stopProfiling() 210 | a.blockReporter.stopProfiling() 211 | } 212 | 213 | func (a *Agent) StartCPUProfiler() { 214 | if !agentStarted || a.AutoProfiling { 215 | return 216 | } 217 | 218 | defer a.recoverAndLog() 219 | 220 | a.cpuReporter.start() 221 | a.cpuReporter.startProfiling(true, false, "") 222 | } 223 | 224 | func (a *Agent) StopCPUProfiler() { 225 | if !agentStarted || a.AutoProfiling { 226 | return 227 | } 228 | 229 | defer a.recoverAndLog() 230 | 231 | a.cpuReporter.stopProfiling() 232 | a.cpuReporter.report(false) 233 | a.cpuReporter.stop() 234 | a.messageQueue.flush(false) 235 | } 236 | 237 | func (a *Agent) StartBlockProfiler() { 238 | if !agentStarted || a.AutoProfiling { 239 | return 240 | } 241 | 242 | defer a.recoverAndLog() 243 | 244 | a.blockReporter.start() 245 | a.blockReporter.startProfiling(true, false, "") 246 | } 247 | 248 | func (a *Agent) StopBlockProfiler() { 249 | if !agentStarted || a.AutoProfiling { 250 | return 251 | } 252 | 253 | defer a.recoverAndLog() 254 | 255 | a.blockReporter.stopProfiling() 256 | a.blockReporter.report(false) 257 | a.blockReporter.stop() 258 | a.messageQueue.flush(false) 259 | } 260 | 261 | func (a *Agent) ReportAllocationProfile() { 262 | if !agentStarted || a.AutoProfiling { 263 | return 264 | } 265 | 266 | defer a.recoverAndLog() 267 | 268 | a.allocationReporter.start() 269 | a.allocationReporter.spanTrigger = TriggerAPI 270 | a.allocationReporter.report(false) 271 | a.allocationReporter.stop() 272 | a.messageQueue.flush(false) 273 | } 274 | 275 | func (a *Agent) RecordSpan(name string, duration float64) { 276 | if !agentStarted { 277 | return 278 | } 279 | 280 | a.spanReporter.recordSpan(name, duration) 281 | } 282 | 283 | func (a *Agent) RecordError(group string, msg interface{}, skipFrames int) { 284 | if !agentStarted { 285 | return 286 | } 287 | 288 | var err error 289 | switch v := msg.(type) { 290 | case error: 291 | err = v 292 | default: 293 | err = fmt.Errorf("%v", v) 294 | } 295 | 296 | a.errorReporter.recordError(group, err, skipFrames+1) 297 | } 298 | 299 | func (a *Agent) Report() { 300 | if !agentStarted || a.AutoProfiling { 301 | return 302 | } 303 | 304 | defer a.recoverAndLog() 305 | 306 | a.configLoader.load() 307 | 308 | a.cpuReporter.report(true) 309 | a.allocationReporter.report(true) 310 | a.blockReporter.report(true) 311 | 312 | a.messageQueue.flush(true) 313 | } 314 | 315 | func (a *Agent) log(format string, values ...interface{}) { 316 | if a.Debug { 317 | a.Logger.Printf("["+time.Now().Format(time.StampMilli)+"]"+ 318 | " StackImpact "+AgentVersion+": "+ 319 | format+"\n", values...) 320 | } 321 | } 322 | 323 | func (a *Agent) error(err error) { 324 | if a.Debug { 325 | a.Logger.Println("[" + time.Now().Format(time.StampMilli) + "]" + 326 | " StackImpact " + AgentVersion + ": Error") 327 | a.Logger.Println(err) 328 | } 329 | } 330 | 331 | func (a *Agent) recoverAndLog() { 332 | if err := recover(); err != nil { 333 | a.log("Recovered from panic in agent: %v", err) 334 | } 335 | } 336 | 337 | func (a *Agent) uuid() string { 338 | n := atomic.AddInt64(&a.nextId, 1) 339 | 340 | uuid := 341 | strconv.FormatInt(time.Now().Unix(), 10) + 342 | strconv.FormatInt(a.random(1000000000), 10) + 343 | strconv.FormatInt(n, 10) 344 | 345 | return sha1String(uuid) 346 | } 347 | 348 | func (a *Agent) random(max int64) int64 { 349 | a.randLock.Lock() 350 | defer a.randLock.Unlock() 351 | 352 | return a.randSource.Int63n(max) 353 | } 354 | 355 | func sha1String(s string) string { 356 | sha1 := sha1.New() 357 | sha1.Write([]byte(s)) 358 | 359 | return hex.EncodeToString(sha1.Sum(nil)) 360 | } 361 | 362 | func (a *Agent) calculateProgramSHA1() string { 363 | file, err := os.Open(os.Args[0]) 364 | if err != nil { 365 | a.error(err) 366 | return "" 367 | } 368 | defer file.Close() 369 | 370 | hash := sha1.New() 371 | if _, err := io.Copy(hash, file); err != nil { 372 | a.error(err) 373 | return "" 374 | } 375 | 376 | return hex.EncodeToString(hash.Sum(nil)) 377 | } 378 | 379 | type Timer struct { 380 | agent *Agent 381 | delayTimer *time.Timer 382 | delayTimerDone chan bool 383 | intervalTicker *time.Ticker 384 | intervalTickerDone chan bool 385 | stopped bool 386 | } 387 | 388 | func NewTimer(agent *Agent, delay time.Duration, interval time.Duration, job func()) *Timer { 389 | t := &Timer{ 390 | agent: agent, 391 | stopped: false, 392 | } 393 | 394 | t.delayTimerDone = make(chan bool) 395 | t.delayTimer = time.NewTimer(delay) 396 | go func() { 397 | defer t.agent.recoverAndLog() 398 | 399 | select { 400 | case <-t.delayTimer.C: 401 | if interval > 0 { 402 | t.intervalTickerDone = make(chan bool) 403 | t.intervalTicker = time.NewTicker(interval) 404 | go func() { 405 | defer t.agent.recoverAndLog() 406 | 407 | for { 408 | select { 409 | case <-t.intervalTicker.C: 410 | job() 411 | case <-t.intervalTickerDone: 412 | return 413 | } 414 | } 415 | }() 416 | } 417 | 418 | if delay > 0 { 419 | job() 420 | } 421 | case <-t.delayTimerDone: 422 | return 423 | } 424 | }() 425 | 426 | return t 427 | } 428 | 429 | func (t *Timer) Stop() { 430 | if !t.stopped { 431 | t.stopped = true 432 | 433 | t.delayTimer.Stop() 434 | close(t.delayTimerDone) 435 | 436 | if t.intervalTicker != nil { 437 | t.intervalTicker.Stop() 438 | close(t.intervalTickerDone) 439 | } 440 | } 441 | } 442 | 443 | func (a *Agent) createTimer(delay time.Duration, interval time.Duration, job func()) *Timer { 444 | return NewTimer(a, delay, interval, job) 445 | } 446 | 447 | type Flag struct { 448 | value int32 449 | } 450 | 451 | func (f *Flag) SetIfUnset() bool { 452 | return atomic.CompareAndSwapInt32(&f.value, 0, 1) 453 | } 454 | 455 | func (f *Flag) UnsetIfSet() bool { 456 | return atomic.CompareAndSwapInt32(&f.value, 1, 0) 457 | } 458 | 459 | func (f *Flag) Set() { 460 | atomic.StoreInt32(&f.value, 1) 461 | } 462 | 463 | func (f *Flag) Unset() { 464 | atomic.StoreInt32(&f.value, 0) 465 | } 466 | 467 | func (f *Flag) IsSet() bool { 468 | return atomic.LoadInt32(&f.value) == 1 469 | } 470 | -------------------------------------------------------------------------------- /internal/agent_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "compress/gzip" 5 | "fmt" 6 | "io/ioutil" 7 | "math/rand" 8 | "net/http" 9 | "net/http/httptest" 10 | "strings" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | func TestStart(t *testing.T) { 16 | agent := NewAgent() 17 | agent.DashboardAddress = "http://localhost:5000" 18 | agent.AgentKey = "key" 19 | agent.AppName = "GoTestApp" 20 | agent.Debug = true 21 | agent.Start() 22 | 23 | if agent.AgentKey == "" { 24 | t.Error("AgentKey not set") 25 | } 26 | 27 | if agent.AppName == "" { 28 | t.Error("AppName not set") 29 | } 30 | 31 | if agent.HostName == "" { 32 | t.Error("HostName not set") 33 | } 34 | } 35 | 36 | func TestCalculateProgramSHA1(t *testing.T) { 37 | agent := NewAgent() 38 | agent.Debug = true 39 | hash := agent.calculateProgramSHA1() 40 | 41 | if hash == "" { 42 | t.Error("failed calculating program SHA1") 43 | } 44 | } 45 | 46 | func TestStartStopProfiling(t *testing.T) { 47 | agent := NewAgent() 48 | agent.Debug = true 49 | agent.AutoProfiling = false 50 | 51 | agent.cpuReporter.start() 52 | agent.StartProfiling("") 53 | 54 | time.Sleep(50 * time.Millisecond) 55 | 56 | agent.StopProfiling() 57 | 58 | if agent.cpuReporter.profileDuration == 0 { 59 | t.Error("profileDuration should be > 0") 60 | } 61 | } 62 | 63 | func TestManualCPUProfiler(t *testing.T) { 64 | payload := make(chan string) 65 | 66 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 67 | zr, _ := gzip.NewReader(r.Body) 68 | body, _ := ioutil.ReadAll(zr) 69 | payload <- string(body) 70 | fmt.Fprintf(w, "{}") 71 | })) 72 | defer server.Close() 73 | 74 | agent := NewAgent() 75 | agent.Debug = true 76 | agent.AutoProfiling = false 77 | agent.ProfileAgent = true 78 | agent.DashboardAddress = server.URL 79 | 80 | go func() { 81 | agent.StartCPUProfiler() 82 | time.Sleep(250 * time.Millisecond) 83 | 84 | for i := 0; i < 10000000; i++ { 85 | rand.Intn(1000) 86 | } 87 | 88 | time.Sleep(250 * time.Millisecond) 89 | agent.StopCPUProfiler() 90 | }() 91 | 92 | payloadJson := <-payload 93 | if !strings.Contains(payloadJson, "Intn") { 94 | t.Error("The test function is not found in the payload") 95 | } 96 | } 97 | 98 | func TestManualBlockProfiler(t *testing.T) { 99 | payload := make(chan string) 100 | 101 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 102 | zr, _ := gzip.NewReader(r.Body) 103 | body, _ := ioutil.ReadAll(zr) 104 | payload <- string(body) 105 | fmt.Fprintf(w, "{}") 106 | })) 107 | defer server.Close() 108 | 109 | agent := NewAgent() 110 | agent.Debug = true 111 | agent.AutoProfiling = false 112 | agent.ProfileAgent = true 113 | agent.DashboardAddress = server.URL 114 | 115 | go func() { 116 | agent.StartBlockProfiler() 117 | time.Sleep(250 * time.Millisecond) 118 | 119 | wait := make(chan bool) 120 | 121 | for i := 0; i < 10; i++ { 122 | go func() { 123 | time.Sleep(250 * time.Millisecond) 124 | wait <- true 125 | }() 126 | <-wait 127 | } 128 | 129 | agent.StopBlockProfiler() 130 | }() 131 | 132 | time.Sleep(250 * time.Millisecond) 133 | 134 | payloadJson := <-payload 135 | if !strings.Contains(payloadJson, "TestManualBlockProfiler") { 136 | t.Error("The test function is not found in the payload") 137 | } 138 | } 139 | 140 | func TestManualAllocationProfiler(t *testing.T) { 141 | payload := make(chan string) 142 | 143 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 144 | zr, _ := gzip.NewReader(r.Body) 145 | body, _ := ioutil.ReadAll(zr) 146 | payload <- string(body) 147 | fmt.Fprintf(w, "{}") 148 | })) 149 | defer server.Close() 150 | 151 | agent := NewAgent() 152 | agent.Debug = true 153 | agent.AutoProfiling = false 154 | agent.ProfileAgent = true 155 | agent.DashboardAddress = server.URL 156 | 157 | go func() { 158 | objs = make([]string, 0) 159 | for i := 0; i < 100000; i++ { 160 | objs = append(objs, string(i)) 161 | } 162 | 163 | agent.ReportAllocationProfile() 164 | }() 165 | 166 | payloadJson := <-payload 167 | if !strings.Contains(payloadJson, "TestManualAllocationProfiler") { 168 | t.Error("The test function is not found in the payload") 169 | } 170 | } 171 | 172 | func TestTimerPeriod(t *testing.T) { 173 | agent := NewAgent() 174 | agent.Debug = true 175 | 176 | fired := 0 177 | timer := agent.createTimer(0, 10*time.Millisecond, func() { 178 | fired++ 179 | }) 180 | 181 | time.Sleep(20 * time.Millisecond) 182 | 183 | timer.Stop() 184 | 185 | time.Sleep(30 * time.Millisecond) 186 | 187 | if fired > 2 { 188 | t.Errorf("interval fired too many times: %v", fired) 189 | } 190 | } 191 | 192 | func TestTimerDelay(t *testing.T) { 193 | agent := NewAgent() 194 | agent.Debug = true 195 | 196 | fired := 0 197 | timer := agent.createTimer(10*time.Millisecond, 0, func() { 198 | fired++ 199 | }) 200 | 201 | time.Sleep(20 * time.Millisecond) 202 | 203 | timer.Stop() 204 | 205 | if fired != 1 { 206 | t.Errorf("delay should fire once: %v", fired) 207 | } 208 | } 209 | 210 | func TestTimerDelayStop(t *testing.T) { 211 | agent := NewAgent() 212 | agent.Debug = true 213 | 214 | fired := 0 215 | timer := agent.createTimer(10*time.Millisecond, 0, func() { 216 | fired++ 217 | }) 218 | 219 | timer.Stop() 220 | 221 | time.Sleep(20 * time.Millisecond) 222 | 223 | if fired == 1 { 224 | t.Errorf("delay should not fire") 225 | } 226 | } 227 | -------------------------------------------------------------------------------- /internal/allocation_profiler.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "math" 9 | "runtime" 10 | "runtime/pprof" 11 | 12 | "github.com/stackimpact/stackimpact-go/internal/pprof/profile" 13 | ) 14 | 15 | type recordSorter []runtime.MemProfileRecord 16 | 17 | func (x recordSorter) Len() int { 18 | return len(x) 19 | } 20 | 21 | func (x recordSorter) Swap(i, j int) { 22 | x[i], x[j] = x[j], x[i] 23 | } 24 | 25 | func (x recordSorter) Less(i, j int) bool { 26 | return x[i].InUseBytes() > x[j].InUseBytes() 27 | } 28 | 29 | func readMemAlloc() float64 { 30 | memStats := &runtime.MemStats{} 31 | runtime.ReadMemStats(memStats) 32 | return float64(memStats.Alloc) 33 | } 34 | 35 | type AllocationProfiler struct { 36 | agent *Agent 37 | } 38 | 39 | func newAllocationProfiler(agent *Agent) *AllocationProfiler { 40 | ar := &AllocationProfiler{ 41 | agent: agent, 42 | } 43 | 44 | return ar 45 | } 46 | 47 | func (ap *AllocationProfiler) reset() { 48 | } 49 | 50 | func (ap *AllocationProfiler) startProfiler() error { 51 | return nil 52 | } 53 | 54 | func (ap *AllocationProfiler) stopProfiler() error { 55 | return nil 56 | } 57 | 58 | func (ap *AllocationProfiler) buildProfile(duration int64, workloads map[string]int64) ([]*ProfileData, error) { 59 | p, err := ap.readHeapProfile() 60 | if err != nil { 61 | return nil, err 62 | } 63 | if p == nil { 64 | return nil, errors.New("no profile returned") 65 | } 66 | 67 | if callGraph, aerr := ap.createAllocationCallGraph(p); err != nil { 68 | return nil, aerr 69 | } else { 70 | callGraph.propagate() 71 | // filter calls with lower than 10KB 72 | callGraph.filter(2, 10000, math.Inf(0)) 73 | 74 | data := []*ProfileData{ 75 | &ProfileData{ 76 | category: CategoryMemoryProfile, 77 | name: NameHeapAllocation, 78 | unit: UnitByte, 79 | unitInterval: 0, 80 | profile: callGraph, 81 | }, 82 | } 83 | 84 | return data, nil 85 | } 86 | } 87 | 88 | func (ap *AllocationProfiler) createAllocationCallGraph(p *profile.Profile) (*BreakdownNode, error) { 89 | // find "inuse_space" type index 90 | inuseSpaceTypeIndex := -1 91 | for i, s := range p.SampleType { 92 | if s.Type == "inuse_space" { 93 | inuseSpaceTypeIndex = i 94 | break 95 | } 96 | } 97 | 98 | // find "inuse_space" type index 99 | inuseObjectsTypeIndex := -1 100 | for i, s := range p.SampleType { 101 | if s.Type == "inuse_objects" { 102 | inuseObjectsTypeIndex = i 103 | break 104 | } 105 | } 106 | 107 | if inuseSpaceTypeIndex == -1 || inuseObjectsTypeIndex == -1 { 108 | return nil, errors.New("Unrecognized profile data") 109 | } 110 | 111 | // build call graph 112 | rootNode := newBreakdownNode("Allocation call graph") 113 | rootNode.setType(BreakdownTypeCallgraph) 114 | 115 | for _, s := range p.Sample { 116 | if !ap.agent.ProfileAgent && isAgentStack(s) { 117 | continue 118 | } 119 | 120 | value := s.Value[inuseSpaceTypeIndex] 121 | count := s.Value[inuseObjectsTypeIndex] 122 | if value == 0 { 123 | continue 124 | } 125 | 126 | currentNode := rootNode 127 | for i := len(s.Location) - 1; i >= 0; i-- { 128 | l := s.Location[i] 129 | funcName, fileName, fileLine := readFuncInfo(l) 130 | 131 | if (!ap.agent.ProfileAgent && isAgentFrame(fileName)) || funcName == "runtime.goexit" { 132 | continue 133 | } 134 | 135 | frameName := fmt.Sprintf("%v (%v:%v)", funcName, fileName, fileLine) 136 | currentNode = currentNode.findOrAddChild(frameName) 137 | currentNode.setType(BreakdownTypeCallsite) 138 | } 139 | currentNode.increment(float64(value), int64(count)) 140 | } 141 | 142 | return rootNode, nil 143 | } 144 | 145 | func (ap *AllocationProfiler) readHeapProfile() (*profile.Profile, error) { 146 | var buf bytes.Buffer 147 | w := bufio.NewWriter(&buf) 148 | 149 | err := pprof.WriteHeapProfile(w) 150 | if err != nil { 151 | return nil, err 152 | } 153 | 154 | w.Flush() 155 | r := bufio.NewReader(&buf) 156 | 157 | if p, perr := profile.Parse(r); perr == nil { 158 | if serr := symbolizeProfile(p); serr != nil { 159 | return nil, serr 160 | } 161 | 162 | if verr := p.CheckValid(); verr != nil { 163 | return nil, verr 164 | } 165 | 166 | return p, nil 167 | } else { 168 | return nil, perr 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /internal/allocation_profiler_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "strings" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | var objs []string 12 | 13 | func TestCreateAllocationCallGraph(t *testing.T) { 14 | agent := NewAgent() 15 | agent.Debug = true 16 | agent.ProfileAgent = true 17 | 18 | objs = make([]string, 0) 19 | for i := 0; i < 100000; i++ { 20 | objs = append(objs, string(i)) 21 | } 22 | 23 | runtime.GC() 24 | runtime.GC() 25 | 26 | allocationProfiler := newAllocationProfiler(agent) 27 | 28 | time.Sleep(250 * time.Millisecond) 29 | 30 | p, _ := allocationProfiler.readHeapProfile() 31 | 32 | // size 33 | callGraph, err := allocationProfiler.createAllocationCallGraph(p) 34 | if err != nil { 35 | t.Error(err) 36 | return 37 | } 38 | callGraph.propagate() 39 | //fmt.Printf("ALLOCATED SIZE: %f\n", callGraph.measurement) 40 | //fmt.Printf("CALL GRAPH: %v\n", callGraph.printLevel(0)) 41 | if callGraph.measurement < 100000 { 42 | t.Error("Allocated size is too low") 43 | } 44 | if callGraph.numSamples < 1 { 45 | t.Error("Number of samples should be > 0") 46 | } 47 | 48 | if !strings.Contains(fmt.Sprintf("%v", callGraph.toMap()), "TestCreateAllocationCallGraph") { 49 | t.Error("The test function is not found in the profile") 50 | } 51 | 52 | objs = nil 53 | } 54 | -------------------------------------------------------------------------------- /internal/api_request.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bytes" 5 | "compress/gzip" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io/ioutil" 10 | "net/http" 11 | "net/url" 12 | "os" 13 | "runtime" 14 | "strconv" 15 | "time" 16 | ) 17 | 18 | type APIRequest struct { 19 | agent *Agent 20 | } 21 | 22 | func newAPIRequest(agent *Agent) *APIRequest { 23 | ar := &APIRequest{ 24 | agent: agent, 25 | } 26 | 27 | return ar 28 | } 29 | 30 | func (ar *APIRequest) post(endpoint string, payload map[string]interface{}) (map[string]interface{}, error) { 31 | reqBody := map[string]interface{}{ 32 | "runtime_type": "go", 33 | "runtime_version": runtime.Version(), 34 | "agent_version": AgentVersion, 35 | "app_name": ar.agent.AppName, 36 | "app_version": ar.agent.AppVersion, 37 | "app_environment": ar.agent.AppEnvironment, 38 | "host_name": ar.agent.HostName, 39 | "process_id": strconv.Itoa(os.Getpid()), 40 | "build_id": ar.agent.buildId, 41 | "run_id": ar.agent.runId, 42 | "run_ts": ar.agent.runTs, 43 | "sent_at": time.Now().Unix(), 44 | "payload": payload, 45 | } 46 | 47 | gopath, exists := os.LookupEnv("GOPATH") 48 | if exists { 49 | reqBody["runtime_path"] = gopath 50 | } 51 | 52 | reqBodyJson, _ := json.Marshal(reqBody) 53 | 54 | var buf bytes.Buffer 55 | w := gzip.NewWriter(&buf) 56 | w.Write(reqBodyJson) 57 | w.Close() 58 | 59 | u := ar.agent.DashboardAddress + "/agent/v1/" + endpoint 60 | req, err := http.NewRequest("POST", u, &buf) 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | req.SetBasicAuth(ar.agent.AgentKey, "") 66 | req.Header.Set("Content-Type", "application/json") 67 | req.Header.Set("Content-Encoding", "gzip") 68 | 69 | ar.agent.log("Posting API request to %v", u) 70 | 71 | var httpClient *http.Client 72 | if ar.agent.HTTPClient != nil { 73 | httpClient = ar.agent.HTTPClient 74 | } else if ar.agent.ProxyAddress != "" { 75 | proxyURL, err := url.Parse(ar.agent.ProxyAddress) 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | httpClient = &http.Client{ 81 | Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}, 82 | Timeout: time.Second * 20, 83 | } 84 | } else { 85 | httpClient = &http.Client{ 86 | Timeout: time.Second * 20, 87 | } 88 | } 89 | res, err := httpClient.Do(req) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | resBodyJson, err := ioutil.ReadAll(res.Body) 95 | defer res.Body.Close() 96 | if err != nil { 97 | return nil, err 98 | } 99 | 100 | if res.StatusCode != 200 { 101 | return nil, errors.New(fmt.Sprintf("Received %v: %v", res.StatusCode, string(resBodyJson))) 102 | } else { 103 | var resBody map[string]interface{} 104 | if err := json.Unmarshal(resBodyJson, &resBody); err != nil { 105 | return nil, errors.New(fmt.Sprintf("Cannot parse response body %v", string(resBodyJson))) 106 | } else { 107 | return resBody, nil 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /internal/api_request_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "compress/gzip" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "net/http/httptest" 10 | "runtime" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | func TestPost(t *testing.T) { 16 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 17 | gr, _ := gzip.NewReader(r.Body) 18 | bodyJson, _ := ioutil.ReadAll(gr) 19 | gr.Close() 20 | 21 | var body map[string]interface{} 22 | if err := json.Unmarshal(bodyJson, &body); err != nil { 23 | t.Error(err) 24 | } else { 25 | if prop, ok := body["runtime_type"]; !ok || prop.(string) != "go" { 26 | t.Errorf("Invalid or missing property: %v", "runtime_type") 27 | } 28 | 29 | if prop, ok := body["runtime_version"]; !ok || prop.(string) != runtime.Version() { 30 | t.Errorf("Invalid or missing property: %v", "runtime_version") 31 | } 32 | 33 | if prop, ok := body["agent_version"]; !ok || prop.(string) != AgentVersion { 34 | t.Errorf("Invalid or missing property: %v", "agent_version") 35 | } 36 | 37 | if prop, ok := body["app_name"]; !ok || prop.(string) != "App1" { 38 | t.Errorf("Invalid or missing property: %v", "app_name") 39 | } 40 | 41 | if prop, ok := body["host_name"]; !ok || prop.(string) != "Host1" { 42 | t.Errorf("Invalid or missing property: %v", "host_name") 43 | } 44 | 45 | if _, ok := body["run_id"]; !ok { 46 | t.Errorf("Invalid or missing property: %v", "run_id") 47 | } 48 | 49 | if prop, ok := body["run_ts"]; !ok || prop.(float64) < float64(time.Now().Unix()-60) { 50 | t.Errorf("Invalid or missing property: %v", "run_ts") 51 | } 52 | 53 | if prop, ok := body["sent_at"]; !ok || prop.(float64) < float64(time.Now().Unix()-60) { 54 | t.Errorf("Invalid or missing property: %v", "sent_at") 55 | } 56 | 57 | if _, ok := body["payload"]; !ok { 58 | t.Errorf("Invalid or missing property: %v", "payload") 59 | } else { 60 | payload := body["payload"].(map[string]interface{}) 61 | 62 | if payload["a"].(float64) != 1 { 63 | t.Error(fmt.Sprintf("Invalid payload received: %v", payload)) 64 | } 65 | } 66 | } 67 | 68 | fmt.Fprintf(w, "{}") 69 | })) 70 | defer server.Close() 71 | 72 | agent := NewAgent() 73 | agent.AppName = "App1" 74 | agent.HostName = "Host1" 75 | agent.Debug = true 76 | agent.DashboardAddress = server.URL 77 | 78 | p := map[string]interface{}{ 79 | "a": 1, 80 | } 81 | agent.apiRequest.post("test", p) 82 | } 83 | -------------------------------------------------------------------------------- /internal/block_profiler.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "math" 9 | "runtime" 10 | "runtime/pprof" 11 | 12 | profile "github.com/stackimpact/stackimpact-go/internal/pprof/profile" 13 | ) 14 | 15 | type BlockValues struct { 16 | delay float64 17 | contentions int64 18 | } 19 | 20 | type BlockProfiler struct { 21 | agent *Agent 22 | blockProfile *BreakdownNode 23 | blockTrace *BreakdownNode 24 | prevValues map[string]*BlockValues 25 | partialProfile *pprof.Profile 26 | } 27 | 28 | func newBlockProfiler(agent *Agent) *BlockProfiler { 29 | br := &BlockProfiler{ 30 | agent: agent, 31 | blockProfile: nil, 32 | blockTrace: nil, 33 | prevValues: make(map[string]*BlockValues), 34 | partialProfile: nil, 35 | } 36 | 37 | return br 38 | } 39 | 40 | func (bp *BlockProfiler) reset() { 41 | bp.blockProfile = newBreakdownNode("Block call graph") 42 | bp.blockProfile.setType(BreakdownTypeCallgraph) 43 | 44 | bp.blockTrace = newBreakdownNode("Block call graph") 45 | bp.blockTrace.setType(BreakdownTypeCallgraph) 46 | } 47 | 48 | func (bp *BlockProfiler) startProfiler() error { 49 | err := bp.startBlockProfiler() 50 | if err != nil { 51 | return err 52 | } 53 | 54 | return nil 55 | } 56 | 57 | func (bp *BlockProfiler) stopProfiler() error { 58 | p, err := bp.stopBlockProfiler() 59 | if err != nil { 60 | return err 61 | } 62 | if p == nil { 63 | return errors.New("no profile returned") 64 | } 65 | 66 | if uerr := bp.updateBlockProfile(p); uerr != nil { 67 | return uerr 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func (bp *BlockProfiler) buildProfile(duration int64, workloads map[string]int64) ([]*ProfileData, error) { 74 | bp.blockProfile.normalize(float64(duration) / 1e9) 75 | bp.blockProfile.propagate() 76 | bp.blockProfile.filter(2, 1, math.Inf(0)) 77 | 78 | bp.blockTrace.evaluateP95() 79 | bp.blockTrace.propagate() 80 | bp.blockTrace.round() 81 | bp.blockTrace.filter(2, 1, math.Inf(0)) 82 | 83 | data := []*ProfileData{ 84 | &ProfileData{ 85 | category: CategoryBlockProfile, 86 | name: NameBlockingCallTimes, 87 | unit: UnitMillisecond, 88 | unitInterval: 1, 89 | profile: bp.blockProfile, 90 | }, 91 | &ProfileData{ 92 | category: CategoryBlockTrace, 93 | name: NameBlockingCallTimes, 94 | unit: UnitMillisecond, 95 | unitInterval: 0, 96 | profile: bp.blockTrace, 97 | }, 98 | } 99 | 100 | return data, nil 101 | } 102 | 103 | func (bp *BlockProfiler) updateBlockProfile(p *profile.Profile) error { 104 | contentionIndex := -1 105 | delayIndex := -1 106 | for i, s := range p.SampleType { 107 | if s.Type == "contentions" { 108 | contentionIndex = i 109 | } else if s.Type == "delay" { 110 | delayIndex = i 111 | } 112 | } 113 | 114 | if contentionIndex == -1 || delayIndex == -1 { 115 | return errors.New("Unrecognized profile data") 116 | } 117 | 118 | for _, s := range p.Sample { 119 | if !bp.agent.ProfileAgent && isAgentStack(s) { 120 | continue 121 | } 122 | 123 | delay := float64(s.Value[delayIndex]) 124 | contentions := s.Value[contentionIndex] 125 | 126 | valueKey := generateValueKey(s) 127 | delay, contentions = bp.getValueChange(valueKey, delay, contentions) 128 | 129 | if contentions == 0 || delay == 0 { 130 | continue 131 | } 132 | 133 | // to milliseconds 134 | delay = delay / 1e6 135 | 136 | currentNode := bp.blockProfile 137 | for i := len(s.Location) - 1; i >= 0; i-- { 138 | l := s.Location[i] 139 | funcName, fileName, fileLine := readFuncInfo(l) 140 | 141 | if (!bp.agent.ProfileAgent && isAgentFrame(fileName)) || funcName == "runtime.goexit" { 142 | continue 143 | } 144 | 145 | frameName := fmt.Sprintf("%v (%v:%v)", funcName, fileName, fileLine) 146 | currentNode = currentNode.findOrAddChild(frameName) 147 | currentNode.setType(BreakdownTypeCallsite) 148 | } 149 | currentNode.increment(delay, contentions) 150 | 151 | currentNode = bp.blockTrace 152 | for i := len(s.Location) - 1; i >= 0; i-- { 153 | l := s.Location[i] 154 | funcName, fileName, fileLine := readFuncInfo(l) 155 | 156 | if (!bp.agent.ProfileAgent && isAgentFrame(fileName)) || funcName == "runtime.goexit" { 157 | continue 158 | } 159 | 160 | frameName := fmt.Sprintf("%v (%v:%v)", funcName, fileName, fileLine) 161 | currentNode = currentNode.findOrAddChild(frameName) 162 | currentNode.setType(BreakdownTypeCallsite) 163 | } 164 | currentNode.updateP95(delay / float64(contentions)) 165 | } 166 | 167 | return nil 168 | } 169 | 170 | func generateValueKey(s *profile.Sample) string { 171 | key := "" 172 | for _, l := range s.Location { 173 | key += fmt.Sprintf("%v:", l.Address) 174 | } 175 | 176 | return key 177 | } 178 | 179 | func (bp *BlockProfiler) getValueChange(key string, delay float64, contentions int64) (float64, int64) { 180 | if pv, exists := bp.prevValues[key]; exists { 181 | delayChange := delay - pv.delay 182 | contentionsChange := contentions - pv.contentions 183 | 184 | pv.delay = delay 185 | pv.contentions = contentions 186 | 187 | return delayChange, contentionsChange 188 | } else { 189 | bp.prevValues[key] = &BlockValues{ 190 | delay: delay, 191 | contentions: contentions, 192 | } 193 | 194 | return delay, contentions 195 | } 196 | } 197 | 198 | func (bp *BlockProfiler) startBlockProfiler() error { 199 | bp.partialProfile = pprof.Lookup("block") 200 | if bp.partialProfile == nil { 201 | return errors.New("No block profile found") 202 | } 203 | 204 | runtime.SetBlockProfileRate(1e6) 205 | 206 | return nil 207 | } 208 | 209 | func (bp *BlockProfiler) stopBlockProfiler() (*profile.Profile, error) { 210 | runtime.SetBlockProfileRate(0) 211 | 212 | var buf bytes.Buffer 213 | w := bufio.NewWriter(&buf) 214 | 215 | err := bp.partialProfile.WriteTo(w, 0) 216 | if err != nil { 217 | return nil, err 218 | } 219 | 220 | w.Flush() 221 | r := bufio.NewReader(&buf) 222 | 223 | if p, perr := profile.Parse(r); perr == nil { 224 | if serr := symbolizeProfile(p); serr != nil { 225 | return nil, serr 226 | } 227 | 228 | if verr := p.CheckValid(); verr != nil { 229 | return nil, verr 230 | } 231 | 232 | return p, nil 233 | } else { 234 | return nil, perr 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /internal/block_profiler_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestCreateBlockProfile(t *testing.T) { 12 | agent := NewAgent() 13 | agent.Debug = true 14 | agent.ProfileAgent = true 15 | 16 | done := make(chan bool) 17 | 18 | go func() { 19 | time.Sleep(200 * time.Millisecond) 20 | 21 | wait := make(chan bool) 22 | 23 | go func() { 24 | time.Sleep(150 * time.Millisecond) 25 | 26 | wait <- true 27 | }() 28 | 29 | <-wait 30 | 31 | done <- true 32 | }() 33 | 34 | blockProfiler := newBlockProfiler(agent) 35 | blockProfiler.reset() 36 | blockProfiler.startProfiler() 37 | time.Sleep(500 * time.Millisecond) 38 | blockProfiler.stopProfiler() 39 | data, _ := blockProfiler.buildProfile(500*1e6, nil) 40 | blockProfile := data[0].profile 41 | 42 | if false { 43 | fmt.Printf("WAIT TIME: %v\n", blockProfile.measurement) 44 | fmt.Printf("CALL GRAPH: %v\n", blockProfile.printLevel(0)) 45 | } 46 | if blockProfile.measurement < 100 { 47 | t.Errorf("Wait time is too low: %v", blockProfile.measurement) 48 | } 49 | if blockProfile.numSamples < 1 { 50 | t.Error("Number of samples should be > 0") 51 | } 52 | 53 | if !strings.Contains(fmt.Sprintf("%v", blockProfile.toMap()), "TestCreateBlockProfile") { 54 | t.Error("The test function is not found in the profile") 55 | } 56 | 57 | <-done 58 | } 59 | 60 | func waitForServer(url string) { 61 | for { 62 | if _, err := http.Get(url); err == nil { 63 | time.Sleep(100 * time.Millisecond) 64 | break 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /internal/caller_frames.go: -------------------------------------------------------------------------------- 1 | // +build go1.7 2 | 3 | package internal 4 | 5 | import ( 6 | "fmt" 7 | "runtime" 8 | ) 9 | 10 | func callerFrames(skip int, count int) []string { 11 | pc := make([]uintptr, count) 12 | n := runtime.Callers(skip+2, pc) 13 | if n == 0 { 14 | return []string{} 15 | } 16 | 17 | frames := runtime.CallersFrames(pc) 18 | 19 | fmtFrames := make([]string, 0) 20 | 21 | for i := 0; i < count; i++ { 22 | frame, more := frames.Next() 23 | 24 | if frame.Function != "" || frame.File != "" { 25 | if frame.Function != "runtime.goexit" { 26 | fmtFrames = append(fmtFrames, fmt.Sprintf("%v (%v:%v)", frame.Function, frame.File, frame.Line)) 27 | } 28 | } 29 | 30 | if !more { 31 | break 32 | } 33 | } 34 | 35 | return fmtFrames 36 | } 37 | -------------------------------------------------------------------------------- /internal/caller_frames_1_6.go: -------------------------------------------------------------------------------- 1 | // +build !go1.7 2 | 3 | package internal 4 | 5 | import ( 6 | "fmt" 7 | "runtime" 8 | ) 9 | 10 | func callerFrames(skip int, count int) []string { 11 | stack := make([]uintptr, count) 12 | runtime.Callers(skip+2, stack) 13 | 14 | frames := make([]string, 0) 15 | for _, pc := range stack { 16 | if pc != 0 { 17 | if fn := runtime.FuncForPC(pc); fn != nil { 18 | funcName := fn.Name() 19 | 20 | if funcName == "runtime.goexit" { 21 | continue 22 | } 23 | 24 | fileName, lineNumber := fn.FileLine(pc) 25 | frames = append(frames, fmt.Sprintf("%v (%v:%v)", fn.Name(), fileName, lineNumber)) 26 | } 27 | } 28 | } 29 | 30 | return frames 31 | } 32 | -------------------------------------------------------------------------------- /internal/config.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type Config struct { 8 | agent *Agent 9 | configLock *sync.RWMutex 10 | agentEnabled bool 11 | profilingDisabled bool 12 | } 13 | 14 | func newConfig(agent *Agent) *Config { 15 | c := &Config{ 16 | agent: agent, 17 | configLock: &sync.RWMutex{}, 18 | agentEnabled: false, 19 | profilingDisabled: false, 20 | } 21 | 22 | return c 23 | } 24 | 25 | func (c *Config) setAgentEnabled(val bool) { 26 | c.configLock.Lock() 27 | defer c.configLock.Unlock() 28 | 29 | c.agentEnabled = val 30 | } 31 | 32 | func (c *Config) isAgentEnabled() bool { 33 | c.configLock.RLock() 34 | defer c.configLock.RUnlock() 35 | 36 | return c.agentEnabled 37 | } 38 | 39 | func (c *Config) setProfilingDisabled(val bool) { 40 | c.configLock.Lock() 41 | defer c.configLock.Unlock() 42 | 43 | c.profilingDisabled = val 44 | } 45 | 46 | func (c *Config) isProfilingDisabled() bool { 47 | c.configLock.RLock() 48 | defer c.configLock.RUnlock() 49 | 50 | return c.profilingDisabled 51 | } 52 | -------------------------------------------------------------------------------- /internal/config_loader.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type ConfigLoader struct { 8 | LoadDelay int64 9 | LoadInterval int64 10 | 11 | agent *Agent 12 | started *Flag 13 | 14 | loadTimer *Timer 15 | 16 | lastLoadTimestamp int64 17 | } 18 | 19 | func newConfigLoader(agent *Agent) *ConfigLoader { 20 | cl := &ConfigLoader{ 21 | LoadDelay: 2, 22 | LoadInterval: 120, 23 | 24 | agent: agent, 25 | started: &Flag{}, 26 | 27 | loadTimer: nil, 28 | 29 | lastLoadTimestamp: 0, 30 | } 31 | 32 | return cl 33 | } 34 | 35 | func (cl *ConfigLoader) start() { 36 | if !cl.started.SetIfUnset() { 37 | return 38 | } 39 | 40 | if cl.agent.AutoProfiling { 41 | cl.loadTimer = cl.agent.createTimer(time.Duration(cl.LoadDelay)*time.Second, time.Duration(cl.LoadInterval)*time.Second, func() { 42 | cl.load() 43 | }) 44 | } 45 | } 46 | 47 | func (cl *ConfigLoader) stop() { 48 | if !cl.started.UnsetIfSet() { 49 | return 50 | } 51 | 52 | if cl.loadTimer != nil { 53 | cl.loadTimer.Stop() 54 | } 55 | } 56 | 57 | func (cl *ConfigLoader) load() { 58 | now := time.Now().Unix() 59 | if !cl.agent.AutoProfiling && cl.lastLoadTimestamp > now-cl.LoadInterval { 60 | return 61 | } 62 | cl.lastLoadTimestamp = now 63 | 64 | payload := map[string]interface{}{} 65 | if config, err := cl.agent.apiRequest.post("config", payload); err == nil { 66 | // agent_enabled yes|no 67 | if agentEnabled, exists := config["agent_enabled"]; exists { 68 | cl.agent.config.setAgentEnabled(agentEnabled.(string) == "yes") 69 | } else { 70 | cl.agent.config.setAgentEnabled(false) 71 | } 72 | 73 | // profiling_enabled yes|no 74 | if profilingDisabled, exists := config["profiling_disabled"]; exists { 75 | cl.agent.config.setProfilingDisabled(profilingDisabled.(string) == "yes") 76 | } else { 77 | cl.agent.config.setProfilingDisabled(false) 78 | } 79 | 80 | if cl.agent.config.isAgentEnabled() && !cl.agent.config.isProfilingDisabled() { 81 | cl.agent.cpuReporter.start() 82 | cl.agent.allocationReporter.start() 83 | cl.agent.blockReporter.start() 84 | } else { 85 | cl.agent.cpuReporter.stop() 86 | cl.agent.allocationReporter.stop() 87 | cl.agent.blockReporter.stop() 88 | } 89 | 90 | if cl.agent.config.isAgentEnabled() { 91 | cl.agent.spanReporter.start() 92 | cl.agent.errorReporter.start() 93 | cl.agent.processReporter.start() 94 | cl.agent.log("Agent enabled") 95 | } else { 96 | cl.agent.spanReporter.stop() 97 | cl.agent.errorReporter.stop() 98 | cl.agent.processReporter.stop() 99 | cl.agent.log("Agent disabled") 100 | } 101 | } else { 102 | cl.agent.log("Error loading config from Dashboard") 103 | cl.agent.error(err) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /internal/config_loader_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func TestConfigLoad(t *testing.T) { 11 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 12 | fmt.Fprintf(w, "{\"agent_enabled\":\"yes\"}") 13 | })) 14 | defer server.Close() 15 | 16 | agent := NewAgent() 17 | agent.AgentKey = "key1" 18 | agent.AppName = "App1" 19 | agent.HostName = "Host1" 20 | agent.Debug = true 21 | agent.DashboardAddress = server.URL 22 | 23 | agent.config.setAgentEnabled(false) 24 | 25 | agent.configLoader.load() 26 | 27 | if !agent.config.isAgentEnabled() { 28 | t.Errorf("Config loading wasn't successful") 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/cpu_profiler.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "errors" 7 | "fmt" 8 | "math" 9 | "runtime" 10 | "runtime/pprof" 11 | "sort" 12 | "strings" 13 | "time" 14 | 15 | "github.com/stackimpact/stackimpact-go/internal/pprof/profile" 16 | ) 17 | 18 | type CPUProfiler struct { 19 | agent *Agent 20 | profile *BreakdownNode 21 | labelProfiles map[string]*BreakdownNode 22 | profWriter *bufio.Writer 23 | profBuffer *bytes.Buffer 24 | startNano int64 25 | } 26 | 27 | func newCPUProfiler(agent *Agent) *CPUProfiler { 28 | cp := &CPUProfiler{ 29 | agent: agent, 30 | profile: nil, 31 | labelProfiles: nil, 32 | profWriter: nil, 33 | profBuffer: nil, 34 | startNano: 0, 35 | } 36 | 37 | return cp 38 | } 39 | 40 | func (cp *CPUProfiler) reset() { 41 | cp.profile = newBreakdownNode("CPU call graph") 42 | cp.profile.setType(BreakdownTypeCallgraph) 43 | cp.labelProfiles = make(map[string]*BreakdownNode) 44 | } 45 | 46 | func (cp *CPUProfiler) startProfiler() error { 47 | err := cp.startCPUProfiler() 48 | if err != nil { 49 | return err 50 | } 51 | 52 | return nil 53 | } 54 | 55 | func (cp *CPUProfiler) stopProfiler() error { 56 | p, err := cp.stopCPUProfiler() 57 | if err != nil { 58 | return err 59 | } 60 | if p == nil { 61 | return errors.New("no profile returned") 62 | } 63 | 64 | if uerr := cp.updateCPUProfile(p); uerr != nil { 65 | return uerr 66 | } 67 | 68 | return nil 69 | } 70 | 71 | type ProfileDataSorter []*ProfileData 72 | 73 | func (pds ProfileDataSorter) Len() int { 74 | return len(pds) 75 | } 76 | func (pds ProfileDataSorter) Swap(i, j int) { 77 | pds[i].profile.measurement, pds[j].profile.measurement = pds[j].profile.measurement, pds[i].profile.measurement 78 | } 79 | func (pds ProfileDataSorter) Less(i, j int) bool { 80 | return pds[i].profile.measurement < pds[j].profile.measurement 81 | } 82 | 83 | func (cp *CPUProfiler) buildProfile(duration int64, workloads map[string]int64) ([]*ProfileData, error) { 84 | cp.profile.convertToPercentage(float64(duration * int64(runtime.NumCPU()))) 85 | cp.profile.propagate() 86 | // filter calls with lower than 1% CPU stake 87 | cp.profile.filter(2, 1, 100) 88 | 89 | data := make([]*ProfileData, 0) 90 | 91 | for label, labelProfile := range cp.labelProfiles { 92 | numSpans, ok := workloads[label] 93 | if !ok { 94 | continue 95 | } 96 | 97 | labelProfile.normalize(float64(numSpans * 1e6)) 98 | labelProfile.propagate() 99 | labelProfile.filter(2, 1, math.Inf(0)) 100 | 101 | data = append(data, &ProfileData{ 102 | category: CategoryCPUTrace, 103 | name: fmt.Sprintf("%v: %v", NameCPUTime, label), 104 | unit: UnitMillisecond, 105 | unitInterval: 0, 106 | profile: labelProfile, 107 | }) 108 | } 109 | 110 | sort.Sort(sort.Reverse(ProfileDataSorter(data))) 111 | if len(data) > 5 { 112 | data = data[:5] 113 | } 114 | 115 | // prepend full profile 116 | data = append([]*ProfileData{ 117 | &ProfileData{ 118 | category: CategoryCPUProfile, 119 | name: NameCPUUsage, 120 | unit: UnitPercent, 121 | unitInterval: 0, 122 | profile: cp.profile, 123 | }, 124 | }, data...) 125 | 126 | return data, nil 127 | } 128 | 129 | func (cp *CPUProfiler) updateCPUProfile(p *profile.Profile) error { 130 | samplesIndex := -1 131 | cpuIndex := -1 132 | for i, s := range p.SampleType { 133 | if s.Type == "samples" { 134 | samplesIndex = i 135 | } else if s.Type == "cpu" { 136 | cpuIndex = i 137 | } 138 | } 139 | 140 | if samplesIndex == -1 || cpuIndex == -1 { 141 | return errors.New("Unrecognized profile data") 142 | } 143 | 144 | // build call graph 145 | for _, s := range p.Sample { 146 | if !cp.agent.ProfileAgent && isAgentStack(s) { 147 | continue 148 | } 149 | 150 | stackSamples := s.Value[samplesIndex] 151 | stackDuration := float64(s.Value[cpuIndex]) 152 | 153 | currentNode := cp.profile 154 | for i := len(s.Location) - 1; i >= 0; i-- { 155 | l := s.Location[i] 156 | funcName, fileName, fileLine := readFuncInfo(l) 157 | 158 | if (!cp.agent.ProfileAgent && isAgentFrame(fileName)) || funcName == "runtime.goexit" { 159 | continue 160 | } 161 | 162 | frameName := fmt.Sprintf("%v (%v:%v)", funcName, fileName, fileLine) 163 | currentNode = currentNode.findOrAddChild(frameName) 164 | currentNode.setType(BreakdownTypeCallsite) 165 | } 166 | 167 | currentNode.increment(stackDuration, stackSamples) 168 | 169 | labelProfile := cp.findLabelProfile(s) 170 | if labelProfile != nil { 171 | currentNode := labelProfile 172 | for i := len(s.Location) - 1; i >= 0; i-- { 173 | l := s.Location[i] 174 | funcName, fileName, fileLine := readFuncInfo(l) 175 | 176 | if (!cp.agent.ProfileAgent && isAgentFrame(fileName)) || funcName == "runtime.goexit" { 177 | continue 178 | } 179 | 180 | frameName := fmt.Sprintf("%v (%v:%v)", funcName, fileName, fileLine) 181 | currentNode = currentNode.findOrAddChild(frameName) 182 | currentNode.setType(BreakdownTypeCallsite) 183 | } 184 | 185 | currentNode.increment(stackDuration, stackSamples) 186 | } 187 | } 188 | 189 | return nil 190 | } 191 | 192 | func (cp *CPUProfiler) findLabelProfile(sample *profile.Sample) *BreakdownNode { 193 | if sample.Label != nil { 194 | if labels, ok := sample.Label["workload"]; ok { 195 | if len(labels) > 0 { 196 | if lp, ok := cp.labelProfiles[labels[0]]; ok { 197 | return lp 198 | } else { 199 | lp = newBreakdownNode("root") 200 | cp.labelProfiles[labels[0]] = lp 201 | return lp 202 | } 203 | } 204 | } 205 | } 206 | 207 | return nil 208 | } 209 | 210 | func (cp *CPUProfiler) startCPUProfiler() error { 211 | cp.profBuffer = &bytes.Buffer{} 212 | cp.profWriter = bufio.NewWriter(cp.profBuffer) 213 | cp.startNano = time.Now().UnixNano() 214 | 215 | err := pprof.StartCPUProfile(cp.profWriter) 216 | if err != nil { 217 | return err 218 | } 219 | 220 | return nil 221 | } 222 | 223 | func (cp *CPUProfiler) stopCPUProfiler() (*profile.Profile, error) { 224 | pprof.StopCPUProfile() 225 | 226 | cp.profWriter.Flush() 227 | r := bufio.NewReader(cp.profBuffer) 228 | 229 | if p, perr := profile.Parse(r); perr == nil { 230 | cp.profWriter = nil 231 | cp.profBuffer = nil 232 | 233 | if p.TimeNanos == 0 { 234 | p.TimeNanos = cp.startNano 235 | } 236 | if p.DurationNanos == 0 { 237 | p.DurationNanos = time.Now().UnixNano() - cp.startNano 238 | } 239 | 240 | if serr := symbolizeProfile(p); serr != nil { 241 | return nil, serr 242 | } 243 | 244 | if verr := p.CheckValid(); verr != nil { 245 | return nil, verr 246 | } 247 | 248 | return p, nil 249 | } else { 250 | cp.profWriter = nil 251 | cp.profBuffer = nil 252 | 253 | return nil, perr 254 | } 255 | } 256 | 257 | func isAgentStack(sample *profile.Sample) bool { 258 | return stackContains(sample, "", agentPathInternal) 259 | } 260 | 261 | func isAgentFrame(fileNameTest string) bool { 262 | return strings.Contains(fileNameTest, agentPath) && 263 | !strings.Contains(fileNameTest, agentPathExamples) 264 | } 265 | 266 | func stackContains(sample *profile.Sample, funcNameTest string, fileNameTest string) bool { 267 | for i := len(sample.Location) - 1; i >= 0; i-- { 268 | l := sample.Location[i] 269 | funcName, fileName, _ := readFuncInfo(l) 270 | 271 | if (funcNameTest == "" || strings.Contains(funcName, funcNameTest)) && 272 | (fileNameTest == "" || strings.Contains(fileName, fileNameTest)) { 273 | return true 274 | } 275 | } 276 | 277 | return false 278 | } 279 | 280 | func readFuncInfo(l *profile.Location) (funcName string, fileName string, fileLine int64) { 281 | for li := range l.Line { 282 | if fn := l.Line[li].Function; fn != nil { 283 | return fn.Name, fn.Filename, l.Line[li].Line 284 | } 285 | } 286 | 287 | return "", "", 0 288 | } 289 | -------------------------------------------------------------------------------- /internal/cpu_profiler_labels_test.go: -------------------------------------------------------------------------------- 1 | // +build go1.9 2 | 3 | package internal 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "runtime/pprof" 9 | "strconv" 10 | "strings" 11 | "testing" 12 | "time" 13 | ) 14 | 15 | func cpuWork1() { 16 | for i := 0; i < 5000000; i++ { 17 | str := "str" + strconv.Itoa(i) 18 | str = str + "a" 19 | } 20 | } 21 | 22 | func cpuWork2() { 23 | for i := 0; i < 5000000; i++ { 24 | str := "str" + strconv.Itoa(i) 25 | str = str + "a" 26 | } 27 | } 28 | 29 | func TestCPUProfileLabels(t *testing.T) { 30 | agent := NewAgent() 31 | agent.Debug = true 32 | agent.ProfileAgent = true 33 | 34 | done := make(chan bool) 35 | 36 | go func() { 37 | labelSet := pprof.Labels("workload", "label1") 38 | pprof.Do(context.Background(), labelSet, func(ctx context.Context) { 39 | cpuWork1() 40 | 41 | done <- true 42 | }) 43 | 44 | cpuWork2() 45 | 46 | done <- true 47 | }() 48 | 49 | cpuProfiler := newCPUProfiler(agent) 50 | cpuProfiler.reset() 51 | cpuProfiler.startProfiler() 52 | time.Sleep(500 * time.Millisecond) 53 | cpuProfiler.stopProfiler() 54 | workload := map[string]int64{"label1": 2} 55 | data, _ := cpuProfiler.buildProfile(500*1e6, workload) 56 | 57 | if len(data) == 1 { 58 | fmt.Println("label profile is missing") 59 | } 60 | 61 | profile := data[1].profile 62 | 63 | if false { 64 | fmt.Printf("CALL GRAPH: %v\n", profile.printLevel(0)) 65 | } 66 | 67 | if !strings.Contains(fmt.Sprintf("%v", profile.toMap()), "cpuWork1") { 68 | t.Error("The test function is not found in the profile") 69 | } 70 | 71 | if strings.Contains(fmt.Sprintf("%v", profile.toMap()), "cpuWork2") { 72 | t.Error("The test function is found in the profile") 73 | } 74 | 75 | <-done 76 | } 77 | -------------------------------------------------------------------------------- /internal/cpu_profiler_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestCreateCPUProfile(t *testing.T) { 12 | agent := NewAgent() 13 | agent.Debug = true 14 | agent.ProfileAgent = true 15 | 16 | done := make(chan bool) 17 | 18 | go func() { 19 | // cpu 20 | //start := time.Now().UnixNano() 21 | for i := 0; i < 5000000; i++ { 22 | str := "str" + strconv.Itoa(i) 23 | str = str + "a" 24 | } 25 | //took := time.Now().UnixNano() - start 26 | //fmt.Printf("TOOK: %v\n", took) 27 | 28 | done <- true 29 | }() 30 | 31 | cpuProfiler := newCPUProfiler(agent) 32 | cpuProfiler.reset() 33 | cpuProfiler.startProfiler() 34 | time.Sleep(500 * time.Millisecond) 35 | cpuProfiler.stopProfiler() 36 | data, _ := cpuProfiler.buildProfile(500*1e6, nil) 37 | profile := data[0].profile 38 | 39 | if false { 40 | fmt.Printf("CPU USAGE: %v\n", profile.measurement) 41 | fmt.Printf("CALL GRAPH: %v\n", profile.printLevel(0)) 42 | } 43 | if profile.measurement < 2 { 44 | t.Errorf("CPU usage is too low: %v", profile.measurement) 45 | } 46 | if profile.numSamples < 1 { 47 | t.Error("Number of samples should be > 0") 48 | } 49 | if !strings.Contains(fmt.Sprintf("%v", profile.toMap()), "TestCreateCPUProfile") { 50 | t.Error("The test function is not found in the profile") 51 | } 52 | 53 | <-done 54 | } 55 | -------------------------------------------------------------------------------- /internal/error_reporter.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type ErrorReporter struct { 9 | ReportInterval int64 10 | 11 | agent *Agent 12 | started *Flag 13 | reportTimer *Timer 14 | 15 | recordLock *sync.RWMutex 16 | errorGraphs map[string]*BreakdownNode 17 | } 18 | 19 | func newErrorReporter(agent *Agent) *ErrorReporter { 20 | er := &ErrorReporter{ 21 | ReportInterval: 60, 22 | 23 | agent: agent, 24 | started: &Flag{}, 25 | reportTimer: nil, 26 | 27 | recordLock: &sync.RWMutex{}, 28 | } 29 | 30 | return er 31 | } 32 | 33 | func (er *ErrorReporter) reset() { 34 | er.recordLock.Lock() 35 | defer er.recordLock.Unlock() 36 | 37 | er.errorGraphs = make(map[string]*BreakdownNode) 38 | } 39 | 40 | func (er *ErrorReporter) start() { 41 | if !er.agent.AutoProfiling { 42 | return 43 | } 44 | 45 | if !er.started.SetIfUnset() { 46 | return 47 | } 48 | 49 | er.reset() 50 | 51 | er.reportTimer = er.agent.createTimer(0, time.Duration(er.ReportInterval)*time.Second, func() { 52 | er.report() 53 | }) 54 | } 55 | 56 | func (er *ErrorReporter) stop() { 57 | if !er.started.UnsetIfSet() { 58 | return 59 | } 60 | 61 | if er.reportTimer != nil { 62 | er.reportTimer.Stop() 63 | } 64 | } 65 | 66 | func (er *ErrorReporter) incrementError(group string, errorGraph *BreakdownNode, err error, frames []string) { 67 | currentNode := errorGraph 68 | currentNode.updateCounter(1, 0) 69 | for i := len(frames) - 1; i >= 0; i-- { 70 | f := frames[i] 71 | currentNode = currentNode.findOrAddChild(f) 72 | currentNode.setType(BreakdownTypeCallsite) 73 | currentNode.updateCounter(1, 0) 74 | } 75 | 76 | message := err.Error() 77 | if message == "" { 78 | message = "Undefined" 79 | } 80 | messageNode := currentNode.findChild(message) 81 | if messageNode == nil { 82 | if len(currentNode.children) < 5 { 83 | messageNode = currentNode.findOrAddChild(message) 84 | } else { 85 | messageNode = currentNode.findOrAddChild("Other") 86 | } 87 | } 88 | messageNode.setType(BreakdownTypeError) 89 | messageNode.updateCounter(1, 0) 90 | } 91 | 92 | func (er *ErrorReporter) recordError(group string, err error, skip int) { 93 | if !er.started.IsSet() { 94 | return 95 | } 96 | 97 | frames := callerFrames(skip+1, 25) 98 | 99 | if err == nil { 100 | er.agent.log("Missing error object") 101 | return 102 | } 103 | 104 | // Error graph exists for the current interval. 105 | er.recordLock.RLock() 106 | errorGraph, exists := er.errorGraphs[group] 107 | if exists { 108 | er.incrementError(group, errorGraph, err, frames) 109 | } 110 | er.recordLock.RUnlock() 111 | 112 | // Error graph does not exist yet for the current interval. 113 | if !exists { 114 | er.recordLock.Lock() 115 | errorGraph, exists := er.errorGraphs[group] 116 | if !exists { 117 | // If error was not created by other recordError call between locks, create it. 118 | errorGraph = newBreakdownNode(group) 119 | errorGraph.setType(BreakdownTypeCallgraph) 120 | er.errorGraphs[group] = errorGraph 121 | } 122 | er.recordLock.Unlock() 123 | 124 | er.recordLock.RLock() 125 | er.incrementError(group, errorGraph, err, frames) 126 | er.recordLock.RUnlock() 127 | } 128 | } 129 | 130 | func (er *ErrorReporter) report() { 131 | if !er.started.IsSet() { 132 | return 133 | } 134 | 135 | er.recordLock.Lock() 136 | outgoing := er.errorGraphs 137 | er.errorGraphs = make(map[string]*BreakdownNode) 138 | er.recordLock.Unlock() 139 | 140 | for _, errorGraph := range outgoing { 141 | errorGraph.evaluateCounter() 142 | 143 | metric := newMetric(er.agent, TypeState, CategoryErrorProfile, errorGraph.name, UnitNone) 144 | metric.createMeasurement(TriggerTimer, errorGraph.measurement, 60, errorGraph) 145 | er.agent.messageQueue.addMessage("metric", metric.toMap()) 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /internal/error_reporter_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strings" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestRecordError(t *testing.T) { 12 | agent := NewAgent() 13 | agent.Debug = true 14 | 15 | agent.errorReporter.reset() 16 | agent.errorReporter.started.Set() 17 | 18 | for i := 0; i < 100; i++ { 19 | go func() { 20 | agent.errorReporter.recordError("group1", errors.New("error1"), 0) 21 | 22 | go func() { 23 | agent.errorReporter.recordError("group1", errors.New("error2"), 0) 24 | }() 25 | }() 26 | } 27 | 28 | time.Sleep(50 * time.Millisecond) 29 | 30 | errorGraphs := agent.errorReporter.errorGraphs 31 | 32 | group1 := errorGraphs["group1"] 33 | group1.evaluateCounter() 34 | if group1.measurement != 200 { 35 | t.Errorf("Measurement is wrong: %v", group1.measurement) 36 | } 37 | 38 | if !strings.Contains(fmt.Sprintf("%v", group1.toMap()), "TestRecordError.func1.1") { 39 | t.Error("The test function is not found in the error profile") 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /internal/message_queue.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type Message struct { 9 | topic string 10 | content map[string]interface{} 11 | addedAt int64 12 | } 13 | 14 | type MessageQueue struct { 15 | MessageTTL int64 16 | FlushInterval int64 17 | 18 | agent *Agent 19 | started *Flag 20 | 21 | flushTimer *Timer 22 | 23 | queue []Message 24 | queueLock *sync.Mutex 25 | lastFlushTimestamp int64 26 | backoffSeconds int64 27 | } 28 | 29 | func newMessageQueue(agent *Agent) *MessageQueue { 30 | mq := &MessageQueue{ 31 | MessageTTL: 10 * 60, 32 | FlushInterval: 5, 33 | 34 | agent: agent, 35 | started: &Flag{}, 36 | 37 | flushTimer: nil, 38 | 39 | queue: make([]Message, 0), 40 | queueLock: &sync.Mutex{}, 41 | lastFlushTimestamp: 0, 42 | backoffSeconds: 0, 43 | } 44 | 45 | return mq 46 | } 47 | 48 | func (mq *MessageQueue) start() { 49 | if !mq.started.SetIfUnset() { 50 | return 51 | } 52 | 53 | if mq.agent.AutoProfiling { 54 | mq.flushTimer = mq.agent.createTimer(0, time.Duration(mq.FlushInterval)*time.Second, func() { 55 | mq.flush(false) 56 | }) 57 | } 58 | } 59 | 60 | func (mq *MessageQueue) stop() { 61 | if !mq.started.UnsetIfSet() { 62 | return 63 | } 64 | 65 | if mq.flushTimer != nil { 66 | mq.flushTimer.Stop() 67 | } 68 | } 69 | 70 | func (mq *MessageQueue) size() int { 71 | mq.queueLock.Lock() 72 | defer mq.queueLock.Unlock() 73 | 74 | return len(mq.queue) 75 | } 76 | 77 | func (mq *MessageQueue) expire() { 78 | mq.queueLock.Lock() 79 | defer mq.queueLock.Unlock() 80 | 81 | if len(mq.queue) == 0 { 82 | return 83 | } 84 | 85 | now := time.Now().Unix() 86 | 87 | if mq.queue[0].addedAt > now-mq.MessageTTL { 88 | return 89 | } 90 | 91 | for i := len(mq.queue) - 1; i >= 0; i-- { 92 | if mq.queue[i].addedAt < now-mq.MessageTTL { 93 | mq.queue = mq.queue[i+1:] 94 | break 95 | } 96 | } 97 | 98 | mq.agent.log("Expired old messages from the queue") 99 | } 100 | 101 | func (mq *MessageQueue) addMessage(topic string, message map[string]interface{}) { 102 | m := Message{ 103 | topic: topic, 104 | content: message, 105 | addedAt: time.Now().Unix(), 106 | } 107 | 108 | // add new message 109 | mq.queueLock.Lock() 110 | mq.queue = append(mq.queue, m) 111 | mq.queueLock.Unlock() 112 | 113 | mq.agent.log("Added message to the queue for topic: %v", topic) 114 | mq.agent.log("%v", message) 115 | 116 | mq.expire() 117 | } 118 | 119 | func (mq *MessageQueue) flush(withInterval bool) { 120 | now := time.Now().Unix() 121 | if withInterval && mq.lastFlushTimestamp > now-mq.FlushInterval { 122 | return 123 | } 124 | 125 | if mq.size() == 0 { 126 | return 127 | } 128 | 129 | // flush only if backoff time is elapsed 130 | if mq.lastFlushTimestamp+mq.backoffSeconds > now { 131 | return 132 | } 133 | 134 | mq.expire() 135 | 136 | mq.queueLock.Lock() 137 | outgoing := mq.queue 138 | mq.queue = make([]Message, 0) 139 | mq.queueLock.Unlock() 140 | 141 | messages := make([]interface{}, 0) 142 | for _, m := range outgoing { 143 | message := map[string]interface{}{ 144 | "topic": m.topic, 145 | "content": m.content, 146 | } 147 | 148 | messages = append(messages, message) 149 | } 150 | 151 | payload := map[string]interface{}{ 152 | "messages": messages, 153 | } 154 | 155 | mq.lastFlushTimestamp = now 156 | 157 | if _, err := mq.agent.apiRequest.post("upload", payload); err == nil { 158 | // reset backoff 159 | mq.backoffSeconds = 0 160 | } else { 161 | // prepend outgoing messages back to the queue 162 | mq.queueLock.Lock() 163 | mq.queue = append(outgoing, mq.queue...) 164 | mq.queueLock.Unlock() 165 | 166 | // increase backoff up to 1 minute 167 | mq.agent.log("Error uploading messages to dashboard, backing off next upload") 168 | if mq.backoffSeconds == 0 { 169 | mq.backoffSeconds = 10 170 | } else if mq.backoffSeconds*2 < 60 { 171 | mq.backoffSeconds *= 2 172 | } 173 | 174 | mq.agent.error(err) 175 | } 176 | } 177 | 178 | func (mq *MessageQueue) read() []Message { 179 | mq.queueLock.Lock() 180 | defer mq.queueLock.Unlock() 181 | 182 | messages := mq.queue 183 | mq.queue = make([]Message, 0) 184 | 185 | return messages 186 | } 187 | -------------------------------------------------------------------------------- /internal/message_queue_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestExpire(t *testing.T) { 12 | agent := NewAgent() 13 | agent.Debug = true 14 | 15 | msg := map[string]interface{}{ 16 | "a": 1, 17 | } 18 | 19 | agent.messageQueue.addMessage("test", msg) 20 | 21 | msg = map[string]interface{}{ 22 | "a": 2, 23 | } 24 | 25 | agent.messageQueue.addMessage("test", msg) 26 | 27 | if len(agent.messageQueue.queue) != 2 { 28 | t.Errorf("Message len is not 2, but %v", len(agent.messageQueue.queue)) 29 | return 30 | } 31 | 32 | agent.messageQueue.queue[0].addedAt = time.Now().Unix() - 20*60 33 | 34 | agent.messageQueue.expire() 35 | 36 | if len(agent.messageQueue.queue) != 1 { 37 | t.Errorf("Message len is not 1, but %v", len(agent.messageQueue.queue)) 38 | return 39 | } 40 | } 41 | 42 | func TestFlush(t *testing.T) { 43 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 44 | fmt.Fprintf(w, "{}") 45 | })) 46 | defer server.Close() 47 | 48 | agent := NewAgent() 49 | agent.AgentKey = "key1" 50 | agent.AppName = "App1" 51 | agent.HostName = "Host1" 52 | agent.Debug = true 53 | agent.DashboardAddress = server.URL 54 | 55 | msg := map[string]interface{}{ 56 | "a": 1, 57 | } 58 | agent.messageQueue.addMessage("test", msg) 59 | 60 | msg = map[string]interface{}{ 61 | "a": 2, 62 | } 63 | agent.messageQueue.addMessage("test", msg) 64 | 65 | agent.messageQueue.flush(false) 66 | 67 | if len(agent.messageQueue.queue) > 0 { 68 | t.Errorf("Should have no messages, but has %v", len(agent.messageQueue.queue)) 69 | } 70 | } 71 | 72 | func TestFlushFail(t *testing.T) { 73 | server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 74 | fmt.Fprintf(w, "invalidjson") 75 | })) 76 | defer server.Close() 77 | 78 | agent := NewAgent() 79 | agent.AgentKey = "key1" 80 | agent.AppName = "App1" 81 | agent.HostName = "Host1" 82 | agent.Debug = true 83 | agent.DashboardAddress = server.URL 84 | 85 | msg := map[string]interface{}{ 86 | "a": 1, 87 | } 88 | agent.messageQueue.addMessage("test", msg) 89 | 90 | msg = map[string]interface{}{ 91 | "a": 2, 92 | } 93 | agent.messageQueue.addMessage("test", msg) 94 | 95 | agent.messageQueue.flush(false) 96 | 97 | if len(agent.messageQueue.queue) != 2 { 98 | t.Errorf("Should have 2 messages, but has %v", len(agent.messageQueue.queue)) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /internal/metric.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "math/rand" 7 | "sort" 8 | "sync" 9 | "sync/atomic" 10 | "time" 11 | ) 12 | 13 | const TypeState string = "state" 14 | const TypeCounter string = "counter" 15 | const TypeProfile string = "profile" 16 | const TypeTrace string = "trace" 17 | 18 | const CategoryCPU string = "cpu" 19 | const CategoryMemory string = "memory" 20 | const CategoryGC string = "gc" 21 | const CategoryRuntime string = "runtime" 22 | const CategorySpan string = "span" 23 | const CategoryCPUProfile string = "cpu-profile" 24 | const CategoryMemoryProfile string = "memory-profile" 25 | const CategoryBlockProfile string = "block-profile" 26 | const CategoryLockProfile string = "lock-profile" 27 | const CategoryCPUTrace string = "cpu-trace" 28 | const CategoryBlockTrace string = "block-trace" 29 | const CategoryErrorProfile string = "error-profile" 30 | 31 | const NameCPUTime string = "CPU time" 32 | const NameCPUUsage string = "CPU usage" 33 | const NameMaxRSS string = "Max RSS" 34 | const NameCurrentRSS string = "Current RSS" 35 | const NameVMSize string = "VM Size" 36 | const NameNumGoroutines string = "Number of goroutines" 37 | const NameNumCgoCalls string = "Number of cgo calls" 38 | const NameAllocated string = "Allocated memory" 39 | const NameLookups string = "Lookups" 40 | const NameMallocs string = "Mallocs" 41 | const NameFrees string = "Frees" 42 | const NameHeapSys string = "Heap obtained" 43 | const NameHeapIdle string = "Heap idle" 44 | const NameHeapInuse string = "Heap non-idle" 45 | const NameHeapReleased string = "Heap released" 46 | const NameHeapObjects string = "Heap objects" 47 | const NameGCTotalPause string = "GC total pause" 48 | const NameNumGC string = "Number of GCs" 49 | const NameGCCPUFraction string = "GC CPU fraction" 50 | const NameHeapAllocation string = "Heap allocation" 51 | const NameBlockingCallTimes string = "Blocking call times" 52 | const NameHTTPTransactionBreakdown string = "HTTP transaction breakdown" 53 | 54 | const UnitNone string = "" 55 | const UnitMillisecond string = "millisecond" 56 | const UnitMicrosecond string = "microsecond" 57 | const UnitNanosecond string = "nanosecond" 58 | const UnitByte string = "byte" 59 | const UnitKilobyte string = "kilobyte" 60 | const UnitPercent string = "percent" 61 | 62 | const TriggerTimer string = "timer" 63 | const TriggerAPI string = "api" 64 | 65 | const BreakdownTypeCallgraph string = "callgraph" 66 | const BreakdownTypeCallsite string = "callsite" 67 | const BreakdownTypeError string = "error" 68 | 69 | const ReservoirSize int = 1000 70 | 71 | type filterFuncType func(name string) bool 72 | 73 | type Reservoir []uint64 74 | 75 | func (r Reservoir) Len() int { 76 | return len(r) 77 | } 78 | func (r Reservoir) Swap(i, j int) { 79 | r[i], r[j] = r[j], r[i] 80 | } 81 | func (r Reservoir) Less(i, j int) bool { 82 | return r[i] < r[j] 83 | } 84 | 85 | type BreakdownNode struct { 86 | name string 87 | typ string 88 | metadata map[string]string 89 | measurement float64 90 | numSamples int64 91 | counter int64 92 | reservoir Reservoir 93 | children map[string]*BreakdownNode 94 | updateLock *sync.RWMutex 95 | } 96 | 97 | func newBreakdownNode(name string) *BreakdownNode { 98 | bn := &BreakdownNode{ 99 | name: name, 100 | typ: "", 101 | metadata: make(map[string]string), 102 | measurement: 0, 103 | numSamples: 0, 104 | counter: 0, 105 | reservoir: nil, 106 | children: make(map[string]*BreakdownNode), 107 | updateLock: &sync.RWMutex{}, 108 | } 109 | 110 | return bn 111 | } 112 | 113 | func (bn *BreakdownNode) setType(typ string) { 114 | bn.typ = typ 115 | } 116 | 117 | func (bn *BreakdownNode) addMetadata(key, string, val string) { 118 | bn.metadata[key] = val 119 | } 120 | 121 | func (bn *BreakdownNode) getMetadata(key string) (string, bool) { 122 | if val, exists := bn.metadata[key]; exists { 123 | return val, true 124 | } else { 125 | return "", false 126 | } 127 | } 128 | 129 | func (bn *BreakdownNode) findChild(name string) *BreakdownNode { 130 | bn.updateLock.RLock() 131 | defer bn.updateLock.RUnlock() 132 | 133 | if child, exists := bn.children[name]; exists { 134 | return child 135 | } 136 | 137 | return nil 138 | } 139 | 140 | func (bn *BreakdownNode) maxChild() *BreakdownNode { 141 | bn.updateLock.RLock() 142 | defer bn.updateLock.RUnlock() 143 | 144 | var maxChild *BreakdownNode = nil 145 | for _, child := range bn.children { 146 | if maxChild == nil || child.measurement > maxChild.measurement { 147 | maxChild = child 148 | } 149 | } 150 | return maxChild 151 | } 152 | 153 | func (bn *BreakdownNode) minChild() *BreakdownNode { 154 | bn.updateLock.RLock() 155 | defer bn.updateLock.RUnlock() 156 | 157 | var minChild *BreakdownNode = nil 158 | for _, child := range bn.children { 159 | if minChild == nil || child.measurement < minChild.measurement { 160 | minChild = child 161 | } 162 | } 163 | return minChild 164 | } 165 | 166 | func (bn *BreakdownNode) addChild(child *BreakdownNode) { 167 | bn.updateLock.Lock() 168 | defer bn.updateLock.Unlock() 169 | 170 | bn.children[child.name] = child 171 | } 172 | 173 | func (bn *BreakdownNode) removeChild(child *BreakdownNode) { 174 | bn.updateLock.Lock() 175 | defer bn.updateLock.Unlock() 176 | 177 | delete(bn.children, child.name) 178 | } 179 | 180 | func (bn *BreakdownNode) findOrAddChild(name string) *BreakdownNode { 181 | child := bn.findChild(name) 182 | if child == nil { 183 | child = newBreakdownNode(name) 184 | bn.addChild(child) 185 | } 186 | 187 | return child 188 | } 189 | 190 | func (bn *BreakdownNode) filter(fromLevel int, min float64, max float64) { 191 | bn.filterLevel(1, fromLevel, min, max) 192 | } 193 | 194 | func (bn *BreakdownNode) filterLevel(currentLevel int, fromLevel int, min float64, max float64) { 195 | for key, child := range bn.children { 196 | if currentLevel >= fromLevel && (child.measurement < min || child.measurement > max) { 197 | delete(bn.children, key) 198 | } else { 199 | child.filterLevel(currentLevel+1, fromLevel, min, max) 200 | } 201 | } 202 | } 203 | 204 | func (bn *BreakdownNode) filterByName(filterFunc filterFuncType) { 205 | for key, child := range bn.children { 206 | if filterFunc(child.name) { 207 | child.filterByName(filterFunc) 208 | } else { 209 | delete(bn.children, key) 210 | } 211 | } 212 | } 213 | 214 | func (bn *BreakdownNode) depth() int { 215 | max := 0 216 | for _, child := range bn.children { 217 | cd := child.depth() 218 | if cd > max { 219 | max = cd 220 | } 221 | } 222 | 223 | return max + 1 224 | } 225 | 226 | func (bn *BreakdownNode) propagate() { 227 | for _, child := range bn.children { 228 | child.propagate() 229 | bn.measurement += child.measurement 230 | bn.numSamples += child.numSamples 231 | } 232 | } 233 | 234 | func (bn *BreakdownNode) increment(value float64, numSamples int64) { 235 | bn.measurement += value 236 | bn.numSamples += numSamples 237 | } 238 | 239 | func (bn *BreakdownNode) updateCounter(value int64, numSamples int64) { 240 | atomic.AddInt64(&bn.counter, value) 241 | atomic.AddInt64(&bn.numSamples, numSamples) 242 | } 243 | 244 | func (bn *BreakdownNode) evaluateCounter() { 245 | bn.measurement = float64(bn.counter) 246 | 247 | for _, child := range bn.children { 248 | child.evaluateCounter() 249 | } 250 | } 251 | 252 | func (bn *BreakdownNode) updateP95(value float64) { 253 | rLen := 0 254 | rExists := true 255 | 256 | bn.updateLock.RLock() 257 | if bn.reservoir == nil { 258 | rExists = false 259 | } else { 260 | rLen = len(bn.reservoir) 261 | } 262 | bn.updateLock.RUnlock() 263 | 264 | if !rExists { 265 | bn.updateLock.Lock() 266 | bn.reservoir = make(Reservoir, 0, ReservoirSize) 267 | bn.updateLock.Unlock() 268 | } 269 | 270 | if rLen < ReservoirSize { 271 | bn.updateLock.Lock() 272 | bn.reservoir = append(bn.reservoir, math.Float64bits(value)) 273 | bn.updateLock.Unlock() 274 | } else { 275 | atomic.StoreUint64(&bn.reservoir[rand.Intn(ReservoirSize)], math.Float64bits(value)) 276 | } 277 | 278 | atomic.AddInt64(&bn.numSamples, 1) 279 | } 280 | 281 | func (bn *BreakdownNode) evaluateP95() { 282 | if bn.reservoir != nil && len(bn.reservoir) > 0 { 283 | sort.Sort(bn.reservoir) 284 | index := int(math.Floor(float64(len(bn.reservoir)) / 100.0 * 95.0)) 285 | bn.measurement = math.Float64frombits(bn.reservoir[index]) 286 | 287 | bn.reservoir = bn.reservoir[:0] 288 | } 289 | 290 | for _, child := range bn.children { 291 | child.evaluateP95() 292 | } 293 | } 294 | 295 | func (bn *BreakdownNode) convertToPercentage(total float64) { 296 | bn.measurement = (bn.measurement / total) * 100.0 297 | for _, child := range bn.children { 298 | child.convertToPercentage(total) 299 | } 300 | } 301 | 302 | func (bn *BreakdownNode) normalize(factor float64) { 303 | bn.measurement = bn.measurement / factor 304 | bn.numSamples = int64(math.Ceil(float64(bn.numSamples) / factor)) 305 | for _, child := range bn.children { 306 | child.normalize(factor) 307 | } 308 | } 309 | 310 | func (bn *BreakdownNode) round() { 311 | _, d := math.Modf(bn.measurement) 312 | if d > 0.5 { 313 | bn.measurement = math.Ceil(bn.measurement) 314 | } else { 315 | bn.measurement = math.Floor(bn.measurement) 316 | } 317 | for _, child := range bn.children { 318 | child.round() 319 | } 320 | } 321 | 322 | func round(val float64, roundOn float64, places int) float64 { 323 | var round float64 324 | pow := math.Pow(10, float64(places)) 325 | digit := pow * val 326 | _, div := math.Modf(digit) 327 | if div >= roundOn { 328 | round = math.Ceil(digit) 329 | } else { 330 | round = math.Floor(digit) 331 | } 332 | 333 | return round / pow 334 | } 335 | 336 | func (bn *BreakdownNode) clone() *BreakdownNode { 337 | cln := newBreakdownNode(bn.name) 338 | cln.measurement = bn.measurement 339 | cln.numSamples = bn.numSamples 340 | 341 | for _, child := range bn.children { 342 | cln.addChild(child.clone()) 343 | } 344 | 345 | return cln 346 | } 347 | 348 | func (bn *BreakdownNode) toMap() map[string]interface{} { 349 | childrenMap := make([]interface{}, 0) 350 | for _, child := range bn.children { 351 | childrenMap = append(childrenMap, child.toMap()) 352 | } 353 | 354 | nodeMap := map[string]interface{}{ 355 | "name": bn.name, 356 | "metadata": bn.metadata, 357 | "measurement": bn.measurement, 358 | "num_samples": bn.numSamples, 359 | "children": childrenMap, 360 | } 361 | 362 | if bn.typ != "" { 363 | nodeMap["type"] = bn.typ 364 | } 365 | 366 | return nodeMap 367 | } 368 | 369 | func (bn *BreakdownNode) printLevel(level int) string { 370 | str := "" 371 | 372 | for i := 0; i < level; i++ { 373 | str += " " 374 | } 375 | 376 | str += fmt.Sprintf("%v - %v (%v)\n", bn.name, bn.measurement, bn.numSamples) 377 | for _, child := range bn.children { 378 | str += child.printLevel(level + 1) 379 | } 380 | 381 | return str 382 | } 383 | 384 | type Measurement struct { 385 | id string 386 | trigger string 387 | value float64 388 | duration int64 389 | breakdown *BreakdownNode 390 | timestamp int64 391 | } 392 | 393 | type Metric struct { 394 | agent *Agent 395 | id string 396 | typ string 397 | category string 398 | name string 399 | unit string 400 | measurement *Measurement 401 | hasLastValue bool 402 | lastValue float64 403 | } 404 | 405 | func newMetric(agent *Agent, typ string, category string, name string, unit string) *Metric { 406 | metricID := sha1String(agent.AppName + agent.AppEnvironment + agent.HostName + typ + category + name + unit) 407 | 408 | m := &Metric{ 409 | agent: agent, 410 | id: metricID, 411 | typ: typ, 412 | category: category, 413 | name: name, 414 | unit: unit, 415 | measurement: nil, 416 | hasLastValue: false, 417 | lastValue: 0, 418 | } 419 | 420 | return m 421 | } 422 | 423 | func (m *Metric) hasMeasurement() bool { 424 | return m.measurement != nil 425 | } 426 | 427 | func (m *Metric) createMeasurement(trigger string, value float64, duration int64, breakdown *BreakdownNode) { 428 | ready := true 429 | 430 | if m.typ == TypeCounter { 431 | if !m.hasLastValue { 432 | ready = false 433 | m.hasLastValue = true 434 | m.lastValue = value 435 | } else { 436 | tmpValue := value 437 | value = value - m.lastValue 438 | m.lastValue = tmpValue 439 | } 440 | } 441 | 442 | if ready { 443 | m.measurement = &Measurement{ 444 | id: m.agent.uuid(), 445 | trigger: trigger, 446 | value: value, 447 | duration: duration, 448 | breakdown: breakdown, 449 | timestamp: time.Now().Unix(), 450 | } 451 | } 452 | } 453 | 454 | func (m *Metric) toMap() map[string]interface{} { 455 | var measurementMap map[string]interface{} = nil 456 | if m.measurement != nil { 457 | var breakdownMap map[string]interface{} = nil 458 | if m.measurement.breakdown != nil { 459 | breakdownMap = m.measurement.breakdown.toMap() 460 | } 461 | 462 | measurementMap = map[string]interface{}{ 463 | "id": m.measurement.id, 464 | "trigger": m.measurement.trigger, 465 | "value": m.measurement.value, 466 | "duration": m.measurement.duration, 467 | "breakdown": breakdownMap, 468 | "timestamp": m.measurement.timestamp, 469 | } 470 | } 471 | 472 | metricMap := map[string]interface{}{ 473 | "id": m.id, 474 | "type": m.typ, 475 | "category": m.category, 476 | "name": m.name, 477 | "unit": m.unit, 478 | "measurement": measurementMap, 479 | } 480 | 481 | return metricMap 482 | } 483 | -------------------------------------------------------------------------------- /internal/metric_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | ) 7 | 8 | func TestCreateMeasurement(t *testing.T) { 9 | agent := NewAgent() 10 | agent.Debug = true 11 | 12 | m := newMetric(agent, TypeCounter, CategoryCPU, NameCPUUsage, UnitNone) 13 | 14 | m.createMeasurement(TriggerTimer, 100, 0, nil) 15 | 16 | if m.hasMeasurement() { 17 | t.Errorf("Should not have measurement") 18 | } 19 | 20 | m.createMeasurement(TriggerTimer, 110, 0, nil) 21 | 22 | if m.measurement.value != 10 { 23 | t.Errorf("Value should be 10, but is %v", m.measurement.value) 24 | } 25 | 26 | m.createMeasurement(TriggerTimer, 115, 0, nil) 27 | 28 | if m.measurement.value != 5 { 29 | t.Errorf("Value should be 5, but is %v", m.measurement.value) 30 | } 31 | 32 | } 33 | 34 | func TestBreakdownFilter(t *testing.T) { 35 | agent := NewAgent() 36 | agent.Debug = true 37 | 38 | root := newBreakdownNode("root") 39 | root.measurement = 10 40 | 41 | child1 := newBreakdownNode("child1") 42 | child1.measurement = 9 43 | root.addChild(child1) 44 | 45 | child2 := newBreakdownNode("child2") 46 | child2.measurement = 1 47 | root.addChild(child2) 48 | 49 | child2child1 := newBreakdownNode("child2child1") 50 | child2child1.measurement = 1 51 | child2.addChild(child2child1) 52 | 53 | root.filter(2, 3, 100) 54 | 55 | if root.findChild("child1") == nil { 56 | t.Errorf("child1 should not be filtered") 57 | } 58 | 59 | if root.findChild("child2") == nil { 60 | t.Errorf("child2 should not be filtered") 61 | } 62 | 63 | if child2.findChild("child2child1") != nil { 64 | t.Errorf("child2child1 should be filtered") 65 | } 66 | } 67 | 68 | func TestBreakdownDepth(t *testing.T) { 69 | root := newBreakdownNode("root") 70 | 71 | child1 := newBreakdownNode("child1") 72 | root.addChild(child1) 73 | 74 | child2 := newBreakdownNode("child2") 75 | root.addChild(child2) 76 | 77 | child2child1 := newBreakdownNode("child2child1") 78 | child2.addChild(child2child1) 79 | 80 | if root.depth() != 3 { 81 | t.Errorf("root depth should be 3, but is %v", root.depth()) 82 | } 83 | 84 | if child1.depth() != 1 { 85 | t.Errorf("child1 depth should be 1, but is %v", child1.depth()) 86 | } 87 | 88 | if child2.depth() != 2 { 89 | t.Errorf("child2 depth should be 2, but is %v", child2.depth()) 90 | } 91 | } 92 | 93 | func TestBreakdownIncrement(t *testing.T) { 94 | root := newBreakdownNode("root") 95 | 96 | root.increment(12.3, 1) 97 | root.increment(0, 0) 98 | root.increment(5, 2) 99 | 100 | if root.measurement != 17.3 { 101 | t.Errorf("root measurement should be 17.3, but is %v", root.measurement) 102 | } 103 | 104 | if root.numSamples != 3 { 105 | t.Errorf("root numSamples should be 3, but is %v", root.numSamples) 106 | } 107 | } 108 | 109 | func TestBreakdownCounter(t *testing.T) { 110 | root := newBreakdownNode("root") 111 | 112 | child1 := newBreakdownNode("child1") 113 | root.addChild(child1) 114 | 115 | child2 := newBreakdownNode("child2") 116 | root.addChild(child2) 117 | 118 | child2child1 := newBreakdownNode("child2child1") 119 | child2.addChild(child2child1) 120 | 121 | child2child1.updateCounter(6, 1) 122 | child2child1.updateCounter(4, 1) 123 | child2child1.updateCounter(0, 0) 124 | child2child1.evaluateCounter() 125 | root.propagate() 126 | 127 | if root.measurement != 10 { 128 | t.Errorf("root measurement should be 10, but is %v", root.measurement) 129 | } 130 | 131 | if root.numSamples != 2 { 132 | t.Errorf("root numSamples should be 2, but is %v", root.numSamples) 133 | } 134 | } 135 | 136 | func TestBreakdownP95(t *testing.T) { 137 | root := newBreakdownNode("root") 138 | 139 | child1 := newBreakdownNode("child1") 140 | root.addChild(child1) 141 | 142 | child2 := newBreakdownNode("child2") 143 | root.addChild(child2) 144 | 145 | child2child1 := newBreakdownNode("child2child1") 146 | child2.addChild(child2child1) 147 | 148 | child2child1.updateP95(6.5) 149 | child2child1.updateP95(4.2) 150 | child2child1.updateP95(5.0) 151 | child2child1.evaluateP95() 152 | root.propagate() 153 | 154 | if root.measurement != 6.5 { 155 | t.Errorf("root measurement should be 6, but is %v", root.measurement) 156 | } 157 | 158 | if root.numSamples != 3 { 159 | t.Errorf("root numSamples should be 3, but is %v", root.numSamples) 160 | } 161 | } 162 | 163 | func TestBreakdownP95Big(t *testing.T) { 164 | root := newBreakdownNode("root") 165 | 166 | for i := 0; i < 10000; i++ { 167 | root.updateP95(200.0 + float64(rand.Intn(50))) 168 | } 169 | root.evaluateP95() 170 | 171 | if root.measurement < 200 || root.measurement > 250 { 172 | t.Errorf("root measurement should be in [200, 250], but is %v", root.measurement) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /internal/pprof/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/pprof/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/pprof/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 | // 8 | // This package is only for testing runtime/pprof. 9 | // It is not used by production Go programs. 10 | package profile 11 | 12 | import ( 13 | "bytes" 14 | "compress/gzip" 15 | "fmt" 16 | "io" 17 | "io/ioutil" 18 | "regexp" 19 | "strings" 20 | "time" 21 | ) 22 | 23 | // Profile is an in-memory representation of profile.proto. 24 | type Profile struct { 25 | SampleType []*ValueType 26 | Sample []*Sample 27 | Mapping []*Mapping 28 | Location []*Location 29 | Function []*Function 30 | 31 | DropFrames string 32 | KeepFrames string 33 | 34 | TimeNanos int64 35 | DurationNanos int64 36 | PeriodType *ValueType 37 | Period int64 38 | 39 | dropFramesX int64 40 | keepFramesX int64 41 | stringTable []string 42 | } 43 | 44 | // ValueType corresponds to Profile.ValueType 45 | type ValueType struct { 46 | Type string // cpu, wall, inuse_space, etc 47 | Unit string // seconds, nanoseconds, bytes, etc 48 | 49 | typeX int64 50 | unitX int64 51 | } 52 | 53 | // Sample corresponds to Profile.Sample 54 | type Sample struct { 55 | Location []*Location 56 | Value []int64 57 | Label map[string][]string 58 | NumLabel map[string][]int64 59 | 60 | locationIDX []uint64 61 | labelX []Label 62 | } 63 | 64 | // Label corresponds to Profile.Label 65 | type Label struct { 66 | keyX int64 67 | // Exactly one of the two following values must be set 68 | strX int64 69 | numX int64 // Integer value for this label 70 | } 71 | 72 | // Mapping corresponds to Profile.Mapping 73 | type Mapping struct { 74 | ID uint64 75 | Start uint64 76 | Limit uint64 77 | Offset uint64 78 | File string 79 | BuildID string 80 | HasFunctions bool 81 | HasFilenames bool 82 | HasLineNumbers bool 83 | HasInlineFrames bool 84 | 85 | fileX int64 86 | buildIDX int64 87 | } 88 | 89 | // Location corresponds to Profile.Location 90 | type Location struct { 91 | ID uint64 92 | Mapping *Mapping 93 | Address uint64 94 | Line []Line 95 | 96 | mappingIDX uint64 97 | } 98 | 99 | // Line corresponds to Profile.Line 100 | type Line struct { 101 | Function *Function 102 | Line int64 103 | 104 | functionIDX uint64 105 | } 106 | 107 | // Function corresponds to Profile.Function 108 | type Function struct { 109 | ID uint64 110 | Name string 111 | SystemName string 112 | Filename string 113 | StartLine int64 114 | 115 | nameX int64 116 | systemNameX int64 117 | filenameX int64 118 | } 119 | 120 | // Parse parses a profile and checks for its validity. The input 121 | // may be a gzip-compressed encoded protobuf or one of many legacy 122 | // profile formats which may be unsupported in the future. 123 | func Parse(r io.Reader) (*Profile, error) { 124 | orig, err := ioutil.ReadAll(r) 125 | if err != nil { 126 | return nil, err 127 | } 128 | 129 | var p *Profile 130 | if len(orig) >= 2 && orig[0] == 0x1f && orig[1] == 0x8b { 131 | gz, err := gzip.NewReader(bytes.NewBuffer(orig)) 132 | if err != nil { 133 | return nil, fmt.Errorf("decompressing profile: %v", err) 134 | } 135 | data, err := ioutil.ReadAll(gz) 136 | if err != nil { 137 | return nil, fmt.Errorf("decompressing profile: %v", err) 138 | } 139 | orig = data 140 | } 141 | if p, err = parseUncompressed(orig); err != nil { 142 | if p, err = parseLegacy(orig); err != nil { 143 | return nil, fmt.Errorf("parsing profile: %v", err) 144 | } 145 | } 146 | 147 | if err := p.CheckValid(); err != nil { 148 | return nil, fmt.Errorf("malformed profile: %v", err) 149 | } 150 | return p, nil 151 | } 152 | 153 | var errUnrecognized = fmt.Errorf("unrecognized profile format") 154 | var errMalformed = fmt.Errorf("malformed profile format") 155 | 156 | func parseLegacy(data []byte) (*Profile, error) { 157 | parsers := []func([]byte) (*Profile, error){ 158 | parseCPU, 159 | parseHeap, 160 | parseGoCount, // goroutine, threadcreate 161 | parseThread, 162 | parseContention, 163 | } 164 | 165 | for _, parser := range parsers { 166 | p, err := parser(data) 167 | if err == nil { 168 | p.setMain() 169 | p.addLegacyFrameInfo() 170 | return p, nil 171 | } 172 | if err != errUnrecognized { 173 | return nil, err 174 | } 175 | } 176 | return nil, errUnrecognized 177 | } 178 | 179 | func parseUncompressed(data []byte) (*Profile, error) { 180 | p := &Profile{} 181 | if err := unmarshal(data, p); err != nil { 182 | return nil, err 183 | } 184 | 185 | if err := p.postDecode(); err != nil { 186 | return nil, err 187 | } 188 | 189 | return p, nil 190 | } 191 | 192 | var libRx = regexp.MustCompile(`([.]so$|[.]so[._][0-9]+)`) 193 | 194 | // setMain scans Mapping entries and guesses which entry is main 195 | // because legacy profiles don't obey the convention of putting main 196 | // first. 197 | func (p *Profile) setMain() { 198 | for i := 0; i < len(p.Mapping); i++ { 199 | file := strings.TrimSpace(strings.Replace(p.Mapping[i].File, "(deleted)", "", -1)) 200 | if len(file) == 0 { 201 | continue 202 | } 203 | if len(libRx.FindStringSubmatch(file)) > 0 { 204 | continue 205 | } 206 | if strings.HasPrefix(file, "[") { 207 | continue 208 | } 209 | // Swap what we guess is main to position 0. 210 | tmp := p.Mapping[i] 211 | p.Mapping[i] = p.Mapping[0] 212 | p.Mapping[0] = tmp 213 | break 214 | } 215 | } 216 | 217 | // Write writes the profile as a gzip-compressed marshaled protobuf. 218 | func (p *Profile) Write(w io.Writer) error { 219 | p.preEncode() 220 | b := marshal(p) 221 | zw := gzip.NewWriter(w) 222 | defer zw.Close() 223 | _, err := zw.Write(b) 224 | return err 225 | } 226 | 227 | // CheckValid tests whether the profile is valid. Checks include, but are 228 | // not limited to: 229 | // - len(Profile.Sample[n].value) == len(Profile.value_unit) 230 | // - Sample.id has a corresponding Profile.Location 231 | func (p *Profile) CheckValid() error { 232 | // Check that sample values are consistent 233 | sampleLen := len(p.SampleType) 234 | if sampleLen == 0 && len(p.Sample) != 0 { 235 | return fmt.Errorf("missing sample type information") 236 | } 237 | for _, s := range p.Sample { 238 | if len(s.Value) != sampleLen { 239 | return fmt.Errorf("mismatch: sample has: %d values vs. %d types", len(s.Value), len(p.SampleType)) 240 | } 241 | } 242 | 243 | // Check that all mappings/locations/functions are in the tables 244 | // Check that there are no duplicate ids 245 | mappings := make(map[uint64]*Mapping, len(p.Mapping)) 246 | for _, m := range p.Mapping { 247 | if m.ID == 0 { 248 | return fmt.Errorf("found mapping with reserved ID=0") 249 | } 250 | if mappings[m.ID] != nil { 251 | return fmt.Errorf("multiple mappings with same id: %d", m.ID) 252 | } 253 | mappings[m.ID] = m 254 | } 255 | functions := make(map[uint64]*Function, len(p.Function)) 256 | for _, f := range p.Function { 257 | if f.ID == 0 { 258 | return fmt.Errorf("found function with reserved ID=0") 259 | } 260 | if functions[f.ID] != nil { 261 | return fmt.Errorf("multiple functions with same id: %d", f.ID) 262 | } 263 | functions[f.ID] = f 264 | } 265 | locations := make(map[uint64]*Location, len(p.Location)) 266 | for _, l := range p.Location { 267 | if l.ID == 0 { 268 | return fmt.Errorf("found location with reserved id=0") 269 | } 270 | if locations[l.ID] != nil { 271 | return fmt.Errorf("multiple locations with same id: %d", l.ID) 272 | } 273 | locations[l.ID] = l 274 | if m := l.Mapping; m != nil { 275 | if m.ID == 0 || mappings[m.ID] != m { 276 | return fmt.Errorf("inconsistent mapping %p: %d", m, m.ID) 277 | } 278 | } 279 | for _, ln := range l.Line { 280 | if f := ln.Function; f != nil { 281 | if f.ID == 0 || functions[f.ID] != f { 282 | return fmt.Errorf("inconsistent function %p: %d", f, f.ID) 283 | } 284 | } 285 | } 286 | } 287 | return nil 288 | } 289 | 290 | // Aggregate merges the locations in the profile into equivalence 291 | // classes preserving the request attributes. It also updates the 292 | // samples to point to the merged locations. 293 | func (p *Profile) Aggregate(inlineFrame, function, filename, linenumber, address bool) error { 294 | for _, m := range p.Mapping { 295 | m.HasInlineFrames = m.HasInlineFrames && inlineFrame 296 | m.HasFunctions = m.HasFunctions && function 297 | m.HasFilenames = m.HasFilenames && filename 298 | m.HasLineNumbers = m.HasLineNumbers && linenumber 299 | } 300 | 301 | // Aggregate functions 302 | if !function || !filename { 303 | for _, f := range p.Function { 304 | if !function { 305 | f.Name = "" 306 | f.SystemName = "" 307 | } 308 | if !filename { 309 | f.Filename = "" 310 | } 311 | } 312 | } 313 | 314 | // Aggregate locations 315 | if !inlineFrame || !address || !linenumber { 316 | for _, l := range p.Location { 317 | if !inlineFrame && len(l.Line) > 1 { 318 | l.Line = l.Line[len(l.Line)-1:] 319 | } 320 | if !linenumber { 321 | for i := range l.Line { 322 | l.Line[i].Line = 0 323 | } 324 | } 325 | if !address { 326 | l.Address = 0 327 | } 328 | } 329 | } 330 | 331 | return p.CheckValid() 332 | } 333 | 334 | // Print dumps a text representation of a profile. Intended mainly 335 | // for debugging purposes. 336 | func (p *Profile) String() string { 337 | 338 | ss := make([]string, 0, len(p.Sample)+len(p.Mapping)+len(p.Location)) 339 | if pt := p.PeriodType; pt != nil { 340 | ss = append(ss, fmt.Sprintf("PeriodType: %s %s", pt.Type, pt.Unit)) 341 | } 342 | ss = append(ss, fmt.Sprintf("Period: %d", p.Period)) 343 | if p.TimeNanos != 0 { 344 | ss = append(ss, fmt.Sprintf("Time: %v", time.Unix(0, p.TimeNanos))) 345 | } 346 | if p.DurationNanos != 0 { 347 | ss = append(ss, fmt.Sprintf("Duration: %v", time.Duration(p.DurationNanos))) 348 | } 349 | 350 | ss = append(ss, "Samples:") 351 | var sh1 string 352 | for _, s := range p.SampleType { 353 | sh1 = sh1 + fmt.Sprintf("%s/%s ", s.Type, s.Unit) 354 | } 355 | ss = append(ss, strings.TrimSpace(sh1)) 356 | for _, s := range p.Sample { 357 | var sv string 358 | for _, v := range s.Value { 359 | sv = fmt.Sprintf("%s %10d", sv, v) 360 | } 361 | sv = sv + ": " 362 | for _, l := range s.Location { 363 | sv = sv + fmt.Sprintf("%d ", l.ID) 364 | } 365 | ss = append(ss, sv) 366 | const labelHeader = " " 367 | if len(s.Label) > 0 { 368 | ls := labelHeader 369 | for k, v := range s.Label { 370 | ls = ls + fmt.Sprintf("%s:%v ", k, v) 371 | } 372 | ss = append(ss, ls) 373 | } 374 | if len(s.NumLabel) > 0 { 375 | ls := labelHeader 376 | for k, v := range s.NumLabel { 377 | ls = ls + fmt.Sprintf("%s:%v ", k, v) 378 | } 379 | ss = append(ss, ls) 380 | } 381 | } 382 | 383 | ss = append(ss, "Locations") 384 | for _, l := range p.Location { 385 | locStr := fmt.Sprintf("%6d: %#x ", l.ID, l.Address) 386 | if m := l.Mapping; m != nil { 387 | locStr = locStr + fmt.Sprintf("M=%d ", m.ID) 388 | } 389 | if len(l.Line) == 0 { 390 | ss = append(ss, locStr) 391 | } 392 | for li := range l.Line { 393 | lnStr := "??" 394 | if fn := l.Line[li].Function; fn != nil { 395 | lnStr = fmt.Sprintf("%s %s:%d s=%d", 396 | fn.Name, 397 | fn.Filename, 398 | l.Line[li].Line, 399 | fn.StartLine) 400 | if fn.Name != fn.SystemName { 401 | lnStr = lnStr + "(" + fn.SystemName + ")" 402 | } 403 | } 404 | ss = append(ss, locStr+lnStr) 405 | // Do not print location details past the first line 406 | locStr = " " 407 | } 408 | } 409 | 410 | ss = append(ss, "Mappings") 411 | for _, m := range p.Mapping { 412 | bits := "" 413 | if m.HasFunctions { 414 | bits = bits + "[FN]" 415 | } 416 | if m.HasFilenames { 417 | bits = bits + "[FL]" 418 | } 419 | if m.HasLineNumbers { 420 | bits = bits + "[LN]" 421 | } 422 | if m.HasInlineFrames { 423 | bits = bits + "[IN]" 424 | } 425 | ss = append(ss, fmt.Sprintf("%d: %#x/%#x/%#x %s %s %s", 426 | m.ID, 427 | m.Start, m.Limit, m.Offset, 428 | m.File, 429 | m.BuildID, 430 | bits)) 431 | } 432 | 433 | return strings.Join(ss, "\n") + "\n" 434 | } 435 | 436 | // Merge adds profile p adjusted by ratio r into profile p. Profiles 437 | // must be compatible (same Type and SampleType). 438 | // TODO(rsilvera): consider normalizing the profiles based on the 439 | // total samples collected. 440 | func (p *Profile) Merge(pb *Profile, r float64) error { 441 | if err := p.Compatible(pb); err != nil { 442 | return err 443 | } 444 | 445 | pb = pb.Copy() 446 | 447 | // Keep the largest of the two periods. 448 | if pb.Period > p.Period { 449 | p.Period = pb.Period 450 | } 451 | 452 | p.DurationNanos += pb.DurationNanos 453 | 454 | p.Mapping = append(p.Mapping, pb.Mapping...) 455 | for i, m := range p.Mapping { 456 | m.ID = uint64(i + 1) 457 | } 458 | p.Location = append(p.Location, pb.Location...) 459 | for i, l := range p.Location { 460 | l.ID = uint64(i + 1) 461 | } 462 | p.Function = append(p.Function, pb.Function...) 463 | for i, f := range p.Function { 464 | f.ID = uint64(i + 1) 465 | } 466 | 467 | if r != 1.0 { 468 | for _, s := range pb.Sample { 469 | for i, v := range s.Value { 470 | s.Value[i] = int64((float64(v) * r)) 471 | } 472 | } 473 | } 474 | p.Sample = append(p.Sample, pb.Sample...) 475 | return p.CheckValid() 476 | } 477 | 478 | // Compatible determines if two profiles can be compared/merged. 479 | // returns nil if the profiles are compatible; otherwise an error with 480 | // details on the incompatibility. 481 | func (p *Profile) Compatible(pb *Profile) error { 482 | if !compatibleValueTypes(p.PeriodType, pb.PeriodType) { 483 | return fmt.Errorf("incompatible period types %v and %v", p.PeriodType, pb.PeriodType) 484 | } 485 | 486 | if len(p.SampleType) != len(pb.SampleType) { 487 | return fmt.Errorf("incompatible sample types %v and %v", p.SampleType, pb.SampleType) 488 | } 489 | 490 | for i := range p.SampleType { 491 | if !compatibleValueTypes(p.SampleType[i], pb.SampleType[i]) { 492 | return fmt.Errorf("incompatible sample types %v and %v", p.SampleType, pb.SampleType) 493 | } 494 | } 495 | 496 | return nil 497 | } 498 | 499 | // HasFunctions determines if all locations in this profile have 500 | // symbolized function information. 501 | func (p *Profile) HasFunctions() bool { 502 | for _, l := range p.Location { 503 | if l.Mapping == nil || !l.Mapping.HasFunctions { 504 | return false 505 | } 506 | } 507 | return true 508 | } 509 | 510 | // HasFileLines determines if all locations in this profile have 511 | // symbolized file and line number information. 512 | func (p *Profile) HasFileLines() bool { 513 | for _, l := range p.Location { 514 | if l.Mapping == nil || (!l.Mapping.HasFilenames || !l.Mapping.HasLineNumbers) { 515 | return false 516 | } 517 | } 518 | return true 519 | } 520 | 521 | func compatibleValueTypes(v1, v2 *ValueType) bool { 522 | if v1 == nil || v2 == nil { 523 | return true // No grounds to disqualify. 524 | } 525 | return v1.Type == v2.Type && v1.Unit == v2.Unit 526 | } 527 | 528 | // Copy makes a fully independent copy of a profile. 529 | func (p *Profile) Copy() *Profile { 530 | p.preEncode() 531 | b := marshal(p) 532 | 533 | pp := &Profile{} 534 | if err := unmarshal(b, pp); err != nil { 535 | panic(err) 536 | } 537 | if err := pp.postDecode(); err != nil { 538 | panic(err) 539 | } 540 | 541 | return pp 542 | } 543 | 544 | // Demangler maps symbol names to a human-readable form. This may 545 | // include C++ demangling and additional simplification. Names that 546 | // are not demangled may be missing from the resulting map. 547 | type Demangler func(name []string) (map[string]string, error) 548 | 549 | // Demangle attempts to demangle and optionally simplify any function 550 | // names referenced in the profile. It works on a best-effort basis: 551 | // it will silently preserve the original names in case of any errors. 552 | func (p *Profile) Demangle(d Demangler) error { 553 | // Collect names to demangle. 554 | var names []string 555 | for _, fn := range p.Function { 556 | names = append(names, fn.SystemName) 557 | } 558 | 559 | // Update profile with demangled names. 560 | demangled, err := d(names) 561 | if err != nil { 562 | return err 563 | } 564 | for _, fn := range p.Function { 565 | if dd, ok := demangled[fn.SystemName]; ok { 566 | fn.Name = dd 567 | } 568 | } 569 | return nil 570 | } 571 | 572 | // Empty returns true if the profile contains no samples. 573 | func (p *Profile) Empty() bool { 574 | return len(p.Sample) == 0 575 | } 576 | -------------------------------------------------------------------------------- /internal/pprof/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 | 26 | func TestParseContention(t *testing.T) { 27 | tests := []struct { 28 | name string 29 | in string 30 | wantErr bool 31 | }{ 32 | { 33 | name: "valid", 34 | in: `--- mutex: 35 | cycles/second=3491920901 36 | sampling period=1 37 | 43227965305 1659640 @ 0x45e851 0x45f764 0x4a2be1 0x44ea31 38 | 34035731690 15760 @ 0x45e851 0x45f764 0x4a2b17 0x44ea31 39 | `, 40 | }, 41 | { 42 | name: "valid with comment", 43 | in: `--- mutex: 44 | cycles/second=3491920901 45 | sampling period=1 46 | 43227965305 1659640 @ 0x45e851 0x45f764 0x4a2be1 0x44ea31 47 | # 0x45e850 sync.(*Mutex).Unlock+0x80 /go/src/sync/mutex.go:126 48 | # 0x45f763 sync.(*RWMutex).Unlock+0x83 /go/src/sync/rwmutex.go:125 49 | # 0x4a2be0 main.main.func3+0x70 /go/src/internal/pprof/profile/a_binary.go:58 50 | 51 | 34035731690 15760 @ 0x45e851 0x45f764 0x4a2b17 0x44ea31 52 | # 0x45e850 sync.(*Mutex).Unlock+0x80 /go/src/sync/mutex.go:126 53 | # 0x45f763 sync.(*RWMutex).Unlock+0x83 /go/src/sync/rwmutex.go:125 54 | # 0x4a2b16 main.main.func2+0xd6 /go/src/internal/pprof/profile/a_binary.go:48 55 | `, 56 | }, 57 | { 58 | name: "empty", 59 | in: `--- mutex:`, 60 | wantErr: true, 61 | }, 62 | { 63 | name: "invalid header", 64 | in: `--- channel: 65 | 43227965305 1659640 @ 0x45e851 0x45f764 0x4a2be1 0x44ea31`, 66 | wantErr: true, 67 | }, 68 | } 69 | for _, tc := range tests { 70 | _, err := parseContention([]byte(tc.in)) 71 | if tc.wantErr && err == nil { 72 | t.Errorf("parseContention(%q) succeeded unexpectedly", tc.name) 73 | } 74 | if !tc.wantErr && err != nil { 75 | t.Errorf("parseContention(%q) failed unexpectedly: %v", tc.name, err) 76 | } 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /internal/pprof/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 | if len(x) > 2 { 68 | // Use packed encoding 69 | n1 := len(b.data) 70 | for _, u := range x { 71 | encodeVarint(b, u) 72 | } 73 | n2 := len(b.data) 74 | encodeLength(b, tag, n2-n1) 75 | n3 := len(b.data) 76 | copy(b.tmp[:], b.data[n2:n3]) 77 | copy(b.data[n1+(n3-n2):], b.data[n1:n2]) 78 | copy(b.data[n1:], b.tmp[:n3-n2]) 79 | return 80 | } 81 | for _, u := range x { 82 | encodeUint64(b, tag, u) 83 | } 84 | } 85 | 86 | func encodeUint64Opt(b *buffer, tag int, x uint64) { 87 | if x == 0 { 88 | return 89 | } 90 | encodeUint64(b, tag, x) 91 | } 92 | 93 | func encodeInt64(b *buffer, tag int, x int64) { 94 | u := uint64(x) 95 | encodeUint64(b, tag, u) 96 | } 97 | 98 | func encodeInt64Opt(b *buffer, tag int, x int64) { 99 | if x == 0 { 100 | return 101 | } 102 | encodeInt64(b, tag, x) 103 | } 104 | 105 | func encodeInt64s(b *buffer, tag int, x []int64) { 106 | if len(x) > 2 { 107 | // Use packed encoding 108 | n1 := len(b.data) 109 | for _, u := range x { 110 | encodeVarint(b, uint64(u)) 111 | } 112 | n2 := len(b.data) 113 | encodeLength(b, tag, n2-n1) 114 | n3 := len(b.data) 115 | copy(b.tmp[:], b.data[n2:n3]) 116 | copy(b.data[n1+(n3-n2):], b.data[n1:n2]) 117 | copy(b.data[n1:], b.tmp[:n3-n2]) 118 | return 119 | } 120 | for _, u := range x { 121 | encodeInt64(b, tag, u) 122 | } 123 | } 124 | 125 | func encodeString(b *buffer, tag int, x string) { 126 | encodeLength(b, tag, len(x)) 127 | b.data = append(b.data, x...) 128 | } 129 | 130 | func encodeStrings(b *buffer, tag int, x []string) { 131 | for _, s := range x { 132 | encodeString(b, tag, s) 133 | } 134 | } 135 | 136 | func encodeStringOpt(b *buffer, tag int, x string) { 137 | if x == "" { 138 | return 139 | } 140 | encodeString(b, tag, x) 141 | } 142 | 143 | func encodeBool(b *buffer, tag int, x bool) { 144 | if x { 145 | encodeUint64(b, tag, 1) 146 | } else { 147 | encodeUint64(b, tag, 0) 148 | } 149 | } 150 | 151 | func encodeBoolOpt(b *buffer, tag int, x bool) { 152 | if x == false { 153 | return 154 | } 155 | encodeBool(b, tag, x) 156 | } 157 | 158 | func encodeMessage(b *buffer, tag int, m message) { 159 | n1 := len(b.data) 160 | m.encode(b) 161 | n2 := len(b.data) 162 | encodeLength(b, tag, n2-n1) 163 | n3 := len(b.data) 164 | copy(b.tmp[:], b.data[n2:n3]) 165 | copy(b.data[n1+(n3-n2):], b.data[n1:n2]) 166 | copy(b.data[n1:], b.tmp[:n3-n2]) 167 | } 168 | 169 | func unmarshal(data []byte, m message) (err error) { 170 | b := buffer{data: data, typ: 2} 171 | return decodeMessage(&b, m) 172 | } 173 | 174 | func le64(p []byte) uint64 { 175 | 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 176 | } 177 | 178 | func le32(p []byte) uint32 { 179 | return uint32(p[0]) | uint32(p[1])<<8 | uint32(p[2])<<16 | uint32(p[3])<<24 180 | } 181 | 182 | func decodeVarint(data []byte) (uint64, []byte, error) { 183 | var i int 184 | var u uint64 185 | for i = 0; ; i++ { 186 | if i >= 10 || i >= len(data) { 187 | return 0, nil, errors.New("bad varint") 188 | } 189 | u |= uint64(data[i]&0x7F) << uint(7*i) 190 | if data[i]&0x80 == 0 { 191 | return u, data[i+1:], nil 192 | } 193 | } 194 | } 195 | 196 | func decodeField(b *buffer, data []byte) ([]byte, error) { 197 | x, data, err := decodeVarint(data) 198 | if err != nil { 199 | return nil, err 200 | } 201 | b.field = int(x >> 3) 202 | b.typ = int(x & 7) 203 | b.data = nil 204 | b.u64 = 0 205 | switch b.typ { 206 | case 0: 207 | b.u64, data, err = decodeVarint(data) 208 | if err != nil { 209 | return nil, err 210 | } 211 | case 1: 212 | if len(data) < 8 { 213 | return nil, errors.New("not enough data") 214 | } 215 | b.u64 = le64(data[:8]) 216 | data = data[8:] 217 | case 2: 218 | var n uint64 219 | n, data, err = decodeVarint(data) 220 | if err != nil { 221 | return nil, err 222 | } 223 | if n > uint64(len(data)) { 224 | return nil, errors.New("too much data") 225 | } 226 | b.data = data[:n] 227 | data = data[n:] 228 | case 5: 229 | if len(data) < 4 { 230 | return nil, errors.New("not enough data") 231 | } 232 | b.u64 = uint64(le32(data[:4])) 233 | data = data[4:] 234 | default: 235 | return nil, errors.New("unknown type: " + string(b.typ)) 236 | } 237 | 238 | return data, nil 239 | } 240 | 241 | func checkType(b *buffer, typ int) error { 242 | if b.typ != typ { 243 | return errors.New("type mismatch") 244 | } 245 | return nil 246 | } 247 | 248 | func decodeMessage(b *buffer, m message) error { 249 | if err := checkType(b, 2); err != nil { 250 | return err 251 | } 252 | dec := m.decoder() 253 | data := b.data 254 | for len(data) > 0 { 255 | // pull varint field# + type 256 | var err error 257 | data, err = decodeField(b, data) 258 | if err != nil { 259 | return err 260 | } 261 | if b.field >= len(dec) || dec[b.field] == nil { 262 | continue 263 | } 264 | if err := dec[b.field](b, m); err != nil { 265 | return err 266 | } 267 | } 268 | return nil 269 | } 270 | 271 | func decodeInt64(b *buffer, x *int64) error { 272 | if err := checkType(b, 0); err != nil { 273 | return err 274 | } 275 | *x = int64(b.u64) 276 | return nil 277 | } 278 | 279 | func decodeInt64s(b *buffer, x *[]int64) error { 280 | if b.typ == 2 { 281 | // Packed encoding 282 | data := b.data 283 | for len(data) > 0 { 284 | var u uint64 285 | var err error 286 | 287 | if u, data, err = decodeVarint(data); err != nil { 288 | return err 289 | } 290 | *x = append(*x, int64(u)) 291 | } 292 | return nil 293 | } 294 | var i int64 295 | if err := decodeInt64(b, &i); err != nil { 296 | return err 297 | } 298 | *x = append(*x, i) 299 | return nil 300 | } 301 | 302 | func decodeUint64(b *buffer, x *uint64) error { 303 | if err := checkType(b, 0); err != nil { 304 | return err 305 | } 306 | *x = b.u64 307 | return nil 308 | } 309 | 310 | func decodeUint64s(b *buffer, x *[]uint64) error { 311 | if b.typ == 2 { 312 | data := b.data 313 | // Packed encoding 314 | for len(data) > 0 { 315 | var u uint64 316 | var err error 317 | 318 | if u, data, err = decodeVarint(data); err != nil { 319 | return err 320 | } 321 | *x = append(*x, u) 322 | } 323 | return nil 324 | } 325 | var u uint64 326 | if err := decodeUint64(b, &u); err != nil { 327 | return err 328 | } 329 | *x = append(*x, u) 330 | return nil 331 | } 332 | 333 | func decodeString(b *buffer, x *string) error { 334 | if err := checkType(b, 2); err != nil { 335 | return err 336 | } 337 | *x = string(b.data) 338 | return nil 339 | } 340 | 341 | func decodeStrings(b *buffer, x *[]string) error { 342 | var s string 343 | if err := decodeString(b, &s); err != nil { 344 | return err 345 | } 346 | *x = append(*x, s) 347 | return nil 348 | } 349 | 350 | func decodeBool(b *buffer, x *bool) error { 351 | if err := checkType(b, 0); err != nil { 352 | return err 353 | } 354 | if int64(b.u64) == 0 { 355 | *x = false 356 | } else { 357 | *x = true 358 | } 359 | return nil 360 | } 361 | -------------------------------------------------------------------------------- /internal/pprof/profile/proto_test.go: -------------------------------------------------------------------------------- 1 | package profile 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestPackedEncoding(t *testing.T) { 9 | 10 | type testcase struct { 11 | uint64s []uint64 12 | int64s []int64 13 | encoded []byte 14 | } 15 | for i, tc := range []testcase{ 16 | { 17 | []uint64{0, 1, 10, 100, 1000, 10000}, 18 | []int64{1000, 0, 1000}, 19 | []byte{10, 8, 0, 1, 10, 100, 232, 7, 144, 78, 18, 5, 232, 7, 0, 232, 7}, 20 | }, 21 | { 22 | []uint64{10000}, 23 | nil, 24 | []byte{8, 144, 78}, 25 | }, 26 | { 27 | nil, 28 | []int64{-10000}, 29 | []byte{16, 240, 177, 255, 255, 255, 255, 255, 255, 255, 1}, 30 | }, 31 | } { 32 | source := &packedInts{tc.uint64s, tc.int64s} 33 | if got, want := marshal(source), tc.encoded; !reflect.DeepEqual(got, want) { 34 | t.Errorf("failed encode %d, got %v, want %v", i, got, want) 35 | } 36 | 37 | dest := new(packedInts) 38 | if err := unmarshal(tc.encoded, dest); err != nil { 39 | t.Errorf("failed decode %d: %v", i, err) 40 | continue 41 | } 42 | if got, want := dest.uint64s, tc.uint64s; !reflect.DeepEqual(got, want) { 43 | t.Errorf("failed decode uint64s %d, got %v, want %v", i, got, want) 44 | } 45 | if got, want := dest.int64s, tc.int64s; !reflect.DeepEqual(got, want) { 46 | t.Errorf("failed decode int64s %d, got %v, want %v", i, got, want) 47 | } 48 | } 49 | } 50 | 51 | type packedInts struct { 52 | uint64s []uint64 53 | int64s []int64 54 | } 55 | 56 | func (u *packedInts) decoder() []decoder { 57 | return []decoder{ 58 | nil, 59 | func(b *buffer, m message) error { return decodeUint64s(b, &m.(*packedInts).uint64s) }, 60 | func(b *buffer, m message) error { return decodeInt64s(b, &m.(*packedInts).int64s) }, 61 | } 62 | } 63 | 64 | func (u *packedInts) encode(b *buffer) { 65 | encodeUint64s(b, 1, u.uint64s) 66 | encodeInt64s(b, 2, u.int64s) 67 | } 68 | -------------------------------------------------------------------------------- /internal/pprof/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/process_reporter.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "runtime" 5 | "time" 6 | ) 7 | 8 | type ProcessReporter struct { 9 | ReportInterval int64 10 | 11 | agent *Agent 12 | started *Flag 13 | reportTimer *Timer 14 | 15 | metrics map[string]*Metric 16 | } 17 | 18 | func newProcessReporter(agent *Agent) *ProcessReporter { 19 | pr := &ProcessReporter{ 20 | ReportInterval: 60, 21 | 22 | agent: agent, 23 | started: &Flag{}, 24 | reportTimer: nil, 25 | 26 | metrics: nil, 27 | } 28 | 29 | return pr 30 | } 31 | 32 | func (pr *ProcessReporter) reset() { 33 | pr.metrics = make(map[string]*Metric) 34 | } 35 | 36 | func (pr *ProcessReporter) start() { 37 | if !pr.agent.AutoProfiling { 38 | return 39 | } 40 | 41 | if !pr.started.SetIfUnset() { 42 | return 43 | } 44 | 45 | pr.reset() 46 | 47 | pr.reportTimer = pr.agent.createTimer(0, time.Duration(pr.ReportInterval)*time.Second, func() { 48 | pr.report() 49 | }) 50 | } 51 | 52 | func (pr *ProcessReporter) stop() { 53 | if !pr.started.UnsetIfSet() { 54 | return 55 | } 56 | 57 | if pr.reportTimer != nil { 58 | pr.reportTimer.Stop() 59 | } 60 | } 61 | 62 | func (pr *ProcessReporter) reportMetric(typ string, category string, name string, unit string, value float64) *Metric { 63 | key := typ + category + name 64 | var metric *Metric 65 | if existingMetric, exists := pr.metrics[key]; !exists { 66 | metric = newMetric(pr.agent, typ, category, name, unit) 67 | pr.metrics[key] = metric 68 | } else { 69 | metric = existingMetric 70 | } 71 | 72 | metric.createMeasurement(TriggerTimer, value, 0, nil) 73 | 74 | if metric.hasMeasurement() { 75 | pr.agent.messageQueue.addMessage("metric", metric.toMap()) 76 | } 77 | 78 | return metric 79 | } 80 | 81 | func (pr *ProcessReporter) report() { 82 | if !pr.started.IsSet() { 83 | return 84 | } 85 | 86 | cpuTime, err := readCPUTime() 87 | if err == nil { 88 | cpuTimeMetric := pr.reportMetric(TypeCounter, CategoryCPU, NameCPUTime, UnitNanosecond, float64(cpuTime)) 89 | if cpuTimeMetric.hasMeasurement() { 90 | cpuUsage := (float64(cpuTimeMetric.measurement.value) / float64(60*1e9)) * 100 91 | cpuUsage = cpuUsage / float64(runtime.NumCPU()) 92 | pr.reportMetric(TypeState, CategoryCPU, NameCPUUsage, UnitPercent, float64(cpuUsage)) 93 | } 94 | } else { 95 | pr.agent.error(err) 96 | } 97 | 98 | maxRSS, err := readMaxRSS() 99 | if err == nil { 100 | pr.reportMetric(TypeState, CategoryMemory, NameMaxRSS, UnitKilobyte, float64(maxRSS)) 101 | } else { 102 | pr.agent.error(err) 103 | } 104 | 105 | currentRSS, err := readCurrentRSS() 106 | if err == nil { 107 | pr.reportMetric(TypeState, CategoryMemory, NameCurrentRSS, UnitKilobyte, float64(currentRSS)) 108 | } else { 109 | pr.agent.error(err) 110 | } 111 | 112 | vmSize, err := readVMSize() 113 | if err == nil { 114 | pr.reportMetric(TypeState, CategoryMemory, NameVMSize, UnitKilobyte, float64(vmSize)) 115 | } else { 116 | pr.agent.error(err) 117 | } 118 | 119 | memStats := &runtime.MemStats{} 120 | runtime.ReadMemStats(memStats) 121 | pr.reportMetric(TypeState, CategoryMemory, NameAllocated, UnitByte, float64(memStats.Alloc)) 122 | pr.reportMetric(TypeCounter, CategoryMemory, NameLookups, UnitNone, float64(memStats.Lookups)) 123 | pr.reportMetric(TypeCounter, CategoryMemory, NameMallocs, UnitNone, float64(memStats.Mallocs)) 124 | pr.reportMetric(TypeCounter, CategoryMemory, NameFrees, UnitNone, float64(memStats.Frees)) 125 | pr.reportMetric(TypeState, CategoryMemory, NameHeapIdle, UnitByte, float64(memStats.HeapIdle)) 126 | pr.reportMetric(TypeState, CategoryMemory, NameHeapInuse, UnitByte, float64(memStats.HeapInuse)) 127 | pr.reportMetric(TypeState, CategoryMemory, NameHeapObjects, UnitNone, float64(memStats.HeapObjects)) 128 | pr.reportMetric(TypeCounter, CategoryGC, NameGCTotalPause, UnitNanosecond, float64(memStats.PauseTotalNs)) 129 | pr.reportMetric(TypeCounter, CategoryGC, NameNumGC, UnitNone, float64(memStats.NumGC)) 130 | pr.reportMetric(TypeState, CategoryGC, NameGCCPUFraction, UnitNone, float64(memStats.GCCPUFraction)) 131 | 132 | numGoroutine := runtime.NumGoroutine() 133 | pr.reportMetric(TypeState, CategoryRuntime, NameNumGoroutines, UnitNone, float64(numGoroutine)) 134 | 135 | numCgoCall := runtime.NumCgoCall() 136 | pr.reportMetric(TypeCounter, CategoryRuntime, NameNumCgoCalls, UnitNone, float64(numCgoCall)) 137 | } 138 | -------------------------------------------------------------------------------- /internal/process_reporter_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "testing" 7 | ) 8 | 9 | func TestReport(t *testing.T) { 10 | agent := NewAgent() 11 | agent.Debug = true 12 | 13 | agent.processReporter.reset() 14 | agent.processReporter.started.Set() 15 | 16 | agent.processReporter.report() 17 | agent.processReporter.report() 18 | agent.processReporter.report() 19 | 20 | metrics := agent.processReporter.metrics 21 | 22 | isValid(t, metrics, TypeCounter, CategoryCPU, NameCPUTime, 0, math.Inf(0)) 23 | isValid(t, metrics, TypeState, CategoryCPU, NameCPUUsage, 0, math.Inf(0)) 24 | isValid(t, metrics, TypeState, CategoryMemory, NameMaxRSS, 0, math.Inf(0)) 25 | isValid(t, metrics, TypeState, CategoryMemory, NameAllocated, 0, math.Inf(0)) 26 | isValid(t, metrics, TypeCounter, CategoryMemory, NameLookups, 0, math.Inf(0)) 27 | isValid(t, metrics, TypeCounter, CategoryMemory, NameMallocs, 0, math.Inf(0)) 28 | isValid(t, metrics, TypeCounter, CategoryMemory, NameFrees, 0, math.Inf(0)) 29 | isValid(t, metrics, TypeState, CategoryMemory, NameHeapIdle, 0, math.Inf(0)) 30 | isValid(t, metrics, TypeState, CategoryMemory, NameHeapInuse, 0, math.Inf(0)) 31 | isValid(t, metrics, TypeState, CategoryMemory, NameHeapObjects, 0, math.Inf(0)) 32 | isValid(t, metrics, TypeCounter, CategoryGC, NameGCTotalPause, 0, math.Inf(0)) 33 | isValid(t, metrics, TypeCounter, CategoryGC, NameNumGC, 0, math.Inf(0)) 34 | isValid(t, metrics, TypeState, CategoryGC, NameGCCPUFraction, 0, 1) 35 | isValid(t, metrics, TypeState, CategoryRuntime, NameNumGoroutines, 0, math.Inf(0)) 36 | isValid(t, metrics, TypeCounter, CategoryRuntime, NameNumCgoCalls, 0, math.Inf(0)) 37 | } 38 | 39 | func isValid(t *testing.T, metrics map[string]*Metric, typ string, category string, name string, minValue float64, maxValue float64) { 40 | if metric, exists := metrics[typ+category+name]; exists { 41 | if metric.hasMeasurement() { 42 | valid := metric.measurement.value >= minValue && metric.measurement.value <= maxValue 43 | 44 | if !valid { 45 | t.Error(fmt.Sprintf("%v - %v: %v", category, name, metric.measurement.value)) 46 | } 47 | 48 | return 49 | } 50 | } 51 | 52 | t.Errorf("%v - %v", category, name) 53 | } 54 | -------------------------------------------------------------------------------- /internal/profile_reporter.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "math/rand" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | type ProfilerConfig struct { 10 | logPrefix string 11 | reportOnly bool 12 | maxProfileDuration int64 13 | maxSpanDuration int64 14 | maxSpanCount int32 15 | spanInterval int64 16 | reportInterval int64 17 | } 18 | 19 | type Profiler interface { 20 | reset() 21 | startProfiler() error 22 | stopProfiler() error 23 | buildProfile(duration int64, workloads map[string]int64) ([]*ProfileData, error) 24 | } 25 | 26 | type ProfileData struct { 27 | category string 28 | name string 29 | unit string 30 | unitInterval int64 31 | profile *BreakdownNode 32 | } 33 | 34 | type ProfileReporter struct { 35 | agent *Agent 36 | started *Flag 37 | profiler Profiler 38 | config *ProfilerConfig 39 | spanTimer *Timer 40 | reportTimer *Timer 41 | profileLock *sync.Mutex 42 | profileStartTimestamp int64 43 | profileDuration int64 44 | spanCount int32 45 | spanActive *Flag 46 | spanStart int64 47 | spanTimeout *Timer 48 | spanTrigger string 49 | workloads map[string]int64 50 | } 51 | 52 | func newProfileReporter(agent *Agent, profiler Profiler, config *ProfilerConfig) *ProfileReporter { 53 | pr := &ProfileReporter{ 54 | agent: agent, 55 | started: &Flag{}, 56 | profiler: profiler, 57 | config: config, 58 | spanTimer: nil, 59 | reportTimer: nil, 60 | profileLock: &sync.Mutex{}, 61 | profileStartTimestamp: 0, 62 | profileDuration: 0, 63 | spanCount: 0, 64 | spanActive: &Flag{}, 65 | spanStart: 0, 66 | spanTimeout: nil, 67 | spanTrigger: "", 68 | workloads: nil, 69 | } 70 | 71 | return pr 72 | } 73 | 74 | func (pr *ProfileReporter) start() { 75 | if !pr.started.SetIfUnset() { 76 | return 77 | } 78 | 79 | pr.profileLock.Lock() 80 | defer pr.profileLock.Unlock() 81 | 82 | pr.reset() 83 | 84 | if pr.agent.AutoProfiling { 85 | if !pr.config.reportOnly { 86 | pr.spanTimer = pr.agent.createTimer(0, time.Duration(pr.config.spanInterval)*time.Second, func() { 87 | time.Sleep(time.Duration(rand.Int63n(pr.config.spanInterval-pr.config.maxSpanDuration)) * time.Second) 88 | pr.startProfiling(false, true, "") 89 | }) 90 | } 91 | 92 | pr.reportTimer = pr.agent.createTimer(0, time.Duration(pr.config.reportInterval)*time.Second, func() { 93 | pr.report(false) 94 | }) 95 | } 96 | } 97 | 98 | func (pr *ProfileReporter) stop() { 99 | if !pr.started.UnsetIfSet() { 100 | return 101 | } 102 | 103 | if pr.spanTimer != nil { 104 | pr.spanTimer.Stop() 105 | } 106 | 107 | if pr.reportTimer != nil { 108 | pr.reportTimer.Stop() 109 | } 110 | } 111 | 112 | func (pr *ProfileReporter) reset() { 113 | pr.profiler.reset() 114 | pr.profileStartTimestamp = time.Now().Unix() 115 | pr.profileDuration = 0 116 | pr.spanCount = 0 117 | pr.spanTrigger = TriggerTimer 118 | pr.workloads = make(map[string]int64) 119 | } 120 | 121 | func (pr *ProfileReporter) startProfiling(apiCall bool, withTimeout bool, workload string) bool { 122 | if !pr.started.IsSet() { 123 | return false 124 | } 125 | 126 | pr.profileLock.Lock() 127 | defer pr.profileLock.Unlock() 128 | 129 | if pr.profileDuration > pr.config.maxProfileDuration*1e9 { 130 | pr.agent.log("%v: max profiling duration reached.", pr.config.logPrefix) 131 | return false 132 | } 133 | 134 | if apiCall && pr.spanCount >= pr.config.maxSpanCount { 135 | pr.agent.log("%v: max profiling span count reached.", pr.config.logPrefix) 136 | return false 137 | } 138 | 139 | if !pr.agent.profilerActive.SetIfUnset() { 140 | pr.agent.log("%v: another profiler currently active.", pr.config.logPrefix) 141 | return false 142 | } 143 | 144 | pr.agent.log("%v: starting profiler.", pr.config.logPrefix) 145 | 146 | err := pr.profiler.startProfiler() 147 | if err != nil { 148 | pr.agent.profilerActive.Unset() 149 | pr.agent.error(err) 150 | return false 151 | } 152 | 153 | if withTimeout { 154 | pr.spanTimeout = pr.agent.createTimer(time.Duration(pr.config.maxSpanDuration)*time.Second, 0, func() { 155 | pr.stopProfiling() 156 | }) 157 | } 158 | 159 | pr.spanCount++ 160 | pr.spanActive.Set() 161 | pr.spanStart = time.Now().UnixNano() 162 | 163 | if apiCall { 164 | pr.spanTrigger = TriggerAPI 165 | } 166 | 167 | if workload != "" { 168 | if _, ok := pr.workloads[workload]; ok { 169 | pr.workloads[workload]++ 170 | } else { 171 | pr.workloads[workload] = 1 172 | } 173 | } 174 | 175 | return true 176 | } 177 | 178 | func (pr *ProfileReporter) stopProfiling() { 179 | pr.profileLock.Lock() 180 | defer pr.profileLock.Unlock() 181 | 182 | if !pr.spanActive.UnsetIfSet() { 183 | return 184 | } 185 | 186 | if pr.spanTimeout != nil { 187 | pr.spanTimeout.Stop() 188 | } 189 | 190 | defer pr.agent.profilerActive.Unset() 191 | 192 | err := pr.profiler.stopProfiler() 193 | if err != nil { 194 | pr.agent.error(err) 195 | return 196 | } 197 | pr.agent.log("%v: profiler stopped.", pr.config.logPrefix) 198 | 199 | pr.profileDuration += time.Now().UnixNano() - pr.spanStart 200 | } 201 | 202 | func (pr *ProfileReporter) report(withInterval bool) { 203 | if !pr.started.IsSet() { 204 | return 205 | } 206 | 207 | pr.profileLock.Lock() 208 | defer pr.profileLock.Unlock() 209 | 210 | if withInterval { 211 | if pr.profileStartTimestamp > time.Now().Unix()-pr.config.reportInterval { 212 | return 213 | } else if pr.profileStartTimestamp < time.Now().Unix()-2*pr.config.reportInterval { 214 | pr.reset() 215 | return 216 | } 217 | } 218 | 219 | if !pr.config.reportOnly && pr.profileDuration == 0 { 220 | return 221 | } 222 | 223 | pr.agent.log("%v: reporting profile.", pr.config.logPrefix) 224 | 225 | profileData, err := pr.profiler.buildProfile(pr.profileDuration, pr.workloads) 226 | if err != nil { 227 | pr.agent.error(err) 228 | return 229 | } 230 | 231 | for _, d := range profileData { 232 | metric := newMetric(pr.agent, TypeProfile, d.category, d.name, d.unit) 233 | metric.createMeasurement(pr.spanTrigger, d.profile.measurement, d.unitInterval, d.profile) 234 | pr.agent.messageQueue.addMessage("metric", metric.toMap()) 235 | } 236 | 237 | pr.reset() 238 | } 239 | -------------------------------------------------------------------------------- /internal/profile_reporter_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | type TestProfiler struct { 9 | profile *BreakdownNode 10 | startCount int 11 | started bool 12 | closed bool 13 | } 14 | 15 | func (tp *TestProfiler) reset() { 16 | tp.profile = newBreakdownNode("root") 17 | } 18 | 19 | func (tp *TestProfiler) startProfiler() error { 20 | tp.started = true 21 | tp.startCount++ 22 | return nil 23 | } 24 | 25 | func (tp *TestProfiler) stopProfiler() error { 26 | tp.started = false 27 | return nil 28 | } 29 | 30 | func (tp *TestProfiler) buildProfile(duration int64, workloads map[string]int64) ([]*ProfileData, error) { 31 | tp.closed = true 32 | 33 | data := []*ProfileData{ 34 | &ProfileData{ 35 | category: "test-category", 36 | name: "Test", 37 | unit: "percent", 38 | unitInterval: 0, 39 | profile: tp.profile, 40 | }, 41 | } 42 | 43 | return data, nil 44 | } 45 | 46 | func TestMaxDuration(t *testing.T) { 47 | agent := NewAgent() 48 | agent.Debug = true 49 | 50 | prof := &TestProfiler{} 51 | 52 | conf := &ProfilerConfig{ 53 | logPrefix: "Test profiler", 54 | maxProfileDuration: 20, 55 | maxSpanDuration: 2, 56 | maxSpanCount: 30, 57 | spanInterval: 8, 58 | reportInterval: 120, 59 | } 60 | 61 | rep := newProfileReporter(agent, prof, conf) 62 | rep.start() 63 | 64 | rep.startProfiling(true, true, "") 65 | rep.stopProfiling() 66 | 67 | if prof.startCount != 1 { 68 | t.Errorf("Start count is not 1") 69 | } 70 | 71 | if prof.started { 72 | t.Error("Not stopped") 73 | } 74 | 75 | rep.profileDuration = 21 * 1e9 76 | 77 | rep.startProfiling(true, true, "") 78 | rep.stopProfiling() 79 | 80 | if prof.startCount > 1 { 81 | t.Error("Should not be started") 82 | } 83 | } 84 | 85 | func TestMaxSpanCount(t *testing.T) { 86 | agent := NewAgent() 87 | agent.Debug = true 88 | 89 | prof := &TestProfiler{} 90 | 91 | conf := &ProfilerConfig{ 92 | logPrefix: "Test profiler", 93 | maxProfileDuration: 20, 94 | maxSpanDuration: 2, 95 | maxSpanCount: 1, 96 | spanInterval: 8, 97 | reportInterval: 120, 98 | } 99 | 100 | rep := newProfileReporter(agent, prof, conf) 101 | rep.start() 102 | 103 | rep.startProfiling(true, true, "") 104 | rep.stopProfiling() 105 | 106 | if prof.startCount != 1 { 107 | t.Errorf("Start count is not 1") 108 | } 109 | 110 | rep.startProfiling(true, true, "") 111 | rep.stopProfiling() 112 | 113 | if prof.startCount > 1 { 114 | t.Error("Should not be started") 115 | } 116 | } 117 | 118 | func TestReportProfile(t *testing.T) { 119 | agent := NewAgent() 120 | agent.Debug = true 121 | 122 | prof := &TestProfiler{} 123 | 124 | conf := &ProfilerConfig{ 125 | logPrefix: "Test profiler", 126 | maxProfileDuration: 20, 127 | maxSpanDuration: 2, 128 | maxSpanCount: 30, 129 | spanInterval: 8, 130 | reportInterval: 120, 131 | } 132 | 133 | rep := newProfileReporter(agent, prof, conf) 134 | rep.start() 135 | 136 | rep.startProfiling(true, true, "") 137 | time.Sleep(10 * time.Millisecond) 138 | rep.stopProfiling() 139 | 140 | rep.report(false) 141 | 142 | if !prof.closed { 143 | t.Error("Should be closed") 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /internal/span_reporter.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type SpanReporter struct { 9 | ReportInterval int64 10 | 11 | agent *Agent 12 | started *Flag 13 | reportTimer *Timer 14 | 15 | spanNodes map[string]*BreakdownNode 16 | recordLock *sync.RWMutex 17 | } 18 | 19 | func newSpanReporter(agent *Agent) *SpanReporter { 20 | sr := &SpanReporter{ 21 | ReportInterval: 60, 22 | 23 | agent: agent, 24 | started: &Flag{}, 25 | reportTimer: nil, 26 | 27 | spanNodes: nil, 28 | recordLock: &sync.RWMutex{}, 29 | } 30 | 31 | return sr 32 | } 33 | 34 | func (sr *SpanReporter) reset() { 35 | sr.recordLock.Lock() 36 | defer sr.recordLock.Unlock() 37 | 38 | sr.spanNodes = make(map[string]*BreakdownNode) 39 | } 40 | 41 | func (sr *SpanReporter) start() { 42 | if !sr.agent.AutoProfiling { 43 | return 44 | } 45 | 46 | if !sr.started.SetIfUnset() { 47 | return 48 | } 49 | 50 | sr.reset() 51 | 52 | sr.reportTimer = sr.agent.createTimer(0, time.Duration(sr.ReportInterval)*time.Second, func() { 53 | sr.report() 54 | }) 55 | } 56 | 57 | func (sr *SpanReporter) stop() { 58 | if !sr.started.UnsetIfSet() { 59 | return 60 | } 61 | 62 | if sr.reportTimer != nil { 63 | sr.reportTimer.Stop() 64 | } 65 | } 66 | 67 | func (sr *SpanReporter) recordSpan(name string, duration float64) { 68 | if !sr.started.IsSet() { 69 | return 70 | } 71 | 72 | if name == "" { 73 | sr.agent.log("Empty span name") 74 | return 75 | } 76 | 77 | // Span exists for the current interval. 78 | sr.recordLock.RLock() 79 | node, nExists := sr.spanNodes[name] 80 | if nExists { 81 | node.updateP95(duration) 82 | } 83 | sr.recordLock.RUnlock() 84 | 85 | // Span does not exist yet for the current interval. 86 | if !nExists { 87 | sr.recordLock.Lock() 88 | node, nExists := sr.spanNodes[name] 89 | if !nExists { 90 | // If span was not created by other recordSpan call between locks, create it. 91 | node = newBreakdownNode(name) 92 | sr.spanNodes[name] = node 93 | } 94 | sr.recordLock.Unlock() 95 | 96 | sr.recordLock.RLock() 97 | node.updateP95(duration) 98 | sr.recordLock.RUnlock() 99 | } 100 | } 101 | 102 | func (sr *SpanReporter) report() { 103 | if !sr.started.IsSet() { 104 | return 105 | } 106 | 107 | sr.recordLock.Lock() 108 | outgoing := sr.spanNodes 109 | sr.spanNodes = make(map[string]*BreakdownNode) 110 | sr.recordLock.Unlock() 111 | 112 | for _, spanNode := range outgoing { 113 | spanRoot := newBreakdownNode("root") 114 | spanRoot.addChild(spanNode) 115 | spanRoot.evaluateP95() 116 | spanRoot.propagate() 117 | 118 | metric := newMetric(sr.agent, TypeState, CategorySpan, spanNode.name, UnitMillisecond) 119 | metric.createMeasurement(TriggerTimer, spanRoot.measurement, 0, nil) 120 | sr.agent.messageQueue.addMessage("metric", metric.toMap()) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /internal/span_reporter_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestRecordSpan(t *testing.T) { 9 | agent := NewAgent() 10 | agent.Debug = true 11 | 12 | agent.spanReporter.reset() 13 | agent.spanReporter.started.Set() 14 | 15 | for i := 0; i < 100; i++ { 16 | go func() { 17 | defer agent.spanReporter.recordSpan("span1", 10) 18 | 19 | time.Sleep(10 * time.Millisecond) 20 | }() 21 | } 22 | 23 | time.Sleep(150 * time.Millisecond) 24 | 25 | spanNodes := agent.spanReporter.spanNodes 26 | agent.spanReporter.report() 27 | 28 | span1Counter := spanNodes["span1"] 29 | if span1Counter.name != "span1" || span1Counter.measurement < 10 { 30 | t.Errorf("Measurement of span1 is too low: %v", span1Counter.measurement) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/symbolizer.go: -------------------------------------------------------------------------------- 1 | // +build go1.10 2 | 3 | package internal 4 | 5 | import ( 6 | "github.com/stackimpact/stackimpact-go/internal/pprof/profile" 7 | ) 8 | 9 | func symbolizeProfile(p *profile.Profile) error { 10 | return nil 11 | } 12 | -------------------------------------------------------------------------------- /internal/symbolizer_1_9.go: -------------------------------------------------------------------------------- 1 | // +build !go1.10 2 | 3 | package internal 4 | 5 | import ( 6 | "github.com/stackimpact/stackimpact-go/internal/pprof/profile" 7 | "runtime" 8 | ) 9 | 10 | func symbolizeProfile(p *profile.Profile) error { 11 | functions := make(map[string]*profile.Function) 12 | 13 | for _, l := range p.Location { 14 | if l.Address != 0 && len(l.Line) == 0 { 15 | if f := runtime.FuncForPC(uintptr(l.Address)); f != nil { 16 | name := f.Name() 17 | fileName, lineNumber := f.FileLine(uintptr(l.Address)) 18 | 19 | pf := functions[name] 20 | if pf == nil { 21 | pf = &profile.Function{ 22 | ID: uint64(len(p.Function) + 1), 23 | Name: name, 24 | SystemName: name, 25 | Filename: fileName, 26 | } 27 | 28 | functions[name] = pf 29 | p.Function = append(p.Function, pf) 30 | } 31 | 32 | line := profile.Line{ 33 | Function: pf, 34 | Line: int64(lineNumber), 35 | } 36 | 37 | l.Line = []profile.Line{line} 38 | if l.Mapping != nil { 39 | l.Mapping.HasFunctions = true 40 | l.Mapping.HasFilenames = true 41 | l.Mapping.HasLineNumbers = true 42 | } 43 | } 44 | } 45 | } 46 | 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /internal/system.go: -------------------------------------------------------------------------------- 1 | // +build !linux,!darwin,!windows 2 | 3 | package internal 4 | 5 | import ( 6 | "errors" 7 | ) 8 | 9 | func readCPUTime() (int64, error) { 10 | return 0, errors.New("readCPUTime is not supported.") 11 | } 12 | 13 | func readMaxRSS() (int64, error) { 14 | return 0, errors.New("readMaxRSS is not supported.") 15 | } 16 | 17 | func readCurrentRSS() (int64, error) { 18 | return 0, errors.New("readCurrentRSS is not supported.") 19 | } 20 | 21 | func readVMSize() (int64, error) { 22 | return 0, errors.New("readVMSize is not supported.") 23 | } 24 | -------------------------------------------------------------------------------- /internal/system_appengine.go: -------------------------------------------------------------------------------- 1 | // +build appengine 2 | 3 | package internal 4 | 5 | import ( 6 | "errors" 7 | ) 8 | 9 | func readCPUTime() (int64, error) { 10 | return 0, errors.New("readCPUTime is not supported on App Engine") 11 | } 12 | 13 | func readMaxRSS() (int64, error) { 14 | return 0, errors.New("readMaxRSS is not supported on App Engine") 15 | } 16 | 17 | func readCurrentRSS() (int64, error) { 18 | return 0, errors.New("readCurrentRSS is not supported App Engine") 19 | } 20 | 21 | func readVMSize() (int64, error) { 22 | return 0, errors.New("readVMSize is not supported on App Engine") 23 | } 24 | -------------------------------------------------------------------------------- /internal/system_darwin.go: -------------------------------------------------------------------------------- 1 | // +build darwin 2 | 3 | package internal 4 | 5 | import ( 6 | "errors" 7 | "runtime" 8 | "syscall" 9 | ) 10 | 11 | func readCPUTime() (int64, error) { 12 | rusage := new(syscall.Rusage) 13 | if err := syscall.Getrusage(0, rusage); err != nil { 14 | return 0, err 15 | } 16 | 17 | var cpuTimeNanos int64 18 | cpuTimeNanos = 19 | int64(rusage.Utime.Sec*1e9) + 20 | int64(rusage.Utime.Usec) + 21 | int64(rusage.Stime.Sec*1e9) + 22 | int64(rusage.Stime.Usec) 23 | 24 | return cpuTimeNanos, nil 25 | } 26 | 27 | func readMaxRSS() (int64, error) { 28 | rusage := new(syscall.Rusage) 29 | if err := syscall.Getrusage(0, rusage); err != nil { 30 | return 0, err 31 | } 32 | 33 | var maxRSS int64 34 | maxRSS = int64(rusage.Maxrss) 35 | 36 | if runtime.GOOS == "darwin" { 37 | maxRSS = maxRSS / 1000 38 | } 39 | 40 | return maxRSS, nil 41 | } 42 | 43 | func readCurrentRSS() (int64, error) { 44 | return 0, errors.New("readCurrentRSS is not supported on OS X") 45 | } 46 | 47 | func readVMSize() (int64, error) { 48 | return 0, errors.New("readVMSize is not supported on OS X") 49 | } 50 | -------------------------------------------------------------------------------- /internal/system_linux.go: -------------------------------------------------------------------------------- 1 | // +build linux,!appengine 2 | 3 | package internal 4 | 5 | import ( 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "os" 10 | "regexp" 11 | "runtime" 12 | "strconv" 13 | "syscall" 14 | ) 15 | 16 | var VmSizeRe = regexp.MustCompile(`VmSize:\s+(\d+)\s+kB`) 17 | var VmRSSRe = regexp.MustCompile(`VmRSS:\s+(\d+)\s+kB`) 18 | 19 | func readCPUTime() (int64, error) { 20 | rusage := new(syscall.Rusage) 21 | if err := syscall.Getrusage(0, rusage); err != nil { 22 | return 0, err 23 | } 24 | 25 | var cpuTimeNanos int64 26 | cpuTimeNanos = 27 | int64(rusage.Utime.Sec*1e9) + 28 | int64(rusage.Utime.Usec) + 29 | int64(rusage.Stime.Sec*1e9) + 30 | int64(rusage.Stime.Usec) 31 | 32 | return cpuTimeNanos, nil 33 | } 34 | 35 | func readMaxRSS() (int64, error) { 36 | rusage := new(syscall.Rusage) 37 | if err := syscall.Getrusage(0, rusage); err != nil { 38 | return 0, err 39 | } 40 | 41 | var maxRSS int64 42 | maxRSS = int64(rusage.Maxrss) 43 | 44 | if runtime.GOOS == "darwin" { 45 | maxRSS = maxRSS / 1000 46 | } 47 | 48 | return maxRSS, nil 49 | } 50 | 51 | func readCurrentRSS() (int64, error) { 52 | pid := os.Getpid() 53 | 54 | output, err := ioutil.ReadFile(fmt.Sprintf("/proc/%v/status", pid)) 55 | if err != nil { 56 | return 0, err 57 | } 58 | 59 | results := VmRSSRe.FindStringSubmatch(string(output)) 60 | 61 | if len(results) >= 2 { 62 | if v, e := strconv.ParseInt(results[1], 10, 64); e == nil { 63 | return v, nil 64 | } else { 65 | return 0, e 66 | } 67 | } else { 68 | return 0, errors.New("Unable to read current RSS") 69 | } 70 | } 71 | 72 | func readVMSize() (int64, error) { 73 | pid := os.Getpid() 74 | 75 | output, err := ioutil.ReadFile(fmt.Sprintf("/proc/%v/status", pid)) 76 | if err != nil { 77 | return 0, err 78 | } 79 | 80 | results := VmSizeRe.FindStringSubmatch(string(output)) 81 | 82 | if len(results) >= 2 { 83 | if v, e := strconv.ParseInt(results[1], 10, 64); e == nil { 84 | return v, nil 85 | } else { 86 | return 0, e 87 | } 88 | } else { 89 | return 0, errors.New("Unable to read VM size") 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /internal/system_windows.go: -------------------------------------------------------------------------------- 1 | // +build windows 2 | 3 | package internal 4 | 5 | import ( 6 | "errors" 7 | "syscall" 8 | ) 9 | 10 | var currentProcess syscall.Handle = 0 11 | 12 | func readCPUTime() (int64, error) { 13 | if currentProcess == 0 { 14 | h, err := syscall.GetCurrentProcess() 15 | if err != nil { 16 | return 0, err 17 | } 18 | currentProcess = h 19 | } 20 | 21 | var rusage syscall.Rusage 22 | err := syscall.GetProcessTimes( 23 | currentProcess, 24 | &rusage.CreationTime, 25 | &rusage.ExitTime, 26 | &rusage.KernelTime, 27 | &rusage.UserTime) 28 | if err != nil { 29 | return 0, err 30 | } 31 | 32 | return rusage.KernelTime.Nanoseconds() + rusage.UserTime.Nanoseconds(), nil 33 | } 34 | 35 | func readMaxRSS() (int64, error) { 36 | return 0, errors.New("readMaxRSS is not supported on Windows") 37 | } 38 | 39 | func readCurrentRSS() (int64, error) { 40 | return 0, errors.New("readCurrentRSS is not supported on Windows") 41 | } 42 | 43 | func readVMSize() (int64, error) { 44 | return 0, errors.New("readVMSize is not supported on Windows") 45 | } 46 | -------------------------------------------------------------------------------- /pprof_labels.go: -------------------------------------------------------------------------------- 1 | // +build go1.9 2 | 3 | package stackimpact 4 | 5 | import ( 6 | "context" 7 | "net/http" 8 | "runtime/pprof" 9 | ) 10 | 11 | func WithPprofLabel(key string, val string, req *http.Request, fn func()) { 12 | labelSet := pprof.Labels(key, val) 13 | pprof.Do(req.Context(), labelSet, func(ctx context.Context) { 14 | fn() 15 | }) 16 | } 17 | -------------------------------------------------------------------------------- /pprof_labels_1_8.go: -------------------------------------------------------------------------------- 1 | // +build !go1.9 2 | 3 | package stackimpact 4 | 5 | import ( 6 | "net/http" 7 | ) 8 | 9 | func WithPprofLabel(key string, val string, req *http.Request, fn func()) { 10 | fn() 11 | } 12 | -------------------------------------------------------------------------------- /segment.go: -------------------------------------------------------------------------------- 1 | package stackimpact 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Segment struct { 8 | agent *Agent 9 | Name string 10 | startTime time.Time 11 | Duration float64 12 | } 13 | 14 | func newSegment(agent *Agent, name string) *Segment { 15 | s := &Segment{ 16 | agent: agent, 17 | Name: name, 18 | Duration: 0, 19 | } 20 | 21 | return s 22 | } 23 | 24 | func (s *Segment) start() { 25 | s.startTime = time.Now() 26 | } 27 | 28 | // Stops the measurement of a code segment execution time. 29 | func (s *Segment) Stop() { 30 | s.Duration = float64(time.Since(s.startTime).Nanoseconds()) / 1e6 31 | 32 | s.agent.internalAgent.RecordSpan(s.Name, s.Duration) 33 | } 34 | -------------------------------------------------------------------------------- /span.go: -------------------------------------------------------------------------------- 1 | package stackimpact 2 | 3 | import ( 4 | "sync/atomic" 5 | "time" 6 | ) 7 | 8 | type Span struct { 9 | agent *Agent 10 | name string 11 | timestamp time.Time 12 | started bool 13 | active bool 14 | } 15 | 16 | func newSpan(agent *Agent, name string) *Span { 17 | s := &Span{ 18 | agent: agent, 19 | name: name, 20 | started: false, 21 | active: false, 22 | } 23 | 24 | return s 25 | } 26 | 27 | func (s *Span) start() { 28 | s.started = atomic.CompareAndSwapInt32(&s.agent.spanStarted, 0, 1) 29 | if s.started { 30 | s.active = s.agent.internalAgent.StartProfiling(s.name) 31 | } 32 | 33 | s.timestamp = time.Now() 34 | } 35 | 36 | // Stops profiling. 37 | func (s *Span) Stop() { 38 | duration := float64(time.Since(s.timestamp).Nanoseconds()) / 1e6 39 | s.agent.internalAgent.RecordSpan(s.name, duration) 40 | 41 | if s.started { 42 | if s.active { 43 | s.agent.internalAgent.StopProfiling() 44 | } 45 | 46 | if !s.agent.internalAgent.AutoProfiling { 47 | s.agent.internalAgent.Report() 48 | } 49 | 50 | atomic.StoreInt32(&s.agent.spanStarted, 0) 51 | } 52 | } 53 | --------------------------------------------------------------------------------