dry-validation-rust 0.1.0.pre5 → 0.1.0.pre6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +13 -0
  3. data/.markdownlint.yml +9 -0
  4. data/.rubocop.yml +124 -0
  5. data/.ruby-version +1 -0
  6. data/.tool-versions +1 -0
  7. data/.yardopts +4 -0
  8. data/AGENTS.md +376 -0
  9. data/CHANGELOG.md +71 -0
  10. data/CODE_OF_CONDUCT.md +42 -0
  11. data/CONTRIBUTING.md +159 -0
  12. data/Cargo.lock +809 -0
  13. data/Cargo.toml +10 -0
  14. data/GOVERNANCE.md +68 -0
  15. data/Gemfile +15 -0
  16. data/Gemfile.lock +132 -0
  17. data/NOTICE.md +5 -5
  18. data/README.md +206 -181
  19. data/Rakefile +350 -0
  20. data/SECURITY.md +98 -0
  21. data/SECURITY_AUDIT.md +33 -0
  22. data/SUMMARY.md +30 -0
  23. data/SUPPORT.md +67 -0
  24. data/book.toml +9 -0
  25. data/codecov.yml +11 -0
  26. data/compatibility.yml +453 -0
  27. data/deny.toml +7 -0
  28. data/dry-validation-rust.gemspec +21 -23
  29. data/ext/dry_validation_rust/Cargo.toml +7 -7
  30. data/ext/dry_validation_rust/build.rs +5 -0
  31. data/ext/dry_validation_rust/extconf.rb +12 -0
  32. data/ext/dry_validation_rust/fuzz/.gitignore +5 -0
  33. data/ext/dry_validation_rust/fuzz/Cargo.toml +19 -0
  34. data/ext/dry_validation_rust/fuzz/corpus/parse_plan/basic_params.json +1 -0
  35. data/ext/dry_validation_rust/fuzz/fuzz_targets/parse_plan.rs +9 -0
  36. data/lib/dry/validation/rust/path_trie.rb +15 -8
  37. data/lib/dry/validation/rust/schema/dsl.rb +8 -0
  38. data/lib/dry/validation/rust/schema/field_definition.rb +3 -1
  39. data/lib/dry/validation/rust/schema/predicate_block.rb +2 -1
  40. data/lib/dry/validation/rust/schema/ruby_type_processor.rb +38 -23
  41. data/lib/dry/validation/rust/version.rb +1 -1
  42. data/supply-chain/audits.toml +4 -0
  43. data/supply-chain/config.toml +368 -0
  44. data/supply-chain/imports.lock +4 -0
  45. data/support_matrix.yml +9 -0
  46. metadata +64 -25
  47. data/docs/ARCHITECTURE.md +0 -256
  48. data/docs/COMPATIBILITY.md +0 -198
  49. data/docs/FEASIBILITY.md +0 -207
  50. data/docs/SUPPORT_MATRIX.md +0 -66
  51. data/docs/VERIFICATION.md +0 -128
data/README.md CHANGED
@@ -5,20 +5,27 @@ engine with familiar dry-validation-style contract syntax and a precisely
5
5
  documented compatible subset. Rust handles the immutable declarative schema
6
6
  execution path; Ruby preserves dynamic rules and Ruby-specific semantics.
7
7
 
8
- > Status: `0.1.0.pre5` alpha pre-release. The side-by-side API has a defined
9
- > `0.1.x` compatibility promise and is covered by focused tests, differential
10
- > checks, package verification, and reproducible benchmark evidence. It is not
11
- > a full, production-ready drop-in replacement for upstream `dry-validation`.
8
+ > Note: This is an early-stage project. The side-by-side API is covered by
9
+ > focused tests, differential checks, package verification, and reproducible
10
+ > benchmark evidence, but it is not a full, production-ready drop-in
11
+ > replacement for upstream `dry-validation`.
12
12
 
13
13
  Before adoption, review [the support matrix](docs/SUPPORT_MATRIX.md),
14
14
  [compatibility matrix](docs/COMPATIBILITY.md), and
15
15
  [verification evidence](docs/VERIFICATION.md) for the exact supported surface,
16
16
  platforms, and known boundaries.
17
17
 
18
+ New users can follow the [Getting started guide](docs/getting-started.md) to
19
+ install the gem and build their first contract.
20
+
21
+ The native extension's [Rust API reference](https://alex-tomilov.github.io/dry-validation-rust/rustdoc/)
22
+ is published with the documentation site.
23
+
18
24
  For project participation and reporting routes, see
19
25
  [CONTRIBUTING.md](CONTRIBUTING.md), [SUPPORT.md](SUPPORT.md),
20
26
  [SECURITY.md](SECURITY.md), [GOVERNANCE.md](GOVERNANCE.md), and the
21
- [Code of Conduct](CODE_OF_CONDUCT.md).
27
+ [Code of Conduct](CODE_OF_CONDUCT.md). Ask usage questions in
28
+ [GitHub Discussions](https://github.com/alex-tomilov/dry-validation-rust/discussions).
22
29
  See the concise [roadmap](docs/ROADMAP.md) for planned outcomes and
23
30
  [project-management policy](docs/PROJECT_MANAGEMENT.md) for issue workflow.
24
31
 
@@ -44,136 +51,46 @@ desirable. Calling those blocks through Ruby preserves the feature that makes
44
51
 
45
52
  ## Installation
46
53
 
47
- ### Precompiled (recommended)
48
-
49
54
  When a precompiled gem is published for your platform, install it with:
50
55
 
51
56
  ```bash
52
57
  gem install dry-validation-rust
53
58
  ```
54
59
 
55
- The current `0.1.x` support target is source builds; see the
56
- [support matrix](docs/SUPPORT_MATRIX.md) for the authoritative platform status.
57
-
58
- ### From source
59
-
60
- Requires Rust 1.75 or newer, libclang, and a C toolchain. The MSRV is Rust
61
- 1.75 and is tested in CI. A source checkout pins Rust 1.75.0 automatically
62
- through `rust-toolchain.toml`.
63
-
64
- ```bash
65
- gem install dry-validation-rust --platform ruby
66
- ```
60
+ See the [support matrix](docs/SUPPORT_MATRIX.md) for the authoritative version
61
+ and platform status. If a source build is required, see the
62
+ [source-build instructions](docs/getting-started.md#build-from-source).
67
63
 
68
64
  ## Primary safe API
69
65
 
70
- Use the side-by-side namespace first:
71
-
72
- ```ruby
73
- require "dry/validation/rust"
74
-
75
- class NewUserContract < Dry::Validation::Rust::Contract
76
- params do
77
- required(:email).filled(:string, format?: /\A[^@]+@[^@]+\z/)
78
- required(:age).value(:integer)
79
- optional(:display_name).maybe(:string)
80
-
81
- required(:addresses).array(:hash) do
82
- required(:city).filled(:string)
83
- required(:postcode).filled(:string)
84
- end
85
- end
86
-
87
- rule(:age) do
88
- key.failure("must be at least 18") if value < 18
89
- end
90
- end
91
-
92
- result = NewUserContract.new.call(
93
- "email" => "jane@example.org",
94
- "age" => "17",
95
- "display_name" => "",
96
- "addresses" => [{"city" => "Astana", "postcode" => "010000"}]
97
- )
98
-
99
- result.to_h
100
- result.success?
101
- result.errors.to_h
102
- ```
66
+ For new work, use `require "dry/validation/rust"` and subclass
67
+ `Dry::Validation::Rust::Contract`. The [Getting started guide](docs/getting-started.md)
68
+ has a copy-pasteable contract, rule, error-handling, and web-framework examples.
103
69
 
104
- This is the primary supported API. Version, platform, and upstream-reference
105
- targets are listed in [SUPPORT_MATRIX.md](docs/SUPPORT_MATRIX.md). Supported
106
- DSL and semantic differences are listed in [COMPATIBILITY.md](docs/COMPATIBILITY.md).
70
+ Version, platform, and upstream-reference targets are listed in
71
+ [SUPPORT_MATRIX.md](docs/SUPPORT_MATRIX.md). Supported DSL and semantic
72
+ differences are listed in [COMPATIBILITY.md](docs/COMPATIBILITY.md).
107
73
 
108
74
  ### Side-by-side API stability
109
75
 
110
- For the `0.1.x` line, the public side-by-side API is
111
- `Dry::Validation::Rust::Contract`, its nested `Result` and `Values` types, and
112
- the directly exposed `Schema`, `MessageSet`, and `Evaluator` types. Their
113
- documented public methods will not be removed or changed incompatibly in a patch
114
- release; a breaking side-by-side API change requires the next minor release. The
115
- exact-compatibility entrypoints are explicitly experimental and are not covered
116
- by this promise.
117
-
118
- ## Migration-compatible subset
119
-
120
- The safe API intentionally keeps familiar contract syntax where that behavior
121
- is implemented and covered. Use it for comparison work and gradual migration
122
- without taking over upstream constants:
123
-
124
- ```ruby
125
- require "dry/validation/rust"
126
-
127
- class AgeContract < Dry::Validation::Rust::Contract
128
- params do
129
- required(:age).value(:integer)
130
- end
131
- end
132
- ```
133
-
134
- ## Exact compatibility shim
135
-
136
- Exact compatibility mode keeps upstream-like require paths and constants:
137
-
138
- ```ruby
139
- require "dry/validation"
76
+ The public side-by-side API is `Dry::Validation::Rust::Contract`, its nested
77
+ `Result` and `Values` types, and the directly exposed `Schema`, `MessageSet`,
78
+ and `Evaluator` types. Its compatibility policy is defined in the
79
+ [support matrix](docs/SUPPORT_MATRIX.md), with individual classifications in
80
+ [API_STABILITY.md](docs/API_STABILITY.md). The exact-compatibility entrypoints
81
+ are explicitly experimental and are not covered by that policy.
140
82
 
141
- class AgeContract < Dry::Validation::Contract
142
- params do
143
- required(:age).value(:integer)
144
- end
145
- end
146
- ```
147
-
148
- > Collision warning: exact compatibility mode is experimental and opt-in. Do
149
- > not install or activate upstream `dry-validation` / `dry-schema` in the same
150
- > process when using `require "dry/validation"` or `require "dry/schema"` from
151
- > this gem. Both implementations own the same require paths and constants. This
152
- > gem raises a clear `LoadError` when it can detect such a collision.
153
-
154
- The exact shim currently lives in this gem. If maintaining the shim separately
155
- becomes necessary, the intended product split is `dry-validation-rust` for the
156
- safe namespace and `dry-validation-rust-compat` for the upstream-like require
157
- paths. No split is planned for the `0.1.x` line without concrete maintenance
158
- evidence.
159
-
160
- ## Loading modes
161
-
162
- ### Side-by-side mode
83
+ ## Loading
163
84
 
164
85
  `require "dry/validation/rust"` exposes only the
165
86
  `Dry::Validation::Rust` namespace. It does not define
166
87
  `Dry::Validation::Contract` or `Dry::Schema`.
167
88
 
168
- ### Exact compatibility mode
169
-
170
- `require "dry/validation"` defines:
171
-
172
- - `Dry::Validation::Contract` and related result/message aliases;
173
- - minimal `Dry::Schema.Params`, `Dry::Schema.JSON`, and
174
- `Dry::Schema.define` factories for reusable schemas.
175
-
176
- The collision warning above applies to every exact-mode entrypoint.
89
+ The upstream-like `require "dry/validation"` and `require "dry/schema"`
90
+ entrypoints are deprecated and cannot safely coexist with upstream
91
+ `dry-validation` or `dry-schema` in one Ruby process. Use side-by-side mode for
92
+ new work; existing applications should follow the
93
+ [exact-mode migration guide](docs/MIGRATION_FROM_EXACT_MODE.md).
177
94
 
178
95
  ## Supported highlights
179
96
 
@@ -206,12 +123,8 @@ The complete exclusions and semantic differences are explicit in
206
123
 
207
124
  ## Building from source
208
125
 
209
- Requirements:
210
-
211
- - Ruby 3.3 or newer with development headers;
212
- - Rust 1.75 or newer and Cargo (the MSRV, tested in CI);
213
- - a C toolchain;
214
- - libclang where the selected `rb-sys` build uses bindgen.
126
+ Meet the [source-build prerequisites](docs/getting-started.md#build-from-source)
127
+ first.
215
128
 
216
129
  Then:
217
130
 
@@ -226,40 +139,133 @@ native-extension lifecycle.
226
139
 
227
140
  ## Verification
228
141
 
229
- Current release evidence was collected with:
230
-
231
- - CRuby 3.3.7;
232
- - Rust 1.97.0;
233
- - Magnus 0.8.2;
234
- - rb-sys 0.9.128;
235
- - an optimized release profile.
142
+ Representative verification evidence, including its pinned runtime versions,
143
+ is recorded in [VERIFICATION.md](docs/VERIFICATION.md).
236
144
 
237
145
  The test suite covers the native plan, coercion modes, nested data, rules,
238
146
  rule skipping, array rules, macros, options, context, inheritance, external
239
147
  schemas, loading modes, pattern matching, metadata, concurrent calls, malformed
240
148
  input resilience, package contents, and differential compatibility fixtures.
241
149
 
242
- Run the representative benchmark matrix with:
150
+ ## Benchmarking
151
+
152
+ The validation benchmark system has three deliberately separate jobs:
153
+
154
+ 1. an interactive `Benchmark.ips`/`MemoryProfiler` showcase for local
155
+ exploration and screenshots;
156
+ 2. a repeatable publication runner for README, release, post, and CV evidence;
157
+ 3. CI regression gates for detecting performance changes on CI hosts.
158
+
159
+ The detailed protocol and metric wording are documented in
160
+ [`docs/BENCHMARKING.md`](docs/BENCHMARKING.md).
161
+
162
+ ### Interactive comparison
163
+
164
+ Run the representative matrix directly when exploring a change:
243
165
 
244
166
  ```bash
245
167
  ruby -Ilib benchmark/schema_throughput.rb
246
- N=500000 ruby -Ilib benchmark/schema_throughput.rb
247
- ENGINE=rust ruby -Ilib benchmark/schema_throughput.rb
248
- ENGINE=upstream ruby -Ilib benchmark/schema_throughput.rb
249
- SCENARIO=array_of_objects ENGINE=rust ruby -Ilib benchmark/schema_throughput.rb
250
- VALIDATE_KEYS=true SCENARIO=large_form ENGINE=rust ruby -Ilib benchmark/schema_throughput.rb
168
+ ENGINE=rust SCENARIO=medium_form ruby -Ilib benchmark/schema_throughput.rb
169
+ ENGINE=upstream SCENARIO=nested_object ruby -Ilib benchmark/schema_throughput.rb
170
+ IPS_WARMUP=3 IPS_TIME=10 MEMORY_PROFILE_N=5000 \
171
+ SCENARIO=array_of_objects ruby -Ilib benchmark/schema_throughput.rb
172
+ ```
173
+
174
+ Text output uses `Benchmark.ips` for warmed throughput, `MemoryProfiler` for
175
+ Ruby-side allocation detail, and the fixed-run path for peak RSS. A single text
176
+ run is useful evidence while developing, but it is not the canonical source for
177
+ README/CV performance claims.
178
+
179
+ The matrix currently covers eleven validation shapes: small, medium, and large
180
+ flat forms; deep nesting; arrays of nested objects; an all-invalid error path;
181
+ sparse optional data; mixed scalar coercions; a large primitive array; wide
182
+ nested data; and a contract that combines declarative schema work with
183
+ Ruby-owned dynamic rules.
184
+
185
+ ### Publication-quality evidence
186
+
187
+ Use the publication runner before updating benchmark claims:
188
+
189
+ ```bash
190
+ bundle exec rake compile
191
+ bundle exec script/benchmark-publication
192
+ ```
193
+
194
+ By default it performs five independent measurements per engine/scenario. It
195
+ first calibrates the iteration count for each scenario, then runs each engine in
196
+ a fresh Ruby process using an engine-specific calibrated `N`, so a faster engine
197
+ is not accidentally measured for a much shorter interval. Engine order alternates
198
+ between runs and scenario order reverses every other run to reduce systematic
199
+ host drift. Successful results are never discarded automatically. The full
200
+ default matrix has 110 measurements, so it performs about 9 minutes of measured
201
+ work before calibration, warmups, and process startup; the runner reports live
202
+ calibration and measurement progress to stderr.
203
+
204
+ The runner records the exact Git SHA and dirty state, Ruby/platform/CPU details,
205
+ YJIT state, Rust toolchain, requested and actually loaded upstream gem versions,
206
+ protocol settings, every raw measurement, medians,
207
+ ranges, median absolute deviation, paired throughput ratios, Ruby allocation
208
+ changes, and peak RSS changes.
209
+
210
+ Results and checkpoints are written under `tmp/benchmarks/` (already ignored by
211
+ Git). If a run is interrupted, resume it from the printed checkpoint path:
212
+
213
+ ```bash
214
+ RESUME_FROM=tmp/benchmarks/publication-...checkpoint.json \
215
+ bundle exec script/benchmark-publication
216
+ ```
217
+
218
+ For a longer final evidence run:
219
+
220
+ ```bash
221
+ RUNS=7 TARGET_SECONDS=10 bundle exec script/benchmark-publication
222
+ ```
223
+
224
+ `RETRIES` applies only to process/tooling failures. The runner does not retry a
225
+ successful measurement merely because it is slow or weakens the speedup claim.
226
+ Publication mode refuses a dirty working tree unless `ALLOW_DIRTY=true` is set;
227
+ dirty runs are exploratory and should not become canonical README/CV evidence.
228
+
229
+ Use `FORMAT=json` on the lower-level benchmark only when tooling needs one
230
+ single fixed-run payload:
231
+
232
+ ```bash
233
+ FORMAT=json ENGINE=all SCENARIO=small_form \
234
+ N=10000 WARMUP=1000 LATENCY_SAMPLES=500 \
235
+ ruby -Ilib benchmark/schema_throughput.rb
251
236
  ```
252
237
 
253
- The six fixed scenarios cover small (5-field), medium (25-field, 80% valid),
254
- and large (100-field, 50% valid) forms; a 10-level nested object; 100 objects
255
- with five fields each (90% valid); and a 20-field all-invalid case. Each result
256
- includes validations/second, sampled p50/p95/p99 latency, Ruby allocations per
257
- call, and peak process RSS under the sustained run. Use `FORMAT=json` for
258
- machine-readable output, and tune `WARMUP`, `N`, and `LATENCY_SAMPLES` when
259
- collecting evidence.
238
+ ### Process-memory evidence
260
239
 
261
- Refresh the allocation-regression baseline only after intentionally reviewing an
262
- allocation change:
240
+ Ruby allocation counters are useful for understanding GC pressure, but an
241
+ allocated-object count is not a whole-process memory measurement. For a
242
+ same-work comparison of the hybrid and upstream implementations, run:
243
+
244
+ ```bash
245
+ bundle exec rake compile
246
+ bundle exec script/benchmark-memory-footprint
247
+ ```
248
+
249
+ The memory runner uses the same validation count and warmup for both engines in
250
+ each scenario. On Linux it records current/peak RSS plus PSS and USS around the
251
+ timed validation loop. Peak RSS includes resident Ruby and Rust/native memory;
252
+ PSS apportions shared pages and USS reports private resident pages. See
253
+ [`docs/MEMORY_BENCHMARKING.md`](docs/MEMORY_BENCHMARKING.md) for exact metric
254
+ semantics and limitations.
255
+
256
+ These are process-footprint metrics, not cumulative bytes allocated over time.
257
+ Ruby object counts from `GC.stat` remain a separate GC-pressure signal.
258
+
259
+ Ruby allocation counters (`GC.stat` and the interactive `MemoryProfiler`
260
+ section) describe Ruby-side allocation activity. Peak RSS is a whole-process
261
+ resident high-water mark and already includes resident Ruby and Rust/native
262
+ memory, but it is not cumulative allocated bytes and it fully counts shared
263
+ resident pages. Use the separate process-memory runner for same-work RSS/PSS/USS
264
+ comparisons. Throughput ratios apply to validation calls after contract/plan
265
+ construction and must not be presented as end-to-end Rails request speedups.
266
+
267
+ Refresh the allocation-regression baseline only after intentionally reviewing
268
+ an allocation change:
263
269
 
264
270
  ```bash
265
271
  bundle exec script/record-allocation-baseline
@@ -270,13 +276,10 @@ artifact without changing the repository. Review its value before replacing
270
276
  `benchmark/baseline_allocations.json`; do not accept an allocation regression
271
277
  merely by refreshing the baseline.
272
278
 
273
- By default the benchmark compares this Rust-backed hybrid implementation with
274
- the upstream `dry-validation` gem in a separate Ruby process. The upstream gem
275
- is intentionally not a project dependency; install it for the same Ruby with
276
- `gem install dry-validation` before running `ENGINE=all` or `ENGINE=upstream`.
277
- The matrix is a reproducible measurement harness, not a published performance
278
- claim: compare repeated runs on the same machine and report neutral or negative
279
- results alongside favorable ones.
279
+ The upstream `dry-validation` gem remains intentionally outside the project
280
+ dependencies. Install the comparison version for the same Ruby before running
281
+ comparison/publication benchmarks. Report the exact upstream version with every
282
+ published result.
280
283
 
281
284
  ### Plan-compilation benchmark
282
285
 
@@ -318,7 +321,8 @@ The following coercion results were measured locally on 2026-08-14 with CRuby
318
321
  3.4.4, Rust 1.90.0, and `dry-validation-rust` 0.1.0.pre4 on x86_64 Linux
319
322
  (kernel 7.0.0-29-generic, AMD Ryzen 7 5800H). Criterion used 100 samples with
320
323
  a 500 ms warm-up and 1 s measurement period per case. These are host-local
321
- baseline observations, not cross-host performance guarantees. `Infinity` exercises the intentional non-finite-float rejection path; the datetime-shaped date literal exercises the Ruby fallback path.
324
+ baseline observations, not cross-host performance guarantees. `Infinity` exercises the intentional
325
+ non-finite-float rejection path; the datetime-shaped date literal exercises the Ruby fallback path.
322
326
 
323
327
  | Group | Input | Criterion estimate (95% confidence interval) | Point estimate |
324
328
  | ----------------- | ---------------------- | -------------------------------------------: | -------------: |
@@ -410,35 +414,56 @@ performance guarantee.
410
414
  | 100-object array | 318.85–322.47 µs | 320.55 µs |
411
415
  | 20-field invalid | 63.480–64.437 µs | 63.953 µs |
412
416
 
413
- ## Representative benchmark results
414
-
415
- The six default rows were measured on 2026-08-13 with CRuby 3.3.7 on x86_64 Linux (kernel 7.0.0-29-generic, AMD Ryzen 7 5800H), comparing dry-validation-rust 0.1.0.pre4 with dry-validation 1.11.1. The strict-key row remains the 2026-08-10 pre3 measurement. Each `SCENARIO` ran in its own process three times with `N=1000`, `WARMUP=200`, and `LATENCY_SAMPLES=200`; the table shows medians and the throughput range across those runs. Values are evidence for this host and workload only, not a general performance guarantee.
416
-
417
- | `SCENARIO` | Rust validations/s (range) | Upstream validations/s (range) | Throughput ratio | Rust p50/p95/p99 | Upstream p50/p95/p99 |
418
- | ------------------------------ | -------------------------: | -----------------------------: | ---------------: | -------------------------: | -------------------------: |
419
- | `small_form` | 71,433 (67,480–76,023) | 35,157 (32,095–36,674) | 2.03× | 12.9/17.5/63.9 µs | 27.6/36.2/91.8 µs |
420
- | `medium_form` | 10,595 (9,783–11,012) | 2,605 (1,731–2,689) | 4.07× | 38.3/295.9/475.9 µs | 86.0/1,707.8/1,945.3 µs |
421
- | `large_form` | 1,549 (1,475–1,654) | 307 (209–331) | 5.04× | 955.9/1,298.0/2,117.3 µs | 5,440.0/6,719.5/7,119.9 µs |
422
- | `nested_object` | 48,723 (35,219–49,256) | 19,212 (12,892–19,990) | 2.54× | 19.5/29.5/216.5 µs | 51.5/77.1/397.1 µs |
423
- | `array_of_objects` | 1,951 (1,811–2,028) | 781 (743–802) | 2.50× | 455.9/707.2/824.6 µs | 1,207.2/1,594.1/1,979.3 µs |
424
- | `all_invalid` | 4,071 (3,560–4,088) | 772 (730–837) | 5.28× | 218.5/420.6/611.6 µs | 1,209.3/1,582.9/1,918.4 µs |
425
- | `large_form` (`validate_keys`) | 974 (970–1,022) | 304 (303–304) | 3.21× | 1,621.4/2,118.8/2,345.3 µs | 5,432.8/6,300.7/7,223.7 µs |
426
-
427
- | `SCENARIO` | Rust Ruby allocations/call | Upstream Ruby allocations/call | Rust peak RSS | Upstream peak RSS |
428
- | ------------------------------ | -------------------------: | -----------------------------: | ------------: | ----------------: |
429
- | `small_form` | 81.01 | 49.01 | 24.6 MiB | 29.5 MiB |
430
- | `medium_form` | 451.00 | 1,116.80 | 25.4 MiB | 29.9 MiB |
431
- | `large_form` | 2,836.00 | 10,286.00 | 25.6 MiB | 30.4 MiB |
432
- | `nested_object` | 145.00 | 113.00 | 25.8 MiB | 30.5 MiB |
433
- | `array_of_objects` | 3,460.80 | 1,713.60 | 25.8 MiB | 30.8 MiB |
434
- | `all_invalid` | 976.00 | 4,063.00 | 25.8 MiB | 30.9 MiB |
435
- | `large_form` (`validate_keys`) | 2,835.01 | 10,490.01 | 25.3 MiB | 30.4 MiB |
436
-
437
- The Rust path had higher Ruby allocation counts for the small, nested, and array scenarios; this benchmark does not establish a native-allocation total. It measures validation calls after plan construction, so it does not isolate plan-deserialization changes. Peak RSS is process high-water memory, not per-call memory. Reproduce an individual row with, for example:
438
-
439
- ```bash
440
- N=1000 WARMUP=200 LATENCY_SAMPLES=200 ENGINE=all SCENARIO=large_form ruby -Ilib benchmark/schema_throughput.rb
441
- ```
417
+ ## Representative publication results (2026-08-24)
418
+
419
+ The following publication runs compare the hybrid engine with
420
+ dry-validation 1.11.1 (dry-schema 1.16.0 and dry-types 1.9.1) on CRuby 3.3.7,
421
+ x86_64 Linux, and an AMD Ryzen 7 5800H. They are host-local evidence, not a
422
+ cross-host guarantee or an end-to-end Rails request benchmark.
423
+
424
+ The throughput run measured commit `e26bbb18e90f` in seven isolated Ruby
425
+ processes per engine and scenario, targeting 10 seconds each. It measures
426
+ validation calls after contract/plan construction; ranges show the full set of
427
+ successful measurements.
428
+
429
+ | `SCENARIO` | Rust validations/s, median (range) | Upstream validations/s, median (range) | Median speedup (range) |
430
+ | --------------------- | ---------------------------------: | -------------------------------------: | ---------------------: |
431
+ | `small_form` | 100,083 (84,791–101,865) | 40,111 (36,933–41,111) | 2.50× (2.11–2.56) |
432
+ | `medium_form` | 14,517 (13,099–14,657) | 2,792 (2,439–2,921) | 5.14× (4.97–5.51) |
433
+ | `large_form` | 2,140 (1,816–2,153) | 307 (275–344) | 6.73× (6.24–7.36) |
434
+ | `nested_object` | 56,291 (50,939–62,997) | 19,402 (17,589–20,917) | 2.98× (2.90–3.15) |
435
+ | `array_of_objects` | 4,411 (4,202–4,778) | 800 (709–832) | 5.68× (5.33–5.93) |
436
+ | `all_invalid` | 5,397 (5,057–5,625) | 823 (762–887) | 6.44× (6.16–7.38) |
437
+ | `sparse_optional` | 30,623 (29,528–32,315) | 7,345 (7,073–8,073) | 4.12× (3.66–4.41) |
438
+ | `mixed_types` | 38,408 (36,599–39,227) | 15,420 (14,069–16,033) | 2.48× (2.45–2.69) |
439
+ | `array_of_primitives` | 16,656 (15,718–17,299) | 3,220 (3,083–3,272) | 5.20× (4.80–5.32) |
440
+ | `wide_nested_object` | 8,472 (7,993–8,802) | 3,744 (3,440–4,002) | 2.26× (2.06–2.33) |
441
+ | `ruby_rules` | 18,127 (16,774–19,774) | 8,812 (8,487–9,239) | 2.09× (1.93–2.16) |
442
+
443
+ The separate process-memory run measured commit `0f2f46414bc9`, also with
444
+ seven runs and identical validation count/warmup for both engines within each
445
+ scenario. Peak RSS is a whole-process high-water mark during the loop; the
446
+ Linux-only PSS and USS measurements are taken after it. PSS apportions shared
447
+ pages and USS counts private resident pages.
448
+
449
+ | `SCENARIO` | Peak RSS reduction | PSS reduction | USS reduction | Ruby object reduction |
450
+ | --------------------- | -----------------: | ----------------: | ----------------: | --------------------: |
451
+ | `small_form` | 12.9% (12.6–13.2) | 14.8% (14.5–15.0) | 16.2% (15.8–16.4) | -42.9% |
452
+ | `medium_form` | 12.3% (12.2–12.7) | 14.6% (14.3–14.8) | 15.9% (15.6–16.1) | 63.7% |
453
+ | `large_form` | 11.2% (10.7–11.4) | 12.9% (12.2–13.1) | 14.1% (13.5–14.3) | 74.9% |
454
+ | `nested_object` | 13.1% (12.8–13.4) | 15.3% (15.0–15.6) | 16.7% (16.4–17.0) | -13.3% |
455
+ | `array_of_objects` | 13.0% (12.2–13.1) | 15.1% (14.4–15.4) | 16.4% (15.7–16.7) | 3.6% |
456
+ | `all_invalid` | 9.5% (9.0–9.9) | 10.8% (10.2–11.1) | 12.0% (11.4–12.2) | 78.0% |
457
+ | `sparse_optional` | 15.7% (15.6–16.1) | 18.7% (18.2–18.9) | 20.1% (19.7–20.4) | 39.3% |
458
+ | `mixed_types` | 16.0% (15.4–16.2) | 18.8% (18.5–19.1) | 20.2% (19.9–20.6) | -74.3% |
459
+ | `array_of_primitives` | 12.9% (11.9–13.1) | 14.6% (13.4–14.8) | 15.7% (14.5–15.9) | -2.6% |
460
+ | `wide_nested_object` | 14.8% (14.4–15.2) | 17.1% (16.8–17.7) | 18.5% (18.2–19.1) | -179.6% |
461
+ | `ruby_rules` | 11.4% (10.9–12.3) | 13.1% (12.5–14.2) | 14.4% (13.7–15.4) | 34.2% |
462
+
463
+ Ruby object reduction is a `GC.stat` count, not a byte total; negative values
464
+ mean the hybrid path allocated more Ruby objects. None of RSS, PSS, or USS is a
465
+ measure of cumulative allocated bytes. Reproduce fresh evidence with the
466
+ publication runners above rather than extrapolating these host-local figures.
442
467
 
443
468
  ## Important performance caveat
444
469