java – 为什么执行Mockito mocks的性能如此不稳定?

任何人都有一个解释,甚至更好的建议解决方案,为什么执行Mockito嘲笑所花费的时间是如此不稳定?我能想出的最简单的SSCCE如下:

import static org.mockito.Mockito.mock;

public class TestSimpleMockTiming
{
    public static final void main (final String args [])
    {
        final Runnable theMock = mock (Runnable.class);

        int tookShort = 0;
        int tookMedium = 0;
        int tookLong = 0;
        int tookRidiculouslyLong = 0;
        long longest = 0;

        for (int n = 0; n < 2000000; n++)
        {
            final long startTime = System.nanoTime ();
            theMock.run ();
            final long duration = System.nanoTime () - startTime;

            if (duration < 1000000)                 // 0.001 seconds
                tookShort++;
            else if (duration < 100000000)      // 0.1 seconds
                tookMedium++;
            else if (duration < 1000000000)     // 1 second !!!
                tookLong++;
            else
                tookRidiculouslyLong++;

            longest = Math.max (longest, duration);
        }

        System.out.println (tookShort + ", " + tookMedium + ", " + tookLong + ", " + tookRidiculouslyLong);
        System.out.println ("Longest duration was " + longest + " ns");
    }
}

如果我运行它(在Eclipse中,在Win 7 x64上使用JDK 1.7.45),典型输出如下:

1999983, 4, 9, 4
Longest duration was 5227445252 ns

因此,虽然在大多数情况下模拟执行速度非常快,但有几次执行甚至超过1秒.对于一种什么都不做的方法来说,这是永恒的.从我的实验中,我不相信问题是System.nanoTime()的准确性,我认为模拟确实需要花费很长时间才能执行.我能做些什么来改善这一点并使时间表现更一致吗?

(仅供参考,为什么这是一个问题是我有一个包含各种框架的Swing应用程序,我尝试为框架编写JUnit测试,以便我可以测试layoutManagers的行为是否正常,而无需启动整个应用程序并导航在一个这样的测试中,屏幕使用javax.swing.Timer来实现滚动,因此当鼠标靠近帧的末端时,显示器将围绕一个区域平移.我注意到这个行为是非常不稳定,滚动虽然通常很好但会周期性地冻结一秒钟而且看起来很可怕.我在这周围写了一个SSCCE,认为问题是Swing Timers不能依赖于以一致的速率发射,并且在SSCCE,它完美地运作.

经过几个小时的撕裂我的头发,然后试图发现我的真实代码和滚动演示SSCCE之间的差异,我开始将纳米计时器放在重复运行的代码块周围,注意到我的paintComponent方法花费的时间非常不稳定并最终缩小这是一个模拟电话.通过运行真实的应用程序来测试屏幕,滚动行为很顺利,它只是来自JUnit测试的问题,因为模拟调用,这导致我使用上面发布的SSCCE单独测试一个简单的模拟.)

非常感谢!

解决方法:

该测试以多种方式存在缺陷.如果你想要正确地进行基准测试,我强烈建议使用JMH,它由Alexey Shipilev完成,它比我们聪明得多,并且在JVM上比在我们心爱的星球上做Java的人更有知识.

这是测试存在缺陷的最显着方式.

>测试忽略了JVM正在做的事情,比如预热阶段,编译C1和C2线程,GC,线程问题(即使这个代码不是多线程的,JVM / OS可能还需要做其他事情)等等. .
>如果实际的OS / JVM / CPU组合提供高达纳秒的正确分辨率,测试似乎确实忽略了.

即使有System.nanoTime(),您确定JVM和操作系统具有适当的分辨率.例如,在Windows上,JVM无法访问真正的纳秒,而是访问某些计数器,而不是挂钟时间. javadoc声明了这一点,这里是片段:

This method can only be used to measure elapsed time and is not related to any other notion of system or wall-clock time. The value returned represents nanoseconds since some fixed but arbitrary origin time (perhaps in the future, so values may be negative). The same origin is used by all invocations of this method in an instance of a Java virtual machine; other virtual machine instances are likely to use a different origin.

This method provides nanosecond precision, but not necessarily nanosecond resolution (that is, how frequently the value changes) – no guarantees are made except that the resolution is at least as good as that of currentTimeMillis().

>测试也忽略了Mockito的工作方式.

Mockito将每个调用存储在自己的模型中,以便能够在执行场景后验证这些调用.因此,在循环的每次迭代中,Mockito都会存储另一个最多2M调用的调用,这将影响JVM(也许模拟实例将持有几代并升级到终端,这对于GC来说肯定更昂贵).这意味着迭代次数越多,此代码就越强调JVM而不是Mockito.

我相信它没有发布(然而在jcentral上有dev二进制文件),但Mockito将提供一个设置,允许mockito只存储因此它不会存储调用,这可能允许Mockito适合这样的场景.
>测试缺乏适当的统计分析.

有趣的是,测试代码具有伪百分位方法.哪个好!虽然它不能像那样工作,但在这种情况下它无法解决大问题.相反,它应该记录每个度量,以便提取随着迭代计数的推进而模拟时间的演变趋势.

如果您愿意,最好存储每个记录的度量,因此可以将它们提供给适当的统计分析工具,如R,以便提取图表,百分位数据等.

在统计问题上,使用HDRHistogram肯定会很有趣.当然,在微基准测试之外它会影响内存并改变微基准测试的结果.让我们为JMH保留.

如果更改代码以使用JMH,则可以解决第1点和第2点.

希望有所帮助.

上一篇:java – 重构以编写“漂亮”的JUnit测试


下一篇:android – Mockito runnable:想要但是没有被调用?