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(¬ify_block)
click to toggle source
# File lib/argtrace/tracer.rb, line 405 def set_notify(¬ify_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