ruby/tool/timeline/capture.rb
2026-07-07 19:07:22 +09:00

120 lines
3.1 KiB
Ruby
Executable File

#!/usr/bin/env ruby
# frozen_string_literal: true
require 'erb'
require 'optparse'
require 'pathname'
require_relative 'lib/tracepoint_defs'
EXTRA_GROUPS = RubyTimelineTool::USDT_DEFS.keys - RubyTimelineTool::DEFAULT_GROUPS
options = {}
OptionParser.new do |parser|
parser.accept(Pathname) do |value|
Pathname.new(value)
end
parser.on('-r', '--ruby-bin RUBY', Pathname, 'Path to the ruby executable')
parser.on('-m', '--gc-module-so GC_MODULE', Pathname, 'Path to the GC module .so')
parser.on('-e', '--erb-debug', TrueClass, 'Print the Ruby script generated by ERB. For debugging.')
parser.on('-p', '--print-script', TrueClass, 'Print the content of the bpftrace script')
parser.on('-d', '--dry-run', TrueClass, 'Dry run. Print the bpftrace command to be executed, but do not actually execute it.')
parser.on('-g', '--extra-groups=', "Enable extra USDT groups, separated by comma. Available groups: #{EXTRA_GROUPS.join(', ')}")
end.parse!(into: options)
[:'ruby-bin'].each do |k|
if !options.include?(k)
raise "Option --#{k} is required"
end
end
ruby_bin = options[:'ruby-bin']
if !ruby_bin.file?
raise "Ruby executable '#{ruby_bin}' does not exist"
end
gc_module_so = options[:'gc-module-so']
gc_module_kind = case
when gc_module_so.nil?
nil
when m = /librubygc\.(\w+)/.match(gc_module_so.basename.to_s)
m[1]
else
raise "Unexpected gc module so name: #{gc_module_so}"
end
script_path = Pathname(__dir__) / 'capture.bt'
script = File.read(script_path)
template = ERB.new(script, trim_mode: '%<>')
template.location = [script_path.to_s, 1]
if options[:'erb-debug']
$stderr.puts template.src
end
extra_groups_opt = options[:'extra-groups']
selected_groups = RubyTimelineTool::DEFAULT_GROUPS.dup
if !extra_groups_opt.nil?
extra_groups_opt.split(',').each do |group|
if !EXTRA_GROUPS.include?(group)
raise "Unknown USDT group #{group}"
end
selected_groups << group
end
end
selected_groups.uniq!
$stderr.puts "Selected groups: #{selected_groups.join(',')}"
usdts = RubyTimelineTool::USDT_DEFS.values_at(*selected_groups).flatten
ScriptUsdtEntry = Struct.new('ScriptUsdtEntry', :file, :probe_name, :nargs)
usdt_entries = []
usdts.each do |t|
file = case [gc_module_kind, t.where]
in nil, 'ruby' then ruby_bin
in nil, 'default' then ruby_bin
in x, 'ruby' then ruby_bin
in x, y if x == y then gc_module_so
else nil
end
if file.nil?
@stderr.puts "Skipped probe '#{probe_name}' which is for '#{location}'. Current GC module: '#{gc_module_kind}'."
next
end
nargs = t.args.length
usdt_entries << ScriptUsdtEntry.new(file, t.probe_name, nargs)
end
content = template.result_with_hash({
usdt_entries:,
})
if options[:'print-script']
$stderr.puts content
end
IO.write('capture.out.bt', content)
command_line = ['sudo', 'bpftrace', '-v', '--unsafe', 'capture.out.bt']
$stderr.puts 'Command to execute:'
$stderr.puts command_line.map { |s| "'#{s}'" }.join(' ')
if options[:'dry-run']
$stderr.puts 'Dry run. Exit...'
exit 0
end
exec(*command_line)