[ 
https://issues.apache.org/jira/browse/GROOVY-12151?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18096099#comment-18096099
 ] 

ASF GitHub Bot commented on GROOVY-12151:
-----------------------------------------

Copilot commented on code in PR #2709:
URL: https://github.com/apache/groovy/pull/2709#discussion_r3576679993


##########
subprojects/performance/src/jmh/groovy/org/apache/groovy/bench/ClosurePackBench.java:
##########
@@ -0,0 +1,154 @@
+/*
+ *  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.bench;
+
+import groovy.lang.GroovyClassLoader;
+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.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Steady-state closure invocation under closure packing (GROOVY-12151,
+ * {@code groovy.target.closure.pack}) versus generated closure classes, for 
the
+ * two hot closure shapes: a non-capturing {@code collect} body and a
+ * written-capture {@code each} accumulator.
+ * <p>
+ * The flag is read at closure-emission time, so {@link #setup} compiles the
+ * fixture classes with a fresh {@link GroovyClassLoader} after setting it from
+ * the {@code pack} param — JMH runs each param value in its own forks, so the
+ * two states never share a JVM or its profile. A vacuity guard asserts the 
flag
+ * really flipped the emission (no {@code _closure} classes when packing, some
+ * when not).
+ * <p>
+ * Each shape runs twice: {@code mono} always invokes one fixture class, the
+ * best case for JIT inlining; {@code mega} rotates over {@link #CLASSES}
+ * fixture classes, making the shared {@code PackedClosure} adapter's 
dispatcher
+ * call site megamorphic (each class has its own hidden dispatcher class) — the
+ * realistic many-classes regime, and the honest comparison against generated
+ * closure classes, whose {@code call} sites in DGM are equally megamorphic.
+ * Single-class microbenches overstate packed dispatch costs (a deep 
monomorphic
+ * inline of the shared adapter trips the JIT's {@code InlineSmallCode}
+ * heuristic); this pair brackets the real-world range instead.
+ */
+@Warmup(iterations = 5, time = 1)
+@Measurement(iterations = 5, time = 1)
+@Fork(2)
+@BenchmarkMode(Mode.Throughput)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@State(Scope.Benchmark)
+public class ClosurePackBench {
+
+    /** Implemented by every compiled fixture class, so invocation needs no 
reflection. */
+    public interface PackWorkload {
+        Object squared(List<Integer> xs);
+        int sum(List<Integer> xs);
+    }
+
+    private static final int CLASSES = 16; // power of two: mega rotation uses 
a mask
+
+    /** Compile-time flag: {@code true} packs eligible closures, {@code false} 
= closure classes. */
+    @Param({"false", "true"})
+    public String pack;
+
+    private final PackWorkload[] fixtures = new PackWorkload[CLASSES];
+    private List<Integer> xs;
+    private int idx;
+
+    @Setup(Level.Trial)
+    public void setup() throws Exception {
+        System.setProperty("groovy.target.closure.pack", pack);
+        try (GroovyClassLoader loader = new 
GroovyClassLoader(getClass().getClassLoader())) {
+            for (int i = 0; i < CLASSES; i += 1) {
+                String source =
+                        "@groovy.transform.CompileStatic\n" +
+                        "class Fix" + i + " implements 
org.apache.groovy.bench.ClosurePackBench.PackWorkload {\n" +
+                        "    Object squared(List<Integer> xs) { xs.collect { 
it * it } }\n" +
+                        "    int sum(List<Integer> xs) { int s = 0; xs.each { 
Integer x -> s += x }; s }\n" +
+                        "}\n";
+                Class<?> c = loader.parseClass(source, "Fix" + i + ".groovy");
+                fixtures[i] = (PackWorkload) 
c.getDeclaredConstructor().newInstance();
+            }
+            // vacuity guard: the flag must actually gate the emission
+            boolean closureClasses = false;
+            for (Class<?> c : loader.getLoadedClasses()) {
+                if (c.getName().contains("_closure")) closureClasses = true;
+            }
+            if (Boolean.parseBoolean(pack) == closureClasses) {
+                throw new IllegalStateException("closure.pack=" + pack + " but 
closure classes "
+                        + (closureClasses ? "were" : "were not") + " 
generated");
+            }
+        }
+        xs = new ArrayList<>();

Review Comment:
   setup() sets the global system property groovy.target.closure.pack but never 
restores it. In a single JMH fork, benchmarks run in the same JVM, so this can 
leak into other benchmarks (and into any Groovy compilation they do), skewing 
results.



##########
src/main/java/org/codehaus/groovy/classgen/asm/ClosureWriter.java:
##########
@@ -158,6 +237,644 @@ public void writeClosure(final ClosureExpression 
expression) {
         controller.getOperandStack().replace(ClassHelper.CLOSURE_TYPE, 
localVariableParams.length);
     }
 
+    /**
+     * GEP-27 capability analysis: choose the compilation strategy for a 
closure literal.
+     * <p>
+     * A closure is packed (S1, {@link PackStrategy#PACKED_ADAPTER}) when it 
is <em>triggered</em> —
+     * either the enclosing scope opts in via {@code @PackedClosures} (the 
dynamic trust path), or the
+     * capability analysis <em>proves</em> it delegate-independent under 
{@code @CompileStatic} (the
+     * automatic path, see {@link #isDelegateIndependent}) — and it is in a 
packable position:
+     * it needs no real {@code Closure} semantics we cannot reproduce ({@link 
#isPackable}), it is
+     * written directly in a method rather than nested in another closure 
(owner-retargeting for
+     * nested closures is later work), and it does not visibly escape its 
method. Otherwise it stays a
+     * full generated class (S0, {@link PackStrategy#FULL_CLASS}) exactly as 
today.
+     */
+    private PackStrategy chooseStrategy(final ClosureExpression expression) {
+        if (!isPackable(expression)) return PackStrategy.FULL_CLASS;
+        // The most-specific @PackedClosures mode in scope (method wins over 
class), or null if not
+        // annotated. DISABLED is a fine-grained opt-out that beats an 
enclosing opt-in AND the flag.
+        PackMode mode = annotatedMode();
+        if (mode == PackMode.DISABLED) return PackStrategy.FULL_CLASS;
+        boolean annotated = (mode != null); // LENIENT/WARN/STRICT all opt in 
(DISABLED handled above)
+        if (!isPackTriggered(expression, annotated)) return 
PackStrategy.FULL_CLASS;
+        if (controller.isInGeneratedFunction()) return PackStrategy.FULL_CLASS;
+        if (escapesEnclosingMethod(expression)) return PackStrategy.FULL_CLASS;
+        return PackStrategy.PACKED_ADAPTER;
+    }
+
+    /**
+     * Whether a packable, non-escaping closure literal should actually be 
packed. Dynamic compilation
+     * has no proof of delegate-independence, so the only trigger is the 
{@code @PackedClosures} trust
+     * assertion; {@code StaticTypesClosureWriter} overrides this to add the
+     * {@code groovy.target.closure.pack} flag and the delegate-independence 
proof.
+     *
+     * @param annotated whether a non-DISABLED {@code @PackedClosures} is in 
scope
+     */
+    protected boolean isPackTriggered(final ClosureExpression expression, 
final boolean annotated) {
+        return annotated;
+    }
+
+    /**
+     * A trigger-specific decline reason for {@link #declineReason}, or {@code 
null} if the trigger did
+     * not decline this closure. Only the static writer has one (a 
delegate-resolved body); the dynamic
+     * path trusts the annotation and never declines on trigger grounds.
+     */
+    protected String triggerDeclineReason(final ClosureExpression expression) {
+        return null;
+    }
+
+    /** Whether the hoisted body is compiled statically (true only for {@code 
@CompileStatic}). */
+    protected boolean compilesHoistedBodyStatically() {
+        return false;
+    }
+
+    /**
+     * Whether the packed adapter installs the runtime delegate guard. The 
dynamic trust path needs it
+     * (an unverifiable assertion must fail fast on misuse); the static writer 
proved independence, so
+     * it overrides this to {@code false} and a caller-set delegate is then 
stored and ignored.
+     */
+    protected boolean packedClosureUsesDelegateGuard() {
+        return true;
+    }
+
+    /** The closure's inferred return type, normalised the same way {@code 
createClosureClass} does for doCall. */
+    private static ClassNode inferredClosureReturnType(final ClosureExpression 
expression) {
+        ClassNode returnType = 
expression.getNodeMetaData(INFERRED_RETURN_TYPE);
+        if (returnType == null) returnType = ClassHelper.OBJECT_TYPE;
+        else if (returnType.isPrimaryClassNode()) returnType = 
returnType.getPlainNodeReference();
+        else if (ClassHelper.isPrimitiveType(returnType)) returnType = 
ClassHelper.getWrapper(returnType);
+        else if (GenericsUtils.hasUnresolvedGenerics(returnType)) returnType = 
GenericsUtils.nonGeneric(returnType);
+        return returnType;
+    }
+
+    /**
+     * The erasure of a captured variable's type, safe to declare on the 
hoisted method: generic
+     * placeholders are replaced by their bound (the hoisted method does not 
declare the enclosing
+     * method's type variables) and type arguments are dropped ({@code 
List<T>} → {@code List}).
+     */
+    private static ClassNode erasedType(final ClassNode type) {
+        ClassNode t = type;
+        if (t == null) return ClassHelper.OBJECT_TYPE;
+        if (t.isGenericsPlaceHolder()) t = t.redirect();
+        return t.getPlainNodeReference();
+    }
+
+    /**
+     * Reports a top-level closure that was NOT packed, per the {@code mode} 
of the enclosing
+     * {@code @PackedClosures} annotation (WARN => compiler warning, STRICT => 
compiler error). Only the
+     * annotation opts in; the automatic {@code groovy.target.closure.pack} 
path is always lenient. A
+     * nested closure is a structural decline (its enclosing context is a 
generated function, not the
+     * owner class), so it is not reported.
+     */
+    private void reportUnpacked(final ClosureExpression expression) {
+        if (controller.isInGeneratedFunction()) return;
+        PackMode mode = annotatedMode();
+        // LENIENT/DISABLED are both silent 

> hoist eligible closure bodies to a shared adapter @PackedClosures/flag 
> (GEP-27 S1)
> ----------------------------------------------------------------------------------
>
>                 Key: GROOVY-12151
>                 URL: https://issues.apache.org/jira/browse/GROOVY-12151
>             Project: Groovy
>          Issue Type: Improvement
>            Reporter: Paul King
>            Assignee: Paul King
>            Priority: Major
>




--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to