Skip to content

Add caching for GenericVersion instances in GenericVersionScheme #1498

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
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 @@ -18,6 +18,9 @@
*/
package org.eclipse.aether.util.version;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.eclipse.aether.version.InvalidVersionSpecificationException;

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

private final ConcurrentMap<String, GenericVersion> versionCache = new ConcurrentHashMap<>(256);

@Override
public GenericVersion parseVersion(final String version) throws InvalidVersionSpecificationException {
return new GenericVersion(version);
return versionCache.computeIfAbsent(version, GenericVersion::new);
}

/**
Expand All @@ -67,20 +73,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 {
for (String version : args) {
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) {
throw new IllegalArgumentException(e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
public class GenericVersionRangeTest {
private final GenericVersionScheme versionScheme = new GenericVersionScheme();

private Version newVersion(String version) {
return new GenericVersion(version);
private Version newVersion(String version) throws InvalidVersionSpecificationException {
return versionScheme.parseVersion(version);
}

private VersionRange parseValid(String range) {
Expand All @@ -49,16 +49,16 @@ private void parseInvalid(String range) {
}
}

private void assertContains(VersionRange range, String version) {
private void assertContains(VersionRange range, String version) throws InvalidVersionSpecificationException {
assertTrue(range.containsVersion(newVersion(version)), range + " should contain " + version);
}

private void assertNotContains(VersionRange range, String version) {
private void assertNotContains(VersionRange range, String version) throws InvalidVersionSpecificationException {
assertFalse(range.containsVersion(newVersion(version)), range + " should not contain " + version);
}

@Test
void testLowerBoundInclusiveUpperBoundInclusive() {
void testLowerBoundInclusiveUpperBoundInclusive() throws InvalidVersionSpecificationException {
VersionRange range = parseValid("[1,2]");
assertContains(range, "1");
assertContains(range, "1.1-SNAPSHOT");
Expand All @@ -67,23 +67,23 @@ void testLowerBoundInclusiveUpperBoundInclusive() {
}

@Test
void testLowerBoundInclusiveUpperBoundExclusive() {
void testLowerBoundInclusiveUpperBoundExclusive() throws InvalidVersionSpecificationException {
VersionRange range = parseValid("[1.2.3.4.5,1.2.3.4.6)");
assertContains(range, "1.2.3.4.5");
assertNotContains(range, "1.2.3.4.6");
assertEquals(range, parseValid(range.toString()));
}

@Test
void testLowerBoundExclusiveUpperBoundInclusive() {
void testLowerBoundExclusiveUpperBoundInclusive() throws InvalidVersionSpecificationException {
VersionRange range = parseValid("(1a,1b]");
assertNotContains(range, "1a");
assertContains(range, "1b");
assertEquals(range, parseValid(range.toString()));
}

@Test
void testLowerBoundExclusiveUpperBoundExclusive() {
void testLowerBoundExclusiveUpperBoundExclusive() throws InvalidVersionSpecificationException {
VersionRange range = parseValid("(1,3)");
assertNotContains(range, "1");
assertContains(range, "2-SNAPSHOT");
Expand All @@ -92,7 +92,7 @@ void testLowerBoundExclusiveUpperBoundExclusive() {
}

@Test
void testSingleVersion() {
void testSingleVersion() throws InvalidVersionSpecificationException {
VersionRange range = parseValid("[1]");
assertContains(range, "1");
assertEquals(range, parseValid(range.toString()));
Expand All @@ -103,7 +103,7 @@ void testSingleVersion() {
}

@Test
void testSingleWildcardVersion() {
void testSingleWildcardVersion() throws InvalidVersionSpecificationException {
VersionRange range = parseValid("[1.2.*]");
assertContains(range, "1.2-alpha-1");
assertContains(range, "1.2-SNAPSHOT");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*
* 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() throws InvalidVersionSpecificationException {
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) {
scheme.parseVersion(version);
}
}

// Test with caching (repeated parsing of same versions)
long startTime = System.nanoTime();
for (int i = 0; i < iterations; i++) {
for (String version : commonVersions) {
GenericVersion parsed = scheme.parseVersion(version);
assertNotNull(parsed);
assertEquals(version, parsed.toString());
}
}
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() throws InvalidVersionSpecificationException {
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) {
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());
}
}

@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 IllegalStateException(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");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,32 @@ void testSameUpperAndLowerBound() throws InvalidVersionSpecificationException {
assertEquals(c, c2);
assertTrue(c.containsVersion(new GenericVersion("1.0")));
}

@Test
void testVersionCaching() throws InvalidVersionSpecificationException {
// Test that parsing the same version string returns the same instance (cached)
GenericVersion v1 = scheme.parseVersion("1.0.0");
GenericVersion v2 = scheme.parseVersion("1.0.0");

// Should return the same cached instance
assertSame(v1, v2, "Parsing the same version string should return the same cached instance");

// Test that different version strings create different instances
GenericVersion v3 = scheme.parseVersion("2.0.0");
assertNotSame(v1, v3, "Different version strings should create different instances");

// Test that parsing the first version again still returns the cached instance
GenericVersion v4 = scheme.parseVersion("1.0.0");
assertSame(v1, v4, "Re-parsing the first version should still return the cached instance");

// Test with various version formats
GenericVersion snapshot1 = scheme.parseVersion("1.0.0-SNAPSHOT");
GenericVersion snapshot2 = scheme.parseVersion("1.0.0-SNAPSHOT");
assertSame(snapshot1, snapshot2, "Snapshot versions should also be cached");

// Test that semantically equivalent but different strings are treated as different
GenericVersion v5 = scheme.parseVersion("1.0");
GenericVersion v6 = scheme.parseVersion("1.0.0");
assertNotSame(v5, v6, "Different string representations should not be cached together");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@

public class UnionVersionRangeTest {

private final GenericVersionScheme versionScheme = new GenericVersionScheme();

private VersionRange newRange(String range) {
try {
return new GenericVersionScheme().parseVersionRange(range);
return versionScheme.parseVersionRange(range);
} catch (InvalidVersionSpecificationException e) {
throw new IllegalArgumentException(e);
}
Expand All @@ -44,7 +46,7 @@ private void assertBound(String version, boolean inclusive, VersionRange.Bound b
assertNotNull(bound.getVersion());
assertEquals(inclusive, bound.isInclusive());
try {
assertEquals(new GenericVersionScheme().parseVersion(version), bound.getVersion());
assertEquals(versionScheme.parseVersion(version), bound.getVersion());
} catch (InvalidVersionSpecificationException e) {
throw new IllegalArgumentException(e);
}
Expand Down