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
data/Rakefile
ADDED
@@ -0,0 +1,11 @@
|
|
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 'rspec/core/rake_task'
|
5
|
+
require 'yard'
|
6
|
+
|
7
|
+
RSpec::Core::RakeTask.new(:spec)
|
8
|
+
|
9
|
+
task :default => :spec
|
10
|
+
|
11
|
+
YARD::Rake::YardocTask.new
|
data/TODO
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# Copyright 2013 Google Inc. All Rights Reserved.
|
2
|
+
# Licensed under the Apache License, Version 2.0, found in the LICENSE file.
|
3
|
+
|
4
|
+
Gem::Specification.new do |spec|
|
5
|
+
spec.name = "allocation_stats"
|
6
|
+
spec.version = "0.1.1"
|
7
|
+
spec.authors = ["Sam Rawlins"]
|
8
|
+
spec.email = ["sam.rawlins@gmail.com"]
|
9
|
+
spec.license = "Apache v2"
|
10
|
+
spec.summary = "Tooling for tracing object allocations in Ruby 2.1"
|
11
|
+
spec.description = "Tooling for tracing object allocations in Ruby 2.1"
|
12
|
+
|
13
|
+
spec.files = `git ls-files`.split("\n")
|
14
|
+
spec.require_paths = ["lib"]
|
15
|
+
|
16
|
+
spec.add_development_dependency "rspec"
|
17
|
+
|
18
|
+
# ">= 2.1.0" seems logical, but rubygems thought that "2.1.0.dev.0" did not fit that bill.
|
19
|
+
# "> 2.0.0" was my next guess, but apparently "2.0.0.247" _does_ fit that bill.
|
20
|
+
spec.required_ruby_version = "> 2.0.99"
|
21
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class MyClass
|
2
|
+
def my_method
|
3
|
+
@hash = {2 => "foo", 3 => "bar", 5 => "baz"}
|
4
|
+
@string = "quux"
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
require File.join(__dir__, "..", "lib", "allocation_stats")
|
9
|
+
|
10
|
+
stats = AllocationStats.trace { MyClass.new.my_method }
|
11
|
+
puts stats.allocations.group_by(:sourcefile, :sourceline, :class).to_text
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class MyClass
|
2
|
+
def my_method
|
3
|
+
@hash = {2 => "foo", 3 => "bar", 5 => "baz"}
|
4
|
+
@string = "quux"
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
require File.join(__dir__, "..", "lib", "allocation_stats")
|
9
|
+
|
10
|
+
stats = AllocationStats.trace { MyClass.new.my_method }
|
11
|
+
puts stats.allocations.to_text(columns: [:sourcefile, :sourceline, :class_path, :method_id, :class])
|
@@ -0,0 +1,8 @@
|
|
1
|
+
require "yaml"
|
2
|
+
require File.join(__dir__, "..", "lib", "allocation_stats")
|
3
|
+
|
4
|
+
stats = AllocationStats.trace do
|
5
|
+
y = YAML.dump(["one string", "two string"]) # lots of objects from Rbconfig::CONFIG["rubylibdir"]
|
6
|
+
end
|
7
|
+
|
8
|
+
puts stats.allocations(alias_paths: true).group_by(:sourcefile, :class).to_text
|
@@ -0,0 +1,8 @@
|
|
1
|
+
require "yaml"
|
2
|
+
require File.join(__dir__, "..", "lib", "allocation_stats")
|
3
|
+
|
4
|
+
stats = AllocationStats.trace do
|
5
|
+
y = YAML.dump(["one string", "two string"]) # lots of objects from Rbconfig::CONFIG["rubylibdir"]
|
6
|
+
end
|
7
|
+
|
8
|
+
stats.allocations.group_by(:sourcefile, :class).all.keys.each { |key| puts key.inspect }
|
@@ -0,0 +1,203 @@
|
|
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
|
@@ -0,0 +1,144 @@
|
|
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 "objspace"
|
5
|
+
require_relative "allocation_stats/allocation"
|
6
|
+
require_relative "allocation_stats/allocations_proxy"
|
7
|
+
|
8
|
+
require "rubygems"
|
9
|
+
|
10
|
+
# Container for an aggregation of object allocation data. Pass a block to
|
11
|
+
# {#trace AllocationStats.new.trace}. Then use the AllocationStats object's public
|
12
|
+
# interface to dig into the data and discover useful information.
|
13
|
+
class AllocationStats
|
14
|
+
# a convenience constant
|
15
|
+
RUBYLIBDIR = RbConfig::CONFIG["rubylibdir"]
|
16
|
+
|
17
|
+
# a convenience constant
|
18
|
+
GEMDIR = Gem.dir
|
19
|
+
|
20
|
+
# @!attribute [rw] burn
|
21
|
+
# @return [Fixnum]
|
22
|
+
# burn count for block tracing. Defaults to 0. When called with a block,
|
23
|
+
# #trace will yield the block @burn-times before actually tracing the object
|
24
|
+
# allocations. This offers the benefit of pre-memoizing objects, and loading
|
25
|
+
# any required Ruby files before tracing.
|
26
|
+
attr_accessor :burn
|
27
|
+
|
28
|
+
attr_accessor :gc_profiler_report
|
29
|
+
|
30
|
+
# @!attribute [r] new_allocations
|
31
|
+
# @return [Array]
|
32
|
+
# allocation data for all new objects that were allocated
|
33
|
+
# during the {#initialize} block. It is better to use {#allocations}, which
|
34
|
+
# returns an {AllocationsProxy}, which has a much more convenient,
|
35
|
+
# domain-specific API for filtering, sorting, and grouping {Allocation}
|
36
|
+
# objects, than this plain Array object.
|
37
|
+
attr_reader :new_allocations
|
38
|
+
|
39
|
+
def initialize(burn: 0)
|
40
|
+
@burn = burn
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.trace(&block)
|
44
|
+
allocation_stats = AllocationStats.new
|
45
|
+
allocation_stats.trace(&block)
|
46
|
+
end
|
47
|
+
|
48
|
+
def trace(&block)
|
49
|
+
if block_given?
|
50
|
+
trace_block(&block)
|
51
|
+
else
|
52
|
+
start
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def trace_block
|
57
|
+
@burn.times { yield }
|
58
|
+
|
59
|
+
GC.start
|
60
|
+
GC.disable
|
61
|
+
|
62
|
+
@existing_object_ids = {}
|
63
|
+
|
64
|
+
ObjectSpace.each_object.to_a.each do |object|
|
65
|
+
@existing_object_ids[object.object_id / 1000] ||= []
|
66
|
+
@existing_object_ids[object.object_id / 1000] << object.object_id
|
67
|
+
end
|
68
|
+
|
69
|
+
ObjectSpace.trace_object_allocations {
|
70
|
+
yield
|
71
|
+
}
|
72
|
+
|
73
|
+
collect_new_allocations
|
74
|
+
ObjectSpace.trace_object_allocations_clear
|
75
|
+
profile_and_start_gc
|
76
|
+
|
77
|
+
return self
|
78
|
+
end
|
79
|
+
|
80
|
+
def start
|
81
|
+
GC.start
|
82
|
+
GC.disable
|
83
|
+
|
84
|
+
@existing_object_ids = {}
|
85
|
+
|
86
|
+
ObjectSpace.each_object.to_a.each do |object|
|
87
|
+
@existing_object_ids[object.object_id / 1000] ||= []
|
88
|
+
@existing_object_ids[object.object_id / 1000] << object.object_id
|
89
|
+
end
|
90
|
+
|
91
|
+
ObjectSpace.trace_object_allocations_start
|
92
|
+
|
93
|
+
return self
|
94
|
+
end
|
95
|
+
|
96
|
+
def collect_new_allocations
|
97
|
+
@new_allocations = []
|
98
|
+
ObjectSpace.each_object.to_a.each do |object|
|
99
|
+
next if ObjectSpace.allocation_sourcefile(object).nil?
|
100
|
+
next if ObjectSpace.allocation_sourcefile(object) == __FILE__
|
101
|
+
next if @existing_object_ids[object.object_id / 1000] &&
|
102
|
+
@existing_object_ids[object.object_id / 1000].include?(object.object_id)
|
103
|
+
|
104
|
+
@new_allocations << Allocation.new(object)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def stop
|
109
|
+
collect_new_allocations
|
110
|
+
ObjectSpace.trace_object_allocations_stop
|
111
|
+
ObjectSpace.trace_object_allocations_clear
|
112
|
+
profile_and_start_gc
|
113
|
+
end
|
114
|
+
|
115
|
+
# Inspect @new_allocations, the canonical array of {Allocation} objects.
|
116
|
+
def inspect
|
117
|
+
@new_allocations.inspect
|
118
|
+
end
|
119
|
+
|
120
|
+
# Proxy for the @new_allocations array that allows for individual filtering,
|
121
|
+
# sorting, and grouping of the Allocation objects.
|
122
|
+
def allocations(alias_paths: false)
|
123
|
+
AllocationsProxy.new(@new_allocations, alias_paths: alias_paths)
|
124
|
+
end
|
125
|
+
|
126
|
+
def profile_and_start_gc
|
127
|
+
GC::Profiler.enable
|
128
|
+
GC.enable
|
129
|
+
GC.start
|
130
|
+
@gc_profiler_report = GC::Profiler.result
|
131
|
+
GC::Profiler.disable
|
132
|
+
end
|
133
|
+
private :profile_and_start_gc
|
134
|
+
end
|
135
|
+
|
136
|
+
if ENV["TRACE_PROCESS_ALLOCATIONS"]
|
137
|
+
$allocation_stats = AllocationStats.new.trace
|
138
|
+
|
139
|
+
at_exit do
|
140
|
+
$allocation_stats.stop
|
141
|
+
puts "Object Allocation Report"
|
142
|
+
puts "------------------------"
|
143
|
+
end
|
144
|
+
end
|