AS::Callbacks remove useless code, improve performance #6351

Merged
merged 1 commit into from May 17, 2012

Projects

None yet

6 participants

@bogdan
Contributor
bogdan commented May 16, 2012

This patch is yet another small step to make Callbacks a library of my dream.

Benchmark:
https://gist.github.com/2710476

Running benchmark with current working tree
Checkout HEAD^
Running benchmark with HEAD^
Checkout to previous HEAD again

                    user     system      total        real
-----------------------------------------------set_callback
After patch:    0.010000   0.000000   0.010000 (  0.014398)
Before patch:   0.030000   0.000000   0.030000 (  0.031771)

-------------------------------------------define_callbacks
After patch:    0.020000   0.000000   0.020000 (  0.011218)
Before patch:   0.010000   0.000000   0.010000 (  0.011084)

----------------------------------------------run_callbacks
After patch:    0.000000   0.000000   0.000000 (  0.003216)
Before patch:   0.000000   0.000000   0.000000 (  0.003345)

----------------------------------------------skip_callback
After patch:    0.010000   0.000000   0.010000 (  0.012708)
Before patch:   0.020000   0.000000   0.020000 (  0.017077)
@jeremy
Member
jeremy commented May 16, 2012

What was the point of undef_method on the runner method? Is it safe to remove?

another small step to make Callbacks a library of my dream.

❤️❤️❤️

@bogdan
Contributor
bogdan commented May 16, 2012

I supposed that no one will ask :), but you did so..
Warning: understanding a story below may seriously drain your today stamina.

Dynamically generated method acts as cache for callback chain code.
Previously this cache was generated on per class basics. So every class had it's own runner method:

-        "_run__#{self.name.hash.abs}__#{kind}__callbacks"
#                        ^ class name

Every time we update callbacks - a cache method should be undefined - what __reset_runner was doing.

(I can assume that this use case is pretty esoteric. This method is only generated after first run of callbacks. So undefine runner can only be valuable if someone changes callbacks after initializaton process finishes somewhere later).

But that is a past.

Now it's generated on per chain basis.

+        name = "_run_callbacks_#{chain.object_id}"

At first it means that when different classes has same chain object - they will share runner method as well.
At second it means that do not need to flush a cache - as when callback chain gets updated it's object_id changes and new callback method will be generated.(which is still pretty esoteric but possible)

@josevalim
Member

So the trade-off here is that we are sharing callback chains between child and parent (good) at the cost that callbacks dynamically updated will be recompiled under a new method without expiring the previous one?

This seems risky, it means that someone doing something crazy (adding and/or removing callbacks in a request lifecycle) will now have a memory leak. Not sure if we should support these cases, but the hole will be there.

@bogdan
Contributor
bogdan commented May 17, 2012

You will get memory leak if you update callbacks in runtime in any case with or without a patch

def create
  User.after_save, if: "some_madness" { puts 'saved' }  
end

This will cause continuous add of conditions into callbacks options hash.

But, the crazy case:

def update
  u = User.first
  u.singleton_class.after_save, if: "some_madness" { puts 'saved' }  
end

(I remember someone mentioned this use case once)
Won't cause memory leak because singleton class will be GCed with User object in both cases as well.

So, that memory leak exists right now. This patch is maybe making leak more but not 10 times more.

Alternatively we can use chain.hash.abs instead of chain.object_id to identify callback runner method name.
This will make less runners to be generated. What do you think?

@josevalim
Member

Yeah, since the correct is to modify the singleton class (you won't be thread safe if you modify the class), I am assuming this fine. /cc @jeremy can we merge?

@jeremy
Member
jeremy commented May 17, 2012

+1 to merge. Thank you for expending your stamina @bogdan ;)

@josevalim josevalim merged commit ad8b0a4 into rails:master May 17, 2012
@rafaelfranca
Member
@spastorino
Contributor

Never though object_id could be negative. We should do abs in there.

Member

@bogdan wouldn't it be better to tr '-' into '_' ? I know that's highly unlikely, but abs could give the same object_id as other object. I don't know what are the chances, though, maybe that's something that we can ignore.

Member

Yep when I first saw this I thought the same thing @drogus is mentioning. @bogdan can you provide a PR to fix this? thanks :)

Contributor
Member

I doubt that it will change performance in any meaningful way:

>> Benchmark.measure { 1_000_000.times { "_run__#{no}___callbacks".tr("-", "_") } }.to_s
=> "  1.310000   0.000000   1.310000 (  1.324470)\n"
>> Benchmark.measure { 1_000_000.times { "_run__#{no.abs}___callbacks" } }.to_s
=> "  0.690000   0.000000   0.690000 (  0.700540)\n"

That said, if that's the way this code worked, I guess it can stay with abs.

Member

Yeah, performance should not matter in this case because this is run just once per class in the app lifecycle. We should just have a patch and fix the build asap.

Contributor

@josevalim Unfortunatelly this is not true. Runner method name is build every time #run_callbacks is called. Performance matter here.

Member
Contributor

Good idea. But if cache will be done in instance variable - this will require to flush the cache in initialize_dup or thing like this so that dup object won't share runner method name

Member

@bogdan this commit make the Active Model suite to brake sometimes. Reverting this commit all the tests pass.

I used this script to run the suite many times.

With this commit: 50 times, 3 failures
Without this commit: 100 times, 0 failures

Could you investigate?

Contributor

Sure,

Can you share some additional information with me:

  • a failure message
  • what are your thoughts on the reason
  • how did you detect this issue?

Thanks for detecting this. Can't even imagine how hard it could be to detect.

Member

These are some examples of broken builds:

  1. http://travis-ci.org/#!/rails/rails/jobs/1498992
  2. http://travis-ci.org/#!/rails/rails/jobs/1496948
  3. http://travis-ci.org/#!/rails/rails/jobs/1489985

These errors started on the same day that we merged this commit, but we don't started to investigate, so I tried to revert this commit today because I thought it was the only reason to make the validations tests break, because we don't changed anything related with validations.

I think that we are getting callbacks name collision, and one of reason to this thought is this failure:

[/home/vagrant/builds/rails/rails/activemodel/lib/active_model/errors.rb:360]:
unexpected invocation: #<ActiveModel::Errors:0x9cbc5c4>.generate_message(:title, :accepted, {})
unsatisfied expectations:
- expected exactly once, not yet invoked: #<ActiveModel::Errors:0x9cbc5c4>.generate_message(:title, :less_than, {:value => 1, :count => 0})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment