[
https://issues.apache.org/jira/browse/GROOVY-10307?page=com.atlassian.jira.plugin.system.issuetabpanels:comment-tabpanel&focusedCommentId=18062440#comment-18062440
]
ASF GitHub Bot commented on GROOVY-10307:
-----------------------------------------
Copilot commented on code in PR #2390:
URL: https://github.com/apache/groovy/pull/2390#discussion_r2878081190
##########
subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/grails/CallSiteInvalidationBench.groovy:
##########
@@ -0,0 +1,225 @@
+/*
+ * 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.grails
+
+import groovy.lang.ExpandoMetaClass
Review Comment:
Unused import: `groovy.lang.ExpandoMetaClass` isn't referenced in this
benchmark. Removing it will avoid compiler warnings and keep the benchmark
focused on SwitchPoint invalidation via `metaClass` changes.
```suggestion
```
##########
subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/grails/MetaclassVariationBench.groovy:
##########
@@ -0,0 +1,259 @@
+/*
+ * 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.grails
+
+import groovy.lang.ExpandoMetaClass
+import groovy.lang.GroovySystem
+
+import org.openjdk.jmh.annotations.*
+import org.openjdk.jmh.infra.Blackhole
+
+import java.util.concurrent.TimeUnit
+
+/**
+ * Per-instance metaclass variation overhead (GORM domain class enhancement
pattern).
+ *
+ * @see <a
href="https://issues.apache.org/jira/browse/GROOVY-10307">GROOVY-10307</a>
+ */
+@Warmup(iterations = 3, time = 2, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Fork(2)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@State(Scope.Thread)
+class MetaclassVariationBench {
+ static final int ITERATIONS = 100_000
+ static final int INSTANCE_COUNT = 20
+
+ // Simulates a GORM domain class
+ static class DomainEntity {
+ Long id
+ String name
+ String email
+ boolean active = true
+ int version = 0
+
+ String getFullName() { name ?: 'Unknown' }
+ boolean isActive() { active }
+ int getVersion() { version }
+
+ DomainEntity save() {
+ version++
+ if (id == null) id = System.nanoTime()
+ this
+ }
+
+ Map toMap() {
+ [id: id, name: name, email: email, active: active, version:
version]
+ }
+ }
+
+ // Additional domain class types
+ static class DomainTypeB {
+ String label = "dept"
+ int count = 5
+ int getCount() { count }
+ }
+
+ static class DomainTypeC {
+ String status = "ACTIVE"
+ BigDecimal budget = 100000.0
+ String getStatus() { status }
+ }
+
+ static class DomainTypeD {
+ int priority = 5
+ String assignee = "unassigned"
+ int getPriority() { priority }
+ }
+
+ // Unrelated type for cross-type invalidation
+ static class ServiceType {
+ String config = "default"
+ }
+
+ List<DomainEntity> sharedMetaclassInstances
+ List<DomainEntity> perInstanceMetaclassInstances
+ DomainTypeB typeB
+ DomainTypeC typeC
+ DomainTypeD typeD
+
+ @Setup(Level.Iteration)
+ void setup() {
+ GroovySystem.metaClassRegistry.removeMetaClass(DomainEntity)
+ GroovySystem.metaClassRegistry.removeMetaClass(DomainTypeB)
+ GroovySystem.metaClassRegistry.removeMetaClass(DomainTypeC)
+ GroovySystem.metaClassRegistry.removeMetaClass(DomainTypeD)
+ GroovySystem.metaClassRegistry.removeMetaClass(ServiceType)
+
+ // Shared default class metaclass
+ sharedMetaclassInstances = (1..INSTANCE_COUNT).collect { i ->
+ new DomainEntity(id: i, name: "User$i", email: "user${i}@test.com")
+ }
+
+ // Per-instance ExpandoMetaClass (GORM trait pattern)
+ perInstanceMetaclassInstances = (1..INSTANCE_COUNT).collect { i ->
+ def entity = new DomainEntity(id: i, name: "Enhanced$i", email:
"e${i}@test.com")
+ def emc = new ExpandoMetaClass(DomainEntity, false, true)
+ // GORM-injected methods
+ emc.validate = { -> delegate.name != null && delegate.email !=
null }
+ emc.delete = { -> delegate.id = null; delegate }
+ emc.addToDependencies = { item -> delegate }
+ emc.initialize()
+ entity.metaClass = emc
+ entity
+ }
+
+ typeB = new DomainTypeB()
+ typeC = new DomainTypeC()
+ typeD = new DomainTypeD()
+ }
+
+ /** Method calls on instances sharing default class metaclass. */
+ @Benchmark
+ void baselineSharedMetaclass(Blackhole bh) {
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ def entity = sharedMetaclassInstances[i % INSTANCE_COUNT]
+ sum += entity.getFullName().length()
+ sum += entity.getVersion()
+ }
+ bh.consume(sum)
+ }
+
+ /** Method calls on instances each with their own ExpandoMetaClass. */
+ @Benchmark
+ void perInstanceMetaclass(Blackhole bh) {
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ def entity = perInstanceMetaclassInstances[i % INSTANCE_COUNT]
+ sum += entity.getFullName().length()
+ sum += entity.getVersion()
+ }
+ bh.consume(sum)
+ }
+
+ /** Calling GORM-injected methods on per-instance EMC objects. */
+ @Benchmark
+ void perInstanceInjectedMethodCalls(Blackhole bh) {
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ def entity = perInstanceMetaclassInstances[i % INSTANCE_COUNT]
+ boolean valid = entity.validate()
+ sum += valid ? 1 : 0
+ }
+ bh.consume(sum)
+ }
+
+ /** GORM startup: enhance 4 domain types then steady-state calls. */
+ @Benchmark
+ void multiClassStartupThenSteadyState(Blackhole bh) {
+ // Phase 1: Enhance 4 domain class types
+ DomainEntity.metaClass.static.findAllByName = { String n -> [] }
+ DomainEntity.metaClass.static.countByActive = { boolean a -> 0 }
+
+ DomainTypeB.metaClass.static.findAllByLabel = { String l -> [] }
+ DomainTypeB.metaClass.static.countByCount = { int c -> 0 }
+
+ DomainTypeC.metaClass.static.findAllByStatus = { String s -> [] }
+ DomainTypeC.metaClass.static.findByBudgetGreaterThan = { BigDecimal b
-> null }
+
+ DomainTypeD.metaClass.static.findAllByPriority = { int p -> [] }
+ DomainTypeD.metaClass.static.findByAssignee = { String a -> null }
+
+ // Phase 2: Steady-state calls
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ def entity = sharedMetaclassInstances[i % INSTANCE_COUNT]
+ sum += entity.getFullName().length()
+ sum += typeB.getCount()
+ sum += typeC.getStatus().length()
+ sum += typeD.getPriority()
+ }
+ bh.consume(sum)
+ }
+
+ /** Baseline: same steady-state work without preceding metaclass
enhancements. */
+ @Benchmark
+ void baselineMultiClassNoStartup(Blackhole bh) {
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ def entity = sharedMetaclassInstances[i % INSTANCE_COUNT]
+ sum += entity.getFullName().length()
+ sum += typeB.getCount()
+ sum += typeC.getStatus().length()
+ sum += typeD.getPriority()
+ }
+ bh.consume(sum)
+ }
+
+ /** Calling dynamic finders injected via static metaclass. */
+ @Benchmark
+ void dynamicFinderCalls(Blackhole bh) {
+ // Inject dynamic finders
+ DomainEntity.metaClass.static.findByName = { String n ->
+ [new DomainEntity(name: n)]
+ }
+ DomainEntity.metaClass.static.findAllByActive = { boolean a ->
Review Comment:
`dynamicFinderCalls` injects/overwrites static metaclass methods inside the
benchmark method, so the reported time includes the injection + invalidation
cost on every JMH invocation (and repeatedly invalidates call sites during
measurement). If the intent is to measure call overhead after a single
injection event, move the metaclass injection to `@Setup` (e.g.,
`Level.Invocation` or `Level.Iteration`) and keep the benchmark body focused on
the calls.
##########
subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/grails/MetaclassVariationBench.groovy:
##########
@@ -0,0 +1,259 @@
+/*
+ * 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.grails
+
+import groovy.lang.ExpandoMetaClass
+import groovy.lang.GroovySystem
+
+import org.openjdk.jmh.annotations.*
+import org.openjdk.jmh.infra.Blackhole
+
+import java.util.concurrent.TimeUnit
+
+/**
+ * Per-instance metaclass variation overhead (GORM domain class enhancement
pattern).
+ *
+ * @see <a
href="https://issues.apache.org/jira/browse/GROOVY-10307">GROOVY-10307</a>
+ */
+@Warmup(iterations = 3, time = 2, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Fork(2)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@State(Scope.Thread)
+class MetaclassVariationBench {
+ static final int ITERATIONS = 100_000
+ static final int INSTANCE_COUNT = 20
+
+ // Simulates a GORM domain class
+ static class DomainEntity {
+ Long id
+ String name
+ String email
+ boolean active = true
+ int version = 0
+
+ String getFullName() { name ?: 'Unknown' }
+ boolean isActive() { active }
+ int getVersion() { version }
+
+ DomainEntity save() {
+ version++
+ if (id == null) id = System.nanoTime()
+ this
+ }
+
+ Map toMap() {
+ [id: id, name: name, email: email, active: active, version:
version]
+ }
+ }
+
+ // Additional domain class types
+ static class DomainTypeB {
+ String label = "dept"
+ int count = 5
+ int getCount() { count }
+ }
+
+ static class DomainTypeC {
+ String status = "ACTIVE"
+ BigDecimal budget = 100000.0
+ String getStatus() { status }
+ }
+
+ static class DomainTypeD {
+ int priority = 5
+ String assignee = "unassigned"
+ int getPriority() { priority }
+ }
+
+ // Unrelated type for cross-type invalidation
+ static class ServiceType {
+ String config = "default"
+ }
+
+ List<DomainEntity> sharedMetaclassInstances
+ List<DomainEntity> perInstanceMetaclassInstances
+ DomainTypeB typeB
+ DomainTypeC typeC
+ DomainTypeD typeD
+
+ @Setup(Level.Iteration)
+ void setup() {
+ GroovySystem.metaClassRegistry.removeMetaClass(DomainEntity)
+ GroovySystem.metaClassRegistry.removeMetaClass(DomainTypeB)
+ GroovySystem.metaClassRegistry.removeMetaClass(DomainTypeC)
+ GroovySystem.metaClassRegistry.removeMetaClass(DomainTypeD)
+ GroovySystem.metaClassRegistry.removeMetaClass(ServiceType)
+
+ // Shared default class metaclass
+ sharedMetaclassInstances = (1..INSTANCE_COUNT).collect { i ->
+ new DomainEntity(id: i, name: "User$i", email: "user${i}@test.com")
+ }
+
+ // Per-instance ExpandoMetaClass (GORM trait pattern)
+ perInstanceMetaclassInstances = (1..INSTANCE_COUNT).collect { i ->
+ def entity = new DomainEntity(id: i, name: "Enhanced$i", email:
"e${i}@test.com")
+ def emc = new ExpandoMetaClass(DomainEntity, false, true)
+ // GORM-injected methods
+ emc.validate = { -> delegate.name != null && delegate.email !=
null }
+ emc.delete = { -> delegate.id = null; delegate }
+ emc.addToDependencies = { item -> delegate }
+ emc.initialize()
+ entity.metaClass = emc
+ entity
+ }
+
+ typeB = new DomainTypeB()
+ typeC = new DomainTypeC()
+ typeD = new DomainTypeD()
+ }
+
+ /** Method calls on instances sharing default class metaclass. */
+ @Benchmark
+ void baselineSharedMetaclass(Blackhole bh) {
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ def entity = sharedMetaclassInstances[i % INSTANCE_COUNT]
+ sum += entity.getFullName().length()
+ sum += entity.getVersion()
+ }
+ bh.consume(sum)
+ }
+
+ /** Method calls on instances each with their own ExpandoMetaClass. */
+ @Benchmark
+ void perInstanceMetaclass(Blackhole bh) {
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ def entity = perInstanceMetaclassInstances[i % INSTANCE_COUNT]
+ sum += entity.getFullName().length()
+ sum += entity.getVersion()
+ }
+ bh.consume(sum)
+ }
+
+ /** Calling GORM-injected methods on per-instance EMC objects. */
+ @Benchmark
+ void perInstanceInjectedMethodCalls(Blackhole bh) {
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ def entity = perInstanceMetaclassInstances[i % INSTANCE_COUNT]
+ boolean valid = entity.validate()
+ sum += valid ? 1 : 0
+ }
+ bh.consume(sum)
+ }
+
+ /** GORM startup: enhance 4 domain types then steady-state calls. */
+ @Benchmark
+ void multiClassStartupThenSteadyState(Blackhole bh) {
+ // Phase 1: Enhance 4 domain class types
+ DomainEntity.metaClass.static.findAllByName = { String n -> [] }
+ DomainEntity.metaClass.static.countByActive = { boolean a -> 0 }
+
+ DomainTypeB.metaClass.static.findAllByLabel = { String l -> [] }
+ DomainTypeB.metaClass.static.countByCount = { int c -> 0 }
+
+ DomainTypeC.metaClass.static.findAllByStatus = { String s -> [] }
+ DomainTypeC.metaClass.static.findByBudgetGreaterThan = { BigDecimal b
-> null }
+
+ DomainTypeD.metaClass.static.findAllByPriority = { int p -> [] }
+ DomainTypeD.metaClass.static.findByAssignee = { String a -> null }
+
+ // Phase 2: Steady-state calls
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ def entity = sharedMetaclassInstances[i % INSTANCE_COUNT]
+ sum += entity.getFullName().length()
+ sum += typeB.getCount()
+ sum += typeC.getStatus().length()
+ sum += typeD.getPriority()
+ }
+ bh.consume(sum)
+ }
+
+ /** Baseline: same steady-state work without preceding metaclass
enhancements. */
+ @Benchmark
+ void baselineMultiClassNoStartup(Blackhole bh) {
+ int sum = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ def entity = sharedMetaclassInstances[i % INSTANCE_COUNT]
+ sum += entity.getFullName().length()
+ sum += typeB.getCount()
+ sum += typeC.getStatus().length()
+ sum += typeD.getPriority()
+ }
+ bh.consume(sum)
+ }
+
+ /** Calling dynamic finders injected via static metaclass. */
+ @Benchmark
+ void dynamicFinderCalls(Blackhole bh) {
+ // Inject dynamic finders
+ DomainEntity.metaClass.static.findByName = { String n ->
+ [new DomainEntity(name: n)]
+ }
+ DomainEntity.metaClass.static.findAllByActive = { boolean a ->
+ [new DomainEntity(active: a)]
+ }
+
+ for (int i = 0; i < ITERATIONS / 10; i++) {
+ def result1 = DomainEntity.findByName("User${i % 10}")
+ def result2 = DomainEntity.findAllByActive(true)
+ bh.consume(result1)
+ bh.consume(result2)
+ }
+ }
+
+ /** Mixed compiled method calls and dynamic finder calls. */
+ @Benchmark
+ void mixedCompiledAndDynamicFinders(Blackhole bh) {
+ DomainEntity.metaClass.static.findByName = { String n ->
+ [new DomainEntity(name: n)]
+ }
Review Comment:
Same issue as `dynamicFinderCalls`: this benchmark redefines
`DomainEntity.metaClass.static.findByName` inside the benchmark method, which
makes results sensitive to the metaclass update cost rather than steady-state
mixed dispatch. Consider injecting the dynamic finder in `@Setup` and
benchmarking only the mixed call pattern here (or split into separate “inject”
and “call” benchmarks).
##########
subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/grails/GrailsWorkloadBench.groovy:
##########
@@ -0,0 +1,467 @@
+/*
+ * 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.grails
+
+import groovy.lang.GroovySystem
+
+import org.openjdk.jmh.annotations.*
+import org.openjdk.jmh.infra.Blackhole
+
+import java.util.concurrent.TimeUnit
+
+/**
+ * Groovy collection and DSL patterns from the grails7-performance-regression
demo app.
+ *
+ * @see <a
href="https://github.com/jglapa/grails7-performance-regression">Demo app</a>
+ * @see <a
href="https://issues.apache.org/jira/browse/GROOVY-10307">GROOVY-10307</a>
+ */
+@Warmup(iterations = 3, time = 2, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Fork(2)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@State(Scope.Thread)
+class GrailsWorkloadBench {
+ static final int ITERATIONS = 10_000
+
+ // Domain-like entities from the demo app
+ static class Employee {
+ Long id
+ String firstName
+ String lastName
+ String email
+ String jobTitle
+ String department
+ BigDecimal salary
+ boolean isActive
+ int performanceRating
+ List<String> skills = []
+
+ String getFullName() { "$firstName $lastName" }
+ Map toMap() {
+ [id: id, name: getFullName(), email: email, title: jobTitle,
+ dept: department, salary: salary, active: isActive,
+ rating: performanceRating, skillCount: skills.size()]
+ }
+ }
+
+ static class Project {
+ Long id
+ String name
+ String status
+ BigDecimal budget
+ String department
+ int priority
+ List<Task> tasks = []
+ List<Milestone> milestones = []
+
+ Map toMap() {
+ [id: id, name: name, status: status, budget: budget,
+ taskCount: tasks.size(), milestoneCount: milestones.size()]
+ }
+ }
+
+ static class Task {
+ Long id
+ String name
+ String status
+ int priority
+ int estimatedHours
+ String assignee
+
+ Map toMap() { [id: id, name: name, status: status, priority: priority]
}
+ }
+
+ static class Milestone {
+ Long id
+ String name
+ boolean isCompleted
+ Map toMap() { [id: id, name: name, completed: isCompleted] }
+ }
+
+ // Unrelated type for cross-type invalidation
+ static class PluginConfig {
+ String setting = "default"
+ }
+
+ List<Employee> employees
+ List<Project> projects
+ List<Task> tasks
+
+ @Setup(Level.Iteration)
+ void setup() {
+ GroovySystem.metaClassRegistry.removeMetaClass(Employee)
+ GroovySystem.metaClassRegistry.removeMetaClass(Project)
+ GroovySystem.metaClassRegistry.removeMetaClass(Task)
+ GroovySystem.metaClassRegistry.removeMetaClass(Milestone)
+ GroovySystem.metaClassRegistry.removeMetaClass(PluginConfig)
+
+ def statuses = ['TODO', 'IN_PROGRESS', 'DONE', 'BLOCKED']
+ def departments = ['Engineering', 'Marketing', 'Sales', 'Support',
'HR']
+ def titles = ['Developer', 'Designer', 'Manager', 'Analyst', 'Lead']
+
+ // Sample data matching demo app scale
+ employees = (1..50).collect { i ->
+ new Employee(
+ id: i,
+ firstName: "First$i",
+ lastName: "Last$i",
+ email: "user${i}@example.com",
+ jobTitle: titles[i % titles.size()],
+ department: departments[i % departments.size()],
+ salary: 50000 + (i * 1000),
+ isActive: i % 5 != 0,
+ performanceRating: (i % 5) + 1,
+ skills: (1..(i % 4 + 1)).collect { s -> "Skill$s" }
+ )
+ }
+
+ tasks = (1..100).collect { i ->
+ new Task(
+ id: i,
+ name: "Task$i",
+ status: statuses[i % statuses.size()],
+ priority: (i % 10) + 1,
+ estimatedHours: (i % 8) + 1,
+ assignee: "First${(i % 50) + 1}"
+ )
+ }
+
+ projects = (1..20).collect { i ->
+ def projectTasks = tasks.subList(
+ (i - 1) * 5, Math.min(i * 5, tasks.size())
+ )
+ def milestones = (1..3).collect { m ->
+ new Milestone(id: (i * 3) + m, name: "M${i}-${m}",
isCompleted: m <= 2)
+ }
+ new Project(
+ id: i,
+ name: "Project$i",
+ status: statuses[i % statuses.size()],
+ budget: 100000 + (i * 50000),
+ department: departments[i % departments.size()],
+ priority: (i % 10) + 1,
+ tasks: projectTasks,
+ milestones: milestones
+ )
+ }
+ }
+
+ /** Baseline: findAll/collect/groupBy/collectEntries closure chains. */
+ @Benchmark
+ void baselineCollectionClosureChain(Blackhole bh) {
+ for (int i = 0; i < ITERATIONS; i++) {
+ def activeEmployees = employees.findAll { it.isActive }
+ def mapped = activeEmployees.collect { it.toMap() }
+ def byDept = mapped.groupBy { it.dept }
+ def deptStats = byDept.collectEntries { dept, emps ->
+ [dept, [count: emps.size(), avgRating: emps.sum { it.rating }
/ emps.size()]]
+ }
+ bh.consume(deptStats.size())
+ }
+ }
+
+ /** Collection closure chains with periodic cross-type invalidation. */
+ @Benchmark
+ void collectionClosureChainWithInvalidation(Blackhole bh) {
+ for (int i = 0; i < ITERATIONS; i++) {
+ def activeEmployees = employees.findAll { it.isActive }
+ def mapped = activeEmployees.collect { it.toMap() }
+ def byDept = mapped.groupBy { it.dept }
+ def deptStats = byDept.collectEntries { dept, emps ->
+ [dept, [count: emps.size(), avgRating: emps.sum { it.rating }
/ emps.size()]]
+ }
+ bh.consume(deptStats.size())
+ if (i % 100 == 0) {
+ PluginConfig.metaClass."helper${i % 5}" = { -> i }
+ }
+ }
+ }
+
+ /** Baseline: spread operator (employees*.salary). */
+ @Benchmark
+ void baselineSpreadOperator(Blackhole bh) {
+ for (int i = 0; i < ITERATIONS; i++) {
+ def names = employees*.firstName
+ def salaries = employees*.salary
+ def ratings = employees*.performanceRating
+ bh.consume(names.size() + salaries.size() + ratings.size())
+ }
+ }
+
+ /** Spread operator with periodic cross-type invalidation. */
+ @Benchmark
+ void spreadOperatorWithInvalidation(Blackhole bh) {
+ for (int i = 0; i < ITERATIONS; i++) {
+ def names = employees*.firstName
+ def salaries = employees*.salary
+ def ratings = employees*.performanceRating
+ bh.consume(names.size() + salaries.size() + ratings.size())
+ if (i % 100 == 0) {
+ PluginConfig.metaClass."helper${i % 5}" = { -> i }
+ }
+ }
+ }
+
+ static class CriteriaBuilder {
+ Map result = [:]
+
+ void eq(String field, Object value) {
+ result[field] = value
+ }
+
+ void gt(String field, Object value) {
+ result["${field}_gt"] = value
+ }
+
+ void nested(String name, @DelegatesTo(CriteriaBuilder) Closure cl) {
+ def inner = new CriteriaBuilder()
+ cl.delegate = inner
+ cl.resolveStrategy = Closure.DELEGATE_FIRST
+ cl()
+ result[name] = inner.result
+ }
+
+ Map build() { result }
+ }
+
+ /** Baseline: 3-level nested closure delegation (GORM criteria pattern). */
+ @Benchmark
+ void baselineNestedClosureDelegation(Blackhole bh) {
+ for (int i = 0; i < ITERATIONS; i++) {
+ def builder = new CriteriaBuilder()
+ builder.nested('project') {
+ eq('status', 'IN_PROGRESS')
+ gt('priority', 5)
+ nested('department') {
+ eq('name', "Dept${i % 5}")
+ nested('company') {
+ eq('active', true)
+ }
+ }
+ }
+ bh.consume(builder.build().size())
+ }
+ }
+
+ /** Nested closure delegation with periodic cross-type invalidation. */
+ @Benchmark
+ void nestedClosureDelegationWithInvalidation(Blackhole bh) {
+ for (int i = 0; i < ITERATIONS; i++) {
+ def builder = new CriteriaBuilder()
+ builder.nested('project') {
+ eq('status', 'IN_PROGRESS')
+ gt('priority', 5)
+ nested('department') {
+ eq('name', "Dept${i % 5}")
+ nested('company') {
+ eq('active', true)
+ }
+ }
+ }
+ bh.consume(builder.build().size())
+ if (i % 100 == 0) {
+ PluginConfig.metaClass."helper${i % 5}" = { -> i }
+ }
+ }
+ }
+
+ /** Baseline: GString interpolation with dynamic property access. */
+ @Benchmark
+ void baselineGStringInterpolation(Blackhole bh) {
+ int totalLen = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ def emp = employees[i % employees.size()]
+ String full = "${emp.firstName} ${emp.lastName}"
+ String detail = "${emp.jobTitle} at ${emp.department} -
\$${emp.salary}"
+ String summary = "Employee #${emp.id}: ${full}
(${emp.performanceRating}/5)"
+ totalLen += full.length() + detail.length() + summary.length()
+ }
+ bh.consume(totalLen)
+ }
+
+ /** GString interpolation with periodic cross-type invalidation. */
+ @Benchmark
+ void gstringInterpolationWithInvalidation(Blackhole bh) {
+ int totalLen = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ def emp = employees[i % employees.size()]
+ String full = "${emp.firstName} ${emp.lastName}"
+ String detail = "${emp.jobTitle} at ${emp.department} -
\$${emp.salary}"
+ String summary = "Employee #${emp.id}: ${full}
(${emp.performanceRating}/5)"
+ totalLen += full.length() + detail.length() + summary.length()
+ if (i % 100 == 0) {
+ PluginConfig.metaClass."helper${i % 5}" = { -> i }
+ }
+ }
+ bh.consume(totalLen)
+ }
+
+ /** Baseline: dynamic property access by name string. */
+ @Benchmark
+ void baselineDynamicPropertyByName(Blackhole bh) {
+ String[] fields = ['firstName', 'lastName', 'email', 'jobTitle',
'department']
+ int totalLen = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ def emp = employees[i % employees.size()]
+ for (int f = 0; f < fields.length; f++) {
+ def val = emp."${fields[f]}"
+ totalLen += val?.toString()?.length() ?: 0
+ }
+ }
+ bh.consume(totalLen)
+ }
+
+ /** Dynamic property access with periodic cross-type invalidation. */
+ @Benchmark
+ void dynamicPropertyByNameWithInvalidation(Blackhole bh) {
+ String[] fields = ['firstName', 'lastName', 'email', 'jobTitle',
'department']
+ int totalLen = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ def emp = employees[i % employees.size()]
+ for (int f = 0; f < fields.length; f++) {
+ def val = emp."${fields[f]}"
+ totalLen += val?.toString()?.length() ?: 0
+ }
+ if (i % 100 == 0) {
+ PluginConfig.metaClass."helper${i % 5}" = { -> i }
+ }
+ }
+ bh.consume(totalLen)
+ }
+
+ /** Baseline: project metrics aggregation (demo app's getProjectMetrics).
*/
+ @Benchmark
+ void baselineProjectMetrics(Blackhole bh) {
+ for (int i = 0; i < ITERATIONS; i++) {
+ def project = projects[i % projects.size()]
+ def completedTasks = project.tasks.count { it.status == 'DONE' }
+ def totalHours = project.tasks.sum { it.estimatedHours } ?: 0
+ def completedMilestones = project.milestones.count {
it.isCompleted }
+ def completion = project.tasks.size() > 0 ?
+ (completedTasks / project.tasks.size() * 100) : 0
+ def metrics = [
+ name: project.name,
+ tasks: project.tasks.size(),
+ completed: completedTasks,
+ hours: totalHours,
+ milestones: completedMilestones,
+ completion: completion
+ ]
+ bh.consume(metrics.size())
+ }
+ }
+
+ /** Project metrics with periodic cross-type invalidation. */
+ @Benchmark
+ void projectMetricsWithInvalidation(Blackhole bh) {
+ for (int i = 0; i < ITERATIONS; i++) {
+ def project = projects[i % projects.size()]
+ def completedTasks = project.tasks.count { it.status == 'DONE' }
+ def totalHours = project.tasks.sum { it.estimatedHours } ?: 0
+ def completedMilestones = project.milestones.count {
it.isCompleted }
+ def completion = project.tasks.size() > 0 ?
+ (completedTasks / project.tasks.size() * 100) : 0
+ def metrics = [
+ name: project.name,
+ tasks: project.tasks.size(),
+ completed: completedTasks,
+ hours: totalHours,
+ milestones: completedMilestones,
+ completion: completion
+ ]
+ bh.consume(metrics.size())
+ if (i % 100 == 0) {
+ PluginConfig.metaClass."helper${i % 5}" = { -> i }
+ }
+ }
+ }
+
+ /** Baseline: full analysis combining all patterns (demo app's
runComplexAnalysis). */
+ @Benchmark
+ void baselineFullAnalysis(Blackhole bh) {
+ // Employee analysis
+ def activeEmps = employees.findAll { it.isActive }
+ def empNames = activeEmps*.getFullName()
+ def byDept = activeEmps.groupBy { it.department }
+ def deptSummary = byDept.collectEntries { dept, emps ->
+ def avgSalary = emps.sum { it.salary } / emps.size()
+ def topPerformer = emps.max { it.performanceRating }
+ [dept, [count: emps.size(), avgSalary: avgSalary,
+ top: topPerformer.getFullName()]]
+ }
+
+ // Project metrics
+ def projectSummary = projects.collect { proj ->
+ def done = proj.tasks.count { it.status == 'DONE' }
+ def blocked = proj.tasks.count { it.status == 'BLOCKED' }
+ [name: proj.name, status: proj.status,
+ done: done, blocked: blocked, budget: proj.budget]
+ }
+
+ // Cross-entity: high-priority tasks by department
+ def highPriority = tasks.findAll { it.priority > 7 }
+ def taskSummary = highPriority.groupBy { it.status }
+ .collectEntries { status, tl ->
+ [status, tl.collect { "${it.name} (P${it.priority})" }]
+ }
+
+ bh.consume(deptSummary.size() + projectSummary.size() +
+ taskSummary.size() + empNames.size())
+ }
+
+ /** Full analysis with cross-type invalidation before and during
execution. */
+ @Benchmark
+ void fullAnalysisWithInvalidation(Blackhole bh) {
+ // Ongoing framework metaclass activity
+ PluginConfig.metaClass."preRequest${System.nanoTime() % 3}" = { ->
'init' }
+
Review Comment:
Using `System.nanoTime()` to vary the metaclass property name adds
non-trivial extra work and noise that’s unrelated to SwitchPoint invalidation,
and makes runs harder to compare. Prefer a cheap deterministic counter stored
in the benchmark state (e.g., an `int` incremented/modded) to select among a
small set of property names.
##########
subprojects/performance/src/jmh/groovy/org/apache/groovy/perf/grails/GrailsWorkloadBench.groovy:
##########
@@ -0,0 +1,467 @@
+/*
+ * 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.grails
+
+import groovy.lang.GroovySystem
+
+import org.openjdk.jmh.annotations.*
+import org.openjdk.jmh.infra.Blackhole
+
+import java.util.concurrent.TimeUnit
+
+/**
+ * Groovy collection and DSL patterns from the grails7-performance-regression
demo app.
+ *
+ * @see <a
href="https://github.com/jglapa/grails7-performance-regression">Demo app</a>
+ * @see <a
href="https://issues.apache.org/jira/browse/GROOVY-10307">GROOVY-10307</a>
+ */
+@Warmup(iterations = 3, time = 2, timeUnit = TimeUnit.SECONDS)
+@Measurement(iterations = 5, time = 2, timeUnit = TimeUnit.SECONDS)
+@Fork(2)
+@BenchmarkMode(Mode.AverageTime)
+@OutputTimeUnit(TimeUnit.MILLISECONDS)
+@State(Scope.Thread)
+class GrailsWorkloadBench {
+ static final int ITERATIONS = 10_000
+
+ // Domain-like entities from the demo app
+ static class Employee {
+ Long id
+ String firstName
+ String lastName
+ String email
+ String jobTitle
+ String department
+ BigDecimal salary
+ boolean isActive
+ int performanceRating
+ List<String> skills = []
+
+ String getFullName() { "$firstName $lastName" }
+ Map toMap() {
+ [id: id, name: getFullName(), email: email, title: jobTitle,
+ dept: department, salary: salary, active: isActive,
+ rating: performanceRating, skillCount: skills.size()]
+ }
+ }
+
+ static class Project {
+ Long id
+ String name
+ String status
+ BigDecimal budget
+ String department
+ int priority
+ List<Task> tasks = []
+ List<Milestone> milestones = []
+
+ Map toMap() {
+ [id: id, name: name, status: status, budget: budget,
+ taskCount: tasks.size(), milestoneCount: milestones.size()]
+ }
+ }
+
+ static class Task {
+ Long id
+ String name
+ String status
+ int priority
+ int estimatedHours
+ String assignee
+
+ Map toMap() { [id: id, name: name, status: status, priority: priority]
}
+ }
+
+ static class Milestone {
+ Long id
+ String name
+ boolean isCompleted
+ Map toMap() { [id: id, name: name, completed: isCompleted] }
+ }
+
+ // Unrelated type for cross-type invalidation
+ static class PluginConfig {
+ String setting = "default"
+ }
+
+ List<Employee> employees
+ List<Project> projects
+ List<Task> tasks
+
+ @Setup(Level.Iteration)
+ void setup() {
+ GroovySystem.metaClassRegistry.removeMetaClass(Employee)
+ GroovySystem.metaClassRegistry.removeMetaClass(Project)
+ GroovySystem.metaClassRegistry.removeMetaClass(Task)
+ GroovySystem.metaClassRegistry.removeMetaClass(Milestone)
+ GroovySystem.metaClassRegistry.removeMetaClass(PluginConfig)
+
+ def statuses = ['TODO', 'IN_PROGRESS', 'DONE', 'BLOCKED']
+ def departments = ['Engineering', 'Marketing', 'Sales', 'Support',
'HR']
+ def titles = ['Developer', 'Designer', 'Manager', 'Analyst', 'Lead']
+
+ // Sample data matching demo app scale
+ employees = (1..50).collect { i ->
+ new Employee(
+ id: i,
+ firstName: "First$i",
+ lastName: "Last$i",
+ email: "user${i}@example.com",
+ jobTitle: titles[i % titles.size()],
+ department: departments[i % departments.size()],
+ salary: 50000 + (i * 1000),
+ isActive: i % 5 != 0,
+ performanceRating: (i % 5) + 1,
+ skills: (1..(i % 4 + 1)).collect { s -> "Skill$s" }
+ )
+ }
+
+ tasks = (1..100).collect { i ->
+ new Task(
+ id: i,
+ name: "Task$i",
+ status: statuses[i % statuses.size()],
+ priority: (i % 10) + 1,
+ estimatedHours: (i % 8) + 1,
+ assignee: "First${(i % 50) + 1}"
+ )
+ }
+
+ projects = (1..20).collect { i ->
+ def projectTasks = tasks.subList(
+ (i - 1) * 5, Math.min(i * 5, tasks.size())
+ )
+ def milestones = (1..3).collect { m ->
+ new Milestone(id: (i * 3) + m, name: "M${i}-${m}",
isCompleted: m <= 2)
+ }
+ new Project(
+ id: i,
+ name: "Project$i",
+ status: statuses[i % statuses.size()],
+ budget: 100000 + (i * 50000),
+ department: departments[i % departments.size()],
+ priority: (i % 10) + 1,
+ tasks: projectTasks,
+ milestones: milestones
+ )
+ }
+ }
+
+ /** Baseline: findAll/collect/groupBy/collectEntries closure chains. */
+ @Benchmark
+ void baselineCollectionClosureChain(Blackhole bh) {
+ for (int i = 0; i < ITERATIONS; i++) {
+ def activeEmployees = employees.findAll { it.isActive }
+ def mapped = activeEmployees.collect { it.toMap() }
+ def byDept = mapped.groupBy { it.dept }
+ def deptStats = byDept.collectEntries { dept, emps ->
+ [dept, [count: emps.size(), avgRating: emps.sum { it.rating }
/ emps.size()]]
+ }
+ bh.consume(deptStats.size())
+ }
+ }
+
+ /** Collection closure chains with periodic cross-type invalidation. */
+ @Benchmark
+ void collectionClosureChainWithInvalidation(Blackhole bh) {
+ for (int i = 0; i < ITERATIONS; i++) {
+ def activeEmployees = employees.findAll { it.isActive }
+ def mapped = activeEmployees.collect { it.toMap() }
+ def byDept = mapped.groupBy { it.dept }
+ def deptStats = byDept.collectEntries { dept, emps ->
+ [dept, [count: emps.size(), avgRating: emps.sum { it.rating }
/ emps.size()]]
+ }
+ bh.consume(deptStats.size())
+ if (i % 100 == 0) {
+ PluginConfig.metaClass."helper${i % 5}" = { -> i }
+ }
+ }
+ }
+
+ /** Baseline: spread operator (employees*.salary). */
+ @Benchmark
+ void baselineSpreadOperator(Blackhole bh) {
+ for (int i = 0; i < ITERATIONS; i++) {
+ def names = employees*.firstName
+ def salaries = employees*.salary
+ def ratings = employees*.performanceRating
+ bh.consume(names.size() + salaries.size() + ratings.size())
+ }
+ }
+
+ /** Spread operator with periodic cross-type invalidation. */
+ @Benchmark
+ void spreadOperatorWithInvalidation(Blackhole bh) {
+ for (int i = 0; i < ITERATIONS; i++) {
+ def names = employees*.firstName
+ def salaries = employees*.salary
+ def ratings = employees*.performanceRating
+ bh.consume(names.size() + salaries.size() + ratings.size())
+ if (i % 100 == 0) {
+ PluginConfig.metaClass."helper${i % 5}" = { -> i }
+ }
+ }
+ }
+
+ static class CriteriaBuilder {
+ Map result = [:]
+
+ void eq(String field, Object value) {
+ result[field] = value
+ }
+
+ void gt(String field, Object value) {
+ result["${field}_gt"] = value
+ }
+
+ void nested(String name, @DelegatesTo(CriteriaBuilder) Closure cl) {
+ def inner = new CriteriaBuilder()
+ cl.delegate = inner
+ cl.resolveStrategy = Closure.DELEGATE_FIRST
+ cl()
+ result[name] = inner.result
+ }
+
+ Map build() { result }
+ }
+
+ /** Baseline: 3-level nested closure delegation (GORM criteria pattern). */
+ @Benchmark
+ void baselineNestedClosureDelegation(Blackhole bh) {
+ for (int i = 0; i < ITERATIONS; i++) {
+ def builder = new CriteriaBuilder()
+ builder.nested('project') {
+ eq('status', 'IN_PROGRESS')
+ gt('priority', 5)
+ nested('department') {
+ eq('name', "Dept${i % 5}")
+ nested('company') {
+ eq('active', true)
+ }
+ }
+ }
+ bh.consume(builder.build().size())
+ }
+ }
+
+ /** Nested closure delegation with periodic cross-type invalidation. */
+ @Benchmark
+ void nestedClosureDelegationWithInvalidation(Blackhole bh) {
+ for (int i = 0; i < ITERATIONS; i++) {
+ def builder = new CriteriaBuilder()
+ builder.nested('project') {
+ eq('status', 'IN_PROGRESS')
+ gt('priority', 5)
+ nested('department') {
+ eq('name', "Dept${i % 5}")
+ nested('company') {
+ eq('active', true)
+ }
+ }
+ }
+ bh.consume(builder.build().size())
+ if (i % 100 == 0) {
+ PluginConfig.metaClass."helper${i % 5}" = { -> i }
+ }
+ }
+ }
+
+ /** Baseline: GString interpolation with dynamic property access. */
+ @Benchmark
+ void baselineGStringInterpolation(Blackhole bh) {
+ int totalLen = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ def emp = employees[i % employees.size()]
+ String full = "${emp.firstName} ${emp.lastName}"
+ String detail = "${emp.jobTitle} at ${emp.department} -
\$${emp.salary}"
+ String summary = "Employee #${emp.id}: ${full}
(${emp.performanceRating}/5)"
+ totalLen += full.length() + detail.length() + summary.length()
+ }
+ bh.consume(totalLen)
+ }
+
+ /** GString interpolation with periodic cross-type invalidation. */
+ @Benchmark
+ void gstringInterpolationWithInvalidation(Blackhole bh) {
+ int totalLen = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ def emp = employees[i % employees.size()]
+ String full = "${emp.firstName} ${emp.lastName}"
+ String detail = "${emp.jobTitle} at ${emp.department} -
\$${emp.salary}"
+ String summary = "Employee #${emp.id}: ${full}
(${emp.performanceRating}/5)"
+ totalLen += full.length() + detail.length() + summary.length()
+ if (i % 100 == 0) {
+ PluginConfig.metaClass."helper${i % 5}" = { -> i }
+ }
+ }
+ bh.consume(totalLen)
+ }
+
+ /** Baseline: dynamic property access by name string. */
+ @Benchmark
+ void baselineDynamicPropertyByName(Blackhole bh) {
+ String[] fields = ['firstName', 'lastName', 'email', 'jobTitle',
'department']
+ int totalLen = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ def emp = employees[i % employees.size()]
+ for (int f = 0; f < fields.length; f++) {
+ def val = emp."${fields[f]}"
+ totalLen += val?.toString()?.length() ?: 0
+ }
+ }
+ bh.consume(totalLen)
+ }
+
+ /** Dynamic property access with periodic cross-type invalidation. */
+ @Benchmark
+ void dynamicPropertyByNameWithInvalidation(Blackhole bh) {
+ String[] fields = ['firstName', 'lastName', 'email', 'jobTitle',
'department']
+ int totalLen = 0
+ for (int i = 0; i < ITERATIONS; i++) {
+ def emp = employees[i % employees.size()]
+ for (int f = 0; f < fields.length; f++) {
+ def val = emp."${fields[f]}"
+ totalLen += val?.toString()?.length() ?: 0
+ }
+ if (i % 100 == 0) {
+ PluginConfig.metaClass."helper${i % 5}" = { -> i }
+ }
+ }
+ bh.consume(totalLen)
+ }
+
+ /** Baseline: project metrics aggregation (demo app's getProjectMetrics).
*/
+ @Benchmark
+ void baselineProjectMetrics(Blackhole bh) {
+ for (int i = 0; i < ITERATIONS; i++) {
+ def project = projects[i % projects.size()]
+ def completedTasks = project.tasks.count { it.status == 'DONE' }
+ def totalHours = project.tasks.sum { it.estimatedHours } ?: 0
+ def completedMilestones = project.milestones.count {
it.isCompleted }
+ def completion = project.tasks.size() > 0 ?
+ (completedTasks / project.tasks.size() * 100) : 0
+ def metrics = [
+ name: project.name,
+ tasks: project.tasks.size(),
+ completed: completedTasks,
+ hours: totalHours,
+ milestones: completedMilestones,
+ completion: completion
+ ]
+ bh.consume(metrics.size())
+ }
+ }
+
+ /** Project metrics with periodic cross-type invalidation. */
+ @Benchmark
+ void projectMetricsWithInvalidation(Blackhole bh) {
+ for (int i = 0; i < ITERATIONS; i++) {
+ def project = projects[i % projects.size()]
+ def completedTasks = project.tasks.count { it.status == 'DONE' }
+ def totalHours = project.tasks.sum { it.estimatedHours } ?: 0
+ def completedMilestones = project.milestones.count {
it.isCompleted }
+ def completion = project.tasks.size() > 0 ?
+ (completedTasks / project.tasks.size() * 100) : 0
+ def metrics = [
+ name: project.name,
+ tasks: project.tasks.size(),
+ completed: completedTasks,
+ hours: totalHours,
+ milestones: completedMilestones,
+ completion: completion
+ ]
+ bh.consume(metrics.size())
+ if (i % 100 == 0) {
+ PluginConfig.metaClass."helper${i % 5}" = { -> i }
+ }
+ }
+ }
+
+ /** Baseline: full analysis combining all patterns (demo app's
runComplexAnalysis). */
+ @Benchmark
+ void baselineFullAnalysis(Blackhole bh) {
+ // Employee analysis
+ def activeEmps = employees.findAll { it.isActive }
+ def empNames = activeEmps*.getFullName()
+ def byDept = activeEmps.groupBy { it.department }
+ def deptSummary = byDept.collectEntries { dept, emps ->
+ def avgSalary = emps.sum { it.salary } / emps.size()
+ def topPerformer = emps.max { it.performanceRating }
+ [dept, [count: emps.size(), avgSalary: avgSalary,
+ top: topPerformer.getFullName()]]
+ }
+
+ // Project metrics
+ def projectSummary = projects.collect { proj ->
+ def done = proj.tasks.count { it.status == 'DONE' }
+ def blocked = proj.tasks.count { it.status == 'BLOCKED' }
+ [name: proj.name, status: proj.status,
+ done: done, blocked: blocked, budget: proj.budget]
+ }
+
+ // Cross-entity: high-priority tasks by department
+ def highPriority = tasks.findAll { it.priority > 7 }
+ def taskSummary = highPriority.groupBy { it.status }
+ .collectEntries { status, tl ->
+ [status, tl.collect { "${it.name} (P${it.priority})" }]
+ }
+
+ bh.consume(deptSummary.size() + projectSummary.size() +
+ taskSummary.size() + empNames.size())
+ }
+
+ /** Full analysis with cross-type invalidation before and during
execution. */
+ @Benchmark
+ void fullAnalysisWithInvalidation(Blackhole bh) {
+ // Ongoing framework metaclass activity
+ PluginConfig.metaClass."preRequest${System.nanoTime() % 3}" = { ->
'init' }
+
+ // Employee analysis
+ def activeEmps = employees.findAll { it.isActive }
+ def empNames = activeEmps*.getFullName()
+ def byDept = activeEmps.groupBy { it.department }
+ def deptSummary = byDept.collectEntries { dept, emps ->
+ def avgSalary = emps.sum { it.salary } / emps.size()
+ def topPerformer = emps.max { it.performanceRating }
+ [dept, [count: emps.size(), avgSalary: avgSalary,
+ top: topPerformer.getFullName()]]
+ }
+
+ // Mid-request metaclass change
+ PluginConfig.metaClass."midRequest${System.nanoTime() % 3}" = { ->
'lazy' }
+
Review Comment:
Same concern as the pre-request invalidation: `System.nanoTime()` here adds
unrelated overhead and run-to-run variance. Use a state counter (or reuse the
loop index if you want per-iteration variation) to pick among a bounded set of
metaclass property names.
> Groovy 4 runtime performance on average 2.4x slower than Groovy 3
> -----------------------------------------------------------------
>
> Key: GROOVY-10307
> URL: https://issues.apache.org/jira/browse/GROOVY-10307
> Project: Groovy
> Issue Type: Bug
> Components: bytecode, performance
> Affects Versions: 4.0.0-beta-1, 3.0.9
> Environment: OpenJDK Runtime Environment AdoptOpenJDK-11.0.11+9
> (build 11.0.11+9)
> OpenJDK 64-Bit Server VM AdoptOpenJDK-11.0.11+9 (build 11.0.11+9, mixed mode)
> WIN10 (tests) / REL 8 (web application)
> IntelliJ 2021.2
> Reporter: mgroovy
> Priority: Major
> Attachments: groovy_3_0_9_gc.png, groovy_3_0_9_loop2.png,
> groovy_3_0_9_loop4.png, groovy_3_0_9_mem.png, groovy_4_0_0_b1_loop2.png,
> groovy_4_0_0_b1_loop4.png, groovy_4_0_0_b1_loop4_gc.png,
> groovy_4_0_0_b1_loop4_mem.png,
> groovysql_performance_groovy4_2_xx_yy_zzzz.groovy, loops.groovy,
> profile3.txt, profile4-loops.txt, profile4.txt, profile4d.txt
>
>
> Groovy 4.0.0-beta-1 runtime performance in our framework is on average 2 to 3
> times slower compared to using Groovy 3.0.9 (regular i.e. non-INDY)
> * Our complete framework and application code is completely written in
> Groovy, spread over multiple IntelliJ modules
> ** mixed @CompileDynamic/@TypeChecked and @CompileStatic
> ** No Java classes left in project, i.e. no cross compilation occurs
> * We build using IntelliJ 2021.2 Groovy build process, then run / deploy the
> compiled class files
> ** We do _not_ use a Groovy based DSL, nor do we execute Groovy scripts
> during execution
> * Performance degradation when using Groovy 4.0.0-beta-1 instead of Groovy
> 3.0.9 (non-INDY):
> ** The performance of the largest of our web applications has dropped 3x
> (startup) / 2x (table refresh) respectively
> *** Stack: Tomcat/Vaadin/Ebean plus framework generated SQL
> ** Our test suite runs about 2.4 times as long as before (120 min when using
> G4, compared to about 50 min with G3)
> *** JUnit 5
> *** test suite also contains no scripts / dynamic code execution
> *** Individual test performance varies: A small number of tests runs faster,
> but the majority is slower, with some extreme cases taking nearly 10x as long
> to finish
> * Using Groovy 3.0.9 INDY displays nearly identical performance degradation,
> so it seems that the use of invoke dynamic is somehow at fault
--
This message was sent by Atlassian Jira
(v8.20.10#820010)