performance - Spock 性能问题

标签 performance testing groovy spock

我在 Spock 中实现的规范的性能存在一些问题 - 我的意思是特别是执行时间。在深入研究问题后,我注意到它与设置规范有某种关系 - 我不是说 setup()方法特别。

这个发现后,我加了@Shared规范中声明的所有字段的注释,它的运行速度比以前快 2 倍。然后,我想,性能问题可能与ConcurrentHashMap有关。或 random*方法(来自 commons-lang3)但事实并非如此。

最后,无奈之下,我用以下方式装饰了规范中的所有字段:

class EntryFacadeSpec extends Specification {

  static {
    println(System.currentTimeMillis())
  }
  @Shared
  def o = new Object()
  static {
    println(System.currentTimeMillis())
  }
  @Shared
  private salesEntries = new InMemorySalesEntryRepository()
  static {
    println(System.currentTimeMillis())
  }
  @Shared
  private purchaseEntries = new InMemoryPurchaseEntryRepository()
  static { 
    println(System.currentTimeMillis())
  }

  ...

有趣的是,无论哪个字段被声明为第一个字段,初始化该字段都需要数百毫秒:
1542801494583
1542801495045
1542801495045
1542801495045
1542801495045
1542801495045
1542801495045
1542801495045
1542801495045
1542801495045
1542801495046
1542801495046
1542801495046
1542801495046
1542801495047
1542801495047

有什么问题?如何节省这几百毫秒?

最佳答案

TL; 博士

调用 println在第一个静态块中初始化了大约 30k+ 个与 Groovy Development Kit 相关的对象。至少需要 50 毫秒才能完成,具体取决于我们运行此测试的笔记本电脑的马力。

细节

我无法重现数百毫秒级别的延迟,但我能够获得 30 到 80 毫秒的延迟。让我们从我在本地测试中使用的重现您的用例的类开始。

import spock.lang.Shared
import spock.lang.Specification

class EntryFacadeSpec extends Specification {

    static {
        println("${System.currentTimeMillis()} - start")
    }

    @Shared
    def o = new Object()

    static {
        println("${System.currentTimeMillis()} - object")
    }

    @Shared
    private salesEntries = new InMemorySalesEntryRepository()

    static {
        println("${System.currentTimeMillis()} - sales")
    }

    @Shared
    private purchaseEntries = new InMemoryPurchaseEntryRepository()

    static {
        println("${System.currentTimeMillis()} - purchase")
    }

    def "test 1"() {
        setup:
        System.out.println(String.format('%d - test 1', System.currentTimeMillis()))

        when:
        def a = 1

        then:
        a == 1
    }

    def "test 2"() {
        setup:
        System.out.println(String.format('%d - test 2', System.currentTimeMillis()))

        when:
        def a = 2

        then:
        a == 2
    }

    static class InMemorySalesEntryRepository {}

    static class InMemoryPurchaseEntryRepository {}
}

现在,当我运行它时,我会在控制台中看到类似的内容。
1542819186960 - start
1542819187019 - object
1542819187019 - sales
1542819187019 - purchase
1542819187035 - test 1
1542819187058 - test 2

我们可以看到两个第一个静态块之间的延迟为 59 毫秒。这两个块之间是什么并不重要,因为 Groovy 编译器将所有这 4 个静态块合并为一个静态块,在普通 Java 中看起来像这样:
static {
    $getCallSiteArray()[0].callStatic(EntryFacadeSpec.class, new GStringImpl(new Object[]{$getCallSiteArray()[1].call(System.class)}, new String[]{"", " - start"}));
    $getCallSiteArray()[2].callStatic(EntryFacadeSpec.class, new GStringImpl(new Object[]{$getCallSiteArray()[3].call(System.class)}, new String[]{"", " - object"}));
    $getCallSiteArray()[4].callStatic(EntryFacadeSpec.class, new GStringImpl(new Object[]{$getCallSiteArray()[5].call(System.class)}, new String[]{"", " - sales"}));
    $getCallSiteArray()[6].callStatic(EntryFacadeSpec.class, new GStringImpl(new Object[]{$getCallSiteArray()[7].call(System.class)}, new String[]{"", " - purchase"}));
}

所以这 59 毫秒的延迟发生在两条第一行之间。让我们在第一行放置一个断点并运行调试器。

enter image description here

让我们跨过这一行到下一行,看看会发生什么:

enter image description here

我们可以看到调用 Groovy 的 println("${System.currentTimeMillis()} - start")导致在 JVM 中创建超过 30k 个对象。现在,让我们跨过第二行到第三行,看看会发生什么:

enter image description here

只创建了几个对象。

此示例显示添加
static {
    println(System.currentTimeMillis())
}

给测试设置增加了意外的复杂性,它没有显示两个类方法的初始化之间存在延迟,但会造成这种延迟。但是,初始化所有与 Groovy 相关的对象的成本是我们无法完全避免的,必须在某处支付。例如,如果我们将测试简化为这样的:
import spock.lang.Specification

class EntryFacadeSpec extends Specification {

    def "test 1"() {
        setup:
        println "asd ${System.currentTimeMillis()}"
        println "asd ${System.currentTimeMillis()}"

        when:
        def a = 1

        then:
        a == 1
    }

    def "test 2"() {
        setup:
        System.out.println(String.format('%d - test 2', System.currentTimeMillis()))

        when:
        def a = 2

        then:
        a == 2
    }
}

我们在第一个 println 中放置了一个断点语句并转到下一个,我们将看到如下内容:

enter image description here

它仍然创建了几千个对象,但比第一个示例中的要少得多,因为我们在第一个示例中看到的大多数对象在 Spock 执行第一个方法之前已经创建。

超频Spock测试性能

我们可以做的第一件事就是使用静态编译。在我的简单测试中,它将执行时间从 300 毫秒(非静态编译)减少到大约 227 毫秒。必须初始化的对象数量也显着减少。如果我使用 @CompileStatic 运行与上面显示的最后一个相同的调试器场景添加,我会得到这样的东西:

enter image description here

它仍然非常重要,但我们看到初始化调用的对象数量 println方法被删除。

还有最后一件事值得一提。当我们使用静态编译并且希望避免在类静态块中调用 Groovy 方法来打印一些输出时,我们可以使用以下组合:
System.out.println(String.format("...", args))

因为 Groovy 正是这样执行的。另一方面,Groovy 中的以下代码:
System.out.printf("...", args)

可能看起来与前一个类似,但它被编译成这样(启用静态编译):
DefaultGroovyMethods.printf(System.out, "...", args)

第二种情况在类静态块中使用时会慢很多,因为此时 Groovy jar 尚未加载,类加载器必须解析 DefaultGroovyMethods JAR 文件中的类。当 Spock 执行测试方法时,如果您使用 System.out.println 并没有太大区别或 DefaultGroovyMethods.printf ,因为 Groovy 类已经加载。

这就是为什么如果我们将您的初始示例重写为以下内容:
import groovy.transform.CompileStatic
import spock.lang.Shared
import spock.lang.Specification

@CompileStatic
class EntryFacadeSpec extends Specification {

    static {
        System.out.println(String.format('%d - start', System.currentTimeMillis()))
    }

    @Shared
    def o = new Object()

    static {
        System.out.println(String.format('%d - object', System.currentTimeMillis()))
    }

    @Shared
    private salesEntries = new InMemorySalesEntryRepository()

    static {
        System.out.println(String.format('%d - sales', System.currentTimeMillis()))
    }

    @Shared
    private purchaseEntries = new InMemoryPurchaseEntryRepository()

    static {
        System.out.println(String.format('%d - purchase', System.currentTimeMillis()))
    }

    def "test 1"() {
        setup:
        System.out.println(String.format('%d - test 1', System.currentTimeMillis()))

        when:
        def a = 1

        then:
        a == 1
    }

    def "test 2"() {
        setup:
        System.out.println(String.format('%d - test 2', System.currentTimeMillis()))

        when:
        def a = 2

        then:
        a == 2
    }

    static class InMemorySalesEntryRepository {}

    static class InMemoryPurchaseEntryRepository {}

}

我们将得到以下控制台输出:
1542821438552 - start
1542821438552 - object
1542821438552 - sales
1542821438552 - purchase
1542821438774 - test 1
1542821438786 - test 2

但更重要的是,它不记录字段初始化时间,因为 Groovy 将这 4 个块编译为一个,如下所示:
static {
    System.out.println(String.format("%d - start", System.currentTimeMillis()));
    Object var10000 = null;
    System.out.println(String.format("%d - object", System.currentTimeMillis()));
    var10000 = null;
    System.out.println(String.format("%d - sales", System.currentTimeMillis()));
    var10000 = null;
    System.out.println(String.format("%d - purchase", System.currentTimeMillis()));
    var10000 = null;
}

第一次和第二次调用之间没有延迟,因为此时不需要加载 Groovy 类。

关于performance - Spock 性能问题,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53412363/

相关文章:

javascript - 检查数组是否已定义且有长度

testing - 我什么时候应该通过手动创建 "stub"版本而不是使用模拟框架来 stub 类型

Jenkinsfile - groovy readFile 方法导致序列化错误

java - 外部化 DEFAULT 配置属性

delphi - 有没有办法保存对象的状态以便以后更快地重新加载?

mysql - 如何以最有效的方式跟踪观看次数?

javascript - JavaScript 数组如何在索引处获取项目?

javascript - 在 NodeJS 中测试对象相等性

testing - 用户验收测试(UAT)和端到端(E2E)测试是一回事吗?

grails - 从 Config.groovy 在 resources.xml 中配置 Spring Integration XML