Optimize model building pipeline: defer Dependency.build() and reduce allocations - #12653
Draft
gnodet wants to merge 2 commits into
Draft
Optimize model building pipeline: defer Dependency.build() and reduce allocations#12653gnodet wants to merge 2 commits into
gnodet wants to merge 2 commits into
Conversation
… allocations Reduce CPU and memory overhead in Maven 4's immutable model building pipeline by deferring Dependency.build() across pipeline stages and optimizing hot paths in model object pooling. Key changes: - Add Builder getters to generated model classes (model.vm) enabling field access without materializing immutable objects - Add *ToBuilder merger variants (merger.vm) that return Builder instead of calling build(), letting callers accumulate changes across stages - Defer build() in DependencyManagementInjector to batch-build only modified dependencies at the end of the merge loop - Replace Stream.concat().collect() with HashMap.putAll() in computeLocations() and precompute locations hash code to eliminate repeated map iteration during pooling - Optimize PoolKey.locationsEqual() to use direct map comparison with fast-path for empty maps and hash-based inequality check - Add addLocationInformation API to XmlReaderRequest for future use in skipping location tracking on imported BOMs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…r through pipeline Extend the model code generation (model.vm) so that Builder classes store Collection<X.Builder> instead of Collection<X> for model-class list fields. This enables accumulating changes across pipeline stages without intermediate build() calls. Key changes: - model.vm: Builder fields for model-class lists now use child builders. Backward-compatible setter wraps immutable objects into builders. Added getModifiable*() methods for lazy base-list wrapping. Added reset(T base) method to replace builder state in-place. Short-circuit optimization: skip build when no fields are set. - Pipeline stage interfaces (10 interfaces): added default builder-accepting methods that bridge to the existing Model-accepting implementations. Fully backward compatible for existing implementations. - DefaultModelBuilder: buildEffectiveModel() and readEffectiveModel() now thread a Model.Builder between stages instead of rebuilding at each step. - Hot stage implementations: overrode builder-accepting methods in DefaultModelNormalizer, DefaultDependencyManagementInjector, DefaultPluginManagementInjector, DefaultModelPathTranslator, and DefaultPluginConfigurationExpander to write directly to the passed builder, avoiding redundant newBuilder() allocations and intermediate build() calls. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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.
Summary
model.vm) — enables reading builder fields through the base chain without callingbuild(), supporting deferred materialization across pipeline stages*ToBuildermerger variants (merger.vm) — returnsBuilderinstead of callingbuild(), letting callers accumulate changes across multiple merge passes without intermediate allocationsbuild()inDefaultDependencyManagementInjector— accumulates merged dependencies asBuilderobjects, callingbuild()only once per modified dependency at the end of the loopcomputeLocations()— replacesStream.concat().collect(Collectors.toUnmodifiableMap(...))withHashMap.putAll()+Map.copyOf(), and returnsoldlocsdirectly whennewlocsis empty (avoids unnecessaryMap.copyOfsince the base map is already immutable)locationsHashCode— cached at build time in the generated constructor, used byDefaultModelObjectPool.PoolKeyfor fast inequality checks before full map comparisonPoolKey.locationsEqual()— uses directgetLocations()map comparison instead of iterating individual keys, with fast-path for both-empty maps (common for imported deps)addLocationInformationAPI toXmlReaderRequest— wired throughDefaultModelXmlFactoryfor future use in skipping location tracking on imported BOM POMsContext
JFR profiling on a 4,383-module reactor shows 37% CPU in
ModelObjectPool(dependency interning), 18% inDependencyimmutable builders, and 2.3 GB allocated inKeyValueHolderfromcomputeLocations(). EachDependencypasses through ~7 pipeline stages, each callingbuild()which allocates a new immutable object + recomputes location maps + callsprocessObject().This PR targets the three hottest code paths:
build()calls — deferred via Builder getters and ToBuilder merger variantscomputeLocations()stream overhead — replaced with HashMap merge + early returnPoolKeylocation comparison — precomputed hash + direct map equalityTest plan
mvn verify -Bpasses across all reactor modules (all tests green)FileToRawModelMergerTestupdated to exclude new*ToBuildermethods from reflection-based override check🤖 Generated with Claude Code