Reduce per-request allocations on the request hot path#2696
Open
ericproulx wants to merge 1 commit intomasterfrom
Open
Reduce per-request allocations on the request hot path#2696ericproulx wants to merge 1 commit intomasterfrom
ericproulx wants to merge 1 commit intomasterfrom
Conversation
Danger ReportNo issues found. |
4c86825 to
312510b
Compare
A bundle of small, behavior-preserving cleanups that drop allocations
or chain walks from every request. Stable values (filter buckets,
versioner options, etc.) are computed once at compile/init and read via
`attr_reader`, instead of being re-resolved through nested hash lookups
on every request.
* `Endpoint#run`: precompute filter buckets (befores, before_validations,
after_validations, afters, finallies) and `:build_params_with` once
in `compile!`; replace the dynamic `define_method` accessor block
with `attr_reader`; remove a dead `header 'Allow', header['Allow']`
self-assignment.
* `Versioner::Base`: replace the two `define_method`-per-key blocks
(top-level and `version_options` keys) with `attr_reader` plus ivars
precomputed in `initialize`. Per-request `pattern`/`prefix`/`vendor`/
`strict`/etc. lookups now hit attr loads.
* `Request#make_params`: skip the `except(:version, :route_info)` hash
allocation in the common case where routing args contain only the
known keys.
* `Validators::Base#validate!`: lazy-allocate `array_errors`. Saves one
Array allocation per validator per request when validation succeeds.
* `InheritableValues#[]`: drop the per-call merge with `Hash#fetch`
plus a fallback block. Lookup is 1.55x faster microbenched, allocates
zero. Mostly helps boot/compile time (where ~30+ reads per endpoint
hit this).
* `Grape::API::Instance`: rename `ROOT_PREFIX_VERSIONING_KEY` to
`ROOT_PREFIX_VERSIONING_KEYS` (it holds 3 symbols), pair via `zip`
instead of `each_with_index` + manual indexing.
* `Validations::Types::CustomTypeCoercer#infer_coercion_method`:
guard-clause refactor of the nested if/else.
End-to-end, 3-run averaged on Ruby 4.0.3, arm64-darwin25, against
`/api/v1/hello` returning a small JSON object:
no-yjit yjit
master HEAD: 52,088 i/s 101,563 i/s
master + this PR: 57,433 i/s 107,982 i/s
+10.3% +6.3%
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
312510b to
3bdba32
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A bundle of small, behavior-preserving cleanups that reduce per-request allocations and method-dispatch chains on the request hot path. None change semantics — existing specs pass unchanged. The unifying pattern: stable values (filter buckets, versioner options, etc.) are computed once at compile/init time and read via
attr_reader, instead of being re-resolved through nested hash lookups on every request.Changes
Endpoint#run— precompute filter buckets (befores,before_validations,after_validations,afters,finallies) and:build_params_withonce incompile!; replace the dynamicdefine_methodaccessor block withattr_reader; remove a deadheader 'Allow', header['Allow']self-assignment.Versioner::Base— same shape: replace the twodefine_method-per-key blocks withattr_reader+ ivars precomputed ininitialize. Per-requestpattern/prefix/vendor/strict/etc. lookups now hit attr loads.Request#make_params— skip theexcept(:version, :route_info)hash allocation in the common case where routing args contain only the known keys.Validators::Base#validate!— lazy-allocatearray_errors. Saves one Array allocation per validator per request when validation succeeds.InheritableValues#[]— drop the per-call@inherited_values.merge(@new_values)and useHash#fetchwith a fallback block. 1.55x faster lookup, zero allocation. Mostly helps boot/compile time (where ~30+ reads per endpoint hit this).Grape::API::Instance— renameROOT_PREFIX_VERSIONING_KEY→ROOT_PREFIX_VERSIONING_KEYS(holds 3 symbols); pair withzipinstead ofeach_with_index+ manual indexing.Validations::Types::CustomTypeCoercer#infer_coercion_method— guard-clause refactor of nested if/else.Throughput impact
3-run averaged on Ruby 4.0.3, arm64-darwin25, against
/api/v1/helloreturning a small JSON object:The relative gain compresses under YJIT (10% → 6%) since the JIT was already specializing some of the hot-path indirection; the absolute gain stays at ~+6.4k req/s either way.
Test plan
bundle exec rspec— full suite green (2236 examples, 0 failures), no test changes needed.bundle exec rubocopon touched files — clean.🤖 Generated with Claude Code