Instance_exec

Mauricio F. wrote:

On Mon, Jul 10, 2006 at 10:44:32PM +0900, [email protected] wrote:
This can create a lot of different symbols, leading to large memory leaks.

This makes the memleak even heavier, going from potentially ~70 bytes to
nearly 300 bytes per call…

This will fail if the class was frozen.

Right. I expected as much. But can you apply yuor fixes to
Proc#to_method?

T.

Mauricio F. wrote:

This will fail if the class was frozen.

Actually, it seems to pass the frozen test.

T.

Okay, I think I’ve improved it some more:

require ‘thread’

class Proc
MethodMutexes = Hash.new do |hash, key|
hash[key] = Mutex.new
end

Creates a local method based on a Proc.

def to_method(name=nil, recv=nil)
unless name
n = 0
n += 1 while recv.respond_to?(name = “_to_method#{n}”)
end
recv ||= eval(“self”, self)
klass = recv.class
MethodMutexes[klass => name].synchronize do
begin
klass.send(:define_method, name, &self)
return recv.method(name)
ensure
klass.send(:remove_method, name) unless name
end
end
end
end

This can create a lot of different symbols, leading to large memory leaks.

  recv ||= eval("self", self)
  klass = recv.class
  MethodMutexes[klass => name].synchronize do
    ============================

This makes the memleak even heavier, going from potentially ~70 bytes to
nearly 300 bytes per call…

I don’t understand this though. isn’t the memory freed when done? Or is
some symbol getting created in the process, or ?

    begin
      klass.send(:define_method, name, &self)
        ==========

This will fail if the class was frozen.

Ah, I see if the klass is frozen. But that will fail regardless won’t
it? You can;t even include a module (ef InstanceExecHelper) in a frozen
class. Right?

Thanks,
T.

I don’t understand this though. isn’t the memory freed when
done? Or is some symbol getting created in the process, or
?

define_method creates a symbol, beneath the surface:

symbols1 = Symbol.all_symbols
Module.new.instance_eval{define_method(“bar”){}}
symbols2 = Symbol.all_symbols
p symbols2-symbols1 # ===> [:bar]

Yes, that’s why InstanceExecHelper must be included in Object
(and you only have to do that once).

You should include the Module before Object gets frozen. Who
wants to freeze Object, anyway?.. ;]

Thread.critical = old

AFAIK, we need to be in critical mode when determining the
method name. As soon as we know it, we can (and should…)
return to old_critical. At least before we call the
block/method.

module Kernel
def instance_exec(*args, &block)

Kernel#instance_exec ? Does that make sense?

About $SAFE: It’s possible to do an instance_eval up to level
5, whereas instance_exec fails on level 4. Challenge?.. ;]

gegroet,
Erik V. - http://www.erikveen.dds.nl/

On Tue, Jul 11, 2006 at 01:21:47AM +0900, [email protected] wrote:

Okay, I think I’ve improved it some more:

[…]

  recv ||= eval("self", self)
  klass = recv.class
  MethodMutexes[klass => name].synchronize do
    ============================

This makes the memleak even heavier, going from potentially ~70 bytes to
nearly 300 bytes per call…

I don’t understand this though. isn’t the memory freed when done? Or is
some symbol getting created in the process, or ?

Two things:

  • you’re using a klass => name hash as the key (instead of [klass,
    name],
    which would take less memory)
  • MethodMutexes grows monotonically since you don’t delete the key =>
    mutex
    associations when you’re done, so you keep the keys and the mutexes,
    which are pretty heavy

In the first version, the name included #{object_id}, so many values
were
possible (as many as different Procs you call to_method on, modulo
object_id
recycling). Different symbols and entries in the MethodMutexes hash
would
accumulate.

    begin
      klass.send(:define_method, name, &self)
        ==========

This will fail if the class was frozen.

Ah, I see if the klass is frozen. But that will fail regardless won’t
it? You can;t even include a module (ef InstanceExecHelper) in a frozen
class. Right?

Yes, that’s why InstanceExecHelper must be included in Object (and you
only
have to do that once).

Here’s an improved (hastily written, not thoroughly tested) version:

class Object
module InstanceExecHelper; end
include InstanceExecHelper
end

class Proc

Creates a local method based on a Proc.

def to_method(name=nil, recv=nil)
recv ||= eval(“self”, self)
do_remove = false
begin
old, Thread.critical = Thread.critical, true
unless name
do_remove = true
n = 0
n += 1 while recv.respond_to?(name = “_to_method#{n}”)
end
me = self
InstanceExecHelper.module_eval{ define_method(name, &me) }
return recv.method(name)
ensure
if do_remove
InstanceExecHelper.module_eval{ remove_method(name) rescue nil }
end
Thread.critical = old
end
end
end

module Kernel
def instance_exec(*args, &block)
block.to_method(nil,self).call(*args)
end
end

class Dummy
def f
:dummy_value
end
end
Dummy.freeze

require ‘test/unit’
class TestInstanceEvalWithArgs < Test::Unit::TestCase
def test_instance_exec
# Create a block that returns the value of an argument and a value
# of a method call to +self+.
block = lambda { |a| [a, f] }

assert_equal [:arg_value, :dummy_value],
  Dummy.new.instance_exec(:arg_value, &block)

end

def test_instance_exec_with_frozen_obj
block = lambda { |a| [a, f] }

obj = Dummy.new
obj.freeze
assert_equal [:arg_value, :dummy_value],
  obj.instance_exec(:arg_value, &block)

end

def test_instance_exec_nested
i = 0
obj = Dummy.new
block = lambda do |arg|
[arg] + instance_exec(1){|a| [f, a] }
end

# the following assertion expanded by the xmp filter automagically 

from:
# obj.instance_exec(:arg_value, &block) #=>
assert_equal([:arg_value, :dummy_value, 1],
obj.instance_exec(:arg_value, &block))
end
end

>> Loaded suite -

>> Started

>> …

>> Finished in 0.00091 seconds.

>>

>> 3 tests, 3 assertions, 0 failures, 0 errors

Mauricio F. wrote:

Here’s an improved (hastily written, not thoroughly tested) version:

Okay, I took your draft and worked on it some more and came up with the
following, but there’s something very strange…

require ‘thread’

class Object
module TemporaryMethods; end
include TemporaryMethods
end

class Proc

Creates a local method based on a Proc.

def to_method(name=nil, recv=nil)
recv ||= eval(“self”, self)
klass = name ? recv.class : TemporaryMethods
begin
old, Thread.critical = Thread.critical, true
unless name
n = 0
n += 1 while klass.method_defined?(name = “_to_method#{n}”)
#n += 1 while recv.respond_to?(name = “_to_method#{n}”)
end
me = self
klass.module_eval{ define_method(name, &me) }
recv.method(name) #if klass == TemporaryMethods
ensure
if klass == TemporaryMethods
klass.module_eval{ remove_method(name) } #rescue nil }
#klass.module_eval{ undef_method(name) } #rescue nil }
end
Thread.critical = old
end
end

end

Notice the two remarks on n+=1… and klass.module_eval…, if you
unremark the first one, the one with recv.respond_to? it doesn’t alwasy
see that the method has been removed – I have to idea why that is
since it certainly has been. If you use undef_method instead of
remove_method, it work again though! That really blows my min b/c I
though undef_method didn;t actually remove the method. So I’m really
confused about that. Here’s the test I use for the Symbols:

This is dependent on other tests! Be aware!

def test_memory
  a = 2
  tproc = proc { |x| x + a }
  100.times {
    tmethod = tproc.to_method
    assert_equal( 3, tmethod.call(1) )
  }
  meths = Symbol.all_symbols.select { |s| s.to_s =~ /^__to_method/

}
assert_equal( 1, meths.size )
end

Also is the ‘resuce nil’ really needed?

T.

P.S. Is there any reason you use module_eval { define_method… rather
then send(:defined_method?..

end

Also is the ‘resuce nil’ really needed?

You can’t remove a method if it isn’t defined. If it’s the
define_method which fails, you won’t see the root cause.

Compare the scripts below. test1.rb doesn’t give you a clue
about the real problem. test2.rb does.

It’s probably better to give a real warning in this rescue
block, instead of returning just nil.

gegroet,
Erik V. - http://www.erikveen.dds.nl/


$ cat test1.rb
begin
Object.define_method(:bar){}
ensure
Object.module_eval{ remove_method(:bar) }
end

$ cat test2.rb
begin
Object.define_method(:bar){}
ensure
Object.module_eval{ remove_method(:bar) } rescue nil
end

$ ruby test1.rb
test1.rb:4:in remove_method': method bar’ not defined in Object
(NameError)
from test1.rb:4
from test1.rb:4

$ ruby test2.rb
test2.rb:2: private method `define_method’ called for Object:Class
(NoMethodError)