rbtrace 0.1.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 +115 -0
- data/bin/rbtrace +270 -0
- data/ext/extconf.rb +8 -0
- data/ext/rbtrace.c +593 -0
- data/ext/test.rb +57 -0
- data/rbtrace.gemspec +18 -0
- data/server.rb +18 -0
- metadata +87 -0
data/Gemfile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
gemspec
|
data/README.md
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
# rbtrace
|
2
|
+
|
3
|
+
like strace, but for ruby code
|
4
|
+
|
5
|
+
## usage
|
6
|
+
|
7
|
+
### require rbtrace into a process
|
8
|
+
|
9
|
+
% cat server.rb
|
10
|
+
require 'ext/rbtrace'
|
11
|
+
|
12
|
+
class String
|
13
|
+
def multiply_vowels(num)
|
14
|
+
gsub(/[aeiou]/){ |m| m*num }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
while true
|
19
|
+
proc {
|
20
|
+
Dir.chdir("/tmp") do
|
21
|
+
Dir.pwd
|
22
|
+
Process.pid
|
23
|
+
'hello'.multiply_vowels(3)
|
24
|
+
sleep rand*0.5
|
25
|
+
end
|
26
|
+
}.call
|
27
|
+
end
|
28
|
+
|
29
|
+
### run the process
|
30
|
+
|
31
|
+
% ruby server.rb &
|
32
|
+
[1] 95532
|
33
|
+
|
34
|
+
### trace a function using the process's pid
|
35
|
+
|
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#*"
|
52
|
+
|
53
|
+
Dir.chdir
|
54
|
+
Dir.pwd <0.000094>
|
55
|
+
Process.pid <0.000016>
|
56
|
+
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>
|
62
|
+
|
63
|
+
Dir.chdir
|
64
|
+
Dir.pwd <0.000088>
|
65
|
+
Process.pid <0.000017>
|
66
|
+
String#gsub
|
67
|
+
String#* <0.000020>
|
68
|
+
String#* <0.000020>
|
69
|
+
String#gsub <0.000071>
|
70
|
+
^C./bin/rbtrace:113: Interrupt
|
71
|
+
|
72
|
+
### trace all functions in a class/module
|
73
|
+
|
74
|
+
% ./bin/rbtrace $! "Kernel#"
|
75
|
+
|
76
|
+
Kernel#proc <0.000071>
|
77
|
+
Kernel#rand <0.000029>
|
78
|
+
Kernel#sleep <0.331857>
|
79
|
+
Kernel#proc <0.000019>
|
80
|
+
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
|
86
|
+
|
87
|
+
### get values of variables and other expressions
|
88
|
+
|
89
|
+
% ./bin/rbtrace $! "String#gsub(self)" "String#*(self)" "String#multiply_vowels(self, self.length, num)"
|
90
|
+
|
91
|
+
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>
|
97
|
+
|
98
|
+
^C./bin/rbtrace:113: Interrupt
|
99
|
+
|
100
|
+
### watch for method calls slower than 250ms
|
101
|
+
|
102
|
+
% ./bin/rbtrace $! watch 250
|
103
|
+
Kernel#sleep <0.402916>
|
104
|
+
Dir.chdir <0.403122>
|
105
|
+
Proc#call <0.403152>
|
106
|
+
|
107
|
+
Kernel#sleep <0.390635>
|
108
|
+
Dir.chdir <0.390937>
|
109
|
+
Proc#call <0.390983>
|
110
|
+
|
111
|
+
Kernel#sleep <0.399413>
|
112
|
+
Dir.chdir <0.399753>
|
113
|
+
Proc#call <0.399797>
|
114
|
+
|
115
|
+
^C./bin/rbtrace:113: Interrupt
|
data/bin/rbtrace
ADDED
@@ -0,0 +1,270 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rubygems'
|
3
|
+
require 'ffi'
|
4
|
+
|
5
|
+
module RBTracer
|
6
|
+
def self.process_line(line)
|
7
|
+
@tracers ||= {}
|
8
|
+
@nesting ||= 0
|
9
|
+
@last_tracer ||= nil
|
10
|
+
|
11
|
+
time, event, id, *args = line.strip.split(',')
|
12
|
+
time = time.to_i
|
13
|
+
id = id.to_i
|
14
|
+
tracer = @tracers[id] if id > -1
|
15
|
+
|
16
|
+
case event
|
17
|
+
when 'add'
|
18
|
+
if id == -1
|
19
|
+
puts line
|
20
|
+
else
|
21
|
+
name = args.first
|
22
|
+
@tracers[id] = {
|
23
|
+
:name => name,
|
24
|
+
:times => [],
|
25
|
+
:names => [],
|
26
|
+
:exprs => {},
|
27
|
+
:last => false,
|
28
|
+
:arglist => false
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
when 'remove'
|
33
|
+
if id == -1
|
34
|
+
puts line
|
35
|
+
else
|
36
|
+
@tracers.delete(id)
|
37
|
+
end
|
38
|
+
|
39
|
+
when 'newexpr'
|
40
|
+
expr_id, expr = *args
|
41
|
+
expr_id = expr_id.to_i
|
42
|
+
|
43
|
+
if expr_id > -1 and tracer
|
44
|
+
tracer[:exprs][expr_id] = expr
|
45
|
+
end
|
46
|
+
|
47
|
+
when 'exprval'
|
48
|
+
expr_id, val = *args
|
49
|
+
expr_id = expr_id.to_i
|
50
|
+
expr = tracer[:exprs][expr_id]
|
51
|
+
|
52
|
+
if tracer[:arglist]
|
53
|
+
print ', '
|
54
|
+
else
|
55
|
+
print '('
|
56
|
+
end
|
57
|
+
|
58
|
+
print "#{expr}="
|
59
|
+
print val
|
60
|
+
tracer[:arglist] = true
|
61
|
+
|
62
|
+
when 'call','ccall'
|
63
|
+
method, is_singleton, klass = *args
|
64
|
+
is_singleton = (is_singleton == '1')
|
65
|
+
name = klass ? "#{klass}#{ is_singleton ? '.' : '#' }" : ''
|
66
|
+
name += method
|
67
|
+
|
68
|
+
tracer[:times] << time
|
69
|
+
tracer[:names] << name
|
70
|
+
|
71
|
+
if @last_tracer and @last_tracer[:arglist]
|
72
|
+
print ')'
|
73
|
+
@last_tracer[:arglist] = false
|
74
|
+
end
|
75
|
+
puts
|
76
|
+
print ' '*@nesting if @nesting > 0
|
77
|
+
print name
|
78
|
+
|
79
|
+
@nesting += 1
|
80
|
+
@last_tracer = tracer
|
81
|
+
tracer[:last] = name
|
82
|
+
|
83
|
+
when 'return','creturn'
|
84
|
+
@nesting -= 1 if @nesting > 0
|
85
|
+
|
86
|
+
if start = tracer[:times].pop
|
87
|
+
name = tracer[:names].pop
|
88
|
+
diff = time - start
|
89
|
+
@last_tracer[:arglist] = false if @last_tracer and @last_tracer[:last] != name
|
90
|
+
|
91
|
+
print ')' if @last_tracer and @last_tracer[:arglist]
|
92
|
+
|
93
|
+
unless tracer == @last_tracer and @last_tracer[:last] == name
|
94
|
+
puts
|
95
|
+
print ' '*@nesting if @nesting > 0
|
96
|
+
print name
|
97
|
+
end
|
98
|
+
print ' <%f>' % (diff/1_000_000.0)
|
99
|
+
puts if @nesting == 0 and (tracer != @last_tracer || @last_tracer[:last] != name)
|
100
|
+
end
|
101
|
+
|
102
|
+
tracer[:arglist] = false
|
103
|
+
|
104
|
+
when 'slow', 'cslow'
|
105
|
+
diff, nesting, method, is_singleton, klass = *args
|
106
|
+
nesting = nesting.to_i
|
107
|
+
diff = diff.to_i
|
108
|
+
|
109
|
+
is_singleton = (is_singleton == '1')
|
110
|
+
name = klass ? "#{klass}#{ is_singleton ? '.' : '#' }" : ''
|
111
|
+
name += method
|
112
|
+
|
113
|
+
print ' '*nesting if nesting > 0
|
114
|
+
print name
|
115
|
+
print ' '
|
116
|
+
puts "<%f>" % (diff/1_000_000.0)
|
117
|
+
puts if nesting == 0
|
118
|
+
|
119
|
+
else
|
120
|
+
puts "unknown event: #{line}"
|
121
|
+
|
122
|
+
end
|
123
|
+
rescue => e
|
124
|
+
puts "error on: #{line}"
|
125
|
+
raise e
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
class EventMsg < FFI::Struct
|
130
|
+
BUF_SIZE = RUBY_PLATFORM =~ /linux/ ? 256 : 120
|
131
|
+
IPC_NOWAIT = 004000
|
132
|
+
|
133
|
+
layout :mtype, :long,
|
134
|
+
:buf, [:char, BUF_SIZE]
|
135
|
+
|
136
|
+
def self.send_cmd(q, str)
|
137
|
+
msg = EventMsg.new
|
138
|
+
msg[:mtype] = 1
|
139
|
+
msg[:buf].to_ptr.put_string(0, str)
|
140
|
+
|
141
|
+
ret = MsgQ.msgsnd(q, msg, BUF_SIZE, 0)
|
142
|
+
FFI::LastError.raise if ret == -1
|
143
|
+
end
|
144
|
+
|
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
|
152
|
+
|
153
|
+
FFI::LastError.raise
|
154
|
+
end
|
155
|
+
|
156
|
+
msg[:buf].to_ptr.read_string
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
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
|
168
|
+
|
169
|
+
module MsgQ
|
170
|
+
extend FFI::Library
|
171
|
+
|
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
|
177
|
+
|
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/)
|
182
|
+
|
183
|
+
qi = ipci.to_i(16)
|
184
|
+
qo = 0xffffffff - qi + 1
|
185
|
+
ipco = "0x#{qo.to_s(16)}"
|
186
|
+
|
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
|
193
|
+
|
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
|
200
|
+
|
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
|
215
|
+
|
216
|
+
funcs = [ARGV[1] || 'sleep']
|
217
|
+
if ARGV.size > 2
|
218
|
+
funcs += ARGV[2..-1]
|
219
|
+
end
|
220
|
+
|
221
|
+
qi = MsgQ.msgget(pid, 0666)
|
222
|
+
qo = MsgQ.msgget(-pid, 0666)
|
223
|
+
|
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
|
238
|
+
|
239
|
+
EventMsg.send_cmd(qo, "add,#{func}")
|
240
|
+
Process.kill 'URG', pid
|
241
|
+
|
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
|
249
|
+
end
|
250
|
+
|
251
|
+
while true
|
252
|
+
# block until a message arrives
|
253
|
+
lines = [EventMsg.recv_cmd(qi)]
|
254
|
+
|
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
|
261
|
+
|
262
|
+
lines.each do |line|
|
263
|
+
RBTracer.process_line(line)
|
264
|
+
end
|
265
|
+
end
|
266
|
+
ensure
|
267
|
+
EventMsg.send_cmd(qo, funcs.first == 'watch' ? 'unwatch' : 'delall')
|
268
|
+
Process.kill 'URG', pid
|
269
|
+
end
|
270
|
+
end
|
data/ext/extconf.rb
ADDED
data/ext/rbtrace.c
ADDED
@@ -0,0 +1,593 @@
|
|
1
|
+
#include <inttypes.h>
|
2
|
+
#include <errno.h>
|
3
|
+
#include <signal.h>
|
4
|
+
#include <stdbool.h>
|
5
|
+
#include <stdio.h>
|
6
|
+
#include <stdint.h>
|
7
|
+
#include <stdlib.h>
|
8
|
+
#include <string.h>
|
9
|
+
#include <strings.h>
|
10
|
+
#include <sys/ipc.h>
|
11
|
+
#include <sys/msg.h>
|
12
|
+
#include <sys/time.h>
|
13
|
+
#include <sys/types.h>
|
14
|
+
#include <time.h>
|
15
|
+
#include <unistd.h>
|
16
|
+
|
17
|
+
#include <ruby.h>
|
18
|
+
|
19
|
+
#ifndef RUBY_VM
|
20
|
+
#include <node.h>
|
21
|
+
#include <intern.h>
|
22
|
+
#endif
|
23
|
+
|
24
|
+
#ifndef RSTRING_PTR
|
25
|
+
#define RSTRING_PTR(str) RSTRING(str)->ptr
|
26
|
+
#endif
|
27
|
+
#ifndef RSTRING_LEN
|
28
|
+
#define RSTRING_LEN(str) RSTRING(str)->len
|
29
|
+
#endif
|
30
|
+
|
31
|
+
static uint64_t
|
32
|
+
timeofday_usec()
|
33
|
+
{
|
34
|
+
struct timeval tv;
|
35
|
+
gettimeofday(&tv, NULL);
|
36
|
+
return (uint64_t)tv.tv_sec*1e6 + (uint64_t)tv.tv_usec;
|
37
|
+
}
|
38
|
+
|
39
|
+
#define MAX_EXPRS 10
|
40
|
+
struct rbtracer_t {
|
41
|
+
int id;
|
42
|
+
|
43
|
+
char *query;
|
44
|
+
VALUE self;
|
45
|
+
VALUE klass;
|
46
|
+
ID mid;
|
47
|
+
|
48
|
+
int num_exprs;
|
49
|
+
char *exprs[MAX_EXPRS];
|
50
|
+
};
|
51
|
+
typedef struct rbtracer_t rbtracer_t;
|
52
|
+
|
53
|
+
#define MAX_TRACERS 100
|
54
|
+
static rbtracer_t tracers[MAX_TRACERS];
|
55
|
+
static unsigned int num_tracers = 0;
|
56
|
+
|
57
|
+
key_t mqi_key, mqo_key;
|
58
|
+
int mqi_id = -1, mqo_id = -1;
|
59
|
+
|
60
|
+
#ifndef BUF_SIZE
|
61
|
+
#define BUF_SIZE 120
|
62
|
+
#endif
|
63
|
+
|
64
|
+
struct event_msg {
|
65
|
+
long mtype;
|
66
|
+
char buf[BUF_SIZE];
|
67
|
+
};
|
68
|
+
|
69
|
+
#define SEND_EVENT(format, ...) do {\
|
70
|
+
uint64_t usec = timeofday_usec();\
|
71
|
+
if (false) {\
|
72
|
+
fprintf(stderr, "%" PRIu64 "," format, usec, __VA_ARGS__);\
|
73
|
+
fprintf(stderr, "\n");\
|
74
|
+
} else {\
|
75
|
+
struct event_msg msg;\
|
76
|
+
int ret = -1, n = 0;\
|
77
|
+
\
|
78
|
+
msg.mtype = 1;\
|
79
|
+
snprintf(msg.buf, sizeof(msg.buf), "%" PRIu64 "," format, usec, __VA_ARGS__);\
|
80
|
+
\
|
81
|
+
for (n=0; n<10 && ret==-1; n++)\
|
82
|
+
ret = msgsnd(mqo_id, &msg, sizeof(msg)-sizeof(long), IPC_NOWAIT);\
|
83
|
+
if (ret == -1) {\
|
84
|
+
fprintf(stderr, "msgsnd(): %s\n", strerror(errno));\
|
85
|
+
struct msqid_ds stat;\
|
86
|
+
msgctl(mqo_id, IPC_STAT, &stat);\
|
87
|
+
fprintf(stderr, "cbytes: %lu, qbytes: %lu, qnum: %lu\n", stat.msg_cbytes, stat.msg_qbytes, stat.msg_qnum);\
|
88
|
+
}\
|
89
|
+
}\
|
90
|
+
} while (0)
|
91
|
+
|
92
|
+
static int in_event_hook = 0;
|
93
|
+
static bool event_hook_installed = false;
|
94
|
+
|
95
|
+
#define MAX_CALLS 32768
|
96
|
+
static struct {
|
97
|
+
bool enabled;
|
98
|
+
uint64_t call_times[ MAX_CALLS ];
|
99
|
+
int num_calls;
|
100
|
+
uint32_t threshold;
|
101
|
+
} slow_tracer = {
|
102
|
+
.enabled = false,
|
103
|
+
.num_calls = 0,
|
104
|
+
.threshold = 250
|
105
|
+
};
|
106
|
+
|
107
|
+
static void
|
108
|
+
#ifdef RUBY_VM
|
109
|
+
event_hook(rb_event_flag_t event, VALUE data, VALUE self, ID mid, VALUE klass)
|
110
|
+
#else
|
111
|
+
event_hook(rb_event_t event, NODE *node, VALUE self, ID mid, VALUE klass)
|
112
|
+
#endif
|
113
|
+
{
|
114
|
+
// do not re-enter this function
|
115
|
+
// after this, must `goto out` instead of `return`
|
116
|
+
if (in_event_hook) return;
|
117
|
+
in_event_hook++;
|
118
|
+
|
119
|
+
// skip allocators
|
120
|
+
if (mid == ID_ALLOCATOR) goto out;
|
121
|
+
|
122
|
+
// normalize klass and check for class-level methods
|
123
|
+
bool singleton = 0;
|
124
|
+
if (klass) {
|
125
|
+
if (TYPE(klass) == T_ICLASS) {
|
126
|
+
klass = RBASIC(klass)->klass;
|
127
|
+
}
|
128
|
+
singleton = FL_TEST(klass, FL_SINGLETON);
|
129
|
+
}
|
130
|
+
|
131
|
+
// are we watching for any slow methods?
|
132
|
+
if (slow_tracer.enabled) {
|
133
|
+
uint64_t usec = timeofday_usec(), diff = 0;
|
134
|
+
|
135
|
+
switch (event) {
|
136
|
+
case RUBY_EVENT_C_CALL:
|
137
|
+
case RUBY_EVENT_CALL:
|
138
|
+
if (slow_tracer.num_calls < MAX_CALLS)
|
139
|
+
slow_tracer.call_times[ slow_tracer.num_calls ] = usec;
|
140
|
+
|
141
|
+
slow_tracer.num_calls++;
|
142
|
+
break;
|
143
|
+
|
144
|
+
case RUBY_EVENT_C_RETURN:
|
145
|
+
case RUBY_EVENT_RETURN:
|
146
|
+
if (slow_tracer.num_calls > 0) {
|
147
|
+
slow_tracer.num_calls--;
|
148
|
+
|
149
|
+
if (slow_tracer.num_calls < MAX_CALLS)
|
150
|
+
diff = usec - slow_tracer.call_times[ slow_tracer.num_calls ];
|
151
|
+
}
|
152
|
+
break;
|
153
|
+
}
|
154
|
+
|
155
|
+
if (diff > slow_tracer.threshold * 1e3) {
|
156
|
+
SEND_EVENT(
|
157
|
+
"%s,-1,%" PRIu64 ",%d,%s,%d,%s",
|
158
|
+
event == RUBY_EVENT_RETURN ? "slow" : "cslow",
|
159
|
+
diff,
|
160
|
+
slow_tracer.num_calls,
|
161
|
+
rb_id2name(mid),
|
162
|
+
singleton,
|
163
|
+
klass ? rb_class2name(singleton ? self : klass) : ""
|
164
|
+
);
|
165
|
+
}
|
166
|
+
|
167
|
+
goto out;
|
168
|
+
}
|
169
|
+
|
170
|
+
// are there specific methods we're waiting for?
|
171
|
+
if (num_tracers == 0) goto out;
|
172
|
+
|
173
|
+
int i, n;
|
174
|
+
rbtracer_t *tracer = NULL;
|
175
|
+
|
176
|
+
for (i=0, n=0; i<MAX_TRACERS && n<num_tracers; i++) {
|
177
|
+
if (tracers[i].query) {
|
178
|
+
n++;
|
179
|
+
|
180
|
+
if (!tracers[i].mid || tracers[i].mid == mid) {
|
181
|
+
if (!tracers[i].klass || tracers[i].klass == klass) {
|
182
|
+
if (!tracers[i].self || tracers[i].self == self) {
|
183
|
+
tracer = &tracers[i];
|
184
|
+
}
|
185
|
+
}
|
186
|
+
}
|
187
|
+
}
|
188
|
+
}
|
189
|
+
|
190
|
+
// no matching method tracer found, so bail!
|
191
|
+
if (!tracer) goto out;
|
192
|
+
|
193
|
+
switch (event) {
|
194
|
+
case RUBY_EVENT_C_CALL:
|
195
|
+
case RUBY_EVENT_CALL:
|
196
|
+
SEND_EVENT(
|
197
|
+
"%s,%d,%s,%d,%s",
|
198
|
+
event == RUBY_EVENT_CALL ? "call" : "ccall",
|
199
|
+
tracer->id,
|
200
|
+
rb_id2name(mid),
|
201
|
+
singleton,
|
202
|
+
klass ? rb_class2name(singleton ? self : klass) : ""
|
203
|
+
);
|
204
|
+
|
205
|
+
if (tracer->num_exprs) {
|
206
|
+
for (i=0; i<tracer->num_exprs; i++) {
|
207
|
+
char *expr = tracer->exprs[i];
|
208
|
+
size_t len = strlen(expr);
|
209
|
+
VALUE str = Qnil, val = Qnil;
|
210
|
+
|
211
|
+
if (len == 4 && strcmp("self", expr) == 0) {
|
212
|
+
val = rb_inspect(self);
|
213
|
+
|
214
|
+
} else if (event == RUBY_EVENT_CALL) {
|
215
|
+
char code[len+50];
|
216
|
+
snprintf(code, len+50, "(begin; %s; rescue Exception => e; e; end).inspect", expr);
|
217
|
+
|
218
|
+
str = rb_str_new2(code);
|
219
|
+
val = rb_obj_instance_eval(1, &str, self);
|
220
|
+
}
|
221
|
+
|
222
|
+
if (RTEST(val) && TYPE(val) == T_STRING) {
|
223
|
+
char *result = RSTRING_PTR(val);
|
224
|
+
SEND_EVENT(
|
225
|
+
"%s,%d,%d,%s",
|
226
|
+
"exprval",
|
227
|
+
tracer->id,
|
228
|
+
i,
|
229
|
+
result
|
230
|
+
);
|
231
|
+
}
|
232
|
+
}
|
233
|
+
}
|
234
|
+
break;
|
235
|
+
|
236
|
+
case RUBY_EVENT_C_RETURN:
|
237
|
+
case RUBY_EVENT_RETURN:
|
238
|
+
SEND_EVENT(
|
239
|
+
"%s,%d",
|
240
|
+
event == RUBY_EVENT_RETURN ? "return" : "creturn",
|
241
|
+
tracer->id
|
242
|
+
);
|
243
|
+
break;
|
244
|
+
}
|
245
|
+
|
246
|
+
out:
|
247
|
+
in_event_hook--;
|
248
|
+
}
|
249
|
+
|
250
|
+
static void
|
251
|
+
event_hook_install()
|
252
|
+
{
|
253
|
+
if (!event_hook_installed) {
|
254
|
+
rb_add_event_hook(
|
255
|
+
event_hook,
|
256
|
+
RUBY_EVENT_CALL | RUBY_EVENT_C_CALL |
|
257
|
+
RUBY_EVENT_RETURN | RUBY_EVENT_C_RETURN
|
258
|
+
#ifdef RB_EVENT_HOOKS_HAVE_CALLBACK_DATA
|
259
|
+
, 0
|
260
|
+
#endif
|
261
|
+
);
|
262
|
+
event_hook_installed = true;
|
263
|
+
}
|
264
|
+
}
|
265
|
+
|
266
|
+
static void
|
267
|
+
event_hook_remove()
|
268
|
+
{
|
269
|
+
if (event_hook_installed) {
|
270
|
+
rb_remove_event_hook(event_hook);
|
271
|
+
event_hook_installed = false;
|
272
|
+
}
|
273
|
+
}
|
274
|
+
|
275
|
+
static int
|
276
|
+
rbtracer_remove(char *query, int id)
|
277
|
+
{
|
278
|
+
int i;
|
279
|
+
int tracer_id = -1;
|
280
|
+
rbtracer_t *tracer = NULL;
|
281
|
+
|
282
|
+
if (query) {
|
283
|
+
for (i=0; i<MAX_TRACERS; i++) {
|
284
|
+
if (tracers[i].query) {
|
285
|
+
if (0 == strcmp(query, tracers[i].query)) {
|
286
|
+
tracer = &tracers[i];
|
287
|
+
break;
|
288
|
+
}
|
289
|
+
}
|
290
|
+
}
|
291
|
+
} else {
|
292
|
+
if (id >= MAX_TRACERS) goto out;
|
293
|
+
tracer = &tracers[id];
|
294
|
+
}
|
295
|
+
|
296
|
+
if (tracer->query) {
|
297
|
+
tracer_id = tracer->id;
|
298
|
+
tracer->mid = 0;
|
299
|
+
|
300
|
+
free(tracer->query);
|
301
|
+
tracer->query = NULL;
|
302
|
+
|
303
|
+
if (tracer->num_exprs) {
|
304
|
+
for(i=0; i<tracer->num_exprs; i++) {
|
305
|
+
free(tracer->exprs[i]);
|
306
|
+
tracer->exprs[i] = NULL;
|
307
|
+
}
|
308
|
+
tracer->num_exprs = 0;
|
309
|
+
}
|
310
|
+
|
311
|
+
num_tracers--;
|
312
|
+
if (num_tracers == 0)
|
313
|
+
event_hook_remove();
|
314
|
+
}
|
315
|
+
|
316
|
+
out:
|
317
|
+
SEND_EVENT(
|
318
|
+
"remove,%d,%s",
|
319
|
+
tracer_id,
|
320
|
+
query
|
321
|
+
);
|
322
|
+
return tracer_id;
|
323
|
+
}
|
324
|
+
|
325
|
+
static void
|
326
|
+
rbtracer_remove_all()
|
327
|
+
{
|
328
|
+
int i;
|
329
|
+
for (i=0; i<MAX_TRACERS; i++) {
|
330
|
+
if (tracers[i].query) {
|
331
|
+
rbtracer_remove(NULL, i);
|
332
|
+
}
|
333
|
+
}
|
334
|
+
}
|
335
|
+
|
336
|
+
static int
|
337
|
+
rbtracer_add(char *query)
|
338
|
+
{
|
339
|
+
int i;
|
340
|
+
int tracer_id = -1;
|
341
|
+
rbtracer_t *tracer = NULL;
|
342
|
+
|
343
|
+
if (num_tracers >= MAX_TRACERS) goto out;
|
344
|
+
|
345
|
+
for (i=0; i<MAX_TRACERS; i++) {
|
346
|
+
if (!tracers[i].query) {
|
347
|
+
tracer = &tracers[i];
|
348
|
+
tracer_id = i;
|
349
|
+
break;
|
350
|
+
}
|
351
|
+
}
|
352
|
+
if (!tracer) goto out;
|
353
|
+
|
354
|
+
char *idx, *method;
|
355
|
+
VALUE klass = 0, self = 0;
|
356
|
+
ID mid = 0;
|
357
|
+
|
358
|
+
if (NULL != (idx = rindex(query, '.'))) {
|
359
|
+
*idx = 0;
|
360
|
+
self = rb_eval_string_protect(query, 0);
|
361
|
+
*idx = '.';
|
362
|
+
|
363
|
+
method = idx+1;
|
364
|
+
} else if (NULL != (idx = rindex(query, '#'))) {
|
365
|
+
*idx = 0;
|
366
|
+
klass = rb_eval_string_protect(query, 0);
|
367
|
+
*idx = '#';
|
368
|
+
|
369
|
+
method = idx+1;
|
370
|
+
} else {
|
371
|
+
method = query;
|
372
|
+
}
|
373
|
+
|
374
|
+
if (method && *method) {
|
375
|
+
mid = rb_intern(method);
|
376
|
+
if (!mid) goto out;
|
377
|
+
} else if (klass || self) {
|
378
|
+
mid = 0;
|
379
|
+
} else {
|
380
|
+
goto out;
|
381
|
+
}
|
382
|
+
|
383
|
+
memset(tracer, 0, sizeof(*tracer));
|
384
|
+
|
385
|
+
tracer->id = tracer_id;
|
386
|
+
tracer->self = self;
|
387
|
+
tracer->klass = klass;
|
388
|
+
tracer->mid = mid;
|
389
|
+
tracer->query = strdup(query);
|
390
|
+
tracer->num_exprs = 0;
|
391
|
+
|
392
|
+
if (num_tracers == 0)
|
393
|
+
event_hook_install();
|
394
|
+
|
395
|
+
num_tracers++;
|
396
|
+
|
397
|
+
out:
|
398
|
+
SEND_EVENT(
|
399
|
+
"add,%d,%s",
|
400
|
+
tracer_id,
|
401
|
+
query
|
402
|
+
);
|
403
|
+
return tracer_id;
|
404
|
+
}
|
405
|
+
|
406
|
+
static void
|
407
|
+
rbtracer_add_expr(int id, char *expr)
|
408
|
+
{
|
409
|
+
int expr_id = -1;
|
410
|
+
int tracer_id = -1;
|
411
|
+
rbtracer_t *tracer = NULL;
|
412
|
+
|
413
|
+
if (id >= MAX_TRACERS) goto out;
|
414
|
+
tracer = &tracers[id];
|
415
|
+
|
416
|
+
if (tracer->query) {
|
417
|
+
tracer_id = tracer->id;
|
418
|
+
|
419
|
+
if (tracer->num_exprs < MAX_EXPRS) {
|
420
|
+
expr_id = tracer->num_exprs++;
|
421
|
+
tracer->exprs[expr_id] = strdup(expr);
|
422
|
+
}
|
423
|
+
}
|
424
|
+
|
425
|
+
out:
|
426
|
+
SEND_EVENT(
|
427
|
+
"newexpr,%d,%d,%s",
|
428
|
+
tracer_id,
|
429
|
+
expr_id,
|
430
|
+
expr
|
431
|
+
);
|
432
|
+
}
|
433
|
+
|
434
|
+
static void
|
435
|
+
rbtracer_watch(uint32_t threshold)
|
436
|
+
{
|
437
|
+
if (!slow_tracer.enabled) {
|
438
|
+
slow_tracer.num_calls = 0;
|
439
|
+
slow_tracer.threshold = threshold;
|
440
|
+
slow_tracer.enabled = true;
|
441
|
+
|
442
|
+
event_hook_install();
|
443
|
+
}
|
444
|
+
}
|
445
|
+
|
446
|
+
static void
|
447
|
+
rbtracer_unwatch()
|
448
|
+
{
|
449
|
+
if (slow_tracer.enabled) {
|
450
|
+
event_hook_remove();
|
451
|
+
|
452
|
+
slow_tracer.enabled = false;
|
453
|
+
}
|
454
|
+
}
|
455
|
+
|
456
|
+
static VALUE
|
457
|
+
rbtrace(VALUE self, VALUE query)
|
458
|
+
{
|
459
|
+
Check_Type(query, T_STRING);
|
460
|
+
|
461
|
+
char *str = RSTRING_PTR(query);
|
462
|
+
int tracer_id = -1;
|
463
|
+
|
464
|
+
tracer_id = rbtracer_add(str);
|
465
|
+
return tracer_id == -1 ? Qfalse : Qtrue;
|
466
|
+
}
|
467
|
+
|
468
|
+
static VALUE
|
469
|
+
untrace(VALUE self, VALUE query)
|
470
|
+
{
|
471
|
+
Check_Type(query, T_STRING);
|
472
|
+
|
473
|
+
char *str = RSTRING_PTR(query);
|
474
|
+
int tracer_id = -1;
|
475
|
+
|
476
|
+
tracer_id = rbtracer_remove(str, -1);
|
477
|
+
return tracer_id == -1 ? Qfalse : Qtrue;
|
478
|
+
}
|
479
|
+
|
480
|
+
static void
|
481
|
+
cleanup()
|
482
|
+
{
|
483
|
+
if (mqo_id != -1) {
|
484
|
+
msgctl(mqo_id, IPC_RMID, NULL);
|
485
|
+
mqo_id = -1;
|
486
|
+
}
|
487
|
+
if (mqi_id != -1) {
|
488
|
+
msgctl(mqi_id, IPC_RMID, NULL);
|
489
|
+
mqi_id = -1;
|
490
|
+
}
|
491
|
+
}
|
492
|
+
|
493
|
+
static void
|
494
|
+
cleanup_ruby(VALUE data)
|
495
|
+
{
|
496
|
+
cleanup();
|
497
|
+
}
|
498
|
+
|
499
|
+
static void
|
500
|
+
sigurg(int signal)
|
501
|
+
{
|
502
|
+
static int last_tracer_id = -1; // hax
|
503
|
+
if (mqi_id == -1) return;
|
504
|
+
|
505
|
+
struct event_msg msg;
|
506
|
+
char *query = NULL;
|
507
|
+
size_t len = 0;
|
508
|
+
int n = 0;
|
509
|
+
|
510
|
+
while (true) {
|
511
|
+
int ret = -1;
|
512
|
+
|
513
|
+
for (n=0; n<10 && ret==-1; n++)
|
514
|
+
ret = msgrcv(mqi_id, &msg, sizeof(msg)-sizeof(long), 0, IPC_NOWAIT);
|
515
|
+
|
516
|
+
if (ret == -1) {
|
517
|
+
break;
|
518
|
+
} else {
|
519
|
+
len = strlen(msg.buf);
|
520
|
+
if (msg.buf[len-1] == '\n')
|
521
|
+
msg.buf[len-1] = 0;
|
522
|
+
|
523
|
+
if (0 == strncmp("add,", msg.buf, 4)) {
|
524
|
+
query = msg.buf + 4;
|
525
|
+
last_tracer_id = rbtracer_add(query);
|
526
|
+
|
527
|
+
} else if (0 == strncmp("del,", msg.buf, 4)) {
|
528
|
+
query = msg.buf + 4;
|
529
|
+
rbtracer_remove(query, -1);
|
530
|
+
|
531
|
+
} else if (0 == strncmp("delall", msg.buf, 6)) {
|
532
|
+
rbtracer_remove_all();
|
533
|
+
|
534
|
+
} else if (0 == strncmp("addexpr,", msg.buf, 8)) {
|
535
|
+
query = msg.buf + 8;
|
536
|
+
rbtracer_add_expr(last_tracer_id, query);
|
537
|
+
|
538
|
+
} else if (0 == strncmp("watch,", msg.buf, 6)) {
|
539
|
+
int msec = 250;
|
540
|
+
|
541
|
+
query = msg.buf + 6;
|
542
|
+
if (query && *query)
|
543
|
+
msec = atoi(query);
|
544
|
+
|
545
|
+
rbtracer_watch(msec);
|
546
|
+
|
547
|
+
} else if (0 == strncmp("unwatch", msg.buf, 7)) {
|
548
|
+
rbtracer_unwatch();
|
549
|
+
|
550
|
+
}
|
551
|
+
}
|
552
|
+
}
|
553
|
+
}
|
554
|
+
|
555
|
+
void
|
556
|
+
Init_rbtrace()
|
557
|
+
{
|
558
|
+
atexit(cleanup);
|
559
|
+
rb_set_end_proc(cleanup_ruby, 0);
|
560
|
+
|
561
|
+
signal(SIGURG, sigurg);
|
562
|
+
|
563
|
+
memset(&tracers, 0, sizeof(tracers));
|
564
|
+
|
565
|
+
mqo_key = (key_t) getpid();
|
566
|
+
mqo_id = msgget(mqo_key, 0666 | IPC_CREAT);
|
567
|
+
if (mqo_id == -1)
|
568
|
+
rb_sys_fail("msgget");
|
569
|
+
|
570
|
+
mqi_key = (key_t) -getpid();
|
571
|
+
mqi_id = msgget(mqi_key, 0666 | IPC_CREAT);
|
572
|
+
if (mqi_id == -1)
|
573
|
+
rb_sys_fail("msgget");
|
574
|
+
|
575
|
+
/*
|
576
|
+
struct msqid_ds stat;
|
577
|
+
int ret;
|
578
|
+
|
579
|
+
msgctl(mqo_id, IPC_STAT, &stat);
|
580
|
+
printf("cbytes: %lu, qbytes: %lu, qnum: %lu\n", stat.msg_cbytes, stat.msg_qbytes, stat.msg_qnum);
|
581
|
+
|
582
|
+
stat.msg_qbytes += 10;
|
583
|
+
ret = msgctl(mqo_id, IPC_SET, &stat);
|
584
|
+
printf("cbytes: %lu, qbytes: %lu, qnum: %lu\n", stat.msg_cbytes, stat.msg_qbytes, stat.msg_qnum);
|
585
|
+
printf("ret: %d, errno: %d\n", ret, errno);
|
586
|
+
|
587
|
+
msgctl(mqo_id, IPC_STAT, &stat);
|
588
|
+
printf("cbytes: %lu, qbytes: %lu, qnum: %lu\n", stat.msg_cbytes, stat.msg_qbytes, stat.msg_qnum);
|
589
|
+
*/
|
590
|
+
|
591
|
+
rb_define_method(rb_cObject, "rbtrace", rbtrace, 1);
|
592
|
+
rb_define_method(rb_cObject, "untrace", untrace, 1);
|
593
|
+
}
|
data/ext/test.rb
ADDED
@@ -0,0 +1,57 @@
|
|
1
|
+
class Test
|
2
|
+
def call
|
3
|
+
self[:a] = :b
|
4
|
+
end
|
5
|
+
def []=(k,v)
|
6
|
+
Another.new.call
|
7
|
+
:ok
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class Another
|
12
|
+
def call
|
13
|
+
self[:a] = :b
|
14
|
+
end
|
15
|
+
def []=(key, value)
|
16
|
+
(@hash ||= {})[key]=value
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module Do
|
21
|
+
module It
|
22
|
+
def something
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class Slow
|
28
|
+
include Do::It
|
29
|
+
def self.something
|
30
|
+
sleep 0.01
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
require 'rbtrace'
|
35
|
+
rbtrace 'Slow.something'
|
36
|
+
rbtrace 'Do::It#something'
|
37
|
+
rbtrace '[]='
|
38
|
+
rbtrace 'call'
|
39
|
+
|
40
|
+
1.times do
|
41
|
+
Slow.something
|
42
|
+
Slow.new.something
|
43
|
+
Test.new.call
|
44
|
+
end
|
45
|
+
|
46
|
+
__END__
|
47
|
+
|
48
|
+
Slow.something <10047>
|
49
|
+
Do::It#something <4>
|
50
|
+
Test#call
|
51
|
+
Test#[]=
|
52
|
+
Another#call
|
53
|
+
Another#[]= <4>
|
54
|
+
<14>
|
55
|
+
<28>
|
56
|
+
<40>
|
57
|
+
|
data/rbtrace.gemspec
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'rbtrace'
|
3
|
+
s.version = '0.1.0'
|
4
|
+
s.homepage = 'http://github.com/tmm1/rbtrace'
|
5
|
+
|
6
|
+
s.authors = "Aman Gupta"
|
7
|
+
s.email = "aman@tmm1.net"
|
8
|
+
|
9
|
+
s.files = `git ls-files`.split("\n")
|
10
|
+
s.extensions = "ext/extconf.rb"
|
11
|
+
|
12
|
+
s.bindir = 'bin'
|
13
|
+
s.executables << 'rbtrace'
|
14
|
+
|
15
|
+
s.add_dependency 'ffi'
|
16
|
+
|
17
|
+
s.summary = 'rbtrace: like strace but for ruby code'
|
18
|
+
end
|
data/server.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'ext/rbtrace'
|
2
|
+
|
3
|
+
class String
|
4
|
+
def multiply_vowels(num)
|
5
|
+
gsub(/[aeiou]/){ |m| m*num }
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
while true
|
10
|
+
proc {
|
11
|
+
Dir.chdir("/tmp") do
|
12
|
+
Dir.pwd
|
13
|
+
Process.pid
|
14
|
+
'hello'.multiply_vowels(3)
|
15
|
+
sleep rand*0.5
|
16
|
+
end
|
17
|
+
}.call
|
18
|
+
end
|
metadata
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rbtrace
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
- 0
|
10
|
+
version: 0.1.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Aman Gupta
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-02-14 00:00:00 -08:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: ffi
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
30
|
+
segments:
|
31
|
+
- 0
|
32
|
+
version: "0"
|
33
|
+
type: :runtime
|
34
|
+
version_requirements: *id001
|
35
|
+
description:
|
36
|
+
email: aman@tmm1.net
|
37
|
+
executables:
|
38
|
+
- rbtrace
|
39
|
+
extensions:
|
40
|
+
- ext/extconf.rb
|
41
|
+
extra_rdoc_files: []
|
42
|
+
|
43
|
+
files:
|
44
|
+
- Gemfile
|
45
|
+
- README.md
|
46
|
+
- bin/rbtrace
|
47
|
+
- ext/extconf.rb
|
48
|
+
- ext/rbtrace.c
|
49
|
+
- ext/test.rb
|
50
|
+
- rbtrace.gemspec
|
51
|
+
- server.rb
|
52
|
+
has_rdoc: true
|
53
|
+
homepage: http://github.com/tmm1/rbtrace
|
54
|
+
licenses: []
|
55
|
+
|
56
|
+
post_install_message:
|
57
|
+
rdoc_options: []
|
58
|
+
|
59
|
+
require_paths:
|
60
|
+
- lib
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ">="
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
hash: 3
|
67
|
+
segments:
|
68
|
+
- 0
|
69
|
+
version: "0"
|
70
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
71
|
+
none: false
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
hash: 3
|
76
|
+
segments:
|
77
|
+
- 0
|
78
|
+
version: "0"
|
79
|
+
requirements: []
|
80
|
+
|
81
|
+
rubyforge_project:
|
82
|
+
rubygems_version: 1.4.2
|
83
|
+
signing_key:
|
84
|
+
specification_version: 3
|
85
|
+
summary: "rbtrace: like strace but for ruby code"
|
86
|
+
test_files: []
|
87
|
+
|