Skip to content

Commit

Permalink
Optimize Grape::Path -> Reduce Hash Allocation (#2513)
Browse files Browse the repository at this point in the history
* Grape::Path is now exposing only path and suffix
Last parameter is now double splatted

* Remove double splat operator

* Optimize default path settings
Use const static function for route match?

* Revert compile_many_routes.rb

* Add CHANGELOG.md
  • Loading branch information
ericproulx authored Nov 30, 2024
1 parent 86648fa commit 92573ea
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 251 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* [#2501](https://github.com/ruby-grape/grape/pull/2501): Remove deprecated `except` and `proc` options in values validator - [@ericproulx](https://github.com/ericproulx).
* [#2502](https://github.com/ruby-grape/grape/pull/2502): Remove deprecation `options` in `desc` - [@ericproulx](https://github.com/ericproulx).
* [#2512](https://github.com/ruby-grape/grape/pull/2512): Optimize hash alloc - [@ericproulx](https://github.com/ericproulx).
* [#2513](https://github.com/ruby-grape/grape/pull/2513): Optimize Grape::Path - [@ericproulx](https://github.com/ericproulx).
* Your contribution here.

#### Fixes
Expand Down
18 changes: 9 additions & 9 deletions lib/grape/endpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -160,12 +160,13 @@ def mount_in(router)
end

def to_routes
route_options = prepare_default_route_attributes
map_routes do |method, path|
path = prepare_path(path)
route_options[:suffix] = path.suffix
params = options[:route_options].merge(route_options)
route = Grape::Router::Route.new(method, path.path, params)
default_route_options = prepare_default_route_attributes
default_path_settings = prepare_default_path_settings

map_routes do |method, raw_path|
prepared_path = Path.new(raw_path, namespace, default_path_settings)
params = options[:route_options].present? ? options[:route_options].merge(default_route_options) : default_route_options
route = Grape::Router::Route.new(method, prepared_path.origin, prepared_path.suffix, params)
route.apply(self)
end.flatten
end
Expand Down Expand Up @@ -200,11 +201,10 @@ def map_routes
options[:method].map { |method| options[:path].map { |path| yield method, path } }
end

def prepare_path(path)
def prepare_default_path_settings
namespace_stackable_hash = inheritable_setting.namespace_stackable.to_hash
namespace_inheritable_hash = inheritable_setting.namespace_inheritable.to_hash
path_settings = namespace_stackable_hash.merge!(namespace_inheritable_hash)
Path.new(path, namespace, path_settings)
namespace_stackable_hash.merge!(namespace_inheritable_hash)
end

def namespace
Expand Down
95 changes: 39 additions & 56 deletions lib/grape/path.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,65 +3,66 @@
module Grape
# Represents a path to an endpoint.
class Path
attr_reader :raw_path, :namespace, :settings
DEFAULT_FORMAT_SEGMENT = '(/.:format)'
NO_VERSIONING_WITH_VALID_PATH_FORMAT_SEGMENT = '(.:format)'
VERSION_SEGMENT = ':version'

def initialize(raw_path, namespace, settings)
@raw_path = raw_path
@namespace = namespace
@settings = settings
end
attr_reader :origin, :suffix

def mount_path
settings[:mount_path]
def initialize(raw_path, raw_namespace, settings)
@origin = PartsCache[build_parts(raw_path, raw_namespace, settings)]
@suffix = build_suffix(raw_path, raw_namespace, settings)
end

def root_prefix
settings[:root_prefix]
def to_s
"#{origin}#{suffix}"
end

def uses_specific_format?
return false unless settings.key?(:format) && settings.key?(:content_types)
private

settings[:format] && Array(settings[:content_types]).size == 1
def build_suffix(raw_path, raw_namespace, settings)
if uses_specific_format?(settings)
"(.#{settings[:format]})"
elsif !uses_path_versioning?(settings) || (valid_part?(raw_namespace) || valid_part?(raw_path))
NO_VERSIONING_WITH_VALID_PATH_FORMAT_SEGMENT
else
DEFAULT_FORMAT_SEGMENT
end
end

def uses_path_versioning?
return false unless settings.key?(:version) && settings[:version_options]&.key?(:using)

settings[:version] && settings[:version_options][:using] == :path
def build_parts(raw_path, raw_namespace, settings)
[].tap do |parts|
add_part(parts, settings[:mount_path])
add_part(parts, settings[:root_prefix])
parts << VERSION_SEGMENT if uses_path_versioning?(settings)
add_part(parts, raw_namespace)
add_part(parts, raw_path)
end
end

def namespace?
namespace&.match?(/^\S/) && not_slash?(namespace)
def add_part(parts, value)
parts << value if value && not_slash?(value)
end

def path?
raw_path&.match?(/^\S/) && not_slash?(raw_path)
def not_slash?(value)
value != '/'
end

def suffix
if uses_specific_format?
"(.#{settings[:format]})"
elsif !uses_path_versioning? || (namespace? || path?)
'(.:format)'
else
'(/.:format)'
end
end
def uses_specific_format?(settings)
return false unless settings.key?(:format) && settings.key?(:content_types)

def path
PartsCache[parts]
settings[:format] && Array(settings[:content_types]).size == 1
end

def path_with_suffix
"#{path}#{suffix}"
end
def uses_path_versioning?(settings)
return false unless settings.key?(:version) && settings[:version_options]&.key?(:using)

def to_s
path_with_suffix
settings[:version] && settings[:version_options][:using] == :path
end

private
def valid_part?(part)
part&.match?(/^\S/) && not_slash?(part)
end

class PartsCache < Grape::Util::Cache
def initialize
Expand All @@ -71,23 +72,5 @@ def initialize
end
end
end

def parts
[].tap do |parts|
add_part(parts, mount_path)
add_part(parts, root_prefix)
parts << ':version' if uses_path_versioning?
add_part(parts, namespace)
add_part(parts, raw_path)
end
end

def add_part(parts, value)
parts << value if value && not_slash?(value)
end

def not_slash?(value)
value != '/'
end
end
end
43 changes: 24 additions & 19 deletions lib/grape/router/pattern.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ class Pattern

attr_reader :origin, :path, :pattern, :to_regexp

def_delegators :pattern, :named_captures, :params
def_delegators :pattern, :params
def_delegators :to_regexp, :===
alias match? ===

def initialize(pattern, options)
@origin = pattern
@path = build_path(pattern, options)
@pattern = build_pattern(@path, options)
def initialize(origin, suffix, options)
@origin = origin
@path = build_path(origin, options[:anchor], suffix)
@pattern = build_pattern(@path, options[:params], options[:format], options[:version], options[:requirements])
@to_regexp = @pattern.to_regexp
end

Expand All @@ -28,33 +28,34 @@ def captures_default

private

def build_pattern(path, options)
def build_pattern(path, params, format, version, requirements)
Mustermann::Grape.new(
path,
uri_decode: true,
params: options[:params],
capture: extract_capture(options)
params: params,
capture: extract_capture(format, version, requirements)
)
end

def build_path(pattern, options)
PatternCache[[build_path_from_pattern(pattern, options), options[:suffix]]]
def build_path(pattern, anchor, suffix)
PatternCache[[build_path_from_pattern(pattern, anchor), suffix]]
end

def extract_capture(options)
sliced_options = options
.slice(:format, :version)
.delete_if { |_k, v| v.blank? }
.transform_values { |v| Array.wrap(v).map(&:to_s) }
return sliced_options if options[:requirements].blank?
def extract_capture(format, version, requirements)
capture = {}.tap do |h|
h[:format] = map_str(format) if format.present?
h[:version] = map_str(version) if version.present?
end

return capture if requirements.blank?

options[:requirements].merge(sliced_options)
requirements.merge(capture)
end

def build_path_from_pattern(pattern, options)
def build_path_from_pattern(pattern, anchor)
if pattern.end_with?('*path')
pattern.dup.insert(pattern.rindex('/') + 1, '?')
elsif options[:anchor]
elsif anchor
pattern
elsif pattern.end_with?('/')
"#{pattern}?*path"
Expand All @@ -63,6 +64,10 @@ def build_path_from_pattern(pattern, options)
end
end

def map_str(value)
Array.wrap(value).map(&:to_s)
end

class PatternCache < Grape::Util::Cache
def initialize
super
Expand Down
12 changes: 8 additions & 4 deletions lib/grape/router/route.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ class Router
class Route < BaseRoute
extend Forwardable

FORWARD_MATCH_METHOD = ->(input, pattern) { input.start_with?(pattern.origin) }
NON_FORWARD_MATCH_METHOD = ->(input, pattern) { pattern.match?(input) }

attr_reader :app, :request_method

def_delegators :pattern, :path, :origin

def initialize(method, pattern, options)
def initialize(method, origin, path, options)
@request_method = upcase_method(method)
@pattern = Grape::Router::Pattern.new(pattern, options)
@pattern = Grape::Router::Pattern.new(origin, path, options)
@match_function = options[:forward_match] ? FORWARD_MATCH_METHOD : NON_FORWARD_MATCH_METHOD
super(options)
end

Expand All @@ -31,7 +35,7 @@ def apply(app)
def match?(input)
return false if input.blank?

options[:forward_match] ? input.start_with?(pattern.origin) : pattern.match?(input)
@match_function.call(input, pattern)
end

def params(input = nil)
Expand All @@ -46,7 +50,7 @@ def params(input = nil)
private

def params_without_input
pattern.captures_default.merge(attributes.params)
@params_without_input ||= pattern.captures_default.merge(attributes.params)
end

def upcase_method(method)
Expand Down
Loading

0 comments on commit 92573ea

Please sign in to comment.