Skip to content

Generate JSON codecs at runtime - #1354

Open
adwsingh wants to merge 1 commit into
adwsingh/runtime-codegen-serde-http-bindingfrom
adwsingh/runtime-codegen-serde-json
Open

adwsingh wants to merge 1 commit into
adwsingh/runtime-codegen-serde-http-bindingfrom
adwsingh/runtime-codegen-serde-json

Conversation

@adwsingh

@adwsingh adwsingh commented Sep 7, 2026

Copy link
Copy Markdown
Contributor

What behavior changes?

  • JsonCodec can now generate per-shape serializers and deserializers as bytecode at runtime. Generation is opt-in via JsonCodec.builder().runtimeCodegen(true) or the smithy-java.runtime-codegen / smithy-java.runtime-codegen.json system property, and requires JDK 25+. Default behavior is unchanged.
  • Two modes govern shapes that cannot be generated: enabled falls back transparently to the dispatch serde, and strict throws instead of falling back. The property and a builder's explicit setting combine by severity: disabled on either side wins, then strict escalates. The property is therefore both the default for codecs that set nothing and an operational kill switch for codecs that opted in through code.
  • Generated programs are defined as hidden classes and cached per (schema, settings) in the shared registry from Add shared runtime codegen serde machinery #1353.
  • restJson1 bodies reach generated programs through MemberSubsetCodec bridging on both the serialize and deserialize sides, so the HTTP binding's member-subset body path is covered in addition to full-struct codecs.
  • The stock JSON JMH benchmarks gain an implementation={generic,generated} param that A/Bs this same production path through RestJsonClientProtocol and AwsJson1Protocol (run with -Pjmh.implementation=generic,generated).

Why is this change needed?

Performance. The dispatch serde pays per-member schema dispatch through megamorphic call sites, repeated member-name handling, and intermediate allocation on every operation. A generated program per shape is straight-line, monomorphic code with pre-encoded field-name bytes. The CPU-efficiency gains run ahead of the latency gains because lower allocation also cuts GC work per operation.

How was this validated?

  • DifferentialRuntimeCodegenJsonFuzzTest: the generated and interpreted codecs produce byte-identical output and fail on identical inputs across the fuzz corpus. The codegen arm runs in strict mode, so a silent fallback fails the fuzz instead of quietly comparing dispatch against dispatch.
  • RuntimeJsonCodegenTest and RuntimeCodecRegistryTest: generation across shape types, caching, strict semantics, and property handling.
  • RuntimeCodegenStrictCoverageTest: every benchmark case drives the real client protocols under strict mode, so a backend change that stops generating for a benchmark shape fails CI rather than silently skewing a benchmark run.
  • Existing protocol test suites pass with the provider enabled.
  • JMH A/B on EC2 (below).

Benchmark Results

Environment: m7i.xlarge, Corretto 25 (JDK 25), G1GC, 1G heap, nanosecond precision. Latency columns are p50 and p99 ns/op from JMH sample mode, single fork, both arms in one run via -p implementation=generic,generated, driving the real protocol entry points. ops/CPU-s is operations per process CPU-second (a custom profiler; it includes GC threads, so allocation savings show up here); higher is better. The generated arm flips this PR's JSON backend together with the generated HTTP binding pass from #1358; the two switch as a unit.

Summary

Protocol / Direction             p50    p99   ops/CPU-s
──────────────────────────────────────────────────────────────────────────
restJson1 deserialization       -22%   -15%   █████████████  +29%
restJson1 serialization         -14%    -7%   ███████        +16%
awsJson1.0 deserialization      -17%   -26%   █████████      +20%
awsJson1.0 serialization        -11%    -6%   █████          +13%

restJson1 deserialization

Benchmark                      Generic Generated    p50    p99   ops/CPU-s
────────────────────────────────────────────────────────────────────────────────────────────────
CopyObjectOutput_M                 767       551   -28%   -47%   ███████████              +48%
WideTypesResponse_L              4,616     3,336   -28%   -42%   █████████                +39%
CopyObjectOutput_OutOfOrder      1,228       900   -27%   -40%   █████████                +37%
WideTypesResponse_M              2,448     1,796   -27%   -15%   ████████                 +34%
WideTypesResponse_S              1,366     1,060   -22%   -44%   ███████                  +29%
CopyObjectOutput_Baseline           75        79    +5%    -9%   ██                        +9%
GetObject_M                        189       175    -7%    +9%   █                         +6%
GetObject_L                        192       190    -1%    +9%   ░                         -0%
GetObject_S                        172       178    +3%    -4%   ░                         -2%

restJson1 serialization

Benchmark                      Generic Generated    p50    p99   ops/CPU-s
────────────────────────────────────────────────────────────────────────────────────────────────
WideTypesRequest_L               2,980     2,104   -29%   -26%   ██████████               +41%
WideTypesRequest_M               1,588     1,168   -26%   -10%   █████████                +37%
PutMetricDataRequest_S             212       161   -24%   -26%   ████████                 +34%
WideTypesRequest_S                 918       752   -18%   +11%   █████                    +23%
PutObject_M                        180       155   -14%    -7%   ████                     +19%
PutMetricDataRequest_M             940       810   -14%   -26%   ████                     +16%
PutMetricDataRequest_L           8,192     7,328   -11%   -29%   ███                      +13%
PutObject_L                        174       153   -12%    -2%   ███                      +12%
PutObject_S                        163       152    -7%    +3%   ██                        +7%
CopyObjectRequest_M                842       810    -4%    +5%   ░                         +2%
CopyObjectRequest_Baseline         187       183    -2%    -1%   ░                         +1%

awsJson1.0 deserialization

Benchmark                      Generic Generated    p50    p99   ops/CPU-s
────────────────────────────────────────────────────────────────────────────────────────────────
HealthcheckResponse_Example         51        39   -24%   -46%   ███████████████████████  +99%
GetItemOutput_Baseline              51        41   -20%   -52%   ███████████████          +66%
WideTypesResponse_L              4,200     3,336   -21%   -28%   ██████                   +26%
GetItemOutputBinary_L           18,080    14,496   -20%   -19%   ██████                   +25%
GetItemOutput_OutOfOrder         2,592     2,148   -17%   -23%   █████                    +21%
WideTypesResponse_M              2,164     1,788   -17%   -10%   █████                    +21%
GetItemOutputBinary_S              602       506   -16%   -39%   █████                    +20%
GetItemOutput_S                    559       477   -15%   -34%   ████                     +19%
GetItemOutputBinary_M            2,572     2,204   -14%   -20%   ████                     +17%
GetItemOutput_M                  2,232     1,932   -13%   -20%   ████                     +16%
GetItemOutput_L                 14,800    12,912   -13%   -33%   ████                     +16%
WideTypesResponse_S              1,084     1,092    +1%    -9%   ░                         -1%

awsJson1.0 serialization

Benchmark                      Generic Generated    p50    p99   ops/CPU-s
────────────────────────────────────────────────────────────────────────────────────────────────
PutItemRequest_Nested_L            867       530   -39%   -43%   ███████████████          +63%
PutItemRequest_ShallowMap_S        395       256   -35%   -45%   ██████████████           +58%
PutItemRequest_Nested_M            294       208   -29%   -27%   ████████████             +49%
PutItemRequest_ShallowMap_M      1,748     1,324   -24%    +7%   ████████                 +32%
PutItemRequest_ShallowMap_L      5,400     4,040   -25%   -16%   ███████                  +32%
WideTypesRequest_L               2,444     2,048   -16%   -15%   █████                    +20%
PutItemRequest_MixedItem_S         416       352   -15%    +4%   █████                    +20%
PutItemRequest_MixedItem_M       1,794     1,574   -12%    +5%   ████                     +15%
PutItemRequest_BinaryData_S        183       166    -9%   -31%   ███                      +13%
PutItemRequest_MixedItem_L       5,144     4,560   -11%    -1%   ███                      +12%
WideTypesRequest_M               1,150     1,034   -10%    +8%   ██                       +10%
WideTypesRequest_S                 674       624    -7%   -25%   ██                        +8%
GetItemInput_Baseline              140       129    -8%   -12%   █                         +5%
PutItemRequest_BinaryData_M      1,484     1,414    -5%    -4%   █                         +5%
PutItemRequest_BinaryData_L     12,032    11,584    -4%    -6%   █                         +5%
PutItemRequest_Baseline            135       131    -3%    -3%   ░                         +2%
HealthcheckRequest_Example         105       108    +3%    +1%   ░                        -10%

Reading notes: the GetObject_* response cases and the Baseline/CopyObjectRequest* rows run identical or near-identical code on both arms (payload bodies bypass the codec and empty bodies are dropped before it), so they calibrate this run's noise floor: roughly ±7% on p50 and ±10% on p99, which is the noisiest column at this run length. WideTypesResponse_S under awsJson1.0 reads flat while its M and L siblings read -17% and -21%; treat it as a noisy fork pending a full-rigor rerun. HealthcheckRequest_Example (+3%) shows the real cost profile: a small fixed overhead that only matters on a near-empty serialize. Median ops per process CPU-second (which includes GC threads) improved +18.6% across the 49 cases.

What should reviewers focus on?

  • The fallback boundaries: enabled must never change observable behavior, and strict must never silently fall back.
  • JsonRuntimeCodegenBackend: how each shape type is lowered, and where it declines.
  • The MemberSubsetCodec bridging in both directions, which is what lets restJson1 bodies use generated programs.

Additional Links


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@adwsingh
adwsingh marked this pull request as draft September 7, 2026 19:39
@adwsingh
adwsingh force-pushed the adwsingh/runtime-codegen-serde-json branch from fd3bddb to 7570e75 Compare September 7, 2026 20:13
@adwsingh
adwsingh force-pushed the adwsingh/runtime-codegen-serde-json branch 2 times, most recently from 32e16fe to 5e1f35a Compare September 7, 2026 21:11
@adwsingh
adwsingh force-pushed the adwsingh/runtime-codegen-serde-json branch from 5e1f35a to 7bff019 Compare September 7, 2026 21:21
@adwsingh
adwsingh changed the base branch from adwsingh/runtime-codegen-serde-common to adwsingh/runtime-codegen-serde-http-binding September 7, 2026 21:25
@adwsingh
adwsingh force-pushed the adwsingh/runtime-codegen-serde-json branch from 7bff019 to 574f406 Compare September 7, 2026 22:59
@adwsingh
adwsingh force-pushed the adwsingh/runtime-codegen-serde-json branch from 574f406 to b020581 Compare September 7, 2026 23:06
@adwsingh
adwsingh force-pushed the adwsingh/runtime-codegen-serde-json branch from b020581 to 42c958b Compare September 7, 2026 23:15
@adwsingh
adwsingh force-pushed the adwsingh/runtime-codegen-serde-json branch from 42c958b to 028dc06 Compare September 8, 2026 00:10
@adwsingh
adwsingh force-pushed the adwsingh/runtime-codegen-serde-json branch from 028dc06 to dafc3e1 Compare September 8, 2026 10:56
@adwsingh
adwsingh force-pushed the adwsingh/runtime-codegen-serde-json branch 2 times, most recently from 2817e78 to f2040a9 Compare September 8, 2026 15:11
@adwsingh
adwsingh force-pushed the adwsingh/runtime-codegen-serde-json branch from f2040a9 to e5547df Compare September 8, 2026 18:46
@adwsingh
adwsingh force-pushed the adwsingh/runtime-codegen-serde-json branch 3 times, most recently from a5fe3f1 to 157af5c Compare September 9, 2026 00:28
@adwsingh
adwsingh force-pushed the adwsingh/runtime-codegen-serde-json branch from 157af5c to 87f08fc Compare September 9, 2026 00:42
@adwsingh
adwsingh force-pushed the adwsingh/runtime-codegen-serde-json branch from 87f08fc to 701e0a6 Compare September 9, 2026 00:50
@adwsingh
adwsingh marked this pull request as ready for review September 9, 2026 00:55
@adwsingh
adwsingh force-pushed the adwsingh/runtime-codegen-serde-json branch from 701e0a6 to 9232969 Compare September 9, 2026 10:55
@adwsingh
adwsingh force-pushed the adwsingh/runtime-codegen-serde-json branch from 9232969 to 0f00c7d Compare September 9, 2026 11:27
@adwsingh
adwsingh force-pushed the adwsingh/runtime-codegen-serde-json branch from 0f00c7d to 97d45c1 Compare September 9, 2026 11:28
@adwsingh
adwsingh force-pushed the adwsingh/runtime-codegen-serde-json branch from 97d45c1 to 701e0a6 Compare September 9, 2026 12:18
@adwsingh
adwsingh force-pushed the adwsingh/runtime-codegen-serde-json branch from 701e0a6 to 3dd70b7 Compare September 9, 2026 18:50
@adwsingh
adwsingh force-pushed the adwsingh/runtime-codegen-serde-json branch from 3dd70b7 to ef51fe6 Compare September 9, 2026 19:55
@adwsingh
adwsingh force-pushed the adwsingh/runtime-codegen-serde-json branch from ef51fe6 to 6bba6d4 Compare September 10, 2026 18:02
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.

1 participant