module Rcov
# A CallSiteAnalyzer can be used to obtain information about:
# * where a method is defined ("+defsite+")
# * where a method was called from ("+callsite+")
#
# == Example
# <tt>example.rb</tt>:
# class X
# def f1; f2 end
# def f2; 1 + 1 end
# def f3; f1 end
# end
#
# analyzer = Rcov::CallSiteAnalyzer.new
# x = X.new
# analyzer.run_hooked do
# x.f1
# end
# # ....
#
# analyzer.run_hooked do
# x.f3
# # the information generated in this run is aggregated
# # to the previously recorded one
# end
#
# analyzer.analyzed_classes # => ["X", ... ]
# analyzer.methods_for_class("X") # => ["f1", "f2", "f3"]
# analyzer.defsite("X#f1") # => DefSite object
# analyzer.callsites("X#f2") # => hash with CallSite => count
# # associations
# defsite = analyzer.defsite("X#f1")
# defsite.file # => "example.rb"
# defsite.line # => 2
#
# You can have several CallSiteAnalyzer objects at a time, and it is
# possible to nest the #run_hooked / #install_hook/#remove_hook blocks: each
# analyzer will manage its data separately. Note however that no special
# provision is taken to ignore code executed "inside" the CallSiteAnalyzer
# class.
#
# +defsite+ information is only available for methods that were called under
# the inspection of the CallSiteAnalyzer, i.o.w. you will only have +defsite+
# information for those methods for which callsite information is
# available.
class CallSiteAnalyzer < DifferentialAnalyzer
# A method definition site.
class DefSite < Struct.new(:file, :line)
end
# Object representing a method call site.
# It corresponds to a part of the callstack starting from the context that
# called the method.
class CallSite < Struct.new(:backtrace)
# The depth of a CallSite is the number of stack frames
# whose information is included in the CallSite object.
def depth
backtrace.size
end
# File where the method call originated.
# Might return +nil+ or "" if it is not meaningful (C extensions, etc).
def file(level = 0)
stack_frame = backtrace[level]
stack_frame ? stack_frame[2] : nil
end
# Line where the method call originated.
# Might return +nil+ or 0 if it is not meaningful (C extensions, etc).
def line(level = 0)
stack_frame = backtrace[level]
stack_frame ? stack_frame[3] : nil
end
# Name of the method where the call originated.
# Returns +nil+ if the call originated in +toplevel+.
# Might return +nil+ if it could not be determined.
def calling_method(level = 0)
stack_frame = backtrace[level]
stack_frame ? stack_frame[1] : nil
end
# Name of the class holding the method where the call originated.
# Might return +nil+ if it could not be determined.
def calling_class(level = 0)
stack_frame = backtrace[level]
stack_frame ? stack_frame[0] : nil
end
end
@hook_level = 0
# defined this way instead of attr_accessor so that it's covered
def self.hook_level # :nodoc:
@hook_level
end
def self.hook_level=(x) # :nodoc:
@hook_level = x
end
def initialize
super(:install_callsite_hook, :remove_callsite_hook,
:reset_callsite)
end
# Classes whose methods have been called.
# Returns an array of strings describing the classes (just klass.to_s for
# each of them). Singleton classes are rendered as:
# #<Class:MyNamespace::MyClass>
def analyzed_classes
raw_data_relative.first.keys.map{|klass, meth| klass}.uniq.sort
end
# Methods that were called for the given class. See #analyzed_classes for
# the notation used for singleton classes.
# Returns an array of strings or +nil+
def methods_for_class(classname)
a = raw_data_relative.first.keys.select{|kl,_| kl == classname}.map{|_,meth| meth}.sort
a.empty? ? nil : a
end
alias_method :analyzed_methods, :methods_for_class
# Returns a hash with <tt>CallSite => call count</tt> associations or +nil+
# Can be called in two ways:
# analyzer.callsites("Foo#f1") # instance method
# analyzer.callsites("Foo.g1") # singleton method of the class
# or
# analyzer.callsites("Foo", "f1")
# analyzer.callsites("#<class:Foo>", "g1")
def callsites(classname_or_fullname, methodname = nil)
rawsites = raw_data_relative.first[expand_name(classname_or_fullname, methodname)]
return nil unless rawsites
ret = {}
# could be a job for inject but it's slow and I don't mind the extra loc
rawsites.each_pair do |backtrace, count|
ret[CallSite.new(backtrace)] = count
end
ret
end
# Returns a DefSite object corresponding to the given method
# Can be called in two ways:
# analyzer.defsite("Foo#f1") # instance method
# analyzer.defsite("Foo.g1") # singleton method of the class
# or
# analyzer.defsite("Foo", "f1")
# analyzer.defsite("#<class:Foo>", "g1")
def defsite(classname_or_fullname, methodname = nil)
file, line = raw_data_relative[1][expand_name(classname_or_fullname, methodname)]
return nil unless file && line
DefSite.new(file, line)
end
private
def expand_name(classname_or_fullname, methodname = nil)
if methodname.nil?
case classname_or_fullname
when /(.*)#(.*)/ then classname, methodname = $1, $2
when /(.*)\.(.*)/ then classname, methodname = "#<Class:#{$1}>", $2
else
raise ArgumentError, "Incorrect method name"
end
return [classname, methodname]
end
[classname_or_fullname, methodname]
end
def data_default; [{}, {}] end
def raw_data_absolute
raw, method_def_site = RCOV__.generate_callsite_info
ret1 = {}
ret2 = {}
raw.each_pair do |(klass, method), hash|
begin
key = [klass.to_s, method.to_s]
ret1[key] = hash.clone #Marshal.load(Marshal.dump(hash))
ret2[key] = method_def_site[[klass, method]]
#rescue Exception
end
end
[ret1, ret2]
end
def aggregate_data(aggregated_data, delta)
callsites1, defsites1 = aggregated_data
callsites2, defsites2 = delta
callsites2.each_pair do |(klass, method), hash|
dest_hash = (callsites1[[klass, method]] ||= {})
hash.each_pair do |callsite, count|
dest_hash[callsite] ||= 0
dest_hash[callsite] += count
end
end
defsites1.update(defsites2)
end
def compute_raw_data_difference(first, last)
difference = {}
default = Hash.new(0)
callsites1, defsites1 = *first
callsites2, defsites2 = *last
callsites2.each_pair do |(klass, method), hash|
old_hash = callsites1[[klass, method]] || default
hash.each_pair do |callsite, count|
diff = hash[callsite] - (old_hash[callsite] || 0)
if diff > 0
difference[[klass, method]] ||= {}
difference[[klass, method]][callsite] = diff
end
end
end
[difference, defsites1.update(defsites2)]
end
end
end