Skip to content

Set values directly for no-arg constructors (plan item 3.2) - #4403

Merged
evanchooly merged 1 commit into
masterfrom
noarg-creator
Oct 20, 2026
Merged

evanchooly merged 1 commit into
masterfrom
noarg-creator

Conversation

@evanchooly

@evanchooly evanchooly commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Plan item 3.2 (review A2/B4).

The problem

bestConstructor picks a no-arg constructor whenever one exists, so most entities (every POJO-style one) get a ConstructorCreator with an empty parameter plan. That creator still buffered every decoded property in two ArrayLists and replayed them into the instance through setValue once the document was done. It also cloned a zero-length parameter array per instance. The InstanceCreatorFactoryImpl branch that would have used NoArgCreator was unreachable, because bestConstructor had already returned any no-arg constructor.

Change

  • No-arg fast path. When the constructor takes no parameters, ConstructorCreator.set constructs the instance on the first value and sets each value through model.setValue as it arrives. Conversions still apply, unlike in the deleted NoArgCreator.
  • Lazy pending lists. They're allocated only when a partial constructor actually has values to hold until it runs.
  • Shared empty parameter array for no-arg creators.
  • arg[0-9]+ is a precompiled Pattern, instead of String.matches compiling it once per constructor parameter.
  • NoArgCreator deleted (@MorphiaInternal), along with the unreachable factory branch and its LOG.info("using old creator approach").

Tests

  • ConstructorCreatorTest.noArgConstructorSetsValuesAsTheyArrive checks that a no-arg creator is eager-safe, that values land on the one instance, and that a value needing conversion (42.0 into an int) still goes through Conversions. It pins behavior. It would also pass on master, because the old path replayed the same values, so the speedup itself is shown by the benchmark below.
  • The harness is unchanged and passes. The core and Kotlin suites pass with -Dmorphia.mapper=critter and with reflection: 1486 + 5 tests, 0 failures.

JMH: CodecBenchmark, master (67dc8c46533, after 3.0 + 3.1) vs this branch

Same machine as the baseline (i9-11900H, Temurin 17), -prof gc, the benchmark's own settings (3 forks × 5 × 1 s). Before and after ran back to back with nothing else running. Critter is the AOT variant.

reflection

benchmark model before ns/op after ns/op Δ time before B/op after B/op Δ alloc
decode ARRAYS 68,627 ± 1,327 68,718 ± 1,396 +0.1% 77,770 77,706 -0.1%
decode COLLECTIONS 9,441 ± 211 9,362 ± 170 -0.8% 19,376 18,840 -2.8%
decode CONCRETE_COLLECTIONS 3,092 ± 34 3,048 ± 59 -1.4% 6,664 6,608 -0.8%
decode LAZY_REFERENCE 3,355 ± 113 3,465 ± 42 +3.3% 8,176 8,131 -0.6%
decode LIFECYCLE 1,188 ± 29 1,134 ± 23 -4.5% 3,035 2,960 -2.5%
decode NESTED 3,261 ± 85 3,128 ± 35 -4.1% 7,024 6,776 -3.5%
decode POLYMORPHIC 6,076 ± 96 5,879 ± 117 -3.2% 12,672 12,024 -5.1%
decode SIMPLE 1,545 ± 381 1,274 ± 30 -17.5% 2,648 2,584 -2.4%
decodeInCursor ARRAYS 69,156 ± 1,411 69,003 ± 1,204 -0.2% 77,458 77,394 -0.1%
decodeInCursor COLLECTIONS 9,703 ± 222 9,372 ± 139 -3.4% 19,056 18,520 -2.8%
decodeInCursor CONCRETE_COLLECTIONS 3,102 ± 91 3,115 ± 65 +0.4% 6,368 6,283 -1.3%
decodeInCursor LAZY_REFERENCE 3,639 ± 140 3,564 ± 72 -2.0% 7,907 7,843 -0.8%
decodeInCursor LIFECYCLE 1,218 ± 26 1,197 ± 16 -1.7% 2,712 2,648 -2.4%
decodeInCursor NESTED 3,322 ± 28 3,277 ± 56 -1.3% 6,720 6,464 -3.8%
decodeInCursor POLYMORPHIC 6,145 ± 98 6,060 ± 130 -1.4% 12,344 11,704 -5.2%
decodeInCursor SIMPLE 1,321 ± 51 1,305 ± 26 -1.3% 2,336 2,272 -2.7%
encode ARRAYS 96,921 ± 1,860 99,863 ± 2,915 +3.0% 137,187 137,187 -0.0%
encode COLLECTIONS 7,757 ± 128 7,747 ± 149 -0.1% 7,336 7,336 +0.0%
encode CONCRETE_COLLECTIONS 2,053 ± 25 2,177 ± 129 +6.0% 3,104 3,147 +1.4%
encode LAZY_REFERENCE 1,809 ± 28 1,909 ± 81 +5.5% 4,072 4,072 +0.0%
encode LIFECYCLE 837 ± 24 839 ± 16 +0.3% 3,043 3,021 -0.7%
encode NESTED 2,374 ± 113 2,311 ± 42 -2.7% 1,856 1,856 -0.0%
encode POLYMORPHIC 4,443 ± 233 4,197 ± 80 -5.5% 6,024 6,024 -0.0%
encode SIMPLE 792 ± 18 784 ± 16 -1.1% 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 72,482 ± 1,907 73,698 ± 4,112 +1.7% 77,770 77,706 -0.1%
decode COLLECTIONS 10,260 ± 349 10,747 ± 142 +4.7% 19,376 18,840 -2.8%
decode CONCRETE_COLLECTIONS 3,313 ± 113 3,242 ± 41 -2.1% 6,664 6,616 -0.7%
decode LAZY_REFERENCE 3,715 ± 98 3,396 ± 70 -8.6% 8,131 8,120 -0.1%
decode LIFECYCLE 1,250 ± 28 1,209 ± 25 -3.3% 3,024 2,939 -2.8%
decode NESTED 3,762 ± 134 3,829 ± 80 +1.8% 7,024 6,784 -3.4%
decode POLYMORPHIC 6,817 ± 218 6,855 ± 88 +0.6% 12,648 12,016 -5.0%
decode SIMPLE 1,342 ± 41 1,441 ± 60 +7.4% 2,648 2,584 -2.4%
decodeInCursor ARRAYS 74,118 ± 2,296 72,694 ± 1,617 -1.9% 77,458 77,394 -0.1%
decodeInCursor COLLECTIONS 10,521 ± 290 10,200 ± 156 -3.1% 19,056 18,520 -2.8%
decodeInCursor CONCRETE_COLLECTIONS 3,438 ± 91 3,405 ± 95 -1.0% 6,347 6,312 -0.5%
decodeInCursor LAZY_REFERENCE 3,750 ± 106 3,670 ± 109 -2.1% 7,848 7,816 -0.4%
decodeInCursor LIFECYCLE 1,299 ± 22 1,297 ± 14 -0.2% 2,712 2,659 -2.0%
decodeInCursor NESTED 3,947 ± 150 3,750 ± 80 -5.0% 6,720 6,464 -3.8%
decodeInCursor POLYMORPHIC 7,123 ± 167 6,784 ± 101 -4.8% 12,344 11,704 -5.2%
decodeInCursor SIMPLE 1,448 ± 43 1,454 ± 35 +0.4% 2,336 2,272 -2.7%
encode ARRAYS 107,407 ± 1,909 106,766 ± 2,456 -0.6% 145,763 145,785 +0.0%
encode COLLECTIONS 8,437 ± 120 8,245 ± 96 -2.3% 7,336 7,336 -0.0%
encode CONCRETE_COLLECTIONS 2,252 ± 44 2,172 ± 35 -3.6% 3,125 3,104 -0.7%
encode LAZY_REFERENCE 1,940 ± 55 1,811 ± 48 -6.7% 4,072 4,072 -0.0%
encode LIFECYCLE 886 ± 34 851 ± 13 -3.9% 3,085 3,043 -1.4%
encode NESTED 2,613 ± 88 2,475 ± 40 -5.3% 1,856 1,856 -0.0%
encode POLYMORPHIC 4,882 ± 241 4,613 ± 66 -5.5% 5,640 5,640 -0.0%
encode SIMPLE 856 ± 154 796 ± 5 -7.0% 1,216 1,216 -0.0%

Reading it:

  • Decode allocation drops wherever a no-arg entity is built: −64 B per entity on SIMPLE (−2.4%), −248/256 B on NESTED (−3.5%), −536 B on COLLECTIONS (−2.8%) and −632/640 B on POLYMORPHIC (−5%). That's the same on both mappers and in decodeInCursor. Models whose entities use full constructors barely move, as expected.
  • This is less than the review estimated. It predicted ~216 B per no-arg entity, from a microbenchmark of the creator alone. In a full decode, the JIT probably already eliminated part of the old buffering, so the measured saving is about 64 B per entity. I haven't confirmed that cause.
  • Decode time is flat to a few percent faster. Two critter decode cells looked slower (SIMPLE +7.4%, COLLECTIONS +4.7%). I reran both twice, alternating master and this branch: SIMPLE 1,348 vs 1,373 and 1,374 vs 1,392 ns/op, within the error bars; COLLECTIONS 10,247 vs 10,205 and 10,467 vs 10,330 ns/op, slightly faster. So both were noise.
  • Encode doesn't touch the creator. Its swings in either direction, of up to ±7%, are run-to-run noise.

Decisions to review

  1. The no-arg instance is now constructed when the first property arrives, not after the whole document has been read. Field values end up the same, because the constructor runs before any setValue in both versions. If decoding fails partway through a document, an instance has already been constructed and is then discarded. EntityDecoder's eager path for reference cycles already did this.
  2. The fast path lives in ConstructorCreator, keyed on the plan having no parameters. I didn't add a separate creator class. That keeps one creator type and one factory path, which matters because 3.17 has to hold values that arrive before _t for exactly these creators.
  3. The gain is smaller than the review's estimate. The numbers above are what was measured.

Copilot AI balanced review requested due to automatic review settings October 20, 2026 03:07

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 optimization has no identified blocking issues in constructor selection, value conversion, or instance isolation.

No open issues 🎉

What changed in this PR

Optimizes Morphia’s no-argument entity construction to reduce decoding allocations while preserving value conversion.

Changes:

  • Assigns no-arg entity values immediately and allocates pending buffers lazily.
  • Reuses an empty parameter array, precompiles parameter-name matching, and removes the unused creator.
  • Adds assignment/conversion coverage and marks plan item 3.2 complete.
File Description
design/​mapping-game-plan.md Marks the optimization complete.
core/​src/​test/​java/​dev/​morphia/​test/​mapping/​internal/​ConstructorCreatorTest.java Tests instance reuse and value conversion.
core/​src/​main/​java/​dev/​morphia/​mapping/​NoArgCreator.java Removes the unused creator.
core/​src/​main/​java/​dev/​morphia/​mapping/​internal/​ConstructorCreator.java Adds immediate assignment and reduces allocations.
core/​src/​main/​java/​dev/​morphia/​mapping/​InstanceCreatorFactoryImpl.java Removes the unreachable fallback and logging.

Review effort: Balanced

evanchooly added a commit that referenced this pull request Oct 20, 2026
…start on new kernels (#4404)

Unblocks CI. Every PR's Build job, #4403 included, is failing in
`MongoExtension.beforeAll` with `Container startup failed for image
mongo:8 … Container exited with code 1`.

## Cause
- **`mongo:8` moved to MongoDB 8.3.13 on 2026-10-17.** That release
refuses to start on Linux 6.19 and newer: *"MongoDB cannot start: Linux
kernel versions 6.19 and newer has a known incompatibility with this
version of MongoDB"*
([SERVER-121912](https://jira.mongodb.org/browse/SERVER-121912)).
- **The current 8.0 release, 8.0.34, has the same check.** 7.0.45,
8.2.12 and 9.0.4 start fine. I checked each image locally on a 7.0
kernel.
- **The version flag was being ignored.** `MongoExtension` and
`TestBase.withSharding` built the image tag from the **major version
only**. So the matrix's `8.3.13`, `8.2.12` and `8.0.34` entries, and the
`8.0.0` local default, all ran `mongo:8`. The 8.x entries weren't
testing the versions they're named for.

## Changes
- **Exact image tags.** `MongoExtension.imageName(version)` uses
`-Dmongodb` as given: `mongo:8.2.12` for a full version, or `mongo:8.2`
for that line's latest release. `withSharding` uses the same helper.
- **New default.** The default changes from `8.0.0` (which ran
`mongo:8`) to `8.2`.
- **Matrix filter.** `BuildMatrix` skips exactly 8.0.34 and 8.3.13, with
a comment pointing at SERVER-121912, so the matrix is now `['9.0.4',
'8.2.12', '7.0.45']`. A later 8.0 or 8.3 patch release is picked up
automatically. If one of those is still broken, CI fails until it's
added to the list.

## Verified
- The core and Kotlin suites pass with the new default (`mongo:8.2`)
under `-Dmorphia.mapper=critter` and reflection: 1485 + 5 tests, 0
failures.
- `TestSharding`'s sharded cluster starts on `mongo:8.2`.
- `-Dmongodb=7.0.45` runs exactly `mongo:7.0.45`, and `TestQuery` passes
on it.

## Decisions to review
1. **The skip lists exact versions (8.0.34, 8.3.13), not whole release
lines.** This was changed in review. Fixed patch releases come back into
the matrix on their own, at the cost of a failing build if a new patch
release is still affected.
Entities with a no-arg constructor, which bestConstructor picks whenever
one exists, went through ConstructorCreator buffering every decoded
property in two lists and replaying them into the instance at the end.
With no constructor parameters there is nothing to wait for, so the
creator now constructs on the first value and sets each one through
model.setValue as it arrives, which keeps Conversions. The pending lists
are allocated only when a partial constructor needs them, a no-arg
creator shares one empty parameter array, and the unnamed-parameter check
uses a precompiled pattern.

NoArgCreator and the factory branch that created it were unreachable:
bestConstructor already returns any no-arg constructor. Both are removed,
along with the branch's log line.
@evanchooly
evanchooly merged commit 7d357ef into master Oct 20, 2026
4 checks passed
@evanchooly
evanchooly deleted the noarg-creator branch October 20, 2026 03:58
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