Streaming RawJson deserialize + hot-path cleanups (1.15-1.60x round-trip)#18
Merged
Merged
Conversation
Round-trip end-to-end measured on a paired-pipe dispatch loop (5000 iterations): tiny 1.15x, medium 1.21x, large (3.5KB) 1.60x. Phase 2 — RawJson wrapper + streaming deserialize - New io.moderne.jsonrpc.RawJson: library-owned wrapper holding either a Jackson TokenBuffer (inbound, lazy materialization) or a POJO (outbound). Keeps Jackson out of the public ABI. - JsonRpcRequest.params and JsonRpcSuccess.result change from Object to RawJson. JsonRpcRequest.newRequest(method, POJO) factory keeps working. - JsonMessageFormatter.deserialize: full streaming via JsonParser + TokenBuffer, no intermediate JsonNode tree. convertValue unwraps RawJson for either POJO or TokenBuffer. Phase 3 — hot-path cleanups - JsonRpcMethod: paramType resolved once in the constructor instead of per dispatch via getClass().getGenericSuperclass(). - JsonRpcIdDeserializer: direct JsonToken inspection, no JsonNode allocation per inbound id. - internal/SnowflakeId: lock-free generation via single packed AtomicLong (timestamp << SEQUENCE_BITS | sequence) preserving millisecond monotonicity. New concurrency test in SnowflakeIdTest verifies no duplicates under contention. - HeaderDelimitedMessageHandler / NewLineDelimitedMessageHandler: wrap input in BufferedInputStream when the caller hasn't already. - MeteredMessageHandler: pre-build the four no-error Timers in the constructor; cache error-tagged Timers in a bounded ConcurrentHashMap (cap 64) to avoid the cardinality trap of unbounded per-method tags.
This was referenced May 7, 2026
jkschneider
added a commit
that referenced
this pull request
May 10, 2026
…ackson 2.21 (#21) The Blackbird default registered in #20 was benched against Jackson 2.17.2, where it showed a ~2x deserialize win. moderne-cli (the primary consumer) and most current openrewrite components have moved to Jackson 2.21, where Blackbird no longer wins on deserialize and *regresses* serialize by ~70% on the real-trace JMH bench (vanilla 317±19 µs vs Blackbird 540±31 µs on a 303 KB GetObject payload — non-overlapping CIs). End-to-end this manifests as visibly slower `mod run` in production after 1.0.8 shipped: BatchVisit p50/p99 times rose 4-12x / 4-65x compared to the 1.0.7 baseline on the JS Applications working set. While here, bump jackson-module-parameter-names from 2.17.2 to 2.21.1 to match what consumers force via their version-alignment rules; this keeps the BOM resolution stable and avoids accidental version skew at runtime. The streaming TokenBuffer-based deserialize from #18 stays — that's the deserialize-side win Blackbird was attempting to chase, already shipped.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two compounding optimizations on the hot path of
io.moderne.jsonrpc, the IPC layer for OpenRewrite multi-language work. Round-trip end-to-end measured on a paired-pipe dispatch loop (5000 iterations):Caveat: artificial bench. Real-workload (JFR on a real OpenRewrite recipe) deferred — see Test plan.
Phase 2 — RawJson wrapper + streaming deserialize
io.moderne.jsonrpc.RawJson: library-owned wrapper holding either a JacksonTokenBuffer(inbound, lazy materialization) or a POJO (outbound). Keeps Jackson out of the public ABI.JsonRpcRequest.paramsandJsonRpcSuccess.resultchange fromObjecttoRawJson.JsonRpcRequest.newRequest(method, POJO)factory keeps working.JsonMessageFormatter.deserialize: full streaming viaJsonParser+TokenBuffer, no intermediateJsonNodetree.convertValueunwrapsRawJsonfor either POJO or TokenBuffer.The streaming path moves cost from RECEIVE → DISPATCH (deferred conversion). On large payloads RECEIVE p50 dropped 64.9 → 46.6 μs (1.39×) while DISPATCH p50 rose 18.8 → 22.7 μs — net win is the streaming saving.
Phase 3 — Hot-path cleanups
JsonRpcMethod— paramType resolved once in the constructor instead of per dispatch viagetClass().getGenericSuperclass().JsonRpcIdDeserializer— directJsonTokeninspection, noJsonNodeallocation per inbound id.internal/SnowflakeId— lock-free generation via single packedAtomicLong(timestamp << SEQUENCE_BITS | sequence), preserving millisecond monotonicity. New concurrency test inSnowflakeIdTest.HeaderDelimitedMessageHandler/NewLineDelimitedMessageHandler— wrap input inBufferedInputStreamwhen the caller hasn't already.MeteredMessageHandler— pre-build the four no-error Timers in the constructor; cache error-tagged Timers in a boundedConcurrentHashMap(cap 64) to avoid the cardinality trap of unbounded per-method tags.ABI note
JsonRpcRequest.paramsandJsonRpcSuccess.resultare now typedRawJsonrather thanObject. Consumers that previously cast toMapwill get a deterministic compile-time error rather than silent misbehavior. Major-version bump expected.Test plan
./gradlew testpassesSnowflakeIdTestconfirms no duplicate ids under concurrent generationJsonRpcTestround-trip suite passes against the streaming deserializer