-
-
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -37,15 +37,14 @@ def initialize(object, options = {}) | |
@base = @join_dependency.join_base | ||
@engine = @base.arel_engine | ||
end | ||
end | ||
|
||
@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 commentThe 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 = Hash.new do |hash, key| | ||
parent, attr_name = get_parent_and_attribute_name(key) | ||
if parent && attr_name | ||
hash[key] = [parent, attr_name] | ||
end | ||
def bind_pair_for(key) | ||
@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 commentThe 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. |
||
[parent, attr_name] if parent && attr_name | ||
end | ||
end | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,27 @@ module Ransack | |
module Configuration | ||
|
||
mattr_accessor :predicates, :options | ||
self.predicates = {} | ||
|
||
class PredicateCollection | ||
attr_reader :sorted_names_with_underscores | ||
|
||
def initialize | ||
@collection = {} | ||
@sorted_names_with_underscores = [] | ||
end | ||
|
||
delegate :[], :keys, :has_key?, to: :@collection | ||
|
||
def []=(key, value) | ||
@sorted_names_with_underscores << [key, '_' + key] | ||
@sorted_names_with_underscores.sort! { |(a, _), (b, _)| b.length <=> a.length } | ||
|
||
@collection[key] = value | ||
end | ||
end | ||
|
||
self.predicates = PredicateCollection.new | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. It also assumes that the same predicate key is never set twice. |
||
|
||
self.options = { | ||
:search_key => :q, | ||
:ignore_unknown_conditions => true, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -40,7 +40,7 @@ def klassify(obj) | |
# Convert a string representing a chain of associations and an attribute | ||
# into the attribute itself | ||
def contextualize(str) | ||
parent, attr_name = @bind_pairs[str] | ||
parent, attr_name = bind_pair_for(str) | ||
table_for(parent)[attr_name] | ||
end | ||
|
||
|
@@ -59,24 +59,24 @@ def scope_arity(scope) | |
|
||
def bind(object, str) | ||
return nil unless str | ||
object.parent, object.attr_name = @bind_pairs[str] | ||
object.parent, object.attr_name = bind_pair_for(str) | ||
end | ||
|
||
def traverse(str, base = @base) | ||
str ||= ''.freeze | ||
|
||
if (segments = str.split(/_/)).size > 0 | ||
if (segments = str.split(Constants::UNDERSCORE)).size > 0 | ||
remainder = [] | ||
found_assoc = nil | ||
while !found_assoc && segments.size > 0 do | ||
# Strip the _of_Model_type text from the association name, but hold | ||
# onto it in klass, for use as the next base | ||
assoc, klass = unpolymorphize_association( | ||
segments.join('_'.freeze) | ||
segments.join(Constants::UNDERSCORE) | ||
) | ||
if found_assoc = get_association(assoc, base) | ||
base = traverse( | ||
remainder.join('_'.freeze), klass || found_assoc.klass | ||
remainder.join(Constants::UNDERSCORE), klass || found_assoc.klass | ||
) | ||
end | ||
|
||
|
@@ -93,9 +93,9 @@ def association_path(str, base = @base) | |
base = klassify(base) | ||
str ||= ''.freeze | ||
path = [] | ||
segments = str.split(/_/) | ||
segments = str.split(Constants::UNDERSCORE) | ||
association_parts = [] | ||
if (segments = str.split(/_/)).size > 0 | ||
if (segments = str.split(Constants::UNDERSCORE)).size > 0 | ||
while segments.size > 0 && | ||
!base.columns_hash[segments.join(Constants::UNDERSCORE)] && | ||
association_parts << segments.shift do | ||
|
@@ -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 commentThe 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 commentThe 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 commentThe 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 commentThe 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 commentThe 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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. ref #857 CC @seanfcarroll |
||
end | ||
|
||
def searchable_attributes(str = ''.freeze) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 commentThe 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 commentThe 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 commentThe reason will be displayed to describe this comment to others. Learn more. Ah, activeadmin/activeadmin#4951 replaced this code with |
||
|
||
def named(name) | ||
Ransack.predicates[name.to_s] | ||
end | ||
|
||
def detect_and_strip_from_string!(str) | ||
if p = detect_from_string(str) | ||
str.sub! /_#{p}$/, ''.freeze | ||
p | ||
end | ||
detect_from_string str, chomp: true | ||
end | ||
|
||
def detect_from_string(str) | ||
names_by_decreasing_length.detect { |p| str.end_with?("_#{p}") } | ||
end | ||
def detect_from_string(str, chomp: false) | ||
return unless str | ||
|
||
# def name_from_attribute_name(attribute_name) | ||
# names_by_decreasing_length.detect { | ||
# |p| attribute_name.to_s.match(/_#{p}$/) | ||
# } | ||
# end | ||
Ransack.predicates.sorted_names_with_underscores.each do |predicate, underscored| | ||
if str.end_with? underscored | ||
str.chomp! underscored if chomp | ||
return predicate | ||
end | ||
end | ||
|
||
# def for_attribute_name(attribute_name) | ||
# self.named(detect_from_string(attribute_name.to_s)) | ||
# end | ||
nil | ||
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.
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:
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 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: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, addinghas_paper_trail
to the model would unload & reload the model, soransackable_associations
is no longer cached.AFAICT there aren't any scenarios where this could cause a bug.