Skip to content

Instantly share code, notes, and snippets.

@baweaver
Created January 26, 2019 20:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save baweaver/7e5d8fe356765d7af7e15749687ec28c to your computer and use it in GitHub Desktop.
Save baweaver/7e5d8fe356765d7af7e15749687ec28c to your computer and use it in GitHub Desktop.
Raw notes from my TracePoint session

TracePoint

What is TracePoint?

A class that provides the functionality of Kernel#set_trace_func in a nice Object-Oriented API.

Well what is set_trace_func?

Establishes proc as the handler for tracing, or disables tracing if the parameter is nil.

proc takes up to six parameters:

  • an event name
  • a filename
  • a line number
  • an object id
  • a binding
  • the name of a class

c-call call a C-language routine

c-return return from a C-language routine

call call a Ruby method

class start a class or module definition

end finish a class or module definition

line execute code on a new line

raise raise an exception

return return from a Ruby method

Aphyr Post

https://aphyr.com/posts/173-monkeypatching-is-for-wimps-use-set-trace-func

class Fixnum
  def add(other)
    self + other
  end
end

set_trace_func proc { |event, file, line, id, binding, classname|
  if classname == Fixnum && id == :add && event == 'call'
    # We can, of course, find the receiver of the current method
    me = binding.eval("self")

    # And the binding gives us access to all variables declared
    # in that method's scope. At call time only the method arguments will be
    # defined.
    args = binding.eval("local_variables")
      .each_with_object({}) do |var_name, vars|
          value = binding.eval var_name
          vars[var_name] = value unless value.nil?
        end

    # We can also *change* those arguments.
    args.each do |var_name, value|
      if value.is_a?(Numeric)
        binding.eval "#{var_name} = #{value + 1}"
      end
    end
  end
}
    
puts 1.add 1 # => 3

TracePoint Documentation

trace = TracePoint.new(:raise) do |tp|
    p [tp.lineno, tp.event, tp.raised_exception]
end
#=> #<TracePoint:disabled>

trace.enable
#=> false

0 / 0
#=> [5, :raise, #<ZeroDivisionError: divided by 0>]
  • TracePoint#event - Type of event
  • TracePoint#lineno - Line number the event
  • TracePoint#method_id - Return the name at the definition of the method being called. (basically method_name)
  • TracePoint#binding -Return the generated binding object from event
  • TracePoint#defined_class - Return class or module of the method being called.
  • TracePoint#enable/disable - Enable or disable the trace
  • TracePoint#parameters - Return the parameters of the method or block that the current hook belongs to.
  • TracePoint#path - Path of the file being run
  • TracePoint#self - Return the trace object during event
class Integer
  def add(other) self + other end
end

add_trace = TracePoint.new(:call) do |trace|
  if trace.defined_class == Integer && trace.method_id == :add
    args = trace
      .parameters
      .map(&:last)
      .to_h { |variable_name| [variable_name, trace.binding.eval(variable_name.to_s)] }

    args.each do |variable_name, value|
      if value.is_a?(Numeric)
        trace.binding.eval "#{variable_name} = #{value + 1}"
      end
    end
  end
end
  • Fixnum was deprecated in Ruby 2.5
  • trace.binding.eval isn't fond of symbols

Custom Wrapper around TracePoint?

MyTracePoint

def on_method(method_name, &fn)
  TracePoint.new(:call, :c_call) do |trace|
    begin
      next unless trace.method_id == method_name.to_sym
    
      yield(trace)
    rescue RuntimeError => e
      p e
    end
  end
end

def extract_args(trace)
  trace.binding.eval('local_variables').to_h do |name|
    [name, trace.binding.eval(name.to_s)]
  end
end

def method_with_args_like(method_name, arg_matchers, &fn)
  on_method(method_name) do |trace|
    arg_hash = extract_args(trace)
    
    next unless Qo[**arg_matchers].match?(arg_hash)
      
    yield(trace)
  end
end

def method_matches(method_name, qo_matcher, &fn)
  on_method(method_name) do |trace|
    arg_hash = extract_args(trace)
    
    next unless qo_matcher.match?(arg_hash)
      
    yield(trace)
  end
end

def on_method_return(method_name, &fn)
  TracePoint.new(:c_return, :return) do |trace|
    begin
      next unless trace.method_id == method_name.to_sym
    
      yield(trace)
    rescue RuntimeError => e
      p e
    end
  end
end

def method_return_matches(method_name, expected_match, &fn)
  on_method_return(method_name) do |trace|
    next unless expected_match === trace.return_value
    yield(trace)
  end
end

def on_method_raise(method_name, &fn)
  TracePoint.new(:raise) do |trace|
    begin
      next unless trace.method_id == method_name.to_sym
    
      yield(trace)
    rescue RuntimeError => e
      p e
    end
  end
end

def method_raised_with(method_name, qo_matcher, &fn)
  on_method_raise(method_name) do |trace|
    arg_hash = extract_args(trace).tap { |v| p v }
    
    next unless qo_matcher.match?(arg_hash)
      
    yield(trace)
  end
end

https://bugs.ruby-lang.org/issues/9358

In TracePoint class, if a particular introspection method is not supported then 'not supported by this event (RuntimeError)' is thrown.

https://ruby-doc.org/core-2.6/TracePoint.html#method-c-new

A block must be given, otherwise an ArgumentError is raised.

If the trace method isn't included in the given events filter, a RuntimeError is raised.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment