Skip to content

Commit 66aadad

Browse files
ericproulxclaude
andauthored
Return frozen EMPTY array from StackableValues on miss (#2690)
`StackableValues#[]` is the accessor behind `namespace_stackable[key]` — called multiple times per request (five filter phases, plus ad-hoc settings lookups). When neither `inherited_values` nor `new_values` have an entry for the key, the old `return new_value || []` allocated a fresh empty array on every miss. Introduce a frozen `EMPTY` singleton and return it in the miss branch. Saves two short-lived empty-array allocations per request in the realistic shape (some filters set, some not) and up to five in the worst case. One caller (`Endpoint#initialize`) stored `namespace_stackable[:validations]` directly into `route[:saved_validations]` by reference, and later `.concat`'d parent validations into it via `inherit_settings`. With the stackable now able to return a frozen array, that mutation would `FrozenError`. Dup at capture so the route keeps its own mutable list — good hygiene regardless of this change. Realistic (3 keys populated, 2 absent): current: 2.01 M i/s, 80B / 2 objects allocated variant: 2.19 M i/s, 0B / 0 objects allocated Worst case (all 5 absent): current: 2.18 M i/s, 200B / 5 objects variant: 2.49 M i/s, 0B / 0 objects (1.14x faster) No behavior change; all 2,236 specs pass. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 952a74c commit 66aadad

3 files changed

Lines changed: 6 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* [#2687](https://github.com/ruby-grape/grape/pull/2687): Skip backtrace capture on internal validation exceptions - [@ericproulx](https://github.com/ericproulx).
1212
* [#2688](https://github.com/ruby-grape/grape/pull/2688): Consolidate user-registered rescue handler lookup into `Middleware::Error#registered_rescue_handler` backed by a shared `rescue_handler_from` primitive - [@ericproulx](https://github.com/ericproulx).
1313
* [#2689](https://github.com/ruby-grape/grape/pull/2689): Avoid empty-hash merges on request hot paths - [@ericproulx](https://github.com/ericproulx).
14+
* [#2690](https://github.com/ruby-grape/grape/pull/2690): Avoid allocating an empty array on every `StackableValues#[]` miss - [@ericproulx](https://github.com/ericproulx).
1415
* Your contribution here.
1516

1617
#### Fixes

lib/grape/endpoint.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def initialize(new_settings, **options, &block)
4646
# this endpoint and its parents, but later it will be cleaned up,
4747
# see +reset_validations!+ in lib/grape/dsl/validations.rb
4848
inheritable_setting.route[:declared_params] = inheritable_setting.namespace_stackable[:declared_params].flatten
49-
inheritable_setting.route[:saved_validations] = inheritable_setting.namespace_stackable[:validations]
49+
inheritable_setting.route[:saved_validations] = inheritable_setting.namespace_stackable[:validations].dup
5050

5151
inheritable_setting.namespace_stackable[:representations] ||= []
5252
inheritable_setting.namespace_inheritable[:default_error_status] ||= 500

lib/grape/util/stackable_values.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
module Grape
44
module Util
55
class StackableValues < BaseInheritable
6-
# Even if there is no value, an empty array will be returned.
6+
EMPTY = [].freeze
7+
8+
# Even if there is no value, an empty (frozen) array will be returned.
79
def [](name)
810
inherited_value = inherited_values[name]
911
new_value = new_values[name]
1012

11-
return new_value || [] unless inherited_value
13+
return new_value || EMPTY unless inherited_value
1214

1315
concat_values(inherited_value, new_value)
1416
end

0 commit comments

Comments
 (0)