From 59fbf4125210ca86a917755a266a293d60ae7338 Mon Sep 17 00:00:00 2001 From: Jonathan Schneider Date: Sun, 10 May 2026 11:15:16 -0400 Subject: [PATCH] Register Blackbird module by default in JsonMessageFormatter 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). --- build.gradle.kts | 6 ++++++ .../io/moderne/jsonrpc/formatter/JsonMessageFormatter.java | 5 +++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 1e37d2b..802c93f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -16,6 +16,12 @@ dependencies { implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310") compileOnly("io.micrometer:micrometer-core:latest.release") implementation("com.fasterxml.jackson.module:jackson-module-parameter-names:2.17.2") + // Blackbird generates LambdaMetafactory-backed property accessors so + // Jackson skips the reflective MethodHandle path. ~1.5-2x on real RPC + // traffic per a JMH bench replaying a captured trace from `mod run` + // org.openrewrite.node.migrate.upgrade-node-24 — measurable on the + // GetObject deserialize path where field counts in nested Maps are high. + implementation("com.fasterxml.jackson.module:jackson-module-blackbird:2.17.2") testImplementation("org.openrewrite:rewrite-test:latest.release") } diff --git a/src/main/java/io/moderne/jsonrpc/formatter/JsonMessageFormatter.java b/src/main/java/io/moderne/jsonrpc/formatter/JsonMessageFormatter.java index bbfd205..209072d 100644 --- a/src/main/java/io/moderne/jsonrpc/formatter/JsonMessageFormatter.java +++ b/src/main/java/io/moderne/jsonrpc/formatter/JsonMessageFormatter.java @@ -23,6 +23,7 @@ import com.fasterxml.jackson.databind.cfg.ConstructorDetector; import com.fasterxml.jackson.databind.json.JsonMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.fasterxml.jackson.module.blackbird.BlackbirdModule; import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; import io.moderne.jsonrpc.JsonRpcError; import io.moderne.jsonrpc.JsonRpcMessage; @@ -44,7 +45,7 @@ public JsonMessageFormatter() { // see https://cowtowncoder.medium.com/jackson-2-12-most-wanted-3-5-246624e2d3d0 .constructorDetector(ConstructorDetector.USE_PROPERTIES_BASED) .build() - .registerModules(new ParameterNamesModule(), new JavaTimeModule()) + .registerModules(new ParameterNamesModule(), new JavaTimeModule(), new BlackbirdModule()) .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) .setSerializationInclusion(JsonInclude.Include.NON_NULL)); mapper.setVisibility(mapper.getSerializationConfig().getDefaultVisibilityChecker() @@ -60,7 +61,7 @@ public JsonMessageFormatter(com.fasterxml.jackson.databind.Module... modules) { // see https://cowtowncoder.medium.com/jackson-2-12-most-wanted-3-5-246624e2d3d0 .constructorDetector(ConstructorDetector.USE_PROPERTIES_BASED) .build() - .registerModules(new ParameterNamesModule(), new JavaTimeModule()) + .registerModules(new ParameterNamesModule(), new JavaTimeModule(), new BlackbirdModule()) .registerModules(modules) .disable(SerializationFeature.FAIL_ON_EMPTY_BEANS) .setSerializationInclusion(JsonInclude.Include.NON_NULL));