Register Blackbird module by default in JsonMessageFormatter#20
Merged
Conversation
Jackson's reflective property access (MethodHandleObjectFieldAccessorImpl) shows up as a meaningful share of CPU on real OpenRewrite RPC traffic when deserializing GetObject responses with many-keyed nested Maps. Registering jackson-module-blackbird swaps the reflective accessors for LambdaMetafactory-generated ones with no protocol or schema change. Measured on a JMH bench replaying a captured trace from mod run org.openrewrite.node.migrate.upgrade-node-24 against the JavaScript Applications working set: roughly 1.5-2x on serialize/deserialize for the max-size GetObject payloads. Synthetic NO_CHANGE-heavy fixtures see no benefit, which is consistent with reflection cost scaling with field count. rewrite-rpc test suite passes against this snapshot (RewriteRpcTest 17/17, RpcSendQueueTest 3/3, RpcReceiveQueueTest 5/5).
2 tasks
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
Register
jackson-module-blackbirdin the defaultJsonMessageFormatterbuilder. Blackbird generatesLambdaMetafactory-backed property accessors so Jackson skips its reflectiveMethodHandlepath. No protocol or schema change.Why
Profiling a real
mod runoforg.openrewrite.node.migrate.upgrade-node-24against the JavaScript Applications working set showed Jackson serialize/deserialize as a meaningful share of Java-side CPU on the RPC path — concentrated inBeanPropertyWriter.serializeAsField,BeanDeserializer.deserializeFromObject, andConcurrentHashMap.get(the per-type serializer cache). The hot deserialize path is GetObject responses whosevaluefield is a many-keyed nestedMap, and field-count is exactly what the reflective accessor pays for.Bench
JMH bench replaying a 100MB captured trace (5,661 frames; 1,266
GetObjectresponses; p99 message size 45KB; max 303KB):deserialize_max(303KB)serialize_maxdeserialize_median(173B)Synthetic NO_CHANGE-heavy fixtures saw no benefit, which is consistent with reflection cost scaling with field count: the small messages are too small to matter, the big ones are where Blackbird helps.
Error bars on the max-payload runs are wide (single 303KB allocation per iteration → GC noise), but every measurement put Blackbird ahead. The bench harness lives in a follow-up branch and the captured trace is reproducible by setting
JSONRPC_CAPTURE_PATHduring amod run.Tradeoffs considered
mod run. For very short invocations (mod --version) the AOT cache won't include the generated classes, so each run re-pays the small bytecode-gen cost.RpcObjectDatauses a slightly unusual Lombok@Value+ private@JsonCreator+ field-visibility deserialization pattern; the rewrite-rpc suite passes against this snapshot.Test plan
./gradlew test) passRewriteRpcTest: 17 tests, 0 failures (2 skipped)RpcSendQueueTest: 3/3 passRpcReceiveQueueTest: 5/5 passmod runend-to-end on a representative recipe and confirm no behavioral diff vs current main