Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,13 @@ public final class ConfigurationProperties {
*/
public static final String PREFIX_GENERATOR = PREFIX_AETHER + "generator.";

/**
* Prefix for util related configurations. <em>For internal use only.</em>
*
* @since 2.0.10
*/
public static final String PREFIX_UTIL = PREFIX_AETHER + "util.";

/**
* Prefix for transport related configurations. <em>For internal use only.</em>
*
Expand Down Expand Up @@ -544,6 +551,25 @@ public final class ConfigurationProperties {
*/
public static final String REPOSITORY_SYSTEM_DEPENDENCY_VISITOR_LEVELORDER = "levelOrder";

/**
* A flag indicating whether version scheme cache statistics should be printed on JVM shutdown.
* This is useful for analyzing cache performance and effectiveness in development and testing scenarios.
*
* @since 2.0.10
* @configurationSource {@link RepositorySystemSession#getConfigProperties()}
* @configurationType {@link java.lang.Boolean}
* @configurationDefaultValue {@link #DEFAULT_VERSION_SCHEME_CACHE_DEBUG}
* @configurationRepoIdSuffix No
*/
public static final String VERSION_SCHEME_CACHE_DEBUG = PREFIX_UTIL + "versionScheme.cacheDebug";

/**
* The default value for version scheme cache debug if {@link #VERSION_SCHEME_CACHE_DEBUG} isn't set.
*
* @since 2.0.10
*/
public static final boolean DEFAULT_VERSION_SCHEME_CACHE_DEBUG = false;

private ConfigurationProperties() {
// hide constructor
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
*/
package org.eclipse.aether.util.version;

import java.util.Collections;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.concurrent.atomic.AtomicLong;

import org.eclipse.aether.ConfigurationProperties;
import org.eclipse.aether.version.InvalidVersionSpecificationException;

/**
Expand Down Expand Up @@ -46,9 +52,99 @@
* </p>
*/
public class GenericVersionScheme extends VersionSchemeSupport {

// Using WeakHashMap wrapped in synchronizedMap for thread safety and memory-sensitive caching
private final Map<String, GenericVersion> versionCache = Collections.synchronizedMap(new WeakHashMap<>());

// Cache statistics
private final AtomicLong cacheHits = new AtomicLong(0);
private final AtomicLong cacheMisses = new AtomicLong(0);
private final AtomicLong totalRequests = new AtomicLong(0);

// Static statistics across all instances
private static final AtomicLong GLOBAL_CACHE_HITS = new AtomicLong(0);
private static final AtomicLong GLOBAL_CACHE_MISSES = new AtomicLong(0);
private static final AtomicLong GLOBAL_TOTAL_REQUESTS = new AtomicLong(0);
private static final AtomicLong INSTANCE_COUNT = new AtomicLong(0);

static {
// Register shutdown hook to print statistics if enabled
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
if (isStatisticsEnabled()) {
printGlobalStatistics();
}
}));
}

public GenericVersionScheme() {
INSTANCE_COUNT.incrementAndGet();
}

/**
* Checks if version scheme cache statistics should be printed.
* This checks both the system property and the configuration property.
*/
private static boolean isStatisticsEnabled() {
// Check system property first (for backwards compatibility and ease of use)
String sysProp = System.getProperty(ConfigurationProperties.VERSION_SCHEME_CACHE_DEBUG);
if (sysProp != null) {
return Boolean.parseBoolean(sysProp);
}

// Default to false if not configured
return ConfigurationProperties.DEFAULT_VERSION_SCHEME_CACHE_DEBUG;
}

@Override
public GenericVersion parseVersion(final String version) throws InvalidVersionSpecificationException {
return new GenericVersion(version);
totalRequests.incrementAndGet();
GLOBAL_TOTAL_REQUESTS.incrementAndGet();

GenericVersion existing = versionCache.get(version);
if (existing != null) {
cacheHits.incrementAndGet();
GLOBAL_CACHE_HITS.incrementAndGet();
return existing;
} else {
cacheMisses.incrementAndGet();
GLOBAL_CACHE_MISSES.incrementAndGet();
return versionCache.computeIfAbsent(version, GenericVersion::new);
}
}

/**
* Get cache statistics for this instance.
*/
public String getCacheStatistics() {
long hits = cacheHits.get();
long misses = cacheMisses.get();
long total = totalRequests.get();
double hitRate = total > 0 ? (double) hits / total * 100.0 : 0.0;

return String.format(
"GenericVersionScheme Cache Stats: hits=%d, misses=%d, total=%d, hit-rate=%.2f%%, cache-size=%d",
hits, misses, total, hitRate, versionCache.size());
}

/**
* Print global statistics across all instances.
*/
private static void printGlobalStatistics() {
long hits = GLOBAL_CACHE_HITS.get();
long misses = GLOBAL_CACHE_MISSES.get();
long total = GLOBAL_TOTAL_REQUESTS.get();
long instances = INSTANCE_COUNT.get();
double hitRate = total > 0 ? (double) hits / total * 100.0 : 0.0;

System.err.println("=== GenericVersionScheme Global Cache Statistics (WeakHashMap) ===");
System.err.println(String.format("Total instances created: %d", instances));
System.err.println(String.format("Total requests: %d", total));
System.err.println(String.format("Cache hits: %d", hits));
System.err.println(String.format("Cache misses: %d", misses));
System.err.println(String.format("Hit rate: %.2f%%", hitRate));
System.err.println(
String.format("Average requests per instance: %.2f", instances > 0 ? (double) total / instances : 0.0));
System.err.println("=== End Cache Statistics ===");
}

/**
Expand All @@ -67,20 +163,25 @@ public static void main(String... args) {
return;
}

GenericVersionScheme scheme = new GenericVersionScheme();
GenericVersion prev = null;
int i = 1;
for (String version : args) {
GenericVersion c = new GenericVersion(version);
try {
GenericVersion c = scheme.parseVersion(version);

if (prev != null) {
int compare = prev.compareTo(c);
System.out.println(
" " + prev + ' ' + ((compare == 0) ? "==" : ((compare < 0) ? "<" : ">")) + ' ' + version);
}
if (prev != null) {
int compare = prev.compareTo(c);
System.out.println(
" " + prev + ' ' + ((compare == 0) ? "==" : ((compare < 0) ? "<" : ">")) + ' ' + version);
}

System.out.println((i++) + ". " + version + " -> " + c.asString() + "; tokens: " + c.asItems());
System.out.println((i++) + ". " + version + " -> " + c.asString() + "; tokens: " + c.asItems());

prev = c;
prev = c;
} catch (InvalidVersionSpecificationException e) {
System.err.println("Invalid version: " + version + " - " + e.getMessage());
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ public class GenericVersionRangeTest {
private final GenericVersionScheme versionScheme = new GenericVersionScheme();

private Version newVersion(String version) {
return new GenericVersion(version);
try {
return versionScheme.parseVersion(version);
} catch (InvalidVersionSpecificationException e) {
throw new RuntimeException(e);
}
}

private VersionRange parseValid(String range) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.eclipse.aether.util.version;

import org.eclipse.aether.version.InvalidVersionSpecificationException;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

/**
* Performance test to demonstrate the benefits of caching in GenericVersionScheme.
* This test is not run as part of the regular test suite but can be used to verify
* that caching provides performance benefits.
*/
public class GenericVersionSchemeCachingPerformanceTest {

@Test
void testCachingPerformance() {
GenericVersionScheme scheme = new GenericVersionScheme();

// Common version strings that would be parsed repeatedly in real scenarios
String[] commonVersions = {
"1.0.0",
"1.0.1",
"1.0.2",
"1.1.0",
"1.1.1",
"2.0.0",
"2.0.1",
"1.0.0-SNAPSHOT",
"1.1.0-SNAPSHOT",
"2.0.0-SNAPSHOT",
"1.0.0-alpha",
"1.0.0-beta",
"1.0.0-rc1",
"1.0.0-final",
"3.0.0",
"3.1.0",
"3.2.0",
"4.0.0",
"5.0.0"
};

int iterations = 10000;

// Warm up
for (int i = 0; i < 1000; i++) {
for (String version : commonVersions) {
try {
scheme.parseVersion(version);
} catch (InvalidVersionSpecificationException e) {
fail("Unexpected exception during warmup: " + e.getMessage());
}
}
}

// Test with caching (repeated parsing of same versions)
long startTime = System.nanoTime();
for (int i = 0; i < iterations; i++) {
for (String version : commonVersions) {
try {
GenericVersion parsed = scheme.parseVersion(version);
assertNotNull(parsed);
assertEquals(version, parsed.toString());
} catch (InvalidVersionSpecificationException e) {
fail("Unexpected exception during caching test: " + e.getMessage());
}
}
}
long cachedTime = System.nanoTime() - startTime;

// Test without caching (direct instantiation)
startTime = System.nanoTime();
for (int i = 0; i < iterations; i++) {
for (String version : commonVersions) {
GenericVersion parsed = new GenericVersion(version);
assertNotNull(parsed);
assertEquals(version, parsed.toString());
}
}
long directTime = System.nanoTime() - startTime;

System.out.println("Performance Test Results:");
System.out.println("Cached parsing time: " + (cachedTime / 1_000_000) + " ms");
System.out.println("Direct instantiation time: " + (directTime / 1_000_000) + " ms");
System.out.println("Speedup factor: " + String.format("%.2f", (double) directTime / cachedTime));

// The cached version should be significantly faster for repeated parsing
// Note: This assertion might be too strict for CI environments, so we use a conservative factor
assertTrue(
cachedTime < directTime,
"Cached parsing should be faster than direct instantiation for repeated versions");
}

@Test
void testCachingCorrectness() {
GenericVersionScheme scheme = new GenericVersionScheme();

// Test that caching doesn't affect correctness
String[] versions = {
"1.0.0", "1.0.1", "1.1.0", "2.0.0", "1.0.0-SNAPSHOT", "1.0.0-alpha", "1.0.0-beta", "1.0.0-rc1"
};

// Parse each version multiple times and verify they're the same instance
for (String versionStr : versions) {
try {
GenericVersion first = scheme.parseVersion(versionStr);
GenericVersion second = scheme.parseVersion(versionStr);
GenericVersion third = scheme.parseVersion(versionStr);

// Should be the same cached instance
assertSame(first, second, "Second parse should return cached instance");
assertSame(first, third, "Third parse should return cached instance");

// Should have correct string representation
assertEquals(versionStr, first.toString());
assertEquals(versionStr, second.toString());
assertEquals(versionStr, third.toString());
} catch (InvalidVersionSpecificationException e) {
fail("Unexpected exception for version " + versionStr + ": " + e.getMessage());
}
}
}

@Test
void testConcurrentCaching() throws InterruptedException {
GenericVersionScheme scheme = new GenericVersionScheme();
String version = "1.0.0";
int numThreads = 10;
Thread[] threads = new Thread[numThreads];
GenericVersion[] results = new GenericVersion[numThreads];

// Create threads that parse the same version concurrently
for (int i = 0; i < numThreads; i++) {
final int index = i;
threads[i] = new Thread(() -> {
try {
results[index] = scheme.parseVersion(version);
} catch (InvalidVersionSpecificationException e) {
throw new RuntimeException("Unexpected exception in thread " + index, e);
}
});
}

// Start all threads
for (Thread thread : threads) {
thread.start();
}

// Wait for all threads to complete
for (Thread thread : threads) {
thread.join();
}

// All results should be the same cached instance
GenericVersion first = results[0];
assertNotNull(first);
for (int i = 1; i < numThreads; i++) {
assertSame(first, results[i], "All concurrent parses should return the same cached instance");
}
}
}
Loading