Skip to content

perf(downgrader): tell a schema met again mid-cycle by the conversions still running - #328

Merged
dinwwwh merged 1 commit into
mainfrom
claude/project-thread-rpneon
Nov 1, 2026
Merged

dinwwwh merged 1 commit into
mainfrom
claude/project-thread-rpneon

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Nov 1, 2026

Copy link
Copy Markdown
Member

Requested by dinwwwh · project thread

Summary

Found while checking main against oRPC before the next release. Since 0.5.0, 3.1 → 3.0 runs about 45% slower on real oRPC documents, and most of that comes from #319. #319 added every converted schema to a DONE WeakSet, so that a schema met again before it was done counts as loose. A CPU profile shows finishSchema self time doubling and more GC, because each output object goes into a WeakSet.

This PR keeps the same rule but tracks it differently. convertObject keeps the outputs whose conversion is running on a stack in the call's Context (pushed when converting starts, popped after finish). It calls revisit only for an output that is still on that stack. revisitSchema then always loosens, and DONE is removed.

The output does not change. An output is on the stack exactly while its conversion (including finishSchema) is running, which is exactly when it was missing from DONE. The stack lives in the per-call Context, so nothing outlives a call, even one that throws.

Measurements

3.1 → 3.0 over 66 real oRPC documents (oRPC's own tests plus Zod 4, Valibot, ArkType and Effect 4 routers), median ms per pass over 8 interleaved processes, Node 22:

ms
0.5.0 (c599c27) 9.97
main (f065d30) 14.7
this PR 11.15

The remaining gap to 0.5.0 is spread across #309 to #318 (each about 0.3 to 1 ms, inside the noise).

Test plan

  • pnpm run check, pnpm test (361 pass), coverage still 100%.
  • Output equality against main's build: 200,000 random 3.1 documents from the loosen-only fuzzer (FUZZ_WIDE on and off, 4 seeds) plus the 66 oRPC documents: 0 differences. The same harness finds 737 to 3,164 differences for two mutants (revisit never loosens, or always loosens), so it does reach cycles.

Generated by Claude Code

…s still running

#319 marked every converted schema done in a WeakSet, so a schema met
again before that counts as loose. Adding each output to a WeakSet made
3.1 to 3.0 about 45% slower than 0.5.0 on real oRPC documents.

convertObject now keeps the outputs whose conversion is running on a
per-call stack, and calls `revisit` only for one still on it. The output
is the same: an output is on the stack exactly while it is not done.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7PYvCYrJkB3NF1rgbx
@dinwwwh dinwwwh self-assigned this Nov 1, 2026
@codecov

codecov Bot commented Nov 1, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Nov 1, 2026

Copy link
Copy Markdown

Merging this PR will improve performance by ×2.5

2 warnings

⚠️ 1 benchmark is dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

⚠️ 4 benchmarks with a significant change ran on different hardware

Their base and head results were measured on machines with a different CPU, core count or memory, so the changes might not come from the code. Run base and head on the same runner type.

Learn more about Macro Runners

See affected benchmarks

⚡ 4 improved benchmarks
✅ 25 untouched benchmarks
⏩ 2 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
⚡ property schema 1,030.3 µs 128.8 µs ×8
⚡ dereferenced diamond, 64 levels 1,858.9 µs 925.5 µs ×2
⚡ order schema with $defs 2.3 ms 1.4 ms +66.08%
⚡ reference diamond into removed webhooks, 64 levels 3.2 ms 2.3 ms +41.59%

Tip

Curious why performance improved? Comment @codspeedbot explain why performance improved on this PR, or directly use the CodSpeed MCP with your agent.


Comparing claude/project-thread-rpneon (bd732b0) with main (f065d30)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

  • Cycle detection moved from DONE WeakSet to a Context.converting stack — convertObject pushes each output just after seen.set and pops it after finish, and the memoized-hit branch loosens only when the met-again output is still on that stack; DONE and its finishSchema write are removed.
  • revisitSchema now loosens unconditionally — the on-stack filter in convertObject replaces the old !DONE.has(out) guard.

I traced the equivalence: the old "missing from DONE" window runs from output creation to the end of finishSchema (where DONE.add sat), and the new on-stack window runs from just after seen.set to just after finish, with no callback in between — so ctx.converting.includes(known) matches !DONE.has(known) at every memoized hit. revisitSchema is only supplied by convertSchema, and SCHEMA_FIELDS is only used there, so the memoized known is always a schema output finished by finishSchema, which is exactly what the old check assumed. The stack is shared across inlined { ...ctx, seen } contexts by reference, preserving the old WeakSet's context-wide reach, and push/pop stay balanced except on throw, where the per-call Context is discarded. vitest run packages/downgrader passes (302 tests), and no stale DONE references remain.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@dinwwwh
dinwwwh merged commit 9c8668f into main Nov 1, 2026
11 checks passed
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