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 +4 -4
- data/CHANGELOG.markdown +10 -0
- data/Gemfile +1 -1
- data/Gemfile.lock +8 -8
- data/README.markdown +206 -53
- data/Rakefile +1 -1
- data/TODO +1 -0
- data/allocation_stats.gemspec +2 -2
- data/examples/trace_specs/strings.rb +15 -0
- data/examples/trace_specs/strings_spec.rb +15 -0
- data/lib/allocation_stats.rb +2 -1
- data/lib/allocation_stats/allocation.rb +16 -7
- data/lib/allocation_stats/allocations_proxy.rb +11 -4
- data/lib/allocation_stats/trace_rspec.rb +118 -0
- data/spec/allocation_stats/allocations_proxy_spec.rb +3 -3
- data/spec/allocation_stats_spec.rb +1 -1
- data/spec/spec_helper.rb +150 -1
- data/spec/trace_rspec_spec.rb +89 -0
- metadata +7 -4
- data/lib/active_support/core_ext/module/delegation.rb +0 -203
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e58e350537667c95ba3ae3893f1ce695dd35276e
|
4
|
+
data.tar.gz: c7d1b50337b122cb95776005d6549bd453be4746
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2dbd708b65f3ff8187f3940239fae01aaf23f1047cd644adbfe6c40c073b6778ec34b6813fb6c4480330ef4b379a3e5e848b810f6e5c76e2b2733610d2e49ff8
|
7
|
+
data.tar.gz: be7d0661887495448fcce247692c5f32940e42172ffe343e6d52c77fc1ebdb88d51c8e973991e87e4d4d577928cd5b0ddafe6bcc441f7ff81ab837d87461b61e
|
data/CHANGELOG.markdown
CHANGED
@@ -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
data/Gemfile.lock
CHANGED
@@ -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.
|
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.
|
15
|
-
rspec-core (~> 2.
|
16
|
-
rspec-expectations (~> 2.
|
17
|
-
rspec-mocks (~> 2.
|
18
|
-
rspec-core (2.
|
19
|
-
rspec-expectations (2.
|
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.
|
21
|
+
rspec-mocks (2.14.6)
|
22
22
|
simplecov (0.8.2)
|
23
23
|
docile (~> 1.1.0)
|
24
24
|
multi_json
|
data/README.markdown
CHANGED
@@ -1,6 +1,25 @@
|
|
1
1
|
AllocationStats [](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
|
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
|
-
|
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
data/allocation_stats.gemspec
CHANGED
@@ -1,9 +1,9 @@
|
|
1
|
-
# Copyright
|
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.
|
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
|
+
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
|
data/lib/allocation_stats.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
|
-
# Copyright
|
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
|
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
|
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
|
-
|
52
|
+
|
51
53
|
alias :line :sourceline
|
52
54
|
|
53
|
-
# If the source file has recognized paths in it, those portions of the full
|
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
|
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
|
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
|
-
|
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 =
|
186
|
+
conditions = conditions.inject({}) do |memo, pair|
|
180
187
|
faux, value = *pair
|
181
188
|
getter = attribute_getters([faux]).first
|
182
|
-
|
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
|
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 ==
|
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
|
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
|
data/spec/spec_helper.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Copyright
|
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
|
+
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-
|
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
|
-
-
|
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.
|
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
|