├── .gitignore ├── .rspec ├── .travis.yml ├── .yardopts ├── CHANGELOG.markdown ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── README.markdown ├── Rakefile ├── TODO ├── allocation_stats.gemspec ├── examples ├── my_class.rb ├── trace_my_class_group_by.rb ├── trace_my_class_raw.rb ├── trace_object_allocations.rb ├── trace_psych_group_by.rb ├── trace_psych_keys.rb └── trace_specs │ ├── strings.rb │ └── strings_spec.rb ├── lib ├── allocation_stats.rb └── allocation_stats │ ├── allocation.rb │ ├── allocations_proxy.rb │ ├── core_ext │ └── basic_object.rb │ └── trace_rspec.rb └── spec ├── allocation_stats └── allocations_proxy_spec.rb ├── allocation_stats_spec.rb ├── spec_helper.rb └── trace_rspec_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | doc 3 | .yardoc 4 | demos 5 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.1.0 4 | -------------------------------------------------------------------------------- /.yardopts: -------------------------------------------------------------------------------- 1 | -m markdown - README.markdown LICENSE 2 | -------------------------------------------------------------------------------- /CHANGELOG.markdown: -------------------------------------------------------------------------------- 1 | v0.1.5 2 | 3 | * Added: README is much more complete now. 4 | * Added: `AllocationStats.trace_rspec` is documented better. 5 | * Added: `AllocationStats.reace_rspec` now always burns once to prevent 6 | `#autoload` from allocating where unexpected. 7 | * Fixed: typo in README; thanks @tjchambers 8 | 9 | v0.1.4 10 | 11 | * Added: Build status now tracked with Travis 12 | * Fixed: Working... better? with new frozen String keys 13 | * Fixed: alias order changed so that PWD is searched after GEMDIR and 14 | RUBYLIBDIR, in case of vendored bundler directory. 15 | * Added: `at_least` method for the AllocationsProxy, tested and documented 16 | * Added: `AllocationStats.trace_rspec` to trace an RSpec run, tested and 17 | moderately documented 18 | 19 | v0.1.3 20 | 21 | * Fixed: BasicObjects can be tracked; fixes #1 22 | * Added: much more documentation: up to 83% 23 | * Fixed: Working with new frozen String keys 24 | 25 | v0.1.2 26 | 27 | * Added: `homepage` in the gemspec 28 | * Added: more documentation: up to 71% now 29 | 30 | v0.1.1 31 | 32 | * Fixed: `required_ruby_version` in the gemspec 33 | 34 | v0.1.0 35 | 36 | * A lot of stuff 37 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All Rights Reserved. 2 | # Licensed under the Apache License, Version 2.0, found in the LICENSE file. 3 | 4 | source "https://rubygems.org" 5 | 6 | group :development do 7 | gem "yard" 8 | end 9 | 10 | group :test do 11 | gem "rake" 12 | gem "rspec", "~> 3.3" 13 | gem "simplecov", require: false 14 | gem "pry" 15 | 16 | gem "yajl-ruby", "= 1.1.0" 17 | end 18 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | coderay (1.0.9) 5 | diff-lcs (1.2.5) 6 | docile (1.1.1) 7 | method_source (0.8.2) 8 | multi_json (1.8.2) 9 | pry (0.9.12.2) 10 | coderay (~> 1.0.5) 11 | method_source (~> 0.8) 12 | slop (~> 3.4) 13 | rake (10.1.0) 14 | rspec (3.3.0) 15 | rspec-core (~> 3.3.0) 16 | rspec-expectations (~> 3.3.0) 17 | rspec-mocks (~> 3.3.0) 18 | rspec-core (3.3.2) 19 | rspec-support (~> 3.3.0) 20 | rspec-expectations (3.3.1) 21 | diff-lcs (>= 1.2.0, < 2.0) 22 | rspec-support (~> 3.3.0) 23 | rspec-mocks (3.3.2) 24 | diff-lcs (>= 1.2.0, < 2.0) 25 | rspec-support (~> 3.3.0) 26 | rspec-support (3.3.0) 27 | simplecov (0.8.2) 28 | docile (~> 1.1.0) 29 | multi_json 30 | simplecov-html (~> 0.8.0) 31 | simplecov-html (0.8.0) 32 | slop (3.4.6) 33 | yajl-ruby (1.1.0) 34 | yard (0.8.7) 35 | 36 | PLATFORMS 37 | ruby 38 | 39 | DEPENDENCIES 40 | pry 41 | rake 42 | rspec (~> 3.3) 43 | simplecov 44 | yajl-ruby (= 1.1.0) 45 | yard 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | # @markup rdoc 2 | 3 | Apache License 4 | Version 2.0, January 2004 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, and 12 | distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 15 | owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all other entities 18 | that control, are controlled by, or are under common control with that entity. 19 | For the purposes of this definition, "control" means (i) the power, direct or 20 | indirect, to cause the direction or management of such entity, whether by 21 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity exercising 25 | permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, including 28 | but not limited to software source code, documentation source, and configuration 29 | files. 30 | 31 | "Object" form shall mean any form resulting from mechanical transformation or 32 | translation of a Source form, including but not limited to compiled object code, 33 | generated documentation, and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or Object form, made 36 | available under the License, as indicated by a copyright notice that is included 37 | in or attached to the work (an example is provided in the Appendix below). 38 | 39 | "Derivative Works" shall mean any work, whether in Source or Object form, that 40 | is based on (or derived from) the Work and for which the editorial revisions, 41 | annotations, elaborations, or other modifications represent, as a whole, an 42 | original work of authorship. For the purposes of this License, Derivative Works 43 | shall not include works that remain separable from, or merely link (or bind by 44 | name) to the interfaces of, the Work and Derivative Works thereof. 45 | 46 | "Contribution" shall mean any work of authorship, including the original version 47 | of the Work and any modifications or additions to that Work or Derivative Works 48 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 49 | by the copyright owner or by an individual or Legal Entity authorized to submit 50 | on behalf of the copyright owner. For the purposes of this definition, 51 | "submitted" means any form of electronic, verbal, or written communication sent 52 | to the Licensor or its representatives, including but not limited to 53 | communication on electronic mailing lists, source code control systems, and 54 | issue tracking systems that are managed by, or on behalf of, the Licensor for 55 | the purpose of discussing and improving the Work, but excluding communication 56 | that is conspicuously marked or otherwise designated in writing by the copyright 57 | owner as "Not a Contribution." 58 | 59 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 60 | of whom a Contribution has been received by Licensor and subsequently 61 | incorporated within the Work. 62 | 63 | 2. Grant of Copyright License. 64 | 65 | Subject to the terms and conditions of this License, each Contributor hereby 66 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 67 | irrevocable copyright license to reproduce, prepare Derivative Works of, 68 | publicly display, publicly perform, sublicense, and distribute the Work and such 69 | Derivative Works in Source or Object form. 70 | 71 | 3. Grant of Patent License. 72 | 73 | Subject to the terms and conditions of this License, each Contributor hereby 74 | grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, 75 | irrevocable (except as stated in this section) patent license to make, have 76 | made, use, offer to sell, sell, import, and otherwise transfer the Work, where 77 | such license applies only to those patent claims licensable by such Contributor 78 | that are necessarily infringed by their Contribution(s) alone or by combination 79 | of their Contribution(s) with the Work to which such Contribution(s) was 80 | submitted. If You institute patent litigation against any entity (including a 81 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 82 | Contribution incorporated within the Work constitutes direct or contributory 83 | patent infringement, then any patent licenses granted to You under this License 84 | for that Work shall terminate as of the date such litigation is filed. 85 | 86 | 4. Redistribution. 87 | 88 | You may reproduce and distribute copies of the Work or Derivative Works thereof 89 | in any medium, with or without modifications, and in Source or Object form, 90 | provided that You meet the following conditions: 91 | 92 | You must give any other recipients of the Work or Derivative Works a copy of 93 | this License; and 94 | You must cause any modified files to carry prominent notices stating that You 95 | changed the files; and 96 | You must retain, in the Source form of any Derivative Works that You distribute, 97 | all copyright, patent, trademark, and attribution notices from the Source form 98 | of the Work, excluding those notices that do not pertain to any part of the 99 | Derivative Works; and 100 | If the Work includes a "NOTICE" text file as part of its distribution, then any 101 | Derivative Works that You distribute must include a readable copy of the 102 | attribution notices contained within such NOTICE file, excluding those notices 103 | that do not pertain to any part of the Derivative Works, in at least one of the 104 | following places: within a NOTICE text file distributed as part of the 105 | Derivative Works; within the Source form or documentation, if provided along 106 | with the Derivative Works; or, within a display generated by the Derivative 107 | Works, if and wherever such third-party notices normally appear. The contents of 108 | the NOTICE file are for informational purposes only and do not modify the 109 | License. You may add Your own attribution notices within Derivative Works that 110 | You distribute, alongside or as an addendum to the NOTICE text from the Work, 111 | provided that such additional attribution notices cannot be construed as 112 | modifying the License. 113 | You may add Your own copyright statement to Your modifications and may provide 114 | additional or different license terms and conditions for use, reproduction, or 115 | distribution of Your modifications, or for any such Derivative Works as a whole, 116 | provided Your use, reproduction, and distribution of the Work otherwise complies 117 | with the conditions stated in this License. 118 | 119 | 5. Submission of Contributions. 120 | 121 | Unless You explicitly state otherwise, any Contribution intentionally submitted 122 | for inclusion in the Work by You to the Licensor shall be under the terms and 123 | conditions of this License, without any additional terms or conditions. 124 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 125 | any separate license agreement you may have executed with Licensor regarding 126 | such Contributions. 127 | 128 | 6. Trademarks. 129 | 130 | This License does not grant permission to use the trade names, trademarks, 131 | service marks, or product names of the Licensor, except as required for 132 | reasonable and customary use in describing the origin of the Work and 133 | reproducing the content of the NOTICE file. 134 | 135 | 7. Disclaimer of Warranty. 136 | 137 | Unless required by applicable law or agreed to in writing, Licensor provides the 138 | Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, 139 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, 140 | including, without limitation, any warranties or conditions of TITLE, 141 | NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are 142 | solely responsible for determining the appropriateness of using or 143 | redistributing the Work and assume any risks associated with Your exercise of 144 | permissions under this License. 145 | 146 | 8. Limitation of Liability. 147 | 148 | In no event and under no legal theory, whether in tort (including negligence), 149 | contract, or otherwise, unless required by applicable law (such as deliberate 150 | and grossly negligent acts) or agreed to in writing, shall any Contributor be 151 | liable to You for damages, including any direct, indirect, special, incidental, 152 | or consequential damages of any character arising as a result of this License or 153 | out of the use or inability to use the Work (including but not limited to 154 | damages for loss of goodwill, work stoppage, computer failure or malfunction, or 155 | any and all other commercial damages or losses), even if such Contributor has 156 | been advised of the possibility of such damages. 157 | 158 | 9. Accepting Warranty or Additional Liability. 159 | 160 | While redistributing the Work or Derivative Works thereof, You may choose to 161 | offer, and charge a fee for, acceptance of support, warranty, indemnity, or 162 | other liability obligations and/or rights consistent with this License. However, 163 | in accepting such obligations, You may act only on Your own behalf and on Your 164 | sole responsibility, not on behalf of any other Contributor, and only if You 165 | agree to indemnify, defend, and hold each Contributor harmless for any liability 166 | incurred by, or claims asserted against, such Contributor by reason of your 167 | accepting any such warranty or additional liability. 168 | 169 | END OF TERMS AND CONDITIONS 170 | 171 | APPENDIX: How to apply the Apache License to your work 172 | 173 | To apply the Apache License to your work, attach the following boilerplate 174 | notice, with the fields enclosed by brackets "{}" replaced with your own 175 | identifying information. (Don't include the brackets!) The text should be 176 | enclosed in the appropriate comment syntax for the file format. We also 177 | recommend that a file or class name and description of purpose be included on 178 | the same "printed page" as the copyright notice for easier identification within 179 | third-party archives. 180 | 181 | Copyright {yyyy} {name of copyright owner} 182 | 183 | Licensed under the Apache License, Version 2.0 (the "License"); 184 | you may not use this file except in compliance with the License. 185 | You may obtain a copy of the License at 186 | 187 | http://www.apache.org/licenses/LICENSE-2.0 188 | 189 | Unless required by applicable law or agreed to in writing, software 190 | distributed under the License is distributed on an "AS IS" BASIS, 191 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 192 | See the License for the specific language governing permissions and 193 | limitations under the License. 194 | 195 | 196 | 197 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | AllocationStats [![Build Status](https://travis-ci.org/srawlins/allocation_stats.png?branch=master)](https://travis-ci.org/srawlins/allocation_stats) 2 | =============== 3 | 4 | * [Introduction](#introduction) 5 | * [Install](#install) 6 | * [Tabular output examples](#tabular-output-examples) 7 | * [More on `trace_object_allocations()`](#more-on-trace_object_allocations) 8 | * [The API](#the-api) 9 | * [`AllocationStats` API](#allocationstats-api) 10 | * [Burn one](#burn-one) 11 | * [`AllocationsProxy` API](#allocationsproxy-api) 12 | * [What are faux attributes?](#what-are-faux-attributes) 13 | * [What is `class_plus`?](#what-is-class_plus) 14 | * [Gotchas](#gotchas) 15 | * [Allocations in C](#allocations-in-c) 16 | * [`autoload`](#autoload) 17 | * [Examples](#examples) 18 | * [Example from the specs](#example-from-the-specs) 19 | * [A little slower](#a-little-slower) 20 | * [Psych example](#psych-example) 21 | * [References](#references) 22 | 23 | Introduction 24 | ------------ 25 | 26 | AllocationStats is a rubygem that makes use of Ruby 2.1's new abilities to trace 27 | Ruby object allocations (MRI only). Ruby 2.1's new 28 | `ObjectSpace.trace_object_allocations` methods give only raw information, and 29 | are not immediately useful for anything beyond micro profiling. The data must be 30 | aggregated! 31 | 32 | AllocationStats collects all of the allocation information generated by 33 | `ObjectSpace.trace_object_allocations`, then provides mechanisms for filtering, 34 | grouping, and sorting the allocation information. 35 | 36 | ### Install 37 | 38 | To install AllocationStats, add the following line to a project's Gemfile, perhaps to the development group: 39 | 40 | ```ruby 41 | gem 'allocation_stats' 42 | ``` 43 | 44 | or run the following command: 45 | 46 | ```shell 47 | gem install allocation_stats 48 | ``` 49 | 50 | ### Tabular output examples 51 | 52 | It is very easy to get some simple statistics out of AllocationStats. 53 | Wrap some code with `AllocationStats.trace` and print out a listing of all of the 54 | new object allocation information. 55 | 56 | As an example, lets look at `examples/my_class.rb`: 57 | 58 | ```ruby 59 | class MyClass 60 | def my_method 61 | @hash = {1 => "foo", 2 => "bar"} 62 | end 63 | end 64 | ``` 65 | 66 | And use that class in a bit of ad-hoc Ruby: 67 | 68 | ``` 69 | $ ruby -r ./lib/allocation_stats -r ./allocation 70 | stats = AllocationStats.trace { MyClass.new.my_method } 71 | puts stats.allocations(alias_paths: true).to_text 72 | ^D 73 | sourcefile sourceline class_path method_id memsize class 74 | ------------------- ---------- ---------- --------- ------- ------- 75 | /allocation.rb 4 MyClass my_method 0 String 76 | /allocation.rb 3 MyClass my_method 192 Hash 77 | /allocation.rb 3 MyClass my_method 0 String 78 | /allocation.rb 3 MyClass my_method 0 String 79 | /allocation.rb 3 MyClass my_method 0 String 80 | - 1 Class new 0 MyClass 81 | ``` 82 | 83 | (I've used `alias_paths: true` above for readability. This way, the `sourcefile` 84 | column is not crazy long, using the full file path on my filesystem.) 85 | 86 | (This full example is found in `examples/trace_my_class_raw.rb`) 87 | 88 | We can also group allocations by one or more attributes, and get an aggregate 89 | count. Below, we group allocations by source file, source line, and class: 90 | 91 | ``` 92 | $ ruby -r ./lib/allocation_stats -r ./allocation 93 | stats = AllocationStats.trace { MyClass.new.my_method } 94 | puts stats.allocations(alias_paths: true).group_by(:sourcefile, :sourceline, :class).to_text 95 | ^D 96 | sourcefile sourceline class count 97 | ------------------- ---------- ------- ----- 98 | /allocation.rb 4 String 1 99 | /allocation.rb 3 Hash 1 100 | /allocation.rb 3 String 3 101 | - 1 MyClass 1 102 | ``` 103 | 104 | (This full example is found in `examples/trace_my_class_group_by.rb`) 105 | 106 | 107 | ### More on `trace_object_allocations()` 108 | 109 | To start at the beginning: Ruby 2.1 will be released with a new feature that 110 | enables one to trace object allocations. Through 111 | `ObjectSpace.trace_object_allocations`, one can trace the following properties 112 | for any new object allocation: 113 | 114 | * source file 115 | * source line 116 | * class path 117 | * method ID 118 | * generation 119 | 120 | Let's see this in action: 121 | 122 | ``` 123 | $ cat examples/trace_object_allocations.rb 124 | require 'objspace' 125 | 126 | ObjectSpace.trace_object_allocations do 127 | a = [2,3,5,7,11,13,17,19,23,29,31] 128 | puts ObjectSpace.allocation_sourcefile(a) 129 | puts ObjectSpace.allocation_sourceline(a) 130 | end 131 | $ ruby ./examples/trace_object_allocations.rb 132 | ./examples/trace_object_allocations.rb 133 | 4 134 | ``` 135 | 136 | To see some detailed examples, review the [Examples](#examples) section. 137 | 138 | The API 139 | ------- 140 | 141 | ### `AllocationStats` API 142 | 143 | The tracing of allocations can be kicked off in a few different ways, to provide flexibility: 144 | 145 | #### Block-style 146 | 147 | Just pass a block to `AllocationStats.trace`: 148 | 149 | ```ruby 150 | stats = AllocationStats.trace do 151 | # code to trace 152 | end 153 | ``` 154 | 155 | Or initialize an `AllocationStats`, then call `#trace` with a block: 156 | 157 | ```ruby 158 | stats = AllocationStats.new 159 | stats.trace do 160 | # code to trace 161 | end 162 | ``` 163 | 164 | #### Inline 165 | 166 | Wrap lines of code to trace with calls to `#trace` (or `#start`) and `#stop`: 167 | 168 | ```ruby 169 | stats = AllocationStats.new 170 | stats.trace # also stats.start 171 | # code to trace 172 | stats.stop 173 | ``` 174 | 175 | #### Burn One 176 | 177 | If you find a lot of allocations in `kernel_require.rb` or a lot of allocations 178 | of `RubyVM::InstructuinSequences`, you can "burn" one or more iterations. 179 | Instantiate your `AllocationStats` instance with the `burn` keyword, and trace 180 | your code block-style. For example: `AllocationStats.new(burn: 3).trace{ ... }` 181 | will first call the block 3 times, without tracing allocations, before calling 182 | the block a 4th time, tracing allocations. 183 | 184 | ### `AllocationsProxy` API 185 | 186 | Here are the methods available on the `AllocationStats::AllocationsProxy` 187 | object that is returned by `AllocationStats#allocations`: 188 | 189 | * `#group_by` 190 | * `#from` takes one String argument, which will matched against the 191 | allocation filename. 192 | * `#not_from` is the opposite of `#from`. 193 | * `#from_pwd` will filter the allocations down to those originating from `pwd` 194 | (e.g. allocations originating from "my project") 195 | * `#where` accepts a hash of faux attribute keys. For example, 196 | 197 | ```ruby 198 | allocations.where(class: String) 199 | ``` 200 | 201 | It does not yet accept lambdas as values, which _would_ enable 202 | ActiveRecord-4-like calls, like 203 | 204 | ```ruby 205 | allocations.where(class: Array, size: ->(size) { size > 10 } 206 | ``` 207 | * `#at_least(n)` selects allocation groups with at least `n` allocations per group. 208 | * `#bytes`, which has an inconsistent definition, I think... TODO 209 | 210 | #### What are faux attributes? 211 | 212 | Valid values for `#group_by` and `#where` include: 213 | * instance variables on each `Allocation`. These include `:sourcefile`, 214 | `:sourceline`, etc. 215 | * methods available on the objects that were allocated. These include things 216 | like `:class`, or even `:size` if you know you only have objects that respond 217 | to `:size`. 218 | * Allocation helper methods that return something special about the allocated 219 | object. Right now this just includes `:class_plus`. 220 | 221 | I'm calling these things that you can group by or filter by, "faux attributes." 222 | 223 | #### What is `class_plus`? 224 | 225 | ### Tracing RSpec 226 | 227 | You can trace an RSpec test suite by including this at the top of your 228 | `spec_helper.rb`: 229 | 230 | ```ruby 231 | require 'allocation_stats' 232 | AllocationStats.trace_rspec 233 | ``` 234 | 235 | This will put hooks around your RSpec tests, tracing each RSpec test 236 | individually. When RSpec exits, the top sourcefile/sourceline/class 237 | combinations will be printed out. 238 | 239 | Tracing RSpec gives maintainers of existing libraries a great place to start to 240 | search for inefficiencies in their project. You can trace an RSpec run of your 241 | whole test suite, or a subset, and if any one spec allocates hundreds of 242 | objects from the same line, then it might be something worth investigating. 243 | 244 | Here's the example from [`examples/trace_specs/`](examples/trace_specs): 245 | 246 | ``` 247 | rspec strings_spec.rb 248 | .. 249 | 250 | Top 2 slowest examples (0.08615 seconds, 100.0% of total time): 251 | Array of Strings allocates Strings and Arrays 252 | 0.0451 seconds ./strings_spec.rb:8 253 | Array of Strings allocates more Strings 254 | 0.04105 seconds ./strings_spec.rb:12 255 | 256 | Finished in 0.08669 seconds 257 | 2 examples, 0 failures 258 | 259 | Randomized with seed 56224 260 | 261 | Top 7 allocation sites: 262 | 5 allocations of String at /strings.rb:2 263 | during ./strings_spec.rb:8 264 | 2 allocations of Array at /strings_spec.rb:9 265 | during ./strings_spec.rb:8 266 | 2 allocations of Array at /strings_spec.rb:13 267 | during ./strings_spec.rb:12 268 | 1 allocations of Array at /strings.rb:2 269 | during ./strings_spec.rb:8 270 | 1 allocations of String at /strings.rb:6 271 | during ./strings_spec.rb:8 272 | 1 allocations of String at /strings.rb:14 273 | during ./strings_spec.rb:12 274 | 1 allocations of String at /strings.rb:10 275 | during ./strings_spec.rb:12 276 | ``` 277 | 278 | We are informed that during the spec at `strings_spec.rb:8`, there were 5x 279 | Strings allocated at `strings.rb:2`. 280 | 281 | `#trace_rspec` always burns each test run once, mostly to prevent `#autoload` 282 | from appearing in the allocations. 283 | 284 | Gotchas 285 | ------- 286 | 287 | ### Allocations in C 288 | 289 | If allocations occur in C code (such as a C extension), their allocation site 290 | will be somewhat obfuscated. The allocation will still be logged, but the 291 | sourcefile and sourceline will register as the deepest Ruby file that _called_ 292 | a C function that maybe called other C functions that at some point allocated 293 | an object. This brings us to the next gotcha: 294 | 295 | ### `autoload` 296 | 297 | `Kernel#autoload` is tricky! Autoloading can hide allocations (during the 298 | ensuing `require`) in a simple constant reference. For example, in the mail 299 | gem, tracing object allocations during `rspec spec/mail/body_spec.rb:339` makes 300 | it look like the following line allocates 219 Strings: 301 | 302 | ```ruby 303 | # lib/mail/configuration.rb 304 | 305 | 28 def lookup_delivery_method(method) 306 | 29 case method.is_a?(String) ? method.to_sym : method 307 | 30 when nil 308 | 31 Mail::SMTP # <-- 219 Strings alloctated here? 309 | ``` 310 | 311 | To fight this, either [don't use 312 | autoload](https://www.ruby-forum.com/topic/3036681), which can be eased into 313 | with a fancy mechanism like mail's 314 | [#eager_autoload](https://github.com/mikel/mail/blob/master/lib/mail.rb), or 315 | rely on the `burn` mechanism. `AllocationStats.trace_rspec` always burns each 316 | test run once. 317 | 318 | Examples 319 | -------- 320 | 321 | ### Example from the specs 322 | 323 | ```ruby 324 | existing_array = [1,2,3,4,5] 325 | 326 | stats = AllocationStats.trace do 327 | new_string = "stringy string" 328 | another_string = "another string" 329 | an_array = [1,1,2,3,5,8,13,21,34,55] 330 | a_foreign_string = allocate_a_string_from_spec_helper 331 | end 332 | 333 | results = stats.allocations.group_by(:@sourcefile, :class).to_a 334 | ``` 335 | 336 | We've grouped all of the traced allocations by the **source file** that the 337 | allocation occurred in, and the **class of the object** that was allocated. The 338 | list of allocations can be "transformed" in a number of ways (including 339 | the above `#group_by`), so the transformations must be ultimately resolved by calling 340 | `#to_a` (similar to ActiveRecord relations). The result is the following Hash of (sourcefile, class) tuple keys and 341 | AllocationStats::Allocation values: 342 | 343 | ```ruby 344 | { 345 | [".../spec/spec_helper.rb", String] 346 | => 347 | [# 354 | ], 355 | 356 | [".../spec/allocation_stats_spec.rb", Array] 357 | => 358 | [# 365 | ], 366 | 367 | [".../spec/allocation_stats_spec.rb", String] 368 | => 369 | [ #, 376 | # 383 | ] 384 | } 385 | ``` 386 | 387 | (I've manually inserted the ellipses.) 388 | 389 | You can see that there are three different groups: 390 | 391 | * `[spec_helper.rb, String]` 392 | * `[allocation_stats_spec.rb, Array]` 393 | * `[object_space_stats.rb, String]` 394 | 395 | Only one allocation belongs to each of the first two groups, and two allocations 396 | make up the second group. 397 | 398 | ### A little slower 399 | 400 | Let's look at this example a little slower. Firstly, let's look at how we 401 | collect object allocations using AllocationStats: 402 | 403 | ```ruby 404 | stats = AllocationStats.trace do 405 | new_string = "stringy string" 406 | another_string = "another string" 407 | an_array = [1,1,2,3,5,8,13,21,34,55] 408 | a_foreign_string = allocate_a_string_from_spec_helper 409 | end 410 | ``` 411 | 412 | Stats are collected by running a block through `AllocationStats.trace`. This is 413 | largely just a thin wrapper around `trace_object_allocations()`. You are handed 414 | back your new AllocationStats, which essentially just holds all of the 415 | allocation information, accessible via `#allocations`. Let's look at the next 416 | line to see how we can pull useful information out: 417 | 418 | ```ruby 419 | results = stats.allocations.group_by(:@sourcefile, :class).to_a 420 | ``` 421 | 422 | If you are used to chaining ActiveRecord relations, some of this might look 423 | familiar to you: `stats.allocations` will hand you back an 424 | {AllocationStats::AllocationsProxy} object, designed to hold the various 425 | transformations that you wish to run the allocations through. AllocationsProxy 426 | uses the Command pattern to store up transformations before they will actually 427 | be applied. In this example, we only make one transformation: 428 | `group_by(:@sourcefile, :class)`. This method just returns the same 429 | AllocationsProxy object back, so that transformations can be chained. The final 430 | call that will execute the transformations is `#to_a` (aliased to `#all`, just 431 | like ActiveRecord). 432 | 433 | ### Psych Example 434 | 435 | Let's look at an example with more varied allocations, using Ruby's Psych. This 436 | one is found in `examples/trace_psych_keys.rb`: 437 | 438 | ```ruby 439 | stats = AllocationStats.trace do 440 | y = YAML.dump(["one string", "two string"]) # lots of objects from Rbconfig::CONFIG["rubylibdir"] 441 | end 442 | 443 | stats.allocations.group_by(:sourcefile, :class).all.keys.each { |key| puts key.inspect } 444 | 445 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/visitors/yaml_tree.rb", String] 446 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/visitors/yaml_tree.rb", Array] 447 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/visitors/yaml_tree.rb", MatchData] 448 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/visitors/yaml_tree.rb", Method] 449 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/nodes/node.rb", Array] 450 | ["(eval)", Psych::Nodes::Sequence] 451 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/tree_builder.rb", Psych::Nodes::Document] 452 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/tree_builder.rb", Psych::Nodes::Stream] 453 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/visitors/yaml_tree.rb", Proc] 454 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/visitors/yaml_tree.rb", RubyVM::Env] 455 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/visitors/yaml_tree.rb", Hash] 456 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/visitors/yaml_tree.rb", Psych::Visitors::YAMLTree::Registrar] 457 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/visitors/yaml_tree.rb", Psych::Visitors::YAMLTree] 458 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/scalar_scanner.rb", Hash] 459 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/visitors/yaml_tree.rb", Psych::ScalarScanner] 460 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/class_loader.rb", Hash] 461 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/visitors/yaml_tree.rb", Psych::ClassLoader] 462 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/tree_builder.rb", Array] 463 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/visitors/yaml_tree.rb", Psych::TreeBuilder] 464 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych.rb", Hash] 465 | ["examples/trace_psych_inspect.rb", Array] 466 | ["examples/trace_psych_inspect.rb", String] 467 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/visitors/emitter.rb", String] 468 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/visitors/emitter.rb", Array] 469 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/visitors/emitter.rb", RubyVM::InstructionSequence] 470 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/visitors/visitor.rb", String] 471 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/visitors/visitor.rb", MatchData] 472 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/visitors/visitor.rb", Regexp] 473 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/visitors/emitter.rb", Psych::Emitter] 474 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/nodes/node.rb", Psych::Visitors::Emitter] 475 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/nodes/node.rb", StringIO] 476 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/nodes/node.rb", String] 477 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/tree_builder.rb", Psych::Nodes::Scalar] 478 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/scalar_scanner.rb", String] 479 | ["/usr/local/rbenv/versions/2.1.0-dev/lib/ruby/2.1.0/psych/scalar_scanner.rb", MatchData] 480 | ``` 481 | 482 | Again, it is difficult to find useful information from these results without 483 | aggregating. Let's do that (`examples/trace_psych_group_by`): 484 | 485 | ```ruby 486 | stats = AllocationStats.trace do 487 | y = YAML.dump(["one string", "two string"]) # lots of objects from Rbconfig::CONFIG["rubylibdir"] 488 | end 489 | 490 | puts stats.allocations(alias_paths: true).group_by(:sourcefile, :class).to_text 491 | 492 | sourcefile class count 493 | ---------------------------------------- ------------------------------------ ----- 494 | /psych/visitors/yaml_tree.rb Array 12 495 | /psych/visitors/yaml_tree.rb String 20 496 | /psych/visitors/yaml_tree.rb MatchData 3 497 | /psych/visitors/yaml_tree.rb Method 5 498 | /psych/nodes/node.rb Array 3 499 | (eval) Psych::Nodes::Sequence 1 500 | /psych/tree_builder.rb Psych::Nodes::Document 1 501 | /psych/tree_builder.rb Psych::Nodes::Stream 1 502 | /psych/visitors/yaml_tree.rb Proc 1 503 | /psych/visitors/yaml_tree.rb RubyVM::Env 1 504 | /psych/visitors/yaml_tree.rb Hash 3 505 | /psych/visitors/yaml_tree.rb Psych::Visitors::YAMLTree::Registrar 1 506 | /psych/visitors/yaml_tree.rb Psych::Visitors::YAMLTree 1 507 | /psych/scalar_scanner.rb Hash 2 508 | /psych/visitors/yaml_tree.rb Psych::ScalarScanner 1 509 | /psych/class_loader.rb Hash 1 510 | /psych/visitors/yaml_tree.rb Psych::ClassLoader 1 511 | /psych/tree_builder.rb Array 1 512 | /psych/visitors/yaml_tree.rb Psych::TreeBuilder 1 513 | /psych.rb Hash 1 514 | ./examples/trace_psych_raw.rb Array 1 515 | ./examples/trace_psych_raw.rb String 2 516 | /psych/visitors/emitter.rb String 29 517 | /psych/visitors/emitter.rb Array 3 518 | /psych/visitors/emitter.rb RubyVM::InstructionSequence 1 519 | /psych/visitors/visitor.rb String 38 520 | /psych/visitors/visitor.rb MatchData 5 521 | /psych/visitors/visitor.rb Regexp 1 522 | /psych/visitors/emitter.rb Psych::Emitter 1 523 | /psych/nodes/node.rb Psych::Visitors::Emitter 1 524 | /psych/nodes/node.rb StringIO 1 525 | /psych/nodes/node.rb String 3 526 | /psych/tree_builder.rb Psych::Nodes::Scalar 2 527 | /psych/scalar_scanner.rb String 5 528 | /psych/scalar_scanner.rb MatchData 2 529 | ``` 530 | 531 | References 532 | ---------- 533 | 534 | This new feature was inspired by work that @tmm1 did at GitHub, as 535 | described in 536 | [this post](https://github.com/blog/1489-hey-judy-don-t-make-it-bad). It was 537 | proposed as a feature in Ruby Core by @tmm1 in 538 | [Ruby issue #8107](http://bugs.ruby-lang.org/issues/8107), and @ko1 wrote it 539 | into MRI. He introduces the feature in his Ruby Kaigi 2013 presentation, on 540 | slides 29 through 33 541 | [[pdf](http://www.atdot.net/~ko1/activities/RubyKaigi2013-ko1.pdf)]. 542 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All Rights Reserved. 2 | # Licensed under the Apache License, Version 2.0, found in the LICENSE file. 3 | 4 | require 'rspec/core/rake_task' 5 | require 'yard' 6 | 7 | RSpec::Core::RakeTask.new(:spec) 8 | 9 | task :default => :spec 10 | 11 | YARD::Rake::YardocTask.new 12 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | * more in the README 2 | * binary 3 | * trace minitest 4 | -------------------------------------------------------------------------------- /allocation_stats.gemspec: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All Rights Reserved. 2 | # Licensed under the Apache License, Version 2.0, found in the LICENSE file. 3 | 4 | Gem::Specification.new do |spec| 5 | spec.name = "allocation_stats" 6 | spec.version = "0.1.5" 7 | spec.authors = ["Sam Rawlins"] 8 | spec.email = ["sam.rawlins@gmail.com"] 9 | spec.homepage = "https://github.com/srawlins/allocation_stats" 10 | spec.license = "Apache v2" 11 | spec.summary = "Tooling for tracing object allocations in Ruby 2.1" 12 | spec.description = "Tooling for tracing object allocations in Ruby 2.1" 13 | 14 | spec.files = `git ls-files`.split("\n") 15 | spec.require_paths = ["lib"] 16 | 17 | spec.add_development_dependency "rspec" 18 | 19 | # ">= 2.1.0" seems logical, but rubygems thought that "2.1.0.dev.0" did not fit that bill. 20 | # "> 2.0.0" was my next guess, but apparently "2.0.0.247" _does_ fit that bill. 21 | spec.required_ruby_version = "> 2.0.99" 22 | end 23 | -------------------------------------------------------------------------------- /examples/my_class.rb: -------------------------------------------------------------------------------- 1 | class MyClass 2 | def my_method 3 | @hash = {2 => "foo", 3 => "bar", 5 => "baz"} 4 | @string = "quux" 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /examples/trace_my_class_group_by.rb: -------------------------------------------------------------------------------- 1 | class MyClass 2 | def my_method 3 | @hash = {2 => "foo", 3 => "bar", 5 => "baz"} 4 | @string = "quux" 5 | end 6 | end 7 | 8 | require File.join(__dir__, "..", "lib", "allocation_stats") 9 | 10 | stats = AllocationStats.trace { MyClass.new.my_method } 11 | puts stats.allocations.group_by(:sourcefile, :sourceline, :class).to_text 12 | -------------------------------------------------------------------------------- /examples/trace_my_class_raw.rb: -------------------------------------------------------------------------------- 1 | class MyClass 2 | def my_method 3 | @hash = {2 => "foo", 3 => "bar", 5 => "baz"} 4 | @string = "quux" 5 | end 6 | end 7 | 8 | require File.join(__dir__, "..", "lib", "allocation_stats") 9 | 10 | stats = AllocationStats.trace { MyClass.new.my_method } 11 | puts stats.allocations.to_text(columns: [:sourcefile, :sourceline, :class_path, :method_id, :class]) 12 | -------------------------------------------------------------------------------- /examples/trace_object_allocations.rb: -------------------------------------------------------------------------------- 1 | require 'objspace' 2 | 3 | ObjectSpace.trace_object_allocations do 4 | a = [2,3,5,7,11,13,17,19,23,29,31] 5 | puts ObjectSpace.allocation_sourcefile(a) 6 | puts ObjectSpace.allocation_sourceline(a) 7 | end 8 | -------------------------------------------------------------------------------- /examples/trace_psych_group_by.rb: -------------------------------------------------------------------------------- 1 | require "yaml" 2 | require File.join(__dir__, "..", "lib", "allocation_stats") 3 | 4 | stats = AllocationStats.trace do 5 | y = YAML.dump(["one string", "two string"]) # lots of objects from Rbconfig::CONFIG["rubylibdir"] 6 | end 7 | 8 | puts stats.allocations(alias_paths: true).group_by(:sourcefile, :class).to_text 9 | -------------------------------------------------------------------------------- /examples/trace_psych_keys.rb: -------------------------------------------------------------------------------- 1 | require "yaml" 2 | require File.join(__dir__, "..", "lib", "allocation_stats") 3 | 4 | stats = AllocationStats.trace do 5 | y = YAML.dump(["one string", "two string"]) # lots of objects from Rbconfig::CONFIG["rubylibdir"] 6 | end 7 | 8 | stats.allocations.group_by(:sourcefile, :class).all.keys.each { |key| puts key.inspect } 9 | -------------------------------------------------------------------------------- /examples/trace_specs/strings.rb: -------------------------------------------------------------------------------- 1 | def an_array_of_strings 2 | ["foo", "bar", "baz", "qux", "quux"] 3 | end 4 | 5 | def foo 6 | "foo" 7 | end 8 | 9 | def teamwork 10 | "Teamwork" 11 | end 12 | 13 | def tea 14 | "Tea" 15 | end 16 | -------------------------------------------------------------------------------- /examples/trace_specs/strings_spec.rb: -------------------------------------------------------------------------------- 1 | require "pry" 2 | 3 | require_relative File.join("..", "..", "lib", "allocation_stats") 4 | require_relative "strings" 5 | AllocationStats.trace_rspec 6 | 7 | describe "Array of Strings" do 8 | it "allocates Strings and Arrays" do 9 | expect(an_array_of_strings).to include(foo) 10 | end 11 | 12 | it "allocates more Strings" do 13 | expect(teamwork).to include(tea) 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/allocation_stats.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All Rights Reserved. 2 | # Licensed under the Apache License, Version 2.0, found in the LICENSE file. 3 | 4 | require "objspace" 5 | require_relative "allocation_stats/core_ext/basic_object" 6 | require_relative "allocation_stats/allocation" 7 | require_relative "allocation_stats/allocations_proxy" 8 | require_relative "allocation_stats/trace_rspec" 9 | 10 | require "rubygems" 11 | 12 | # Container for an aggregation of object allocation data. Pass a block to 13 | # {#trace AllocationStats.new.trace}. Then use the AllocationStats object's public 14 | # interface to dig into the data and discover useful information. 15 | class AllocationStats 16 | # a convenience constant 17 | RUBYLIBDIR = RbConfig::CONFIG["rubylibdir"] 18 | 19 | # a convenience constant 20 | GEMDIR = Gem.dir 21 | 22 | # @!attribute [rw] burn 23 | # @return [Fixnum] 24 | # burn count for block tracing. Defaults to 0. When called with a block, 25 | # #trace will yield the block @burn-times before actually tracing the object 26 | # allocations. This offers the benefit of pre-memoizing objects, and loading 27 | # any required Ruby files before tracing. 28 | attr_accessor :burn 29 | 30 | attr_accessor :gc_profiler_report 31 | 32 | # @!attribute [r] new_allocations 33 | # @return [Array] 34 | # allocation data for all new objects that were allocated 35 | # during the {#initialize} block. It is better to use {#allocations}, which 36 | # returns an {AllocationsProxy}, which has a much more convenient, 37 | # domain-specific API for filtering, sorting, and grouping {Allocation} 38 | # objects, than this plain Array object. 39 | attr_reader :new_allocations 40 | 41 | def initialize(burn: 0) 42 | @burn = burn 43 | # Copying ridiculous workaround from: 44 | # https://github.com/ruby/ruby/commit/7170baa878ac0223f26fcf8c8bf25492415e6eaa 45 | Class.name 46 | end 47 | 48 | def self.trace(&block) 49 | allocation_stats = AllocationStats.new 50 | allocation_stats.trace(&block) 51 | end 52 | 53 | def trace(&block) 54 | if block_given? 55 | trace_block(&block) 56 | else 57 | start 58 | end 59 | end 60 | 61 | def trace_block 62 | @burn.times { yield } 63 | 64 | GC.start 65 | GC.disable 66 | 67 | @existing_object_ids = {} 68 | 69 | ObjectSpace.each_object.to_a.each do |object| 70 | @existing_object_ids[object.__id__ / 1000] ||= [] 71 | @existing_object_ids[object.__id__ / 1000] << object.__id__ 72 | end 73 | 74 | ObjectSpace.trace_object_allocations { 75 | yield 76 | } 77 | 78 | collect_new_allocations 79 | ObjectSpace.trace_object_allocations_clear 80 | profile_and_start_gc 81 | 82 | return self 83 | end 84 | 85 | # Begin tracing object allocations. Tracing must be stopped with 86 | # AllocationStats#stop. Garbage collection is disabled while tracing is 87 | # enabled. 88 | def start 89 | GC.start 90 | GC.disable 91 | 92 | @existing_object_ids = {} 93 | 94 | ObjectSpace.each_object.to_a.each do |object| 95 | @existing_object_ids[object.__id__ / 1000] ||= [] 96 | @existing_object_ids[object.__id__ / 1000] << object.__id__ 97 | end 98 | 99 | ObjectSpace.trace_object_allocations_start 100 | 101 | return self 102 | end 103 | 104 | def collect_new_allocations 105 | @new_allocations = [] 106 | ObjectSpace.each_object.to_a.each do |object| 107 | next if ObjectSpace.allocation_sourcefile(object).nil? 108 | next if ObjectSpace.allocation_sourcefile(object) == __FILE__ 109 | next if @existing_object_ids[object.__id__ / 1000] && 110 | @existing_object_ids[object.__id__ / 1000].include?(object.__id__) 111 | 112 | @new_allocations << Allocation.new(object) 113 | end 114 | end 115 | 116 | # Stop tracing object allocations that was started with AllocationStats#start. 117 | def stop 118 | collect_new_allocations 119 | ObjectSpace.trace_object_allocations_stop 120 | ObjectSpace.trace_object_allocations_clear 121 | profile_and_start_gc 122 | end 123 | 124 | # Inspect @new_allocations, the canonical array of {Allocation} objects. 125 | def inspect 126 | @new_allocations.inspect 127 | end 128 | 129 | # Proxy for the @new_allocations array that allows for individual filtering, 130 | # sorting, and grouping of the Allocation objects. 131 | def allocations(alias_paths: false) 132 | AllocationsProxy.new(@new_allocations, alias_paths: alias_paths) 133 | end 134 | 135 | def profile_and_start_gc 136 | GC::Profiler.enable 137 | GC.enable 138 | GC.start 139 | @gc_profiler_report = GC::Profiler.result 140 | GC::Profiler.disable 141 | end 142 | private :profile_and_start_gc 143 | end 144 | 145 | if ENV["TRACE_PROCESS_ALLOCATIONS"] 146 | $allocation_stats = AllocationStats.new.trace 147 | 148 | at_exit do 149 | $allocation_stats.stop 150 | puts "Object Allocation Report" 151 | puts "------------------------" 152 | end 153 | end 154 | -------------------------------------------------------------------------------- /lib/allocation_stats/allocation.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All Rights Reserved. 2 | # Licensed under the Apache License, Version 2.0, found in the LICENSE file. 3 | 4 | require "json" 5 | 6 | class AllocationStats 7 | # Information about an individual allocation is captured in this class. 8 | class Allocation 9 | # a convenience constants 10 | PWD = Dir.pwd 11 | 12 | # a list of helper methods that Allocation provides on top of the object 13 | # that was allocated. 14 | HELPERS = [:class_plus, :gem] 15 | 16 | # a list of attributes that Allocation has on itself; inquiries in this 17 | # list should just use Allocation's attributes, rather than the internal 18 | # object's. 19 | ATTRIBUTES = [:sourcefile, :sourceline, :class_path, :method_id, :memsize] 20 | 21 | # @!attribute [rw] memsize 22 | # the memsize of the object which was allocated 23 | attr_accessor :memsize 24 | 25 | # @!attribute [r] class_path 26 | # the classpath of where the object was allocated 27 | attr_reader :class_path 28 | 29 | # @!attribute [r] method_id 30 | # the method ID of where the object was allocated 31 | attr_reader :method_id 32 | 33 | # @!attribute [r] object 34 | # the actual object that was allocated 35 | attr_reader :object 36 | 37 | # @!attribute [r] sourceline 38 | # the line in the sourcefile where the object was allocated 39 | attr_reader :sourceline 40 | 41 | def initialize(object) 42 | @object = object 43 | @memsize = ObjectSpace.memsize_of(object) 44 | @sourcefile = ObjectSpace.allocation_sourcefile(object) 45 | @sourceline = ObjectSpace.allocation_sourceline(object) 46 | @class_path = ObjectSpace.allocation_class_path(object) 47 | @method_id = ObjectSpace.allocation_method_id(object) 48 | end 49 | 50 | # the sourcefile where the object was allocated 51 | def file; @sourcefile; end 52 | 53 | alias :line :sourceline 54 | 55 | # If the source file has recognized paths in it, those portions of the full 56 | # path will be aliased like so: 57 | # 58 | # * the present work directory is aliased to "" 59 | # * the Ruby lib directory (where the standard library lies) is aliased to 60 | # "" 61 | # * the Gem directory (where all gems lie) is aliased to "" 62 | # 63 | # @return the source file, aliased. 64 | def sourcefile_alias 65 | case 66 | when @sourcefile[AllocationStats::RUBYLIBDIR] 67 | @sourcefile.sub(AllocationStats::RUBYLIBDIR, "") 68 | when @sourcefile[AllocationStats::GEMDIR] 69 | @sourcefile.sub(/#{AllocationStats::GEMDIR}\/gems\/([^\/]+)\//, '/') 70 | when @sourcefile[PWD] 71 | @sourcefile.sub(PWD, "") 72 | else 73 | @sourcefile 74 | end 75 | end 76 | 77 | # Either the full source file (via `@sourcefile`), or the aliased source 78 | # file, via {#sourcefile_alias} 79 | # 80 | # @param [TrueClass] alias_path whether or not to alias the path 81 | def sourcefile(alias_path = false) 82 | alias_path ? sourcefile_alias : @sourcefile 83 | end 84 | 85 | # Returns class name, plus, for Arrays, extended information. When all of 86 | # the elements of the Array are instances of a total of three or fewer 87 | # classes, then those classes are listed in brackets. For example: 88 | # 89 | # @example Array with only Fixnum and Bignum elements 90 | # allocation.class_plus #=> "Array" 91 | # @example Array with elements of class A, B, C, and D 92 | # allocation.class_plus #=> "Array" 93 | # @example String (not an Array) 94 | # allocation.class_plus #=> "String" 95 | def class_plus 96 | case @object 97 | when Array 98 | object_classes = element_classes(@object.map {|e| e.class }.uniq) 99 | if object_classes 100 | "Array<#{object_classes}>" 101 | else 102 | "Array" 103 | end 104 | else 105 | @object.class.name 106 | end 107 | end 108 | 109 | # Override Rubygems' Kernel#gem 110 | # 111 | # @return [String] the name of the Rubygem where this allocation occurred. 112 | # @return [nil] if this allocation did not occur in a Rubygem. 113 | def gem 114 | gem_regex = /#{AllocationStats::GEMDIR}#{File::SEPARATOR} 115 | gems#{File::SEPARATOR} 116 | (?[^#{File::SEPARATOR}]+)#{File::SEPARATOR} 117 | /x 118 | match = gem_regex.match(sourcefile) 119 | match && match[:gem_name] 120 | end 121 | 122 | # Convert into a JSON string, which can be used in rack-allocation_stats's 123 | # interactive mode. 124 | def as_json 125 | { 126 | "memsize" => @memsize, 127 | "class_path" => @class_path, 128 | "method_id" => @method_id, 129 | "file" => sourcefile_alias, 130 | "file (raw)" => @sourcefile, 131 | "line" => @sourceline, 132 | "class" => @object.class.name, 133 | "class_plus" => class_plus 134 | } 135 | end 136 | 137 | # Convert into a JSON string, which can be used in rack-allocation_stats's 138 | # interactive mode. 139 | def to_json(*a) 140 | as_json.to_json(*a) 141 | end 142 | 143 | # @return either _the one_ class passed in, the two-to-four classes passed 144 | # in separated by commas, or `nil` if more than four classes were passed 145 | # in. 146 | # 147 | # @api private 148 | def element_classes(classes) 149 | if classes.size == 1 150 | classes.first 151 | elsif classes.size > 1 && classes.size < 4 152 | classes.join(",") 153 | else 154 | nil 155 | end 156 | end 157 | private :element_classes 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /lib/allocation_stats/allocations_proxy.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All Rights Reserved. 2 | # Licensed under the Apache License, Version 2.0, found in the LICENSE file. 3 | 4 | class AllocationStats 5 | # AllocationsProxy acts as a proxy for an array of Allocation objects. The 6 | # idea behind this class is merely to provide some domain-specific methods 7 | # for transforming (filtering, sorting, and grouping) allocation information. 8 | # This class uses the Command pattern heavily, in order to build and maintain 9 | # the list of transforms it will ultimately perform, before retrieving the 10 | # transformed collection of Allocations. 11 | # 12 | # Chaining 13 | # ======== 14 | # 15 | # Use of the Command pattern and Procs allows for transform-chaining in any 16 | # order. Apply methods such as {#from} and {#group_by} to build the internal 17 | # list of transforms. The transforms will not be applied to the collection of 18 | # Allocations until a call to {#to_a} ({#all}) resolves them. 19 | # 20 | # Filtering Transforms 21 | # -------------------- 22 | # 23 | # Methods that filter the collection of Allocations will add a transform to 24 | # an Array, `@wheres`. When the result set is finally retrieved, each where 25 | # is applied serially, so that `@wheres` represents a logical conjunction 26 | # (_"and"_) of of filtering transforms. Presently there is no way to _"or"_ 27 | # filtering transforms together with a logical disjunction. 28 | # 29 | # Mapping Transforms 30 | # ------------------ 31 | # 32 | # Grouping Transform 33 | # ------------------ 34 | # 35 | # Only one method will allow a grouping transform: {#group_by}. Only one 36 | # grouping transform is allowed; subsequent calls to {#group_by} will only 37 | # replace the previous grouping transform. 38 | class AllocationsProxy 39 | 40 | # Instantiate an {AllocationsProxy} with an array of Allocations. 41 | # {AllocationProxy's} view of `pwd` is set at instantiation. 42 | # 43 | # @param [Array] allocations array of Allocation objects 44 | def initialize(allocations, alias_paths: false) 45 | @allocations = allocations 46 | @pwd = Dir.pwd 47 | @wheres = [] 48 | @group_by = nil 49 | @mappers = [] 50 | @alias_paths = alias_paths 51 | end 52 | 53 | # Apply all transformations to the contained list of Allocations. This is 54 | # aliased as `:all`. 55 | def to_a 56 | results = @allocations 57 | 58 | @wheres.each do |where| 59 | results = where.call(results) 60 | end 61 | 62 | # First apply group_by 63 | results = @group_by.call(results) if @group_by 64 | 65 | # Apply each mapper 66 | @mappers.each do |mapper| 67 | results = mapper.call(results) 68 | end 69 | 70 | results 71 | end 72 | alias :all :to_a 73 | 74 | # If a value is passed in, @alias_paths will be set to this value, and the 75 | # AllocationStats object will be returned. If no value is passed in, this 76 | # will return the @alias_paths. 77 | def alias_paths(value = nil) 78 | # reader 79 | return @alias_paths if value.nil? 80 | 81 | # writer 82 | @alias_paths = value 83 | 84 | return self 85 | end 86 | 87 | # Sort allocation groups by the number of allocations in each group. 88 | def sort_by_size 89 | @mappers << Proc.new do |allocations| 90 | allocations.sort_by { |key, value| -value.size } 91 | .inject({}) { |hash, pair| hash[pair[0]] = pair[1]; hash } 92 | end 93 | 94 | self 95 | end 96 | alias :sort_by_count :sort_by_size 97 | 98 | # Select allocation groups which have at least `count` allocations. 99 | # 100 | # @param [Fixnum] count the minimum number of Allocations for each group to 101 | # be selected. 102 | def at_least(count) 103 | @mappers << Proc.new do |allocations| 104 | allocations.delete_if { |key,value| value.size < count } 105 | end 106 | 107 | self 108 | end 109 | 110 | # Select allocations for which the {Allocation#sourcefile sourcefile} 111 | # includes `pattern`. 112 | # 113 | # `#from` can be called multiple times, adding to `@wheres`. See 114 | # documentation for {AllocationsProxy} for more information about chaining. 115 | # 116 | # @param [String] pattern the partial file path to match against, in the 117 | # {Allocation#sourcefile Allocation's sourcefile}. 118 | def from(pattern) 119 | @wheres << Proc.new do |allocations| 120 | allocations.select { |allocation| allocation.sourcefile[pattern] } 121 | end 122 | 123 | self 124 | end 125 | 126 | # Select allocations for which the {Allocation#sourcefile sourcefile} does 127 | # not include `pattern`. 128 | # 129 | # `#not_from` can be called multiple times, adding to `@wheres`. See 130 | # documentation for {AllocationsProxy} for more information about chaining. 131 | # 132 | # @param [String] pattern the partial file path to match against, in the 133 | # {Allocation#sourcefile Allocation's sourcefile}. 134 | def not_from(pattern) 135 | @wheres << Proc.new do |allocations| 136 | allocations.reject { |allocation| allocation.sourcefile[pattern] } 137 | end 138 | 139 | self 140 | end 141 | 142 | # Select allocations for which the {Allocation#sourcefile sourcefile} 143 | # includes the present working directory. 144 | # 145 | # `#from_pwd` can be called multiple times, adding to `@wheres`. See 146 | # documentation for {AllocationsProxy} for more information about chaining. 147 | def from_pwd 148 | @wheres << Proc.new do |allocations| 149 | allocations.select { |allocation| allocation.sourcefile[@pwd] } 150 | end 151 | 152 | self 153 | end 154 | 155 | # Group allocations by one or more attributes, that is, a list of symbols. 156 | # Commonly, you might want to group allocations by: 157 | # 158 | # * :sourcefile, :sourceline, :class 159 | # * :sourcefile, :method_id, :class 160 | # * :classpath, :method_id, :class 161 | # 162 | # In this case, `:class` is the class of the allocated object (as opposed 163 | # to `:classpath`, the classpath where the allocation occured). 164 | def group_by(*args) 165 | @group_keys = args 166 | 167 | @group_by = Proc.new do |allocations| 168 | getters = attribute_getters(@group_keys) 169 | 170 | allocations.group_by do |allocation| 171 | getters.map { |getter| getter.call(allocation) } 172 | end 173 | end 174 | 175 | self 176 | end 177 | 178 | # Select allocations that match `conditions`. 179 | # 180 | # @param [Hash] conditions pairs of attribute names and values to be matched amongst allocations. 181 | # 182 | # @example select allocations of String objects: 183 | # allocations.where(class: String) 184 | def where(conditions) 185 | @wheres << Proc.new do |allocations| 186 | conditions = conditions.inject({}) do |memo, pair| 187 | faux, value = *pair 188 | getter = attribute_getters([faux]).first 189 | memo.merge(getter => value) 190 | end 191 | 192 | allocations.select do |allocation| 193 | conditions.all? { |getter, value| getter.call(allocation) == value } 194 | end 195 | end 196 | 197 | self 198 | end 199 | 200 | def attribute_getters(faux_attributes) 201 | faux_attributes.map do |faux| 202 | if faux == :sourcefile 203 | lambda { |allocation| allocation.sourcefile(@alias_paths) } 204 | elsif Allocation::HELPERS.include?(faux) || 205 | Allocation::ATTRIBUTES.include?(faux) 206 | lambda { |allocation| allocation.__send__(faux) } 207 | else 208 | lambda { |allocation| allocation.object.__send__(faux) } 209 | end 210 | end 211 | end 212 | private :attribute_getters 213 | 214 | # Map to bytes via {Allocation#memsize #memsize}. This is done in one of 215 | # two ways: 216 | # 217 | # * If the current result set is an Array, then this transform just maps 218 | # each Allocation to its `#memsize`. 219 | # * If the current result set is a Hash (meaning it has been grouped), then 220 | # this transform maps each value in the Hash (which is an Array of 221 | # Allocations) to the sum of the Allocation `#memsizes` within. 222 | def bytes 223 | @mappers << Proc.new do |allocations| 224 | if allocations.is_a? Array 225 | allocations.map(&:memsize) 226 | elsif allocations.is_a? Hash 227 | bytes_h = {} 228 | allocations.each do |key, allocations| 229 | bytes_h[key] = allocations.inject(0) { |sum, allocation| sum + allocation.memsize } 230 | end 231 | bytes_h 232 | end 233 | end 234 | 235 | self 236 | end 237 | 238 | # default columns for the tabular output 239 | DEFAULT_COLUMNS = [:sourcefile, :sourceline, :class_path, :method_id, :memsize, :class] 240 | 241 | # columns that should be right-aligned for the tabular output 242 | NUMERIC_COLUMNS = [:sourceline, :memsize] 243 | 244 | # Resolve the AllocationsProxy (by calling {#to_a}) and return tabular 245 | # information about the Allocations as a String. 246 | # 247 | # @param [Array] columns a list of columns to print out 248 | # 249 | # @return [String] information about the Allocations, in a tabular format 250 | def to_text(columns: DEFAULT_COLUMNS) 251 | resolved = to_a 252 | 253 | # if resolved is an Array of Allocations 254 | if resolved.is_a?(Array) && resolved.first.is_a?(Allocation) 255 | to_text_from_plain(resolved, columns: columns) 256 | 257 | # if resolved is a Hash (was grouped) 258 | elsif resolved.is_a?(Hash) 259 | to_text_from_groups(resolved) 260 | end 261 | end 262 | 263 | # Resolve all transformations, and convert the resultant Array to JSON. 264 | def to_json 265 | to_a.to_json 266 | end 267 | 268 | # Return tabular information about the un-grouped list of Allocations. 269 | # 270 | # @private 271 | def to_text_from_plain(resolved, columns: DEFAULT_COLUMNS) 272 | getters = attribute_getters(columns) 273 | 274 | widths = getters.each_with_index.map do |attr, idx| 275 | (resolved.map { |a| attr.call(a).to_s.size } << columns[idx].to_s.size).max 276 | end 277 | 278 | text = [] 279 | 280 | text << columns.each_with_index.map { |attr, idx| 281 | attr.to_s.center(widths[idx]) 282 | }.join(" ").rstrip 283 | 284 | text << widths.map { |width| "-" * width }.join(" ") 285 | 286 | text += resolved.map { |allocation| 287 | getters.each_with_index.map { |getter, idx| 288 | value = getter.call(allocation).to_s 289 | NUMERIC_COLUMNS.include?(columns[idx]) ? value.rjust(widths[idx]) : value.ljust(widths[idx]) 290 | }.join(" ").rstrip 291 | } 292 | 293 | text.join("\n") 294 | end 295 | private :to_text_from_plain 296 | 297 | # Return tabular information about the grouped Allocations. 298 | # 299 | # @private 300 | def to_text_from_groups(resolved) 301 | columns = @group_keys + ["count"] 302 | 303 | keys = resolved.is_a?(Hash) ? resolved.keys : resolved.map(&:first) 304 | widths = columns.each_with_index.map do |column, idx| 305 | (keys.map { |group| group[idx].to_s.size } << columns[idx].to_s.size).max 306 | end 307 | 308 | text = [] 309 | 310 | text << columns.each_with_index.map { |attr, idx| 311 | attr.to_s.center(widths[idx]) 312 | }.join(" ").rstrip 313 | 314 | text << widths.map { |width| "-" * width }.join(" ") 315 | 316 | text += resolved.map { |group, allocations| 317 | line = group.each_with_index.map { |attr, idx| 318 | NUMERIC_COLUMNS.include?(columns[idx]) ? 319 | attr.to_s.rjust(widths[idx]) : 320 | attr.to_s.ljust(widths[idx]) 321 | }.join(" ") 322 | 323 | line << " " + allocations.size.to_s.rjust(5) 324 | } 325 | 326 | text.join("\n") 327 | end 328 | private :to_text_from_groups 329 | end 330 | end 331 | -------------------------------------------------------------------------------- /lib/allocation_stats/core_ext/basic_object.rb: -------------------------------------------------------------------------------- 1 | # monkey patch to BasicObject, allowing it to respnd to :class 2 | # 3 | # @private 4 | class BasicObject 5 | # monkey patch to BasicObject, allowing it to respnd to :class 6 | # 7 | # @private 8 | def class 9 | (class << self; self end).superclass 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/allocation_stats/trace_rspec.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All Rights Reserved. 2 | # Licensed under the Apache License, Version 2.0, found in the LICENSE file. 3 | 4 | class AllocationStats 5 | def self.trace_rspec 6 | @top_sites = [] 7 | 8 | if (!const_defined?(:RSpec)) 9 | raise StandardError, "Cannot trace RSpec until RSpec is loaded" 10 | end 11 | 12 | ::RSpec.configure do |config| 13 | config.around(&TRACE_RSPEC_HOOK) 14 | end 15 | 16 | at_exit do 17 | puts AllocationStats.top_sites_text 18 | end 19 | end 20 | 21 | TRACE_RSPEC_HOOK = proc do |example| 22 | stats = AllocationStats.new(burn: 1).trace { example.run } 23 | 24 | allocations = stats.allocations(alias_paths: true). 25 | not_from("rspec-core").not_from("rspec-expectations").not_from("rspec-mocks"). 26 | not_from("lib/allocation_stats").group_by(:sourcefile, :sourceline, :class). 27 | sort_by_count 28 | 29 | AllocationStats.add_to_top_sites(allocations.all, example.location) 30 | end 31 | 32 | # Read the sorted list of the top "sites", that is, top file/line/class 33 | # groups, encountered while tracing RSpec. 34 | # 35 | # @api private 36 | def self.top_sites 37 | @top_sites 38 | end 39 | 40 | # Write to the sorted list of the top "sites", that is, top file/line/class 41 | # groups, encountered while tracing RSpec. 42 | # 43 | # @api private 44 | def self.top_sites=(value) 45 | @top_sites = value 46 | end 47 | 48 | # Add a Hash of allocation groups (derived from an 49 | # `AllocationStats.allocations...group_by(...)`) to the top allocation sites 50 | # (file/line/class groups). 51 | # 52 | # @param [Hash] allocations 53 | # @param [String] location the RSpec spec location that was being executed 54 | # when the allocations occurred 55 | # @param [Fixnum] limit size of the top sites Array 56 | def self.add_to_top_sites(allocations, location, limit = 10) 57 | if allocations.size > limit 58 | allocations = allocations.to_a[0...limit].to_h # top 10 or so 59 | end 60 | 61 | # TODO: not a great algorithm so far... can instead: 62 | # * oly insert when an allocation won't be immediately dropped 63 | # * insert into correct position and pop rather than sort and slice 64 | allocations.each do |k,v| 65 | next if k[0] =~ /spec_helper\.rb$/ 66 | 67 | if site = @top_sites.detect { |s| s[:key] == k } 68 | if lower_idx = site[:counts].index { |loc, count| count < v.size } 69 | site[:counts].insert(lower_idx, [location, v.size]) 70 | else 71 | site[:counts] << [location, v.size] 72 | end 73 | site[:counts].pop if site[:counts].size > 3 74 | else 75 | @top_sites << { key: k, counts: [[location, v.size]] } 76 | end 77 | end 78 | 79 | @top_sites = @top_sites.sort_by! { |site| 80 | -site[:counts].map(&:last).max 81 | }[0...limit] 82 | end 83 | 84 | # Textual String representing the sorted list of the top allocation sites. 85 | # For each site, this String includes the number of allocations, the class, 86 | # the sourcefile, the sourceline, and the location of the RSpec spec. 87 | # 88 | # @api private 89 | def self.top_sites_text 90 | return "" if @top_sites.empty? 91 | 92 | result = "Top #{@top_sites.size} allocation sites:\n" 93 | @top_sites.each do |site| 94 | result << " %s allocations at %s:%d\n" % [site[:key][2], site[:key][0], site[:key][1]] 95 | site[:counts].each do |location, count| 96 | result << " %3d allocations during %s\n" % [count, location] 97 | end 98 | end 99 | 100 | result 101 | end 102 | end 103 | -------------------------------------------------------------------------------- /spec/allocation_stats/allocations_proxy_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All Rights Reserved. 2 | # Licensed under the Apache License, Version 2.0, found in the LICENSE file. 3 | 4 | require_relative File.join("..", "spec_helper") 5 | SPEC_HELPER_PATH = File.expand_path(File.join(__dir__, "..", "spec_helper.rb")) 6 | MAX_PATH_LENGTH = [SPEC_HELPER_PATH.size, __FILE__.size].max 7 | 8 | describe AllocationStats::AllocationsProxy do 9 | context "when strings are allocated in this sourcefile only" do 10 | it "tracks new objects by path" do 11 | existing_array = [1,2,3,4,5] 12 | 13 | stats = AllocationStats.trace do 14 | new_string = "stringy string" 15 | another_string = "another string" 16 | end 17 | 18 | results = stats.allocations.group_by(:sourcefile).all 19 | expect(results.class).to be Hash 20 | expect(results.keys.size).to eq 1 21 | expect(results.keys.first).to eq [__FILE__] 22 | expect(results[[__FILE__]].class).to be Array 23 | expect(results[[__FILE__]].size).to eq 2 24 | end 25 | end 26 | 27 | context "when a string is allocated from another sourcefile" do 28 | it "tracks new objects by path" do 29 | existing_array = [1,2,3,4,5] 30 | 31 | stats = AllocationStats.trace do 32 | new_string = "stringy string" 33 | another_string = "another string" 34 | a_foreign_string = allocate_a_string_from_spec_helper 35 | end 36 | 37 | results = stats.allocations.group_by(:sourcefile).all 38 | expect(results.keys.size).to eq 2 39 | expect(results.keys).to include([__FILE__]) 40 | expect(results.keys.any? { |file| file[0]["spec_helper"] }).to be true 41 | end 42 | end 43 | 44 | it "tracks new objects by path and class" do 45 | existing_array = [1,2,3,4,5] 46 | 47 | stats = AllocationStats.trace do 48 | new_string = "stringy string" 49 | another_string = "another string" 50 | an_array = [1,1,2,3,5,8,13,21,34,55] 51 | a_foreign_string = allocate_a_string_from_spec_helper 52 | end 53 | 54 | results = stats.allocations.group_by(:sourcefile, :class).all 55 | expect(results.keys.size).to eq 3 56 | expect(results.keys).to include([__FILE__, String]) 57 | expect(results.keys).to include([__FILE__, Array]) 58 | end 59 | 60 | it "tracks new BasicObjects" do 61 | class BO < BasicObject; end 62 | 63 | stats = AllocationStats.trace do 64 | bo = BO.new 65 | end 66 | 67 | results = stats.allocations.group_by(:sourcefile).all 68 | expect(results.class).to be(Hash) 69 | expect(results.keys.size).to eq(1) 70 | expect(results.keys.first).to eq([__FILE__]) 71 | expect(results[[__FILE__]].class).to eq(Array) 72 | expect(results[[__FILE__]].size).to eq(1) 73 | expect(results[[__FILE__]].first.object.class).to be(BO) 74 | end 75 | 76 | it "tracks new objects by path and class_name (Array with 1x type)" do 77 | stats = AllocationStats.trace do 78 | square_groups = [] 79 | 10.times do |i| 80 | square_groups << [(4*i+0)**2, (4*i+1)**2, (4*i+2)**2, (4*i+3)**2] 81 | end 82 | end 83 | 84 | results = stats.allocations.group_by(:sourcefile, :class_plus).all 85 | expect(results.keys.size).to eq 2 86 | expect(results.keys).to include([__FILE__, "Array"]) 87 | expect(results.keys).to include([__FILE__, "Array"]) 88 | end 89 | 90 | it "tracks new objects by path and class_name (Array with 2-3x type)" do 91 | stats = AllocationStats.trace do 92 | two_classes = [1,2,3,"a","b","c"] 93 | three_classes = [1,1.0,"1"] 94 | end 95 | 96 | results = stats.allocations.group_by(:sourcefile, :class_plus).all 97 | expect(results.keys.size).to eq 3 98 | expect(results.keys).to include([__FILE__, "Array"]) 99 | expect(results.keys).to include([__FILE__, "Array"]) 100 | end 101 | 102 | it "tracks new objects by path and class_name (Arrays with same size)" do 103 | stats = AllocationStats.trace do 104 | ary = [] 105 | 10.times do 106 | ary << [1,2,3,4,5] 107 | end 108 | end 109 | 110 | results = stats.allocations.group_by(:sourcefile, :class_plus).all 111 | expect(results.keys.size).to eq 2 112 | expect(results.keys).to include([__FILE__, "Array"]) 113 | expect(results.keys).to include([__FILE__, "Array"]) 114 | end 115 | 116 | it "tracks new objects by class_path, method_id and class" do 117 | existing_array = [1,2,3,4,5] 118 | 119 | stats = AllocationStats.trace do 120 | new_string = "stringy string" 121 | another_string = "another string" 122 | an_array = [1,1,2,3,5,8,13,21,34,55] 123 | a_foreign_string = allocate_a_string_from_spec_helper 124 | end 125 | 126 | results = stats.allocations.group_by(:class_path, :method_id, :class).all 127 | expect(results.keys.size).to eq 3 128 | 129 | # Things allocated inside rspec describe and it blocks have nil as the 130 | # method_id. 131 | expect(results.keys).to include([nil, nil, String]) 132 | expect(results.keys).to include([nil, nil, Array]) 133 | expect(results.keys).to include(["Object", :allocate_a_string_from_spec_helper, String]) 134 | end 135 | 136 | it "tracks new bytes" do 137 | stats = AllocationStats.trace do 138 | an_array = [1,1,2,3,5,8,13,21,34,55] 139 | end 140 | 141 | byte_sums = stats.allocations.bytes.all 142 | expect(byte_sums.size).to eq 1 143 | expect(byte_sums[0]).to be 80 144 | end 145 | 146 | it "tracks new bytes by path and class" do 147 | stats = AllocationStats.trace do 148 | new_string = "stringy string" # 1: String from here 149 | an_array = [1,1,2,3,5,8,13,21,34,55] # 2: Array from here 150 | a_foreign_string = allocate_a_string_from_spec_helper # 3: String from spec_helper 151 | 152 | class A; end # 4: Class from here 153 | an_a = A.new # 5: A from here 154 | end 155 | 156 | byte_sums = stats.allocations.group_by(:sourcefile, :class).bytes.all 157 | expect(byte_sums.keys.size).to eq 5 158 | expect(byte_sums.keys).to include([__FILE__, Array]) 159 | expect(byte_sums[[__FILE__, Array]]).to eq 80 # 10 Fixnums * 8 bytes/Fixnum 160 | end 161 | 162 | it "tracks new allocations in pwd" do 163 | existing_array = [1,2,3,4,5] 164 | 165 | stats = AllocationStats.trace do 166 | new_string = "stringy string" # 1: String from here 167 | another_string = "another string" 168 | an_array = [1,1,2,3,5,8,13,21,34,55] # 2: Array from here 169 | a_range = "aaa".."zzz" 170 | y = YAML.dump(["one string", "two string"]) # lots of objects not from here 171 | end 172 | 173 | results = stats.allocations.from_pwd.group_by(:class).all 174 | expect(results.keys.size).to eq 3 175 | expect(results[[String]].size).to eq 6 176 | expect(results[[Array]].size).to eq 2 177 | expect(results[[Range]].size).to eq 1 178 | end 179 | 180 | it "passes itself to Yajl::Encoder.encode correctly" do 181 | stats = AllocationStats.trace do 182 | new_hash = {0 => "foo", 1 => "bar"} 183 | end 184 | 185 | json = Yajl::Encoder.encode(stats.allocations.to_a) 186 | array = Yajl::Parser.new.parse(json) 187 | 188 | expect(array).to eq [ 189 | {"memsize" => 192, 190 | "class_path" => nil, 191 | "method_id" => nil, 192 | "file" => __FILE__.sub(Dir.pwd, ""), 193 | "file (raw)" => __FILE__, 194 | "line" => __LINE__ - 12, 195 | "class" => "Hash", 196 | "class_plus" => "Hash"}, 197 | {"memsize" => 0, 198 | "class_path" => nil, 199 | "method_id" => nil, 200 | "file" => __FILE__.sub(Dir.pwd, ""), 201 | "file (raw)" => __FILE__, 202 | "line" => __LINE__ - 20, 203 | "class" => "String", 204 | "class_plus" => "String"}, 205 | {"memsize" => 0, 206 | "class_path" => nil, 207 | "method_id" => nil, 208 | "file" => __FILE__.sub(Dir.pwd, ""), 209 | "file (raw)" => __FILE__, 210 | "line" => __LINE__ - 28, 211 | "class" => "String", 212 | "class_plus" => "String"} 213 | ] 214 | end 215 | 216 | it "shortens paths of stuff in RUBYLIBDIR" do 217 | stats = AllocationStats.trace do 218 | y = YAML.dump(["one string", "two string"]) # lots of objects from Rbconfig::CONFIG["rubylibdir"] 219 | end 220 | 221 | files = stats.allocations(alias_paths: true).group_by(:sourcefile, :class).all.keys.map(&:first) 222 | expect(files).to include("/psych/nodes/node.rb") 223 | end 224 | 225 | it "shortens paths of stuff in gems" do 226 | stats = AllocationStats.trace do 227 | j = Yajl.dump(["one string", "two string"]) # lots of objects from Rbconfig::CONFIG["rubylibdir"] 228 | end 229 | 230 | files = stats.allocations(alias_paths: true).group_by(:sourcefile, :class).all.keys.map(&:first) 231 | expect(files).to include("/lib/yajl.rb") 232 | end 233 | 234 | it "tracks new objects by gem" do 235 | stats = AllocationStats.trace do 236 | j = Yajl.dump(["one string", "two string"]) # lots of objects from Rbconfig::CONFIG["rubylibdir"] 237 | end 238 | 239 | gems = stats.allocations.group_by(:gem, :class).all.keys.map(&:first) 240 | expect(gems).to include("yajl-ruby-1.1.0") 241 | expect(gems).to include(nil) 242 | end 243 | 244 | it "is able to filter to just anything from pwd" do 245 | stats = AllocationStats.trace do 246 | j = Yajl.dump(["one string", "two string"]) # lots of objects from Rbconfig::CONFIG["rubylibdir"] 247 | end 248 | 249 | files = stats.allocations.group_by(:sourcefile, :class).from_pwd.all.keys.map(&:first) 250 | expect(files).not_to include("/gems/yajl-ruby-1.1.0/lib/yajl.rb") 251 | end 252 | 253 | it "is able to filter to just anything from pwd, even if from is specified before group_by" do 254 | stats = AllocationStats.trace do 255 | j = Yajl.dump(["one string", "two string"]) # lots of objects from Rbconfig::CONFIG["rubylibdir"] 256 | end 257 | 258 | files = stats.allocations.from_pwd.group_by(:sourcefile, :class).all.keys.map(&:first) 259 | expect(files).not_to include("/gems/yajl-ruby-1.1.0/lib/yajl.rb") 260 | end 261 | 262 | it "is able to filter to just one path" do 263 | stats = AllocationStats.trace do 264 | j = Yajl.dump(["one string", "two string"]) # lots of objects from Rbconfig::CONFIG["rubylibdir"] 265 | end 266 | 267 | files = stats.allocations(alias_paths: true).group_by(:sourcefile, :class).from("yajl.rb").all.keys.map(&:first) 268 | expect(files).to include("/lib/yajl.rb") 269 | end 270 | 271 | it "is able to filter out just one path" do 272 | stats = AllocationStats.trace do 273 | j = Yajl.dump(["one string", "two string"]) # lots of objects from Rbconfig::CONFIG["rubylibdir"] 274 | end 275 | 276 | files = stats.allocations.not_from("yajl.rb").group_by(:sourcefile, :class).all.keys.map(&:first) 277 | expect(files).not_to include("/gems/yajl-ruby-1.1.0/lib/yajl.rb") 278 | end 279 | 280 | it "is able to filter to just one class" do 281 | stats = AllocationStats.trace do 282 | j = Yajl.dump(["one string", "two string"]) # lots of objects from Rbconfig::CONFIG["rubylibdir"] 283 | end 284 | 285 | classes = stats.allocations.where(class: String).group_by(:sourcefile, :class).all.keys.map(&:last) 286 | expect(classes).not_to include(Array) 287 | expect(classes).not_to include(Hash) 288 | expect(classes).to include(String) 289 | end 290 | 291 | describe "#to_text" do 292 | before do 293 | @stats = AllocationStats.trace { MyClass.new.my_method } 294 | @line = __LINE__ - 1 295 | end 296 | 297 | it "outputs to fixed-width text correctly" do 298 | text = @stats.allocations.to_text 299 | spec_helper_plus_line = "#{SPEC_HELPER_PATH.ljust(MAX_PATH_LENGTH)} #{MyClass::MY_METHOD_BODY_LINE}" 300 | 301 | expect(text).to include("#{"sourcefile".center(MAX_PATH_LENGTH)} sourceline class_path method_id memsize class") 302 | expect(text).to include("#{"-" * MAX_PATH_LENGTH} ---------- ---------- --------- ------- -------") 303 | expect(text).to include("#{spec_helper_plus_line} MyClass my_method 192 Hash") 304 | expect(text).to include("#{spec_helper_plus_line} MyClass my_method 0 String") 305 | expect(text).to include("#{__FILE__.ljust(MAX_PATH_LENGTH)} #{@line} Class new 0 MyClass") 306 | end 307 | 308 | context "with custom columns" do 309 | it "outputs to fixed-width text correctly" do 310 | text = @stats.allocations.to_text(columns: [:sourcefile, :sourceline, :class]) 311 | spec_helper_plus_line = "#{SPEC_HELPER_PATH.ljust(MAX_PATH_LENGTH)} #{MyClass::MY_METHOD_BODY_LINE}" 312 | 313 | expect(text).to include("#{"sourcefile".center(MAX_PATH_LENGTH)} sourceline class") 314 | expect(text).to include("#{"-" * MAX_PATH_LENGTH} ---------- -------") 315 | expect(text).to include("#{spec_helper_plus_line} Hash") 316 | expect(text).to include("#{spec_helper_plus_line} String") 317 | expect(text).to include("#{__FILE__.ljust(MAX_PATH_LENGTH)} #{@line} MyClass") 318 | end 319 | end 320 | 321 | context "with custom columns and aliased paths" do 322 | it "outputs to fixed-width text correctly" do 323 | text = @stats.allocations(alias_paths: true).to_text(columns: [:sourcefile, :sourceline, :class]) 324 | spec_helper_plus_line = "/spec/spec_helper.rb #{MyClass::MY_METHOD_BODY_LINE}" 325 | 326 | expect(text).to include(" sourcefile sourceline class") 327 | expect(text).to include("----------------------------------------------------- ---------- -------") 328 | expect(text).to include("#{spec_helper_plus_line} Hash") 329 | expect(text).to include("#{spec_helper_plus_line} String") 330 | expect(text).to include("/spec/allocation_stats/allocations_proxy_spec.rb #{@line} MyClass") 331 | end 332 | end 333 | 334 | context "after #group_by" do 335 | it "outputs to fixed-width text correctly" do 336 | text = @stats.allocations(alias_paths: true).group_by(:sourcefile, :sourceline, :class).to_text 337 | spec_helper_plus_line = "/spec/spec_helper.rb #{MyClass::MY_METHOD_BODY_LINE}" 338 | 339 | expect(text).to include(" sourcefile sourceline class count\n") 340 | expect(text).to include("----------------------------------------------------- ---------- ------- -----\n") 341 | expect(text).to include("#{spec_helper_plus_line} Hash 1") 342 | expect(text).to include("#{spec_helper_plus_line} String 2") 343 | expect(text).to include("/spec/allocation_stats/allocations_proxy_spec.rb #{@line} MyClass 1") 344 | end 345 | end 346 | end 347 | 348 | describe "#to_json" do 349 | before do 350 | @stats = AllocationStats.trace { MyClass.new.my_method } 351 | @line = __LINE__ - 1 352 | end 353 | 354 | it "outputs to JSON without raising an exception" do 355 | json = @stats.allocations.to_json 356 | expect { Yajl::Parser.parse(json) }.to_not raise_error 357 | end 358 | 359 | it "outputs to JSON correctly" do 360 | allocations = @stats.allocations.all 361 | json = allocations.to_json 362 | parsed = Yajl::Parser.parse(json) 363 | 364 | first = { 365 | "file" => "/spec/spec_helper.rb", 366 | "file (raw)" => "#{Dir.pwd}/spec/spec_helper.rb", 367 | "line" => 23, 368 | "class_path" => "MyClass", 369 | "method_id" => :my_method.to_s, 370 | "memsize" => 192, 371 | "class" => "Hash", 372 | "class_plus" => "Hash" 373 | } 374 | 375 | expect(parsed.size).to be(4) 376 | expect(parsed.any? { |allocation| allocation == first } ).to be true 377 | end 378 | end 379 | 380 | describe "sorting" do 381 | before do 382 | @stats = AllocationStats.trace do 383 | ary = [] 384 | 4.times do 385 | ary << [1,2,3,4,5] 386 | end 387 | str_1 = "string"; str_2 = "strang" 388 | end 389 | @lines = [__LINE__ - 6, __LINE__ - 4, __LINE__ - 2] 390 | end 391 | 392 | it "sorts Allocations that have not been grouped" do 393 | results = @stats.allocations.group_by(:sourcefile, :sourceline, :class).sort_by_count.all 394 | 395 | expect(results.keys[0]).to include(@lines[1]) 396 | expect(results.keys[1]).to include(@lines[2]) 397 | expect(results.keys[2]).to include(@lines[0]) 398 | 399 | expect(results.values[0].size).to eq(4) 400 | expect(results.values[1].size).to eq(2) 401 | expect(results.values[2].size).to eq(1) 402 | end 403 | 404 | it "filters out low count Allocations" do 405 | results = @stats.allocations.group_by(:sourcefile, :sourceline, :class).at_least(4).all 406 | 407 | expect(results.size).to eq(1) 408 | 409 | expect(results.keys[0]).to include(@lines[1]) 410 | expect(results.values[0].size).to eq(4) 411 | end 412 | 413 | context "after group_by(...).sort_by_count" do 414 | it "outputs to fixed-width text correctly" do 415 | text = @stats.allocations(alias_paths: true) 416 | .group_by(:sourcefile, :sourceline, :class) 417 | .sort_by_count 418 | .to_text.split("\n") 419 | spec_file = "/spec/allocation_stats/allocations_proxy_spec.rb " 420 | 421 | expect(text[0]).to eq(" sourcefile sourceline class count") 422 | expect(text[1]).to eq("----------------------------------------------------- ---------- ------ -----") 423 | expect(text[2]).to eq("#{spec_file} #{@lines[1]} Array 4") 424 | expect(text[3]).to eq("#{spec_file} #{@lines[2]} String 2") 425 | expect(text[4]).to eq("#{spec_file} #{@lines[0]} Array 1") 426 | end 427 | end 428 | end 429 | end 430 | -------------------------------------------------------------------------------- /spec/allocation_stats_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All Rights Reserved. 2 | # Licensed under the Apache License, Version 2.0, found in the LICENSE file. 3 | 4 | require_relative File.join("spec_helper") 5 | 6 | describe AllocationStats do 7 | it "traces everything if TRACE_PROCESS_ALLOCATIONS env var is set" do 8 | IO.popen({"TRACE_PROCESS_ALLOCATIONS" => "1"}, "ruby -r ./lib/allocation_stats -e 'puts 0'") do |io| 9 | out = io.read 10 | expect(out).to match("Object Allocation Report") 11 | end 12 | end 13 | 14 | it "only tracks new objects" do 15 | existing_array = [1,2,3,4,5] 16 | 17 | stats = AllocationStats.trace do 18 | new_array = [1,2,3,4,5] 19 | end 20 | 21 | expect(stats.new_allocations.class).to be Array 22 | expect(stats.new_allocations.size).to eq 1 23 | end 24 | 25 | it "only tracks new objects, non-block mode" do 26 | existing_array = [1,2,3,4,5] 27 | 28 | stats = AllocationStats.trace 29 | new_array = [1,2,3,4,5] 30 | stats.stop 31 | 32 | expect(stats.new_allocations.class).to be Array 33 | expect(stats.new_allocations.size).to eq 1 34 | end 35 | 36 | it "only tracks new objects; String keys in Hashes are frozen" do 37 | existing_array = [1,2,3,4,5] 38 | 39 | stats = AllocationStats.trace do 40 | new_hash = {"foo" => "bar", "baz" => "quux"} 41 | end 42 | 43 | expect(stats.new_allocations.size).to eq 3 44 | end 45 | 46 | it "only tracks new objects, using instance method" do 47 | existing_array = [1,2,3,4,5] 48 | 49 | stats = AllocationStats.new 50 | 51 | stats.trace do 52 | new_object = Object.new 53 | new_array = [4] 54 | new_string = "yarn" 55 | end 56 | 57 | expect(stats.new_allocations.class).to be Array 58 | expect(stats.new_allocations.size).to eq 3 59 | end 60 | 61 | it "only tracks new objects" do 62 | existing_array = [1,2,3,4,5] 63 | 64 | my_instance = MyClass.new 65 | 66 | stats = AllocationStats.new(burn: 3).trace do 67 | # this method instantiates 2**(n-1) Strings on the n'th call 68 | my_instance.memoizing_method 69 | end 70 | 71 | expect(stats.new_allocations.class).to be Array 72 | expect(stats.new_allocations.size).to eq 8 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All Rights Reserved. 2 | # Licensed under the Apache License, Version 2.0, found in the LICENSE file. 3 | 4 | require "simplecov" 5 | SimpleCov.start 6 | 7 | require_relative "../lib/allocation_stats" 8 | require "yaml" 9 | require "yajl" 10 | require "pry" 11 | 12 | if RbConfig::CONFIG["MAJOR"].to_i < 2 || RbConfig::CONFIG["MINOR"].to_i < 1 13 | warn "Error: AllocationStats requires Ruby 2.1 or greater" 14 | exit 1 15 | end 16 | 17 | def allocate_a_string_from_spec_helper 18 | return "a string from spec_helper" 19 | end 20 | 21 | class MyClass 22 | def my_method 23 | @new_hash = {0 => "foo", 1 => "bar"} 24 | end 25 | 26 | MY_METHOD_BODY_LINE = __LINE__ - 3 27 | 28 | # This method allocates a different number of objects each call: 29 | # 1st call: 1x Array, 1x String 30 | # 2nd call; 2x Strings 31 | # 3rd call; 4x Strings 32 | # 4th call; 8x Strings 33 | def memoizing_method 34 | @c ||= [] 35 | 36 | (@c.size + 1).times { @c << "string" } 37 | end 38 | end 39 | 40 | # from rspec-core 2.14.7's spec_helper.rb: https://github.com/rspec/rspec-core/blob/v2.14.7/spec/spec_helper.rb#L31 41 | class NullObject 42 | private 43 | def method_missing(method, *args, &block) 44 | # ignore 45 | end 46 | end -------------------------------------------------------------------------------- /spec/trace_rspec_spec.rb: -------------------------------------------------------------------------------- 1 | # Copyright 2014 Google Inc. All Rights Reserved. 2 | # Licensed under the Apache License, Version 2.0, found in the LICENSE file. 3 | 4 | require_relative "spec_helper" 5 | 6 | describe "AllocationStats.trace_rspec" do 7 | before do 8 | AllocationStats.top_sites = [] 9 | end 10 | 11 | describe "top_sites" do 12 | before do 13 | @a = [["/foo.rb", 2, String], [:placeholder, :placeholder, :placeholder, :placeholder, :placeholder]] 14 | @b = [["/foo.rb", 3, Hash], [:placeholder, :placeholder, :placeholder]] 15 | @c = [["/foo.rb", 1, Array], [:placeholder, :placeholder]] 16 | @d = [["/foo.rb", 1, String], [:placeholder]] 17 | @e = [["/foo.rb", 4, String], [:placeholder, :placeholder, :placeholder, :placeholder, :placeholder, :placeholder]] 18 | @f = [["/foo.rb", 4, Array], [:placeholder]] 19 | 20 | @allocations01 = [@a, @b, @c, @d].to_h 21 | @allocations02 = [@b, @c, @e, @f].to_h 22 | 23 | @spec_location01 = "./spec/foo_spec.rb:3" 24 | @spec_location02 = "./spec/foo_spec.rb:7" 25 | end 26 | 27 | it "adds allocation groups to the top allocation points, limiting each set of allocations" do 28 | AllocationStats.add_to_top_sites(@allocations01, @spec_location01, 3) 29 | 30 | expect(AllocationStats.top_sites.size).to be(3) 31 | 32 | expect(AllocationStats.top_sites[0][:key]).to eq(@a.first) 33 | expect(AllocationStats.top_sites[0][:counts]).to eq([[@spec_location01, 5]]) 34 | 35 | expect(AllocationStats.top_sites[1][:key]).to eq(@b.first) 36 | expect(AllocationStats.top_sites[1][:counts]).to eq([[@spec_location01, 3]]) 37 | 38 | expect(AllocationStats.top_sites[2][:key]).to eq(@c.first) 39 | expect(AllocationStats.top_sites[2][:counts]).to eq([[@spec_location01, 2]]) 40 | end 41 | 42 | it "adds allocation groups to the top allocation points, organizing when too many" do 43 | AllocationStats.add_to_top_sites(@allocations01, @spec_location01, 5) 44 | AllocationStats.add_to_top_sites(@allocations02, @spec_location02, 5) 45 | 46 | expect(AllocationStats.top_sites.size).to be(5) 47 | 48 | expect(AllocationStats.top_sites[0][:key]).to eq(@e.first) 49 | expect(AllocationStats.top_sites[0][:counts]).to eq([[@spec_location02, 6]]) 50 | 51 | expect(AllocationStats.top_sites[1][:key]).to eq(@a.first) 52 | expect(AllocationStats.top_sites[1][:counts]).to eq([[@spec_location01, 5]]) 53 | 54 | expect(AllocationStats.top_sites[2][:key]).to eq(@b.first) 55 | expect(AllocationStats.top_sites[2][:counts]).to eq([[@spec_location01, 3], [@spec_location02, 3]]) 56 | 57 | expect(AllocationStats.top_sites[3][:key]).to eq(@c.first) 58 | expect(AllocationStats.top_sites[3][:counts]).to eq([[@spec_location01, 2], [@spec_location02, 2]]) 59 | end 60 | end 61 | 62 | describe "top_sites_text" do 63 | let(:example_group) do 64 | RSpec::Core::ExampleGroup.describe("group description") do 65 | around(&AllocationStats::TRACE_RSPEC_HOOK) 66 | end 67 | end 68 | 69 | it "prints top allocation sites after rspecs have run" do 70 | example_group.example do 71 | expect(["abc", "def", "ghi"]).to include("abc") 72 | end 73 | 74 | line = __LINE__ - 4 75 | example_group.run 76 | output = AllocationStats.top_sites_text 77 | 78 | expect(output).to include("Top 2 allocation sites:\n") 79 | expect(output).to include(" String allocations at /spec/trace_rspec_spec.rb:#{line+1}\n") 80 | expect(output).to include(" 4 allocations during ./spec/trace_rspec_spec.rb:#{line}\n") 81 | end 82 | end 83 | end 84 | --------------------------------------------------------------------------------