allocation_stats 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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,3 @@
1
+ * release first gem version
2
+ * more in the README
3
+ * binary
@@ -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,6 @@
1
+ class MyClass
2
+ def my_method
3
+ @hash = {2 => "foo", 3 => "bar", 5 => "baz"}
4
+ @string = "quux"
5
+ end
6
+ 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,7 @@
1
+ require 'objspace'
2
+
3
+ ObjectSpace.trace_object_allocations do
4
+ a = [2,3,5,7,11,13,17,19,23,29,31]
5
+ puts ObjectSpace.allocation_sourcefile(a)
6
+ puts ObjectSpace.allocation_sourceline(a)
7
+ end
@@ -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