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