Skip to content

perf: reuse flattened anyOf and oneOf alternatives - #300

Open
Kaileshwar16 wants to merge 4 commits into
hyperjump-io:mainfrom
Kaileshwar16:perf/295-alternative-flattening
Open

Kaileshwar16 wants to merge 4 commits into
hyperjump-io:mainfrom
Kaileshwar16:perf/295-alternative-flattening

Conversation

@Kaileshwar16

Copy link
Copy Markdown
Contributor

Refs #295.

Ihave reused the flattened alternative results across filtering, validity checks, and error generation

The change is kept small:

Performance and validation:

  • The large generated oneOf case improved by roughly 5 to 8%.
  • All 1406 tests pass.
  • Lint and type checks pass.

Tradeoffs:

The change adds a little helper indirection and potentially more temporary memory during multiple-match handling. Smaller cases showed no consistent performance improvement or regression, while the larger nested case improved by roughly 5–8%. so this isn't a general speedup. I haven't measured the memory impact yet.

your thoughts on whether the gain is worth those tradeoffs

Kaileshwar16 and others added 4 commits October 22, 2026 18:20
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@jdesrosiers jdesrosiers left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, about 20–30% faster than main on oneOf-heavy schemas, with identical output.

I added three small commits:

  • Fixed oneOf treating alternatives with no output as matches (an older bug on a line this PR rewrote)
  • Gave flattened output its own type so unflattened output can't be passed by mistake
  • Reused first-pass errors in the oneOf fallback

Could you try a follow-up that caches flattenOutput instead? Store each flattened result in a WeakMap keyed on the NormalizedOutput object. That would cover every caller that flattens the same output more than once, not just anyOf/oneOf. For example, contains.js calls getValidity three times per item, and the evaluator calls it on every simple-applicator suboutput. It would also let us remove the *FromFlattenedOutput variants and the FlattenedOutput type. A few things to watch:

  • flatten runs in two modes (isApplied for errors and validity, isRequired for getSuccesses), so each mode needs its own cache.
  • The cache is only safe if nothing modifies a normalized output after it's first flattened.
  • Please benchmark it against this PR with a schema that exercises contains and nested allOf/$ref, since a cache lookup on every call isn't free.

@Kaileshwar16

Copy link
Copy Markdown
Contributor Author

@jdesrosiers thanks for the review, Would you prefer I open a separate PR for that or push it to this one once I have the numbers

@jdesrosiers

Copy link
Copy Markdown
Collaborator

I think it's really more of an alternative approach than a follow up. So, if the alternate approach ends up being better, it probably makes sense for it to be a separate PR and we choose which one we like better and close the other.

@Kaileshwar16

Copy link
Copy Markdown
Contributor Author

@jdesrosiers Thanks for the WeakMap suggestion. I tried it on a separate local branch and compared it with your latest version of this PR.
The output was the same in all seven benchmark cases, and the tests, lint, and type checks passed. But the WeakMap version was slower in six of the seven cases. Its small improvement on nested allOf was within the variation between runs. I also found that caching could return stale results if someone changes a normalized output after it has been flattened.

Given that, I think current PR is the better option. I’ve kept the WeakMap version local rather than opening a competing PR.

attached the benchmark for reference

Case Main PR #300 WeakMap WeakMap vs main WeakMap vs PR #300
Large oneOf 7.247 (6.002–8.565) 6.547 (5.692–7.577) 6.964 (5.946–8.794) +3.9% −6.4%
Many anyOf 4.381 (3.755–6.437) 3.906 (3.490–4.572) 4.650 (3.695–6.199) −6.1% −19.1%
contains, many items 2.220 (1.979–3.003) 2.190 (1.945–3.190) 2.379 (2.156–3.353) −7.1% −8.7%
Deep allOf 0.289 (0.236–0.385) 0.291 (0.240–0.364) 0.278 (0.247–0.325) +3.9% +4.5%
Nested $ref 0.231 (0.172–0.475) 0.208 (0.178–0.321) 0.250 (0.194–0.286) −8.3% −20.2%
contains + allOf + $ref 3.095 (2.620–3.570) 2.788 (2.427–3.176) 3.057 (2.423–3.931) +1.2% −9.7%
Small schema 0.050 (0.042–0.068) 0.041 (0.036–0.058) 0.043 (0.040–0.054) +14.4% −3.6%

This branch has not been deployed

No deployments
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