allocation_stats 0.1.1

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.
@@ -0,0 +1,137 @@
1
+ # Copyright 2013 Google Inc. All Rights Reserved.
2
+ # Licensed under the Apache License, Version 2.0, found in the LICENSE file.
3
+
4
+ require "json"
5
+
6
+ class AllocationStats
7
+ class Allocation
8
+ # a convenience constants
9
+ PWD = Dir.pwd
10
+
11
+ # a list of helper methods that Allocation provides on top of the object that was allocated.
12
+ HELPERS = [:class_plus, :gem]
13
+
14
+ # a list of attributes that Allocation has on itself; inquiries in this
15
+ # list should just use Allocation's attributes, rather than the internal
16
+ # object's.
17
+ ATTRIBUTES = [:sourcefile, :sourceline, :class_path, :method_id, :memsize]
18
+
19
+ # @!attribute [rw] memsize
20
+ # the memsize of the object which was allocated
21
+ attr_accessor :memsize
22
+
23
+ # @!attribute [r] class_path
24
+ # the classpath of where the object was allocated
25
+ attr_reader :class_path
26
+
27
+ # @!attribute [r] method_id
28
+ # the method ID of where the object was allocated
29
+ attr_reader :method_id
30
+
31
+ # @!attribute [r] object
32
+ # the actual object that was allocated
33
+ attr_reader :object
34
+
35
+ # @!attribute [r] sourceline
36
+ # the line in the sourcefile where the object was allocated
37
+ attr_reader :sourceline
38
+
39
+ def initialize(object)
40
+ @object = object
41
+ @memsize = ObjectSpace.memsize_of(object)
42
+ @sourcefile = ObjectSpace.allocation_sourcefile(object)
43
+ @sourceline = ObjectSpace.allocation_sourceline(object)
44
+ @class_path = ObjectSpace.allocation_class_path(object)
45
+ @method_id = ObjectSpace.allocation_method_id(object)
46
+ end
47
+
48
+ def file; @sourcefile; end
49
+ #def line; @sourceline; end
50
+ alias :line :sourceline
51
+
52
+ # If the source file has recognized paths in it, those portions of the full path will be aliased like so:
53
+ #
54
+ # * the present work directory is aliased to "<PWD>"
55
+ # * the Ruby lib directory (where the standard library lies) is aliased to "<RUBYLIBDIR>"
56
+ # * the Gem directory (where all gems lie) is aliased to "<GEMDIR>"
57
+ #
58
+ # @return the source file, aliased.
59
+ def sourcefile_alias
60
+ case
61
+ when @sourcefile[PWD]
62
+ @sourcefile.sub(PWD, "<PWD>")
63
+ when @sourcefile[AllocationStats::RUBYLIBDIR]
64
+ @sourcefile.sub(AllocationStats::RUBYLIBDIR, "<RUBYLIBDIR>")
65
+ when @sourcefile[AllocationStats::GEMDIR]
66
+ @sourcefile.sub(/#{AllocationStats::GEMDIR}\/gems\/([^\/]+)\//, '<GEM:\1>/')
67
+ else
68
+ @sourcefile
69
+ end
70
+ end
71
+
72
+ # Either the full source file (via `@sourcefile`), or the aliased source
73
+ # file, via {#sourcefile_alias}
74
+ #
75
+ # @param [TrueClass] alias_path whether or not to alias the path
76
+ def sourcefile(alias_path = false)
77
+ alias_path ? sourcefile_alias : @sourcefile
78
+ end
79
+
80
+ def class_plus
81
+ case @object
82
+ when Array
83
+ object_classes = element_classes(@object.map {|e| e.class }.uniq)
84
+ if object_classes
85
+ "Array<#{object_classes}>"
86
+ else
87
+ "Array"
88
+ end
89
+ else
90
+ @object.class.name
91
+ end
92
+ end
93
+
94
+ # @return [String] the name of the Rubygem where this allocation occurred.
95
+ # @return [nil] if this allocation did not occur in a Rubygem.
96
+ #
97
+ # Override Rubygems' Kernel#gem
98
+ def gem
99
+ gem_regex = /#{AllocationStats::GEMDIR}#{File::SEPARATOR}
100
+ gems#{File::SEPARATOR}
101
+ (?<gem_name>[^#{File::SEPARATOR}]+)#{File::SEPARATOR}
102
+ /x
103
+ match = gem_regex.match(sourcefile)
104
+ match && match[:gem_name]
105
+ end
106
+
107
+ # Convert into a JSON string, which can be used in rack-allocation_stats's
108
+ # interactive mode.
109
+ def as_json
110
+ {
111
+ "memsize" => @memsize,
112
+ "class_path" => @class_path,
113
+ "method_id" => @method_id,
114
+ "file" => sourcefile_alias,
115
+ "file (raw)" => @sourcefile,
116
+ "line" => @sourceline,
117
+ "class" => @object.class.name,
118
+ "class_plus" => class_plus
119
+ }
120
+ end
121
+
122
+ def to_json(*a)
123
+ as_json.to_json(*a)
124
+ end
125
+
126
+ def element_classes(classes)
127
+ if classes.size == 1
128
+ classes.first
129
+ elsif classes.size > 1 && classes.size < 4
130
+ classes.join(",")
131
+ else
132
+ nil
133
+ end
134
+ end
135
+ private :element_classes
136
+ end
137
+ end
@@ -0,0 +1,289 @@
1
+ # Copyright 2013 Google Inc. All Rights Reserved.
2
+ # Licensed under the Apache License, Version 2.0, found in the LICENSE file.
3
+
4
+ class AllocationStats
5
+ # AllocationsProxy acts as a proxy for an array of Allocation objects. The
6
+ # idea behind this class is merely to provide some domain-specific methods
7
+ # for transforming (filtering, sorting, and grouping) allocation information.
8
+ # This class uses the Command pattern heavily, in order to build and maintain
9
+ # the list of transforms it will ultimately perform, before retrieving the
10
+ # transformed collection of Allocations.
11
+ #
12
+ # Chaining
13
+ # ========
14
+ #
15
+ # Use of the Command pattern and Procs allows for transform-chaining in any
16
+ # order. Apply methods such as {#from} and {#group_by} to build the internal
17
+ # list of transforms. The transforms will not be applied to the collection of
18
+ # Allocations until a call to {#to_a} ({#all}) resolves them.
19
+ #
20
+ # Filtering Transforms
21
+ # --------------------
22
+ #
23
+ # Methods that filter the collection of Allocations will add a transform to
24
+ # an Array, `@wheres`. When the result set is finally retrieved, each where
25
+ # is applied serially, so that `@wheres` represents a logical conjunction
26
+ # (_"and"_) of of filtering transforms. Presently there is no way to _"or"_
27
+ # filtering transforms together with a logical disjunction.
28
+ #
29
+ # Mapping Transforms
30
+ # ------------------
31
+ #
32
+ # Grouping Transform
33
+ # ------------------
34
+ #
35
+ # Only one method will allow a grouping transform: {#group_by}. Only one
36
+ # grouping transform is allowed; subsequent calls to {#group_by} will only
37
+ # replace the previous grouping transform.
38
+ class AllocationsProxy
39
+
40
+ # Instantiate an {AllocationsProxy} with an array of Allocations.
41
+ # {AllocationProxy's} view of `pwd` is set at instantiation.
42
+ #
43
+ # @param [Array<Allocation>] allocations array of Allocation objects
44
+ def initialize(allocations, alias_paths: false)
45
+ @allocations = allocations
46
+ @pwd = Dir.pwd
47
+ @wheres = []
48
+ @group_by = nil
49
+ @mappers = []
50
+ @alias_paths = alias_paths
51
+ end
52
+
53
+ def to_a
54
+ results = @allocations
55
+
56
+ @wheres.each do |where|
57
+ results = where.call(results)
58
+ end
59
+
60
+ # First apply group_by
61
+ results = @group_by.call(results) if @group_by
62
+
63
+ # Apply each mapper
64
+ @mappers.each do |mapper|
65
+ results = mapper.call(results)
66
+ end
67
+
68
+ results
69
+ end
70
+ alias :all :to_a
71
+
72
+ def alias_paths(value = nil)
73
+ # reader
74
+ return @alias_paths if value.nil?
75
+
76
+ # writer
77
+ @alias_paths = value
78
+
79
+ return self
80
+ end
81
+
82
+ def sort_by_size
83
+ @mappers << Proc.new do |allocations|
84
+ allocations.sort_by { |key, value| -value.size }
85
+ .inject({}) { |hash, pair| hash[pair[0]] = pair[1]; hash }
86
+ end
87
+
88
+ self
89
+ end
90
+ alias :sort_by_count :sort_by_size
91
+
92
+ # Select allocations for which the {Allocation#sourcefile sourcefile}
93
+ # includes `pattern`.
94
+ #
95
+ # `#from` can be called multiple times, adding to `@wheres`. See
96
+ # documentation for {AllocationsProxy} for more information about chaining.
97
+ #
98
+ # @param [String] pattern the partial file path to match against, in the
99
+ # {Allocation#sourcefile Allocation's sourcefile}.
100
+ def from(pattern)
101
+ @wheres << Proc.new do |allocations|
102
+ allocations.select { |allocation| allocation.sourcefile[pattern] }
103
+ end
104
+
105
+ self
106
+ end
107
+
108
+ # Select allocations for which the {Allocation#sourcefile sourcefile} does
109
+ # not include `pattern`.
110
+ #
111
+ # `#not_from` can be called multiple times, adding to `@wheres`. See
112
+ # documentation for {AllocationsProxy} for more information about chaining.
113
+ #
114
+ # @param [String] pattern the partial file path to match against, in the
115
+ # {Allocation#sourcefile Allocation's sourcefile}.
116
+ def not_from(pattern)
117
+ @wheres << Proc.new do |allocations|
118
+ allocations.reject { |allocation| allocation.sourcefile[pattern] }
119
+ end
120
+
121
+ self
122
+ end
123
+
124
+ # Select allocations for which the {Allocation#sourcefile sourcefile}
125
+ # includes the present working directory.
126
+ #
127
+ # `#from_pwd` can be called multiple times, adding to `@wheres`. See
128
+ # documentation for {AllocationsProxy} for more information about chaining.
129
+ def from_pwd
130
+ @wheres << Proc.new do |allocations|
131
+ allocations.select { |allocation| allocation.sourcefile[@pwd] }
132
+ end
133
+
134
+ self
135
+ end
136
+
137
+ def group_by(*args)
138
+ @group_keys = args
139
+
140
+ @group_by = Proc.new do |allocations|
141
+ getters = attribute_getters(@group_keys)
142
+
143
+ allocations.group_by do |allocation|
144
+ getters.map { |getter| getter.call(allocation) }
145
+ end
146
+ end
147
+
148
+ self
149
+ end
150
+
151
+ def where(hash)
152
+ @wheres << Proc.new do |allocations|
153
+ conditions = hash.inject({}) do |h, pair|
154
+ faux, value = *pair
155
+ getter = attribute_getters([faux]).first
156
+ h.merge(getter => value)
157
+ end
158
+
159
+ allocations.select do |allocation|
160
+ conditions.all? { |getter, value| getter.call(allocation) == value }
161
+ end
162
+ end
163
+
164
+ self
165
+ end
166
+
167
+ def attribute_getters(faux_attributes)
168
+ faux_attributes.map do |faux|
169
+ if faux == :sourcefile
170
+ lambda { |allocation| allocation.sourcefile(@alias_paths) }
171
+ elsif Allocation::HELPERS.include?(faux) ||
172
+ Allocation::ATTRIBUTES.include?(faux)
173
+ lambda { |allocation| allocation.send(faux) }
174
+ else
175
+ lambda { |allocation| allocation.object.send(faux) }
176
+ end
177
+ end
178
+ end
179
+ private :attribute_getters
180
+
181
+ # Map to bytes via {Allocation#memsize #memsize}. This is done in one of two ways:
182
+ #
183
+ # * If the current result set is an Array, then this transform just maps
184
+ # each Allocation to its `#memsize`.
185
+ # * If the current result set is a Hash (meaning it has been grouped), then
186
+ # this transform maps each value in the Hash (which is an Array of
187
+ # Allocations) to the sum of the Allocation `#memsizes` within.
188
+ def bytes
189
+ @mappers << Proc.new do |allocations|
190
+ if allocations.is_a? Array
191
+ allocations.map(&:memsize)
192
+ elsif allocations.is_a? Hash
193
+ bytes_h = {}
194
+ allocations.each do |key, allocations|
195
+ bytes_h[key] = allocations.inject(0) { |sum, allocation| sum + allocation.memsize }
196
+ end
197
+ bytes_h
198
+ end
199
+ end
200
+
201
+ self
202
+ end
203
+
204
+ # default columns for the tabular output
205
+ DEFAULT_COLUMNS = [:sourcefile, :sourceline, :class_path, :method_id, :memsize, :class]
206
+
207
+ # columns that should be right-aligned for the tabular output
208
+ NUMERIC_COLUMNS = [:sourceline, :memsize]
209
+
210
+ # Resolve the AllocationsProxy (by calling {#to_a}) and return tabular
211
+ # information about the Allocations as a String.
212
+ #
213
+ # @param [Array<Symbol>] columns a list of columns to print out
214
+ #
215
+ # @return [String] information about the Allocations, in a tabular format
216
+ def to_text(columns: DEFAULT_COLUMNS)
217
+ resolved = to_a
218
+
219
+ # if resolved is an Array of Allocations
220
+ if resolved.is_a?(Array) && resolved.first.is_a?(Allocation)
221
+ to_text_from_plain(resolved, columns: columns)
222
+
223
+ # if resolved is a Hash (was grouped)
224
+ elsif resolved.is_a?(Hash)
225
+ to_text_from_groups(resolved)
226
+ end
227
+ end
228
+
229
+ def to_json
230
+ to_a.to_json
231
+ end
232
+
233
+ def to_text_from_plain(resolved, columns: DEFAULT_COLUMNS)
234
+ getters = attribute_getters(columns)
235
+
236
+ widths = getters.each_with_index.map do |attr, idx|
237
+ (resolved.map { |a| attr.call(a).to_s.size } << columns[idx].to_s.size).max
238
+ end
239
+
240
+ text = []
241
+
242
+ text << columns.each_with_index.map { |attr, idx|
243
+ attr.to_s.center(widths[idx])
244
+ }.join(" ").rstrip
245
+
246
+ text << widths.map { |width| "-" * width }.join(" ")
247
+
248
+ text += resolved.map { |allocation|
249
+ getters.each_with_index.map { |getter, idx|
250
+ value = getter.call(allocation).to_s
251
+ NUMERIC_COLUMNS.include?(columns[idx]) ? value.rjust(widths[idx]) : value.ljust(widths[idx])
252
+ }.join(" ").rstrip
253
+ }
254
+
255
+ text.join("\n")
256
+ end
257
+ private :to_text_from_plain
258
+
259
+ def to_text_from_groups(resolved)
260
+ columns = @group_keys + ["count"]
261
+
262
+ keys = resolved.is_a?(Hash) ? resolved.keys : resolved.map(&:first)
263
+ widths = columns.each_with_index.map do |column, idx|
264
+ (keys.map { |group| group[idx].to_s.size } << columns[idx].to_s.size).max
265
+ end
266
+
267
+ text = []
268
+
269
+ text << columns.each_with_index.map { |attr, idx|
270
+ attr.to_s.center(widths[idx])
271
+ }.join(" ").rstrip
272
+
273
+ text << widths.map { |width| "-" * width }.join(" ")
274
+
275
+ text += resolved.map { |group, allocations|
276
+ line = group.each_with_index.map { |attr, idx|
277
+ NUMERIC_COLUMNS.include?(columns[idx]) ?
278
+ attr.to_s.rjust(widths[idx]) :
279
+ attr.to_s.ljust(widths[idx])
280
+ }.join(" ")
281
+
282
+ line << " " + allocations.size.to_s.rjust(5)
283
+ }
284
+
285
+ text.join("\n")
286
+ end
287
+ private :to_text_from_groups
288
+ end
289
+ end
@@ -0,0 +1,366 @@
1
+ # Copyright 2013 Google Inc. All Rights Reserved.
2
+ # Licensed under the Apache License, Version 2.0, found in the LICENSE file.
3
+
4
+ require_relative File.join("..", "spec_helper")
5
+ SPEC_HELPER_PATH = File.expand_path(File.join(__dir__, "..", "spec_helper.rb"))
6
+ MAX_PATH_LENGTH = [SPEC_HELPER_PATH.size, __FILE__.size].max
7
+
8
+ describe AllocationStats::AllocationsProxy do
9
+ it "should track new objects by path" do
10
+ existing_array = [1,2,3,4,5]
11
+
12
+ stats = AllocationStats.trace do
13
+ new_string = "stringy string"
14
+ another_string = "another string"
15
+ end
16
+
17
+ results = stats.allocations.group_by(:sourcefile).all
18
+ results.class.should eq Hash
19
+ results.keys.size.should == 1
20
+ results.keys.first.should eq [__FILE__]
21
+ results[[__FILE__]].class.should eq Array
22
+ results[[__FILE__]].size.should == 2
23
+ end
24
+
25
+ it "should track new objects by path" do
26
+ existing_array = [1,2,3,4,5]
27
+
28
+ stats = AllocationStats.trace do
29
+ new_string = "stringy string"
30
+ another_string = "another string"
31
+ a_foreign_string = allocate_a_string_from_spec_helper
32
+ end
33
+
34
+ results = stats.allocations.group_by(:sourcefile).all
35
+ results.keys.size.should == 2
36
+ results.keys.should include([__FILE__])
37
+ results.keys.any? { |file| file[0]["spec_helper"] }.should be_true
38
+ end
39
+
40
+ it "should track new objects by path and class" do
41
+ existing_array = [1,2,3,4,5]
42
+
43
+ stats = AllocationStats.trace do
44
+ new_string = "stringy string"
45
+ another_string = "another string"
46
+ an_array = [1,1,2,3,5,8,13,21,34,55]
47
+ a_foreign_string = allocate_a_string_from_spec_helper
48
+ end
49
+
50
+ results = stats.allocations.group_by(:sourcefile, :class).all
51
+ results.keys.size.should == 3
52
+ results.keys.should include([__FILE__, String])
53
+ results.keys.should include([__FILE__, Array])
54
+ end
55
+
56
+ it "should track new objects by path and class_name (Array with 1x type)" do
57
+ stats = AllocationStats.trace do
58
+ square_groups = []
59
+ 10.times do |i|
60
+ square_groups << [(4*i+0)**2, (4*i+1)**2, (4*i+2)**2, (4*i+3)**2]
61
+ end
62
+ end
63
+
64
+ results = stats.allocations.group_by(:sourcefile, :class_plus).all
65
+ results.keys.size.should == 2
66
+ results.keys.should include([__FILE__, "Array<Array>"])
67
+ results.keys.should include([__FILE__, "Array<Fixnum>"])
68
+ end
69
+
70
+ it "should track new objects by path and class_name (Array with 2-3x type)" do
71
+ stats = AllocationStats.trace do
72
+ two_classes = [1,2,3,"a","b","c"]
73
+ three_classes = [1,1.0,"1"]
74
+ end
75
+
76
+ results = stats.allocations.group_by(:sourcefile, :class_plus).all
77
+ results.keys.size.should == 3
78
+ results.keys.should include([__FILE__, "Array<Fixnum,String>"])
79
+ results.keys.should include([__FILE__, "Array<Fixnum,Float,String>"])
80
+ end
81
+
82
+ it "should track new objects by path and class_name (Arrays with same size)" do
83
+ stats = AllocationStats.trace do
84
+ ary = []
85
+ 10.times do
86
+ ary << [1,2,3,4,5]
87
+ end
88
+ end
89
+
90
+ results = stats.allocations.group_by(:sourcefile, :class_plus).all
91
+ pending "Not written yet"
92
+ end
93
+
94
+ it "should track new objects by class_path, method_id and class" do
95
+ existing_array = [1,2,3,4,5]
96
+
97
+ stats = AllocationStats.trace do
98
+ new_string = "stringy string"
99
+ another_string = "another string"
100
+ an_array = [1,1,2,3,5,8,13,21,34,55]
101
+ a_foreign_string = allocate_a_string_from_spec_helper
102
+ end
103
+
104
+ results = stats.allocations.group_by(:class_path, :method_id, :class).all
105
+ results.keys.size.should == 3
106
+
107
+ # Things allocated inside rspec describe and it blocks have nil as the
108
+ # method_id.
109
+ results.keys.should include([nil, nil, String])
110
+ results.keys.should include([nil, nil, Array])
111
+ results.keys.should include(["Object", :allocate_a_string_from_spec_helper, String])
112
+ end
113
+
114
+ it "should track new bytes" do
115
+ stats = AllocationStats.trace do
116
+ an_array = [1,1,2,3,5,8,13,21,34,55]
117
+ end
118
+
119
+ byte_sums = stats.allocations.bytes.all
120
+ byte_sums.size.should == 1
121
+ byte_sums[0].should be 80
122
+ end
123
+
124
+ it "should track new bytes by path and class" do
125
+ stats = AllocationStats.trace do
126
+ new_string = "stringy string" # 1: String from here
127
+ an_array = [1,1,2,3,5,8,13,21,34,55] # 2: Array from here
128
+ a_foreign_string = allocate_a_string_from_spec_helper # 3: String from spec_helper
129
+
130
+ class A; end # 4: Class from here
131
+ an_a = A.new # 5: A from here
132
+ end
133
+
134
+ byte_sums = stats.allocations.group_by(:sourcefile, :class).bytes.all
135
+ byte_sums.keys.size.should == 5
136
+ byte_sums.keys.should include([__FILE__, Array])
137
+ byte_sums[[__FILE__, Array]].should eq 80 # 10 Fixnums * 8 bytes/Fixnum
138
+ end
139
+
140
+ it "should track new allocations in pwd" do
141
+ existing_array = [1,2,3,4,5]
142
+
143
+ stats = AllocationStats.trace do
144
+ new_string = "stringy string" # 1: String from here
145
+ another_string = "another string"
146
+ an_array = [1,1,2,3,5,8,13,21,34,55] # 2: Array from here
147
+ a_range = "aaa".."zzz"
148
+ y = YAML.dump(["one string", "two string"]) # lots of objects not from here
149
+ end
150
+
151
+ results = stats.allocations.from_pwd.group_by(:class).all
152
+ results.keys.size.should == 3
153
+ results[[String]].size.should == 6
154
+ results[[Array]].size.should == 3 # one for empty *args in YAML.dump
155
+ results[[Range]].size.should == 1
156
+ end
157
+
158
+ it "should pass itself to Yajl::Encoder.encode correctly" do
159
+ pending "I don't know why this isn't passing, but it's not worth worrying about now"
160
+ stats = AllocationStats.trace do
161
+ new_hash = {0 => "foo", 1 => "bar"}
162
+ end
163
+
164
+ Yajl::Encoder.encode(stats.allocations.to_a).should eq \
165
+ "[{\"memsize\":192,\"file\":\"#{__FILE__}\",\"line\":170,\"class_plus\":\"Hash\"}," +
166
+ "{\"memsize\":0,\"file\":\"#{__FILE__}\",\"line\":170,\"class_plus\":\"String\"}," +
167
+ "{\"memsize\":0,\"file\":\"#{__FILE__}\",\"line\":170,\"class_plus\":\"String\"}]"
168
+ end
169
+
170
+ it "should shorten paths of stuff in RUBYLIBDIR" do
171
+ stats = AllocationStats.trace do
172
+ y = YAML.dump(["one string", "two string"]) # lots of objects from Rbconfig::CONFIG["rubylibdir"]
173
+ end
174
+
175
+ files = stats.allocations(alias_paths: true).group_by(:sourcefile, :class).all.keys.map(&:first)
176
+ files.should include("<RUBYLIBDIR>/psych/nodes/node.rb")
177
+ end
178
+
179
+ it "should shorten paths of stuff in gems" do
180
+ stats = AllocationStats.trace do
181
+ j = Yajl.dump(["one string", "two string"]) # lots of objects from Rbconfig::CONFIG["rubylibdir"]
182
+ end
183
+
184
+ files = stats.allocations(alias_paths: true).group_by(:sourcefile, :class).all.keys.map(&:first)
185
+ files.should include("<GEM:yajl-ruby-1.1.0>/lib/yajl.rb")
186
+ end
187
+
188
+ it "should track new objects by gem" do
189
+ stats = AllocationStats.trace do
190
+ j = Yajl.dump(["one string", "two string"]) # lots of objects from Rbconfig::CONFIG["rubylibdir"]
191
+ end
192
+
193
+ gems = stats.allocations.group_by(:gem, :class).all.keys.map(&:first)
194
+ gems.should include("yajl-ruby-1.1.0")
195
+ gems.should include(nil)
196
+ end
197
+
198
+ it "should be able to filter to just anything from pwd" do
199
+ stats = AllocationStats.trace do
200
+ j = Yajl.dump(["one string", "two string"]) # lots of objects from Rbconfig::CONFIG["rubylibdir"]
201
+ end
202
+
203
+ files = stats.allocations.group_by(:sourcefile, :class).from_pwd.all.keys.map(&:first)
204
+ files.should_not include("<GEMDIR>/gems/yajl-ruby-1.1.0/lib/yajl.rb")
205
+ end
206
+
207
+ it "should be able to filter to just anything from pwd, even if from is specified before group_by" do
208
+ stats = AllocationStats.trace do
209
+ j = Yajl.dump(["one string", "two string"]) # lots of objects from Rbconfig::CONFIG["rubylibdir"]
210
+ end
211
+
212
+ files = stats.allocations.from_pwd.group_by(:sourcefile, :class).all.keys.map(&:first)
213
+ files.should_not include("<GEMDIR>/gems/yajl-ruby-1.1.0/lib/yajl.rb")
214
+ end
215
+
216
+ it "should be able to filter to just one path" do
217
+ stats = AllocationStats.trace do
218
+ j = Yajl.dump(["one string", "two string"]) # lots of objects from Rbconfig::CONFIG["rubylibdir"]
219
+ end
220
+
221
+ files = stats.allocations(alias_paths: true).group_by(:sourcefile, :class).from("yajl.rb").all.keys.map(&:first)
222
+ files.should include("<GEM:yajl-ruby-1.1.0>/lib/yajl.rb")
223
+ end
224
+
225
+ it "should be able to filter to just one path" do
226
+ stats = AllocationStats.trace do
227
+ j = Yajl.dump(["one string", "two string"]) # lots of objects from Rbconfig::CONFIG["rubylibdir"]
228
+ end
229
+
230
+ files = stats.allocations.not_from("yajl.rb").group_by(:sourcefile, :class).all.keys.map(&:first)
231
+ files.should_not include("<GEMDIR>/gems/yajl-ruby-1.1.0/lib/yajl.rb")
232
+ end
233
+
234
+ it "should be able to filter to just one path" do
235
+ stats = AllocationStats.trace do
236
+ j = Yajl.dump(["one string", "two string"]) # lots of objects from Rbconfig::CONFIG["rubylibdir"]
237
+ end
238
+
239
+ classes = stats.allocations.where(class: String).group_by(:sourcefile, :class).all.keys.map(&:last)
240
+ classes.should_not include(Array)
241
+ classes.should_not include(Hash)
242
+ classes.should include(String)
243
+ end
244
+
245
+ context "to_text" do
246
+ before do
247
+ @stats = AllocationStats.trace { MyClass.new.my_method }
248
+ @line = __LINE__ - 1
249
+ end
250
+
251
+ it "should output to fixed-width text correctly" do
252
+ text = @stats.allocations.to_text
253
+ spec_helper_plus_line = "#{SPEC_HELPER_PATH.ljust(MAX_PATH_LENGTH)} #{MyClass::MY_METHOD_BODY_LINE}"
254
+
255
+ expect(text).to include(" sourcefile sourceline class_path method_id memsize class")
256
+ expect(text).to include("----------------------------------------------------------------------------------------------------- ---------- ---------- --------- ------- -------")
257
+ expect(text).to include("#{spec_helper_plus_line} MyClass my_method 192 Hash")
258
+ expect(text).to include("#{spec_helper_plus_line} MyClass my_method 0 String")
259
+ expect(text).to include("#{__FILE__.ljust(MAX_PATH_LENGTH)} #{@line} Class new 0 MyClass")
260
+ end
261
+
262
+ it "should output to fixed-width text with custom columns correctly" do
263
+ text = @stats.allocations.to_text(columns: [:sourcefile, :sourceline, :class])
264
+ spec_helper_plus_line = "#{SPEC_HELPER_PATH.ljust(MAX_PATH_LENGTH)} #{MyClass::MY_METHOD_BODY_LINE}"
265
+
266
+ expect(text).to include(" sourcefile sourceline class")
267
+ expect(text).to include("#{"-" * MAX_PATH_LENGTH} ---------- -------")
268
+ expect(text).to include("#{spec_helper_plus_line} Hash")
269
+ expect(text).to include("#{spec_helper_plus_line} String")
270
+ expect(text).to include("#{__FILE__.ljust(MAX_PATH_LENGTH)} #{@line} MyClass")
271
+ end
272
+
273
+ it "should output to fixed-width text with custom columns and aliased paths correctly" do
274
+ text = @stats.allocations(alias_paths: true).to_text(columns: [:sourcefile, :sourceline, :class])
275
+ spec_helper_plus_line = "<PWD>/spec/spec_helper.rb #{MyClass::MY_METHOD_BODY_LINE}"
276
+
277
+ expect(text).to include(" sourcefile sourceline class")
278
+ expect(text).to include("----------------------------------------------------- ---------- -------")
279
+ expect(text).to include("#{spec_helper_plus_line} Hash")
280
+ expect(text).to include("#{spec_helper_plus_line} String")
281
+ expect(text).to include("<PWD>/spec/allocation_stats/allocations_proxy_spec.rb #{@line} MyClass")
282
+ end
283
+
284
+ it "should output to fixed-width text after group_by correctly" do
285
+ text = @stats.allocations(alias_paths: true).group_by(:sourcefile, :sourceline, :class).to_text
286
+ spec_helper_plus_line = "<PWD>/spec/spec_helper.rb #{MyClass::MY_METHOD_BODY_LINE}"
287
+
288
+ expect(text).to include(" sourcefile sourceline class count\n")
289
+ expect(text).to include("----------------------------------------------------- ---------- ------- -----\n")
290
+ expect(text).to include("#{spec_helper_plus_line} Hash 1")
291
+ expect(text).to include("#{spec_helper_plus_line} String 2")
292
+ expect(text).to include("<PWD>/spec/allocation_stats/allocations_proxy_spec.rb #{@line} MyClass 1")
293
+ end
294
+ end
295
+
296
+ context "to_json" do
297
+ before do
298
+ @stats = AllocationStats.trace { MyClass.new.my_method }
299
+ @line = __LINE__ - 1
300
+ end
301
+
302
+ it "should output to json correctly" do
303
+ json = @stats.allocations.to_json
304
+ expect { Yajl::Parser.parse(json) }.to_not raise_error
305
+ end
306
+
307
+ it "should output to json correctly" do
308
+ allocations = @stats.allocations.all
309
+ json = allocations.to_json
310
+ parsed = Yajl::Parser.parse(json)
311
+
312
+ first = {
313
+ "file" => "<PWD>/spec/spec_helper.rb",
314
+ "file (raw)" => "/usr/local/google/home/srawlins/code/allocation_stats/spec/spec_helper.rb",
315
+ "line" => 20,
316
+ "class_path" => "MyClass",
317
+ "method_id" => :my_method.to_s,
318
+ "memsize" => 192,
319
+ "class" => "Hash",
320
+ "class_plus" => "Hash"
321
+ }
322
+
323
+ expect(parsed.size).to be(4)
324
+ expect(parsed.any? { |allocation| allocation == first } ).to be_true
325
+ end
326
+ end
327
+
328
+ context "sorting" do
329
+ before do
330
+ @stats = AllocationStats.trace do
331
+ ary = []
332
+ 4.times do
333
+ ary << [1,2,3,4,5]
334
+ end
335
+ str_1 = "string"; str_2 = "strang"
336
+ end
337
+ @lines = [__LINE__ - 6, __LINE__ - 4, __LINE__ - 2]
338
+ end
339
+
340
+ it "should sort Allocations that have not been grouped" do
341
+ results = @stats.allocations.group_by(:sourcefile, :sourceline, :class).sort_by_count.all
342
+
343
+ expect(results.keys[0]).to include(@lines[1])
344
+ expect(results.keys[1]).to include(@lines[2])
345
+ expect(results.keys[2]).to include(@lines[0])
346
+
347
+ expect(results.values[0].size).to eq(4)
348
+ expect(results.values[1].size).to eq(2)
349
+ expect(results.values[2].size).to eq(1)
350
+ end
351
+
352
+ it "should output to fixed-width text after group_by..sort_by_count correctly" do
353
+ text = @stats.allocations(alias_paths: true)
354
+ .group_by(:sourcefile, :sourceline, :class)
355
+ .sort_by_count
356
+ .to_text.split("\n")
357
+ spec_file = "<PWD>/spec/allocation_stats/allocations_proxy_spec.rb "
358
+
359
+ expect(text[0]).to eq(" sourcefile sourceline class count")
360
+ expect(text[1]).to eq("----------------------------------------------------- ---------- ------ -----")
361
+ expect(text[2]).to eq("#{spec_file} #{@lines[1]} Array 4")
362
+ expect(text[3]).to eq("#{spec_file} #{@lines[2]} String 2")
363
+ expect(text[4]).to eq("#{spec_file} #{@lines[0]} Array 1")
364
+ end
365
+ end
366
+ end