allocation_stats 0.1.4 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 67f90cfc4c5d04fb2be23255b4194c037a506ef4
4
- data.tar.gz: 35c20f3a4e115f097f079c9f2c71e60eeadbf49a
3
+ metadata.gz: e58e350537667c95ba3ae3893f1ce695dd35276e
4
+ data.tar.gz: c7d1b50337b122cb95776005d6549bd453be4746
5
5
  SHA512:
6
- metadata.gz: 1a7d4b6a68b4ef81ba559f87b1cf2eeca88548f294a06f53423e2839049cb5c609e23e956665a1025057c07d66d5604619f4aacd041f4b13d4a174f82febd169
7
- data.tar.gz: 17d1f24af19d8668461b4d1d31bf7b24ac84b18986c26f3566056524e1bf1affdf5d7061cdd40e2ccfdae126661a02cc96c9dc6bc54457f7de47c873e126a7ea
6
+ metadata.gz: 2dbd708b65f3ff8187f3940239fae01aaf23f1047cd644adbfe6c40c073b6778ec34b6813fb6c4480330ef4b379a3e5e848b810f6e5c76e2b2733610d2e49ff8
7
+ data.tar.gz: be7d0661887495448fcce247692c5f32940e42172ffe343e6d52c77fc1ebdb88d51c8e973991e87e4d4d577928cd5b0ddafe6bcc441f7ff81ab837d87461b61e
@@ -1,3 +1,11 @@
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
+
1
9
  v0.1.4
2
10
 
3
11
  * Added: Build status now tracked with Travis
@@ -5,6 +13,8 @@ v0.1.4
5
13
  * Fixed: alias order changed so that PWD is searched after GEMDIR and
6
14
  RUBYLIBDIR, in case of vendored bundler directory.
7
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
8
18
 
9
19
  v0.1.3
10
20
 
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2013 Google Inc. All Rights Reserved.
1
+ # Copyright 2014 Google Inc. All Rights Reserved.
2
2
  # Licensed under the Apache License, Version 2.0, found in the LICENSE file.
3
3
 
4
4
  source "https://rubygems.org"
@@ -2,7 +2,7 @@ GEM
2
2
  remote: https://rubygems.org/
3
3
  specs:
4
4
  coderay (1.0.9)
5
- diff-lcs (1.2.4)
5
+ diff-lcs (1.2.5)
6
6
  docile (1.1.1)
7
7
  method_source (0.8.2)
8
8
  multi_json (1.8.2)
@@ -11,14 +11,14 @@ GEM
11
11
  method_source (~> 0.8)
12
12
  slop (~> 3.4)
13
13
  rake (10.1.0)
14
- rspec (2.13.0)
15
- rspec-core (~> 2.13.0)
16
- rspec-expectations (~> 2.13.0)
17
- rspec-mocks (~> 2.13.0)
18
- rspec-core (2.13.1)
19
- rspec-expectations (2.13.0)
14
+ rspec (2.14.1)
15
+ rspec-core (~> 2.14.0)
16
+ rspec-expectations (~> 2.14.0)
17
+ rspec-mocks (~> 2.14.0)
18
+ rspec-core (2.14.7)
19
+ rspec-expectations (2.14.5)
20
20
  diff-lcs (>= 1.1.3, < 2.0)
21
- rspec-mocks (2.13.1)
21
+ rspec-mocks (2.14.6)
22
22
  simplecov (0.8.2)
23
23
  docile (~> 1.1.0)
24
24
  multi_json
@@ -1,6 +1,25 @@
1
1
  AllocationStats [![Build Status](https://travis-ci.org/srawlins/allocation_stats.png?branch=master)](https://travis-ci.org/srawlins/allocation_stats)
2
2
  ===============
3
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
+
4
23
  Introduction
5
24
  ------------
6
25
 
@@ -28,7 +47,7 @@ or run the following command:
28
47
  gem install allocation_stats
29
48
  ```
30
49
 
31
- ### Tabular Output examples
50
+ ### Tabular output examples
32
51
 
33
52
  It is very easy to get some simple statistics out of AllocationStats.
34
53
  Wrap some code with `AllocationStats.trace` and print out a listing of all of the
@@ -114,6 +133,191 @@ allocations.rb
114
133
  4
115
134
  ```
116
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 <PWD>/strings.rb:2
263
+ during ./strings_spec.rb:8
264
+ 2 allocations of Array at <PWD>/strings_spec.rb:9
265
+ during ./strings_spec.rb:8
266
+ 2 allocations of Array at <PWD>/strings_spec.rb:13
267
+ during ./strings_spec.rb:12
268
+ 1 allocations of Array at <PWD>/strings.rb:2
269
+ during ./strings_spec.rb:8
270
+ 1 allocations of String at <PWD>/strings.rb:6
271
+ during ./strings_spec.rb:8
272
+ 1 allocations of String at <PWD>/strings.rb:14
273
+ during ./strings_spec.rb:12
274
+ 1 allocations of String at <PWD>/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
+
117
321
  ### Example from the specs
118
322
 
119
323
  ```ruby
@@ -212,7 +416,7 @@ allocation information, accessible via `#allocations`. Let's look at the next
212
416
  line to see how we can pull useful information out:
213
417
 
214
418
  ```ruby
215
- results = stats.allocations.group_by(:@sourcefile, :class).to_a
419
+ results = stats.allocations.group_by(:@sourcefile, :class).to_a
216
420
  ```
217
421
 
218
422
  If you are used to chaining ActiveRecord relations, some of this might look
@@ -324,57 +528,6 @@ puts stats.allocations(alias_paths: true).group_by(:sourcefile, :class).to_text
324
528
  <RUBYLIBDIR>/psych/scalar_scanner.rb MatchData 2
325
529
  ```
326
530
 
327
- ### Burn One
328
-
329
- If you find a lot of allocations in `kernel_require.rb` or a lot of allocations
330
- of `RubyVM::InstructuinSequences`, you can "burn" one or more calls to your
331
- block with the `burn` keyword. For example:
332
- `AllocationStats.new(burn: 3).trace{ ... }` will first call the block 3 times,
333
- without tracing allocations, before calling the block a 4th time, tracing
334
- allocations.
335
-
336
- The API
337
- -------
338
-
339
- So what methods are available on that AllocationsProxy thing? So far, the API
340
- consists of:
341
-
342
- * `#group_by`
343
- * `#from` takes one String argument, which will matched against the
344
- allocation filename.
345
- * `#not_from` is the opposite of `#from`.
346
- * `#from_pwd` will filter the allocations down to those originating from `pwd`
347
- (e.g. allocations originating from "my project")
348
- * `#where` accepts a hash of faux attribute keys. For example,
349
-
350
- ```ruby
351
- allocations.where(class: String)
352
- ```
353
-
354
- It does not yet accept lambdas as values, which _would_ enable
355
- ActiveRecord-4-like calls, like
356
-
357
- ```ruby
358
- allocations.where(class: Array, size: ->(size) { size > 10 }
359
- ```
360
- * `#at_least(n)` selects allocation groups with at least `n` allocations per group.
361
- * `#bytes`, which has an inconsistent definition, I think... TODO
362
-
363
- ### What are faux attributes?
364
-
365
- Valid values for `#group_by` and `#where` include:
366
- * instance variables on each `Allocation`. These include `:sourcefile`,
367
- `:sourceline`, etc.
368
- * methods available on the objects that were allocated. These include things
369
- like `:class`, or even `:size` if you know you only have objects that respond
370
- to `:size`.
371
- * Allocation helper methods that return something special about the allocated
372
- object. Right now this just includes `:class_plus`.
373
-
374
- I'm calling these things that you can group by or filter by, "faux attributes."
375
-
376
- ### What is `class_plus`?
377
-
378
531
  References
379
532
  ----------
380
533
 
data/Rakefile CHANGED
@@ -1,4 +1,4 @@
1
- # Copyright 2013 Google Inc. All Rights Reserved.
1
+ # Copyright 2014 Google Inc. All Rights Reserved.
2
2
  # Licensed under the Apache License, Version 2.0, found in the LICENSE file.
3
3
 
4
4
  require 'rspec/core/rake_task'
data/TODO CHANGED
@@ -1,2 +1,3 @@
1
1
  * more in the README
2
2
  * binary
3
+ * trace minitest
@@ -1,9 +1,9 @@
1
- # Copyright 2013 Google Inc. All Rights Reserved.
1
+ # Copyright 2014 Google Inc. All Rights Reserved.
2
2
  # Licensed under the Apache License, Version 2.0, found in the LICENSE file.
3
3
 
4
4
  Gem::Specification.new do |spec|
5
5
  spec.name = "allocation_stats"
6
- spec.version = "0.1.4"
6
+ spec.version = "0.1.5"
7
7
  spec.authors = ["Sam Rawlins"]
8
8
  spec.email = ["sam.rawlins@gmail.com"]
9
9
  spec.homepage = "https://github.com/srawlins/allocation_stats"
@@ -0,0 +1,15 @@
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
@@ -0,0 +1,15 @@
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
@@ -1,10 +1,11 @@
1
- # Copyright 2013 Google Inc. All Rights Reserved.
1
+ # Copyright 2014 Google Inc. All Rights Reserved.
2
2
  # Licensed under the Apache License, Version 2.0, found in the LICENSE file.
3
3
 
4
4
  require "objspace"
5
5
  require_relative "allocation_stats/core_ext/basic_object"
6
6
  require_relative "allocation_stats/allocation"
7
7
  require_relative "allocation_stats/allocations_proxy"
8
+ require_relative "allocation_stats/trace_rspec"
8
9
 
9
10
  require "rubygems"
10
11
 
@@ -1,4 +1,4 @@
1
- # Copyright 2013 Google Inc. All Rights Reserved.
1
+ # Copyright 2014 Google Inc. All Rights Reserved.
2
2
  # Licensed under the Apache License, Version 2.0, found in the LICENSE file.
3
3
 
4
4
  require "json"
@@ -9,7 +9,8 @@ class AllocationStats
9
9
  # a convenience constants
10
10
  PWD = Dir.pwd
11
11
 
12
- # a list of helper methods that Allocation provides on top of the object that was allocated.
12
+ # a list of helper methods that Allocation provides on top of the object
13
+ # that was allocated.
13
14
  HELPERS = [:class_plus, :gem]
14
15
 
15
16
  # a list of attributes that Allocation has on itself; inquiries in this
@@ -46,14 +47,17 @@ class AllocationStats
46
47
  @method_id = ObjectSpace.allocation_method_id(object)
47
48
  end
48
49
 
50
+ # the sourcefile where the object was allocated
49
51
  def file; @sourcefile; end
50
- #def line; @sourceline; end
52
+
51
53
  alias :line :sourceline
52
54
 
53
- # If the source file has recognized paths in it, those portions of the full path will be aliased like so:
55
+ # If the source file has recognized paths in it, those portions of the full
56
+ # path will be aliased like so:
54
57
  #
55
58
  # * the present work directory is aliased to "<PWD>"
56
- # * the Ruby lib directory (where the standard library lies) is aliased to "<RUBYLIBDIR>"
59
+ # * the Ruby lib directory (where the standard library lies) is aliased to
60
+ # "<RUBYLIBDIR>"
57
61
  # * the Gem directory (where all gems lie) is aliased to "<GEMDIR>"
58
62
  #
59
63
  # @return the source file, aliased.
@@ -102,10 +106,10 @@ class AllocationStats
102
106
  end
103
107
  end
104
108
 
109
+ # Override Rubygems' Kernel#gem
110
+ #
105
111
  # @return [String] the name of the Rubygem where this allocation occurred.
106
112
  # @return [nil] if this allocation did not occur in a Rubygem.
107
- #
108
- # Override Rubygems' Kernel#gem
109
113
  def gem
110
114
  gem_regex = /#{AllocationStats::GEMDIR}#{File::SEPARATOR}
111
115
  gems#{File::SEPARATOR}
@@ -136,6 +140,11 @@ class AllocationStats
136
140
  as_json.to_json(*a)
137
141
  end
138
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
139
148
  def element_classes(classes)
140
149
  if classes.size == 1
141
150
  classes.first
@@ -1,4 +1,4 @@
1
- # Copyright 2013 Google Inc. All Rights Reserved.
1
+ # Copyright 2014 Google Inc. All Rights Reserved.
2
2
  # Licensed under the Apache License, Version 2.0, found in the LICENSE file.
3
3
 
4
4
  class AllocationStats
@@ -84,6 +84,7 @@ class AllocationStats
84
84
  return self
85
85
  end
86
86
 
87
+ # Sort allocation groups by the number of allocations in each group.
87
88
  def sort_by_size
88
89
  @mappers << Proc.new do |allocations|
89
90
  allocations.sort_by { |key, value| -value.size }
@@ -174,12 +175,18 @@ class AllocationStats
174
175
  self
175
176
  end
176
177
 
177
- def where(hash)
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)
178
185
  @wheres << Proc.new do |allocations|
179
- conditions = hash.inject({}) do |h, pair|
186
+ conditions = conditions.inject({}) do |memo, pair|
180
187
  faux, value = *pair
181
188
  getter = attribute_getters([faux]).first
182
- h.merge(getter => value)
189
+ memo.merge(getter => value)
183
190
  end
184
191
 
185
192
  allocations.select do |allocation|
@@ -0,0 +1,118 @@
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
+ # TODO s/false/some config option/
23
+ if true # wrap loosely
24
+ stats = AllocationStats.new(burn: 1).trace { example.run }
25
+ else # wrap tightly
26
+ # Super hacky, but presumably more correct results?
27
+ stats = AllocationStats.new(burn: 1)
28
+ example_block = @example.instance_variable_get(:@example_block).clone
29
+
30
+ @example.instance_variable_set(
31
+ :@example_block,
32
+ Proc.new do
33
+ stats.trace { example_block.call }
34
+ end
35
+ )
36
+
37
+ example.run
38
+ end
39
+
40
+ allocations = stats.allocations(alias_paths: true).
41
+ not_from("rspec-core").not_from("rspec-expectations").not_from("rspec-mocks").
42
+ group_by(:sourcefile, :sourceline, :class).
43
+ sort_by_count
44
+
45
+ AllocationStats.add_to_top_sites(allocations.all, @example.location)
46
+ end
47
+
48
+ # Read the sorted list of the top "sites", that is, top file/line/class
49
+ # groups, encountered while tracing RSpec.
50
+ #
51
+ # @api private
52
+ def self.top_sites
53
+ @top_sites
54
+ end
55
+
56
+ # Write to the sorted list of the top "sites", that is, top file/line/class
57
+ # groups, encountered while tracing RSpec.
58
+ #
59
+ # @api private
60
+ def self.top_sites=(value)
61
+ @top_sites = value
62
+ end
63
+
64
+ # Add a Hash of allocation groups (derived from an
65
+ # `AllocationStats.allocations...group_by(...)`) to the top allocation sites
66
+ # (file/line/class groups).
67
+ #
68
+ # @param [Hash] allocations
69
+ # @param [String] location the RSpec spec location that was being executed
70
+ # when the allocations occurred
71
+ # @param [Fixnum] limit size of the top sites Array
72
+ def self.add_to_top_sites(allocations, location, limit = 10)
73
+ if allocations.size > limit
74
+ allocations = allocations.to_a[0...limit].to_h # top 10 or so
75
+ end
76
+
77
+ # TODO: not a great algorithm so far... can instead:
78
+ # * oly insert when an allocation won't be immediately dropped
79
+ # * insert into correct position and pop rather than sort and slice
80
+ allocations.each do |k,v|
81
+ next if k[0] =~ /spec_helper\.rb$/
82
+
83
+ if site = @top_sites.detect { |s| s[:key] == k }
84
+ if lower_idx = site[:counts].index { |loc, count| count < v.size }
85
+ site[:counts].insert(lower_idx, [location, v.size])
86
+ else
87
+ site[:counts] << [location, v.size]
88
+ end
89
+ site[:counts].pop if site[:counts].size > 3
90
+ else
91
+ @top_sites << { key: k, counts: [[location, v.size]] }
92
+ end
93
+ end
94
+
95
+ @top_sites = @top_sites.sort_by! { |site|
96
+ -site[:counts].map(&:last).max
97
+ }[0...limit]
98
+ end
99
+
100
+ # Textual String representing the sorted list of the top allocation sites.
101
+ # For each site, this String includes the number of allocations, the class,
102
+ # the sourcefile, the sourceline, and the location of the RSpec spec.
103
+ #
104
+ # @api private
105
+ def self.top_sites_text
106
+ return "" if @top_sites.empty?
107
+
108
+ result = "Top #{@top_sites.size} allocation sites:\n"
109
+ @top_sites.each do |site|
110
+ result << " %s allocations at %s:%d\n" % [site[:key][2], site[:key][0], site[:key][1]]
111
+ site[:counts].each do |location, count|
112
+ result << " %3d allocations during %s\n" % [count, location]
113
+ end
114
+ end
115
+
116
+ result
117
+ end
118
+ end
@@ -1,4 +1,4 @@
1
- # Copyright 2013 Google Inc. All Rights Reserved.
1
+ # Copyright 2014 Google Inc. All Rights Reserved.
2
2
  # Licensed under the Apache License, Version 2.0, found in the LICENSE file.
3
3
 
4
4
  require_relative File.join("..", "spec_helper")
@@ -167,7 +167,7 @@ describe AllocationStats::AllocationsProxy do
167
167
  results = stats.allocations.from_pwd.group_by(:class).all
168
168
  results.keys.size.should == 3
169
169
  results[[String]].size.should == 6
170
- results[[Array]].size.should == 3 # one for empty *args in YAML.dump
170
+ results[[Array]].size.should == 2
171
171
  results[[Range]].size.should == 1
172
172
  end
173
173
 
@@ -247,7 +247,7 @@ describe AllocationStats::AllocationsProxy do
247
247
  files.should_not include("<GEMDIR>/gems/yajl-ruby-1.1.0/lib/yajl.rb")
248
248
  end
249
249
 
250
- it "should be able to filter to just one path" do
250
+ it "should be able to filter to just one class" do
251
251
  stats = AllocationStats.trace do
252
252
  j = Yajl.dump(["one string", "two string"]) # lots of objects from Rbconfig::CONFIG["rubylibdir"]
253
253
  end
@@ -1,4 +1,4 @@
1
- # Copyright 2013 Google Inc. All Rights Reserved.
1
+ # Copyright 2014 Google Inc. All Rights Reserved.
2
2
  # Licensed under the Apache License, Version 2.0, found in the LICENSE file.
3
3
 
4
4
  require_relative File.join("spec_helper")
@@ -1,4 +1,4 @@
1
- # Copyright 2013 Google Inc. All Rights Reserved.
1
+ # Copyright 2014 Google Inc. All Rights Reserved.
2
2
  # Licensed under the Apache License, Version 2.0, found in the LICENSE file.
3
3
 
4
4
  require "simplecov"
@@ -36,3 +36,152 @@ class MyClass
36
36
  (@c.size + 1).times { @c << "string" }
37
37
  end
38
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
47
+
48
+ # from rspec-core 2.14.7's spec_helper.rb: https://github.com/rspec/rspec-core/blob/v2.14.7/spec/spec_helper.rb#L38
49
+ #
50
+ # THIS WILL NEED TO BE ENTIRELY REPLACED WHEN BUMPING TO RSPEC 3
51
+ module Sandboxing
52
+ def self.sandboxed(&block)
53
+ @orig_config = RSpec.configuration
54
+ @orig_world = RSpec.world
55
+ new_config = RSpec::Core::Configuration.new
56
+ new_world = RSpec::Core::World.new(new_config)
57
+ RSpec.configuration = new_config
58
+ RSpec.world = new_world
59
+ object = Object.new
60
+ object.extend(RSpec::Core::SharedExampleGroup)
61
+
62
+ (class << RSpec::Core::ExampleGroup; self; end).class_eval do
63
+ alias_method :orig_run, :run
64
+ def run(reporter=nil)
65
+ orig_run(reporter || NullObject.new)
66
+ end
67
+ end
68
+
69
+ RSpec::Core::SandboxedMockSpace.sandboxed do
70
+ object.instance_eval(&block)
71
+ end
72
+ ensure
73
+ (class << RSpec::Core::ExampleGroup; self; end).class_eval do
74
+ remove_method :run
75
+ alias_method :run, :orig_run
76
+ remove_method :orig_run
77
+ end
78
+
79
+ RSpec.configuration = @orig_config
80
+ RSpec.world = @orig_world
81
+ end
82
+ end
83
+
84
+ ############
85
+ # from https://raw.github.com/rspec/rspec-core/v2.14.7/spec/support/sandboxed_mock_space.rb
86
+ #
87
+ # THIS WILL NEED TO BE ENTIRELY DELETED WHEN BUMPING TO RSPEC 3
88
+ ############
89
+ require 'rspec/mocks'
90
+
91
+ module RSpec
92
+ module Core
93
+ # Because rspec-core dog-foods itself, rspec-core's spec suite has
94
+ # examples that define example groups and examples and run them. The
95
+ # usual lifetime of an RSpec::Mocks::Proxy is for one example
96
+ # (the proxy cache gets cleared between each example), but since the
97
+ # specs in rspec-core's suite sometimes create test doubles and pass
98
+ # them to examples a spec defines and runs, the test double's proxy
99
+ # must live beyond the inner example: it must live for the scope
100
+ # of wherever it got defined. Here we implement the necessary semantics
101
+ # for rspec-core's specs:
102
+ #
103
+ # - #verify_all and #reset_all affect only mocks that were created
104
+ # within the current scope.
105
+ # - Mock proxies live for the duration of the scope in which they are
106
+ # created.
107
+ #
108
+ # Thus, mock proxies created in an inner example live for only that
109
+ # example, but mock proxies created in an outer example can be used
110
+ # in an inner example but will only be reset/verified when the outer
111
+ # example completes.
112
+ class SandboxedMockSpace < ::RSpec::Mocks::Space
113
+ def self.sandboxed
114
+ orig_space = RSpec::Mocks.space
115
+ RSpec::Mocks.space = RSpec::Core::SandboxedMockSpace.new
116
+
117
+ RSpec::Core::Example.class_eval do
118
+ alias_method :orig_run, :run
119
+ def run(*args)
120
+ RSpec::Mocks.space.sandboxed do
121
+ orig_run(*args)
122
+ end
123
+ end
124
+ end
125
+
126
+ yield
127
+ ensure
128
+ RSpec::Core::Example.class_eval do
129
+ remove_method :run
130
+ alias_method :run, :orig_run
131
+ remove_method :orig_run
132
+ end
133
+
134
+ RSpec::Mocks.space = orig_space
135
+ end
136
+
137
+ class Sandbox
138
+ attr_reader :proxies
139
+
140
+ def initialize
141
+ @proxies = Set.new
142
+ end
143
+
144
+ def verify_all
145
+ @proxies.each { |p| p.verify }
146
+ end
147
+
148
+ def reset_all
149
+ @proxies.each { |p| p.reset }
150
+ end
151
+ end
152
+
153
+ def initialize
154
+ @sandbox_stack = []
155
+ super
156
+ end
157
+
158
+ def sandboxed
159
+ @sandbox_stack << Sandbox.new
160
+ yield
161
+ ensure
162
+ @sandbox_stack.pop
163
+ end
164
+
165
+ def verify_all
166
+ return super unless sandbox = @sandbox_stack.last
167
+ sandbox.verify_all
168
+ end
169
+
170
+ def reset_all
171
+ return super unless sandbox = @sandbox_stack.last
172
+ sandbox.reset_all
173
+ end
174
+
175
+ def proxy_for(object)
176
+ new_proxy = !proxies.has_key?(object.__id__)
177
+ proxy = super
178
+
179
+ if new_proxy && sandbox = @sandbox_stack.last
180
+ sandbox.proxies << proxy
181
+ end
182
+
183
+ proxy
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,89 @@
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
+ RSpec.configure do |config|
7
+ config.around {|example| Sandboxing.sandboxed { example.run }}
8
+ end
9
+
10
+ describe "AllocationStats.trace_rspec" do
11
+ before do
12
+ AllocationStats.top_sites = []
13
+ end
14
+
15
+ describe "top_sites" do
16
+ before do
17
+ @a = [["<PWD>/foo.rb", 2, String], [:placeholder, :placeholder, :placeholder, :placeholder, :placeholder]]
18
+ @b = [["<PWD>/foo.rb", 3, Hash], [:placeholder, :placeholder, :placeholder]]
19
+ @c = [["<PWD>/foo.rb", 1, Array], [:placeholder, :placeholder]]
20
+ @d = [["<PWD>/foo.rb", 1, String], [:placeholder]]
21
+ @e = [["<PWD>/foo.rb", 4, String], [:placeholder, :placeholder, :placeholder, :placeholder, :placeholder, :placeholder]]
22
+ @f = [["<PWD>/foo.rb", 4, Array], [:placeholder]]
23
+
24
+ @allocations01 = [@a, @b, @c, @d].to_h
25
+ @allocations02 = [@b, @c, @e, @f].to_h
26
+
27
+ @spec_location01 = "./spec/foo_spec.rb:3"
28
+ @spec_location02 = "./spec/foo_spec.rb:7"
29
+ end
30
+
31
+ it "adds allocation groups to the top allocation points, limiting each set of allocations" do
32
+ AllocationStats.add_to_top_sites(@allocations01, @spec_location01, 3)
33
+
34
+ expect(AllocationStats.top_sites.size).to be(3)
35
+
36
+ expect(AllocationStats.top_sites[0][:key]).to eq(@a.first)
37
+ expect(AllocationStats.top_sites[0][:counts]).to eq([[@spec_location01, 5]])
38
+
39
+ expect(AllocationStats.top_sites[1][:key]).to eq(@b.first)
40
+ expect(AllocationStats.top_sites[1][:counts]).to eq([[@spec_location01, 3]])
41
+
42
+ expect(AllocationStats.top_sites[2][:key]).to eq(@c.first)
43
+ expect(AllocationStats.top_sites[2][:counts]).to eq([[@spec_location01, 2]])
44
+ end
45
+
46
+ it "adds allocation groups to the top allocation points, organizing when too many" do
47
+ AllocationStats.add_to_top_sites(@allocations01, @spec_location01, 5)
48
+ AllocationStats.add_to_top_sites(@allocations02, @spec_location02, 5)
49
+
50
+ expect(AllocationStats.top_sites.size).to be(5)
51
+
52
+ expect(AllocationStats.top_sites[0][:key]).to eq(@e.first)
53
+ expect(AllocationStats.top_sites[0][:counts]).to eq([[@spec_location02, 6]])
54
+
55
+ expect(AllocationStats.top_sites[1][:key]).to eq(@a.first)
56
+ expect(AllocationStats.top_sites[1][:counts]).to eq([[@spec_location01, 5]])
57
+
58
+ expect(AllocationStats.top_sites[2][:key]).to eq(@b.first)
59
+ expect(AllocationStats.top_sites[2][:counts]).to eq([[@spec_location01, 3], [@spec_location02, 3]])
60
+
61
+ expect(AllocationStats.top_sites[3][:key]).to eq(@c.first)
62
+ expect(AllocationStats.top_sites[3][:counts]).to eq([[@spec_location01, 2], [@spec_location02, 2]])
63
+ end
64
+ end
65
+
66
+ describe "top_sites_text" do
67
+ let(:example_group) do
68
+ RSpec::Core::ExampleGroup.describe("group description") do
69
+ around(&AllocationStats::TRACE_RSPEC_HOOK)
70
+ end
71
+ end
72
+
73
+ it "prints top allocation sites after rspecs have run" do
74
+ example_group.example do
75
+ expect(["abc", "def", "ghi"]).to include("abc")
76
+ end
77
+
78
+ line = __LINE__ - 4
79
+ example_group.run
80
+ output = AllocationStats.top_sites_text
81
+
82
+ expect(output).to include("Top 2 allocation sites:\n")
83
+ expect(output).to include(" String allocations at <PWD>/spec/trace_rspec_spec.rb:#{line+1}\n")
84
+ expect(output).to include(" 4 allocations during ./spec/trace_rspec_spec.rb:#{line}\n")
85
+ expect(output).to include(" Array allocations at <PWD>/spec/trace_rspec_spec.rb:#{line+1}\n")
86
+ expect(output).to include(" 3 allocations during ./spec/trace_rspec_spec.rb:#{line}\n")
87
+ end
88
+ end
89
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: allocation_stats
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sam Rawlins
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-02-07 00:00:00.000000000 Z
11
+ date: 2014-07-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec
@@ -48,14 +48,17 @@ files:
48
48
  - examples/trace_object_allocations.rb
49
49
  - examples/trace_psych_group_by.rb
50
50
  - examples/trace_psych_keys.rb
51
- - lib/active_support/core_ext/module/delegation.rb
51
+ - examples/trace_specs/strings.rb
52
+ - examples/trace_specs/strings_spec.rb
52
53
  - lib/allocation_stats.rb
53
54
  - lib/allocation_stats/allocation.rb
54
55
  - lib/allocation_stats/allocations_proxy.rb
55
56
  - lib/allocation_stats/core_ext/basic_object.rb
57
+ - lib/allocation_stats/trace_rspec.rb
56
58
  - spec/allocation_stats/allocations_proxy_spec.rb
57
59
  - spec/allocation_stats_spec.rb
58
60
  - spec/spec_helper.rb
61
+ - spec/trace_rspec_spec.rb
59
62
  homepage: https://github.com/srawlins/allocation_stats
60
63
  licenses:
61
64
  - Apache v2
@@ -76,7 +79,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
76
79
  version: '0'
77
80
  requirements: []
78
81
  rubyforge_project:
79
- rubygems_version: 2.2.0
82
+ rubygems_version: 2.2.2
80
83
  signing_key:
81
84
  specification_version: 4
82
85
  summary: Tooling for tracing object allocations in Ruby 2.1
@@ -1,203 +0,0 @@
1
- # This file is part of Ruby on Rails (http://rubyonrails.org/) (original
2
- # location: https://github.com/rails/rails/raw/v4.0.0/activesupport/lib/active_support/core_ext/module/delegation.rb)
3
- #
4
- # Ruby on Rails is released under the MIT License (http://www.opensource.org/licenses/MIT).
5
- #
6
- # "Ruby on Rails" is a registered trademark of David Heinemeier Hansson.
7
-
8
- class Module
9
- # Provides a +delegate+ class method to easily expose contained objects'
10
- # public methods as your own.
11
- #
12
- # The macro receives one or more method names (specified as symbols or
13
- # strings) and the name of the target object via the <tt>:to</tt> option
14
- # (also a symbol or string).
15
- #
16
- # Delegation is particularly useful with Active Record associations:
17
- #
18
- # class Greeter < ActiveRecord::Base
19
- # def hello
20
- # 'hello'
21
- # end
22
- #
23
- # def goodbye
24
- # 'goodbye'
25
- # end
26
- # end
27
- #
28
- # class Foo < ActiveRecord::Base
29
- # belongs_to :greeter
30
- # delegate :hello, to: :greeter
31
- # end
32
- #
33
- # Foo.new.hello # => "hello"
34
- # Foo.new.goodbye # => NoMethodError: undefined method `goodbye' for #<Foo:0x1af30c>
35
- #
36
- # Multiple delegates to the same target are allowed:
37
- #
38
- # class Foo < ActiveRecord::Base
39
- # belongs_to :greeter
40
- # delegate :hello, :goodbye, to: :greeter
41
- # end
42
- #
43
- # Foo.new.goodbye # => "goodbye"
44
- #
45
- # Methods can be delegated to instance variables, class variables, or constants
46
- # by providing them as a symbols:
47
- #
48
- # class Foo
49
- # CONSTANT_ARRAY = [0,1,2,3]
50
- # @@class_array = [4,5,6,7]
51
- #
52
- # def initialize
53
- # @instance_array = [8,9,10,11]
54
- # end
55
- # delegate :sum, to: :CONSTANT_ARRAY
56
- # delegate :min, to: :@@class_array
57
- # delegate :max, to: :@instance_array
58
- # end
59
- #
60
- # Foo.new.sum # => 6
61
- # Foo.new.min # => 4
62
- # Foo.new.max # => 11
63
- #
64
- # It's also possible to delegate a method to the class by using +:class+:
65
- #
66
- # class Foo
67
- # def self.hello
68
- # "world"
69
- # end
70
- #
71
- # delegate :hello, to: :class
72
- # end
73
- #
74
- # Foo.new.hello # => "world"
75
- #
76
- # Delegates can optionally be prefixed using the <tt>:prefix</tt> option. If the value
77
- # is <tt>true</tt>, the delegate methods are prefixed with the name of the object being
78
- # delegated to.
79
- #
80
- # Person = Struct.new(:name, :address)
81
- #
82
- # class Invoice < Struct.new(:client)
83
- # delegate :name, :address, to: :client, prefix: true
84
- # end
85
- #
86
- # john_doe = Person.new('John Doe', 'Vimmersvej 13')
87
- # invoice = Invoice.new(john_doe)
88
- # invoice.client_name # => "John Doe"
89
- # invoice.client_address # => "Vimmersvej 13"
90
- #
91
- # It is also possible to supply a custom prefix.
92
- #
93
- # class Invoice < Struct.new(:client)
94
- # delegate :name, :address, to: :client, prefix: :customer
95
- # end
96
- #
97
- # invoice = Invoice.new(john_doe)
98
- # invoice.customer_name # => 'John Doe'
99
- # invoice.customer_address # => 'Vimmersvej 13'
100
- #
101
- # If the target is +nil+ and does not respond to the delegated method a
102
- # +NoMethodError+ is raised, as with any other value. Sometimes, however, it
103
- # makes sense to be robust to that situation and that is the purpose of the
104
- # <tt>:allow_nil</tt> option: If the target is not +nil+, or it is and
105
- # responds to the method, everything works as usual. But if it is +nil+ and
106
- # does not respond to the delegated method, +nil+ is returned.
107
- #
108
- # class User < ActiveRecord::Base
109
- # has_one :profile
110
- # delegate :age, to: :profile
111
- # end
112
- #
113
- # User.new.age # raises NoMethodError: undefined method `age'
114
- #
115
- # But if not having a profile yet is fine and should not be an error
116
- # condition:
117
- #
118
- # class User < ActiveRecord::Base
119
- # has_one :profile
120
- # delegate :age, to: :profile, allow_nil: true
121
- # end
122
- #
123
- # User.new.age # nil
124
- #
125
- # Note that if the target is not +nil+ then the call is attempted regardless of the
126
- # <tt>:allow_nil</tt> option, and thus an exception is still raised if said object
127
- # does not respond to the method:
128
- #
129
- # class Foo
130
- # def initialize(bar)
131
- # @bar = bar
132
- # end
133
- #
134
- # delegate :name, to: :@bar, allow_nil: true
135
- # end
136
- #
137
- # Foo.new("Bar").name # raises NoMethodError: undefined method `name'
138
- #
139
- def delegate(*methods)
140
- options = methods.pop
141
- unless options.is_a?(Hash) && to = options[:to]
142
- raise ArgumentError, 'Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, to: :greeter).'
143
- end
144
-
145
- prefix, allow_nil = options.values_at(:prefix, :allow_nil)
146
-
147
- if prefix == true && to =~ /^[^a-z_]/
148
- raise ArgumentError, 'Can only automatically set the delegation prefix when delegating to a method.'
149
- end
150
-
151
- method_prefix = \
152
- if prefix
153
- "#{prefix == true ? to : prefix}_"
154
- else
155
- ''
156
- end
157
-
158
- file, line = caller.first.split(':', 2)
159
- line = line.to_i
160
-
161
- to = to.to_s
162
- to = 'self.class' if to == 'class'
163
-
164
- methods.each do |method|
165
- # Attribute writer methods only accept one argument. Makes sure []=
166
- # methods still accept two arguments.
167
- definition = (method =~ /[^\]]=$/) ? 'arg' : '*args, &block'
168
-
169
- # The following generated methods call the target exactly once, storing
170
- # the returned value in a dummy variable.
171
- #
172
- # Reason is twofold: On one hand doing less calls is in general better.
173
- # On the other hand it could be that the target has side-effects,
174
- # whereas conceptualy, from the user point of view, the delegator should
175
- # be doing one call.
176
- if allow_nil
177
- module_eval(<<-EOS, file, line - 3)
178
- def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block)
179
- _ = #{to} # _ = client
180
- if !_.nil? || nil.respond_to?(:#{method}) # if !_.nil? || nil.respond_to?(:name)
181
- _.#{method}(#{definition}) # _.name(*args, &block)
182
- end # end
183
- end # end
184
- EOS
185
- else
186
- exception = %(raise "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")
187
-
188
- module_eval(<<-EOS, file, line - 2)
189
- def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block)
190
- _ = #{to} # _ = client
191
- _.#{method}(#{definition}) # _.name(*args, &block)
192
- rescue NoMethodError # rescue NoMethodError
193
- if _.nil? # if _.nil?
194
- #{exception} # # add helpful message to the exception
195
- else # else
196
- raise # raise
197
- end # end
198
- end # end
199
- EOS
200
- end
201
- end
202
- end
203
- end