allocation_stats 0.1.4 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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