You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by su...@apache.org on 2026/08/23 02:41:19 UTC
(groovy) branch GROOVY-12285 updated (9f2e622417 -> 850977fcfb)
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a change to branch GROOVY-12285
in repository https://gitbox.apache.org/repos/asf/groovy.git
omit 9f2e622417 GROOVY-12285: add STC performance verification benchmarks
omit 77ee1a538c GROOVY-12285: STC: index extension methods by name and skip cloning non-generic parameters
new 850977fcfb GROOVY-12285: STC: index extension methods by name and skip cloning non-generic parameters
This update added new revisions after undoing existing revisions.
That is to say, some revisions that were in the old version of the
branch are not in the new version. This situation occurs
when a user --force pushes a change and generates a repository
containing something like this:
* -- * -- B -- O -- O -- O (9f2e622417)
\
N -- N -- N refs/heads/GROOVY-12285 (850977fcfb)
You should already have received notification emails for all of the O
revisions, and so the following emails describe only the N revisions
from the common base, B.
Any revisions marked "omit" are not gone; other references still
refer to them. Any revisions marked "discard" are gone forever.
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
.../stc/AbstractExtensionMethodCache.java | 52 +++++++++++++---
.../transform/stc/StaticTypeCheckingSupport.java | 22 ++++++-
.../transform/stc/DgmMethodLookupTest.groovy | 43 +++++++++++++
.../stc/StaticTypeCheckingSupportTest.groovy | 72 ++++++++++++++++++++--
.../apache/groovy/perf/ChooseBestMethodBench.java | 2 -
.../apache/groovy/perf/DgmMethodLookupBench.java | 1 -
6 files changed, 172 insertions(+), 20 deletions(-)
(groovy) 01/01: GROOVY-12285: STC: index extension methods by name and skip cloning non-generic parameters
Posted by su...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
asf-gitbox-commits pushed a commit to branch GROOVY-12285
in repository https://gitbox.apache.org/repos/asf/groovy.git
commit 850977fcfb00b02e7ee7ea318bbde750ae2c550b
Author: Daniel Sun <su...@apache.org>
AuthorDate: Sat Aug 22 17:06:34 2026 +0900
GROOVY-12285: STC: index extension methods by name and skip cloning non-generic parameters
---
ARCHITECTURE.md | 4 +
.../stc/AbstractExtensionMethodCache.java | 130 +++++++++-
.../groovy/transform/stc/ExtensionMethodCache.java | 3 +-
.../transform/stc/StaticTypeCheckingSupport.java | 71 ++++--
.../transform/stc/DgmMethodLookupTest.groovy | 269 +++++++++++++++++++++
.../stc/StaticTypeCheckingSupportTest.groovy | 171 +++++++++++++
.../apache/groovy/perf/ChooseBestMethodBench.java | 180 ++++++++++++++
.../apache/groovy/perf/DgmMethodLookupBench.java | 122 ++++++++++
.../apache/groovy/perf/StcCompilePerfBench.java | 179 ++++++++++++++
9 files changed, 1098 insertions(+), 31 deletions(-)
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index a7f29ab2af..5c73176864 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -140,6 +140,10 @@ verbatim keeps the reference precise; paraphrasing tends to drift.
- Extensible from user code via type-checking extension scripts; see
`src/spec/doc/_type-checking-extensions.adoc` for the user-facing
documentation of that mechanism.
+- Extension methods consulted during checking are cached per class
+ loader (`ExtensionMethodCache`). Each receiver's method list is
+ indexed by name so a lookup does not scan every DGM method on types
+ such as `Object` or `Collection`.
### Class generation (phase 7)
diff --git a/src/main/java/org/codehaus/groovy/transform/stc/AbstractExtensionMethodCache.java b/src/main/java/org/codehaus/groovy/transform/stc/AbstractExtensionMethodCache.java
index fb298110fb..9af8c477af 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/AbstractExtensionMethodCache.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/AbstractExtensionMethodCache.java
@@ -28,6 +28,7 @@ import org.codehaus.groovy.runtime.m12n.MetaInfExtensionModule;
import org.codehaus.groovy.runtime.memoize.EvictableCache;
import org.codehaus.groovy.runtime.memoize.StampedCommonCache;
+import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -37,6 +38,8 @@ import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
+import java.util.RandomAccess;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.function.Function;
@@ -45,11 +48,16 @@ import java.util.function.Predicate;
import static org.codehaus.groovy.ast.ClassHelper.makeWithoutCaching;
/**
+ * Per-{@link ClassLoader} cache of extension methods (DGM and friends, or
+ * another mapper such as macro methods). Values are immutable lists that
+ * also carry a name index, so a named lookup does not scan every method
+ * stored under a receiver (there are hundreds on {@code Object}).
+ *
* @since 3.0.0
*/
public abstract class AbstractExtensionMethodCache {
/** Caches extension methods per class loader. */
- final EvictableCache<ClassLoader, Map<String, List<MethodNode>>> cache = new StampedCommonCache<>(new WeakHashMap<>());
+ private final EvictableCache<ClassLoader, Map<String, List<MethodNode>>> cache = new StampedCommonCache<>(new WeakHashMap<>());
/** Caches, per class loader, the names of extension methods that declare {@code @ClassTag(preempt=true)} (GROOVY-12115). */
private final EvictableCache<ClassLoader, Set<String>> preemptiveNamesCache = new StampedCommonCache<>(new WeakHashMap<>());
private final String disabledString = SystemUtil.getSystemPropertySafe(getDisablePropertyName());
@@ -58,17 +66,61 @@ public abstract class AbstractExtensionMethodCache {
/**
* Returns the cached extension methods for the supplied class loader.
+ * Each list is immutable and indexed by method name.
*/
- public Map<String, List<MethodNode>> get(ClassLoader loader) {
+ public final Map<String, List<MethodNode>> get(ClassLoader loader) {
return cache.getAndPut(loader, this::getMethodsFromClassLoader);
}
+ /**
+ * Returns methods stored under {@code key} whose
+ * {@linkplain MethodNode#getName() name} is {@code name}.
+ * Never {@code null}; empty when the key is absent or no method of
+ * that name exists. The name index is built when the loader's
+ * methods are scanned, so this is a hash get rather than a linear
+ * scan of the receiver's DGM methods.
+ */
+ List<MethodNode> get(final ClassLoader loader, final String key, final String name) {
+ List<MethodNode> methods = get(loader).get(key);
+ if (methods == null || methods.isEmpty()) {
+ return Collections.emptyList();
+ }
+ if (methods instanceof MethodsByName) {
+ return ((MethodsByName) methods).named(name);
+ }
+ List<MethodNode> matches = new ArrayList<>(2);
+ for (MethodNode method : methods) {
+ if (method.getName().equals(name)) {
+ matches.add(method);
+ }
+ }
+ return matches.isEmpty() ? Collections.emptyList() : Collections.unmodifiableList(matches);
+ }
+
+ /**
+ * Drops cached extension methods for the supplied class loader,
+ * including derived data such as the preemptive-name set.
+ */
+ void invalidate(final ClassLoader loader) {
+ cache.remove(loader);
+ preemptiveNamesCache.remove(loader);
+ }
+
+ /**
+ * Drops all cached extension methods and derived indexes.
+ */
+ void invalidateAll() {
+ cache.clearAll();
+ preemptiveNamesCache.clearAll();
+ }
+
/**
* Returns the names of extension methods that declare {@code @ClassTag(preempt=true)}, for the
* supplied class loader (GROOVY-12115). Preemption matching consults this to skip the full
* candidate scan for the overwhelmingly common name that has no preemptive extension overload;
- * derived once (lazily) from the already-cached method map and sharing its loader lifecycle, so
- * it never goes stale independently. Out of the box this holds only {@code "withDefault"}.
+ * derived once (lazily) from the already-cached method map. {@link #invalidate(ClassLoader)}
+ * and {@link #invalidateAll()} drop this set with the method map so it cannot go stale
+ * independently. Out of the box this holds only {@code "withDefault"}.
*/
Set<String> getPreemptiveNames(ClassLoader loader) {
return preemptiveNamesCache.getAndPut(loader, l -> {
@@ -129,9 +181,13 @@ public abstract class AbstractExtensionMethodCache {
return methods;
}
- private Map<String, List<MethodNode>> makeMethodsUnmodifiable(Map<String, List<MethodNode>> methods) {
- methods.replaceAll((k, v) -> Collections.unmodifiableList(v));
-
+ /**
+ * Freezes each per-key list as a {@link MethodsByName} so callers can
+ * look up overloads by name without a linear scan. {@link #get(ClassLoader, String, String)}
+ * relies on this wrapping.
+ */
+ private Map<String, List<MethodNode>> makeMethodsUnmodifiable(final Map<String, List<MethodNode>> methods) {
+ methods.replaceAll((k, v) -> new MethodsByName(v));
return Collections.unmodifiableMap(methods);
}
@@ -194,4 +250,64 @@ public abstract class AbstractExtensionMethodCache {
List<MethodNode> nodes = accumulator.computeIfAbsent(key, k -> new ArrayList<>());
nodes.add(node);
}
+
+ /**
+ * Immutable random-access list of extension methods with a name index.
+ * Built once per cache key when a loader is scanned; the lists stored
+ * in {@link #named(String)} are themselves unmodifiable.
+ */
+ private static final class MethodsByName extends AbstractList<MethodNode> implements RandomAccess {
+ private static final MethodNode[] EMPTY = new MethodNode[0];
+
+ private final MethodNode[] methods;
+ private final Map<String, List<MethodNode>> byName;
+
+ MethodsByName(final List<MethodNode> source) {
+ this.methods = source.toArray(EMPTY);
+ int count = this.methods.length;
+ if (count == 0) {
+ this.byName = Collections.emptyMap();
+ } else if (count == 1) {
+ MethodNode m = this.methods[0];
+ this.byName = Collections.singletonMap(m.getName(), Collections.singletonList(m));
+ } else if (allSameName(this.methods)) {
+ this.byName = Collections.singletonMap(this.methods[0].getName(), this);
+ } else {
+ Map<String, List<MethodNode>> index = new HashMap<>(Math.max(4, (int) (count / 0.75f) + 1));
+ for (MethodNode method : this.methods) {
+ index.computeIfAbsent(method.getName(), k -> new ArrayList<>(2)).add(method);
+ }
+ index.replaceAll((k, v) -> v.size() == 1
+ ? Collections.singletonList(v.get(0))
+ : Collections.unmodifiableList(v));
+ this.byName = Collections.unmodifiableMap(index);
+ }
+ }
+
+ private static boolean allSameName(final MethodNode[] methods) {
+ String firstName = methods[0].getName();
+ for (int i = 1, n = methods.length; i < n; i += 1) {
+ if (!firstName.equals(methods[i].getName())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ List<MethodNode> named(final String name) {
+ List<MethodNode> found = byName.get(name);
+ return found != null ? found : Collections.emptyList();
+ }
+
+ @Override
+ public MethodNode get(final int index) {
+ Objects.checkIndex(index, methods.length);
+ return methods[index];
+ }
+
+ @Override
+ public int size() {
+ return methods.length;
+ }
+ }
}
diff --git a/src/main/java/org/codehaus/groovy/transform/stc/ExtensionMethodCache.java b/src/main/java/org/codehaus/groovy/transform/stc/ExtensionMethodCache.java
index a55d2af646..49b524204f 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/ExtensionMethodCache.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/ExtensionMethodCache.java
@@ -35,7 +35,8 @@ import static org.codehaus.groovy.runtime.metaclass.MetaClassRegistryImpl.EXTENS
* This class is used to make extension methods lookup faster. Basically, it will only
* collect the list of extension methods (see {@link ExtensionModule}) if the list of
* extension modules has changed. It avoids recomputing the whole list each time we perform
- * a method lookup.
+ * a method lookup. Cached lists are indexed by method name so the static type checker can
+ * collect DGM candidates without a linear scan of every method on the receiver type.
*/
public class ExtensionMethodCache extends AbstractExtensionMethodCache {
/** Shared cache instance used by static type checking. */
diff --git a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingSupport.java b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingSupport.java
index e7405611be..869c5e8a72 100644
--- a/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingSupport.java
+++ b/src/main/java/org/codehaus/groovy/transform/stc/StaticTypeCheckingSupport.java
@@ -273,17 +273,18 @@ public abstract class StaticTypeCheckingSupport {
protected static final ExtensionMethodCache EXTENSION_METHOD_CACHE = ExtensionMethodCache.INSTANCE;
/**
- * Clears cached extension methods for the supplied class loader.
+ * Drops cached extension methods for the supplied class loader,
+ * including the per-receiver name index and the preemptive-name set.
*/
public static void clearExtensionMethodCache(final ClassLoader loader) {
- EXTENSION_METHOD_CACHE.cache.remove(loader);
+ EXTENSION_METHOD_CACHE.invalidate(loader);
}
/**
- * Clears all cached extension methods.
+ * Drops all cached extension methods and derived indexes.
*/
public static void clearExtensionMethodCache() {
- EXTENSION_METHOD_CACHE.cache.clearAll();
+ EXTENSION_METHOD_CACHE.invalidateAll();
}
/**
@@ -341,6 +342,8 @@ public abstract class StaticTypeCheckingSupport {
/**
* Returns extension methods with the supplied name for the receiver hierarchy.
+ * Uses the name-indexed extension-method cache; repeated lookups of the same
+ * name on the same type do not rescan that type's DGM methods.
*/
public static Set<MethodNode> findDGMMethodsForClassNode(final ClassLoader loader, final ClassNode clazz, final String name) {
TreeSet<MethodNode> accumulator = new TreeSet<>(DGM_METHOD_NODE_COMPARATOR);
@@ -358,14 +361,11 @@ public abstract class StaticTypeCheckingSupport {
/**
* Collects extension methods with the supplied name for the receiver hierarchy.
+ * Named lookup uses the per-receiver index built when the loader's extension
+ * methods were scanned, so it does not walk every DGM method on the type.
*/
protected static void findDGMMethodsForClassNode(final ClassLoader loader, final ClassNode clazz, final String name, final TreeSet<MethodNode> accumulator) {
- List<MethodNode> fromDGM = EXTENSION_METHOD_CACHE.get(loader).get(clazz.getName());
- if (fromDGM != null) {
- for (MethodNode node : fromDGM) {
- if (node.getName().equals(name)) accumulator.add(node);
- }
- }
+ accumulator.addAll(EXTENSION_METHOD_CACHE.get(loader, clazz.getName(), name));
for (ClassNode node : clazz.getInterfaces()) {
findDGMMethodsForClassNode(loader, node, name, accumulator);
}
@@ -1100,19 +1100,7 @@ public abstract class StaticTypeCheckingSupport {
// phase 1: argument-parameter distance classifier
int bestDist = Integer.MAX_VALUE;
for (MethodNode method : methods) {
- Parameter[] parameters = method.getParameters();
- int nParameters = parameters.length;
- if (nParameters > 0) {
- parameters = parameters.clone();
- for (int i = 0; i < nParameters; i += 1) {
- Parameter p = parameters[i];
- ClassNode t = p.getOriginType();
- if (t.isGenericsPlaceHolder() || isUsingGenericsOrIsArrayUsingGenerics(t))
- parameters[i] = new Parameter(GenericsUtils.nonGeneric(t), p.getName());
- }
- }
-
- int dist = measureParametersAndArgumentsDistance(parameters, argumentTypes);
+ int dist = measureParametersAndArgumentsDistance(parametersForDistance(method), argumentTypes);
if (dist >= 0 && dist <= bestDist) {
if (dist < bestDist) {
bestDist = dist;
@@ -1168,6 +1156,43 @@ public abstract class StaticTypeCheckingSupport {
return bestMethods;
}
+ /**
+ * Distance measurement treats generic parameters as their erasure so a
+ * {@code List<T>} parameter does not reject a {@code List} argument.
+ * <p>
+ * <b>PERFORMANCE & SAFETY CONTRACT:</b> To avoid redundant array allocations during
+ * overload resolution, this method reuses and returns {@link MethodNode#getParameters()}
+ * directly whenever no generic erasure is needed. The returned array is a cloned copy
+ * <i>only</i> when one or more parameters require generic erasure.
+ * <p>
+ * <b>CRITICAL MUTATION WARNING:</b> Callers of this method and all downstream methods in
+ * the distance measurement chain (e.g. {@link #measureParametersAndArgumentsDistance(Parameter[], ClassNode[])})
+ * <b>MUST NEVER</b> mutate the returned {@code Parameter[]} array or its elements in place.
+ * In-place mutation would silently corrupt the {@link MethodNode}'s parameter definitions
+ * for all subsequent compilation phases across the compiler.
+ */
+ private static Parameter[] parametersForDistance(final MethodNode method) {
+ Parameter[] parameters = method.getParameters();
+ Parameter[] erased = null;
+ for (int i = 0, n = parameters.length; i < n; i += 1) {
+ ClassNode t = parameters[i].getOriginType();
+ if (t.isGenericsPlaceHolder() || isUsingGenericsOrIsArrayUsingGenerics(t)) {
+ if (erased == null) {
+ erased = parameters.clone();
+ }
+ erased[i] = new Parameter(GenericsUtils.nonGeneric(t), parameters[i].getName());
+ }
+ }
+ // WARNING: Returning the un-cloned `parameters` array here is safe ONLY because all callers
+ // in the distance-measurement chain treat the array as strictly read-only.
+ return erased == null ? parameters : erased;
+ }
+
+ /**
+ * Measures parameter-to-argument distance. Note: The supplied {@code parameters} array
+ * may be the internal backing array of a {@link MethodNode} (see {@link #parametersForDistance(MethodNode)});
+ * this method and all helper methods it calls must treat {@code parameters} as strictly read-only.
+ */
@SuppressWarnings("removal")
private static int measureParametersAndArgumentsDistance(final Parameter[] parameters, final ClassNode[] argumentTypes) {
int dist = -1;
diff --git a/src/test/groovy/org/codehaus/groovy/transform/stc/DgmMethodLookupTest.groovy b/src/test/groovy/org/codehaus/groovy/transform/stc/DgmMethodLookupTest.groovy
new file mode 100644
index 0000000000..6916f390fd
--- /dev/null
+++ b/src/test/groovy/org/codehaus/groovy/transform/stc/DgmMethodLookupTest.groovy
@@ -0,0 +1,269 @@
+/*
+ * 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.codehaus.groovy.transform.stc
+
+import groovy.lang.GroovyClassLoader
+import org.codehaus.groovy.ast.ClassHelper
+import org.codehaus.groovy.ast.ClassNode
+import org.codehaus.groovy.ast.MethodNode
+import org.codehaus.groovy.runtime.DefaultGroovyMethods
+import org.junit.jupiter.api.Test
+
+import java.util.function.Function
+import java.util.function.Predicate
+
+import static groovy.test.GroovyAssert.shouldFail
+import static org.codehaus.groovy.ast.ClassHelper.CLOSURE_TYPE
+import static org.codehaus.groovy.ast.ClassHelper.LIST_TYPE
+import static org.codehaus.groovy.ast.ClassHelper.MAP_TYPE
+import static org.codehaus.groovy.ast.ClassHelper.OBJECT_TYPE
+import static org.codehaus.groovy.ast.ClassHelper.STRING_TYPE
+import static org.codehaus.groovy.ast.ClassHelper.int_TYPE
+import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.findDGMMethodsByNameAndArguments
+import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.findDGMMethodsForClassNode
+
+/**
+ * The DGM lookup used by the static type checker must keep returning the same
+ * named methods after the per-receiver name index is populated, including
+ * after the loader's extension cache is dropped and rebuilt.
+ */
+final class DgmMethodLookupTest {
+
+ private static ClassLoader testLoader() {
+ DgmMethodLookupTest.classLoader
+ }
+
+ @Test
+ void 'named DGM lookup is stable after the name index is built'() {
+ def loader = testLoader()
+ def first = findDGMMethodsForClassNode(loader, LIST_TYPE, 'each')
+ def second = findDGMMethodsForClassNode(loader, LIST_TYPE, 'each')
+ assert !first.isEmpty()
+ assert first.size() == second.size()
+ assert first.every { it.name == 'each' }
+
+ def collect = findDGMMethodsForClassNode(loader, LIST_TYPE, 'collect')
+ assert collect.every { it.name == 'collect' }
+ assert collect.size() > 0
+ }
+
+ @Test
+ void 'unknown DGM name is empty'() {
+ def missing = findDGMMethodsForClassNode(testLoader(), STRING_TYPE, 'definitelyNotADgmMethod')
+ assert missing.isEmpty()
+ }
+
+ @Test
+ void 'unknown receiver still only returns the requested name'() {
+ def unknown = ClassHelper.make('com.example.DoesNotExist')
+ def each = findDGMMethodsForClassNode(testLoader(), unknown, 'each')
+ assert each.every { it.name == 'each' }
+ assert findDGMMethodsForClassNode(testLoader(), unknown, 'definitelyNotADgmMethod').isEmpty()
+ }
+
+ @Test
+ void 'clearing the extension cache still allows a subsequent lookup'() {
+ def loader = new GroovyClassLoader()
+ def before = findDGMMethodsForClassNode(loader, OBJECT_TYPE, 'with')
+ assert !before.isEmpty()
+ StaticTypeCheckingSupport.clearExtensionMethodCache(loader)
+ def after = findDGMMethodsForClassNode(loader, OBJECT_TYPE, 'with')
+ assert after.size() == before.size()
+ assert after.every { it.name == 'with' }
+ }
+
+ @Test
+ void 'clearing all extension caches still allows a subsequent lookup'() {
+ def loader = new GroovyClassLoader()
+ def before = findDGMMethodsForClassNode(loader, OBJECT_TYPE, 'with')
+ assert !before.isEmpty()
+ StaticTypeCheckingSupport.clearExtensionMethodCache()
+ def after = findDGMMethodsForClassNode(loader, OBJECT_TYPE, 'with')
+ assert after.size() == before.size()
+ }
+
+ @Test
+ void 'clearing one loader does not drop another loader cache'() {
+ def one = new GroovyClassLoader()
+ def two = new GroovyClassLoader()
+ def fromOne = findDGMMethodsForClassNode(one, OBJECT_TYPE, 'with')
+ def fromTwo = findDGMMethodsForClassNode(two, OBJECT_TYPE, 'with')
+ assert fromOne.size() == fromTwo.size()
+
+ Set<String> namesTwo = ExtensionMethodCache.INSTANCE.getPreemptiveNames(two)
+ StaticTypeCheckingSupport.clearExtensionMethodCache(one)
+
+ def fromTwoAfter = findDGMMethodsForClassNode(two, OBJECT_TYPE, 'with')
+ assert fromTwoAfter.size() == fromTwo.size()
+ assert ExtensionMethodCache.INSTANCE.getPreemptiveNames(two) == namesTwo
+
+ def fromOneAfter = findDGMMethodsForClassNode(one, OBJECT_TYPE, 'with')
+ assert fromOneAfter.size() == fromOne.size()
+ }
+
+ @Test
+ void 'name index matches a linear scan of the cached list'() {
+ def loader = testLoader()
+ def objectMethods = ExtensionMethodCache.INSTANCE.get(loader).get(OBJECT_TYPE.name)
+ assert objectMethods != null
+ assert objectMethods.size() > 1
+
+ def linear = objectMethods.findAll { it.name == 'with' }
+ def indexed = ExtensionMethodCache.INSTANCE.get(loader, OBJECT_TYPE.name, 'with')
+ assert indexed.size() == linear.size()
+ assert indexed.containsAll(linear)
+ assert indexed.every { it.name == 'with' }
+ }
+
+ @Test
+ void 'cached lists and named views are unmodifiable'() {
+ def loader = testLoader()
+ def objectMethods = ExtensionMethodCache.INSTANCE.get(loader).get(OBJECT_TYPE.name)
+ shouldFail(UnsupportedOperationException) { objectMethods.add(null) }
+ shouldFail(UnsupportedOperationException) { objectMethods.clear() }
+ shouldFail(UnsupportedOperationException) { objectMethods.remove(0) }
+ shouldFail(UnsupportedOperationException) { objectMethods.set(0, objectMethods.get(0)) }
+ shouldFail(IndexOutOfBoundsException) { objectMethods.get(-1) }
+ shouldFail(IndexOutOfBoundsException) { objectMethods.get(objectMethods.size()) }
+
+ def named = ExtensionMethodCache.INSTANCE.get(loader, OBJECT_TYPE.name, 'with')
+ assert !named.isEmpty()
+ shouldFail(UnsupportedOperationException) { named.add(null) }
+
+ def missing = ExtensionMethodCache.INSTANCE.get(loader, OBJECT_TYPE.name, 'definitelyNotADgmMethod')
+ assert missing.isEmpty()
+ shouldFail(UnsupportedOperationException) { missing.add(null) }
+
+ def unknownClass = ExtensionMethodCache.INSTANCE.get(loader, 'com.example.DoesNotExist', 'each')
+ assert unknownClass.isEmpty()
+ }
+
+ @Test
+ void 'overloads of the same name are all indexed'() {
+ def loader = testLoader()
+ def plus = findDGMMethodsForClassNode(loader, STRING_TYPE, 'plus')
+ assert plus.size() > 1
+ assert plus.every { it.name == 'plus' }
+
+ def objectMethods = ExtensionMethodCache.INSTANCE.get(loader).get(OBJECT_TYPE.name)
+ def singletonEntry = objectMethods.countBy { it.name }.find { it.value == 1 }
+ assert singletonEntry != null
+ def singleton = ExtensionMethodCache.INSTANCE.get(loader, OBJECT_TYPE.name, singletonEntry.key)
+ assert singleton.size() == 1
+ assert singleton[0].name == singletonEntry.key
+ shouldFail(UnsupportedOperationException) { singleton.add(null) }
+ }
+
+ @Test
+ void 'lookup walks interfaces and superclasses'() {
+ def arrayList = ClassHelper.make(ArrayList)
+ def each = findDGMMethodsForClassNode(testLoader(), arrayList, 'each')
+ assert !each.isEmpty()
+ assert each.every { it.name == 'each' }
+
+ def mapEach = findDGMMethodsForClassNode(testLoader(), MAP_TYPE, 'each')
+ assert !mapEach.isEmpty()
+ assert mapEach.every { it.name == 'each' }
+ }
+
+ @Test
+ void 'lookup on arrays walks component and Object array helpers'() {
+ def loader = testLoader()
+ def stringArrayGetAt = findDGMMethodsForClassNode(loader, STRING_TYPE.makeArray(), 'getAt')
+ assert stringArrayGetAt.every { it.name == 'getAt' }
+ assert !stringArrayGetAt.isEmpty()
+
+ def listArrayGetAt = findDGMMethodsForClassNode(loader, LIST_TYPE.makeArray(), 'getAt')
+ assert !listArrayGetAt.isEmpty()
+ assert listArrayGetAt.every { it.name == 'getAt' }
+
+ def intArrayGetAt = findDGMMethodsForClassNode(loader, int_TYPE.makeArray(), 'getAt')
+ assert !intArrayGetAt.isEmpty()
+ assert intArrayGetAt.every { it.name == 'getAt' }
+
+ def objectArrayGetAt = findDGMMethodsForClassNode(loader, OBJECT_TYPE.makeArray(), 'getAt')
+ assert !objectArrayGetAt.isEmpty()
+ }
+
+ @Test
+ void 'lookup on a primitive type still finds Object extensions'() {
+ def with = findDGMMethodsForClassNode(testLoader(), int_TYPE, 'with')
+ assert !with.isEmpty()
+ assert with.every { it.name == 'with' }
+ }
+
+ @Test
+ void 'named lookup with argument types still selects DGM candidates'() {
+ def each = findDGMMethodsByNameAndArguments(testLoader(), LIST_TYPE, 'each', [CLOSURE_TYPE] as ClassNode[])
+ assert !each.isEmpty()
+ assert each.every { MethodNode it -> it.name == 'each' }
+
+ def missing = findDGMMethodsByNameAndArguments(testLoader(), LIST_TYPE, 'definitelyNotADgmMethod', [CLOSURE_TYPE] as ClassNode[])
+ assert missing.isEmpty()
+ }
+
+ @Test
+ void 'preemptive names rebuild after the loader cache is cleared'() {
+ def loader = new GroovyClassLoader()
+ def before = ExtensionMethodCache.INSTANCE.getPreemptiveNames(loader)
+ assert before.contains('withDefault')
+ StaticTypeCheckingSupport.clearExtensionMethodCache(loader)
+ def after = ExtensionMethodCache.INSTANCE.getPreemptiveNames(loader)
+ assert after == before
+ }
+
+ @Test
+ void 'abstract extension method cache handles single-name buckets and diverse buckets with MethodsByName'() {
+ def singleNameCache = new AbstractExtensionMethodCache() {
+ @Override
+ protected void addAdditionalClassesToScan(Set<Class> instanceExtClasses, Set<Class> staticExtClasses) {
+ instanceExtClasses.add(DefaultGroovyMethods)
+ }
+
+ @Override
+ protected String getDisablePropertyName() {
+ 'custom.extension.disable'
+ }
+
+ @Override
+ protected Predicate<MethodNode> getMethodFilter() {
+ { MethodNode m -> false }
+ }
+
+ @Override
+ protected Function<MethodNode, String> getMethodMapper() {
+ { MethodNode m -> m.name } // key is method name, so all methods under a key have the same name
+ }
+ }
+
+ def loader = testLoader()
+ def eachMethods = singleNameCache.get(loader).get('each')
+ assert eachMethods != null
+ assert !eachMethods.isEmpty()
+ assert eachMethods.every { it.name == 'each' }
+
+ // Named lookup via get(loader, key, name) uses MethodsByName fast-path
+ def found = singleNameCache.get(loader, 'each', 'each')
+ assert found.size() == eachMethods.size()
+ assert found == eachMethods
+
+ def missing = singleNameCache.get(loader, 'each', 'definitelyNotADgmMethod')
+ assert missing.isEmpty()
+ }
+}
diff --git a/src/test/groovy/org/codehaus/groovy/transform/stc/StaticTypeCheckingSupportTest.groovy b/src/test/groovy/org/codehaus/groovy/transform/stc/StaticTypeCheckingSupportTest.groovy
index f8e49e68bd..e9253a4692 100644
--- a/src/test/groovy/org/codehaus/groovy/transform/stc/StaticTypeCheckingSupportTest.groovy
+++ b/src/test/groovy/org/codehaus/groovy/transform/stc/StaticTypeCheckingSupportTest.groovy
@@ -19,6 +19,7 @@
package org.codehaus.groovy.transform.stc
import org.codehaus.groovy.ast.ClassNode
+import org.codehaus.groovy.ast.GenericsType
import org.codehaus.groovy.ast.MethodNode
import org.codehaus.groovy.ast.Parameter
import org.junit.jupiter.api.Test
@@ -26,8 +27,11 @@ import org.junit.jupiter.api.Test
import java.lang.reflect.Modifier
import static org.codehaus.groovy.ast.ClassHelper.Integer_TYPE
+import static org.codehaus.groovy.ast.ClassHelper.LIST_TYPE
import static org.codehaus.groovy.ast.ClassHelper.OBJECT_TYPE
import static org.codehaus.groovy.ast.ClassHelper.STRING_TYPE
+import static org.codehaus.groovy.ast.ClassHelper.VOID_TYPE
+import static org.codehaus.groovy.ast.ClassHelper.makeWithoutCaching
import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.chooseBestMethod
final class StaticTypeCheckingSupportTest {
@@ -54,4 +58,171 @@ final class StaticTypeCheckingSupportTest {
assert candidates == [am, bm]
}
}
+
+ @Test
+ void testChooseBestMethodDoesNotMutateNonGenericParameters() {
+ ClassNode owner = new ClassNode('C', Modifier.PUBLIC, OBJECT_TYPE)
+ MethodNode exact = method(owner, 'm', STRING_TYPE, new Parameter(STRING_TYPE, 's'))
+ MethodNode fallback = method(owner, 'm', OBJECT_TYPE, new Parameter(OBJECT_TYPE, 's'))
+ Parameter[] exactBefore = exact.parameters
+ Parameter exactParamBefore = exactBefore[0]
+ ClassNode exactTypeBefore = exactParamBefore.type
+ ClassNode exactOriginBefore = exactParamBefore.originType
+
+ Parameter[] fallbackBefore = fallback.parameters
+ Parameter fallbackParamBefore = fallbackBefore[0]
+ ClassNode fallbackTypeBefore = fallbackParamBefore.type
+ ClassNode fallbackOriginBefore = fallbackParamBefore.originType
+
+ def chosen = chooseBestMethod(owner, [exact, fallback], STRING_TYPE)
+
+ assert chosen == [exact]
+ // Array references must remain identical
+ assert exact.parameters.is(exactBefore)
+ assert fallback.parameters.is(fallbackBefore)
+ // Parameter instances within the array must remain identical and unmutated
+ assert exact.parameters[0].is(exactParamBefore)
+ assert exact.parameters[0].type.is(exactTypeBefore)
+ assert exact.parameters[0].originType.is(exactOriginBefore)
+ assert exact.parameters[0].name == 's'
+
+ assert fallback.parameters[0].is(fallbackParamBefore)
+ assert fallback.parameters[0].type.is(fallbackTypeBefore)
+ assert fallback.parameters[0].originType.is(fallbackOriginBefore)
+ assert fallback.parameters[0].name == 's'
+ }
+
+ @Test
+ void testParameterElementsAreNeverMutatedDuringResolution() {
+ ClassNode owner = new ClassNode('C', Modifier.PUBLIC, OBJECT_TYPE)
+ ClassNode listOfString = parameterized(LIST_TYPE, STRING_TYPE)
+
+ MethodNode m1 = method(owner, 'dispatch', VOID_TYPE,
+ new Parameter(STRING_TYPE, 's'),
+ new Parameter(Integer_TYPE, 'i'),
+ new Parameter(OBJECT_TYPE, 'o'))
+ MethodNode m2 = method(owner, 'dispatch', VOID_TYPE,
+ new Parameter(STRING_TYPE, 's'),
+ new Parameter(listOfString, 'list'),
+ new Parameter(OBJECT_TYPE.makeArray(), 'vargs'))
+
+ Parameter[] m1Params = m1.parameters
+ Parameter[] m1ParamCopies = m1Params.clone()
+ ClassNode[] m1Types = m1Params.collect { it.type } as ClassNode[]
+ ClassNode[] m1OriginTypes = m1Params.collect { it.originType } as ClassNode[]
+
+ Parameter[] m2Params = m2.parameters
+ Parameter[] m2ParamCopies = m2Params.clone()
+ ClassNode[] m2Types = m2Params.collect { it.type } as ClassNode[]
+ ClassNode[] m2OriginTypes = m2Params.collect { it.originType } as ClassNode[]
+
+ // Perform multiple resolutions (matching m1, matching m2, and ambiguous/mismatched calls)
+ chooseBestMethod(owner, [m1, m2], STRING_TYPE, Integer_TYPE, OBJECT_TYPE)
+ chooseBestMethod(owner, [m1, m2], STRING_TYPE, LIST_TYPE, OBJECT_TYPE.makeArray())
+ chooseBestMethod(owner, [m1, m2], STRING_TYPE, STRING_TYPE, OBJECT_TYPE)
+
+ // Verify m1 parameter array and all parameter elements
+ assert m1.parameters.is(m1Params)
+ for (int i = 0; i < m1Params.length; i += 1) {
+ assert m1.parameters[i].is(m1ParamCopies[i])
+ assert m1.parameters[i].type.is(m1Types[i])
+ assert m1.parameters[i].originType.is(m1OriginTypes[i])
+ }
+
+ // Verify m2 parameter array and all parameter elements
+ assert m2.parameters.is(m2Params)
+ for (int i = 0; i < m2Params.length; i += 1) {
+ assert m2.parameters[i].is(m2ParamCopies[i])
+ assert m2.parameters[i].type.is(m2Types[i])
+ assert m2.parameters[i].originType.is(m2OriginTypes[i])
+ }
+ }
+
+ @Test
+ void testChooseBestMethodErasesGenericParametersWithoutMutatingTheMethod() {
+ ClassNode owner = new ClassNode('C', Modifier.PUBLIC, OBJECT_TYPE)
+ ClassNode listOfString = parameterized(LIST_TYPE, STRING_TYPE)
+ MethodNode generic = method(owner, 'm', VOID_TYPE, new Parameter(listOfString, 'xs'))
+ MethodNode fallback = method(owner, 'm', VOID_TYPE, new Parameter(OBJECT_TYPE, 'xs'))
+ Parameter[] before = generic.parameters
+ ClassNode originBefore = before[0].originType
+ ClassNode typeBefore = before[0].type
+
+ def chosen = chooseBestMethod(owner, [generic, fallback], LIST_TYPE)
+
+ assert chosen == [generic]
+ assert generic.parameters.is(before)
+ assert before[0].originType.is(originBefore)
+ assert before[0].type.is(typeBefore)
+ assert before[0].type.isUsingGenerics()
+ }
+
+ @Test
+ void testChooseBestMethodErasesMixedAndPlaceholderParameters() {
+ ClassNode owner = new ClassNode('C', Modifier.PUBLIC, OBJECT_TYPE)
+ ClassNode t = placeholder('T')
+ MethodNode mixed = method(owner, 'm', VOID_TYPE,
+ new Parameter(STRING_TYPE, 's'),
+ new Parameter(parameterized(LIST_TYPE, STRING_TYPE), 'xs'))
+ MethodNode id = method(owner, 'id', t, new Parameter(t, 'x'))
+ Parameter[] mixedBefore = mixed.parameters
+ Parameter[] idBefore = id.parameters
+
+ def chosenMixed = chooseBestMethod(owner, [mixed], STRING_TYPE, LIST_TYPE)
+ def chosenId = chooseBestMethod(owner, [id], STRING_TYPE)
+
+ assert chosenMixed == [mixed]
+ assert chosenId == [id]
+ assert mixed.parameters.is(mixedBefore)
+ assert mixedBefore[0].type.is(STRING_TYPE)
+ assert mixedBefore[1].type.isUsingGenerics()
+ assert id.parameters.is(idBefore)
+ assert idBefore[0].originType.isGenericsPlaceHolder()
+ }
+
+ @Test
+ void testChooseBestMethodErasesGenericArrayAndVarargsParameters() {
+ ClassNode owner = new ClassNode('C', Modifier.PUBLIC, OBJECT_TYPE)
+ ClassNode listOfString = parameterized(LIST_TYPE, STRING_TYPE)
+ MethodNode arrayParam = method(owner, 'm', VOID_TYPE, new Parameter(listOfString.makeArray(), 'xss'))
+ MethodNode vargs = method(owner, 'v', VOID_TYPE, new Parameter(listOfString.makeArray(), 'xs'))
+ Parameter[] arrayBefore = arrayParam.parameters
+ Parameter[] vargsBefore = vargs.parameters
+
+ def chosenArray = chooseBestMethod(owner, [arrayParam], LIST_TYPE.makeArray())
+ def chosenVargs = chooseBestMethod(owner, [vargs], LIST_TYPE)
+
+ assert chosenArray == [arrayParam]
+ assert chosenVargs == [vargs]
+ assert arrayParam.parameters.is(arrayBefore)
+ assert vargs.parameters.is(vargsBefore)
+ assert arrayBefore[0].type.isArray()
+ assert arrayBefore[0].type.componentType.isUsingGenerics()
+ }
+
+ @Test
+ void testChooseBestMethodEmptyAndNullCandidates() {
+ assert chooseBestMethod(OBJECT_TYPE, [], STRING_TYPE).isEmpty()
+ assert chooseBestMethod(OBJECT_TYPE, null, STRING_TYPE).isEmpty()
+ }
+
+ private static MethodNode method(ClassNode owner, String name, ClassNode returnType, Parameter... params) {
+ owner.addMethod(name, Modifier.PUBLIC, returnType, params, ClassNode.EMPTY_ARRAY, null)
+ }
+
+ private static ClassNode parameterized(ClassNode raw, ClassNode... typeArgs) {
+ ClassNode cn = raw.getPlainNodeReference()
+ cn.usingGenerics = true
+ cn.genericsTypes = typeArgs.collect { it.asGenericsType() } as GenericsType[]
+ return cn
+ }
+
+ private static ClassNode placeholder(String name) {
+ ClassNode t = makeWithoutCaching(name)
+ t.setRedirect(OBJECT_TYPE)
+ t.genericsPlaceHolder = true
+ t.usingGenerics = true
+ t.genericsTypes = [t.asGenericsType()] as GenericsType[]
+ return t
+ }
}
diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/ChooseBestMethodBench.java b/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/ChooseBestMethodBench.java
new file mode 100644
index 0000000000..2a490fab3f
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/ChooseBestMethodBench.java
@@ -0,0 +1,180 @@
+/*
+ * 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.apache.groovy.perf;
+
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.GenericsType;
+import org.codehaus.groovy.ast.MethodNode;
+import org.codehaus.groovy.ast.Parameter;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.lang.reflect.Modifier;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import static org.codehaus.groovy.ast.ClassHelper.Boolean_TYPE;
+import static org.codehaus.groovy.ast.ClassHelper.Double_TYPE;
+import static org.codehaus.groovy.ast.ClassHelper.Integer_TYPE;
+import static org.codehaus.groovy.ast.ClassHelper.LIST_TYPE;
+import static org.codehaus.groovy.ast.ClassHelper.Long_TYPE;
+import static org.codehaus.groovy.ast.ClassHelper.MAP_TYPE;
+import static org.codehaus.groovy.ast.ClassHelper.OBJECT_TYPE;
+import static org.codehaus.groovy.ast.ClassHelper.SET_TYPE;
+import static org.codehaus.groovy.ast.ClassHelper.STRING_TYPE;
+import static org.codehaus.groovy.ast.ClassHelper.VOID_TYPE;
+import static org.codehaus.groovy.ast.ClassHelper.makeWithoutCaching;
+import static org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport.chooseBestMethod;
+
+/**
+ * Microbenchmark measuring chooseBestMethod distance computation and overload resolution.
+ * Isolates the performance and GC allocation impact of eliminating Parameter[] clones
+ * on non-generic parameter lists.
+ */
+@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Fork(2)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@State(Scope.Benchmark)
+public class ChooseBestMethodBench {
+
+ private ClassNode owner;
+ private List<MethodNode> nonGenericOverloads;
+ private List<MethodNode> genericOverloads;
+ private List<MethodNode> mixedOverloads;
+ private List<MethodNode> singleNonGeneric;
+ private List<MethodNode> heavyOverloads;
+
+ private ClassNode[] nonGenericArgs;
+ private ClassNode[] genericArgs;
+ private ClassNode[] heavyArgs;
+
+ @Setup
+ public void setup() {
+ owner = new ClassNode("TestService", Modifier.PUBLIC, OBJECT_TYPE);
+
+ // 1. Non-generic overloads (4 candidates, concrete types)
+ nonGenericOverloads = new ArrayList<>();
+ nonGenericOverloads.add(method(owner, "process", VOID_TYPE, new Parameter(STRING_TYPE, "s"), new Parameter(Integer_TYPE, "i")));
+ nonGenericOverloads.add(method(owner, "process", VOID_TYPE, new Parameter(STRING_TYPE, "s"), new Parameter(Long_TYPE, "l")));
+ nonGenericOverloads.add(method(owner, "process", VOID_TYPE, new Parameter(STRING_TYPE, "s"), new Parameter(Double_TYPE, "d")));
+ nonGenericOverloads.add(method(owner, "process", VOID_TYPE, new Parameter(STRING_TYPE, "s"), new Parameter(OBJECT_TYPE, "o")));
+
+ // 2. Generic overloads (2 candidates with type parameters)
+ ClassNode t = placeholder("T");
+ ClassNode listOfT = parameterized(LIST_TYPE, t);
+ ClassNode setOfT = parameterized(SET_TYPE, t);
+ genericOverloads = new ArrayList<>();
+ genericOverloads.add(method(owner, "handle", VOID_TYPE, new Parameter(listOfT, "items"), new Parameter(t, "sample")));
+ genericOverloads.add(method(owner, "handle", VOID_TYPE, new Parameter(setOfT, "items"), new Parameter(t, "sample")));
+
+ // 3. Mixed overloads (2 non-generic + 2 generic)
+ ClassNode k = placeholder("K");
+ ClassNode v = placeholder("V");
+ ClassNode mapOfKV = parameterized(MAP_TYPE, k, v);
+ mixedOverloads = new ArrayList<>();
+ mixedOverloads.add(method(owner, "transform", VOID_TYPE, new Parameter(STRING_TYPE, "s"), new Parameter(OBJECT_TYPE, "o")));
+ mixedOverloads.add(method(owner, "transform", VOID_TYPE, new Parameter(OBJECT_TYPE, "a"), new Parameter(OBJECT_TYPE, "b")));
+ mixedOverloads.add(method(owner, "transform", VOID_TYPE, new Parameter(listOfT, "list"), new Parameter(t, "val")));
+ mixedOverloads.add(method(owner, "transform", VOID_TYPE, new Parameter(mapOfKV, "map"), new Parameter(k, "key")));
+
+ // 4. Single non-generic method
+ singleNonGeneric = new ArrayList<>();
+ singleNonGeneric.add(method(owner, "execute", VOID_TYPE, new Parameter(STRING_TYPE, "s"), new Parameter(Integer_TYPE, "i")));
+
+ // 5. Heavy non-generic overloads (8 candidates with 3 parameters)
+ heavyOverloads = new ArrayList<>();
+ heavyOverloads.add(method(owner, "dispatch", VOID_TYPE, new Parameter(STRING_TYPE, "s"), new Parameter(Integer_TYPE, "i"), new Parameter(Boolean_TYPE, "b")));
+ heavyOverloads.add(method(owner, "dispatch", VOID_TYPE, new Parameter(STRING_TYPE, "s"), new Parameter(Long_TYPE, "l"), new Parameter(Boolean_TYPE, "b")));
+ heavyOverloads.add(method(owner, "dispatch", VOID_TYPE, new Parameter(STRING_TYPE, "s"), new Parameter(Double_TYPE, "d"), new Parameter(Boolean_TYPE, "b")));
+ heavyOverloads.add(method(owner, "dispatch", VOID_TYPE, new Parameter(STRING_TYPE, "s"), new Parameter(OBJECT_TYPE, "o"), new Parameter(Boolean_TYPE, "b")));
+ heavyOverloads.add(method(owner, "dispatch", VOID_TYPE, new Parameter(OBJECT_TYPE, "s"), new Parameter(Integer_TYPE, "i"), new Parameter(Boolean_TYPE, "b")));
+ heavyOverloads.add(method(owner, "dispatch", VOID_TYPE, new Parameter(OBJECT_TYPE, "s"), new Parameter(Long_TYPE, "l"), new Parameter(Boolean_TYPE, "b")));
+ heavyOverloads.add(method(owner, "dispatch", VOID_TYPE, new Parameter(OBJECT_TYPE, "s"), new Parameter(Double_TYPE, "d"), new Parameter(Boolean_TYPE, "b")));
+ heavyOverloads.add(method(owner, "dispatch", VOID_TYPE, new Parameter(OBJECT_TYPE, "s"), new Parameter(OBJECT_TYPE, "o"), new Parameter(OBJECT_TYPE, "b")));
+
+ nonGenericArgs = new ClassNode[]{STRING_TYPE, Integer_TYPE};
+ genericArgs = new ClassNode[]{LIST_TYPE, STRING_TYPE};
+ heavyArgs = new ClassNode[]{STRING_TYPE, Integer_TYPE, Boolean_TYPE};
+
+ // Warm up invocation
+ chooseBestMethod(owner, nonGenericOverloads, nonGenericArgs);
+ }
+
+ @Benchmark
+ public List<MethodNode> chooseNonGenericOverloads() {
+ return chooseBestMethod(owner, nonGenericOverloads, nonGenericArgs);
+ }
+
+ @Benchmark
+ public List<MethodNode> chooseSingleNonGeneric() {
+ return chooseBestMethod(owner, singleNonGeneric, nonGenericArgs);
+ }
+
+ @Benchmark
+ public List<MethodNode> chooseMixedOverloads() {
+ return chooseBestMethod(owner, mixedOverloads, genericArgs);
+ }
+
+ @Benchmark
+ public List<MethodNode> chooseGenericOverloads() {
+ return chooseBestMethod(owner, genericOverloads, genericArgs);
+ }
+
+ @Benchmark
+ public List<MethodNode> chooseHeavyNonGenericOverloads() {
+ return chooseBestMethod(owner, heavyOverloads, heavyArgs);
+ }
+
+ private static MethodNode method(ClassNode owner, String name, ClassNode returnType, Parameter... params) {
+ MethodNode mn = new MethodNode(name, Modifier.PUBLIC, returnType, params, ClassNode.EMPTY_ARRAY, null);
+ mn.setDeclaringClass(owner);
+ return mn;
+ }
+
+ private static ClassNode parameterized(ClassNode raw, ClassNode... typeArgs) {
+ ClassNode cn = raw.getPlainNodeReference();
+ cn.setUsingGenerics(true);
+ GenericsType[] gt = new GenericsType[typeArgs.length];
+ for (int i = 0; i < typeArgs.length; i++) {
+ gt[i] = new GenericsType(typeArgs[i]);
+ }
+ cn.setGenericsTypes(gt);
+ return cn;
+ }
+
+ private static ClassNode placeholder(String name) {
+ ClassNode t = makeWithoutCaching(name);
+ t.setRedirect(OBJECT_TYPE);
+ t.setGenericsPlaceHolder(true);
+ t.setUsingGenerics(true);
+ t.setGenericsTypes(new GenericsType[]{new GenericsType(t)});
+ return t;
+ }
+}
diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/DgmMethodLookupBench.java b/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/DgmMethodLookupBench.java
new file mode 100644
index 0000000000..8d254c0058
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/DgmMethodLookupBench.java
@@ -0,0 +1,122 @@
+/*
+ * 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.apache.groovy.perf;
+
+import org.codehaus.groovy.ast.ClassNode;
+import org.codehaus.groovy.ast.MethodNode;
+import org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+
+import static org.codehaus.groovy.ast.ClassHelper.CLOSURE_TYPE;
+import static org.codehaus.groovy.ast.ClassHelper.LIST_TYPE;
+import static org.codehaus.groovy.ast.ClassHelper.MAP_TYPE;
+import static org.codehaus.groovy.ast.ClassHelper.OBJECT_TYPE;
+import static org.codehaus.groovy.ast.ClassHelper.STRING_TYPE;
+import static org.codehaus.groovy.ast.ClassHelper.int_TYPE;
+
+/**
+ * Microbenchmark for DGM extension method lookup across various receiver types and methods.
+ * Measures the algorithmic speedup of name-indexed cache lookup vs linear scanning.
+ */
+@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Fork(2)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.NANOSECONDS)
+@State(Scope.Benchmark)
+public class DgmMethodLookupBench {
+
+ private ClassLoader loader;
+ private ClassNode[] closureArgs;
+ private ClassNode stringArrayType;
+ private ClassNode intArrayType;
+
+ @Setup
+ public void setup() {
+ loader = getClass().getClassLoader();
+ closureArgs = new ClassNode[]{CLOSURE_TYPE};
+ stringArrayType = STRING_TYPE.makeArray();
+ intArrayType = int_TYPE.makeArray();
+ // Warm up the extension method cache
+ StaticTypeCheckingSupport.findDGMMethodsForClassNode(loader, OBJECT_TYPE, "with");
+ }
+
+ @Benchmark
+ public Set<MethodNode> lookupObjectHit() {
+ return StaticTypeCheckingSupport.findDGMMethodsForClassNode(loader, OBJECT_TYPE, "with");
+ }
+
+ @Benchmark
+ public Set<MethodNode> lookupObjectMiss() {
+ return StaticTypeCheckingSupport.findDGMMethodsForClassNode(loader, OBJECT_TYPE, "definitelyNotADgmMethod");
+ }
+
+ @Benchmark
+ public Set<MethodNode> lookupListHit() {
+ return StaticTypeCheckingSupport.findDGMMethodsForClassNode(loader, LIST_TYPE, "each");
+ }
+
+ @Benchmark
+ public Set<MethodNode> lookupListMiss() {
+ return StaticTypeCheckingSupport.findDGMMethodsForClassNode(loader, LIST_TYPE, "definitelyNotADgmMethod");
+ }
+
+ @Benchmark
+ public Set<MethodNode> lookupStringHit() {
+ return StaticTypeCheckingSupport.findDGMMethodsForClassNode(loader, STRING_TYPE, "padLeft");
+ }
+
+ @Benchmark
+ public Set<MethodNode> lookupStringMiss() {
+ return StaticTypeCheckingSupport.findDGMMethodsForClassNode(loader, STRING_TYPE, "definitelyNotADgmMethod");
+ }
+
+ @Benchmark
+ public Set<MethodNode> lookupMapHit() {
+ return StaticTypeCheckingSupport.findDGMMethodsForClassNode(loader, MAP_TYPE, "collect");
+ }
+
+ @Benchmark
+ public Set<MethodNode> lookupArrayHit() {
+ return StaticTypeCheckingSupport.findDGMMethodsForClassNode(loader, stringArrayType, "getAt");
+ }
+
+ @Benchmark
+ public Set<MethodNode> lookupPrimitiveArrayHit() {
+ return StaticTypeCheckingSupport.findDGMMethodsForClassNode(loader, intArrayType, "getAt");
+ }
+
+ @Benchmark
+ public List<MethodNode> lookupByNameAndArgs() {
+ return StaticTypeCheckingSupport.findDGMMethodsByNameAndArguments(loader, LIST_TYPE, "each", closureArgs);
+ }
+}
diff --git a/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/StcCompilePerfBench.java b/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/StcCompilePerfBench.java
new file mode 100644
index 0000000000..bc07392d68
--- /dev/null
+++ b/subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/StcCompilePerfBench.java
@@ -0,0 +1,179 @@
+/*
+ * 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.apache.groovy.perf;
+
+import groovy.transform.CompileStatic;
+import org.codehaus.groovy.control.CompilationUnit;
+import org.codehaus.groovy.control.CompilerConfiguration;
+import org.codehaus.groovy.control.Phases;
+import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer;
+import org.openjdk.jmh.annotations.Benchmark;
+import org.openjdk.jmh.annotations.BenchmarkMode;
+import org.openjdk.jmh.annotations.Fork;
+import org.openjdk.jmh.annotations.Level;
+import org.openjdk.jmh.annotations.Measurement;
+import org.openjdk.jmh.annotations.Mode;
+import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
+import org.openjdk.jmh.annotations.Scope;
+import org.openjdk.jmh.annotations.Setup;
+import org.openjdk.jmh.annotations.State;
+import org.openjdk.jmh.annotations.Warmup;
+
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Macrobenchmark measuring static type checking (STC) compilation throughput and latency
+ * under @CompileStatic across diverse extension method lookups and method overload resolutions.
+ */
+@Warmup(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Fork(2)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@State(Scope.Benchmark)
+public class StcCompilePerfBench {
+
+ private static final int CLASSES = 50;
+ private static final int CALLS_PER_CLASS = 60;
+
+ @Param({"dgmDense", "overloadDense", "mixedDense"})
+ private String suite;
+
+ private Map<String, String> corpus;
+
+ @Setup(Level.Trial)
+ public void generateCorpus() {
+ corpus = new LinkedHashMap<>();
+ for (int c = 0; c < CLASSES; c += 1) {
+ corpus.put("StcGen" + c, sourceFor(c));
+ }
+ }
+
+ @Benchmark
+ public int compileStcCorpus() {
+ CompilerConfiguration config = new CompilerConfiguration();
+ config.addCompilationCustomizers(new ASTTransformationCustomizer(CompileStatic.class));
+ CompilationUnit unit = new CompilationUnit(config);
+ for (Map.Entry<String, String> source : corpus.entrySet()) {
+ unit.addSource(source.getKey() + ".groovy", source.getValue());
+ }
+ unit.compile(Phases.INSTRUCTION_SELECTION);
+ return unit.getAST().getModules().size();
+ }
+
+ private String sourceFor(final int index) {
+ switch (suite) {
+ case "dgmDense": return dgmDenseClass(index);
+ case "overloadDense": return overloadDenseClass(index);
+ case "mixedDense": return mixedDenseClass(index);
+ default: throw new IllegalArgumentException("Unknown suite: " + suite);
+ }
+ }
+
+ private String dgmDenseClass(final int index) {
+ String[] templates = {
+ "xs.each { sink(it) }",
+ "sink(xs.collect { it.length() })",
+ "sink(xs.findAll { it.length() > 0 })",
+ "sink(xs.find { it == 'a' })",
+ "sink(xs.join(','))",
+ "sink(xs.max())",
+ "sink(xs.min())",
+ "sink(xs.reverse())",
+ "sink(xs.groupBy { it.length() })",
+ "sink(xs.inject('') { acc, val -> acc + val })",
+ "sink(nums.sum())",
+ "sink(nums.findAll { it > 0 })",
+ "sink(nums.collect { it * 2 })",
+ "m.each { k, v -> sink(v) }",
+ "sink(m.collect { k, v -> k + v })",
+ "sink(m.findAll { k, v -> v > 0 })",
+ "sink(str.padLeft(10))",
+ "sink(str.padRight(10))"
+ };
+ StringBuilder body = new StringBuilder();
+ for (int i = 0; i < CALLS_PER_CLASS; i += 1) {
+ body.append(" ").append(templates[i % templates.length]).append('\n');
+ }
+ return "class StcGen" + index + " {\n"
+ + " void sink(Object o) {}\n"
+ + " void run(List<String> xs, Map<String, Integer> m, List<Integer> nums, String str) {\n"
+ + body
+ + " }\n"
+ + "}\n";
+ }
+
+ private String overloadDenseClass(final int index) {
+ String[] templates = {
+ "sink(dispatch('a', 1, true))",
+ "sink(dispatch('a', 2L, false))",
+ "sink(dispatch('a', 3.0d, true))",
+ "sink(dispatch('a', new Object(), false))",
+ "sink(dispatch(1, 'a', true))",
+ "sink(dispatch(2L, 'a', false))",
+ "sink(dispatch(3.0d, 'a', true))"
+ };
+ StringBuilder body = new StringBuilder();
+ for (int i = 0; i < CALLS_PER_CLASS; i += 1) {
+ body.append(" ").append(templates[i % templates.length]).append('\n');
+ }
+ return "class StcGen" + index + " {\n"
+ + " void sink(Object o) {}\n"
+ + " String dispatch(String s, Integer i, Boolean b) { s + i + b }\n"
+ + " String dispatch(String s, Long l, Boolean b) { s + l + b }\n"
+ + " String dispatch(String s, Double d, Boolean b) { s + d + b }\n"
+ + " String dispatch(String s, Object o, Boolean b) { s + o + b }\n"
+ + " String dispatch(Integer i, String s, Boolean b) { i + s + b }\n"
+ + " String dispatch(Long l, String s, Boolean b) { l + s + b }\n"
+ + " String dispatch(Double d, String s, Boolean b) { d + s + b }\n"
+ + " void run() {\n"
+ + body
+ + " }\n"
+ + "}\n";
+ }
+
+ private String mixedDenseClass(final int index) {
+ String[] templates = {
+ "xs.each { sink(dispatch(it, 1, true)) }",
+ "sink(xs.collect { dispatch(it, 2L, false) })",
+ "sink(xs.findAll { it.length() > 0 })",
+ "sink(m.collect { k, v -> dispatch(k, v, true) })",
+ "sink(nums.sum())",
+ "sink(dispatch('str', nums.size(), false))",
+ "sink(str.padLeft(10))"
+ };
+ StringBuilder body = new StringBuilder();
+ for (int i = 0; i < CALLS_PER_CLASS; i += 1) {
+ body.append(" ").append(templates[i % templates.length]).append('\n');
+ }
+ return "class StcGen" + index + " {\n"
+ + " void sink(Object o) {}\n"
+ + " String dispatch(String s, Integer i, Boolean b) { s + i + b }\n"
+ + " String dispatch(String s, Long l, Boolean b) { s + l + b }\n"
+ + " String dispatch(String s, Double d, Boolean b) { s + d + b }\n"
+ + " String dispatch(String s, Object o, Boolean b) { s + o + b }\n"
+ + " void run(List<String> xs, Map<String, Integer> m, List<Integer> nums, String str) {\n"
+ + body
+ + " }\n"
+ + "}\n";
+ }
+}