-
-
Notifications
You must be signed in to change notification settings - Fork 810
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
Improve memory usage #820
Conversation
@@ -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 } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 | ||
) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 } |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ref #857 CC @seanfcarroll
@seanlinsley any updates here? |
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. |
79ae30f
to
d5ea5ce
Compare
d5ea5ce
to
ebd159f
Compare
I'm going to start using this in my app to see if any bugs appear. |
ebd159f
to
f570a40
Compare
Happy to merge this into master if it's got consensus. Thoughts? |
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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
After also upgrading ActiveAdmin from 1.0 to 1.2, I'm now using this patch in production. |
I vote we merge it. I'll do so over the weekend unless anyone disagrees. |
We haven't seen any issues so far 👍 |
Still no issues, so this should be good to go. |
hi thanks for help |
…memory-usage Improve memory usage
Proof-of-concept for #819
These changes decreased memory usage by 96% 🚀 (1.3 MB to 58 KB)