Skip to content

Work out each declared param's key and empty value when the endpoint compiles - #3074

Open
ericproulx wants to merge 1 commit into
masterfrom
perf/declared-params-compiled
Open

ericproulx wants to merge 1 commit into
masterfrom
perf/declared-params-compiled

Conversation

@ericproulx

Copy link
Copy Markdown
Contributor

declared answers, for each param an endpoint declares, the key it is filed under (its name, or the one as: gave it) and the empty value it stands for when it was not sent (an Array, a Set or a Hash, after its documented type). Neither depends on the request, yet both were worked out again on every call, for every declared param:

  • the path to the param was built to look its new name up,
  • its documented name (address[geo][lat]) was spelled out to look its type up,
  • that type was matched against the ones that have an empty value,
  • and for a Hash, every documented param was scanned for a child.

Grape::DeclaredParams now holds those answers per endpoint. Endpoint#compile! builds it, as it builds Validations::UndeclaredParams, and DeclaredParamsHandler walks its params against the ones the request sent. An empty value is copied on every call, so no two requests share one.

Numbers

Interleaved A/B from one checkout, median of per-round pairs over 6 rounds. In parentheses, the rounds the branch won.

request interpreter YJIT
POST, 23 params declared, 4 of the 15 top-level ones sent, declared(params) +29.7% (6/6) +42.9% (6/6)
the same, declared(params, include_missing: false) +10.6% (6/6) +5.5% (6/6)
POST, 10 params with a Hash and an Array of three Hashes, all sent, declared(params) +10.0% (6/6) +7.6% (6/6)
PUT by id, 9 params, one a Hash, all sent, declared(params, include_missing: false) +5.7% (6/6) +5.1% (6/6)
the 23 params again, on a route that does not call declared (control) -0.4% +0.7%
route with no params (control) +0.6% +1.5%

On the first row declared itself goes from 29.9 to about 13 µs on the interpreter.

Cost

The params are compiled for every endpoint, whether or not it calls declared: nothing at compile time says that it will. On an API of 600 endpoints declaring 12 params each, compiling goes from 289 to 300 ms and retained memory from 11.6 to 12.2 MB.

What changes

Compared with master over random declarations, payloads and every combination of declared's options, about 900,000 calls: the same answer wherever master answered. In two cases master raised, and the branch answers:

  • A param that desc ..., params: documents by hand with a type that is not a String (a Class, say), or with something other than a Hash of details, has no empty value. declared raised NoMethodError or TypeError when such a param was not sent.
  • An optional param named from configuration has no name while the API is called unmounted. declared raised NoMethodError on nil.to_sym there, and now files the param under nil. Mounted, nothing changes.

DeclaredParamsHandler#call takes the compiled params in place of the declared params, the documented ones and the renames. Nothing else in Grape, grape-entity or grape-swagger calls it.

No contract change, so no UPGRADING entry.

🤖 Generated with Claude Code

@ericproulx
ericproulx force-pushed the perf/declared-params-compiled branch 3 times, most recently from 5043c22 to b58229a Compare November 10, 2026 10:52
…compiles

`declared` answers, for each declared param, the key it is filed under
(its name, or the one `as:` gave it) and the empty value it stands for
when it was not passed (an Array, a Set or a Hash, after its documented
type). Neither depends on the request, and both were worked out again on
every call: the path to the param built to look its new name up, its
documented name spelled out to look its type up, that type matched
against the ones that have an empty value, and for a Hash every
documented param scanned for a child.

`Grape::DeclaredParams` holds them per endpoint, built by
`Endpoint#compile!` as `Validations::UndeclaredParams` is, and
`DeclaredParamsHandler` walks its params.

A param a `desc` documents by hand with a type that is not a String, or
with no Hash of details, has no empty value. `declared` raised on it
when it was not passed.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
@ericproulx
ericproulx force-pushed the perf/declared-params-compiled branch from b58229a to 47ccfd2 Compare November 11, 2026 10:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant