[
https://issues.apache.org/jira/browse/GROOVY-11955?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Paul King updated GROOVY-11955:
-------------------------------
Description:
The chained call method flattening we added in GROOVY-7785 generates slightly
slower bytecode and slightly larger bytecode. This isn't necessarily a problem
for most code but for frameworks like Spock which inject quite a bit of code
when converting assertions to their value-recording form, it can reduce the
number of assertions that can appear in a method before hitting JVM limits.
Credit to Juho Naalisvaara on Slack for providing the example (made up example
to show an issue they were having in their real tests):
{code}
static def generateSpec(def count) {
def assertions = (1..count).collect { i ->
"assert obj.field${i % 10} == 'value${i % 10}'"
}.join("\n\t\t\t\t")
def spec = """\
@Grab('org.spockframework:spock-core:2.4-groovy-5.0')
import spock.lang.Specification
class MethodTooLargeReproducerSpec extends Specification {
def "verifyAll with ${count} assertions"() {
given:
def obj = [field0: 'value0', field1: 'value1', field2: 'value2',
field3: 'value3', field4: 'value4', field5: 'value5',
field6: 'value6', field7: 'value7', field8: 'value8',
field9: 'value9']
expect:
verifyAll {
\t${assertions}
}
}
}
"""
}
def gcl = new GroovyClassLoader()
(589..597).step(1).each { count ->
def src = generateSpec(count)
try {
gcl.parseClass(src)
println "OK: ${count}"
} catch (Exception e) {
println "FAILED: ${count} — ${e.message}"
System.exit(1)
}
}
{code}
This code bombs out with an error like this:
{noformat}
FAILED: 589 — startup failed:
General error during instruction selection: Method too large:
MethodTooLargeReproducerSpec$__spock_feature_0_0_closure1.doCall
(Ljava/lang/Object;)Ljava/lang/Object;
groovyjarjarasm.asm.MethodTooLargeException: Method too large:
MethodTooLargeReproducerSpec$__spock_feature_0_0_closure1.doCall
(Ljava/lang/Object;)Ljava/lang/Object;
at
groovyjarjarasm.asm.MethodWriter.computeMethodInfoSize(MethodWriter.java:2088)
...
{noformat}
For Groovy 5.0.4 you could have 595 assertions (with similar complexity to
above) in a method before hitting this limit. In Groovy 5.0.5 you could only
have 585. A small decrease, and yes workarounds like splitting the method come
to mind, but I propose to add a guard so we do the flattening only when
chaining depth gets above a threshold.
In addition, the flattened bytecode is very slightly less performant, so this
will be a win for code not needing the unrolling. We know the stackoverflow
territory covered by GROOVY-7785 is 500-1000 calls deep, so I was going to
propose a threshold of 64. We can adjust if we don't find that gives us best
performance.
was:
If the following statement is pasted into a Groovy shell, it's going to throw a
{{StackoverflowException}}:
{code}
static def generateSpec(def count) {
def assertions = (1..count).collect { i ->
"assert obj.field${i % 10} == 'value${i % 10}'"
}.join("\n\t\t\t\t")
def spec = """\
@Grab('org.spockframework:spock-core:2.4-groovy-5.0')
import spock.lang.Specification
class MethodTooLargeReproducerSpec extends Specification {
def "verifyAll with ${count} assertions"() {
given:
def obj = [field0: 'value0', field1: 'value1', field2: 'value2',
field3: 'value3', field4: 'value4', field5: 'value5',
field6: 'value6', field7: 'value7', field8: 'value8',
field9: 'value9']
expect:
verifyAll {
\t${assertions}
}
}
}
"""
}
def gcl = new GroovyClassLoader()
(589..597).step(1).each { count ->
def src = generateSpec(count)
try {
gcl.parseClass(src)
println "OK: ${count}"
} catch (Exception e) {
println "FAILED: ${count} — ${e.message}"
System.exit(1)
}
}
{code}
Trying to nail it to a certain number of method calls by starting with less
methods and then adding one at a time doesn't work, since that apparently never
throws an exception.
Note that this is not tied to {{StringBuilder::append}}. We discovered this
issue in Gremlin, where it's pretty common to have a lot of chained method
calls.
> Provide a size guard for flattening of chained method calls
> -----------------------------------------------------------
>
> Key: GROOVY-11955
> URL: https://issues.apache.org/jira/browse/GROOVY-11955
> Project: Groovy
> Issue Type: Bug
> Reporter: Daniel Kuppitz
> Assignee: Paul King
> Priority: Major
> Fix For: 6.0.0-alpha-1, 5.0.5
>
>
> The chained call method flattening we added in GROOVY-7785 generates slightly
> slower bytecode and slightly larger bytecode. This isn't necessarily a
> problem for most code but for frameworks like Spock which inject quite a bit
> of code when converting assertions to their value-recording form, it can
> reduce the number of assertions that can appear in a method before hitting
> JVM limits.
> Credit to Juho Naalisvaara on Slack for providing the example (made up
> example to show an issue they were having in their real tests):
> {code}
> static def generateSpec(def count) {
> def assertions = (1..count).collect { i ->
> "assert obj.field${i % 10} == 'value${i % 10}'"
> }.join("\n\t\t\t\t")
> def spec = """\
> @Grab('org.spockframework:spock-core:2.4-groovy-5.0')
> import spock.lang.Specification
> class MethodTooLargeReproducerSpec extends Specification {
> def "verifyAll with ${count} assertions"() {
> given:
> def obj = [field0: 'value0', field1: 'value1', field2: 'value2',
> field3: 'value3', field4: 'value4', field5: 'value5',
> field6: 'value6', field7: 'value7', field8: 'value8',
> field9: 'value9']
> expect:
> verifyAll {
> \t${assertions}
> }
> }
> }
> """
> }
> def gcl = new GroovyClassLoader()
> (589..597).step(1).each { count ->
> def src = generateSpec(count)
> try {
> gcl.parseClass(src)
> println "OK: ${count}"
> } catch (Exception e) {
> println "FAILED: ${count} — ${e.message}"
> System.exit(1)
> }
> }
> {code}
> This code bombs out with an error like this:
> {noformat}
> FAILED: 589 — startup failed:
> General error during instruction selection: Method too large:
> MethodTooLargeReproducerSpec$__spock_feature_0_0_closure1.doCall
> (Ljava/lang/Object;)Ljava/lang/Object;
> groovyjarjarasm.asm.MethodTooLargeException: Method too large:
> MethodTooLargeReproducerSpec$__spock_feature_0_0_closure1.doCall
> (Ljava/lang/Object;)Ljava/lang/Object;
> at
> groovyjarjarasm.asm.MethodWriter.computeMethodInfoSize(MethodWriter.java:2088)
> ...
> {noformat}
> For Groovy 5.0.4 you could have 595 assertions (with similar complexity to
> above) in a method before hitting this limit. In Groovy 5.0.5 you could only
> have 585. A small decrease, and yes workarounds like splitting the method
> come to mind, but I propose to add a guard so we do the flattening only when
> chaining depth gets above a threshold.
> In addition, the flattened bytecode is very slightly less performant, so this
> will be a win for code not needing the unrolling. We know the stackoverflow
> territory covered by GROOVY-7785 is 500-1000 calls deep, so I was going to
> propose a threshold of 64. We can adjust if we don't find that gives us best
> performance.
--
This message was sent by Atlassian Jira
(v8.20.10#820010)