Skip to content

Instantly share code, notes, and snippets.

@jgaskins
Last active March 24, 2019 00:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jgaskins/1dfd19eeb91178575b9c241c80ff3f95 to your computer and use it in GitHub Desktop.
Save jgaskins/1dfd19eeb91178575b9c241c80ff3f95 to your computer and use it in GitHub Desktop.
Trace metrics for methods defined in Ruby code
require 'pp'
require 'set'
module CountThings
COUNTS = Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = 0 } }
TIMINGS = Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = [] } }
TRACED_METHODS = Hash.new { |h, k| h[k] = Set.new }
module_function
def call(klass, method, stack, args)
start = Time.now
result = yield
finish = Time.now
TIMINGS[klass][method] << Invocation.new(
klass,
method,
finish - start,
stack,
args,
)
result
ensure
COUNTS[klass][method] += 1
end
def total_time(klass, method)
TIMINGS[klass][method].sum(&:execution_time)
end
def average_time(klass, method)
total_time(klass, method) / call_count(klass, method)
end
def call_count(klass, method)
COUNTS[klass][method]
end
def median_time(klass, method)
percentile_time(50, klass, method)
end
def percentile_time(percentile, klass, method)
timings = TIMINGS[klass][method]
timings.sort[(timings.count * percentile / 100.0).to_i]
end
def order_by_most_time_spent
TIMINGS
.flat_map do |klass, methods|
methods.map { |method, timings| [[klass, method], timings.sum(&:execution_time)] }
end
.sort_by { |method, total_time| -total_time }
.to_h
end
class Invocation
include Comparable
attr_reader :execution_time
def initialize(klass, method_name, execution_time, call_stack, args)
@klass = klass
@method_name = method_name
@execution_time = execution_time
@call_stack = call_stack
@args = args
end
def <=>(other)
execution_time <=> other.execution_time
end
end
end
class BasicObject
# Enable tracking of method calls. In real code, you might want to until
# after framework code is loaded to execute this.
# It's not foolproof because some classes can override this, but it's
# so uncommon to do this in Ruby that it'll get you most of the way there.
def self.method_added(name)
return if name.to_s.start_with? 'untraced_'
return if instance_methods.include? "untraced_#{name}"
return if ::CountThings::TRACED_METHODS[self].include? name
::CountThings::TRACED_METHODS[self] << name
class_eval do
alias_method "untraced_#{name}", name
define_method name do |*args, &block|
::CountThings.call self.class, name, caller, args do
send "untraced_#{name}", *args, &block
end
end
end
end
end
###################
# All methods defined beyond this point are tracked
class Foo
def hello(world = 'world')
sleep rand(0.0...0.1) # Give it a variance in execution time
"omg #{world}"
end
end
class Bar < Foo
end
# Running these calls in threads because they take too long otherwise
Array
.new(1_000) { Thread.new { Foo.new.hello(:world) } }
.each(&:join)
Foo.new.to_s
Bar.new.hello
pp CountThings::COUNTS
# {Foo=>{:hello=>1000}, Bar=>{:hello=>1}}
pp CountThings.call_count(Foo, :hello)
# 1000
pp CountThings.total_time(Foo, :hello)
# 490.437512
pp CountThings.average_time(Foo, :hello)
# 0.490437512
pp CountThings.median_time(Foo, :hello)
# 0.486538
pp CountThings.percentile_time(99, Foo, :hello)
# 0.990356
pp CountThings.order_by_most_time_spent
# {[Foo, :hello]=>490.437512, [Bar, :hello]=>0.899982}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment