Repository navigation
Single-lookup getProperty; memoize codec lookups by class (plan items 3.4 + 3.5) - #4406
Merged
Merged
Conversation
getProperty used getOrDefault, whose default argument looked the name up in the second map on every call, including every decoded field.
The driver's registry builds several objects per get(Class) to find a codec it has already cached, and Morphia looks codecs up per value. The datastore's registry is now wrapped in MemoizingCodecRegistry, which answers repeat lookups by class from a map; lookups with type arguments or a registry go straight through. MorphiaCodec caches the codec for each subtype it encodes, which the encoders use instead of its composite registry, and ArrayCodec resolves its component codec once.
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
The shared array-component codec cache needs safe publication to preserve initialization visibility during concurrent decoding.
1 open issue
What changed in this PR
Reduces lookup overhead and allocations in Morphia’s encoding and decoding paths, implementing plan items 3.4 and 3.5.
Changes:
- Avoids unnecessary property-name map lookups.
- Caches class, subtype, and array-component codecs.
- Adds registry-cache tests and marks the plan items complete.
| File | Description |
|---|---|
| design/mapping-game-plan.md | Marks both optimizations complete. |
| core/src/test/java/dev/morphia/mapping/codec/TestMemoizingCodecRegistry.java | Tests caching, failures, and contextual delegation. |
| core/src/main/java/dev/morphia/MorphiaDatastore.java | Wraps the datastore registry with memoization. |
| core/src/main/java/dev/morphia/mapping/codec/pojo/MorphiaCodec.java | Adds subtype codec caching. |
| core/src/main/java/dev/morphia/mapping/codec/pojo/LifecycleEncoder.java | Uses cached subtype codecs. |
| core/src/main/java/dev/morphia/mapping/codec/pojo/EntityModel.java | Makes property-name fallback lazy. |
| core/src/main/java/dev/morphia/mapping/codec/pojo/EntityEncoder.java | Uses cached subtype codecs. |
| core/src/main/java/dev/morphia/mapping/codec/MemoizingCodecRegistry.java | Adds class-based registry memoization. |
| core/src/main/java/dev/morphia/mapping/codec/ArrayCodec.java | Caches the decoding component codec. |
Review effort: Balanced
ArrayCodec's component codec and EntityDecoder's id-key codec are cached on codecs shared across threads, and the cached codecs carry non-final state (a MorphiaCodec's encoder and decoder are set after construction), so the cache fields are volatile.
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.

Plan items 3.4 and 3.5. The plan notes that 3.4, a one-line fix, can ride along with any PR, so each has its own commit here.
3.4:
getPropertydoes one lookupEntityModel.getPropertyusedpropertyModelsByMappedName.getOrDefault(name, propertyModelsByName.get(name)). The default argument is evaluated eagerly, so it probed the second map on every call, including once per decoded field. It now checks the mapped-name map first and falls back only on a miss. The result is the same, since neither map holds null values.3.5: memoize codec lookups by class
The driver's
ProvidersCodecRegistry.get(Class)allocates several objects on every call just to find a codec it has already cached: aChildCodecRegistry, aCodecCacheKey, anOptionaland a capturing lambda. Morphia looks codecs up per value, for array elements, map values, references, untyped properties and subtype values. Three changes:MemoizingCodecRegistry(@MorphiaInternal) wraps the datastore's registry. A repeatget(Class)is a singleConcurrentHashMaphit. It usesget+putIfAbsentrather thancomputeIfAbsent, because building a codec can recurse into lookups. Failed lookups aren't cached. Lookups that take type arguments or a registry depend on their context, so they go straight to the wrapped registry. The driver caches one codec per class and returns the cached instance, and that's the instance memoized here, so results are unchanged. This also covers the driver's own per-operation lookups throughdatabase.withCodecRegistry.MorphiaCodec.codecForSubtype(Class)caches the codec for each subtype value encoded through a codec.EntityEncoderandLifecycleEncoderuse it instead of the codec's compositefromRegistries(...)registry, which is a driver registry and so allocates on every lookup.ArrayCodecresolves its component codec once, instead of once per element. The cache field isvolatilebecause the codec is shared across threads.EntityDecoder's cachedpeekIdcodec from Resolve discriminators to codecs once per codec (plan item 3.3) #4405 gets the same fix.Tests
TestMemoizingCodecRegistryuses a counting delegate to check three things:get(String.class)calls reach the delegate once and return the same codec;get(Class, CodecRegistry)lookups always reach the delegate.-Dmorphia.mapper=critterand with reflection: 1492 + 5 tests, 0 failures.JMH:
CodecBenchmark,master(05862218aea) vs this branchSame 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
critter
Reading it:
encode ARRAYSallocation drop includes that cell's usual outlierbeforevalue.Decisions to review