class PowerTrace::Entry

Constants

ATTRIBUTE_COLORS
DEFAULT_LINE_LIMIT
EMPTY_ARRAY
EMPTY_HASH
EMPTY_STRING
SET_IVAR_INSTRUCTION_REGEX
SPACE
UNDEFINED

Attributes

arguments[R]
filepath[R]
frame[R]
ivars[R]
line_number[R]
locals[R]
receiver[R]

Public Class Methods

new(frame) click to toggle source
# File lib/power_trace/entry.rb, line 17
def initialize(frame)
  @frame = frame
  @filepath, @line_number = frame.source_location
  @receiver = frame.receiver
  @locals, @arguments = collect_locals_and_arguments
  @ivars = collect_ivars
end

Public Instance Methods

arguments_string(options = {}) click to toggle source
# File lib/power_trace/entry.rb, line 33
    def arguments_string(options = {})
      <<~STR.chomp
        #{options[:indentation]}(Arguments)
        #{hash_to_string(arguments, false, options)}
      STR
    end
call_trace(options = {}) click to toggle source
# File lib/power_trace/entry.rb, line 54
def call_trace(options = {})
  "#{location(options)}:in `#{name(options)}'"
end
ivars_string(options = {}) click to toggle source
# File lib/power_trace/entry.rb, line 47
    def ivars_string(options = {})
      <<~STR.chomp
        #{options[:indentation]}(Instance Variables)
        #{hash_to_string(ivars, false, options)}
      STR
    end
locals_string(options = {}) click to toggle source
# File lib/power_trace/entry.rb, line 40
    def locals_string(options = {})
      <<~STR.chomp
        #{options[:indentation]}(Locals)
        #{hash_to_string(locals, false, options)}
      STR
    end
location(options = {}) click to toggle source
# File lib/power_trace/entry.rb, line 29
def location(options = {})
  "#{filepath}:#{line_number}"
end
name(options = {}) click to toggle source
# File lib/power_trace/entry.rb, line 25
def name(options = {})
  frame.frame_description
end
to_s(options = {}) click to toggle source
# File lib/power_trace/entry.rb, line 81
def to_s(options = {})
  # this is to prevent entries from polluting each other's options
  # of course, that'd only happen if I did something stupid ;)
  assemble_string(options.dup)
end

Private Instance Methods

assemble_string(options) click to toggle source
# File lib/power_trace/entry.rb, line 93
def assemble_string(options)
  strings = [call_trace(options)]

  indentation = SPACE * (options[:extra_info_indent] || 0)
  options[:indentation] = indentation

  if arguments.present?
    strings << arguments_string(options)
  end

  if locals.present?
    strings << locals_string(options)
  end

  if ivars.present?
    strings << ivars_string(options)
  end

  strings.join("\n")
end
collect_ivars() click to toggle source

we need to make sure

  1. the frame is iseq (vm instructions)

  2. and the instructions contain `setinstancevariable` instructions

and only then we can start capturing instance variables from the frame this is to make sure we only capture the instance variables set inside the current method call otherwise, it'll create a lot noise

# File lib/power_trace/entry.rb, line 165
def collect_ivars
  iseq = frame.instance_variable_get(:@iseq)

  return EMPTY_HASH unless iseq

  set_ivar_instructios = iseq.disasm.split("\n").select { |i| i.match?(SET_IVAR_INSTRUCTION_REGEX) }

  return EMPTY_HASH unless set_ivar_instructios.present?

  new_ivars = set_ivar_instructios.map do |i|
    i.match(/:(@\w+),/)[1]
  end

  new_ivars.inject({}) do |hash, ivar_name|
    hash[ivar_name] = receiver.instance_variable_get(ivar_name.to_sym)
    hash
  end
end
collect_locals_and_arguments() click to toggle source
# File lib/power_trace/entry.rb, line 184
def collect_locals_and_arguments
  locals = {}
  arguments = {}

  frame.local_variables.each do |name|
    value = frame.local_variable_get(name)

    if method_parameters.include?(name)
      arguments[name] = value
    else
      locals[name] = value
    end
  end

  [locals, arguments]
end
hash_to_string(hash, inspect, options) click to toggle source
# File lib/power_trace/entry.rb, line 114
def hash_to_string(hash, inspect, options)
  truncation = options[:line_limit] || DEFAULT_LINE_LIMIT
  indentation = (options[:indentation] || EMPTY_STRING) + SPACE * 2

  elements_string = hash.map do |key, value|
    value_string = value_to_string(value, truncation)
    "#{key.to_s}: #{value_string}"
  end.join("\n#{indentation}")
  "#{indentation}#{elements_string}"
end
method() click to toggle source
# File lib/power_trace/entry.rb, line 201
def method
  @method ||= Object.instance_method(:method).bind(@receiver).call(name)
rescue NameError
  # if any part of the program uses Refinement to extend its methods
  # we might still get NoMethodError when trying to get that method outside the scope
  nil
end
method_parameters() click to toggle source
# File lib/power_trace/entry.rb, line 89
def method_parameters
  []
end
value_to_string(value, truncation) click to toggle source
# File lib/power_trace/entry.rb, line 125
def value_to_string(value, truncation)
  case value
  when Array
    value.to_s.truncate(truncation, omission: "...]")
  when Hash
    elements_string = value.map do |key, val|
      value_string = value_to_string(val, truncation)
      "#{key.to_s}: #{value_string}"
    end.join(", ")

    "{#{elements_string}}".truncate(truncation, omission: "...}")
  when nil
    "nil"
  when Symbol
    ":#{value}"
  when String
    "\"#{value.truncate(truncation)}\""
  else
    if defined?(ActiveRecord::Base)
      case value
      when ActiveRecord::Base
        value.inspect.truncate(truncation, omission: "...>")
      when ActiveRecord::Relation
        "#{value}, SQL - (#{value.to_sql})"
      else
        value.to_s.truncate(truncation)
      end
    else
      value.to_s.truncate(truncation)
    end
  end
end