Skip to content

Commit d07426b

Browse files
committed
Improve Maven cache architecture for better memory efficiency and performance
This comprehensive enhancement to Maven's caching system addresses memory issues and significantly improves performance through several key improvements: - Enhanced DefaultRequestCache with configurable reference types and CSS-like selectors - Pluggable ModelObjectPool service architecture with configurable object types - Comprehensive cache statistics with eviction tracking - Improved InputLocation and InputSource with ImmutableCollections - Custom equality strategy for Dependency pooling - Enhanced parent request matching with interface checking - Configurable cache statistics display - Maven 3: Requires -Xmx1536m, runs in 45 seconds - Maven 4.0.0-rc-4: Runs with -Xmx1024m in 2'32" (cannot run with -Xmx512m) - Maven 4.0.0-rc-4 with -Xmx1536m: 2'5" - Maven 4.0.0-rc-4 + maven3Personality with -Xmx1536m: 1'14" - Maven 4 + this PR: Runs with -Xmx512m in 3'42" (more memory does not help) - Maven 4 + this PR + maven3Personality: Runs with -Xmx512m in 1'0" - Reduced minimum memory requirement from 1024m to 512m - Eliminated memory scaling issues - additional memory beyond 512m provides no benefit - Significant reduction in memory pressure through improved caching strategies This PR definitively solves memory problems while maintaining or improving performance.
1 parent 0e569c0 commit d07426b

File tree

54 files changed

+5305
-488
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+5305
-488
lines changed

api/maven-api-core/src/main/java/org/apache/maven/api/Constants.java

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -662,5 +662,66 @@ public final class Constants {
662662
*/
663663
public static final String MAVEN_LOGGER_LOG_PREFIX = MAVEN_LOGGER_PREFIX + "log.";
664664

665+
/**
666+
* User property key for cache configuration.
667+
*
668+
* @since 4.1.0
669+
*/
670+
public static final String MAVEN_CACHE_CONFIG_PROPERTY = "maven.cache.config";
671+
672+
/**
673+
* User property to enable cache statistics display at the end of the build.
674+
* When set to true, detailed cache statistics including hit/miss ratios,
675+
* request type breakdowns, and retention policy effectiveness will be displayed
676+
* when the build completes.
677+
*
678+
* @since 4.1.0
679+
*/
680+
@Config(type = "java.lang.Boolean", defaultValue = "false")
681+
public static final String MAVEN_CACHE_STATS = "maven.cache.stats";
682+
683+
/**
684+
* User property to configure separate reference types for cache keys and values.
685+
* Format: "key:value" where key and value can be NONE, SOFT, WEAK, or HARD.
686+
* Examples:
687+
* - "HARD:SOFT" - Keep keys strongly referenced, allow values to be garbage collected under memory pressure
688+
* - "WEAK:WEAK" - Allow both keys and values to be garbage collected aggressively
689+
* - "SOFT:HARD" - Allow keys to be GC'd under memory pressure, keep values strongly referenced
690+
*
691+
* This enables fine-grained analysis of cache misses caused by key vs value evictions.
692+
*
693+
* @since 4.1.0
694+
*/
695+
public static final String MAVEN_CACHE_KEY_VALUE_REFS = "maven.cache.keyValueRefs";
696+
697+
/**
698+
* User property key for configuring which object types are pooled by ModelObjectProcessor.
699+
* Value should be a comma-separated list of simple class names (e.g., "Dependency,Plugin,Build").
700+
* Default is "Dependency" for backward compatibility.
701+
*
702+
* @since 4.1.0
703+
*/
704+
@Config(defaultValue = "Dependency")
705+
public static final String MAVEN_MODEL_PROCESSOR_POOLED_TYPES = "maven.model.processor.pooledTypes";
706+
707+
/**
708+
* User property key for configuring the default reference type used by ModelObjectProcessor.
709+
* Valid values are: "SOFT", "HARD", "WEAK", "NONE".
710+
* Default is "HARD" for optimal performance.
711+
*
712+
* @since 4.1.0
713+
*/
714+
@Config(defaultValue = "HARD")
715+
public static final String MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE = "maven.model.processor.referenceType";
716+
717+
/**
718+
* User property key prefix for configuring per-object-type reference types.
719+
* Format: maven.model.processor.referenceType.{ClassName} = {ReferenceType}
720+
* Example: maven.model.processor.referenceType.Dependency = SOFT
721+
*
722+
* @since 4.1.0
723+
*/
724+
public static final String MAVEN_MODEL_PROCESSOR_REFERENCE_TYPE_PREFIX = "maven.model.processor.referenceType.";
725+
665726
private Constants() {}
666727
}

api/maven-api-core/src/main/java/org/apache/maven/api/services/ModelSource.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,22 @@
4242
*/
4343
public interface ModelSource extends Source {
4444

45+
/**
46+
* Returns the model identifier in the format {@code groupId:artifactId:version}
47+
* if this source represents a resolved artifact with known coordinates.
48+
* <p>
49+
* This method is primarily used by resolved sources to provide the model ID
50+
* without requiring the XML to be parsed. For build sources, this typically
51+
* returns {@code null} since the coordinates are determined by parsing the POM.
52+
*
53+
* @return the model identifier, or {@code null} if not available or not applicable
54+
* @since 4.0.0
55+
*/
56+
@Nullable
57+
default String getModelId() {
58+
return null;
59+
}
60+
4561
/**
4662
* Interface for locating POM files within a project structure.
4763
* Implementations of this interface provide the ability to find POM files

api/maven-api-core/src/main/java/org/apache/maven/api/services/Sources.java

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,13 @@ public static ModelSource buildSource(@Nonnull Path path) {
8484
* other sources.
8585
*
8686
* @param path the path to the POM file or project directory
87-
* @param location optional logical location of the source, used for reporting purposes
87+
* @param modelId the modelId (groupId:artifactId:version coordinates)
8888
* @return a new ModelSource instance configured as a resolved source
8989
* @throws NullPointerException if path is null
9090
*/
9191
@Nonnull
92-
public static ModelSource resolvedSource(@Nonnull Path path, @Nullable String location) {
93-
return new ResolvedPathSource(requireNonNull(path, "path"), location);
92+
public static ModelSource resolvedSource(@Nonnull Path path, @Nullable String modelId) {
93+
return new ResolvedPathSource(requireNonNull(path, "path"), path.toString(), modelId);
9494
}
9595

9696
/**
@@ -170,15 +170,25 @@ public String toString() {
170170
* from repositories. Does not support resolving related sources.
171171
*/
172172
static class ResolvedPathSource extends PathSource implements ModelSource {
173-
ResolvedPathSource(Path path, String location) {
173+
@Nullable
174+
private final String modelId;
175+
176+
ResolvedPathSource(Path path, String location, String modelId) {
174177
super(path, location);
178+
this.modelId = modelId;
175179
}
176180

177181
@Override
178182
public Path getPath() {
179183
return null;
180184
}
181185

186+
@Override
187+
@Nullable
188+
public String getModelId() {
189+
return modelId;
190+
}
191+
182192
@Override
183193
public Source resolve(String relative) {
184194
return null;
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.maven.api.services;
20+
21+
import java.nio.file.Path;
22+
import java.nio.file.Paths;
23+
24+
import org.junit.jupiter.api.Test;
25+
26+
import static org.junit.jupiter.api.Assertions.assertEquals;
27+
import static org.junit.jupiter.api.Assertions.assertNotNull;
28+
import static org.junit.jupiter.api.Assertions.assertNull;
29+
import static org.junit.jupiter.api.Assertions.assertTrue;
30+
31+
/**
32+
* Test for ModelSource interface and its implementations.
33+
*/
34+
class ModelSourceTest {
35+
36+
@Test
37+
void testBuildSourceHasNoModelId() {
38+
Path path = Paths.get("/tmp/pom.xml");
39+
ModelSource source = Sources.buildSource(path);
40+
41+
assertNotNull(source);
42+
assertNull(source.getModelId(), "Build sources should not have a modelId");
43+
assertEquals(path, source.getPath());
44+
}
45+
46+
@Test
47+
void testResolvedSourceWithModelId() {
48+
String location = "/tmp/resolved-pom.xml";
49+
Path path = Paths.get(location);
50+
String modelId = "org.apache.maven:maven-core:4.0.0";
51+
52+
ModelSource source = Sources.resolvedSource(path, modelId);
53+
54+
assertNotNull(source);
55+
assertEquals(modelId, source.getModelId(), "Resolved source should return the provided modelId");
56+
assertNull(source.getPath(), "Resolved sources should return null for getPath()");
57+
assertEquals(path.toString(), source.getLocation());
58+
}
59+
60+
@Test
61+
void testModelIdFormat() {
62+
String location = "/tmp/test.xml";
63+
Path path = Paths.get(location);
64+
String modelId = "com.example:test-artifact:1.2.3";
65+
66+
ModelSource source = Sources.resolvedSource(path, modelId);
67+
68+
assertEquals(modelId, source.getModelId());
69+
assertTrue(modelId.matches("^[^:]+:[^:]+:[^:]+$"), "ModelId should follow groupId:artifactId:version format");
70+
}
71+
}

api/maven-api-core/src/test/java/org/apache/maven/api/services/SourcesTest.java

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -64,14 +64,16 @@ void testBuildSource() {
6464

6565
@Test
6666
void testResolvedSource() {
67-
Path path = Paths.get("/tmp");
68-
String location = "custom-location";
69-
ModelSource source = Sources.resolvedSource(path, location);
67+
String location = "/tmp";
68+
Path path = Paths.get(location);
69+
String modelId = "org.example:test:1.0.0";
70+
ModelSource source = Sources.resolvedSource(path, modelId);
7071

7172
assertNotNull(source);
7273
assertInstanceOf(Sources.ResolvedPathSource.class, source);
7374
assertNull(source.getPath());
74-
assertEquals(location, source.getLocation());
75+
assertEquals(path.toString(), source.getLocation());
76+
assertEquals(modelId, source.getModelId());
7577
}
7678

7779
@Test
@@ -109,12 +111,14 @@ void testBuildPathSourceFunctionality() {
109111
@Test
110112
void testResolvedPathSourceFunctionality() {
111113
// Test resolved source functionality
112-
Path path = Paths.get("/tmp");
113-
String location = "custom-location";
114-
Sources.ResolvedPathSource source = (Sources.ResolvedPathSource) Sources.resolvedSource(path, location);
114+
String location = "/tmp";
115+
Path path = Paths.get(location);
116+
String modelId = "org.example:test:1.0.0";
117+
Sources.ResolvedPathSource source = (Sources.ResolvedPathSource) Sources.resolvedSource(path, modelId);
115118

116119
assertNull(source.getPath());
117-
assertEquals(location, source.getLocation());
120+
assertEquals(path.toString(), source.getLocation());
121+
assertEquals(modelId, source.getModelId());
118122
assertNull(source.resolve("subdir"));
119123

120124
ModelSource.ModelLocator locator = mock(ModelSource.ModelLocator.class);
@@ -140,6 +144,6 @@ void testStreamReading() throws IOException {
140144
void testNullHandling() {
141145
assertThrows(NullPointerException.class, () -> Sources.fromPath(null));
142146
assertThrows(NullPointerException.class, () -> Sources.buildSource(null));
143-
assertThrows(NullPointerException.class, () -> Sources.resolvedSource(null, "location"));
147+
assertThrows(NullPointerException.class, () -> Sources.resolvedSource(null, "modelId"));
144148
}
145149
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.maven.api.model;
20+
21+
import java.util.ServiceLoader;
22+
import java.util.concurrent.atomic.AtomicReference;
23+
24+
/**
25+
* A pluggable service for processing model objects during model building.
26+
*
27+
* <p>This service allows implementations to:</p>
28+
* <ul>
29+
* <li>Pool identical objects to reduce memory footprint</li>
30+
* <li>Intern objects for faster equality comparisons</li>
31+
* <li>Apply custom optimization strategies</li>
32+
* <li>Transform or modify objects during building</li>
33+
* </ul>
34+
*
35+
* <p>Implementations are discovered via the Java ServiceLoader mechanism and should
36+
* be registered in {@code META-INF/services/org.apache.maven.api.model.ModelObjectProcessor}.</p>
37+
*
38+
* <p>The service is called during model building for all model objects, allowing
39+
* implementations to decide which objects to process and how to optimize them.</p>
40+
*
41+
* @since 4.0.0
42+
*/
43+
public interface ModelObjectProcessor {
44+
45+
/**
46+
* Process a model object, potentially returning a pooled or optimized version.
47+
*
48+
* <p>This method is called during model building for various model objects.
49+
* Implementations can:</p>
50+
* <ul>
51+
* <li>Return the same object if no processing is desired</li>
52+
* <li>Return a pooled equivalent object to reduce memory usage</li>
53+
* <li>Return a modified or optimized version of the object</li>
54+
* </ul>
55+
*
56+
* <p>The implementation must ensure that the returned object is functionally
57+
* equivalent to the input object from the perspective of the Maven model.</p>
58+
*
59+
* @param <T> the type of the model object
60+
* @param object the model object to process
61+
* @return the processed object (may be the same instance, a pooled instance, or a modified instance)
62+
* @throws IllegalArgumentException if the object cannot be processed
63+
*/
64+
<T> T process(T object);
65+
66+
/**
67+
* Process a model object using the first available processor implementation.
68+
*
69+
* <p>This method discovers processor implementations via ServiceLoader and
70+
* uses the first one found. If no implementations are available, the object
71+
* is returned unchanged. The processor is cached for performance.</p>
72+
*
73+
* @param <T> the type of the model object
74+
* @param object the model object to process
75+
* @return the processed object
76+
*/
77+
static <T> T processObject(T object) {
78+
class ProcessorHolder {
79+
/**
80+
* Cached processor instance for performance.
81+
*/
82+
private static final AtomicReference<ModelObjectProcessor> CACHED_PROCESSOR = new AtomicReference<>();
83+
}
84+
85+
ModelObjectProcessor processor = ProcessorHolder.CACHED_PROCESSOR.get();
86+
if (processor == null) {
87+
processor = loadProcessor();
88+
ProcessorHolder.CACHED_PROCESSOR.compareAndSet(null, processor);
89+
processor = ProcessorHolder.CACHED_PROCESSOR.get();
90+
}
91+
return processor.process(object);
92+
}
93+
94+
/**
95+
* Load the first available processor implementation.
96+
*/
97+
private static ModelObjectProcessor loadProcessor() {
98+
/*
99+
* No-op processor that returns objects unchanged.
100+
*/
101+
class NoOpProcessor implements ModelObjectProcessor {
102+
@Override
103+
public <T> T process(T object) {
104+
return object;
105+
}
106+
}
107+
108+
try {
109+
ServiceLoader<ModelObjectProcessor> loader = ServiceLoader.load(ModelObjectProcessor.class);
110+
for (ModelObjectProcessor processor : loader) {
111+
return processor;
112+
}
113+
} catch (Exception e) {
114+
// If service loading fails, use no-op processor
115+
}
116+
return new NoOpProcessor();
117+
}
118+
}

compat/maven-compat/src/main/java/org/apache/maven/project/interpolation/StringSearchModelInterpolator.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,9 @@ private void traverseObjectWithParents(Class<?> cls, Object target) throws Model
264264

265265
private boolean isQualifiedForInterpolation(Class<?> cls) {
266266
return !cls.getPackage().getName().startsWith("java")
267-
&& !cls.getPackage().getName().startsWith("sun.nio.fs");
267+
&& !cls.getPackage().getName().startsWith("sun.nio.fs")
268+
// org.apache.maven.api.model.InputLocation can be self-referencing
269+
&& !cls.getName().equals("org.apache.maven.api.model.InputLocation");
268270
}
269271

270272
private boolean isQualifiedForInterpolation(Field field, Class<?> fieldType) {

0 commit comments

Comments
 (0)