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.
- checksums.yaml +7 -0
- data/.gitignore +2 -0
- data/.yardopts +1 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +30 -0
- data/LICENSE +196 -0
- data/README.markdown +377 -0
- data/Rakefile +11 -0
- data/TODO +3 -0
- data/allocation_stats.gemspec +21 -0
- data/examples/my_class.rb +6 -0
- data/examples/trace_my_class_group_by.rb +11 -0
- data/examples/trace_my_class_raw.rb +11 -0
- data/examples/trace_object_allocations.rb +7 -0
- data/examples/trace_psych_group_by.rb +8 -0
- data/examples/trace_psych_keys.rb +8 -0
- data/lib/active_support/core_ext/module/delegation.rb +203 -0
- data/lib/allocation_stats.rb +144 -0
- data/lib/allocation_stats/allocation.rb +137 -0
- data/lib/allocation_stats/allocations_proxy.rb +289 -0
- data/spec/allocation_stats/allocations_proxy_spec.rb +366 -0
- data/spec/allocation_stats_spec.rb +74 -0
- data/spec/spec_helper.rb +35 -0
- metadata +80 -0
@@ -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
|