rbtrace 0.4.2 → 0.4.3

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.
@@ -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