class Argtrace::Tracer

Main class for tracing with TracePoint.

Attributes

is_dead[RW]

Public Class Methods

add_running_trace(trace) click to toggle source
# File lib/argtrace/tracer.rb, line 453
def self.add_running_trace(trace)
  @@running_trace << trace
  if @@running_trace_first
    @@running_trace_first = false
    at_exit do
      @@running_trace.each do |trace|
        trace.disable
      end
      @@running_trace.each do |trace|
        trace.call_exit
      end
      @@running_trace.clear
    end
  end
end
force_stop_all() click to toggle source
# File lib/argtrace/tracer.rb, line 473
def self.force_stop_all
  @@running_trace.each do |t|
    t.disable
  end
end
new() click to toggle source
# File lib/argtrace/tracer.rb, line 76
def initialize()
  @notify_block = nil
  @callstack = CallStack.new
  @tp_holder = nil
  @is_dead = false

  # prune_event_count > 0 while no need to notify.
  # This is used to avoid undesirable signature lerning caused by error test.
  @prune_event_count = 0

  # cache of singleton-class => basic-class
  @singleton_class_map_cache = {}

  # cache of method location (klass => method_id => source_path)
  @method_location_cache = Hash.new{|h, klass| h[klass] = {}}

  # cache of judge result whether method is library-defined or user-defined
  @ignore_paths_cache = {}
end
remove_running_trace(trace) click to toggle source
# File lib/argtrace/tracer.rb, line 469
def self.remove_running_trace(trace)
  @@running_trace.delete(trace)
end

Public Instance Methods

call_exit() click to toggle source
# File lib/argtrace/tracer.rb, line 409
def call_exit
  @exit_block.call if @exit_block
end
check_event_filter(tp) click to toggle source

check filter from set_filter

# File lib/argtrace/tracer.rb, line 385
def check_event_filter(tp)
  if @prune_event_filter
    return @prune_event_filter.call(tp)
  else
    return true
  end
end
disable() click to toggle source
# File lib/argtrace/tracer.rb, line 417
def disable
  @tp_holder.disable
end
enable() click to toggle source
# File lib/argtrace/tracer.rb, line 413
def enable
  @tp_holder.enable
end
get_block_param_value(parameters, tp) click to toggle source

pickup block parameter as proc if exists

# File lib/argtrace/tracer.rb, line 308
def get_block_param_value(parameters, tp)
  if tp.event == :c_call
    # I cannot get parameter values of c_call ...
    return nil
  else
    parameters.each do |param|
      if param[0] == :block
        if param[1] == :&
          # workaround for ActiveSupport gem.
          # I don't know why this happen. just discard info about it.
          return nil
        end
        begin
          val = tp.binding.eval(param[1].to_s)
        rescue => e
          $stderr.puts "----- argtrace bug -----"
          $stderr.puts parameters.inspect
          $stderr.puts e.full_message
          $stderr.puts "------------------------"
          raise
        end
        return val
      end
    end
    return nil
  end
end
get_called_method(tp) click to toggle source

current called method object

# File lib/argtrace/tracer.rb, line 337
def get_called_method(tp)
  if tp.defined_class != tp.self.class
    # I cannot identify all cases for this, so checks strictly.

    if tp.defined_class.singleton_class?
      # On class method call, "defined_class" becomes singleton(singular) class.
    elsif tp.self.is_a?(tp.defined_class)
      # On ancestor's method call, "defined_class" is different from self.class.
    else
      # This is unknown case.
      raise "type inconsistent def:#{tp.defined_class} <=> self:#{tp.self.class} "
    end
  end
  return tp.self.method(tp.method_id)
end
get_location(klass, method_id) click to toggle source
# File lib/argtrace/tracer.rb, line 209
def get_location(klass, method_id)
  unless @method_location_cache[klass].key?(method_id)
    path = nil
    m = klass.instance_method(method_id)
    if m and m.source_location
      path = m.source_location[0]
    end
    @method_location_cache[klass][method_id] = path
  end

  return @method_location_cache[klass][method_id]
end
get_param_types(parameters, tp) click to toggle source

convert parameters to Parameter[]

# File lib/argtrace/tracer.rb, line 269
def get_param_types(parameters, tp)
  if tp.event == :c_call
    # I cannot get parameter values of c_call ...
    return []
  else
    return parameters.map{|param|
      # param[0]=:req, param[1]=:x
      p = Parameter.new
      p.mode = param[0]
      p.name = param[1]
      if param[0] == :block
        p.type = Signature.new
      elsif param[1] == :* || param[1] == :&
        # workaround for ActiveSupport gem.
        # I don't know why this happen. just discard info about it.
        type = TypeUnion.new
        p.type = type
      else
        # TODO: this part is performance bottleneck caused by eval,
        # but It's essential code
        type = TypeUnion.new
        begin
          val = tp.binding.eval(param[1].to_s)
        rescue => e
          $stderr.puts "----- argtrace bug -----"
          $stderr.puts parameters.inspect
          $stderr.puts e.full_message
          $stderr.puts "------------------------"
          raise
        end
        type.add Type.new_with_value(val)
        p.type = type
      end
      p
    }
  end
end
ignore_event?(tp) click to toggle source

true for the unhandleable events

# File lib/argtrace/tracer.rb, line 354
def ignore_event?(tp)
  if tp.defined_class.equal?(Class) and tp.method_id == :new
    # On "Foo.new", I want "Foo" here,
    # but "binding.receiver" equals to caller's "self" so I cannot get "Foo" from anywhere.
    # Just ignore.
    return true
  end

  if tp.defined_class.equal?(BasicObject) and tp.method_id == :initialize
    # On "Foo#initialize", I want "Foo" here,
    # but if "Foo" doesn't  have explicit "initialize" method then no clue to get "Foo".
    # Just ignore.
    return true
  end

  if tp.defined_class.equal?(Class) and tp.method_id == :inherited
    # I can't understand this.
    # Just ignore.
    return true
  end

  if tp.defined_class.equal?(Module) and tp.method_id == :method_added
    # I can't understand this.
    # Just ignore.
    return true
  end

  return false
end
non_singleton_class(klass) click to toggle source

convert singleton class (like #<Class:Regexp>) to non singleton class (like Regexp)

# File lib/argtrace/tracer.rb, line 230
def non_singleton_class(klass)
  unless klass.singleton_class?
    return klass
  end

  if /^#<Class:([A-Za-z0-9_:]+)>$/ =~ klass.inspect
    # maybe normal name class
    klass_name = Regexp.last_match[1]
    begin
      ret_klass = klass_name.split('::').inject(Kernel){|nm, sym| nm.const_get(sym)}
    rescue => e
      $stderr.puts "----- argtrace bug -----"
      $stderr.puts "cannot convert class name #{klass} => #{klass_name}"
      $stderr.puts e.full_message
      $stderr.puts "------------------------"
      raise
    end
    return ret_klass
  end

  # maybe this class is object's singleton class / special named class.
  # I can't find efficient way, so cache the calculated result.
  if @singleton_class_map_cache.key?(klass)
    return @singleton_class_map_cache[klass]
  end
  begin
    ret_klass = ObjectSpace.each_object(Module).find{|x| x.singleton_class == klass}
    @singleton_class_map_cache[klass] = ret_klass
  rescue => e
    $stderr.puts "----- argtrace bug -----"
    $stderr.puts "cannot convert class name #{klass} => #{klass_name}"
    $stderr.puts e.full_message
    $stderr.puts "------------------------"
    raise
  end
  return ret_klass
end
set_exit(&exit_block) click to toggle source
# File lib/argtrace/tracer.rb, line 401
def set_exit(&exit_block)
  @exit_block = exit_block
end
set_filter(&prune_event_filter) click to toggle source

set event filter

true = normal process
false = skip notify
:prune = skip notify and skip all nested events
# File lib/argtrace/tracer.rb, line 397
def set_filter(&prune_event_filter)
  @prune_event_filter = prune_event_filter
end
set_notify(&notify_block) click to toggle source
# File lib/argtrace/tracer.rb, line 405
def set_notify(&notify_block)
  @notify_block = notify_block
end
standard_lib_root_path() click to toggle source
# File lib/argtrace/tracer.rb, line 176
def standard_lib_root_path
  # Search for standard lib path by some method location.
  # I choose Pathname#parent here.
  path = get_location(Pathname, :parent)
  lib_dir = File.dirname(path)
  return lib_dir
end
start_trace() click to toggle source

start TracePoint with callback block

# File lib/argtrace/tracer.rb, line 427
def start_trace()
  tp = TracePoint.new(:c_call, :c_return, :call, :return, :b_call) do |tp|
    begin
      tp.disable
      # DEBUG:
      # p [tp.event, tp.defined_class, tp.method_id]
      self.trace(tp)
    rescue => e
      $stderr.puts "----- argtrace catch exception -----"
      $stderr.puts e.full_message
      $stderr.puts "------------------------------------"
      @is_dead = true
    ensure
      tp.enable unless @is_dead
    end
  end
  @tp_holder = tp

  # hold reference and register at_exit
  Tracer.add_running_trace(self)

  tp.enable
end
stop_trace() click to toggle source
# File lib/argtrace/tracer.rb, line 421
def stop_trace()
  self.disable
  Tracer.remove_running_trace(self)
end
trace(tp) click to toggle source

entry point of trace event

# File lib/argtrace/tracer.rb, line 97
def trace(tp)
  if ignore_event?(tp)
    return
  end

  if [:b_call, :b_return].include?(tp.event)
    trace_block_event(tp)
  else
    trace_method_event(tp)
  end
end
trace_block_event(tp) click to toggle source

process block call/return event

# File lib/argtrace/tracer.rb, line 110
def trace_block_event(tp)
  return if tp.event != :b_call

  # I cannot determine the called block instance directly, so use block's location.
  callinfos_with_block = @callstack.find_by_block_location(tp)
  callinfos_with_block.each do |callinfo|
    block_param = callinfo.signature.get_block_param
    # $stderr.puts [tp.event, tp.path, tp.lineno, callinfo.block_proc.parameters, tp.parameters].inspect
    block_param_types = get_param_types(callinfo.block_proc.parameters, tp)
    # TODO: return type (but maybe, there is no demand)
    block_param.type.merge(block_param_types, nil)
  end
end
trace_method_event(tp) click to toggle source

process method call/return event

# File lib/argtrace/tracer.rb, line 125
def trace_method_event(tp)
  if [:call, :c_call].include?(tp.event)
    # I don't know why but tp.parameters is different from called_method.parameters
    # and called_method.parameters not work.
    # called_method = get_called_method(tp)

    case check_event_filter(tp)
    when :prune
      @prune_event_count += 1
      skip_flag = true
    when false
      skip_flag = true
    end

    callinfo = CallInfo.new
    signature = Signature.new
    signature.defined_class = non_singleton_class(tp.defined_class)
    signature.method_id = tp.method_id
    signature.is_singleton_method = tp.defined_class.singleton_class?
    signature.params = get_param_types(tp.parameters, tp)
    callinfo.signature = signature
    callinfo.block_proc = get_block_param_value(tp.parameters, tp)

    @callstack.push_callstack(callinfo)

    if !skip_flag && @prune_event_count == 0
      # skip if it's object specific method
      @notify_block.call(tp.event, callinfo) if @notify_block
    end
  else
    case check_event_filter(tp)
    when :prune
      @prune_event_count -= 1
      skip_flag = true
    when false
      skip_flag = true
    end

    callinfo = @callstack.pop_callstack(tp)
    if callinfo
      rettype = TypeUnion.new
      rettype.add(Type.new_with_value(tp.return_value))
      callinfo.signature.return_type = rettype
         
      if !skip_flag && @prune_event_count == 0
        @notify_block.call(tp.event, callinfo) if @notify_block
      end
    end
  end
end
under_module?(klass, mod) click to toggle source

true if klass is defined under Module

# File lib/argtrace/tracer.rb, line 223
def under_module?(klass, mod)
  ks = non_singleton_class(klass).to_s
  ms = mod.to_s
  return ks == ms || ks.start_with?(ms + "::")
end
user_source?(klass, method_id) click to toggle source

true if method is defined in user source

# File lib/argtrace/tracer.rb, line 185
def user_source?(klass, method_id)
  path = get_location(klass, method_id)
  return false unless path

  unless @ignore_paths_cache.key?(path)
    if path.start_with?("<internal:")
      # skip all ruby internal method
      @ignore_paths_cache[path] = true
    elsif path == "(eval)"
      # skip all eval
      @ignore_paths_cache[path] = true
    elsif path.start_with?(standard_lib_root_path())
      # skip all standard lib
      @ignore_paths_cache[path] = true
    elsif Gem.path.any?{|x| path.start_with?(x)}
      # skip all installed gem files
      @ignore_paths_cache[path] = true
    else
      @ignore_paths_cache[path] = false
    end
  end
  return ! @ignore_paths_cache[path]
end