rbtrace 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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