rbtrace 0.1.0 → 0.2.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.
data/Gemfile CHANGED
@@ -1 +1,2 @@
1
+ source :rubygems
1
2
  gemspec
data/README.md CHANGED
@@ -1,16 +1,25 @@
1
- # rbtrace
1
+ # rbtrace: like strace, but for ruby code
2
2
 
3
- like strace, but for ruby code
3
+ rbtrace shows you method calls happening inside another ruby process in real
4
+ time.
5
+
6
+ rbtrace works on ruby 1.8 and 1.9, running on linux or mac osx.
4
7
 
5
8
  ## usage
6
9
 
10
+ % gem install rbtrace
11
+ % rbtrace --help
12
+
13
+ ## examples
14
+
7
15
  ### require rbtrace into a process
8
16
 
9
17
  % cat server.rb
10
- require 'ext/rbtrace'
18
+ require 'rbtrace'
11
19
 
12
20
  class String
13
21
  def multiply_vowels(num)
22
+ @test = 123
14
23
  gsub(/[aeiou]/){ |m| m*num }
15
24
  end
16
25
  end
@@ -29,87 +38,127 @@ like strace, but for ruby code
29
38
  ### run the process
30
39
 
31
40
  % ruby server.rb &
32
- [1] 95532
41
+ [1] 87854
33
42
 
34
43
  ### trace a function using the process's pid
35
44
 
36
- % ./bin/rbtrace $! sleep
37
-
38
- Kernel#sleep <0.329042>
39
- Kernel#sleep <0.035732>
40
- Kernel#sleep <0.291189>
41
- Kernel#sleep <0.355800>
42
- Kernel#sleep <0.238176>
43
- Kernel#sleep <0.147345>
44
- Kernel#sleep <0.238686>
45
- Kernel#sleep <0.239668>
46
- Kernel#sleep <0.035512>
47
- ^C./bin/rbtrace:113: Interrupt
48
-
49
- ### trace multiple functions
50
-
51
- % ./bin/rbtrace $! sleep Dir.chdir Dir.pwd Process.pid "String#gsub" "String#*"
45
+ % rbtrace -p 87854 -m sleep
46
+ *** attached to process 87854
47
+
48
+ Kernel#sleep <0.138615>
49
+ Kernel#sleep <0.147726>
50
+ Kernel#sleep <0.318728>
51
+ Kernel#sleep <0.338173>
52
+ Kernel#sleep <0.373004>
53
+ Kernel#sleep
54
+ *** detached from process 87854
55
+
56
+ ### trace everything
57
+
58
+ % rbtrace -p 87854 --firehose
59
+ *** attached to process 87938
60
+
61
+ Kernel#proc <0.000082>
62
+ Proc#call
63
+ Dir.chdir
64
+ Dir.pwd <0.000060>
65
+ Process.pid <0.000021>
66
+ String#multiply_vowels
67
+ String#gsub
68
+ String#* <0.000023>
69
+ String#* <0.000022>
70
+ String#gsub <0.000127>
71
+ String#multiply_vowels <0.000175>
72
+ Kernel#rand <0.000018>
73
+ Float#* <0.000022>
74
+ Kernel#sleep <0.344281>
75
+ Dir.chdir <0.344858>
76
+ Proc#call <0.344908>
77
+ *** detached from process 87938
78
+
79
+ ### trace specific functions
80
+
81
+ % rbtrace -p 87854 -m sleep Dir.chdir Dir.pwd Process.pid "String#gsub" "String#*"
82
+ *** attached to process 87854
52
83
 
53
84
  Dir.chdir
54
- Dir.pwd <0.000094>
55
- Process.pid <0.000016>
85
+ Dir.pwd <0.000023>
86
+ Process.pid <0.000008>
56
87
  String#gsub
57
- String#* <0.000020>
58
- String#* <0.000020>
59
- String#gsub <0.000072>
60
- Kernel#sleep <0.369630>
61
- Dir.chdir <0.370220>
88
+ String#* <0.000008>
89
+ String#* <0.000007>
90
+ String#gsub <0.000050>
91
+ Kernel#sleep <0.498809>
92
+ Dir.chdir <0.499025>
62
93
 
63
94
  Dir.chdir
64
- Dir.pwd <0.000088>
65
- Process.pid <0.000017>
95
+ Dir.pwd <0.000024>
96
+ Process.pid <0.000008>
66
97
  String#gsub
67
- String#* <0.000020>
68
- String#* <0.000020>
69
- String#gsub <0.000071>
70
- ^C./bin/rbtrace:113: Interrupt
98
+ String#* <0.000008>
99
+ String#* <0.000007>
100
+ String#gsub <0.000050>
101
+ Kernel#sleep
102
+ *** detached from process 87854
71
103
 
72
104
  ### trace all functions in a class/module
73
105
 
74
- % ./bin/rbtrace $! "Kernel#"
106
+ % rbtrace -p 87854 -m "Kernel#"
107
+ *** attached to process 87854
75
108
 
76
- Kernel#proc <0.000071>
77
- Kernel#rand <0.000029>
78
- Kernel#sleep <0.331857>
79
- Kernel#proc <0.000019>
109
+ Kernel#proc <0.000062>
80
110
  Kernel#rand <0.000010>
81
- Kernel#sleep <0.296361>
82
- Kernel#proc <0.000021>
83
- Kernel#rand <0.000012>
84
- Kernel#sleep <0.281067>
85
- ^C./bin/rbtrace:113: Interrupt
111
+ Kernel#sleep <0.218677>
112
+ Kernel#proc <0.000016>
113
+ Kernel#rand <0.000010>
114
+ Kernel#sleep <0.195914>
115
+ Kernel#proc <0.000016>
116
+ Kernel#rand <0.000009>
117
+ Kernel#sleep
118
+ *** detached from process 87854
86
119
 
87
120
  ### get values of variables and other expressions
88
121
 
89
- % ./bin/rbtrace $! "String#gsub(self)" "String#*(self)" "String#multiply_vowels(self, self.length, num)"
122
+ % rbtrace -p 87854 -m "String#gsub(self)" "String#*(self)" "String#multiply_vowels(self, self.length, num)"
123
+ *** attached to process 87854
124
+
125
+ String#multiply_vowels(self="hello", self.length=5, num=3)
126
+ String#gsub(self="hello", @test=123)
127
+ String#*(self="e", __source__="server.rb:6") <0.000020>
128
+ String#*(self="o", __source__="server.rb:6") <0.000018>
129
+ String#gsub <0.000085>
130
+ String#multiply_vowels <0.000198>
90
131
 
91
132
  String#multiply_vowels(self="hello", self.length=5, num=3)
92
- String#gsub(self="hello")
93
- String#*(self="e") <0.000021>
94
- String#*(self="o") <0.000019>
95
- String#gsub <0.000097>
96
- String#multiply_vowels <0.000203>
133
+ String#gsub(self="hello", @test=123)
134
+ String#*(self="e", __source__="server.rb:6") <0.000020>
135
+ String#*(self="o", __source__="server.rb:6") <0.000020>
136
+ String#gsub <0.000102>
137
+ String#multiply_vowels <0.000218>
97
138
 
98
- ^C./bin/rbtrace:113: Interrupt
139
+ *** detached from process 87854
99
140
 
100
141
  ### watch for method calls slower than 250ms
101
142
 
102
- % ./bin/rbtrace $! watch 250
103
- Kernel#sleep <0.402916>
104
- Dir.chdir <0.403122>
105
- Proc#call <0.403152>
143
+ % rbtrace -p 87854 --slow=250
144
+ *** attached to process 87854
145
+ Kernel#sleep <0.459628>
146
+ Dir.chdir <0.459828>
147
+ Proc#call <0.459849>
148
+
149
+ Kernel#sleep <0.484666>
150
+ Dir.chdir <0.484804>
151
+ Proc#call <0.484818>
152
+
153
+ *** detached from process 87854
106
154
 
107
- Kernel#sleep <0.390635>
108
- Dir.chdir <0.390937>
109
- Proc#call <0.390983>
155
+ ## todo
110
156
 
111
- Kernel#sleep <0.399413>
112
- Dir.chdir <0.399753>
113
- Proc#call <0.399797>
157
+ * switch ipc to [msgpack](https://github.com/dhotson/msgpack/tree/master/c) instead of csv
158
+ * add triggers to start tracing slow methods only inside another method
159
+ * optimize local variable lookup to avoid instance_eval
160
+ * let bin/rbtrace read methods selectors and expressions from a config file
161
+ * use shared memory region for symbol table to avoid lookup on every event
162
+ * use another shm for class name lookup, and wipe on every GC
163
+ * investigate mach_msg on osx since msgget(2) has hard kernel limits
114
164
 
115
- ^C./bin/rbtrace:113: Interrupt
@@ -2,12 +2,242 @@
2
2
  require 'rubygems'
3
3
  require 'ffi'
4
4
 
5
- module RBTracer
6
- def self.process_line(line)
7
- @tracers ||= {}
8
- @nesting ||= 0
9
- @last_tracer ||= nil
5
+ module FFI::LastError
6
+ def self.exception
7
+ Errno::constants.map(&Errno.method(:const_get)).find{ |c| c.const_get(:Errno) == error }
8
+ end
9
+ def self.raise(msg=nil)
10
+ Kernel.raise exception, msg
11
+ end
12
+ end
13
+
14
+ module MsgQ
15
+ extend FFI::Library
16
+ ffi_lib 'c'
17
+
18
+ class EventMsg < FFI::Struct
19
+ BUF_SIZE = RUBY_PLATFORM =~ /linux/ ? 256 : 120
20
+ IPC_NOWAIT = 004000
21
+
22
+ layout :mtype, :long,
23
+ :buf, [:char, BUF_SIZE]
24
+
25
+ def self.send_cmd(q, str)
26
+ msg = EventMsg.new
27
+ msg[:mtype] = 1
28
+ msg[:buf].to_ptr.put_string(0, str)
29
+
30
+ ret = MsgQ.msgsnd(q, msg, BUF_SIZE, 0)
31
+ FFI::LastError.raise if ret == -1
32
+ end
33
+
34
+ def self.recv_cmd(q, block=true)
35
+ msg = EventMsg.new
36
+ ret = MsgQ.msgrcv(q, msg, BUF_SIZE, 0, block ? 0 : IPC_NOWAIT)
37
+ if ret == -1
38
+ if !block and [Errno::EAGAIN, Errno::ENOMSG].include?(FFI::LastError.exception)
39
+ return nil
40
+ end
41
+
42
+ FFI::LastError.raise
43
+ end
44
+
45
+ msg[:buf].to_ptr.read_string
46
+ end
47
+ end
48
+
49
+ attach_function :msgget, [:int, :int], :int
50
+ attach_function :msgrcv, [:int, EventMsg.ptr, :size_t, :long, :int], :int
51
+ attach_function :msgsnd, [:int, EventMsg.ptr, :size_t, :int], :int
52
+ end
53
+
54
+ class RBTracer
55
+ # Suggest increasing the maximum number of bytes allowed on
56
+ # a message queue to 1MB.
57
+ #
58
+ # This defaults to 16k on Linux, and is hardcoded to 2k in OSX kernel.
59
+ #
60
+ # Returns nothing.
61
+ def self.check_msgmnb
62
+ if File.exists?(msgmnb = "/proc/sys/kernel/msgmnb")
63
+ curr = File.read(msgmnb).to_i
64
+ max = 1024*1024
65
+ if curr < max
66
+ STDERR.puts "*** run `sudo sysctl kernel.msgmnb=#{max}` to prevent losing events (currently: #{curr} bytes)"
67
+ end
68
+ end
69
+ end
70
+
71
+ # Look for any message queues pairs (pid/-pid) that no longer have an
72
+ # associated process alive, and remove them.
73
+ #
74
+ # Returns nothing.
75
+ def self.cleanup_queues
76
+ if (pids = `ps ax -o pid`.split("\n").map{ |p| p.strip.to_i }).any?
77
+ ipcs = `ipcs -q`.split("\n").grep(/^q/).map{ |line| line.strip.split[2] }
78
+ ipcs.each do |ipci|
79
+ next if ipci.match(/^0xf/)
80
+
81
+ qi = ipci.to_i(16)
82
+ qo = 0xffffffff - qi + 1
83
+ ipco = "0x#{qo.to_s(16)}"
84
+
85
+ if ipcs.include?(ipco) and !pids.include?(qi)
86
+ STDERR.puts "*** removing stale message queue pair: #{ipci}/#{ipco}"
87
+ system("ipcrm -Q #{ipci} -Q #{ipco}")
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ # Public: The Fixnum pid of the traced process.
94
+ attr_reader :pid
95
+
96
+ # Public: The IO where tracing output is written (default: STDOUT).
97
+ attr_accessor :out
98
+
99
+ # The String prefix used on nested method calls (default: ' ').
100
+ attr_accessor :prefix
101
+
102
+ # The Boolean flag for showing how long method calls take (default: true).
103
+ attr_accessor :show_duration
104
+
105
+ # The Boolean flag for showing the timestamp when method calls start (default: false).
106
+ attr_accessor :show_time
107
+
108
+ # Create a new tracer
109
+ #
110
+ # pid - The String of Fixnum process id
111
+ #
112
+ # Returns a tracer.
113
+ def initialize(pid)
114
+ begin
115
+ raise ArgumentError unless pid
116
+ @pid = pid.to_i
117
+ raise ArgumentError unless @pid > 0
118
+ Process.kill(0, @pid)
119
+ rescue TypeError, ArgumentError
120
+ raise ArgumentError, 'pid required'
121
+ rescue Errno::ESRCH
122
+ raise ArgumentError, 'invalid pid'
123
+ rescue Errno::EPERM
124
+ raise ArgumentError, 'could not signal process (run as root)'
125
+ end
126
+
127
+ Process.kill 'URG', @pid
128
+ sleep 0.25 # wait for process to create msgqs
129
+
130
+ @qi = MsgQ.msgget( pid, 0666)
131
+ @qo = MsgQ.msgget(-pid, 0666)
132
+
133
+ if @qi == -1 || @qo == -1
134
+ raise ArgumentError, 'pid is not listening for messages (did you `require "rbtrace"`)'
135
+ end
136
+
137
+ @tracers = Hash.new{ |h,k|
138
+ h[k] = {
139
+ :name => nil,
140
+ :times => [],
141
+ :names => [],
142
+ :exprs => {},
143
+ :last => false,
144
+ :arglist => false
145
+ }
146
+ }
147
+ @nesting = 0
148
+ @last_tracer = nil
149
+ @out = STDOUT
150
+ @prefix = ' '
151
+ @show_time = false
152
+ @show_duration = true
153
+ end
154
+
155
+ # Watch for method calls slower than a threshold.
156
+ #
157
+ # msec - The Fixnum threshold in milliseconds
158
+ #
159
+ # Returns nothing.
160
+ def watch(msec)
161
+ send_cmd("watch,#{msec}")
162
+ end
163
+
164
+ # Turn on the firehose (show all method calls).
165
+ #
166
+ # Returns nothing.
167
+ def firehose
168
+ send_cmd('firehose')
169
+ end
170
+
171
+ # Add tracers for the given list of methods.
172
+ #
173
+ # methods - The String or Array of method selectors to trace.
174
+ #
175
+ # Returns nothing.
176
+ def add(methods)
177
+ Array(methods).each do |func|
178
+ func.strip!
179
+
180
+ if func =~ /^(.+)\((.+?)\)$/
181
+ func, args = $1, $2
182
+ args = args.split(',').map{ |a| a.strip }
183
+ end
184
+
185
+ send_cmd("add,#{func}")
186
+
187
+ args.each do |arg|
188
+ send_cmd("addexpr,#{arg}")
189
+ end if args and args.any?
190
+ end
191
+ end
192
+
193
+ # Detach from the traced process.
194
+ #
195
+ # Returns nothing.
196
+ def detach
197
+ send_cmd('detach')
198
+ end
199
+
200
+ # Process events from the traced process.
201
+ #
202
+ # Returns nothing.
203
+ def recv_loop
204
+ while true
205
+ # block until a message arrives
206
+ lines = [recv_cmd]
207
+
208
+ # check to see if there are more messages and pull them off
209
+ # so the queue doesn't fill up in kernel land
210
+ 25.times do
211
+ break unless line = recv_cmd(false)
212
+ lines << line
213
+ end
214
+
215
+ lines.each do |line|
216
+ process_line(line)
217
+ end
218
+ end
219
+ end
220
+
221
+ private
222
+
223
+ def send_cmd(msg)
224
+ MsgQ::EventMsg.send_cmd(@qo, msg)
225
+ Process.kill 'URG', @pid
226
+ end
227
+
228
+ def recv_cmd(block=true)
229
+ MsgQ::EventMsg.recv_cmd(@qi, block)
230
+ end
10
231
 
232
+ def print(*args)
233
+ @out.print(*args)
234
+ end
235
+
236
+ def puts(*args)
237
+ @out.puts(*args)
238
+ end
239
+
240
+ def process_line(line)
11
241
  time, event, id, *args = line.strip.split(',')
12
242
  time = time.to_i
13
243
  id = id.to_i
@@ -19,14 +249,8 @@ module RBTracer
19
249
  puts line
20
250
  else
21
251
  name = args.first
22
- @tracers[id] = {
23
- :name => name,
24
- :times => [],
25
- :names => [],
26
- :exprs => {},
27
- :last => false,
28
- :arglist => false
29
- }
252
+ @tracers.delete(id)
253
+ @tracers[id][:name] = name
30
254
  end
31
255
 
32
256
  when 'remove'
@@ -73,7 +297,12 @@ module RBTracer
73
297
  @last_tracer[:arglist] = false
74
298
  end
75
299
  puts
76
- print ' '*@nesting if @nesting > 0
300
+ if @show_time
301
+ t = Time.at(time/1_000_000)
302
+ print t.strftime("%H:%M:%S.")
303
+ print "%06d " % (time - t.to_f*1_000_000).round
304
+ end
305
+ print @prefix*@nesting if @nesting > 0
77
306
  print name
78
307
 
79
308
  @nesting += 1
@@ -92,10 +321,11 @@ module RBTracer
92
321
 
93
322
  unless tracer == @last_tracer and @last_tracer[:last] == name
94
323
  puts
95
- print ' '*@nesting if @nesting > 0
324
+ printf ' '*16 if @show_time
325
+ print @prefix*@nesting if @nesting > 0
96
326
  print name
97
327
  end
98
- print ' <%f>' % (diff/1_000_000.0)
328
+ print ' <%f>' % (diff/1_000_000.0) if @show_duration
99
329
  puts if @nesting == 0 and (tracer != @last_tracer || @last_tracer[:last] != name)
100
330
  end
101
331
 
@@ -110,10 +340,13 @@ module RBTracer
110
340
  name = klass ? "#{klass}#{ is_singleton ? '.' : '#' }" : ''
111
341
  name += method
112
342
 
113
- print ' '*nesting if nesting > 0
343
+ print @prefix*nesting if nesting > 0
114
344
  print name
115
- print ' '
116
- puts "<%f>" % (diff/1_000_000.0)
345
+ if @show_duration
346
+ print ' '
347
+ print "<%f>" % (diff/1_000_000.0)
348
+ end
349
+ puts
117
350
  puts if nesting == 0
118
351
 
119
352
  else
@@ -121,150 +354,132 @@ module RBTracer
121
354
 
122
355
  end
123
356
  rescue => e
124
- puts "error on: #{line}"
357
+ STDERR.puts "error on: #{line}"
125
358
  raise e
126
359
  end
127
360
  end
128
361
 
129
- class EventMsg < FFI::Struct
130
- BUF_SIZE = RUBY_PLATFORM =~ /linux/ ? 256 : 120
131
- IPC_NOWAIT = 004000
362
+ if __FILE__ == $0
363
+ require 'trollop'
132
364
 
133
- layout :mtype, :long,
134
- :buf, [:char, BUF_SIZE]
365
+ opts = Trollop.options do
366
+ version <<-EOS
367
+ rbtrace: like strace, but for ruby code
368
+ version 0.2.0
369
+ (c) 2011 Aman Gupta (tmm1)
370
+ http://github.com/tmm1/rbtrace
371
+ EOS
135
372
 
136
- def self.send_cmd(q, str)
137
- msg = EventMsg.new
138
- msg[:mtype] = 1
139
- msg[:buf].to_ptr.put_string(0, str)
373
+ banner <<-EOS
374
+ rbtrace shows you method calls happening inside another ruby process in real time.
140
375
 
141
- ret = MsgQ.msgsnd(q, msg, BUF_SIZE, 0)
142
- FFI::LastError.raise if ret == -1
143
- end
376
+ to use, simply `require "rbtrace"` in your ruby app.
144
377
 
145
- def self.recv_cmd(q, block=true)
146
- msg = EventMsg.new
147
- ret = MsgQ.msgrcv(q, msg, BUF_SIZE, 0, block ? 0 : IPC_NOWAIT)
148
- if ret == -1
149
- if !block and [Errno::EAGAIN, Errno::ENOMSG].include?(FFI::LastError.exception)
150
- return nil
151
- end
378
+ Usage:
152
379
 
153
- FFI::LastError.raise
154
- end
380
+ rbtrace -p <PID> # trace the given process
381
+ rbtrace -o <FILE> # write output to file
382
+ rbtrace -t # show method call start time
383
+ rbtrace -T # show duration of each method call
384
+ rbtrace -n 3 # use 3 spaces to nest method calls
155
385
 
156
- msg[:buf].to_ptr.read_string
157
- end
158
- end
386
+ Tracers:
159
387
 
160
- module FFI::LastError
161
- def self.exception
162
- Errno::constants.map(&Errno.method(:const_get)).find{ |c| c.const_get(:Errno) == error }
163
- end
164
- def self.raise(msg=nil)
165
- Kernel.raise exception, msg
166
- end
167
- end
388
+ rbtrace --firehose # trace all method calls
389
+ rbtrace --slow=250 # trace method calls slower than 250ms
390
+ rbtrace --methods a b c # trace calls to given methods
168
391
 
169
- module MsgQ
170
- extend FFI::Library
392
+ Method Selectors:
171
393
 
172
- ffi_lib 'c'
173
- attach_function :msgget, [:int, :int], :int
174
- attach_function :msgrcv, [:int, EventMsg.ptr, :size_t, :long, :int], :int
175
- attach_function :msgsnd, [:int, EventMsg.ptr, :size_t, :int], :int
176
- end
394
+ sleep # any instance or class method named sleep
395
+ String#gsub # specific instance method
396
+ Process.pid # specific class method
397
+ Dir. # any class methods in Dir
398
+ Fixnum# # any instance methods of Fixnum
177
399
 
178
- if (pids = `ps ax -o pid`.split("\n").map{ |p| p.strip.to_i }).any?
179
- ipcs = `ipcs -q`.split("\n").grep(/^q/).map{ |line| line.strip.split[2] }
180
- ipcs.each do |ipci|
181
- next if ipci.match(/^0xf/)
400
+ Trace Expressions:
182
401
 
183
- qi = ipci.to_i(16)
184
- qo = 0xffffffff - qi + 1
185
- ipco = "0x#{qo.to_s(16)}"
402
+ method(self) # value of self at method invocation
403
+ method(@ivar) # value of given instance variable
404
+ method(arg1, arg2) # value of argument local variables
405
+ method(self.attr) # value of arbitrary ruby expression
406
+ method(__source__) # source file/line of callsite
186
407
 
187
- if ipcs.include?(ipco) and !pids.include?(qi)
188
- STDERR.puts "** removing stale message queue pair: #{ipci}/#{ipco}"
189
- system("ipcrm -Q #{ipci} -Q #{ipco}")
190
- end
191
- end
192
- end
408
+ All Options:\n
193
409
 
194
- if File.exists?(msgmnb = "/proc/sys/kernel/msgmnb")
195
- max = File.read(msgmnb).to_i
196
- if max < 512*1024
197
- STDERR.puts "** run `sudo sysctl kernel.msgmnb=#{512*1024}` to prevent losing events"
198
- end
199
- end
410
+ EOS
411
+ opt :pid,
412
+ "pid of the ruby process to trace",
413
+ :short => '-p',
414
+ :type => :int
200
415
 
201
- raise 'invalid pid' unless ARGV[0]
202
-
203
- begin
204
- raise ArgumentError unless pid = ARGV[0]
205
- pid = pid.to_i
206
- raise ArgumentError unless pid > 0
207
- Process.kill(0,pid)
208
- rescue TypeError, ArgumentError
209
- raise 'pid required'
210
- rescue Errno::ESRCH
211
- raise 'invalid pid'
212
- rescue Errno::EPERM
213
- raise 'could not signal process (run as root)'
214
- end
416
+ opt :firehose,
417
+ "show all method calls"
215
418
 
216
- funcs = [ARGV[1] || 'sleep']
217
- if ARGV.size > 2
218
- funcs += ARGV[2..-1]
219
- end
419
+ opt :slow,
420
+ "watch for method calls slower than 250 milliseconds",
421
+ :default => 250
220
422
 
221
- qi = MsgQ.msgget(pid, 0666)
222
- qo = MsgQ.msgget(-pid, 0666)
423
+ opt :methods,
424
+ "method(s) to trace (valid formats: sleep String#gsub Process.pid Kernel# Dir.)",
425
+ :type => :strings,
426
+ :short => '-m'
223
427
 
224
- if qi == -1 || qo == -1
225
- raise 'invalid pid'
226
- else
227
- begin
228
- if funcs.first == 'watch'
229
- EventMsg.send_cmd(qo, funcs.join(','))
230
- Process.kill 'URG', pid
231
- else
232
- funcs.each do |func|
233
- func.strip!
234
- if func =~ /^(.+)\((.+?)\)$/
235
- func, args = $1, $2
236
- args = args.split(',').map{ |a| a.strip }
237
- end
428
+ opt :start_time,
429
+ "show start time for each method call",
430
+ :short => '-t'
238
431
 
239
- EventMsg.send_cmd(qo, "add,#{func}")
240
- Process.kill 'URG', pid
432
+ opt :no_duration,
433
+ "hide time spent in each method call",
434
+ :default => false
241
435
 
242
- if args and args.any?
243
- args.each do |arg|
244
- EventMsg.send_cmd(qo, "addexpr,#{arg}")
245
- Process.kill 'URG', pid
246
- end
247
- end
248
- end
436
+ opt :output,
437
+ "write trace to filename",
438
+ :type => String
439
+
440
+ opt :prefix,
441
+ "prefix nested method calls with N spaces",
442
+ :default => 1
443
+ end
444
+
445
+ RBTracer.check_msgmnb
446
+ RBTracer.cleanup_queues
447
+
448
+ begin
449
+ begin
450
+ tracer = RBTracer.new(opts[:pid])
451
+ rescue ArgumentError => e
452
+ Trollop.die :pid, "invalid (#{e.message})"
249
453
  end
250
454
 
251
- while true
252
- # block until a message arrives
253
- lines = [EventMsg.recv_cmd(qi)]
455
+ if opts[:slow_given]
456
+ tracer.watch(opts[:slow])
457
+ elsif opts[:firehose]
458
+ tracer.firehose
459
+ elsif methods = opts[:methods]
460
+ tracer.add(methods)
461
+ end
254
462
 
255
- # check to see if there are more messages and pull them off
256
- # so the queue doesn't fill up in kernel land
257
- 25.times do
258
- break unless line = EventMsg.recv_cmd(qi, false)
259
- lines << line
260
- end
463
+ if out = opts[:output]
464
+ tracer.out = File.open(out,'w')
465
+ end
261
466
 
262
- lines.each do |line|
263
- RBTracer.process_line(line)
264
- end
467
+ tracer.prefix = ' ' * opts[:prefix]
468
+ tracer.show_duration = !opts[:no_duration]
469
+ tracer.show_time = opts[:start_time]
470
+
471
+ begin
472
+ STDERR.puts "*** attached to process #{tracer.pid}"
473
+ tracer.recv_loop
474
+ rescue Interrupt
265
475
  end
266
476
  ensure
267
- EventMsg.send_cmd(qo, funcs.first == 'watch' ? 'unwatch' : 'delall')
268
- Process.kill 'URG', pid
477
+ if tracer
478
+ tracer.detach
479
+ end
480
+ puts
481
+ if tracer
482
+ STDERR.puts "*** detached from process #{tracer.pid}"
483
+ end
269
484
  end
270
485
  end