Skip to content

Coerce through dry-types' non-raising call - #2930

Closed
ericproulx wants to merge 1 commit into
masterfrom
perf/coerce-without-raising
Closed

ericproulx wants to merge 1 commit into
masterfrom
perf/coerce-without-raising

Conversation

@ericproulx

Copy link
Copy Markdown
Contributor

Summary

DryTypeCoercer#call coerced with @coercer[val] and rescued Dry::Types::CoercionError for a value it could not coerce. The raise is what cost: dry-types' CoercionError.handle re-raises with backtrace: exception.backtrace of the error underneath (the ArgumentError from Integer(), Float(), Date.parse, …), which builds that whole backtrace as Strings. At request depth a rejected value cost about 30 µs, where an accepted one costs a few hundred nanoseconds.

It is paid by:

  • every types: [Integer, String] param given a string, since each coercer tried before the one that accepts it fails first;
  • every 400 for a mistyped value (?id=abc for an Integer);
  • invalid dates, numbers and booleans.

dry-types' documented block form of Type#call ("When a block is passed, call will never throw an exception on failed coercion, instead it will call the block") reports a failure by calling the block, so the coercer now passes one that answers InvalidValue. The rescue stays for anything that raises regardless.

A rejected Integer at a request's stack depth: 31.2 → 4.4 µs.

Benchmarks

Median of interleaved subprocess rounds against master, Ruby 4.0.6 (7 rounds without JIT, 5 with YJIT):

request no JIT YJIT
GET with types: [Integer, String] given abc, plus an Array[Integer] with coerce_with +114.7% +195.1%
400 for ?id=abc on requires :id, type: Integer +49.1% +95.6%
GET with valid query params (control) −1.3% (noise)
POST with a valid JSON body (control) +1.1% (noise)

Behaviour

Byte-identical to master over a 2,214-case coercer matrix, calling Types.build_coercer directly: 27 types (every primitive, Numeric, Grape::API::Boolean, TrueClass/FalseClass, Hash, Array, Array[T], Set[T], multiple-type lists including nested collections), each coerced and strict, × 41 inputs (valid, malformed, blank, nil, arrays, hashes, symbols, invalid bytes). 1,640 of those cases are rejections, so the changed path is the one exercised. The request-level coercion and validation matrices are unchanged too.

Test plan

  • Full RSpec suite passes locally.
  • RuboCop clean.
  • Mutation-checked: a failed coercion answering nil fails 37 specs; answering the raw value fails 29. Reverting to the raising call changes nothing observable, which is the point; the benchmark pins that.
  • CI green.

🤖 Generated with Claude Code

`DryTypeCoercer#call` coerced with `@coercer[val]` and rescued
`Dry::Types::CoercionError` for a value it could not coerce. That raise
is what cost: dry-types' `CoercionError.handle` re-raises with
`backtrace: exception.backtrace` of the error underneath, which builds
that whole backtrace as Strings. At request depth a rejected value cost
about 30 µs, where an accepted one costs a few hundred nanoseconds.

It is paid by every `types: [Integer, String]` param given a string,
since each coercer tried before the one that accepts it fails first; by
every 400 for a mistyped value; and by invalid dates, numbers and
booleans.

dry-types' documented block form of `Type#call` reports a failure by
calling the block instead of raising, so the coercer now passes one that
answers `InvalidValue`. The `rescue` stays for anything that raises
regardless. A rejected Integer at request depth went from 31.2 to 4.4 µs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ericproulx
ericproulx force-pushed the perf/coerce-without-raising branch from b64b098 to 86deee6 Compare September 11, 2026 08:32
@github-actions

github-actions Bot commented Sep 11, 2026

Copy link
Copy Markdown

Danger Report

Errors

  • One of the lines below found in CHANGELOG.md doesn't match the expected format. Please make it look like the other lines, pay attention to version numbers, periods, spaces and date formats.

Markdowns

* [#XXXX](https://github.com/ruby-grape/grape/pull/XXXX): Coerce through dry-types' non-raising call so a rejected value no longer builds a backtrace - [@ericproulx](https://github.com/ericproulx).
does not include a pull request link

View run

@ericproulx

Copy link
Copy Markdown
Contributor Author

Closing in favour of #2936, which combines #2922#2934 into one PR, re-benchmarked as a whole against master. The write-up here (behaviour matrix, mutation results) still describes this part of the change.

@ericproulx ericproulx closed this Sep 11, 2026
@ericproulx
ericproulx deleted the perf/coerce-without-raising branch September 15, 2026 21:49
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