├── .rspec ├── .travis.yml ├── Gemfile ├── .gitignore ├── Rakefile ├── spec ├── method_profiler_spec.rb ├── spec_helper.rb ├── support │ └── petition.rb └── method_profiler │ ├── profiler_spec.rb │ └── report_spec.rb ├── lib ├── method_profiler │ ├── hirb.rb │ ├── profiler.rb │ └── report.rb └── method_profiler.rb ├── method_profiler.gemspec ├── LICENSE └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | - 2.0.0 5 | - jruby 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | gem 'rake', :group => :test 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | coverage/* 6 | .yardoc/* 7 | doc/* 8 | 9 | *.pem 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rspec/core/rake_task" 3 | 4 | desc "Run specs" 5 | RSpec::Core::RakeTask.new(:spec) 6 | 7 | task :default => :spec 8 | -------------------------------------------------------------------------------- /spec/method_profiler_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe MethodProfiler do 4 | describe ".observe" do 5 | it "returns a new Profiler instance" do 6 | described_class.observe(Petition).should be_an_instance_of described_class::Profiler 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "simplecov" 2 | SimpleCov.add_filter "spec" 3 | SimpleCov.start 4 | 5 | $:.unshift(File.expand_path("../../lib", __FILE__)) 6 | require "method_profiler" 7 | 8 | RSpec.configure do |config| 9 | config.around do |example| 10 | load File.expand_path("../support/petition.rb", __FILE__) 11 | example.call 12 | Object.send(:remove_const, :Petition) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/method_profiler/hirb.rb: -------------------------------------------------------------------------------- 1 | require 'hirb' 2 | 3 | # All methods in this module can be used as filters in Hirb. 4 | # 5 | module Hirb::Helpers::Table::Filters 6 | # Converts seconds to milliseconds. Used to format times in 7 | # the output of {MethodProfiler::Report}. 8 | # 9 | # @param [Float] seconds The duration to convert. 10 | # @return [String] The duration in milliseconds with units displayed. Rounded to 3 decimal places. 11 | def to_milliseconds(seconds) 12 | "%.3f ms" % (seconds * 1000) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /lib/method_profiler.rb: -------------------------------------------------------------------------------- 1 | require 'method_profiler/profiler' 2 | 3 | # {MethodProfiler} collects performance information about the methods 4 | # in your objects and creates reports to help you identify slow methods. 5 | # 6 | module MethodProfiler 7 | # Create a new {MethodProfiler::Profiler} which will observe all method calls 8 | # on the given object. This is a convenience method and has the same effect 9 | # as {MethodProfiler::Profiler#initialize}. 10 | # 11 | # @param [Object] obj The object to observe. 12 | # @return [MethodProfiler::Profiler] A new profiler. 13 | # 14 | def self.observe(obj) 15 | Profiler.new(obj) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /spec/support/petition.rb: -------------------------------------------------------------------------------- 1 | class Petition 2 | def self.hay 3 | end 4 | 5 | def self.guys 6 | "sup" 7 | end 8 | 9 | def foo 10 | end 11 | 12 | def bar 13 | end 14 | 15 | def baz 16 | "blah" 17 | end 18 | 19 | def method_with_implicit_block 20 | yield "implicit" 21 | end 22 | 23 | def method_with_explicit_block(&block) 24 | block.call "explicit" 25 | end 26 | 27 | def method_with_implicit_block_and_args(*args) 28 | yield args 29 | end 30 | 31 | def method_with_explicit_block_and_args(*args, &block) 32 | block.call args 33 | end 34 | 35 | private 36 | 37 | def shh 38 | "secret" 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /method_profiler.gemspec: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | 3 | Gem::Specification.new do |s| 4 | s.name = "method_profiler" 5 | s.version = "2.0.1" 6 | s.authors = ["Jimmy Cuadra"] 7 | s.email = ["jimmy@jimmycuadra.com"] 8 | s.homepage = "https://github.com/change/method_profiler" 9 | s.summary = %q{Find slow methods in your program.} 10 | s.description = %q{MethodProfiler observes your code and generates reports about the methods that were run and how long they took.} 11 | 12 | s.files = `git ls-files`.split("\n") 13 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 14 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 15 | s.require_paths = ["lib"] 16 | 17 | s.add_runtime_dependency "hirb", ">= 0.6.0" 18 | s.add_development_dependency "rspec" 19 | s.add_development_dependency "simplecov" 20 | end 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012 by Jimmy Cuadra 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /spec/method_profiler/profiler_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe MethodProfiler::Profiler do 4 | let!(:profiler) { described_class.new(Petition) } 5 | let(:petition) { Petition.new } 6 | 7 | it "creates wrapper methods for the object's methods" do 8 | petition.should respond_to(:foo) 9 | petition.should respond_to(:foo_with_profiling) 10 | petition.should respond_to(:foo_without_profiling) 11 | petition.should_not respond_to(:foo_with_profiling_with_profiling) 12 | petition.private_methods.should include(:shh_with_profiling) 13 | petition.private_methods.should include(:shh_without_profiling) 14 | end 15 | 16 | it "returns correct values for class methods" do 17 | Petition.guys.should == "sup" 18 | end 19 | 20 | it "returns correct values for instance methods" do 21 | petition.baz.should == "blah" 22 | end 23 | 24 | it "returns correct values for private methods" do 25 | petition.send(:shh).should == "secret" 26 | end 27 | 28 | it "yields to implicit blocks" do 29 | petition.method_with_implicit_block {|v| v }.should == "implicit" 30 | end 31 | 32 | it "calls explicit blocks" do 33 | petition.method_with_explicit_block {|v| v }.should == "explicit" 34 | end 35 | 36 | it "yields to implicit blocks with arguments" do 37 | petition.method_with_implicit_block_and_args(1,2,3) {|v| v }.should == [1,2,3] 38 | end 39 | 40 | it "calls explicit blocks with arguments" do 41 | petition.method_with_explicit_block_and_args(1,2,3) {|v| v }.should == [1,2,3] 42 | end 43 | 44 | describe "#report" do 45 | it "returns a new Report object" do 46 | profiler.report.should be_an_instance_of MethodProfiler::Report 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /lib/method_profiler/profiler.rb: -------------------------------------------------------------------------------- 1 | require 'method_profiler/report' 2 | 3 | require 'benchmark' 4 | 5 | module MethodProfiler 6 | # Observes an object, keeping track of all its method calls and the wall clock 7 | # time spent executing them. 8 | # 9 | class Profiler 10 | # Initializes a new {Profiler}. Wraps all methods in the object and its singleton 11 | # class with profiling code. 12 | # 13 | # @param [Object] obj The object to observe. 14 | # 15 | def initialize(obj) 16 | @obj = obj 17 | @data = Hash.new { |h, k| h[k] = [] } 18 | 19 | wrap_methods_with_profiling 20 | end 21 | 22 | # Generates a report object with all the data collected so far bay the profiler. This report 23 | # can be displayed in various ways. See {Report}. 24 | # 25 | # @return [Report] A new report with all the data the profiler has collected. 26 | # 27 | def report 28 | Report.new(final_data, @obj.name) 29 | end 30 | 31 | private 32 | 33 | def wrap_methods_with_profiling 34 | profiler = self 35 | 36 | [ 37 | { object: @obj.singleton_class, methods: @obj.methods(false), private: false, singleton: true }, 38 | { object: @obj, methods: @obj.instance_methods(false), private: false }, 39 | { object: @obj, methods: @obj.private_instance_methods(false), private: true } 40 | ].each do |group| 41 | group[:object].module_eval do 42 | group[:methods].each do |method| 43 | define_method("#{method}_with_profiling") do |*args, &block| 44 | profiler.send(:profile, method, singleton: group[:singleton]) do 45 | send("#{method}_without_profiling", *args, &block) 46 | end 47 | end 48 | 49 | alias_method "#{method}_without_profiling", method 50 | alias_method method, "#{method}_with_profiling" 51 | 52 | private "#{method}_with_profiling" if group[:private] 53 | end 54 | end 55 | end 56 | end 57 | 58 | def profile(method, options = {}, &block) 59 | method_name = options[:singleton] ? ".#{method}" : "##{method}" 60 | elapsed_time, result = benchmark(block) 61 | @data[method_name] << elapsed_time 62 | result 63 | end 64 | 65 | def final_data 66 | results = [] 67 | 68 | @data.each do |method, records| 69 | total_calls = records.size 70 | total_time = records.reduce(:+) 71 | average = total_time / total_calls 72 | results << { 73 | method: method, 74 | min: records.min, 75 | max: records.max, 76 | average: average, 77 | total_time: total_time, 78 | total_calls: total_calls, 79 | } 80 | end 81 | 82 | results 83 | end 84 | 85 | def benchmark(block_to_benchmark) 86 | result = nil 87 | elapsed_time = Benchmark.realtime { result = block_to_benchmark.call } 88 | return elapsed_time, result 89 | end 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /spec/method_profiler/report_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe MethodProfiler::Report do 4 | before do 5 | profiler = MethodProfiler::Profiler.new(Petition) 6 | 7 | # Fake the timings for testing purposes 8 | profiler.stub(:benchmark) do |block_to_benchmark| 9 | result = block_to_benchmark.call 10 | rand 11 | end 12 | 13 | petition = Petition.new 14 | 15 | [:hay, :guys].each { |m| petition.class.send(m) } 16 | [:foo, :bar, :baz, :shh].each { |m| petition.send(m) } 17 | 18 | @report = profiler.report 19 | end 20 | 21 | describe "#to_a" do 22 | it "returns an array of records" do 23 | results = @report.to_a 24 | results.should be_an Array 25 | results.size.should == 6 26 | end 27 | 28 | it "returns results sorted by average time (descending) by default" do 29 | results = @report.to_a 30 | average_times = results.map { |r| r[:average] } 31 | average_times.should == average_times.sort.reverse 32 | end 33 | end 34 | 35 | describe "#sort_by" do 36 | it "sets the result key to order results by" do 37 | results = @report.sort_by(:min).to_a 38 | min_times = results.map { |r| r[:min] } 39 | min_times.should == min_times.sort.reverse 40 | end 41 | 42 | it "defaults to average if an invalid sort type is passed" do 43 | results = @report.sort_by(:foo).to_a 44 | average_times = results.map { |r| r[:average] } 45 | average_times.should == average_times.sort.reverse 46 | end 47 | end 48 | 49 | describe "#order" do 50 | it "sets the sort direction" do 51 | results = @report.order(:ascending).to_a 52 | average_times = results.map { |r| r[:average] } 53 | average_times.should == average_times.sort 54 | end 55 | 56 | it "defaults to descending if an invalid direction is passed" do 57 | results = @report.order(:foo).to_a 58 | average_times = results.map { |r| r[:average] } 59 | average_times.should == average_times.sort.reverse 60 | end 61 | 62 | it "allows :asc and :desc aliases" do 63 | results = @report.order(:asc).to_a 64 | average_times = results.map { |r| r[:average] } 65 | average_times.should == average_times.sort 66 | 67 | results = @report.order(:desc).to_a 68 | average_times = results.map { |r| r[:average] } 69 | average_times.should == average_times.sort.reverse 70 | end 71 | end 72 | 73 | describe "#to_s" do 74 | it "displays the name of the profiled object" do 75 | @report.to_s.should include("MethodProfiler results for: Petition") 76 | end 77 | 78 | it "outputs one line for each method that was called" do 79 | output = @report.to_s 80 | 81 | output.should be_a String 82 | output.scan(/\.hay/).size.should == 1 83 | output.scan(/\.guys/).size.should == 1 84 | output.scan(/#foo/).size.should == 1 85 | output.scan(/#bar/).size.should == 1 86 | output.scan(/#baz/).size.should == 1 87 | output.scan(/#shh/).size.should == 1 88 | end 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /lib/method_profiler/report.rb: -------------------------------------------------------------------------------- 1 | require 'hirb' 2 | require 'method_profiler/hirb' 3 | 4 | module MethodProfiler 5 | # Sorts and displays data collected by a {Profiler}. 6 | # 7 | class Report 8 | # Report headers 9 | HEADERS = { 10 | method: "Method", 11 | min: "Min Time", 12 | max: "Max Time", 13 | average: "Average Time", 14 | total_time: "Total Time", 15 | total_calls: "Total Calls", 16 | } 17 | 18 | # Fields that can be passed to {#sort_by}. 19 | FIELDS = HEADERS.keys 20 | 21 | # Directions that can be passed to {#order}. 22 | DIRECTIONS = [:asc, :ascending, :desc, :descending] 23 | 24 | # Initializes a new {Report}. Used to sort and display data collected by a {Profiler}. 25 | # 26 | # @param [Array] data Data collected by a {Profiler}. 27 | # @param [String] name The name of the object that was profiled. 28 | # 29 | def initialize(data, name) 30 | @data = data 31 | @name = name 32 | @sort_by = :average 33 | @order = :descending 34 | end 35 | 36 | # Sorts the report by the given field. Defaults to `:average`. Chainable with {#order}. 37 | # 38 | # @param [Symbol, String] field Any field from {FIELDS} to sort by. 39 | # @return [Report] The {Report} object, suitable for chaining or display. 40 | # 41 | def sort_by(field) 42 | field = field.to_sym 43 | field = :average unless FIELDS.include?(field) 44 | @sort_by = field 45 | self 46 | end 47 | 48 | # Changes the direction of the sort. Defaults to `:descending`. Chainable with {#sort_by}. 49 | # 50 | # @param [Symbol, String] direction Any direction from {DIRECTIONS} to direct the sort. 51 | # @return [Report] The {Report} object, suitable for chaining or display. 52 | # 53 | def order(direction) 54 | direction = direction.to_sym 55 | direction = :descending unless DIRECTIONS.include?(direction) 56 | direction = :descending if direction == :desc 57 | direction = :ascending if direction == :asc 58 | @order = direction 59 | self 60 | end 61 | 62 | # Sorts the data by the currently set criteria and returns an array of profiling results. 63 | # 64 | # @return [Array] An array of profiling results. 65 | # 66 | def to_a 67 | if @order == :ascending 68 | @data.sort { |a, b| a[@sort_by] <=> b[@sort_by] } 69 | else 70 | @data.sort { |a, b| b[@sort_by] <=> a[@sort_by] } 71 | end 72 | end 73 | 74 | # Sorts the data by the currently set criteria and returns a pretty printed table as a string. 75 | # 76 | # @return [String] A table of profiling results. 77 | # 78 | def to_s 79 | [ 80 | "MethodProfiler results for: #{@name}", 81 | Hirb::Helpers::Table.render( 82 | to_a, 83 | headers: HEADERS.dup, 84 | fields: FIELDS.dup, 85 | filters: { 86 | min: :to_milliseconds, 87 | max: :to_milliseconds, 88 | average: :to_milliseconds, 89 | total_time: :to_milliseconds, 90 | }, 91 | description: false 92 | ) 93 | ].join("\n") 94 | end 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://secure.travis-ci.org/change/method_profiler.png)](http://travis-ci.org/change/method_profiler) [![Code Climate](https://codeclimate.com/github/change/method_profiler.png)](https://codeclimate.com/github/change/method_profiler) [![endorse](http://api.coderwall.com/jimmycuadra/endorsecount.png)](http://coderwall.com/jimmycuadra) 2 | 3 | # MethodProfiler 4 | 5 | **MethodProfiler** collects performance information about the methods in your objects and creates reports to help you identify slow methods. The collected data can be sorted in various ways, converted into an array, or pretty printed as a table. 6 | 7 | ## Basic usage 8 | 9 | Create a new profiler by passing the object you want to profile to `MethodProfiler.observe`. All future class and instance methods called on your object will be recorded by the profiler. To see the results of the profiling as a table, simply print out the report returned by `#report` on the profiler object. 10 | 11 | ```ruby 12 | profiler = MethodProfiler.observe(MyClass) 13 | 14 | MyClass.labore_voluptatum 15 | MyClass.labore_voluptatum 16 | 17 | my_obj = MyClass.new 18 | 19 | my_obj.accusamus_est 20 | my_obj.accusamus_est 21 | my_obj.accusamus_est 22 | 23 | puts profiler.report 24 | ``` 25 | 26 | The resulting chart includes each method, the minimum time it took to run, the maximum time, the average across all calls, and the total number of times it was called. Class methods are prefixed by a `.` and instance methods are prefixed with a `#`. 27 | 28 | ``` 29 | MethodProfiler results for: MyClass 30 | +-----------------------+-----------+------------+--------------+------------+-------------+ 31 | | Method | Min Time | Max Time | Average Time | Total Time | Total Calls | 32 | +-----------------------+-----------+------------+--------------+------------+-------------+ 33 | | #accusamus_est | 28.722 ms | 393.649 ms | 150.543 ms | 451.628 ms | 3 | 34 | | #autem_iste! | 26.220 ms | 387.026 ms | 146.644 ms | 439.933 ms | 3 | 35 | | #distinctio_eos | 26.095 ms | 386.903 ms | 146.520 ms | 439.559 ms | 3 | 36 | | #laborum_fugit | 14.887 ms | 351.369 ms | 127.564 ms | 382.692 ms | 3 | 37 | | #suscipit_architecto | 9.876 ms | 269.339 ms | 96.440 ms | 289.319 ms | 3 | 38 | | #et_fugit | 0.005 ms | 63.101 ms | 10.704 ms | 64.225 ms | 6 | 39 | | #porro_rerum | 2.970 ms | 15.137 ms | 7.126 ms | 21.378 ms | 3 | 40 | | #provident_molestiae | 0.097 ms | 17.860 ms | 1.134 ms | 27.225 ms | 24 | 41 | | #nisi_inventore | 0.098 ms | 15.076 ms | 1.044 ms | 54.272 ms | 52 | 42 | | #quis_temporibus | 0.004 ms | 11.908 ms | 0.643 ms | 15.430 ms | 24 | 43 | | .labore_voluptatum | 0.440 ms | 0.470 ms | 0.455 ms | 0.910 ms | 2 | 44 | | #quia_est | 0.004 ms | 11.133 ms | 0.453 ms | 47.092 ms | 104 | 45 | | #ut_reiciendis | 0.004 ms | 5.626 ms | 0.346 ms | 8.302 ms | 24 | 46 | | #sint_quasi | 0.062 ms | 2.152 ms | 0.188 ms | 4.504 ms | 24 | 47 | | #sed_at | 0.065 ms | 0.150 ms | 0.085 ms | 2.034 ms | 24 | 48 | | #repellendus_suscipit | 0.051 ms | 0.122 ms | 0.070 ms | 1.684 ms | 24 | 49 | | .quas_nesciunt | 0.058 ms | 0.124 ms | 0.062 ms | 4.303 ms | 69 | 50 | | #iure_quis | 0.021 ms | 0.025 ms | 0.023 ms | 0.069 ms | 3 | 51 | | #dicta_ipsam | 0.006 ms | 0.266 ms | 0.017 ms | 0.798 ms | 48 | 52 | | #perspiciatis_aut | 0.004 ms | 0.068 ms | 0.013 ms | 0.314 ms | 24 | 53 | | .aperiam_laborum | 0.005 ms | 0.015 ms | 0.006 ms | 0.438 ms | 69 | 54 | | #voluptas_ratione | 0.005 ms | 0.007 ms | 0.006 ms | 0.018 ms | 3 | 55 | | #ex_voluptas | 0.004 ms | 0.010 ms | 0.005 ms | 0.212 ms | 41 | 56 | +-----------------------+-----------+------------+--------------+------------+-------------+ 57 | ``` 58 | 59 | ## Reporting 60 | 61 | `MethodProfiler::Profiler#report` actually returns a report object which can be used to sort and display the data in various ways. A report has chainable `#sort_by` and `#order` methods to control the sorting of the report when it is ultimately displayed. The report can be turned into an array by calling `#to_a` and the table shown above by calling `#to_s`. 62 | 63 | *Example of sorting by the number of total calls, ascending:* 64 | 65 | ```ruby 66 | puts profiler.report.sort_by(:total_calls).order(:ascending) 67 | ``` 68 | 69 | `#sort_by` accepts a symbol or string with the name of any of the columns in the table: `:method`, `:min`, `:max`, `:average`, `:total_time`, or `:total_calls`. 70 | 71 | `#order` accepts a symbol or string of `:ascending` or `:descending`. These can also be abbreviated with `:asc` and `:desc`. 72 | 73 | ## Documentation 74 | 75 | The public API is fully documented using [YARD](http://yardoc.org/) and can be viewed on [RubyDoc.info](http://rubydoc.info/github/change/method_profiler/frames). 76 | 77 | ## Tests 78 | 79 | All code is tested with [RSpec](https://github.com/rspec/rspec). To run the specs, clone the repository, install the dependencies with `bundle install`, and then run `rake`. 80 | 81 | ## Issues 82 | 83 | If you have any problems or suggestions for the project, please open a GitHub issue. 84 | 85 | ## License 86 | 87 | MethodProfiler is available under the included MIT license. 88 | 89 | ## Acknowledgements 90 | 91 | Thank you to [Change.org](http://www.change.org/) for sponsoring the project and to my coworker [Alain Bloch](https://github.com/alainbloch) for the inspiration. 92 | --------------------------------------------------------------------------------