atmorell
atmorell

Reputation: 3068

Ruby/Rails: Prepend, append code to all methods

I wrote a small benchmarking Class for testing my code doing development. At the moment I have to add the Class to the beginning and end of every method. Is it posible to prepend, append on the fly, so that I don't have to clutter my code?

class ApplicationController    
    before_filter :init_perf
    after_filter :write_perf_results_to_log!

    def init_perf
      @perf ||= Perf.new
    end

    def write_perf_results_to_log!
      @perf.results
    end
end

class Products < ApplicationsController    
    def foo      
      @perf.log(__methond__.to_s)
      caculation = 5 *4
      @perf.write! 
    end

    def bar      
      @perf.log(__methond__.to_s)
      caculation = 1 / 5
      @perf.write! 
    end
end

This is the Perf class. It is located in the services folder.

class Perf
  def initialize
    @results = []
  end

  def log(note)
    @start = Time.now
    @note = note
  end

  def write!
    if @results.find {|h| h[:note] == @note } # Update :sec method exists in results 
      @results.select { |h| h["note"] == @note; h[":sec"] = (Time.now - @start).round(3) }
    else # Add new Hash to results
      @results << { :note => @note, :sec => (Time.now - @start).round(3) }
    end
  end

  def results
    content = "
    PERFORMANCE STATISTICS!
    "
    @results.each do |r|
      content += r[:note] + "   " + r[:sec].to_s + "
      "
    end
    content += "
    "
    Rails.logger.info content
  end
end

Upvotes: 2

Views: 3064

Answers (4)

Guoliang Cao
Guoliang Cao

Reputation: 507

I'm the author of aspector gem. Thanks to dimuch for mentioning it.

I've come up with a solution using aspector. Here are the high level steps:

  1. Create an aspect as a subclass of Aspector::Base
  2. Inside the aspect, define advices (before/after/around are the primary types of advices)
  3. Apply the aspect on target class (or module/object)

The full code can be found in this gist. Please feel free to let me know if you have questions or the solution doesn't do what you intend to.

class PerfAspect < Aspector::Base
  around options[:action_methods] do |proxy|
    @perf ||= Perf.new
    proxy.call
    @perf.results
  end

  around options[:other_methods], :method_arg => true do |method, proxy, *args, &block|
    @perf.log(method)
    result = proxy.call *args, &block
    @perf.write!
    result
  end
end

action_methods = [:action]
other_methods  = Products.instance_methods(false) - action_methods

PerfAspect.apply(Products, :action_methods => action_methods, :other_methods => other_methods)

Upvotes: 2

Yevgeniy Anfilofyev
Yevgeniy Anfilofyev

Reputation: 4847

There is better solution.

class ApplicationController
    def self.inherited(klass)
        def klass.method_added(name)
            return if @_not_new
            @_not_new = true
            original = "original #{name}"
            alias_method original, name
            define_method(name) do |*args, &block|
                puts "==> called #{name} with args: #{args.inspect}"
                result = send original, *args, &block
                puts "<== result is #{result}"
                result
            end
            @_not_new = false
        end
    end
end

class Product < ApplicationController

    def meth(a1, a2)
        a1 + a2
    end
end

product = Product.new
puts product.meth(2,3)

And the result:

==> called meth with args: [2, 3]
<== result is 5
5

The source & explanation are here: http://pragprog.com/screencasts/v-dtrubyom/the-ruby-object-model-and-metaprogramming. I recommend to spend not a big money to get this course.

Upvotes: 2

Casper
Casper

Reputation: 34308

In general computing terms what you want to do is called code instrumentation. There are several ways to accomplish this, however here's one (crude) example using some metaprogramming:

First define a new method that we will use for injecting our instrumentation code:

class ApplicationController
  def self.instrument_methods(*methods)
    methods.each { |m|
      # Rename original method
      self.send(:alias_method, "#{m}_orig", m)

      # Redefine old method with instrumentation code added
      define_method m do
        puts "Perf log #{m}"
        self.send "#{m}_orig"
        puts "Perf write"
      end
    }
  end
end

How to use it:

class Product < ApplicationController
  def foo
    puts "Foo"
  end

  def bar
    puts "Bar"
  end

  # This has to be called last, once the original methods are defined
  instrument_methods :foo, :bar
end

Then:

p = Product.new
p.foo
p.bar

Will output:

Perf log foo
Foo
Perf write
Perf log bar
Bar
Perf write

Here are some other ways to instrument ruby code and measure performance:

http://ruby-prof.rubyforge.org/
http://www.igvita.com/2009/06/13/profiling-ruby-with-googles-perftools/

Upvotes: 5

dimuch
dimuch

Reputation: 12818

Guess aspector gem can help. It's not well documented but has useful examples.

Upvotes: 1

Related Questions