This is an automated email from the ASF dual-hosted git repository.

cstamas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/maven-resolver.git


The following commit(s) were added to refs/heads/master by this push:
     new 89cd11d6d Fix thread contention in GenericVersionScheme and 
WeakInternPool (#1937)
89cd11d6d is described below

commit 89cd11d6d1e073fd1d2de5399a0b9bd17aa9f863
Author: Guillaume Nodet <[email protected]>
AuthorDate: Sat Jun 27 16:03:50 2026 +0200

    Fix thread contention in GenericVersionScheme and WeakInternPool (#1937)
    
    ## Problem
    
    PR #1902 introduced `synchronized(versionCache)` and 
`synchronized(this.map)` blocks around compound cache operations to fix a 
thread-safety issue with `computeIfAbsent()` on `synchronizedMap(WeakHashMap)`. 
While correct, this serialized all threads on hot paths, causing a **2x build 
time regression** for Quarkus (~6min → ~12min).
    
    ### Root cause analysis
    
    `Collections.synchronizedMap(new WeakHashMap<>())` wraps each individual 
method (`get`, `put`) in `synchronized(mutex)`, but does NOT make compound 
operations like `computeIfAbsent()` atomic. PR #1902 fixed this by adding outer 
`synchronized` blocks — correct but serializing.
    
    The original pre-#1902 code used separate `get()` + `put()` calls where 
each individually acquires/releases the lock for microseconds. The compound 
race is **benign for a cache** — at worst a duplicate value is created and one 
wins the put.
    
    ## Solution
    
    ### 1. `ConcurrentWeakCache` — new lightweight concurrent cache
    
    Introduces `ConcurrentWeakCache<K,V>` — a stripped-down version of 
maven-impl's `Cache`, optimized for hot-path performance:
    
    - **Lock-free reads** — `ConcurrentHashMap.get()` is a volatile read, no 
lock acquisition
    - **Zero allocation on `get()`** — uses a `ThreadLocal` reusable lookup key 
instead of allocating a new wrapper per read (the main perf issue with the full 
`Cache` class)
    - **O(1) stale entry cleanup** — identity-based removal from 
`ReferenceQueue` instead of `entrySet().removeIf()` full-map scan
    - **Weak keys** — entries GC'd when key is no longer strongly referenced 
(same as `WeakHashMap`)
    - **Weak values** — values also held via `WeakReference`
    
    ### 2. `PathConflictResolver` memory explosion fix
    
    The `PathConflictResolver` had an O(n²) memory issue from copying 
`HashSet<Object> conflictIdsSinceRoot` at every graph node. Replaced with:
    - Parent-chain walk (`hasConflictIdOnPathToRoot()`) — O(depth) per check, 
no copying
    - `LinkedHashSet` partitions — O(1) removal instead of O(n) 
`ArrayList.remove()`
    
    ### 3. Tracking file read cache
    
    `EnhancedLocalRepositoryManager.readRepos()` re-reads 
`_remote.repositories` tracking files from disk (with file locking) on every 
artifact resolution, even for artifacts in the same directory. In a primed 
build, this causes thousands of redundant file reads with synchronized blocks 
and FileLock acquisition.
    
    Added a `ConcurrentHashMap<Path, Properties>` cache keyed by tracking file 
path. Multiple artifacts in the same directory (e.g. jar + pom) share the same 
tracking file, so the cache hit rate is high. The cache is invalidated (not 
updated) on writes via `addRepo()` to avoid a race where two concurrent writes 
could reorder their cache puts.
    
    ### 4. Lock-free fast path for `NamedLockFactorySupport`
    
    `getLockAndRefTrack()` used `ConcurrentHashMap.compute()` on every call, 
which takes a per-bucket exclusive lock even when the holder already exists. In 
the common case (lock exists, just increment refcount), this serialized all 
threads hashing to the same bucket.
    
    Added a lock-free fast path: `ConcurrentHashMap.get()` (volatile read) + 
`tryIncRef()` (CAS loop on `AtomicInteger`). Only falls back to `compute()` 
when the holder is absent or being closed.
    
    The close/acquire race is handled by a CAS sentinel: `closeLock()` marks 
the holder as closed (CAS refcount `0 → MIN_VALUE`) before destroying it. 
`tryIncRef()` rejects `refcount ≤ 0`, preventing revival of a destroyed lock.
    
    ### 5. `InhibitingNameMapper` stream→loop conversion
    
    Replaced `Stream.filter().collect()` with imperative loops and added 
empty-list short-circuit to skip the filtering entirely when no 
`LockingInhibitor`s are registered (the common case).
    
    ### 6. `FileLockNamedLockFactory` FileChannel reuse
    
    `destroyLock()` was closing the `FileChannel` on every lock release, 
forcing a new `open()` syscall on every lock acquisition. Since the lock 
factory is session-scoped, the channels can be kept open for reuse across lock 
acquire/release cycles. Channels are now closed only during factory 
`shutdown()`.
    
    ## Profiling results (async-profiler wall-clock, 5ms interval)
    
    ### End-to-end build time (Quarkus primed build, 8-thread)
    
    | Configuration | Build time | vs Baseline |
    |---|---|---|
    | Baseline (2.0.19-SNAPSHOT) | 169.7s | — |
    | + tracking file cache | ~160s | −6% |
    | + lock-free getLockAndRefTrack | 149.1s | −12% |
    | **+ FileChannel reuse + InhibitingNameMapper** | **93.5s** | **−45%** |
    
    ### Method-level sample comparison (baseline → all patches)
    
    | Method | Baseline | Patched | Change |
    |---|---|---|---|
    | `EnhancedLocalRepositoryManager.find()` | 275 | 109 | **−60%** |
    | `FileLockNamedLockFactory.createLock()` | 246 | 107 | **−56%** |
    | `readRepos()` | 208 | 71 | **−66%** |
    | `LegacyTrackingFileManager.read()` | 201 | 54 | **−73%** |
    | `Retry.retry()` | 167 | 86 | **−49%** |
    | `InhibitingNameMapper.nameLocks()` | 141 | 85 | **−40%** |
    | `getLockAndRefTrack()` | 128 | 67 | **−48%** |
    | `destroyLock()` | 46 | 0 | **−100%** |
    | `mutex()` (String.intern lock) | 46 | 0 | **eliminated** |
    
    ### ConcurrentWeakCache microbenchmark (4 threads, 8M ops/iteration)
    
    | Approach | Version Cache | Intern Pool |
    |---|---|---|
    | **Master** (synchronized blocks from #1902) | 1668ms (4.8M ops/sec) | 
916ms (8.7M ops/sec) |
    | Original synchronizedMap (pre-#1902) | 360ms (22.2M ops/sec) | 457ms 
(17.5M ops/sec) |
    | **ConcurrentWeakCache (this PR)** | **155ms (51.6M ops/sec)** | **176ms 
(45.5M ops/sec)** |
    
    **10.8x faster than master** for version parsing, **5.2x faster** for 
artifact interning, while preserving weak reference semantics for GC-friendly 
memory behavior.
    
    ## Files changed
    
    - `ConcurrentWeakCache.java` — new lightweight concurrent cache with weak 
keys/values
    - `GenericVersionScheme.java` — use `ConcurrentWeakCache` for version 
parsing cache
    - `DataPool.java` — use `ConcurrentWeakCache` for `WeakInternPool`
    - `PathConflictResolver.java` — fix O(n²) memory from HashSet copying
    - `EnhancedLocalRepositoryManager.java` — cache tracking file reads to 
eliminate redundant disk I/O
    - `NamedLockFactorySupport.java` — lock-free fast path for ref-counted lock 
acquisition
    - `InhibitingNameMapper.java` — stream→loop conversion with empty-list 
short-circuit
    - `FileLockNamedLockFactory.java` — FileChannel reuse across lock lifecycle
---
 .../impl/EnhancedLocalRepositoryManager.java       |  47 +++-
 .../aether/internal/impl/collect/DataPool.java     |  34 +--
 .../synccontext/named/InhibitingNameMapper.java    |  47 +++-
 .../named/providers/FileLockNamedLockFactory.java  | 100 +++++----
 .../named/support/NamedLockFactorySupport.java     |  45 +++-
 .../util/concurrency/ConcurrentWeakCache.java      | 250 +++++++++++++++++++++
 .../graph/transformer/PathConflictResolver.java    |  59 +++--
 .../aether/util/version/GenericVersionScheme.java  | 106 +--------
 8 files changed, 482 insertions(+), 206 deletions(-)

diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java
index dd314fed9..14bf81b3b 100644
--- 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/EnhancedLocalRepositoryManager.java
@@ -27,6 +27,7 @@ import java.util.HashSet;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Properties;
+import java.util.concurrent.ConcurrentHashMap;
 
 import org.eclipse.aether.RepositorySystemSession;
 import org.eclipse.aether.artifact.Artifact;
@@ -62,12 +63,46 @@ class EnhancedLocalRepositoryManager extends 
SimpleLocalRepositoryManager {
 
     private static final String LOCAL_REPO_ID = "";
 
+    /**
+     * Shared sentinel for "no tracking data". Mutation is forbidden: the 
instance is shared
+     * across threads via {@link #trackingFileCache} and returned directly 
from {@link #readRepos}.
+     */
+    private static final Properties EMPTY_PROPERTIES = new Properties() {
+        @Override
+        public synchronized Object put(Object key, Object value) {
+            throw new UnsupportedOperationException("EMPTY_PROPERTIES is 
read-only");
+        }
+
+        @Override
+        public synchronized Object remove(Object key) {
+            throw new UnsupportedOperationException("EMPTY_PROPERTIES is 
read-only");
+        }
+
+        @Override
+        public synchronized void clear() {
+            throw new UnsupportedOperationException("EMPTY_PROPERTIES is 
read-only");
+        }
+    };
+
     private final String trackingFilename;
 
     private final TrackingFileManager trackingFileManager;
 
     private final LocalPathPrefixComposer localPathPrefixComposer;
 
+    /**
+     * Cache of tracking file contents, keyed by tracking file path. 
Eliminates redundant disk I/O
+     * when multiple artifacts in the same directory are resolved — they all 
share the same
+     * {@code _remote.repositories} tracking file. Invalidated on writes via 
{@link #addRepo}.
+     * <p>
+     * Cached {@link Properties} instances are shared across threads and must 
be treated as
+     * read-only by callers of {@link #readRepos}. The cache is scoped to this 
manager instance
+     * (one per session), so concurrent builds in separate JVMs each maintain 
independent caches.
+     * If another process updates a tracking file after it has been cached 
here, the stale entry
+     * may cause a redundant (but harmless) download — no data corruption.
+     */
+    private final ConcurrentHashMap<Path, Properties> trackingFileCache = new 
ConcurrentHashMap<>();
+
     EnhancedLocalRepositoryManager(
             Path basedir,
             LocalPathComposer localPathComposer,
@@ -215,10 +250,10 @@ class EnhancedLocalRepositoryManager extends 
SimpleLocalRepositoryManager {
 
     private Properties readRepos(Path artifactPath) {
         Path trackingFile = getTrackingFile(artifactPath);
-
-        Properties props = trackingFileManager.read(trackingFile);
-
-        return (props != null) ? props : new Properties();
+        return trackingFileCache.computeIfAbsent(trackingFile, tf -> {
+            Properties props = trackingFileManager.read(tf);
+            return (props != null) ? props : EMPTY_PROPERTIES;
+        });
     }
 
     private void addRepo(Path artifactPath, Collection<String> repositories) {
@@ -229,6 +264,10 @@ class EnhancedLocalRepositoryManager extends 
SimpleLocalRepositoryManager {
 
         Path trackingPath = getTrackingFile(artifactPath);
 
+        // Invalidate cache before write: using put() with the returned 
Properties would be
+        // racy — two concurrent addRepo() calls could reorder their puts, 
leaving stale data.
+        // Invalidating forces the next readRepos() to re-read from disk.
+        trackingFileCache.remove(trackingPath);
         trackingFileManager.update(trackingPath, updates);
     }
 
diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DataPool.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DataPool.java
index 463dcf804..f82d589ec 100644
--- 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DataPool.java
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/collect/DataPool.java
@@ -18,14 +18,10 @@
  */
 package org.eclipse.aether.internal.impl.collect;
 
-import java.lang.ref.WeakReference;
 import java.util.Collection;
-import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
-import java.util.WeakHashMap;
 import java.util.concurrent.ConcurrentHashMap;
 
 import org.eclipse.aether.Keys;
@@ -46,6 +42,7 @@ import org.eclipse.aether.resolution.ArtifactDescriptorResult;
 import org.eclipse.aether.resolution.VersionRangeRequest;
 import org.eclipse.aether.resolution.VersionRangeResult;
 import org.eclipse.aether.util.ConfigUtils;
+import org.eclipse.aether.util.concurrency.ConcurrentWeakCache;
 import org.eclipse.aether.version.Version;
 import org.eclipse.aether.version.VersionConstraint;
 
@@ -554,33 +551,24 @@ public final class DataPool {
         }
     }
 
+    /**
+     * Intern pool backed by ConcurrentWeakCache with weak keys and weak 
values.
+     * Lock-free reads (ConcurrentHashMap.get is a volatile read, zero 
allocation via
+     * ThreadLocal lookup key), lock-striped writes, weak keys and values 
allow GC of
+     * interned objects when no longer strongly referenced.
+     * Uses putIfAbsent to guarantee concurrent callers for the same key get 
the same instance.
+     */
     private static class WeakInternPool<K, V> implements InternPool<K, V> {
-        private final Map<K, WeakReference<V>> map = 
Collections.synchronizedMap(new WeakHashMap<>(256));
+        private final ConcurrentWeakCache<K, V> cache = new 
ConcurrentWeakCache<>(256);
 
         @Override
         public V get(K key) {
-            WeakReference<V> ref = map.get(key);
-            return ref != null ? ref.get() : null;
+            return cache.get(key);
         }
 
         @Override
-        @SuppressWarnings("unchecked")
         public V intern(K key, V value) {
-            Object[] result = new Object[1];
-            synchronized (map) {
-                map.compute(key, (k, existingRef) -> {
-                    if (existingRef != null) {
-                        V pooled = existingRef.get();
-                        if (pooled != null) {
-                            result[0] = pooled;
-                            return existingRef;
-                        }
-                    }
-                    result[0] = value;
-                    return new WeakReference<>(value);
-                });
-            }
-            return (V) result[0];
+            return cache.putIfAbsent(key, value);
         }
     }
 }
diff --git 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/InhibitingNameMapper.java
 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/InhibitingNameMapper.java
index 202232d51..1fc4b433a 100644
--- 
a/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/InhibitingNameMapper.java
+++ 
b/maven-resolver-impl/src/main/java/org/eclipse/aether/internal/impl/synccontext/named/InhibitingNameMapper.java
@@ -18,9 +18,9 @@
  */
 package org.eclipse.aether.internal.impl.synccontext.named;
 
+import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
-import java.util.stream.Collectors;
 
 import org.eclipse.aether.RepositorySystemSession;
 import org.eclipse.aether.artifact.Artifact;
@@ -54,16 +54,45 @@ public class InhibitingNameMapper implements NameMapper {
             RepositorySystemSession session,
             Collection<? extends Artifact> artifacts,
             Collection<? extends Metadata> metadatas) {
-        if (artifacts != null) {
-            artifacts = artifacts.stream()
-                    .filter(a -> lockingInhibitors.stream().noneMatch(i -> 
i.preventArtifactLocking(a)))
-                    .collect(Collectors.toList());
+        if (lockingInhibitors.isEmpty()) {
+            return delegate.nameLocks(session, artifacts, metadatas);
         }
-        if (metadatas != null) {
-            metadatas = metadatas.stream()
-                    .filter(m -> lockingInhibitors.stream().noneMatch(i -> 
i.preventMetadataLocking(m)))
-                    .collect(Collectors.toList());
+        if (artifacts != null && !artifacts.isEmpty()) {
+            List<Artifact> filtered = new ArrayList<>(artifacts.size());
+            for (Artifact a : artifacts) {
+                if (!isArtifactInhibited(a)) {
+                    filtered.add(a);
+                }
+            }
+            artifacts = filtered;
+        }
+        if (metadatas != null && !metadatas.isEmpty()) {
+            List<Metadata> filtered = new ArrayList<>(metadatas.size());
+            for (Metadata m : metadatas) {
+                if (!isMetadataInhibited(m)) {
+                    filtered.add(m);
+                }
+            }
+            metadatas = filtered;
         }
         return delegate.nameLocks(session, artifacts, metadatas);
     }
+
+    private boolean isArtifactInhibited(Artifact artifact) {
+        for (LockingInhibitor inhibitor : lockingInhibitors) {
+            if (inhibitor.preventArtifactLocking(artifact)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private boolean isMetadataInhibited(Metadata metadata) {
+        for (LockingInhibitor inhibitor : lockingInhibitors) {
+            if (inhibitor.preventMetadataLocking(metadata)) {
+                return true;
+            }
+        }
+        return false;
+    }
 }
diff --git 
a/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/providers/FileLockNamedLockFactory.java
 
b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/providers/FileLockNamedLockFactory.java
index fa808987e..fcaf4583b 100644
--- 
a/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/providers/FileLockNamedLockFactory.java
+++ 
b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/providers/FileLockNamedLockFactory.java
@@ -108,60 +108,70 @@ public class FileLockNamedLockFactory extends 
NamedLockFactorySupport {
     @Override
     protected NamedLockSupport createLock(final NamedLockKey key) {
         Path path = Paths.get(URI.create(key.name()));
-        FileChannel fileChannel = fileChannels.computeIfAbsent(key, k -> {
-            try {
-                Files.createDirectories(path.getParent());
-                FileChannel channel = retry(
-                        ATTEMPTS,
-                        SLEEP_MILLIS,
-                        () -> {
-                            if (DELETE_LOCK_FILES) {
-                                return FileChannel.open(
-                                        path,
-                                        StandardOpenOption.READ,
-                                        StandardOpenOption.WRITE,
-                                        StandardOpenOption.CREATE,
-                                        StandardOpenOption.DELETE_ON_CLOSE);
-                            } else {
-                                return FileChannel.open(
-                                        path,
-                                        StandardOpenOption.READ,
-                                        StandardOpenOption.WRITE,
-                                        StandardOpenOption.CREATE);
-                            }
-                        },
-                        null,
-                        null);
-
-                if (channel == null) {
-                    throw new IllegalStateException(
-                            "Could not open file channel for '" + key + "' 
after " + ATTEMPTS + " attempts; giving up");
-                }
-                return channel;
-            } catch (InterruptedException e) {
-                Thread.currentThread().interrupt();
-                throw new RuntimeException("Interrupted while opening file 
channel for '" + key + "'", e);
-            } catch (IOException e) {
-                throw new UncheckedIOException("Failed to open file channel 
for '" + key + "'", e);
-            }
-        });
+        FileChannel fileChannel = fileChannels.computeIfAbsent(key, k -> 
openFileChannel(key, path));
+        if (!fileChannel.isOpen()) {
+            // Channel was closed externally (I/O error, NFS hiccup, etc.). 
Evict the stale entry
+            // and open a fresh one. remove(key, fileChannel) is atomic: it 
only removes if the
+            // value is still this exact (stale) instance, avoiding races with 
other threads that
+            // may have already replaced it.
+            fileChannels.remove(key, fileChannel);
+            fileChannel = fileChannels.computeIfAbsent(key, k -> 
openFileChannel(key, path));
+        }
         return new FileLockNamedLock(key, fileChannel, this);
     }
 
+    private FileChannel openFileChannel(NamedLockKey key, Path path) {
+        try {
+            Files.createDirectories(path.getParent());
+            FileChannel channel = retry(
+                    ATTEMPTS,
+                    SLEEP_MILLIS,
+                    () -> {
+                        if (DELETE_LOCK_FILES) {
+                            return FileChannel.open(
+                                    path,
+                                    StandardOpenOption.READ,
+                                    StandardOpenOption.WRITE,
+                                    StandardOpenOption.CREATE,
+                                    StandardOpenOption.DELETE_ON_CLOSE);
+                        } else {
+                            return FileChannel.open(
+                                    path, StandardOpenOption.READ, 
StandardOpenOption.WRITE, StandardOpenOption.CREATE);
+                        }
+                    },
+                    null,
+                    null);
+
+            if (channel == null) {
+                throw new IllegalStateException(
+                        "Could not open file channel for '" + key + "' after " 
+ ATTEMPTS + " attempts; giving up");
+            }
+            return channel;
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            throw new RuntimeException("Interrupted while opening file channel 
for '" + key + "'", e);
+        } catch (IOException e) {
+            throw new UncheckedIOException("Failed to open file channel for '" 
+ key + "'", e);
+        }
+    }
+
     @Override
     protected void destroyLock(final NamedLock namedLock) {
-        if (namedLock instanceof FileLockNamedLock) {
-            final NamedLockKey key = namedLock.key();
-            FileChannel fileChannel = fileChannels.remove(key);
-            if (fileChannel == null) {
-                throw new IllegalStateException("File channel expected, but 
does not exist: " + key);
-            }
+        // Keep the FileChannel open in the fileChannels map for reuse by 
future createLock() calls.
+        // Opening a FileChannel is a syscall (open/creat) that shows up as a 
hotspot when locks are
+        // acquired and released frequently (e.g., per-artifact resolution in 
primed builds).
+        // Channels are closed on factory shutdown via doShutdown().
+    }
 
+    @Override
+    protected void doShutdown() {
+        for (FileChannel channel : fileChannels.values()) {
             try {
-                fileChannel.close();
+                channel.close();
             } catch (IOException e) {
-                throw new UncheckedIOException("Failed to close file channel 
for '" + key + "'", e);
+                logger.warn("Failed to close file channel", e);
             }
         }
+        fileChannels.clear();
     }
 }
diff --git 
a/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/NamedLockFactorySupport.java
 
b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/NamedLockFactorySupport.java
index bee186afc..47fa4895b 100644
--- 
a/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/NamedLockFactorySupport.java
+++ 
b/maven-resolver-named-locks/src/main/java/org/eclipse/aether/named/support/NamedLockFactorySupport.java
@@ -115,14 +115,28 @@ public abstract class NamedLockFactorySupport implements 
NamedLockFactory {
     }
 
     protected NamedLock getLockAndRefTrack(final NamedLockKey key, 
Supplier<NamedLockSupport> supplier) {
+        if (shutdown.get()) {
+            throw new IllegalStateException("factory already shut down");
+        }
+        // Fast path: lock-free volatile read + atomic CAS increment.
+        // ConcurrentHashMap.get() is a volatile read — no bucket locking.
+        // In the common case (lock already exists), this avoids compute()'s
+        // per-bucket exclusive lock entirely.
+        NamedLockHolder holder = locks.get(key);
+        if (holder != null && holder.tryIncRef()) {
+            return holder.namedLock;
+        }
+        // Slow path: holder absent or being closed (refcount hit 0).
+        // Use compute() to atomically create a new holder.
         return locks.compute(key, (k, v) -> {
                     if (shutdown.get()) {
                         throw new IllegalStateException("factory already shut 
down");
                     }
-                    if (v == null) {
+                    if (v == null || !v.tryIncRef()) {
                         v = new NamedLockHolder(supplier.get());
+                        v.incRef();
                     }
-                    return v.incRef();
+                    return v;
                 })
                 .namedLock;
     }
@@ -170,8 +184,14 @@ public abstract class NamedLockFactorySupport implements 
NamedLockFactory {
     public void closeLock(final NamedLockKey key) {
         locks.compute(key, (k, v) -> {
             if (v != null && v.decRef() == 0) {
-                destroyLock(v.namedLock);
-                return null;
+                // Mark as closed to prevent a concurrent tryIncRef (lock-free 
fast path)
+                // from reviving this holder. CAS ensures atomicity: if 
tryIncRef already
+                // incremented from 0→1, our CAS fails and we keep the holder 
alive.
+                if (v.referenceCount.compareAndSet(0, Integer.MIN_VALUE)) {
+                    destroyLock(v.namedLock);
+                    return null;
+                }
+                // A concurrent tryIncRef succeeded — holder is still in use, 
keep it
             }
             return v;
         });
@@ -206,6 +226,23 @@ public abstract class NamedLockFactorySupport implements 
NamedLockFactory {
             return this;
         }
 
+        /**
+         * Atomically tries to increment the reference count. Returns {@code 
false} if the
+         * holder has been closed (refcount &le; 0), preventing revival of a 
destroyed lock.
+         * Used by the lock-free fast path in {@link #getLockAndRefTrack}.
+         */
+        private boolean tryIncRef() {
+            while (true) {
+                int current = referenceCount.get();
+                if (current <= 0) {
+                    return false;
+                }
+                if (referenceCount.compareAndSet(current, current + 1)) {
+                    return true;
+                }
+            }
+        }
+
         private int decRef() {
             return referenceCount.decrementAndGet();
         }
diff --git 
a/maven-resolver-util/src/main/java/org/eclipse/aether/util/concurrency/ConcurrentWeakCache.java
 
b/maven-resolver-util/src/main/java/org/eclipse/aether/util/concurrency/ConcurrentWeakCache.java
new file mode 100644
index 000000000..4f15a0749
--- /dev/null
+++ 
b/maven-resolver-util/src/main/java/org/eclipse/aether/util/concurrency/ConcurrentWeakCache.java
@@ -0,0 +1,250 @@
+/*
+ * 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.concurrency;
+
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * A concurrent cache with weak keys and weak values, inspired by maven-impl's 
Cache class
+ * but stripped down and optimized for hot-path performance.
+ * <p>
+ * Design compared to the full Cache:
+ * <ul>
+ *   <li><b>Zero allocation on get()</b> — uses a ThreadLocal reusable lookup 
key
+ *       instead of allocating a new wrapper on every read</li>
+ *   <li><b>O(1) cleanup per stale entry</b> — uses identity-based removal from
+ *       the ReferenceQueue instead of a full {@code entrySet().removeIf()} 
scan</li>
+ *   <li><b>Lock-free reads</b> — {@link ConcurrentHashMap#get} is a volatile 
read
+ *       with no lock acquisition</li>
+ *   <li><b>Lock-striped writes</b> — ConcurrentHashMap uses fine-grained 
locking</li>
+ * </ul>
+ * <p>
+ * Keys are held via {@link WeakReference}: when a key is no longer strongly 
referenced
+ * elsewhere, its entry becomes eligible for garbage collection. Values are 
also held
+ * via WeakReference. Stale entries are cleaned up lazily on {@link #put}.
+ *
+ * @param <K> the type of keys
+ * @param <V> the type of values
+ * @since 2.0.19
+ */
+public class ConcurrentWeakCache<K, V> {
+
+    private final ConcurrentHashMap<Object, WeakReference<V>> map;
+    private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
+
+    @SuppressWarnings("rawtypes")
+    private static final ThreadLocal<LookupKey> LOOKUP_KEY = new 
ThreadLocal<LookupKey>() {
+        @Override
+        protected LookupKey initialValue() {
+            return new LookupKey();
+        }
+    };
+
+    /**
+     * Creates a new cache with default initial capacity.
+     */
+    public ConcurrentWeakCache() {
+        this.map = new ConcurrentHashMap<>();
+    }
+
+    /**
+     * Creates a new cache with the given initial capacity.
+     *
+     * @param initialCapacity the initial capacity
+     */
+    public ConcurrentWeakCache(int initialCapacity) {
+        this.map = new ConcurrentHashMap<>(initialCapacity);
+    }
+
+    /**
+     * Returns the value for the given key, or {@code null} if the key is not 
present
+     * or the value has been garbage collected.
+     * <p>
+     * This method is lock-free and allocates no objects.
+     *
+     * @param key the key to look up
+     * @return the cached value, or {@code null}
+     */
+    @SuppressWarnings("unchecked")
+    public V get(K key) {
+        LookupKey<K> lookupKey = LOOKUP_KEY.get();
+        lookupKey.set(key);
+        try {
+            WeakReference<V> ref = map.get(lookupKey);
+            return ref != null ? ref.get() : null;
+        } finally {
+            lookupKey.clear();
+        }
+    }
+
+    /**
+     * Stores a key-value pair in the cache. Both key and value are held via 
weak references.
+     * <p>
+     * Also performs lazy cleanup of entries whose keys have been garbage 
collected.
+     *
+     * @param key   the key
+     * @param value the value
+     */
+    public void put(K key, V value) {
+        expungeStaleEntries();
+        map.put(new WeakKey<>(key, queue), new WeakReference<>(value));
+    }
+
+    /**
+     * If the key is not already present (or its value has been GC'd), stores 
the key-value pair
+     * and returns the given value. If the key is already present with a live 
value, returns the
+     * existing value without storing. Concurrent callers for the same key are 
guaranteed to
+     * receive the same instance (the insert-or-keep decision is atomic per 
key via
+     * {@link ConcurrentHashMap#merge}).
+     * <p>
+     * Also performs lazy cleanup of entries whose keys have been garbage 
collected.
+     *
+     * @param key   the key
+     * @param value the value to store if absent
+     * @return the existing value if present, or the given value if newly 
stored
+     */
+    public V putIfAbsent(K key, V value) {
+        // Fast path: lock-free lookup, zero allocation (ThreadLocal LookupKey)
+        V existing = get(key);
+        if (existing != null) {
+            return existing;
+        }
+        // Slow path: atomic insert-or-keep via merge() — locks only the 
target bucket,
+        // so contention is per-key, not global. Handles the case where an 
existing entry's
+        // value has been GC'd by atomically replacing it with the new value.
+        expungeStaleEntries();
+        WeakReference<V> newRef = new WeakReference<>(value);
+        WeakReference<V> ref = map.merge(new WeakKey<>(key, queue), newRef, 
(existingRef, incoming) -> {
+            V existingValue = existingRef.get();
+            return existingValue != null ? existingRef : incoming;
+        });
+        if (ref != newRef) {
+            V existingValue = ref.get();
+            if (existingValue != null) {
+                return existingValue;
+            }
+        }
+        return value;
+    }
+
+    /**
+     * Returns the number of entries in the cache (including possibly stale 
ones).
+     *
+     * @return the cache size
+     */
+    public int size() {
+        return map.size();
+    }
+
+    /**
+     * Removes entries whose weak-reference keys have been garbage collected.
+     * Called automatically on {@link #put}. Each stale entry is removed in 
O(1)
+     * via identity match — no map scan required.
+     */
+    private void expungeStaleEntries() {
+        Object staleKey;
+        while ((staleKey = queue.poll()) != null) {
+            // ConcurrentHashMap.remove() checks (storedKey == staleKey) first.
+            // Since the staleKey IS the same WeakKey object that was stored in
+            // the map, identity matches and the entry is removed in O(1).
+            map.remove(staleKey);
+        }
+    }
+
+    /**
+     * Reusable lookup key stored in a ThreadLocal for zero-allocation reads.
+     * Used only for {@link ConcurrentHashMap#get} lookups, never stored in 
the map.
+     * <p>
+     * Implements equals/hashCode to match {@link WeakKey} entries in the map:
+     * ConcurrentHashMap.get(lookupKey) calls {@code 
lookupKey.equals(storedWeakKey)},
+     * which delegates to {@code key.equals(storedWeakKey.get())}.
+     */
+    static class LookupKey<K> {
+        private K key;
+        private int hash;
+
+        void set(K key) {
+            this.key = key;
+            this.hash = key.hashCode();
+        }
+
+        void clear() {
+            this.key = null; // prevent leak via ThreadLocal
+        }
+
+        @Override
+        public int hashCode() {
+            return hash;
+        }
+
+        @Override
+        @SuppressWarnings("rawtypes")
+        public boolean equals(Object o) {
+            if (o instanceof WeakKey) {
+                Object otherKey = ((WeakKey) o).get();
+                return key != null && key.equals(otherKey);
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Weak-reference key stored in the map. When the referent is garbage 
collected,
+     * this object is enqueued in the {@link ReferenceQueue} for cleanup.
+     * <p>
+     * The cleanup path uses identity: {@code map.remove(staleWeakKey)} 
matches because
+     * ConcurrentHashMap checks {@code storedKey == staleWeakKey} before 
calling equals().
+     * Since the queued WeakKey IS the same object that's in the map, this is 
always true.
+     */
+    static class WeakKey<K> extends WeakReference<K> {
+        private final int hash;
+
+        @SuppressWarnings("unchecked")
+        WeakKey(K key, ReferenceQueue<? super K> queue) {
+            super(key, (ReferenceQueue<? super K>) queue);
+            this.hash = key.hashCode();
+        }
+
+        @Override
+        public int hashCode() {
+            return hash;
+        }
+
+        @Override
+        @SuppressWarnings("rawtypes")
+        public boolean equals(Object o) {
+            if (this == o) {
+                return true; // identity match — used for cleanup and normal 
dedup
+            }
+            K k = get();
+            if (k == null) {
+                return false; // referent GC'd
+            }
+            if (o instanceof WeakKey) {
+                return k.equals(((WeakKey) o).get());
+            }
+            if (o instanceof LookupKey) {
+                return k.equals(((LookupKey) o).key);
+            }
+            return false;
+        }
+    }
+}
diff --git 
a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/PathConflictResolver.java
 
b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/PathConflictResolver.java
index 92e3bd495..20b1b6f9a 100644
--- 
a/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/PathConflictResolver.java
+++ 
b/maven-resolver-util/src/main/java/org/eclipse/aether/util/graph/transformer/PathConflictResolver.java
@@ -22,7 +22,7 @@ import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
+import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -175,7 +175,7 @@ public final class PathConflictResolver extends 
ConflictResolver {
         // loop over topographically sorted conflictIds
         for (String conflictId : sortedConflictIds) {
             // paths in given conflict group to consider
-            List<Path> paths = state.partitions.get(conflictId);
+            Set<Path> paths = state.partitions.get(conflictId);
             if (paths.isEmpty()) {
                 // this means that whole group "fall out of scope" (are all on 
loser branches); skip
                 continue;
@@ -208,7 +208,7 @@ public final class PathConflictResolver extends 
ConflictResolver {
             }
             state.resolvedIds.put(conflictId, winnerPath);
 
-            // loop over considered paths and apply selection results; note: 
node may remove itself from iterated list
+            // loop over considered paths and apply selection results; note: 
node may remove itself from iterated set
             for (Path path : new ArrayList<>(paths)) {
                 // apply selected properties scope/optional to winner (winner 
carries version; others are losers)
                 if (path == winnerPath) {
@@ -230,7 +230,7 @@ public final class PathConflictResolver extends 
ConflictResolver {
             stats.put("ConflictResolver.totalTime", time2 - time1);
             stats.put(
                     "ConflictResolver.conflictItemCount",
-                    
state.partitions.values().stream().map(List::size).reduce(0, Integer::sum));
+                    
state.partitions.values().stream().map(Set::size).reduce(0, Integer::sum));
         }
 
         return node;
@@ -279,8 +279,9 @@ public final class PathConflictResolver extends 
ConflictResolver {
         /**
          * A mapping from conflictId to paths represented as {@link Path}s 
that exist for each conflictId. In other
          * words all paths to each {@link DependencyNode} that are member of 
same conflictId group.
+         * Uses {@link LinkedHashSet} for O(1) removal in {@link 
Path#moveOutOfScope()} while preserving insertion order.
          */
-        private final Map<String, List<Path>> partitions;
+        private final Map<String, Set<Path>> partitions;
 
         /**
          * A mapping from conflictIds to winner {@link Path}, hence {@link 
DependencyNode}  for given conflictId.
@@ -322,12 +323,7 @@ public final class PathConflictResolver extends 
ConflictResolver {
          */
         private Path build(DependencyNode node) throws RepositoryException {
             String nodeConflictId = this.conflictIds.get(node);
-            Path root = new Path(
-                    this,
-                    node,
-                    nodeConflictId,
-                    nodeConflictId != null ? 
Collections.singleton(nodeConflictId) : Collections.emptySet(),
-                    null);
+            Path root = new Path(this, node, nodeConflictId, null);
             gatherCRNodes(root);
             return root;
         }
@@ -381,11 +377,6 @@ public final class PathConflictResolver extends 
ConflictResolver {
         private DependencyNode dn;
         private final String conflictId;
         private final Path parent;
-        // Set of conflictIds that we "stepped over" from root to here; is a 
set, and if duplicate element addition is
-        // attempted, it signals we deal with a cycle, as we are about to 
enter into same tree partition (nodes with
-        // same conflictId) we already have been. This could be a list, but 
for our purposes "duplication detection"
-        // (loop) is perfectly enough.
-        private final Set<String> conflictIdsSinceRoot;
         // derived
         private final int depth;
         private final List<Path> children;
@@ -393,22 +384,37 @@ public final class PathConflictResolver extends 
ConflictResolver {
         private String scope;
         private boolean optional;
 
-        private Path(State state, DependencyNode dn, String conflictId, 
Set<String> conflictIdsSinceRoot, Path parent) {
+        private Path(State state, DependencyNode dn, String conflictId, Path 
parent) {
             this.state = state;
             this.dn = dn;
             this.conflictId = conflictId;
             this.parent = parent;
-            this.conflictIdsSinceRoot = conflictIdsSinceRoot;
             this.depth = parent != null ? parent.depth + 1 : 0;
             this.children = new ArrayList<>();
             pull(0);
 
             this.state
                     .partitions
-                    .computeIfAbsent(this.conflictId, k -> new ArrayList<>())
+                    .computeIfAbsent(this.conflictId, k -> new 
LinkedHashSet<>())
                     .add(this);
         }
 
+        /**
+         * Checks whether the given conflictId appears on the path from this 
node to the root.
+         * This replaces the previous approach of copying a HashSet at every 
child, which was the
+         * dominant source of memory consumption (millions of HashMap.Node 
objects for large graphs).
+         */
+        private boolean hasConflictIdOnPathToRoot(String targetConflictId) {
+            Path current = this;
+            while (current != null) {
+                if (Objects.equals(current.conflictId, targetConflictId)) {
+                    return true;
+                }
+                current = current.parent;
+            }
+            return false;
+        }
+
         /**
          * Pulls (possibly updated) scope and optional values from associated 
{@link DependencyNode} to this instance,
          * going down toward children recursively the required count of levels.
@@ -614,6 +620,7 @@ public final class PathConflictResolver extends 
ConflictResolver {
         /**
          * Removes this and all child {@link Path} nodes from winner selection 
scope; essentially marks whole subtree
          * from "this and below" as loser, to not be considered in subsequent 
winner selections.
+         * Uses {@link LinkedHashSet} for O(1) removal instead of the previous 
O(N) ArrayList.remove.
          */
         private void moveOutOfScope() {
             this.state.partitions.get(this.conflictId).remove(this);
@@ -626,8 +633,9 @@ public final class PathConflictResolver extends 
ConflictResolver {
          * Adds node children: this method should be "batch" used, as all 
(potential) children should be added at once.
          * Method will return really added {@link Path} instances, as this 
class avoids cycles. Those forming a cycle
          * are added to {@link Path} structure but are not recursed (not 
returned in list), keeping {@link Path} cycle
-         * free. This method also maintains "conflictIdsSinceRoot", that is a 
set of "conflict IDs" that were touched
-         * while going from tree root to current node instance.
+         * free. Cycle detection is performed by walking up the parent chain 
via
+         * {@link #hasConflictIdOnPathToRoot(String)} instead of maintaining a 
per-node set, trading O(depth) per
+         * check for dramatically reduced memory allocation on large 
dependency graphs.
          * This implies that this conflict resolver, by its nature "redoes" the
          * {@link TransformationContextKeys#CYCLIC_CONFLICT_IDS} calculated by 
{@link ConflictIdSorter}.
          */
@@ -635,9 +643,12 @@ public final class PathConflictResolver extends 
ConflictResolver {
             ArrayList<Path> added = new ArrayList<>(children.size());
             for (DependencyNode child : children) {
                 String childConflictId = this.state.conflictIds.get(child);
-                Set<String> conflictIdsSinceRoot = new 
HashSet<>(this.conflictIdsSinceRoot);
-                boolean cycle = !conflictIdsSinceRoot.add(childConflictId);
-                Path c = new Path(this.state, child, childConflictId, 
conflictIdsSinceRoot, this);
+                // Detect cycles by walking up the parent chain instead of 
copying a HashSet per child.
+                // This trades O(depth) per check for eliminating the dominant 
memory consumer:
+                // previously, each Path copied its parent's entire 
conflict-ID set, creating millions
+                // of HashMap.Node objects for large graphs.
+                boolean cycle = hasConflictIdOnPathToRoot(childConflictId);
+                Path c = new Path(this.state, child, childConflictId, this);
                 this.children.add(c);
                 c.derive(0, false);
                 if (!cycle) {
diff --git 
a/maven-resolver-util/src/main/java/org/eclipse/aether/util/version/GenericVersionScheme.java
 
b/maven-resolver-util/src/main/java/org/eclipse/aether/util/version/GenericVersionScheme.java
index 5a8428f56..01772fa3f 100644
--- 
a/maven-resolver-util/src/main/java/org/eclipse/aether/util/version/GenericVersionScheme.java
+++ 
b/maven-resolver-util/src/main/java/org/eclipse/aether/util/version/GenericVersionScheme.java
@@ -19,12 +19,8 @@
 package org.eclipse.aether.util.version;
 
 import java.nio.charset.StandardCharsets;
-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.util.concurrency.ConcurrentWeakCache;
 import org.eclipse.aether.version.InvalidVersionSpecificationException;
 
 /**
@@ -54,102 +50,18 @@ import 
org.eclipse.aether.version.InvalidVersionSpecificationException;
  */
 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
-        if (isStatisticsEnabled()) {
-            Runtime.getRuntime().addShutdownHook(new 
Thread(GenericVersionScheme::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;
-    }
+    // Concurrent cache with weak keys and weak values: lock-free reads 
(volatile read, no lock
+    // acquisition via ConcurrentHashMap), lock-striped writes, zero 
allocation on get() via
+    // ThreadLocal lookup key, weak references allow GC under memory pressure.
+    private final ConcurrentWeakCache<String, GenericVersion> versionCache = 
new ConcurrentWeakCache<>();
 
     @Override
     public GenericVersion parseVersion(final String version) throws 
InvalidVersionSpecificationException {
-        totalRequests.incrementAndGet();
-        GLOBAL_TOTAL_REQUESTS.incrementAndGet();
-
-        boolean[] created = {false};
-        GenericVersion result;
-        synchronized (versionCache) {
-            result = versionCache.computeIfAbsent(version, v -> {
-                created[0] = true;
-                return new GenericVersion(v);
-            });
-        }
-        if (created[0]) {
-            cacheMisses.incrementAndGet();
-            GLOBAL_CACHE_MISSES.incrementAndGet();
-        } else {
-            cacheHits.incrementAndGet();
-            GLOBAL_CACHE_HITS.incrementAndGet();
+        GenericVersion v = versionCache.get(version);
+        if (v == null) {
+            v = versionCache.putIfAbsent(version, new GenericVersion(version));
         }
-        return result;
-    }
-
-    /**
-     * 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 ===");
+        return v;
     }
 
     /**

Reply via email to