minstrel 0.2.20110428124038 → 0.3.0

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