Skip to content

Avoid empty-hash merges on request hot paths#2689

Merged
ericproulx merged 1 commit into
masterfrom
perf/avoid-empty-hash-merges
Apr 22, 2026
Merged

Avoid empty-hash merges on request hot paths#2689
ericproulx merged 1 commit into
masterfrom
perf/avoid-empty-hash-merges

Conversation

@ericproulx
Copy link
Copy Markdown
Contributor

Summary

Three || {} sites on the per-request path allocated an empty Hash and handed it to a merge that produced a shallow copy of the left-hand side — pure waste. Guard the merge when the right-hand side is absent or empty.

  • Router#process_routeargs.merge(route_params || {})route_params.blank? ? args : args.merge(route_params). On every static route (no path placeholders), this saves both the {} allocation and the identical-copy hash from merge.
  • Request#make_paramsdeep_merge! on an empty routing-args hash walked nothing but still paid the call cost; skip it entirely when empty.
  • DSL::Entity#present (keyed form) — (body || {}).merge(key => representation) → ternary that builds the one-key hash directly when body is nil.

No behavior change.

Perf / Benchmarks

Microbenchmark of the process_route merge shape, 1,000 calls per run:

Scenario old new speedup allocations (old → new)
route_params nil (common) 4.48 M i/s 8.31 M i/s 1.85x 3000 → 1000 (3x fewer)
route_params empty {} 5.16 M i/s 8.02 M i/s 1.55x 2000 → 1000 (2x fewer)
route_params real 4.58 M i/s 4.67 M i/s within noise unchanged

Test plan

  • bundle exec rspec — 2,236 examples, 0 failures.
  • bundle exec rubocop lib/grape/router.rb lib/grape/request.rb lib/grape/dsl/entity.rb — clean.
  • CI green.

🤖 Generated with Claude Code

@ericproulx ericproulx force-pushed the perf/avoid-empty-hash-merges branch from 4949eeb to 1695305 Compare April 20, 2026 23:18
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Apr 20, 2026

Danger Report

No issues found.

View run

@ericproulx ericproulx force-pushed the perf/avoid-empty-hash-merges branch 5 times, most recently from 7d4c209 to 97810df Compare April 20, 2026 23:29
Three `|| {}` patterns on the per-request path allocated an empty Hash
and handed it to a merge that produced a shallow copy of the left-hand
side — pure waste. Guard the merge when the right-hand side is absent
or empty instead.

When a route has no path placeholders (or none captured), `route.params`
returns an empty Hash (or nil). The old code did
`args.merge(route_params || {})`, allocating `{}` and then a new hash
identical to `args` on every matched static route.

`grape_routing_args` falls back to `{}` when env has no routing args.
`deep_merge!` on `{}` is a walking no-op. Skip the call entirely when
routing_args is empty.

`(body || {}).merge(key => representation)` — when no body is set
(the common case), this allocated `{}` only to merge one key into it.
Build the one-key hash directly.

process_route, no route_params (static path, common case):
  old: 4.48 M i/s, 480 objects allocated / 1k calls
  new: 8.31 M i/s, 160 objects allocated / 1k calls  (1.85x faster, 3x fewer)

process_route, empty {} route_params:
  old: 5.16 M i/s, 320 objects / 1k calls
  new: 8.02 M i/s, 160 objects / 1k calls  (1.55x faster, 2x fewer)

process_route, real route_params: within noise (unchanged path).

No behavior change; all 2,236 specs pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ericproulx ericproulx force-pushed the perf/avoid-empty-hash-merges branch from 97810df to e91b191 Compare April 22, 2026 00:57
@ericproulx ericproulx merged commit 952a74c into master Apr 22, 2026
79 checks passed
@ericproulx ericproulx deleted the perf/avoid-empty-hash-merges branch April 22, 2026 01:00
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.

2 participants