Skip to content

Optimize model building pipeline: defer Dependency.build() and reduce allocations - #12653

Draft
gnodet wants to merge 2 commits into
masterfrom
optimize-model-building-pipeline-defer-dependency
Draft

Optimize model building pipeline: defer Dependency.build() and reduce allocations#12653
gnodet wants to merge 2 commits into
masterfrom
optimize-model-building-pipeline-defer-dependency

Conversation

@gnodet

@gnodet gnodet commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add Builder getters to generated model classes (model.vm) — enables reading builder fields through the base chain without calling build(), supporting deferred materialization across pipeline stages
  • Add *ToBuilder merger variants (merger.vm) — returns Builder instead of calling build(), letting callers accumulate changes across multiple merge passes without intermediate allocations
  • Defer build() in DefaultDependencyManagementInjector — accumulates merged dependencies as Builder objects, calling build() only once per modified dependency at the end of the loop
  • Optimize computeLocations() — replaces Stream.concat().collect(Collectors.toUnmodifiableMap(...)) with HashMap.putAll() + Map.copyOf(), and returns oldlocs directly when newlocs is empty (avoids unnecessary Map.copyOf since the base map is already immutable)
  • Precompute locationsHashCode — cached at build time in the generated constructor, used by DefaultModelObjectPool.PoolKey for fast inequality checks before full map comparison
  • Optimize PoolKey.locationsEqual() — uses direct getLocations() map comparison instead of iterating individual keys, with fast-path for both-empty maps (common for imported deps)
  • Add addLocationInformation API to XmlReaderRequest — wired through DefaultModelXmlFactory for future use in skipping location tracking on imported BOM POMs

Context

JFR profiling on a 4,383-module reactor shows 37% CPU in ModelObjectPool (dependency interning), 18% in Dependency immutable builders, and 2.3 GB allocated in KeyValueHolder from computeLocations(). Each Dependency passes through ~7 pipeline stages, each calling build() which allocates a new immutable object + recomputes location maps + calls processObject().

This PR targets the three hottest code paths:

  1. Redundant build() calls — deferred via Builder getters and ToBuilder merger variants
  2. computeLocations() stream overhead — replaced with HashMap merge + early return
  3. PoolKey location comparison — precomputed hash + direct map equality

Test plan

  • mvn verify -B passes across all reactor modules (all tests green)
  • FileToRawModelMergerTest updated to exclude new *ToBuilder methods from reflection-based override check
  • Benchmark with JFR on large reactor to measure allocation reduction

🤖 Generated with Claude Code

gnodet and others added 2 commits August 1, 2026 01:51
… 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>
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