minstrel 0.2.20110428124038 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/lib/minstrel.rb +185 -241
  2. data/minstrel.gemspec +1 -2
  3. metadata +20 -41
@@ -1,282 +1,226 @@
1
1
  # Wrap method calls for a class of your choosing.
2
2
  # Example:
3
- # instrument = Minstrel::Instrument.new()
4
- # instrument.wrap(String) do |point, klass, method, *args|
5
- # ...
6
- # end
7
3
  #
8
- # * point is either :enter or :exit depending if this function is about to be
9
- # called or has finished being called.
10
- # * klass is the class object (String, etc)
11
- # * method is the method (a Symbol)
12
- # * *args is the arguments passed
4
+ # instrument = Minstrel::Instrument.new()
5
+ # instrument.observe(class_or_method) { |event| ... }
13
6
  #
14
- # You can also wrap from the command-line
7
+ # The 'event' is a Minstrel::Event.
15
8
  #
16
- # RUBY_INSTRUMENT=comma_separated_classnames ruby -rminstrel ./your/program.rb
9
+ # You can also wrap from the command-line
10
+ # RUBY_INSTRUMENT=comma_separated_classnames ruby -rminstrel ./your/program.rb
17
11
  #
18
12
 
19
- require "set"
20
-
21
- module Minstrel; class Instrument
22
- attr_accessor :counter
23
-
24
- class << self
25
- @@deferred_wraps = {}
26
- @@deferred_method_wraps = {}
27
- @@wrapped = Set.new
28
- end
29
-
30
- # Put methods we must not be wrapping here.
31
- DONOTWRAP = {
32
- "Minstrel::Instrument" => Minstrel::Instrument.instance_methods.collect { |m| m.to_sym },
33
- "Object" => [ :to_sym, :respond_to?, :send, :java_send, :method, :java_method,
34
- :ancestors, :inspect, :to_s, :instance_eval, :instance_exec,
35
- :class_eval, :class_exec, :module_eval, :module_exec],
36
- }
37
-
38
- # Wrap a class's instance methods with your block.
39
- # The block will be called with 4 arguments, and called
40
- # before and after the original method.
41
- # Arguments:
42
- # * point - the point (symbol, :entry or :exit) of call,
43
- # * this - the object instance in scope (use 'this.class' for the class)
44
- # * method - the method (symbol) being called
45
- # * *args - the arguments (array) passed to this method.
46
- def wrap(klass, method_to_wrap=nil, &block)
47
- return true if @@wrapped.include?(klass)
48
- instrumenter = self # save 'self' for scoping below
49
- @@wrapped << klass
50
-
51
- ancestors = klass.ancestors.collect {|k| k.to_s }
52
- if ancestors.include?("Exception")
53
- return true
13
+ module Minstrel; end
14
+
15
+ class Minstrel::Event
16
+ attr_accessor :action
17
+ attr_accessor :file
18
+ attr_accessor :line
19
+ attr_accessor :method
20
+ attr_accessor :binding
21
+ attr_accessor :class
22
+ attr_accessor :args_hash
23
+ attr_accessor :args_order
24
+ attr_accessor :timestamp
25
+
26
+ # Duration is only valid for 'return' or 'c-return' events
27
+ attr_accessor :duration
28
+
29
+ # The call stack depth of this event
30
+ attr_accessor :depth
31
+
32
+ public
33
+ def initialize(action, file, line, method, binding, klass)
34
+ @action = action
35
+ @file = file
36
+ @line = line
37
+ @method = method
38
+ @binding = binding
39
+ @class = klass
40
+ @timestamp = Time.now
41
+
42
+ if ["c-call", "call"].include?(@action)
43
+ @args_hash = {}
44
+ @args_order = eval('local_variables', binding)
45
+
46
+ # In ruby 1.9, local_variables returns an array of symbols, not strings.
47
+ @args_order.collect { |v| @args_hash[v] = eval(v.to_s, binding) }
54
48
  end
55
- puts "Wrapping #{klass.class} #{klass}" if $DEBUG
56
49
 
57
- # Wrap class instance methods (like File#read)
58
- klass.instance_methods.each do |method|
59
- next if !method_to_wrap.nil? and method != method_to_wrap
50
+ @block_given = eval('block_given?', @binding)
51
+ end # def initialize
52
+
53
+ # Get the call args as an array in-order
54
+ public
55
+ def args
56
+ return nil unless @args_order
57
+ return @args_order.collect { |v| @args_hash[v] }
58
+ end # def args
59
+
60
+ public
61
+ def use_related_event(event)
62
+ @args_order = event.args_order
63
+ @args_hash = event.args_hash
64
+ @block_given = event.block_given?
65
+ @duration = @timestamp - event.timestamp
66
+ end # def use_related_event
67
+
68
+ # Is this event a method entry?
69
+ public
70
+ def entry?
71
+ return ["c-call", "call"].include?(action)
72
+ end # def entry?
73
+
74
+ # Is this event a method return?
75
+ public
76
+ def exit?
77
+ return ["c-return", "return"].include?(action)
78
+ end # def exit?
79
+
80
+ # Was there a block given to this method?
81
+ public
82
+ def block_given?
83
+ @block_given
84
+ end # def block_given ?
85
+
86
+ public
87
+ def symbol
88
+ case @action
89
+ when "c-call", "call" ; return "=>"
90
+ when "c-return", "return" ; return "<="
91
+ when "raise"; return "<E"
92
+ else return " +"
93
+ end
94
+ end # def symbol
60
95
 
61
- method = method.to_sym
96
+ public
97
+ def to_s
98
+ return "#{" " * @depth}#{symbol} #{@class.to_s}##{@method}(#{args.inspect}) #{block_given? ? "{ ... }" : ""} (thread=#{Thread.current}) #{@duration.nil? ? "" : sprintf("(%.5f seconds)", @duration)}"
99
+ end # def to_s
62
100
 
63
- # If we shouldn't wrap a certain class method, skip it.
64
- skip = false
65
- (ancestors & DONOTWRAP.keys).each do |key|
66
- if DONOTWRAP[key].include?(method)
67
- skip = true
68
- break
69
- end
70
- end
71
- if skip
72
- #puts "Skipping #{klass}##{method} (do not wrap)"
73
- next
74
- end
101
+ end # class Minstrel::Event
75
102
 
76
- klass.class_eval do
77
- orig_method = "#{method}_original(wrapped)".to_sym
78
- orig_method_proc = klass.instance_method(method)
79
- alias_method orig_method, method
80
- #block.call(:wrap, klass, method)
81
- puts "Wrapping #{klass.name}##{method} (method)" if $DEBUG
82
- define_method(method) do |*args, &argblock|
83
- exception = false
84
- block.call(:enter, self, method, *args)
85
- begin
86
- # TODO(sissel): Not sure which is better:
87
- # * UnboundMethod#bind(self).call(...)
88
- # * self.method(orig_method).call(...)
89
- val = orig_method_proc.bind(self).call(*args, &argblock)
90
- #m = self.method(orig_method)
91
- #val = m.call(*args, &argblock)
92
- rescue => e
93
- exception = e
94
- end
95
- if exception
96
- # TODO(sissel): Include the exception
97
- block.call(:exit_exception, self, method, *args)
98
- raise e if exception
99
- else
100
- # TODO(sissel): Include the return value
101
- block.call(:exit, self, method, *args)
102
- end
103
- return val
104
- end # define_method(method)
105
- end # klass.class_eval
106
- end # klass.instance_methods.each
103
+ class Minstrel::Instrument
104
+ attr_accessor :counter
107
105
 
108
- # Wrap class methods (like File.open)
109
- klass.methods.each do |method|
110
- next if !method_to_wrap.nil? and method != method_to_wrap
111
- method = method.to_sym
112
- # If we shouldn't wrap a certain class method, skip it.
113
- skip = false
114
- ancestors = klass.ancestors.collect {|k| k.to_s}
115
- (ancestors & DONOTWRAP.keys).each do |key|
116
- if DONOTWRAP[key].include?(method)
117
- skip = true
118
- #break
119
- end
120
- end
106
+ public
107
+ def self.singleton
108
+ @minstrel ||= Minstrel::Instrument.new
109
+ end # def self.singleton
121
110
 
122
- # Doubly-ensure certain methods are not wrapped.
123
- # Some classes like "Timeout" do not have ancestors.
124
- if DONOTWRAP["Object"].include?(method)
125
- #puts "!! Skipping #{klass}##{method} (do not wrap)"
126
- skip = true
127
- end
111
+ public
112
+ def initialize
113
+ @observe = []
114
+ @stack = Hash.new { |h,k| h[k] = [] } # per thread
115
+ end # def initialize
128
116
 
129
- if skip
130
- puts "Skipping #{klass}##{method} (do not wrap, not safe)" if $DEBUG
131
- next
132
- end
117
+ public
118
+ def stack
119
+ return @stack
120
+ end
133
121
 
134
- klass.instance_eval do
135
- orig_method = "#{method}_original(classwrapped)".to_sym
136
- (class << self; self; end).instance_eval do
137
- begin
138
- alias_method orig_method, method.to_sym
139
- rescue NameError => e
140
- # No such method, strange but true.
141
- orig_method = self.method(method.to_sym)
122
+ public
123
+ def observe(thing=nil, &block)
124
+ if !thing.nil?
125
+ # Is thing a class name?
126
+ @observe << lambda do |event|
127
+ if class?(thing)
128
+ if event.class.to_s == thing.to_s
129
+ block.call(event)
142
130
  end
143
- method = method.to_sym
144
- #puts "Wrapping #{klass.name}.#{method} (classmethod)"
145
- define_method(method) do |*args, &argblock|
146
- block.call(:class_enter, self, method, *args)
147
- exception = false
148
- begin
149
- if orig_method.is_a?(Symbol)
150
- val = send(orig_method, *args, &argblock)
151
- else
152
- val = orig_method.call(*args, &argblock)
153
- end
154
- rescue => e
155
- exception = e
156
- end
157
- if exception
158
- block.call(:class_exit_exception, self, method, *args)
159
- raise e if exception
160
- else
161
- block.call(:class_exit, self, method, *args)
162
- end
163
- return val
131
+ else
132
+ # assume it's a method?
133
+ #p :observe => nil, :class => thing, :is => class?(thing)
134
+ klass, method = thing.split(/[.#]/, 2)
135
+ if (event.class.to_s == klass and event.method = method.to_sym)
136
+ block.call(event)
164
137
  end
165
138
  end
166
- #block.call(:class_wrap, self, method, self.method(method))
167
- end # klass.instance_eval
168
- end # klass.instance_methods.each
169
-
170
- return true
171
- end # def wrap
172
-
173
- def wrap_classname(klassname, &block)
174
- begin
175
- klass = eval(klassname)
176
- wrap(klass, &block)
177
- return true
178
- rescue NameError => e
179
- @@deferred_wraps[klassname] = block
180
- end
181
- return false
182
- end
183
-
184
- def wrap_method(fullname, &block)
185
- puts "Want to wrap #{fullname}" if $DEBUG
186
- begin
187
- klassname, method = fullname.split(/[#.]/, 2)
188
- klass = eval(klassname)
189
- wrap(klass, method, &block)
190
- return true
191
- rescue NameError => e
192
- @@deferred_method_wraps[fullname] = block
193
- return false
194
- end
195
- end # def wrap_method
196
-
197
- def wrap_all(&block)
198
- @@deferred_wraps[:all] = block
199
- ObjectSpace.each_object do |obj|
200
- next unless obj.is_a?(Class)
201
- wrap(obj, &block)
202
- end
203
- end
204
-
205
- def self.wrap_require
206
- Kernel.class_eval do
207
- alias_method :old_require, :require
208
- def require(*args)
209
- return Minstrel::Instrument::instrumented_loader(:require, *args)
210
139
  end
140
+ elsif block_given?
141
+ @observe << block
142
+ else
143
+ raise ArgumentError.new("No block given.")
211
144
  end
212
- end
145
+ end # def observe
213
146
 
214
- def self.wrap_load
215
- Kernel.class_eval do
216
- alias_method :old_load, :load
217
- def load(*args)
218
- return Minstrel::Instrument::instrumented_loader(:load, *args)
219
- end
147
+ # Is the thing here a class?
148
+ # Can be a string name of a class or a class object
149
+ private
150
+ def class?(thing)
151
+ #p :method => "class?", :thing => thing, :class => thing.is_a?(Class), :foo => thing.inspect
152
+ begin
153
+ return true if thing.is_a?(Class)
154
+ return false if (thing.is_a?(String) and thing.include?("#"))
155
+ obj = eval(thing)
156
+ return obj.is_a?(Class)
157
+ rescue => e
158
+ false
220
159
  end
221
- end
222
-
223
- def self.instrumented_loader(method, *args)
224
- ret = self.send(:"old_#{method}", *args)
225
- if @@deferred_wraps.include?(:all)
226
- # try to wrap anything new that is not wrapped
227
- wrap_all(@@deferred_wraps[:all])
228
- else
229
- # look for deferred class wraps
230
- klasses = @@deferred_wraps.keys
231
- klasses.each do |klassname|
232
- if @@deferred_wraps.include?("ALL")
233
- all = true
234
- end
235
- block = @@deferred_wraps[klassname]
236
- instrument = Minstrel::Instrument.new
237
- if instrument.wrap_classname(klassname, &block)
238
- $stderr.puts "Wrap of #{klassname} successful"
239
- @@deferred_wraps.delete(klassname) if !all
160
+ end # def class?
161
+
162
+ # Activate tracing
163
+ public
164
+ def enable
165
+ set_trace_func(Proc.new { |*args| trace_callback(*args) })
166
+ end # def enable
167
+
168
+ # Disable tracing
169
+ public
170
+ def disable
171
+ set_trace_func(nil)
172
+ end # def disable
173
+
174
+ # This method is invoked by the ruby tracer
175
+ private
176
+ def trace_callback(action, file, line, method, binding, klass)
177
+ begin
178
+ event = Minstrel::Event.new(action, file, line, method, binding, klass)
179
+ # At the time of "c-call" there's no other local variables other than
180
+ # the method arguments. Pull the args out of binding
181
+ #event.args = eval('local_variables.collect { |v| "(#{v}) #{v.inspect}" }', binding)
182
+ if ["c-call", "call"].include?(action)
183
+ @stack[Thread.current].push event
184
+ event.depth = @stack[Thread.current].size
185
+ elsif ["c-return", "return", "raise"].include?(action)
186
+ # TODO(sissel): validate top of stack looks right?
187
+ event.depth = @stack[Thread.current].size
188
+ entry_event = @stack[Thread.current].pop
189
+ if !entry_event.nil?
190
+ event.use_related_event(entry_event)
240
191
  end
192
+ else
193
+ event.depth = @stack[Thread.current].size
241
194
  end
242
195
 
243
- klassmethods = @@deferred_method_wraps.keys
244
- klassmethods.each do |fullname|
245
- block = @@deferred_method_wraps[fullname]
246
- instrument = Minstrel::Instrument.new
247
- if instrument.wrap_method(fullname, &block)
248
- $stderr.puts "Wrap of #{fullname} successful"
249
- @@deferred_method_wraps.delete(fullname)
250
- end
251
- end
196
+ # Call any observers
197
+ @observe.each { |callback| callback.call(event) }
198
+ rescue => e
199
+ $stderr.puts "Exception in trace_callback: #{e}"
200
+ $stderr.puts e.backtrace
201
+ raise e
252
202
  end
253
- return ret
254
- end
255
- end; end # class Minstrel::Instrument
256
-
257
- Minstrel::Instrument.wrap_require
258
- Minstrel::Instrument.wrap_load
203
+ end # def trace_callback
204
+ end # class Minstrel::Instrument
259
205
 
260
206
  # Provide a way to instrument a class using the command line:
261
207
  # RUBY_INSTRUMENT=String ruby -rminstrel ./your/program
262
208
  if ENV["RUBY_INSTRUMENT"]
263
209
  klasses = ENV["RUBY_INSTRUMENT"].split(",")
264
210
 
265
- callback = proc do |point, this, method, *args|
266
- puts "#{point} #{this.class.to_s}##{method}(#{args.inspect}) (thread=#{Thread.current}, self=#{this.inspect})"
211
+ callback = proc do |event|
212
+ # Only show entry or exits
213
+ next unless (event.entry? or event.exit?)
214
+ puts event
267
215
  end
216
+
268
217
  instrument = Minstrel::Instrument.new
269
218
  if klasses.include?(":all:")
270
- instrument.wrap_all(&callback)
219
+ instrument.observe(&callback)
271
220
  else
272
221
  klasses.each do |klassname|
273
- if klassname =~ /[#.]/ # Someone's asking for a specific method to wrap
274
- # This will wrap one method as indicated by: ClassName#method
275
- # TODO(sissel): Maybe also allow ModuleName::method
276
- instrument.wrap_method(klassname, &callback)
277
- else
278
- instrument.wrap_classname(klassname, &callback)
279
- end
222
+ instrument.observe(klassname, &callback)
280
223
  end # klasses.each
281
224
  end
225
+ instrument.enable
282
226
  end # if ENV["RUBY_INSTRUMENT"]
@@ -6,9 +6,8 @@ Gem::Specification.new do |spec|
6
6
  "./minstrel.gemspec",
7
7
  ]
8
8
 
9
- rev = Time.now.strftime("%Y%m%d%H%M%S")
10
9
  spec.name = "minstrel"
11
- spec.version = "0.2.#{rev}"
10
+ spec.version = "0.3.0"
12
11
  spec.summary = "minstrel - a ruby instrumentation tool"
13
12
  spec.description = "Instrument class methods"
14
13
  spec.files = files
metadata CHANGED
@@ -1,71 +1,50 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: minstrel
3
- version: !ruby/object:Gem::Version
4
- hash: 40220856248091
5
- prerelease: false
6
- segments:
7
- - 0
8
- - 2
9
- - 20110428124038
10
- version: 0.2.20110428124038
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ prerelease:
11
6
  platform: ruby
12
- authors:
7
+ authors:
13
8
  - Jordan Sissel
14
9
  autorequire:
15
10
  bindir: bin
16
11
  cert_chain: []
17
-
18
- date: 2011-04-28 00:00:00 -07:00
19
- default_executable:
12
+ date: 2012-01-21 00:00:00.000000000 Z
20
13
  dependencies: []
21
-
22
14
  description: Instrument class methods
23
15
  email: jls@semicomplete.com
24
- executables:
16
+ executables:
25
17
  - minstrel
26
18
  extensions: []
27
-
28
19
  extra_rdoc_files: []
29
-
30
- files:
20
+ files:
31
21
  - ./lib/minstrel.rb
32
22
  - ./README.textile
33
23
  - ./minstrel.gemspec
34
24
  - bin/minstrel
35
- has_rdoc: true
36
25
  homepage: https://github.com/jordansissel/ruby-minstrel
37
26
  licenses: []
38
-
39
27
  post_install_message:
40
28
  rdoc_options: []
41
-
42
- require_paths:
29
+ require_paths:
43
30
  - lib
44
31
  - lib
45
- required_ruby_version: !ruby/object:Gem::Requirement
32
+ required_ruby_version: !ruby/object:Gem::Requirement
46
33
  none: false
47
- requirements:
48
- - - ">="
49
- - !ruby/object:Gem::Version
50
- hash: 3
51
- segments:
52
- - 0
53
- version: "0"
54
- required_rubygems_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
55
39
  none: false
56
- requirements:
57
- - - ">="
58
- - !ruby/object:Gem::Version
59
- hash: 3
60
- segments:
61
- - 0
62
- version: "0"
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
63
44
  requirements: []
64
-
65
45
  rubyforge_project:
66
- rubygems_version: 1.3.7
46
+ rubygems_version: 1.8.10
67
47
  signing_key:
68
48
  specification_version: 3
69
49
  summary: minstrel - a ruby instrumentation tool
70
50
  test_files: []
71
-