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.
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