vivarium 0.2.0 → 0.3.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.
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbbcc"
4
+ require "time"
5
+
6
+ module Vivarium
7
+ class Correlator
8
+ RawEvent = Struct.new(
9
+ :ktime_ns, :pid, :tid, :event_name, :payload,
10
+ keyword_init: true
11
+ )
12
+
13
+ EVENT_C_TYPE = <<~C
14
+ struct event_t {
15
+ u64 ktime_ns;
16
+ u32 pid;
17
+ u32 tid;
18
+ char event_name[16];
19
+ char payload[256];
20
+ };
21
+ C
22
+
23
+ POLL_TIMEOUT_MS = 200
24
+
25
+ def initialize(pin_dir:, observer_pid:, main_tid:, method_id_queue:, dest: $stdout)
26
+ @pin_dir = pin_dir
27
+ @observer_pid = observer_pid
28
+ @main_tid = main_tid
29
+ @method_id_queue = method_id_queue
30
+ @dest = dest
31
+
32
+ @events = []
33
+ @events_mutex = Mutex.new
34
+ @method_table = {}
35
+ @stop_flag = false
36
+ @started = false
37
+
38
+ @ringbuf = RbBCC::RingBuf.from_pin(
39
+ File.join(@pin_dir, "events"),
40
+ EVENT_C_TYPE,
41
+ Vivarium::EVENTS_RINGBUF_PAGES
42
+ )
43
+ @ringbuf.open_ring_buffer do |_ctx, data, size|
44
+ capture_event(data, size)
45
+ end
46
+ end
47
+
48
+ def start
49
+ return if @started
50
+
51
+ @session_start_iso = Time.now.utc.iso8601(3)
52
+ @session_start_ktime = Vivarium.monotonic_ktime_ns
53
+ @thread = Thread.new { run }
54
+ @started = true
55
+ end
56
+
57
+ def stop
58
+ return unless @started
59
+ return if @stopped
60
+
61
+ @stop_flag = true
62
+ @thread&.join(POLL_TIMEOUT_MS * 4 / 1000.0 + 1)
63
+ @session_stop_iso = Time.now.utc.iso8601(3)
64
+ @session_stop_ktime = Vivarium.monotonic_ktime_ns
65
+
66
+ 3.times { safe_poll(50) }
67
+ drain_method_id_queue
68
+
69
+ events_snapshot = @events_mutex.synchronize { @events.dup }
70
+ method_table_snapshot = @method_table.dup
71
+ @stopped = true
72
+
73
+ TreeRenderer.new(
74
+ events: events_snapshot,
75
+ method_table: method_table_snapshot,
76
+ observer_pid: @observer_pid,
77
+ main_tid: @main_tid,
78
+ session_start_iso: @session_start_iso,
79
+ session_start_ktime: @session_start_ktime,
80
+ session_stop_iso: @session_stop_iso,
81
+ session_stop_ktime: @session_stop_ktime,
82
+ dest: @dest
83
+ ).render
84
+ end
85
+
86
+ private
87
+
88
+ def run
89
+ until @stop_flag
90
+ safe_poll(POLL_TIMEOUT_MS)
91
+ drain_method_id_queue
92
+ end
93
+ end
94
+
95
+ def safe_poll(timeout_ms)
96
+ @ringbuf.ring_buffer_poll(timeout_ms)
97
+ rescue StandardError => e
98
+ warn "[vivarium correlator] poll error: #{e.class}: #{e.message}"
99
+ end
100
+
101
+ def capture_event(data, size)
102
+ bytes = data[0, size].to_s.b
103
+ bytes = bytes.ljust(Vivarium::EVENT_STRUCT_SIZE, "\x00") if bytes.bytesize < Vivarium::EVENT_STRUCT_SIZE
104
+
105
+ ktime_ns = bytes[Vivarium::EVENT_TS_OFFSET, Vivarium::EVENT_TS_SIZE].unpack1("Q<")
106
+ pid = bytes[Vivarium::EVENT_PID_OFFSET, 4].unpack1("L<")
107
+ tid = bytes[Vivarium::EVENT_TID_OFFSET, 4].unpack1("L<")
108
+ event_name = Vivarium.c_string(bytes[Vivarium::EVENT_NAME_OFFSET, Vivarium::EVENT_NAME_SIZE])
109
+ payload = bytes[Vivarium::EVENT_PAYLOAD_OFFSET, Vivarium::EVENT_PAYLOAD_SIZE].to_s.b
110
+
111
+ @events_mutex.synchronize do
112
+ @events << RawEvent.new(
113
+ ktime_ns: ktime_ns,
114
+ pid: pid,
115
+ tid: tid,
116
+ event_name: event_name,
117
+ payload: payload
118
+ )
119
+ end
120
+ rescue StandardError => e
121
+ warn "[vivarium correlator] capture error: #{e.class}: #{e.message}"
122
+ end
123
+
124
+ def drain_method_id_queue
125
+ loop do
126
+ msg = begin
127
+ @method_id_queue.pop(true)
128
+ rescue ThreadError
129
+ return
130
+ end
131
+
132
+ method_id, signature = msg
133
+ @method_table[method_id] = signature
134
+ end
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,543 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Vivarium
6
+ class TreeRenderer
7
+ SPAN_EVENT_NAMES = %w[span_start span_stop].to_set.freeze
8
+ FORK_EVENT_NAME = "proc_fork"
9
+ EXEC_EVENT_NAME = "proc_exec"
10
+
11
+ LSM_EVENT_NAMES = %w[
12
+ path_open sock_connect odd_socket
13
+ ptrace_check sb_mount kernel_read_file task_kill
14
+ setid_change capable_check bprm_creds
15
+ file_symlink file_hardlink file_rename file_chmod
16
+ ].to_set.freeze
17
+
18
+ TP_EVENT_NAMES = %w[
19
+ dns_req proc_exec file_getdents proc_fork
20
+ ].to_set.freeze
21
+
22
+ SYNTHETIC_SPAN_NAME = "<no-span>"
23
+ UNRESOLVED_METHOD_PREFIX = "<method_id="
24
+
25
+ Span = Struct.new(
26
+ :tid, :method_id, :file_id, :lineno, :start_ktime, :stop_ktime, :exit_kind,
27
+ :events, :descendant_pids, :synthetic, :raised,
28
+ keyword_init: true
29
+ ) do
30
+ def duration_ns
31
+ return nil unless stop_ktime && start_ktime
32
+
33
+ stop_ktime - start_ktime
34
+ end
35
+
36
+ # Span is mutable (stop_ktime, events, descendant_pids are written after creation).
37
+ # Use object identity for Hash/Set so keys remain stable across mutations.
38
+ def hash
39
+ object_id
40
+ end
41
+
42
+ def eql?(other)
43
+ equal?(other)
44
+ end
45
+ end
46
+
47
+ EventNode = Struct.new(:kind, :name, :target, :offset_ns, :child_proc, keyword_init: true)
48
+ ProcNode = Struct.new(:pid, :comm, :parent_pid, :children, keyword_init: true)
49
+
50
+ def initialize(events:, method_table:, observer_pid:, main_tid:,
51
+ session_start_iso:, session_start_ktime:,
52
+ session_stop_iso:, session_stop_ktime:, dest:)
53
+ @events = events
54
+ @method_table = method_table
55
+ @observer_pid = observer_pid
56
+ @main_tid = main_tid
57
+ @session_start_iso = session_start_iso
58
+ @session_start_ktime = session_start_ktime
59
+ @session_stop_iso = session_stop_iso
60
+ @session_stop_ktime = session_stop_ktime
61
+ @dest = dest
62
+
63
+ @pid_comm = { observer_pid => "ruby" }
64
+ @pid_parent = {}
65
+ @unresolved_method_ids = []
66
+ end
67
+
68
+ def render
69
+ sorted = @events.sort_by { |e| [e.ktime_ns, e.pid, e.tid] }
70
+
71
+ real_spans, @children_map = build_real_spans(sorted)
72
+ @child_span_set = @children_map.values.flatten.to_set
73
+
74
+ assign_descendants(real_spans, sorted)
75
+
76
+ root_real_spans = real_spans.reject { |s| @child_span_set.include?(s) }
77
+ root_with_synthetics = interleave_synthetic_spans(root_real_spans)
78
+
79
+ synthetic_spans = root_with_synthetics.select(&:synthetic)
80
+ all_spans_for_assign = (synthetic_spans + real_spans).sort_by { |s| s.start_ktime || 0 }
81
+ assign_events_to_spans(all_spans_for_assign, sorted)
82
+
83
+ print_header
84
+ print_warnings
85
+ print_observer_proc(root_with_synthetics)
86
+ end
87
+
88
+ private
89
+
90
+ def build_real_spans(events)
91
+ open_by_tid = Hash.new { |h, k| h[k] = [] }
92
+ closed = []
93
+ children_map = {}
94
+
95
+ events.each do |ev|
96
+ case ev.event_name
97
+ when "span_start"
98
+ mid, fid, lno = read_span_payload(ev.payload)
99
+ span = Span.new(
100
+ tid: ev.tid,
101
+ method_id: mid,
102
+ file_id: fid,
103
+ lineno: lno,
104
+ start_ktime: ev.ktime_ns,
105
+ stop_ktime: nil,
106
+ exit_kind: nil,
107
+ events: [],
108
+ descendant_pids: Set.new,
109
+ synthetic: false,
110
+ raised: false
111
+ )
112
+ parent = open_by_tid[ev.tid].last
113
+ (children_map[parent] ||= []) << span if parent
114
+ open_by_tid[ev.tid].push(span)
115
+ when "span_stop"
116
+ stack = open_by_tid[ev.tid]
117
+ next if stack.empty?
118
+
119
+ span = stack.pop
120
+ span.stop_ktime = ev.ktime_ns
121
+ span.exit_kind = :stopped
122
+ closed << span
123
+ when "span_raise"
124
+ span = open_by_tid[ev.tid].last
125
+ span.raised = true if span
126
+ end
127
+ end
128
+
129
+ open_by_tid.each_value do |stack|
130
+ stack.each do |span|
131
+ span.stop_ktime = @session_stop_ktime || (events.last&.ktime_ns)
132
+ span.exit_kind = :dangling
133
+ closed << span
134
+ end
135
+ end
136
+
137
+ closed.sort_by!(&:start_ktime)
138
+ [closed, children_map]
139
+ end
140
+
141
+ def assign_descendants(spans, events)
142
+ sorted_spans = spans.reject(&:synthetic).sort_by(&:start_ktime)
143
+
144
+ events.each do |ev|
145
+ next unless ev.event_name == FORK_EVENT_NAME
146
+
147
+ child_pid = read_proc_fork_child_pid(ev.payload)
148
+ next if child_pid.zero?
149
+
150
+ @pid_parent[child_pid] = ev.pid
151
+
152
+ owning = innermost_span_for_event(sorted_spans, ev)
153
+ owning&.descendant_pids&.add(child_pid)
154
+
155
+ # Closure: if ev.pid is itself a descendant of some span, that span also gains child_pid
156
+ sorted_spans.each do |span|
157
+ next if span == owning
158
+ next unless event_in_span?(ev, span)
159
+
160
+ span.descendant_pids.add(child_pid) if span.descendant_pids.include?(ev.pid)
161
+ end
162
+ end
163
+ end
164
+
165
+ def interleave_synthetic_spans(real_spans)
166
+ result = []
167
+ cursor = @session_start_ktime || (real_spans.first&.start_ktime) || 0
168
+ session_end = @session_stop_ktime ||
169
+ real_spans.map(&:stop_ktime).compact.max ||
170
+ cursor
171
+
172
+ real_spans.each do |span|
173
+ if span.start_ktime > cursor
174
+ syn = synthetic_span(cursor, span.start_ktime)
175
+ result << syn if syn
176
+ end
177
+ result << span
178
+ cursor = [cursor, span.stop_ktime || span.start_ktime].max
179
+ end
180
+
181
+ if session_end > cursor
182
+ syn = synthetic_span(cursor, session_end)
183
+ result << syn if syn
184
+ end
185
+
186
+ result
187
+ end
188
+
189
+ def synthetic_span(start_ktime, stop_ktime)
190
+ Span.new(
191
+ tid: @main_tid,
192
+ method_id: nil,
193
+ file_id: nil,
194
+ lineno: nil,
195
+ start_ktime: start_ktime,
196
+ stop_ktime: stop_ktime,
197
+ exit_kind: :stopped,
198
+ events: [],
199
+ descendant_pids: Set.new,
200
+ synthetic: true,
201
+ raised: false
202
+ )
203
+ end
204
+
205
+ def assign_events_to_spans(spans, events)
206
+ events.each do |ev|
207
+ next if SPAN_EVENT_NAMES.include?(ev.event_name)
208
+
209
+ if ev.event_name == "proc_exec"
210
+ @pid_comm[ev.pid] = exec_basename(ev.payload) || @pid_comm[ev.pid] || "?"
211
+ end
212
+
213
+ host = find_host_span(spans, ev)
214
+ next unless host
215
+
216
+ host.events << ev
217
+ if ev.event_name == FORK_EVENT_NAME
218
+ child_pid = read_proc_fork_child_pid(ev.payload)
219
+ @pid_comm[child_pid] ||= "?"
220
+ host.descendant_pids.add(child_pid)
221
+ end
222
+ end
223
+ end
224
+
225
+ def find_host_span(spans, ev)
226
+ candidates = spans.select { |s| event_in_span?(ev, s) }
227
+ return nil if candidates.empty?
228
+
229
+ direct = candidates.select { |s| s.tid == ev.tid }
230
+ return direct.last unless direct.empty?
231
+
232
+ desc = candidates.select { |s| s.descendant_pids.include?(ev.pid) }
233
+ return desc.last unless desc.empty?
234
+
235
+ candidates.find(&:synthetic) || candidates.last
236
+ end
237
+
238
+ def innermost_span_for_event(spans, ev)
239
+ candidates = spans.select { |s| event_in_span?(ev, s) && s.tid == ev.tid }
240
+ candidates.last
241
+ end
242
+
243
+ def event_in_span?(ev, span)
244
+ return false unless span.start_ktime
245
+ return false if span.stop_ktime && ev.ktime_ns > span.stop_ktime
246
+
247
+ ev.ktime_ns >= span.start_ktime
248
+ end
249
+
250
+ def print_header
251
+ duration_s = ((@session_stop_ktime || 0) - (@session_start_ktime || 0)) / 1_000_000_000.0
252
+ @dest.puts "# vivarium session"
253
+ @dest.puts "# started iso=#{@session_start_iso} ktime=#{@session_start_ktime}"
254
+ @dest.puts "# stopped iso=#{@session_stop_iso} ktime=#{@session_stop_ktime}"
255
+ @dest.puts "# duration #{format('%.3fs', duration_s)}"
256
+ end
257
+
258
+ def print_warnings
259
+ @unresolved_method_ids.uniq.each do |mid|
260
+ @dest.puts format("# warning method_id=0x%016X unresolved at render time", mid & 0xFFFF_FFFF_FFFF_FFFF)
261
+ end
262
+ end
263
+
264
+ def print_observer_proc(spans)
265
+ @dest.puts "[PROC pid=#{@observer_pid} comm=#{@pid_comm[@observer_pid] || 'ruby'}]"
266
+ children = spans.reject { |s| s.synthetic && s.events.empty? }
267
+ .reject { |s| @child_span_set.include?(s) }
268
+ children.each_with_index do |span, idx|
269
+ print_span(span, prefix: "", is_last: idx == children.size - 1)
270
+ end
271
+ end
272
+
273
+ def print_span(span, prefix:, is_last:)
274
+ marker = is_last ? "└─ " : "├─ "
275
+ @dest.puts "#{prefix}#{marker}#{render_span_header(span)}"
276
+ child_prefix = prefix + (is_last ? " " : "│ ")
277
+ nodes = build_span_children(span)
278
+ print_nodes(nodes, child_prefix)
279
+ end
280
+
281
+ def render_span_header(span)
282
+ name = span_display_name(span)
283
+ dur_text = format_duration(span.duration_ns)
284
+ file_info = span_file_info(span)
285
+ suffix = if span.exit_kind == :dangling
286
+ " (open)"
287
+ elsif span.raised
288
+ " (raise)"
289
+ else
290
+ ""
291
+ end
292
+ "[SPAN tid=#{span.tid} #{name}#{file_info} dur=#{dur_text}#{suffix}]"
293
+ end
294
+
295
+ def span_file_info(span)
296
+ return "" if span.synthetic
297
+ return "" unless span.file_id && span.file_id != -1
298
+
299
+ file_name = Vivarium::Usdt.get_file_name(span.file_id)
300
+ return "" unless file_name
301
+
302
+ lno = span.lineno && span.lineno > 0 ? ":#{span.lineno}" : ""
303
+ " at=#{File.basename(file_name)}#{lno}"
304
+ end
305
+
306
+ def span_display_name(span)
307
+ return SYNTHETIC_SPAN_NAME if span.synthetic
308
+ return SYNTHETIC_SPAN_NAME if span.method_id.nil?
309
+
310
+ name = @method_table[span.method_id]
311
+ name ||= Vivarium::Usdt.get_method_name(span.method_id)
312
+ return name if name
313
+
314
+ @unresolved_method_ids << span.method_id
315
+ format("#{UNRESOLVED_METHOD_PREFIX}0x%016X>", span.method_id & 0xFFFF_FFFF_FFFF_FFFF)
316
+ end
317
+
318
+ def build_span_children(span)
319
+ proc_node_by_pid = {}
320
+ root_children = []
321
+
322
+ span.events.each do |ev|
323
+ if ev.event_name == FORK_EVENT_NAME
324
+ child_pid = read_proc_fork_child_pid(ev.payload)
325
+ child_node = ProcNode.new(
326
+ pid: child_pid,
327
+ comm: @pid_comm[child_pid] || "?",
328
+ parent_pid: ev.pid,
329
+ children: []
330
+ )
331
+ proc_node_by_pid[child_pid] = child_node
332
+
333
+ ev_node = EventNode.new(
334
+ kind: kind_for(ev),
335
+ name: ev.event_name,
336
+ target: render_target(ev),
337
+ offset_ns: ev.ktime_ns - span.start_ktime,
338
+ child_proc: child_node
339
+ )
340
+
341
+ parent_container = container_for_pid(ev.pid, span, proc_node_by_pid, root_children)
342
+ append_event(parent_container, ev_node)
343
+ else
344
+ ev_node = EventNode.new(
345
+ kind: kind_for(ev),
346
+ name: ev.event_name,
347
+ target: render_target(ev),
348
+ offset_ns: ev.ktime_ns - span.start_ktime,
349
+ child_proc: nil
350
+ )
351
+
352
+ if ev.pid == @observer_pid && ev.tid == span.tid
353
+ append_event(root_children, ev_node)
354
+ elsif (node = proc_node_by_pid[ev.pid])
355
+ append_event(node.children, ev_node)
356
+ else
357
+ # event from a descendant pid we haven't materialized — synthesize a stub PROC node
358
+ stub = ProcNode.new(
359
+ pid: ev.pid,
360
+ comm: @pid_comm[ev.pid] || "?",
361
+ parent_pid: @pid_parent[ev.pid] || span.tid,
362
+ children: []
363
+ )
364
+ proc_node_by_pid[ev.pid] = stub
365
+ append_event(stub.children, ev_node)
366
+ root_children << stub
367
+ end
368
+
369
+ if ev.event_name == EXEC_EVENT_NAME && (node = proc_node_by_pid[ev.pid])
370
+ node.comm = @pid_comm[ev.pid] || node.comm
371
+ end
372
+ end
373
+ end
374
+
375
+ # Interleave child spans by start time among the event/proc nodes
376
+ child_spans = (@children_map[span] || []).sort_by(&:start_ktime)
377
+ child_spans.each do |child_span|
378
+ child_offset = child_span.start_ktime - span.start_ktime
379
+ insert_pos = root_children.size
380
+ root_children.each_with_index do |node, i|
381
+ if node.is_a?(EventNode) && node.offset_ns >= child_offset
382
+ insert_pos = i
383
+ break
384
+ end
385
+ end
386
+ root_children.insert(insert_pos, child_span)
387
+ end
388
+
389
+ root_children
390
+ end
391
+
392
+ def container_for_pid(pid, span, proc_node_by_pid, root_children)
393
+ return root_children if pid == @observer_pid
394
+
395
+ node = proc_node_by_pid[pid]
396
+ return node.children if node
397
+
398
+ # Walk up parent chain to find a known node
399
+ cur = pid
400
+ while (parent = @pid_parent[cur])
401
+ return root_children if parent == @observer_pid
402
+ if (parent_node = proc_node_by_pid[parent])
403
+ stub = ProcNode.new(pid: pid, comm: @pid_comm[pid] || "?", parent_pid: parent, children: [])
404
+ proc_node_by_pid[pid] = stub
405
+ parent_node.children << stub
406
+ return stub.children
407
+ end
408
+ cur = parent
409
+ end
410
+
411
+ root_children
412
+ end
413
+
414
+ def append_event(container, ev_node)
415
+ container << ev_node
416
+ end
417
+
418
+ def print_nodes(nodes, prefix)
419
+ nodes.each_with_index do |node, idx|
420
+ is_last = idx == nodes.size - 1
421
+ case node
422
+ when EventNode
423
+ print_event_node(node, prefix: prefix, is_last: is_last)
424
+ when ProcNode
425
+ print_proc_node(node, prefix: prefix, is_last: is_last)
426
+ when Span
427
+ print_span(node, prefix: prefix, is_last: is_last)
428
+ end
429
+ end
430
+ end
431
+
432
+ def print_event_node(node, prefix:, is_last:)
433
+ marker = is_last && node.child_proc.nil? ? "└─ " : "├─ "
434
+ marker = "└─ " if is_last && node.child_proc.nil?
435
+ marker = "├─ " unless is_last
436
+ marker = "└─ " if is_last
437
+ offset_text = format_offset(node.offset_ns)
438
+ line = format("%-4s %-15s → %-30s @+%s", node.kind, node.name, node.target, offset_text)
439
+ @dest.puts "#{prefix}#{marker}#{line}"
440
+
441
+ if node.child_proc
442
+ child_prefix = prefix + (is_last ? " " : "│ ")
443
+ print_proc_node(node.child_proc, prefix: child_prefix, is_last: true)
444
+ end
445
+ end
446
+
447
+ def print_proc_node(node, prefix:, is_last:)
448
+ marker = is_last ? "└─ " : "├─ "
449
+ header = "[PROC pid=#{node.pid} comm=#{node.comm}"
450
+ header += " parent=#{node.parent_pid}" if node.parent_pid
451
+ header += "]"
452
+ @dest.puts "#{prefix}#{marker}#{header}"
453
+ child_prefix = prefix + (is_last ? " " : "│ ")
454
+ print_nodes(node.children, child_prefix)
455
+ end
456
+
457
+ def kind_for(ev)
458
+ return "EXCP" if ev.event_name == "span_raise"
459
+ return "USDT" if SPAN_EVENT_NAMES.include?(ev.event_name)
460
+ return "LSM" if LSM_EVENT_NAMES.include?(ev.event_name)
461
+ return "TP" if TP_EVENT_NAMES.include?(ev.event_name)
462
+
463
+ "EVT"
464
+ end
465
+
466
+ def render_target(ev)
467
+ return render_raise_target(ev) if ev.event_name == "span_raise"
468
+
469
+ text = Vivarium.render_event_payload(ev).to_s
470
+ text = text.gsub(/\s+/, " ").strip
471
+ text.empty? ? "-" : text
472
+ end
473
+
474
+ def render_raise_target(ev)
475
+ bytes = ev.payload.to_s.b
476
+ return "-" if bytes.bytesize < 8
477
+
478
+ error_id = bytes[0, 8].unpack1("q<")
479
+ message_id = bytes.bytesize >= 16 ? bytes[8, 8].unpack1("q<") : -1
480
+ file_id = bytes.bytesize >= 24 ? bytes[16, 8].unpack1("q<") : -1
481
+ lineno = bytes.bytesize >= 32 ? bytes[24, 8].unpack1("q<") : -1
482
+
483
+ error_name = Vivarium::Usdt.get_error_name(error_id) ||
484
+ format("0x%016X", error_id & 0xFFFF_FFFF_FFFF_FFFF)
485
+
486
+ parts = ["error=#{error_name}"]
487
+
488
+ if message_id != -1
489
+ msg = Vivarium::Usdt.get_message_name(message_id)
490
+ parts << "message=#{msg.inspect}" if msg
491
+ end
492
+
493
+ if file_id != -1 && (file_name = Vivarium::Usdt.get_file_name(file_id))
494
+ lno = lineno && lineno > 0 ? ":#{lineno}" : ""
495
+ parts << "at=#{File.basename(file_name)}#{lno}"
496
+ end
497
+
498
+ parts.join(" ")
499
+ end
500
+
501
+ def format_duration(ns)
502
+ return "?" unless ns
503
+
504
+ ms = ns / 1_000_000.0
505
+ ms < 1.0 ? format("%.1fus", ns / 1_000.0) : format("%.1fms", ms)
506
+ end
507
+
508
+ def format_offset(ns)
509
+ return "?" unless ns
510
+
511
+ ms = ns / 1_000_000.0
512
+ ms.abs < 1.0 ? format("%.1fus", ns / 1_000.0) : format("%.1fms", ms)
513
+ end
514
+
515
+ def read_span_payload(payload)
516
+ bytes = payload.to_s.b
517
+ return [0, -1, -1] if bytes.bytesize < 8
518
+
519
+ method_id = bytes[0, 8].unpack1("q<")
520
+ file_id = bytes.bytesize >= 16 ? bytes[8, 8].unpack1("q<") : -1
521
+ lineno = bytes.bytesize >= 24 ? bytes[16, 8].unpack1("q<") : -1
522
+ [method_id, file_id, lineno]
523
+ end
524
+
525
+ def read_proc_fork_child_pid(payload)
526
+ bytes = payload.to_s.b
527
+ return 0 if bytes.bytesize < 4
528
+
529
+ bytes[0, 4].unpack1("L<")
530
+ end
531
+
532
+ def exec_basename(payload)
533
+ slot_size = Vivarium::PROC_EXEC_SLOT_SIZE
534
+ bytes = payload.to_s.b
535
+ return nil if bytes.empty?
536
+
537
+ filename = Vivarium.c_string(bytes[0, slot_size])
538
+ return nil if filename.empty?
539
+
540
+ File.basename(filename)
541
+ end
542
+ end
543
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vivarium
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end