Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Separate profiling and reporting into two classes.
- Loading branch information
1 parent
f48a7b6
commit 97a54fa
Showing
6 changed files
with
203 additions
and
166 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
require 'hirb' | ||
|
||
module Hirb::Helpers::Table::Filters | ||
def to_milliseconds(seconds) | ||
"%.3f ms" % (seconds * 1000) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters