Skip to content

Commit

Permalink
Separate profiling and reporting into two classes.
Browse files Browse the repository at this point in the history
  • Loading branch information
jimmycuadra committed Mar 4, 2012
1 parent f48a7b6 commit 97a54fa
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 166 deletions.
143 changes: 5 additions & 138 deletions lib/method_profiler.rb
@@ -1,142 +1,9 @@
require 'benchmark'
require 'hirb'
require 'method_profiler/profiler'

class MethodProfiler
attr_reader :observed_singleton_methods, :observed_instance_methods, :data
module MethodProfiler
extend self

def initialize(obj)
@obj = obj
@observed_singleton_methods = find_object_methods(obj.singleton_class, true)
@observed_instance_methods = find_object_methods(obj)
initialize_data
wrap_methods_with_profiling
end

def profile(method, singleton = false, &block)
method_name = singleton ? ".#{method}" : "##{method}"
result = nil
benchmark = Benchmark.measure { result = block.call }
elapsed_time = benchmark.to_s.match(/\(\s*([^\)]+)\)/)[1].to_f
@data[method_name] << elapsed_time
result
end

def report(options = {})
normalize_options!(options)
[
"MethodProfiler results for: #{@obj}",
Hirb::Helpers::Table.render(
final_data(options),
headers: {
method: "Method",
min: "Min Time",
max: "Max Time",
average: "Average Time",
total_calls: "Total Calls"
},
fields: [:method, :min, :max, :average, :total_calls],
description: false
)
].join("\n")
end

def reset!
initialize_data
@final_data = nil
end

private

def initialize_data
@data = Hash.new { |h, k| h[k] = [] }
end

def find_object_methods(obj, singleton = false)
obj.instance_methods - obj.ancestors.map do |a|
if a == obj
[]
else
if singleton
a.singleton_class.instance_methods
else
a.instance_methods
end
end
end.flatten
end

def wrap_methods_with_profiling
profiler = self
osm = observed_singleton_methods
oim = observed_instance_methods

@obj.singleton_class.module_eval do
osm.each do |method|
define_method("#{method}_with_profiling") do |*args|
profiler.profile(method, true) { send("#{method}_without_profiling", *args) }
end

alias_method "#{method}_without_profiling", method
alias_method method, "#{method}_with_profiling"
end
end

@obj.module_eval do
oim.each do |method|
define_method("#{method}_with_profiling") do |*args|
profiler.profile(method) { send("#{method}_without_profiling", *args) }
end

alias_method "#{method}_without_profiling", method
alias_method method, "#{method}_with_profiling"
end
end
end

def normalize_options!(options)
options[:sort_by] ||= :average

options[:order] ||= if options[:sort_by] == :method
:ascending
else
:descending
end

options[:order] = :ascending if options[:order] == :asc
options[:order] = :descending if options[:order] == :desc

options
end

def final_data(options)
final_data = []

data.each do |method, records|
total_calls = records.size
average = records.reduce(:+) / total_calls
final_data << {
method: method,
min: records.min,
max: records.max,
average: average,
total_calls: total_calls
}
end

if options[:order] == :ascending
final_data.sort! { |a, b| a[options[:sort_by]] <=> b[options[:sort_by]] }
else
final_data.sort! { |a, b| b[options[:sort_by]] <=> a[options[:sort_by]] }
end

final_data.each do |record|
[:min, :max, :average].each { |k| record[k] = to_ms(record[k]) }
end

final_data
end

def to_ms(seconds)
"%.3f ms" % (seconds * 1000)
def observe(obj)
Profiler.new(obj)
end
end
7 changes: 7 additions & 0 deletions lib/method_profiler/hirb.rb
@@ -0,0 +1,7 @@
require 'hirb'

module Hirb::Helpers::Table::Filters
def to_milliseconds(seconds)
"%.3f ms" % (seconds * 1000)
end
end
98 changes: 98 additions & 0 deletions lib/method_profiler/profiler.rb
@@ -0,0 +1,98 @@
require 'method_profiler/report'

require 'benchmark'

module MethodProfiler
class Profiler
attr_reader :observed_singleton_methods, :observed_instance_methods, :data

def initialize(obj)
@obj = obj
@observed_singleton_methods = find_object_methods(obj.singleton_class, true)
@observed_instance_methods = find_object_methods(obj)

reset!
wrap_methods_with_profiling
end

def profile(method, singleton = false, &block)
method_name = singleton ? ".#{method}" : "##{method}"
result = nil
benchmark = Benchmark.measure { result = block.call }
elapsed_time = benchmark.to_s.match(/\(\s*([^\)]+)\)/)[1].to_f
@data[method_name] << elapsed_time
result
end

def report
Report.new(final_data)
end

def reset!
@data = Hash.new { |h, k| h[k] = [] }
end

private

def find_object_methods(obj, singleton = false)
obj.instance_methods - obj.ancestors.map do |a|
if a == obj
[]
else
if singleton
a.singleton_class.instance_methods
else
a.instance_methods
end
end
end.flatten
end

def wrap_methods_with_profiling
profiler = self
osm = observed_singleton_methods
oim = observed_instance_methods

@obj.singleton_class.module_eval do
osm.each do |method|
define_method("#{method}_with_profiling") do |*args|
profiler.profile(method, true) { send("#{method}_without_profiling", *args) }
end

alias_method "#{method}_without_profiling", method
alias_method method, "#{method}_with_profiling"
end
end

@obj.module_eval do
oim.each do |method|
define_method("#{method}_with_profiling") do |*args|
profiler.profile(method) { send("#{method}_without_profiling", *args) }
end

alias_method "#{method}_without_profiling", method
alias_method method, "#{method}_with_profiling"
end
end
end

def final_data(options)
results = []

data.each do |method, records|
total_calls = records.size
average = records.reduce(:+) / total_calls
results << {
method: method,
min: records.min,
max: records.max,
average: average,
total_calls: total_calls
}
end

results
end

end
end
62 changes: 62 additions & 0 deletions lib/method_profiler/report.rb
@@ -0,0 +1,62 @@
require 'hirb'
require 'method_profiler/hirb'

module MethodProfiler
class Report
FIELDS = [:method, :min, :max, :average, :total_calls]
DIRECTIONS = [:asc, :ascending, :desc, :descending]

def initialize(data)
@data = data
@sort_by = :average
@order = :descending
end

def sort_by(field = nil)
field = :average unless FIELDS.include?(field)

@sort_by = field
end

def order(direction = nil)
direction = :descending unless DIRECTIONS.include?(direction)
direction = :descending if direction == :desc
direction = :ascending if direction == :asc

@order = direction
end

def to_s
[
"MethodProfiler results for: #{@obj}",
Hirb::Helpers::Table.render(
sorted_data,
headers: {
method: "Method",
min: "Min Time",
max: "Max Time",
average: "Average Time",
total_calls: "Total Calls"
},
fields: [:method, :min, :max, :average, :total_calls],
filter_classes: {
min: :to_milliseconds,
max: :to_milliseconds,
average: :to_milliseconds
},
description: false
)
].join("\n")
end

private

def sorted_data
if @order == :ascending
@data.sort { |a, b| a[@sort_by] <=> b[@sort_by] }
else
@data.sort { |a, b| b[@sort_by] <=> a[@sort_by] }
end
end
end
end
29 changes: 29 additions & 0 deletions spec/method_profiler/report_spec.rb
@@ -0,0 +1,29 @@
require 'spec_helper'

describe MethodProfiler::Report do
xit "outputs a string of report data" do
profiler.report.should be_an_instance_of(String)
end

xit "outputs one line for each method that was called" do
@petition.class.hay
@petition.class.guys
@petition.foo
@petition.bar
@petition.baz
profiler.report.scan(/.hay/).size.should == 1
profiler.report.scan(/.guys/).size.should == 1
profiler.report.scan(/#foo/).size.should == 1
profiler.report.scan(/#bar/).size.should == 1
profiler.report.scan(/#baz/).size.should == 1
end

xit "combines multiple calls to the same method into one line" do
@petition.class.hay
@petition.class.hay
@petition.foo
@petition.foo
profiler.report.scan(/.hay/).size.should == 1
profiler.report.scan(/#foo/).size.should == 1
end
end
30 changes: 2 additions & 28 deletions spec/method_profiler_spec.rb
Expand Up @@ -6,10 +6,6 @@
before { @petition = Petition.new }
after { profiler.reset! }

it "can be instantiated with an object to observe" do
profiler.should be_true
end

it "finds all the object's instance methods" do
profiler.observed_singleton_methods.sort.should == [:hay, :guys].sort
profiler.observed_instance_methods.sort.should == [:foo, :bar, :baz].sort
Expand Down Expand Up @@ -40,30 +36,8 @@
end

describe "#report" do
it "outputs a string of report data" do
profiler.report.should be_an_instance_of(String)
end

it "outputs one line for each method that was called" do
@petition.class.hay
@petition.class.guys
@petition.foo
@petition.bar
@petition.baz
profiler.report.scan(/.hay/).size.should == 1
profiler.report.scan(/.guys/).size.should == 1
profiler.report.scan(/#foo/).size.should == 1
profiler.report.scan(/#bar/).size.should == 1
profiler.report.scan(/#baz/).size.should == 1
end

it "combines multiple calls to the same method into one line" do
@petition.class.hay
@petition.class.hay
@petition.foo
@petition.foo
profiler.report.scan(/.hay/).size.should == 1
profiler.report.scan(/#foo/).size.should == 1
it "returns a new Report object" do
profiler.report.should be_an_instance_of MethodProfiler::Report
end
end
end

0 comments on commit 97a54fa

Please sign in to comment.