class TraceGraph::Tracer

Public Class Methods

new(options) click to toggle source
# File lib/trace_graph/tracer.rb, line 5
def initialize(options)
  @options = options
  # We check included paths differently than excluded ones to allow nil to be bassed, to include everyting
  @included_paths = options.key?(:included_paths) ? options[:included_paths] : []
  @excluded_paths = options[:excluded_paths] || []
  @include_protected = options[:include_protected] || false
  @mark_duplicate_calls = options.key?(:mark_duplicate_calls) ? options[:mark_duplicate_calls] : true
  @show_arguments = options[:show_arguments] || false
  @show_return_values = options[:show_return_values] || false
  @only_class_transitions = options[:only_class_transitions] || false

  @trace_point = build_trace_point
  @top_node = TraceGraph::TraceNode.new("trace")
  @stack = []
  @all_nodes = [@top_node]
  @edge_count = 0
  @unique_node_counts = {}
end

Public Instance Methods

call_trace() click to toggle source
# File lib/trace_graph/tracer.rb, line 41
def call_trace
  @top_node
end
node_count() click to toggle source
# File lib/trace_graph/tracer.rb, line 45
def node_count
  @all_nodes.length - 1 # should top_node count as a node?
end
trace() { || ... } click to toggle source
# File lib/trace_graph/tracer.rb, line 24
def trace
  unless block_given?
    raise Error.new "You must pass a block to the tracer."
  end

  @edge_count = 0
  @unique_node_counts = {}
  @trace_point.enable
  yield
  @trace_point.disable
  # TODO : Maybe outputting this should be an options, or a different tracer?
  puts @top_node.tree_graph
  if @options[:png]
    write_png @options[:png]
  end
end

Private Instance Methods

add_node(graph, node, parent) click to toggle source
# File lib/trace_graph/tracer.rb, line 57
def add_node graph, node, parent
  label = node.label.gsub("←","\n←").gsub("→","\n→")
  gnode = graph.add_nodes(label)
  if node.is_duplicate
    gnode[:color] = "red:white"
  end
  if parent
    edge = graph.add_edges(parent.label, label)
    edge[:label] = @edge_count + 1
    @edge_count += 1
    # add edge
  end
  node.sub_nodes.each do |sub_node|
    add_node graph, sub_node, node
  end
end
build_node_label(tp, for_call: true) click to toggle source
# File lib/trace_graph/tracer.rb, line 92
def build_node_label(tp, for_call: true)
  label = "#{tp.defined_class}##{tp.method_id}"
  if tp.defined_class.name != tp.self.class.name
    label = "#{tp.self.class.name} -> #{label}"
  end

  args = extract_arguments(tp)
  if @show_arguments && args.any?
    label = "#{label} →(#{args})"
  end
  is_dupicate = false
  if @mark_duplicate_calls
    if for_call # we don't want to mess with the count on a return
      if @unique_node_counts[label]
        @unique_node_counts[label] += 1
      else
        @unique_node_counts[label] = 0
      end
    end
    if @unique_node_counts[label] && @unique_node_counts[label] > 0
      label = "#{label} (##{@unique_node_counts[label]+1})"
      is_duplicate = true
    end
  end
  return label, is_duplicate
end
build_trace_point() click to toggle source
# File lib/trace_graph/tracer.rb, line 74
def build_trace_point
  TracePoint.new(:call,:return) do |tp|
    if should_include_trace_line(tp)
      if tp.event == :call
        handle_call(tp)
      elsif tp.event == :return
        handle_return(tp)
      end
    end
  end
end
extract_arguments(trace) click to toggle source
# File lib/trace_graph/tracer.rb, line 86
def extract_arguments(trace)
  return {} unless trace.respond_to?(:parameters)
  param_names = trace.parameters.map(&:last)
  param_names.map { |n| [n, trace.binding.eval(n.to_s)] }.to_h
end
handle_call(tp) click to toggle source
# File lib/trace_graph/tracer.rb, line 119
def handle_call(tp)
  if @only_class_transitions
    last_node = @stack.last
    if last_node && last_node.class_name == tp.self.class.name
      return
    end
  end
  parent = @stack.last
  label, is_duplicate = build_node_label(tp)
  new_node = TraceGraph::TraceNode.new(label, is_duplicate: is_duplicate, class_name: tp.self.class.name)
  @stack << new_node
  @all_nodes << new_node
  if parent
    parent << new_node
  else
    @top_node << new_node
  end
end
handle_return(tp) click to toggle source
# File lib/trace_graph/tracer.rb, line 138
def handle_return(tp)
  label, _is_duplicate = build_node_label(tp, for_call: false)
  last_node = @stack.last
  if last_node && last_node.label == label
    if @show_return_values && tp.return_value
      last_node.label = "#{last_node.label} ←(#{tp.return_value})"
    end
    @stack.pop
  end
end
should_include_trace_line(tp) click to toggle source
# File lib/trace_graph/tracer.rb, line 149
def should_include_trace_line(tp)
  should_include = false
  if @included_paths
    @included_paths.each do |path|
      path = path.is_a?(String) ? /#{path}/ : path
      if tp.inspect =~ path
        should_include = true
      end
    end
  else
    # If included_paths was passed as nil that means to include everything that's not excluded
    should_include = true
  end
  @excluded_paths.each do |path|
    path = path.is_a?(String) ? /#{path}/ : path
    if tp.inspect =~ path
      should_include = false
    end
  end
  klass = tp.defined_class
  method = tp.method_id
  public_methods = klass.public_methods(false) + klass.public_instance_methods(false)
  unless public_methods.include?(method) || @include_protected
    should_include = false
  end
  
  return should_include
end
write_png(png) click to toggle source
# File lib/trace_graph/tracer.rb, line 51
def write_png png
  g = ::GraphViz.new( :G, :type => :digraph )
  add_node g, @top_node, nil
  g.output( :png => png )
end