1

I would like to be able to insert some code at the beginning and at the end of methods in my class. I would like to avoid repetition as well.

I found this answer helpful, however it doesn't help with the repetition.

class MyClass
  def initialize
    [:a, :b].each{ |method| add_code(method) }
  end

  def a
    sleep 1
    "returning from a"
  end

  def b
    sleep 1
    "returning from b"
  end

  private

  def elapsed
    start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
    block_value = yield
    finish = Process.clock_gettime(Process::CLOCK_MONOTONIC)
    puts "elapsed: #{finish - start} seconds, block_value: #{block_value}."
    block_value
  end

  def add_code(meth)
    meth = meth.to_sym
    self.singleton_class.send(:alias_method, "old_#{meth}".to_sym, meth)
    self.singleton_class.send(:define_method, meth) do
      elapsed do
        send("old_#{meth}".to_sym)
      end
    end
  end
end

The above does work, but what would be a more elegant solution? I would love to be able to, for example, put attr_add_code at the beginning of the class definition and list the methods I want the code added to, or perhaps even specify that I want it added to all public methods.

Note: The self.singleton_class is just a workaround since I am adding code during the initialisation.

tophan
  • 11
  • 1
  • What do you mean by "repetition"? The title refers to class methods but you only have public instance methods. If would be helpful if you gave an example that shows the method(s) before and after the "addition" of code. I would think one method is sufficient. Perhaps we could figure out what you want to do by studying your code, but that should not be necessary. – Cary Swoveland Jun 08 '19 at 17:46

4 Answers4

2

If by repetition you mean the listing of methods you want to instrument, then you can do something like:

module Measure
  def self.prepended(base)
    method_names = base.instance_methods(false)

    base.instance_eval do
      method_names.each do |method_name|
        alias_method "__#{method_name}_without_timing", method_name
        define_method(method_name) do
          t1 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
          public_send("__#{method_name}_without_timing")
          t2 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
          puts "Method #{method_name} took #{t2 - t1}"
        end
      end
    end
  end
end

class Foo
  def a
    puts "a"
    sleep(1)
  end
  def b
    puts "b"
    sleep(2)
  end
end

Foo.prepend(Measure)

foo = Foo.new
foo.a
foo.b

# => a
# => Method a took 1.0052679998334497
# => b
# => Method b took 2.0026899999938905

Main change is that i use prepend and inside the prepended callback you can find the list of methods defined on the class with instance_methods(false), the falseparameter indicating that ancestors should not be considered.

Pascal
  • 8,464
  • 1
  • 20
  • 31
  • Something along the lines of `alias_method "__#{method_name}_without_timing", method_name` might be slightly more idiomatic in terms of signaling that the original method shouldn't be called, but good solution. You could also do something similar with `method_missing`, but this is probably faster and clearer in the stack at the end of the day. – David Bodow Jun 08 '19 at 21:53
  • @DavidBodow added the `__` prefix. Not sure how you would solve the problem with method missing, since the method `a` is not really missing (would need to rename the method, which seems a bit overkill). – Pascal Jun 09 '19 at 20:12
1

Instead of using method aliasing, which in my opinion is something of the past since the introduction of Module#prepend, we can prepend an anonymous module that has a method for each instance method of the class to be measured. This will cause calling MyClass#a to invoke the method in this anonymous module, which measures the time and simply resorts to super to invoke the actual MyClass#a implementation.

def measure(klass)
  mod = Module.new do
    klass.instance_methods(false).each do |method|
      define_method(method) do |*args, &blk|
        start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
        value = super(*args, &blk)
        finish = Process.clock_gettime(Process::CLOCK_MONOTONIC)
        puts "elapsed: #{finish - start} seconds, value: #{value}."
        value
      end
    end
  end

  klass.prepend(mod)
end

Alternatively, you can use class_eval, which is also faster and allows you to just call super without specifying any arguments to forward all arguments from the method call, which isn't possible with define_method.

def measure(klass)
  mod = Module.new do
    klass.instance_methods(false).each do |method|
      class_eval <<-CODE, __FILE__, __LINE__ + 1
        def #{method}(*)
          start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
          value = super
          finish = Process.clock_gettime(Process::CLOCK_MONOTONIC)
          puts "elapsed: \#{finish - start} seconds, value: \#{value}."
          value
        end
      CODE
    end
  end

  klass.prepend(mod)
end

To use this, simply do:

measure(MyClass)
fphilipe
  • 9,739
  • 1
  • 40
  • 52
  • Ha! This is the exact thing I am half-finished writing up as an answer myself. [I am somewhat passionate about `alias_method` chaining](https://stackoverflow.com/a/4471202/2988). Have an upvote. – Jörg W Mittag Jun 09 '19 at 08:50
0

It looks like you're trying to do some benchmarking. Have you checked out the benchmark library? It's in the standard library.

require 'benchmark'
puts Benchmark.measure { MyClass.new.a }
puts Benchmark.measure { MyClass.new.b }
Murphy
  • 3,827
  • 4
  • 21
  • 35
Aaron Breckenridge
  • 1,723
  • 18
  • 25
  • Thanks, but I'm doing something more complicating than just benchmarking. I didn't include it in my sample code because it's irrelevant. – tophan Jun 08 '19 at 20:55
  • It would be helpful if you gave more details about the broader context and what you're trying to do with the timing information. Logging in Rails and only care about end-to-end timing? Consider a Rack middleware. Looking for a full stack profile? Consider a profiling gem such as https://github.com/ruby-prof/ruby-prof I do like @pascal's `prepend` answer for a direct answer to your question, but I think you'll find that you can get better help if you share the bigger picture of what you're trying to accomplish, which can bring in whole different approaches to the problem – David Bodow Jun 08 '19 at 21:46
0

Another possibility would be to create a wrapper class like so:

class Measure < BasicObject
  def initialize(target)
    @target = target
  end

  def method_missing(name, *args)
    t1 = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
    target.public_send(name, *args)
    t2 = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
    ::Kernel.puts "Method #{name} took #{t2 - t1}"
  end

  def respond_to_missing?(*args)
    target.respond_to?(*args)
  end

  private

  attr_reader :target
end

foo = Measure.new(Foo.new)

foo.a
foo.b
Pascal
  • 8,464
  • 1
  • 20
  • 31