rbtrace 0.4.2 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,50 @@
1
+ require 'ffi'
2
+
3
+ module MsgQ
4
+ extend FFI::Library
5
+ ffi_lib FFI::CURRENT_PROCESS
6
+
7
+ class EventMsg < FFI::Struct
8
+ BUF_SIZE = RUBY_PLATFORM =~ /linux/ ? 256 : 120
9
+ IPC_NOWAIT = 004000
10
+
11
+ layout :mtype, :long,
12
+ :buf, [:char, BUF_SIZE]
13
+
14
+ def self.send_cmd(q, str)
15
+ msg = EventMsg.new
16
+ msg[:mtype] = 1
17
+ msg[:buf].to_ptr.put_string(0, str)
18
+
19
+ ret = MsgQ.msgsnd(q, msg, BUF_SIZE, 0)
20
+ FFI::LastError.raise if ret == -1
21
+ end
22
+
23
+ def self.recv_cmd(q, block=true)
24
+ MsgQ.rb_enable_interrupt if RUBY_VERSION > '1.9' && RUBY_VERSION < '2.0'
25
+
26
+ msg = EventMsg.new
27
+ ret = MsgQ.msgrcv(q, msg, BUF_SIZE, 0, block ? 0 : IPC_NOWAIT)
28
+ if ret == -1
29
+ if !block and [Errno::EAGAIN, Errno::ENOMSG].include?(FFI::LastError.exception)
30
+ return nil
31
+ end
32
+
33
+ FFI::LastError.raise
34
+ end
35
+
36
+ msg[:buf].to_ptr.read_string_length(BUF_SIZE)
37
+ ensure
38
+ MsgQ.rb_disable_interrupt if RUBY_VERSION > '1.9' && RUBY_VERSION < '2.0'
39
+ end
40
+ end
41
+
42
+ attach_function :msgget, [:int, :int], :int
43
+ attach_function :msgrcv, [:int, EventMsg.ptr, :size_t, :long, :int], :int
44
+ attach_function :msgsnd, [:int, EventMsg.ptr, :size_t, :int], :int
45
+
46
+ if RUBY_VERSION > '1.9' && RUBY_VERSION < '2.0'
47
+ attach_function :rb_enable_interrupt, [], :void
48
+ attach_function :rb_disable_interrupt, [], :void
49
+ end
50
+ end
@@ -0,0 +1,570 @@
1
+ require 'socket'
2
+ require 'fileutils'
3
+ require 'msgpack'
4
+ require 'ffi'
5
+ require 'rbtrace/core_ext'
6
+ require 'rbtrace/msgq'
7
+
8
+ class RBTracer
9
+ # Public: The Fixnum pid of the traced process.
10
+ attr_reader :pid
11
+
12
+ # Public: The IO where tracing output is written (default: STDOUT).
13
+ attr_accessor :out
14
+
15
+ # Public: The timeout before giving up on attaching/detaching to a process.
16
+ attr_accessor :timeout
17
+
18
+ # The String prefix used on nested method calls (default: ' ').
19
+ attr_accessor :prefix
20
+
21
+ # The Boolean flag for showing how long method calls take (default: true).
22
+ attr_accessor :show_duration
23
+
24
+ # The Boolean flag for showing the timestamp when method calls start (default: false).
25
+ attr_accessor :show_time
26
+
27
+ # Create a new tracer
28
+ #
29
+ # pid - The String of Fixnum process id
30
+ #
31
+ # Returns a tracer.
32
+ def initialize(pid)
33
+ begin
34
+ raise ArgumentError unless pid
35
+ @pid = pid.to_i
36
+ raise ArgumentError unless @pid > 0
37
+ Process.kill(0, @pid)
38
+ rescue TypeError, ArgumentError
39
+ raise ArgumentError, 'pid required'
40
+ rescue Errno::ESRCH
41
+ raise ArgumentError, 'invalid pid'
42
+ rescue Errno::EPERM
43
+ raise ArgumentError, 'could not signal process, are you running as root?'
44
+ end
45
+
46
+ path = "/tmp/rbtrace-#{@pid}.sock"
47
+ @sock = Socket.new Socket::AF_UNIX, Socket::SOCK_DGRAM, 0
48
+ @sockaddr = Socket.pack_sockaddr_un(path)
49
+ @sock.bind(@sockaddr)
50
+ FileUtils.chmod 0666, path
51
+ at_exit{ FileUtils.rm(path) if File.exists?(path) }
52
+
53
+ 5.times do
54
+ signal
55
+ sleep 0.15 # wait for process to create msgqs
56
+
57
+ @qo = MsgQ.msgget(-@pid, 0666)
58
+
59
+ break if @qo > -1
60
+ end
61
+
62
+ if @qo == -1
63
+ raise ArgumentError, 'pid is not listening for messages, did you `require "rbtrace"`'
64
+ end
65
+
66
+ @klasses = {}
67
+ @methods = {}
68
+ @tracers = Hash.new{ |h,k|
69
+ h[k] = {
70
+ :query => nil,
71
+ :times => [],
72
+ :names => [],
73
+ :exprs => {},
74
+ :last => false,
75
+ :arglist => false
76
+ }
77
+ }
78
+ @max_nesting = @last_nesting = @nesting = 0
79
+ @last_tracer = nil
80
+
81
+ @timeout = 5
82
+
83
+ @out = STDOUT
84
+ @out.sync = true
85
+ @prefix = ' '
86
+ @printed_newline = true
87
+
88
+ @show_time = false
89
+ @show_duration = true
90
+ @watch_slow = false
91
+
92
+ attach
93
+ end
94
+
95
+ # Watch for method calls slower than a threshold.
96
+ #
97
+ # msec - The Fixnum threshold in milliseconds
98
+ #
99
+ # Returns nothing.
100
+ def watch(msec, cpu_only=false)
101
+ @watch_slow = true
102
+ send_cmd(cpu_only ? :watchcpu : :watch, msec)
103
+ end
104
+
105
+ # Turn on the firehose (show all method calls).
106
+ #
107
+ # Returns nothing.
108
+ def firehose
109
+ send_cmd(:firehose)
110
+ end
111
+
112
+ # Turn on dev mode.
113
+ #
114
+ # Returns nothing.
115
+ def devmode
116
+ send_cmd(:devmode)
117
+ end
118
+
119
+ # Fork the process and return the copy's pid.
120
+ #
121
+ # Returns a Fixnum pid.
122
+ def fork
123
+ send_cmd(:fork)
124
+ if wait('for fork', 30){ !!@forked_pid }
125
+ @forked_pid
126
+ else
127
+ STDERR.puts '*** timed out waiting for fork'
128
+ end
129
+ end
130
+
131
+ # Evaluate some ruby code.
132
+ #
133
+ # Returns the String result.
134
+ def eval(code)
135
+ if (err = valid_syntax?(code)) != true
136
+ raise ArgumentError, "#{err.class} for expression #{code.inspect}"
137
+ end
138
+
139
+ send_cmd(:eval, code)
140
+
141
+ if wait('for eval response', 15){ !!@eval_result }
142
+ @eval_result
143
+ else
144
+ STDERR.puts '*** timed out waiting for eval response'
145
+ end
146
+ end
147
+
148
+ # Turn on GC tracing.
149
+ #
150
+ # Returns nothing.
151
+ def gc
152
+ send_cmd(:gc)
153
+ end
154
+
155
+ # Restrict slow tracing to a specific list of methods.
156
+ #
157
+ # methods - The String or Array of method selectors.
158
+ #
159
+ # Returns nothing.
160
+ def add_slow(methods)
161
+ add(methods, true)
162
+ end
163
+
164
+ # Add tracers for the given list of methods.
165
+ #
166
+ # methods - The String or Array of method selectors to trace.
167
+ #
168
+ # Returns nothing.
169
+ def add(methods, slow=false)
170
+ Array(methods).each do |func|
171
+ func = func.strip
172
+ next if func.empty?
173
+
174
+ if func =~ /^(.+?)\((.+)\)$/
175
+ name, args = $1, $2
176
+ args = args.split(',').map{ |a| a.strip }
177
+ end
178
+
179
+ send_cmd(:add, name || func, slow)
180
+
181
+ if args and args.any?
182
+ args.each do |arg|
183
+ if (err = valid_syntax?(arg)) != true
184
+ raise ArgumentError, "#{err.class} for expression #{arg.inspect} in method #{func.inspect}"
185
+ end
186
+ if arg =~ /^@/ and arg !~ /^@[_a-z][_a-z0-9]+$/i
187
+ # arg[0]=='@' means ivar, but if this is an expr
188
+ # we can hack a space in front so it gets eval'd instead
189
+ arg = " #{arg}"
190
+ end
191
+ send_cmd(:addexpr, arg)
192
+ end
193
+ end
194
+ end
195
+ end
196
+
197
+ # Attach to the process.
198
+ #
199
+ # Returns nothing.
200
+ def attach
201
+ send_cmd(:attach, Process.pid)
202
+ if wait('to attach'){ @attached == true }
203
+ STDERR.puts "*** attached to process #{pid}"
204
+ else
205
+ raise ArgumentError, 'process already being traced?'
206
+ end
207
+ end
208
+
209
+ # Detach from the traced process.
210
+ #
211
+ # Returns nothing.
212
+ def detach
213
+ begin
214
+ send_cmd(:detach)
215
+ rescue Errno::ESRCH
216
+ end
217
+
218
+ newline
219
+
220
+ if wait('to detach cleanly'){ @attached == false }
221
+ newline
222
+ STDERR.puts "*** detached from process #{pid}"
223
+ else
224
+ newline
225
+ STDERR.puts "*** could not detach cleanly from process #{pid}"
226
+ end
227
+ rescue Errno::EINVAL, Errno::EIDRM
228
+ newline
229
+ STDERR.puts "*** process #{pid} is gone"
230
+ # STDERR.puts "*** #{$!.inspect}"
231
+ # STDERR.puts $!.backtrace.join("\n ")
232
+ rescue Interrupt, SignalException
233
+ retry
234
+ end
235
+
236
+ # Process events from the traced process.
237
+ #
238
+ # Returns nothing.
239
+ def recv_loop
240
+ while true
241
+ # block until a message arrives
242
+ process_line(recv_cmd)
243
+
244
+ # process any remaining messages
245
+ recv_lines
246
+ end
247
+ rescue Errno::EINVAL, Errno::EIDRM
248
+ # process went away
249
+ end
250
+
251
+ # Process events from the traced process, without blocking if
252
+ # there is nothing to do. This is a useful way to drain the buffer
253
+ # so messages do not accumulate in kernel land.
254
+ #
255
+ # Returns nothing.
256
+ def recv_lines
257
+ 50.times do
258
+ break unless line = recv_cmd(false)
259
+ process_line(line)
260
+ end
261
+ end
262
+
263
+ def puts(arg=nil)
264
+ @printed_newline = true
265
+ arg ? @out.puts(arg) : @out.puts
266
+ end
267
+
268
+ private
269
+
270
+ def signal
271
+ Process.kill 'URG', @pid
272
+ end
273
+
274
+ # Process incoming events until either a timeout or a condition becomes true.
275
+ #
276
+ # time - The Fixnum timeout in seconds.
277
+ # block - The Block that is checked every 50ms until it returns true.
278
+ #
279
+ # Returns true when the condition was met, or false on a timeout.
280
+ def wait(reason, time=@timeout)
281
+ wait = 0.05 # polling interval
282
+
283
+ (time/wait).to_i.times do
284
+ begin
285
+ recv_lines
286
+ sleep(wait)
287
+ begin
288
+ signal
289
+ rescue Errno::ESRCH
290
+ break
291
+ end
292
+ time -= wait
293
+
294
+ return true if yield
295
+ rescue Interrupt
296
+ STDERR.puts "*** waiting #{reason} (#{time.to_i}s left)"
297
+ retry
298
+ end
299
+ end
300
+
301
+ false
302
+ end
303
+
304
+ def send_cmd(*cmd)
305
+ begin
306
+ msg = cmd.to_msgpack
307
+ raise ArgumentError, 'command is too long' if msg.bytesize > MsgQ::EventMsg::BUF_SIZE
308
+ MsgQ::EventMsg.send_cmd(@qo, msg)
309
+ rescue Errno::EINTR
310
+ retry
311
+ end
312
+ signal
313
+ recv_lines
314
+ end
315
+
316
+ def recv_cmd(block=true)
317
+ if block
318
+ @sock.recv(65536)
319
+ else
320
+ @sock.recv_nonblock(65536)
321
+ end
322
+ rescue Errno::EAGAIN
323
+ nil
324
+ end
325
+
326
+ def valid_syntax?(code)
327
+ begin
328
+ Kernel.eval("#{code}\nBEGIN {return true}", nil, 'rbtrace_expression', 0)
329
+ rescue Exception => e
330
+ e
331
+ end
332
+ end
333
+
334
+ def print(arg)
335
+ @printed_newline = false
336
+ @out.print(arg)
337
+ end
338
+
339
+ def newline
340
+ puts unless @printed_newline
341
+ @printed_newline = true
342
+ end
343
+
344
+ def parse_cmd(line)
345
+ unpacker = MessagePack::Unpacker.new
346
+ unpacker.feed(line)
347
+
348
+ obj = nil
349
+ unpacker.each{|o| obj = o; break }
350
+ obj
351
+ end
352
+
353
+ def process_line(line)
354
+ return unless cmd = parse_cmd(line)
355
+ event = cmd.shift
356
+
357
+ case event
358
+ when 'during_gc'
359
+ sleep 0.01
360
+ signal
361
+ return
362
+
363
+ when 'attached'
364
+ tracer_pid, = *cmd
365
+ if tracer_pid != Process.pid
366
+ STDERR.puts "*** process #{pid} is already being traced (#{tracer_pid} != #{Process.pid})"
367
+ exit!(-1)
368
+ end
369
+
370
+ @attached = true
371
+ return
372
+
373
+ when 'detached'
374
+ tracer_pid, = *cmd
375
+ if tracer_pid != Process.pid
376
+ STDERR.puts "*** process #{pid} detached #{tracer_pid}, but we are #{Process.pid}"
377
+ else
378
+ @attached = false
379
+ end
380
+
381
+ return
382
+ end
383
+
384
+ unless @attached
385
+ STDERR.puts "*** got #{event} before attaching"
386
+ return
387
+ end
388
+
389
+ case event
390
+ when 'forked'
391
+ pid, = *cmd
392
+ @forked_pid = pid
393
+
394
+ when 'evaled'
395
+ res, = *cmd
396
+ @eval_result = res
397
+
398
+ when 'mid'
399
+ mid, name = *cmd
400
+ @methods[mid] = name
401
+
402
+ when 'klass'
403
+ kid, name = *cmd
404
+ @klasses[kid] = name
405
+
406
+ when 'add'
407
+ tracer_id, query = *cmd
408
+ if tracer_id == -1
409
+ STDERR.puts "*** unable to add tracer for #{query}"
410
+ else
411
+ @tracers.delete(tracer_id)
412
+ @tracers[tracer_id][:query] = query
413
+ end
414
+
415
+ when 'newexpr'
416
+ tracer_id, expr_id, expr = *cmd
417
+ tracer = @tracers[tracer_id]
418
+
419
+ if expr_id > -1
420
+ tracer[:exprs][expr_id] = expr.strip
421
+ end
422
+
423
+ when 'exprval'
424
+ tracer_id, expr_id, val = *cmd
425
+
426
+ tracer = @tracers[tracer_id]
427
+ expr = tracer[:exprs][expr_id]
428
+
429
+ if tracer[:arglist]
430
+ print ', '
431
+ else
432
+ print '('
433
+ end
434
+
435
+ print "#{expr}="
436
+ print val
437
+ tracer[:arglist] = true
438
+
439
+ when 'call','ccall'
440
+ time, tracer_id, mid, is_singleton, klass = *cmd
441
+
442
+ tracer = @tracers[tracer_id]
443
+ klass = @klasses[klass]
444
+ name = klass ? "#{klass}#{ is_singleton ? '.' : '#' }" : ''
445
+ name += @methods[mid] || '(unknown)'
446
+
447
+ tracer[:times] << time
448
+ tracer[:names] << name
449
+
450
+ if @last_tracer and @last_tracer[:arglist]
451
+ print ')'
452
+ @last_tracer[:arglist] = false
453
+ end
454
+ newline
455
+ if @show_time
456
+ t = Time.at(time/1_000_000)
457
+ print t.strftime("%H:%M:%S.")
458
+ print "%06d " % (time - t.to_f*1_000_000).round
459
+ end
460
+ print @prefix*@nesting if @nesting > 0
461
+ print name
462
+
463
+ @nesting += 1
464
+ @max_nesting = @nesting if @nesting > @max_nesting
465
+ @last_nesting = @nesting
466
+ @last_tracer = tracer
467
+ tracer[:last] = "#{name}:#{@nesting-1}"
468
+
469
+ when 'return','creturn'
470
+ time, tracer_id = *cmd
471
+ tracer = @tracers[tracer_id]
472
+
473
+ @nesting -= 1 if @nesting > 0
474
+
475
+ if start = tracer[:times].pop
476
+ name = tracer[:names].pop
477
+ diff = time - start
478
+ @last_tracer[:arglist] = false if @last_tracer and @last_tracer[:last] != "#{name}:#{@nesting}"
479
+
480
+ print ')' if @last_tracer and @last_tracer[:arglist]
481
+
482
+ unless tracer == @last_tracer and @last_tracer[:last] == "#{name}:#{@nesting}"
483
+ newline
484
+ print ' '*16 if @show_time
485
+ print @prefix*@nesting if @nesting > 0
486
+ print name
487
+ end
488
+ print ' <%f>' % (diff/1_000_000.0) if @show_duration
489
+ newline
490
+
491
+ if @nesting == 0 and @max_nesting > 1
492
+ # unless tracer == @last_tracer and @last_tracer[:last] == name
493
+ puts
494
+ # end
495
+ end
496
+ end
497
+
498
+ tracer[:arglist] = false
499
+ @last_nesting = @nesting
500
+
501
+ when 'slow', 'cslow'
502
+ time, diff, nesting, mid, is_singleton, klass = *cmd
503
+
504
+ klass = @klasses[klass]
505
+ name = klass ? "#{klass}#{ is_singleton ? '.' : '#' }" : ''
506
+ name += @methods[mid] || '(unknown)'
507
+
508
+ newline
509
+ nesting = @nesting if @nesting > 0
510
+
511
+ if @show_time
512
+ t = Time.at(time/1_000_000)
513
+ print t.strftime("%H:%M:%S.")
514
+ print "%06d " % (time - t.to_f*1_000_000).round
515
+ end
516
+
517
+ print @prefix*nesting if nesting > 0
518
+ print name
519
+ if @show_duration
520
+ print ' '
521
+ print "<%f>" % (diff/1_000_000.0)
522
+ end
523
+ puts
524
+ puts if nesting == 0 and @max_nesting > 1
525
+
526
+ @max_nesting = nesting if nesting > @max_nesting
527
+ @last_nesting = nesting
528
+
529
+ when 'gc_start'
530
+ time, = *cmd
531
+ @gc_start = time
532
+ print 'garbage_collect'
533
+
534
+ when 'gc_end'
535
+ time, = *cmd
536
+ diff = time - @gc_start
537
+ # if @gc_mark
538
+ # mark = ((@gc_mark - @gc_start) * 100.0 / diff).to_i
539
+ # print '(mark: %d%%, sweep: %d%%)' % [mark, 100-mark]
540
+ # end
541
+ print ' <%f>' % (diff/1_000_000.0) if @show_duration
542
+ @gc_start = nil
543
+ newline
544
+
545
+ when 'gc'
546
+ time, = *cmd
547
+ @gc_mark = time
548
+
549
+ unless @gc_start
550
+ newline
551
+ if @show_time
552
+ t = Time.at(time/1_000_000)
553
+ print t.strftime("%H:%M:%S.")
554
+ print "%06d " % (time - t.to_f*1_000_000).round
555
+ end
556
+ print @prefix*@last_nesting if @last_nesting > 0
557
+ print "garbage_collect"
558
+ puts if @watch_slow
559
+ end
560
+
561
+ else
562
+ puts "unknown event #{event}: #{cmd.inspect}"
563
+
564
+ end
565
+ rescue => e
566
+ STDERR.puts "error on #{event}: #{cmd.inspect}"
567
+ raise e
568
+ end
569
+
570
+ end