Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve memory usage #820

Merged

Conversation

seanlinsley
Copy link
Contributor

@seanlinsley seanlinsley commented Aug 2, 2017

Proof-of-concept for #819

These changes decreased memory usage by 96% 🚀 (1.3 MB to 58 KB)

Total allocated: 59848 bytes (1164 objects)
Total retained:  712 bytes (14 objects)

allocated memory by location
-----------------------------------
      8048  ransack/lib/ransack/adapters/active_record/ransack/context.rb:11
      7056  ransack/lib/ransack/adapters/active_record/context.rb:55
      4080  ransack/lib/ransack/adapters/active_record/context.rb:49
      3024  ransack/lib/ransack/adapters/active_record/context.rb:50
      2872  ransack/lib/ransack/adapters/active_record/context.rb:254
      2680  ransack/lib/ransack/adapters/active_record/context.rb:268
      2680  ransack/lib/ransack/adapters/active_record/context.rb:271

allocated objects by location
-----------------------------------
       102  ransack/lib/ransack/adapters/active_record/context.rb:49
        68  ransack/lib/ransack/adapters/active_record/context.rb:254
        67  ransack/lib/ransack/adapters/active_record/context.rb:268
        67  ransack/lib/ransack/adapters/active_record/context.rb:271
        67  ransack/lib/ransack/adapters/active_record/context.rb:272
        67  ransack/lib/ransack/adapters/active_record/context.rb:278
        67  ransack/lib/ransack/adapters/active_record/context.rb:284

@@ -45,7 +45,7 @@ def ransackable_attributes(auth_object = nil)
# For overriding with a whitelist array of strings.
#
def ransackable_associations(auth_object = nil)
reflect_on_all_associations.map { |a| a.name.to_s }
@ransackable_associations ||= reflect_on_all_associations.map { |a| a.name.to_s }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You'll want to be careful about memoization in these -- that's why they weren't to begin with. Technically, new values could be added by code at runtime -- they would need to invalidate the cache.

Copy link
Contributor Author

@seanlinsley seanlinsley Aug 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I hesitated to make this change. This PR isn't meant to be mergeable in its current state, I just went through and applied optimizations to the code that the report highlighted.

Now that we have this proof-of-concept, we can discuss which changes make sense.

Copy link
Contributor

@avit avit Aug 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Possibly:

model_associations = reflect_on_all_associations
if @ransackable_associations && @ransackable_associations.size == model_associations.size
  @ransackable_associations
else
  @ransackable_associations = model_associations.map { |a| a.name.to_s }
end

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, new values could be added by code at runtime -- they would need to invalidate the cache.

Depending on your definition of "runtime", PaperTrail does this (adds associations to your models).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It appears that we can use the to_prepare Rails callback to expire the cache, though that would require that we do one of these:

  • instead of using instance variables, cache the values in a global object
  • cache the values in instance variables, but via a module that would hook into to_prepare

Copy link
Contributor Author

@seanlinsley seanlinsley Dec 29, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't look like we have tests to verify that Rails code reloading works with Ransack. IIRC ActiveAdmin has tests of that type, but they require a lot of setup code.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually to_prepare is unnecessary since these methods are defined on the models themselves. In the Paper Trail case, adding has_paper_trail to the model would unload & reload the model, so ransackable_associations is no longer cached.

AFAICT there aren't any scenarios where this could cause a bug.


@default_table = Arel::Table.new(
@base.table_name, as: @base.aliased_table_name, type_caster: self
)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This variable didn't seem to be used anywhere and it represented a lot of memory usage, so I removed it.

@bind_pairs ||= {}

@bind_pairs[key] ||= begin
parent, attr_name = get_parent_and_attribute_name(key.to_s)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear to me why this would help (other than not initializing a hash until a bind pair is actually needed), but it represented 18 KB, while the line above it represented 30 KB.

end
end

self.predicates = PredicateCollection.new
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This optimization makes it so that we don't have to re-sort the predicate array numerous times, and so that we don't have to re-allocate strings for checking end_with? in predicate.rb.

This change has the biggest impact, saving 806 KB (4 of the top 5 offenders)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is, this code assumes that no object will modify the predicates hash in-place.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also assumes that the same predicate key is never set twice.

@@ -135,7 +135,7 @@ def ransackable_association?(str, klass)
end

def ransackable_scope?(str, klass)
klass.ransackable_scopes(auth_object).any? { |s| s.to_s == str }
klass.ransackable_scopes(auth_object).any? { |s| s.to_sym == str.to_sym }
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can assume that most people use symbols instead of strings, so this prevents 13 KB of string allocations without sacrificing backward compatibility.

Copy link
Contributor

@avit avit Aug 3, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on the ruby version, this could cause a memory leak from different string inputs being added to the symbol table.

Search criteria can come from serialized JSON or params, can we really assume most inputs are symbols?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's Ruby 1.8 and below if I recall correctly, and Ransack currently requires 1.9 or above. (However note that on another line I'm using Ruby 2 keyword arguments).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to say it was in the 2.1 series, too, but I could be wrong.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, Symbol GC was added in ruby 2.2. This may be OK now, depending on what Ransack wants to support.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ref #857 CC @seanfcarroll

@colesteaks
Copy link

@seanlinsley any updates here?

@seanlinsley
Copy link
Contributor Author

I haven't focused on this in quite a while. If you're interested in contributing @colesteaks, I'd be happy to pair with you on this sometime.

@seanlinsley seanlinsley changed the title WIP: Improve memory usage Improve memory usage Dec 29, 2017
@seanlinsley
Copy link
Contributor Author

I'm going to start using this in my app to see if any bugs appear.

@seanlinsley seanlinsley force-pushed the improve-memory-usage branch from ebd159f to f570a40 Compare January 1, 2018 02:31
@scarroll32
Copy link
Member

Happy to merge this into master if it's got consensus. Thoughts?

@seanlinsley
Copy link
Contributor Author

My only concern is that the test suite might not have caught the type of bugs that real Rails apps would surface.

@@ -9,34 +9,26 @@ def names
Ransack.predicates.keys
end

def names_by_decreasing_length
names.sort { |a, b| b.length <=> a.length }
end
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ActiveAdmin relies on this method:

NoMethodError: undefined method 'names_by_decreasing_length' for Ransack::Predicate:Class

active_admin/filters/humanized.rb in predicates at line 50

          end

          def predicates
            Ransack::Predicate.names_by_decreasing_length
          end

          def ransack_predicate_translation

active_admin/filters/humanized.rb in current_predicate at line 46

          end

          def current_predicate
            @current_predicate ||= predicates.detect { |p| @body.end_with?("_#{p}") }
          end

          def predicates

active_admin/filters/humanized.rb in ransack_predicate_translation at line 54

          end

          def ransack_predicate_translation
            I18n.t("ransack.predicates.#{current_predicate}")
          end

          def active_admin_predicate_translation

active_admin/filters/humanized.rb in body at line 17

          end

          def body
            predicate = ransack_predicate_translation

            if current_predicate.nil?
              predicate = @body.titleize

active_admin/filters/resource_extension.rb in block (6 levels) in search_status_section at line 170

                    else
                      active.filters.each do |filter|
                        li do
                          span filter.body
                          b filter.value
                        end
                      end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like ActiveAdmin was duplicating behavior that Ransack already provided, so it could be simplified to:

def current_predicate
  Ransack::Predicate.detect_from_string @body
end

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, activeadmin/activeadmin#4951 replaced this code with Ransack::Translate.predicate

@seanlinsley
Copy link
Contributor Author

After also upgrading ActiveAdmin from 1.0 to 1.2, I'm now using this patch in production.

@scarroll32
Copy link
Member

I vote we merge it. I'll do so over the weekend unless anyone disagrees.

@seanlinsley
Copy link
Contributor Author

We haven't seen any issues so far 👍

@seanlinsley
Copy link
Contributor Author

Still no issues, so this should be good to go.

@seb-sykio
Copy link

hi
I'm still having this issue "exception : undefined method `names_by_decreasing_length' for Ransack::Predicate:Class"
I'm using ransack (2.1.0) and activeadmin (1.4.0)

thanks for help

tekniklr pushed a commit to tekniklr/ransack that referenced this pull request Mar 19, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants