-
Notifications
You must be signed in to change notification settings - Fork 135
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
Optimize URLHelpers.gather_constants
#1935
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 |
---|---|---|
|
@@ -106,19 +106,28 @@ def gather_constants | |
routes_reloader = Rails.application.routes_reloader | ||
routes_reloader.execute_unless_loaded if routes_reloader&.respond_to?(:execute_unless_loaded) | ||
|
||
Object.const_set(:GeneratedUrlHelpersModule, Rails.application.routes.named_routes.url_helpers_module) | ||
Object.const_set(:GeneratedPathHelpersModule, Rails.application.routes.named_routes.path_helpers_module) | ||
url_helpers_module = Rails.application.routes.named_routes.url_helpers_module | ||
path_helpers_module = Rails.application.routes.named_routes.path_helpers_module | ||
|
||
Object.const_set(:GeneratedUrlHelpersModule, url_helpers_module) | ||
Object.const_set(:GeneratedPathHelpersModule, path_helpers_module) | ||
|
||
constants = all_modules.select do |mod| | ||
next unless name_of(mod) | ||
|
||
includes_helper?(mod, GeneratedUrlHelpersModule) || | ||
includes_helper?(mod, GeneratedPathHelpersModule) || | ||
includes_helper?(mod.singleton_class, GeneratedUrlHelpersModule) || | ||
includes_helper?(mod.singleton_class, GeneratedPathHelpersModule) | ||
# Fast-path to quickly disqualify most cases | ||
next false unless url_helpers_module > mod || # rubocop:disable Style/InvertibleUnlessCondition | ||
path_helpers_module > mod || | ||
url_helpers_module > mod.singleton_class || | ||
path_helpers_module > mod.singleton_class | ||
|
||
includes_helper?(mod, url_helpers_module) || | ||
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. Observation: this computes and traverses the ancestor chain for I tried doing it just once, like so: mod_direct_ancestors = direct_ancestors_of(mod)
next true if mod_direct_ancestors.include?(url_helpers_module) ||
mod_direct_ancestors.include?(path_helpers_module)
mod_meta_direct_ancestors = direct_ancestors_of(mod.singleton_class)
mod_meta_direct_ancestors.include?(url_helpers_module) ||
mod_meta_direct_ancestors.include?(path_helpers_module) using: sig { params(mod: Module).returns(T::Array[Module]) }
private def direct_ancestors_of(mod)
if Class === mod
superclass = superclass_of(mod)
ancestors_of(mod).take_while { |a| !superclass.equal?(a) }
else
ancestors_of(mod)
end
end But it turned out to be roughly 25% slower 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 have a bunch of methods returning arrays in https://github.com/Shopify/tapioca/blob/35a6a43b5469d5b03d292330449fd7b6db7593a6/lib/tapioca/runtime/reflection.rb that could benefit from returning sets instead. |
||
includes_helper?(mod, path_helpers_module) || | ||
includes_helper?(mod.singleton_class, url_helpers_module) || | ||
includes_helper?(mod.singleton_class, path_helpers_module) | ||
end | ||
|
||
constants.concat(NON_DISCOVERABLE_INCLUDERS) | ||
constants.concat(NON_DISCOVERABLE_INCLUDERS).push(GeneratedUrlHelpersModule, GeneratedPathHelpersModule) | ||
end | ||
|
||
sig { returns(T::Array[Module]) } | ||
|
@@ -134,17 +143,20 @@ def gather_non_discoverable_includers | |
end.freeze | ||
end | ||
|
||
# Returns `true` if `mod` "directly" includes `helper`. | ||
# For classes, this method will return false if the `helper` is included only by a superclass | ||
sig { params(mod: Module, helper: Module).returns(T::Boolean) } | ||
private def includes_helper?(mod, helper) | ||
superclass_ancestors = [] | ||
ancestors = ancestors_of(mod) | ||
|
||
if Class === mod | ||
superclass = superclass_of(mod) | ||
superclass_ancestors = ancestors_of(superclass) if superclass | ||
own_ancestors = if Class === mod && (superclass = superclass_of(mod)) | ||
# These ancestors are unique to `mod`, and exclude those in common with `superclass`. | ||
ancestors.take(ancestors.count - ancestors_of(superclass).size) | ||
else | ||
ancestors | ||
end | ||
|
||
ancestors = Set.new.compare_by_identity.merge(ancestors_of(mod)).subtract(superclass_ancestors) | ||
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 allocations and set operations here where the majority of the hot path. |
||
ancestors.any? { |ancestor| helper == ancestor } | ||
paracycle marked this conversation as resolved.
Show resolved
Hide resolved
|
||
own_ancestors.include?(helper) | ||
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.
Extracting out these local variables removes the need to do repeated constant look-ups in the hot path below.