Skip to content

Precompute serialization flags; leave @LoadOnly out of the encode order (plan item 3.6) - #4408

Open
evanchooly wants to merge 1 commit into
masterfrom
serialization-flags
Open

evanchooly wants to merge 1 commit into
masterfrom
serialization-flags

Conversation

@evanchooly

Copy link
Copy Markdown
Member

Plan item 3.6 (review B11/A7).

The problem

For every property of every encoded entity, MorphiaPropertySerialization.shouldSerialize:

  • read storeNulls, ignoreFinals and storeEmpties from the config (Boolean getters with defaults);
  • then scanned the property's annotations, calling annotationType() on each, which is a JDK proxy call for reflection-mapped annotations, just to find @LoadOnly.

All of that is fixed once the property is mapped. The encoder also visited @LoadOnly properties that it would never write, and wrapped every document in a capturing lambda.

Change

  • Precomputed flags. MorphiaPropertySerialization works out storeNulls, storeEmpties and neverSerialized (@LoadOnly, or a final field under ignoreFinals) in its constructor. MorphiaConfig is immutable and the annotations are fixed by then, so the results are identical. shouldSerialize is now a few boolean checks plus the empty-collection test.
  • Encode order. CodecPlan.encodeOrder (from 3.0) also leaves out @LoadOnly properties. It already used == for the id, which the plan item also asked for.
  • No lambda. EntityEncoder.encode writes the document's start and end directly instead of passing a capturing lambda to CodecHelper.document.

Tests

  • TestCodecPlanEncodeOrder runs under both mappers on HarnessRenames. The encode order is the model's properties minus the id and the @LoadOnly legacy property, in model order.
  • The harness's four fixture variants already pin shouldSerialize behavior under every storeNulls/storeEmpties combination, plus @LoadOnly, and are unchanged.
  • The core and Kotlin suites pass with -Dmorphia.mapper=critter and with reflection: 1494 + 5 tests, 0 failures.

JMH

Expect a small gain. The review estimated "a few ns per property". This change is encode-only, and decode code is untouched.

Full CodecBenchmark, master (7bcb775f4b0) vs this branch

Same machine and settings as the earlier items, before and after back to back.

reflection

benchmark model before ns/op after ns/op Δ time before B/op after B/op Δ alloc
decode ARRAYS 71,154 ± 1,650 70,570 ± 1,201 -0.8% 74,858 74,858 -0.0%
decode COLLECTIONS 8,720 ± 142 9,062 ± 356 +3.9% 16,704 16,696 -0.0%
decode CONCRETE_COLLECTIONS 2,956 ± 60 3,064 ± 54 +3.7% 6,136 6,128 -0.1%
decode LAZY_REFERENCE 3,293 ± 118 3,444 ± 94 +4.6% 7,315 7,293 -0.3%
decode LIFECYCLE 1,111 ± 21 1,083 ± 14 -2.5% 2,840 2,840 -0.0%
decode NESTED 3,018 ± 100 2,987 ± 50 -1.0% 5,656 5,672 +0.3%
decode POLYMORPHIC 5,297 ± 99 5,406 ± 101 +2.1% 9,536 9,544 +0.1%
decode SIMPLE 1,194 ± 63 1,167 ± 86 -2.2% 2,128 2,136 +0.4%
decodeInCursor ARRAYS 73,808 ± 1,382 70,029 ± 1,333 -5.1% 74,546 74,546 -0.0%
decodeInCursor COLLECTIONS 9,266 ± 510 9,049 ± 102 -2.3% 16,384 16,376 -0.0%
decodeInCursor CONCRETE_COLLECTIONS 3,063 ± 131 3,020 ± 72 -1.4% 5,840 5,840 +0.0%
decodeInCursor LAZY_REFERENCE 3,479 ± 156 3,386 ± 71 -2.7% 7,027 7,027 +0.0%
decodeInCursor LIFECYCLE 1,242 ± 38 1,189 ± 23 -4.3% 2,528 2,528 -0.0%
decodeInCursor NESTED 2,959 ± 105 3,081 ± 86 +4.1% 5,344 5,344 +0.0%
decodeInCursor POLYMORPHIC 5,849 ± 167 5,408 ± 99 -7.5% 9,216 9,216 -0.0%
decodeInCursor SIMPLE 1,171 ± 81 1,221 ± 31 +4.3% 1,824 1,832 +0.4%
encode ARRAYS 109,617 ± 13,020 105,254 ± 3,797 -4.0% 135,598 135,587 -0.0%
encode COLLECTIONS 7,838 ± 107 7,775 ± 87 -0.8% 7,336 7,336 +0.0%
encode CONCRETE_COLLECTIONS 2,200 ± 118 2,158 ± 35 -1.9% 3,115 3,104 -0.3%
encode LAZY_REFERENCE 1,909 ± 46 1,863 ± 31 -2.4% 3,843 3,832 -0.3%
encode LIFECYCLE 850 ± 15 862 ± 19 +1.4% 3,064 3,064 +0.0%
encode NESTED 2,349 ± 34 2,249 ± 29 -4.3% 1,856 1,856 -0.0%
encode POLYMORPHIC 4,193 ± 65 4,225 ± 124 +0.8% 4,584 4,200 -8.4%
encode SIMPLE 818 ± 16 787 ± 8 -3.9% 1,216 1,216 -0.0%

critter

benchmark model before ns/op after ns/op Δ time before B/op after B/op Δ alloc
decode ARRAYS 70,461 ± 1,642 71,336 ± 1,375 +1.2% 74,858 74,858 +0.0%
decode COLLECTIONS 8,953 ± 196 9,215 ± 343 +2.9% 16,696 16,688 -0.0%
decode CONCRETE_COLLECTIONS 2,997 ± 87 3,065 ± 20 +2.3% 6,107 6,128 +0.3%
decode LAZY_REFERENCE 3,222 ± 97 3,289 ± 35 +2.1% 7,293 7,256 -0.5%
decode LIFECYCLE 1,134 ± 43 1,148 ± 50 +1.2% 2,883 2,840 -1.5%
decode NESTED 3,179 ± 30 3,182 ± 32 +0.1% 5,656 5,656 -0.0%
decode POLYMORPHIC 5,697 ± 97 6,129 ± 329 +7.6% 9,528 9,528 +0.0%
decode SIMPLE 1,138 ± 22 1,165 ± 45 +2.3% 2,128 2,136 +0.4%
decodeInCursor ARRAYS 69,197 ± 1,206 72,537 ± 1,606 +4.8% 74,546 74,546 -0.0%
decodeInCursor COLLECTIONS 9,152 ± 167 9,258 ± 195 +1.2% 16,384 16,384 +0.0%
decodeInCursor CONCRETE_COLLECTIONS 3,080 ± 69 3,232 ± 54 +4.9% 5,840 5,840 +0.0%
decodeInCursor LAZY_REFERENCE 3,248 ± 43 3,505 ± 129 +7.9% 6,971 6,963 -0.1%
decodeInCursor LIFECYCLE 1,208 ± 21 1,201 ± 72 -0.6% 2,528 2,528 +0.0%
decodeInCursor NESTED 3,221 ± 79 3,169 ± 54 -1.6% 5,344 5,344 -0.0%
decodeInCursor POLYMORPHIC 5,796 ± 109 5,823 ± 108 +0.5% 9,216 9,216 +0.0%
decodeInCursor SIMPLE 1,177 ± 29 1,226 ± 31 +4.2% 1,824 1,824 +0.0%
encode ARRAYS 98,286 ± 2,463 102,225 ± 2,193 +4.0% 139,886 135,587 -3.1%
encode COLLECTIONS 7,894 ± 80 8,008 ± 179 +1.4% 7,336 7,336 +0.0%
encode CONCRETE_COLLECTIONS 2,057 ± 44 2,128 ± 19 +3.4% 3,104 3,125 +0.7%
encode LAZY_REFERENCE 1,740 ± 36 1,770 ± 39 +1.7% 3,832 3,832 +0.0%
encode LIFECYCLE 813 ± 29 867 ± 27 +6.7% 3,075 3,064 -0.3%
encode NESTED 2,437 ± 24 2,415 ± 25 -0.9% 1,856 1,856 +0.0%
encode POLYMORPHIC 4,235 ± 64 4,449 ± 194 +5.1% 4,200 4,200 -0.0%
encode SIMPLE 782 ± 13 775 ± 9 -1.0% 1,216 1,216 -0.0%

This run was noisy: critter decode cells moved by up to ±8% even though decode code is unchanged, which sets the noise floor for this session. The encode deltas the review predicted are smaller than that, so I reran encode on its own.

encode only, master and this branch alternating, three rounds each

variant model master ns/op (3 runs) 3.6 ns/op (3 runs) Δ median
reflection SIMPLE 797, 839, 869 802, 824, 948 -1.7%
reflection NESTED 2,354, 2,470, 3,668 2,337, 2,378, 2,536 -3.7%
reflection POLYMORPHIC 4,191, 4,337, 4,350 4,195, 4,203, 4,809 -3.1%
reflection LIFECYCLE 864, 901, 1,073 866, 876, 888 -2.8%
critter SIMPLE 796, 798, 799 784, 870, 1,136 +9.1%
critter NESTED 2,419, 2,492, 3,007 2,385, 2,434, 2,728 -2.3%
critter POLYMORPHIC 4,386, 4,579, 4,642 4,217, 4,302, 4,447 -6.0%
critter LIFECYCLE 859, 886, 1,065 845, 856, 880 -3.3%

Reading it:

  • This branch is faster in 7 of 8 cells by median, by 2–6%. That's consistent with "a few ns per property".
  • The exception is critter SIMPLE. Its median is pulled by a single 1,136 ns outlier, and its fastest run (784 ns) still beats master's (796 ns).
  • Single-run outliers of up to +50% (e.g. 3,668 ns) show how noisy the machine was. Treat the gain as small and consistent in direction, not precisely sized.
  • Allocation is unchanged, apart from run-to-run noise. The flags were never allocated, so this change saves work, not memory.

Decisions to review

  1. Only the id and @LoadOnly properties leave the encode order, not every always-skipped property as the plan said. MergingEncoder, used by merge(), relies on visiting skipped properties. A final field skipped by ignoreFinals produces an $unset there. So those properties have to stay in the order. @LoadOnly properties are the only ones that neither encoder writes or unsets.
  2. No benchmark-only proof of the gain. It's small and the machine was noisy. I've reported the medians and the raw runs rather than a single number.

…er (plan item 3.6)

MorphiaPropertySerialization read three config values and scanned the
property's annotations for every value it checked. The config is
immutable and the annotations are fixed once a property is mapped, so it
now works out storeNulls, storeEmpties and whether the property is never
written (@loadonly, or a final field with ignoreFinals) when it's built.
The codec plan's encode order also leaves out @loadonly properties, which
no encoder writes, and EntityEncoder writes the document's start and end
directly instead of through a capturing lambda.
Copilot AI balanced review requested due to automatic review settings October 22, 2026 02:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The focused changes preserve serialization and merge semantics, with no unresolved blocking issues identified.

No open issues 🎉

What changed in this PR

This PR reduces per-property encoding work in Morphia by precomputing serialization decisions and avoiding unnecessary encoder operations, while preserving merge behavior.

Changes:

  • Cache null, empty-value, and never-serialized flags when properties are mapped.
  • Exclude @LoadOnly properties from encode order and write document boundaries directly without a capturing lambda.
  • Add encode-order tests for both mappers and mark plan item 3.6 complete.
File Description
design/​mapping-game-plan.md Marks serialization optimization item 3.6 complete.
core/​src/​test/​java/​dev/​morphia/​mapping/​codec/​pojo/​TestCodecPlanEncodeOrder.java Tests property order and id/@LoadOnly exclusion under both mappers.
core/​src/​main/​java/​dev/​morphia/​mapping/​codec/​pojo/​EntityEncoder.java Replaces the document-wrapper lambda with direct writer calls.
core/​src/​main/​java/​dev/​morphia/​mapping/​codec/​pojo/​CodecPlan.java Removes @LoadOnly properties from encode order.
core/​src/​main/​java/​dev/​morphia/​mapping/​codec/​MorphiaPropertySerialization.java Precomputes flags used by serialization checks.

Review effort: Balanced

This branch has not been deployed

No deployments
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