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 +1 -0
- data/README.md +110 -61
- data/bin/rbtrace +350 -135
- data/ext/.gitignore +4 -0
- data/ext/extconf.rb +3 -0
- data/ext/rbtrace.c +191 -151
- data/rbtrace.gemspec +3 -1
- data/server.rb +1 -0
- data/test.sh +32 -0
- metadata +21 -6
- data/ext/test.rb +0 -57
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -1,16 +1,25 @@
|
|
1
|
-
# rbtrace
|
1
|
+
# rbtrace: like strace, but for ruby code
|
2
2
|
|
3
|
-
|
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 '
|
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]
|
41
|
+
[1] 87854
|
33
42
|
|
34
43
|
### trace a function using the process's pid
|
35
44
|
|
36
|
-
%
|
37
|
-
|
38
|
-
|
39
|
-
Kernel#sleep <0.
|
40
|
-
Kernel#sleep <0.
|
41
|
-
Kernel#sleep <0.
|
42
|
-
Kernel#sleep <0.
|
43
|
-
Kernel#sleep <0.
|
44
|
-
Kernel#sleep
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
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.
|
55
|
-
Process.pid <0.
|
85
|
+
Dir.pwd <0.000023>
|
86
|
+
Process.pid <0.000008>
|
56
87
|
String#gsub
|
57
|
-
String#* <0.
|
58
|
-
String#* <0.
|
59
|
-
String#gsub <0.
|
60
|
-
Kernel#sleep <0.
|
61
|
-
Dir.chdir <0.
|
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.
|
65
|
-
Process.pid <0.
|
95
|
+
Dir.pwd <0.000024>
|
96
|
+
Process.pid <0.000008>
|
66
97
|
String#gsub
|
67
|
-
String#* <0.
|
68
|
-
String#* <0.
|
69
|
-
String#gsub <0.
|
70
|
-
|
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
|
-
%
|
106
|
+
% rbtrace -p 87854 -m "Kernel#"
|
107
|
+
*** attached to process 87854
|
75
108
|
|
76
|
-
Kernel#proc <0.
|
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.
|
82
|
-
Kernel#proc <0.
|
83
|
-
Kernel#rand <0.
|
84
|
-
Kernel#sleep <0.
|
85
|
-
|
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
|
-
%
|
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.
|
94
|
-
String#*(self="o") <0.
|
95
|
-
String#gsub <0.
|
96
|
-
String#multiply_vowels <0.
|
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
|
-
|
139
|
+
*** detached from process 87854
|
99
140
|
|
100
141
|
### watch for method calls slower than 250ms
|
101
142
|
|
102
|
-
%
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
-
|
108
|
-
Dir.chdir <0.390937>
|
109
|
-
Proc#call <0.390983>
|
155
|
+
## todo
|
110
156
|
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
data/bin/rbtrace
CHANGED
@@ -2,12 +2,242 @@
|
|
2
2
|
require 'rubygems'
|
3
3
|
require 'ffi'
|
4
4
|
|
5
|
-
module
|
6
|
-
def self.
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
23
|
-
|
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
|
-
|
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
|
-
|
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
|
343
|
+
print @prefix*nesting if nesting > 0
|
114
344
|
print name
|
115
|
-
|
116
|
-
|
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
|
-
|
130
|
-
|
131
|
-
IPC_NOWAIT = 004000
|
362
|
+
if __FILE__ == $0
|
363
|
+
require 'trollop'
|
132
364
|
|
133
|
-
|
134
|
-
|
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
|
-
|
137
|
-
|
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
|
-
|
142
|
-
FFI::LastError.raise if ret == -1
|
143
|
-
end
|
376
|
+
to use, simply `require "rbtrace"` in your ruby app.
|
144
377
|
|
145
|
-
|
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
|
-
|
154
|
-
|
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
|
-
|
157
|
-
end
|
158
|
-
end
|
386
|
+
Tracers:
|
159
387
|
|
160
|
-
|
161
|
-
|
162
|
-
|
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
|
-
|
170
|
-
extend FFI::Library
|
392
|
+
Method Selectors:
|
171
393
|
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
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
|
-
|
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
|
-
|
184
|
-
|
185
|
-
|
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
|
-
|
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
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
end
|
410
|
+
EOS
|
411
|
+
opt :pid,
|
412
|
+
"pid of the ruby process to trace",
|
413
|
+
:short => '-p',
|
414
|
+
:type => :int
|
200
415
|
|
201
|
-
|
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
|
-
|
217
|
-
|
218
|
-
|
219
|
-
end
|
419
|
+
opt :slow,
|
420
|
+
"watch for method calls slower than 250 milliseconds",
|
421
|
+
:default => 250
|
220
422
|
|
221
|
-
|
222
|
-
|
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
|
-
|
225
|
-
|
226
|
-
|
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
|
-
|
240
|
-
|
432
|
+
opt :no_duration,
|
433
|
+
"hide time spent in each method call",
|
434
|
+
:default => false
|
241
435
|
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
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
|
-
|
252
|
-
|
253
|
-
|
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
|
-
|
256
|
-
|
257
|
-
|
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
|
-
|
263
|
-
|
264
|
-
|
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
|
-
|
268
|
-
|
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
|