require 'benchmark'

namespace :benchmark do

desc 'Measure TracePoint perf for current Ruby'
task :trace_point do
  ARGV.each { |a| task(a.to_sym { ; }) } # Keep rake from treating these ask task names.

  options = {}.tap do |hash|
    ARGV.each { |arg| hash[arg] = true }
  end

  puts "options: #{options}"

  trace_with_bindings = BenchmarkTraceWithBindings.new(options)

  trace_with_bindings.enable
  puts(Benchmark.measure do
    TraceTest.benchmark_with_locals
  end)
  trace_with_bindings.disable

  puts "counts: #{trace_with_bindings.counts}" if options['counts']
end

end

class TraceTest # :nodoc:

class << self
  def benchmark_with_locals
    foo = false

    (0..20_000).each do |index|
      foo = TraceTest

      change_frame_with_locals(foo, index)
    end
  end

  def change_frame_with_locals(foo, _index)
    foo.tap do |obj|
      bar = 'bar'
      hash = { :foo => obj, :bar => bar } # rubocop:disable Lint/UselessAssignment
    end
  end
end

end

class BenchmarkTraceWithBindings # :nodoc:

attr_reader :frames, :exception_frames, :options, :counts

def initialize(options = {})
  @options = options
  @frames = []
  @exception_frames = []
  @exception_signature = nil
  @counts = init_counts({})
end

def init_counts(counts)
  [:call, :b_call, :c_call, :class].each do |event|
    counts[event] = 0
  end
  counts
end

def enable
  return if options['disable']

  trace_point.enable if defined?(TracePoint)
end

def disable
  return if options['disable']

  trace_point.disable if defined?(TracePoint)
end

def trace_point # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
  return unless defined?(TracePoint)

  @trace_point ||= TracePoint.new(:call, :return, :b_call, :b_return, :c_call,
                                  :c_return, :raise) do |tp|
    next if options['hook_only']

    case tp.event
    when :call, :b_call, :c_call, :class
      @counts[tp.event] += 1 if options['counts']
      frame = options['frame'] ? frame(tp) : {}
      frames.push frame if options['stack']
    when :return, :b_return, :c_return, :end
      frames.pop if options['stack']
    end
  end
end

def frame(trace)
  {
    :binding => trace.binding,
    :defined_class => trace.defined_class,
    :method_id => trace.method_id,
    :path => trace.path,
    :lineno => trace.lineno
  }
end

end