rbtrace 0.1.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 +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
|
+
|