2011年9月11日日曜日

Gradleでスローテスト問題を解決するAgain

前回、こんな記事書きました。
見事に効果がわからないという結果が出てきて、どうやればいいのか考えていましたが、
さすがGroovyクラスタに素晴らしい先輩がいらっしゃいました。

@bluepapa32 先輩です。
単純に、スリープするコード


    for (int i = 0; i < 100; i++ ) {
        Thread.sleep(10);
    }


を埋め込めばよかったわけですね。

というわけで、こんなテスト生成スクリプトでテストを大量に作ってみることにしました。


CreateTest.groovy

import static groovyx.gpars.GParsPool.*;
def packagePath = 'C:/Users/mike/IDEA_Project/GradleSample/src/test/java/orz/mikeneck/gradle/sample/boxunbox/test'

def head = $/
package orz.mikeneck.gradle.sample.boxunbox.test;
import org.junit.Before;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
/$

def body = $/
    public static final int SIZE = 100;
    private List<Integer> intList;
    private List<Long> longList;
    @Test
    public void testInteger()   throws InterruptedException {
        int[] array = new int[SIZE];
        int position = 0;
        for(Integer item : intList) {
            array[position++] = item;
            Thread.sleep(10);
        }
        for (int i : array)
            assertThat(i, is(intList.get(i)));
    }
    @Test
    public void testLong()  throws InterruptedException {
        long[] array = new long[SIZE];
        int position = 0;
        for (Long item : longList) {
            array[position++] = item;
            Thread.sleep(10);
        }
        position = 0;
        for(long item : array)
            assertThat(item, is(longList.get(position++)));
    }
    @Before
    public void setUp() throws Exception {
        Integer[] integers = new Integer[SIZE];
        Long[] longs = new Long[SIZE];
        for(int i = 0; i < SIZE; i++)
            integers[i] = new Integer(i);
        intList = Arrays.asList(integers);
        for (int i = 0; i < SIZE; i++)
            longs[i] = new Long(i + Integer.MAX_VALUE);
        longList = Arrays.asList(longs);
    }
}
/$

def numbers = []
(1..300).each {
    numbers << it
}

withPool {
    numbers.collectParallel { number ->
        def className = "BoxUnboxTest${number}"
        def name = "${className}.java"
        def fileName = "${packagePath}/${name}"
        def define = "public class ${className} {"
        def content = new StringWriter()
        content << head
        content << define
        content << body
        println ' ---- '
        println "now processing : $fileName"
        println ' ---- '
        new File(fileName).write(content.toString(), 'UTF-8')
        assert new File(fileName).exists() == true
    }
}


10msecを100回Sleepするテストメソッドが二つで、ひとつのテストの実行にかかる時間は2sec。
これが300個あるので、必要な時間は600sec = 10min かかるテストです。

実際に一つテストを実施してみました。


2secかかっていますね。
これが300個用意されるので、10minかかることが想定されます。

では、次のGradleスクリプトで実行してみましょう。


build.gradle

apply plugin: 'java'

repositories {
    mavenCentral()
}

dependencies {
    testCompile 'junit:junit:4.8.2'
}



なお、マシンの環境は次のような感じです。
OS : Windows 7
CPU : Intel Xeon X3460 (8Core) (64bit)
RAM : 8.00GB




では、実行、というかその間に色々と他のことで負荷がかからないように、
IntelliJ IDEAが起動していて、このブログだけがChrome上で起動しているという状態にしておきます。
で、IntelliJのRunツールで実行するのではなく、ふつうにコマンドから起動します。

さて、オレはその間、『JOJOの奇妙な冒険』を読むこととします。

では、いざ実行…




c:\Users\mike\IdeaProjects\TestingGradle>gradle build
:buildSrc:compileJava UP-TO-DATE
:buildSrc:compileGroovy UP-TO-DATE
:buildSrc:processResources UP-TO-DATE
:buildSrc:classes UP-TO-DATE
:buildSrc:jar UP-TO-DATE
:buildSrc:assemble UP-TO-DATE
:buildSrc:compileTestJava UP-TO-DATE
:buildSrc:compileTestGroovy UP-TO-DATE
:buildSrc:processTestResources UP-TO-DATE
:buildSrc:testClasses UP-TO-DATE
:buildSrc:test UP-TO-DATE
:buildSrc:check UP-TO-DATE
:buildSrc:build UP-TO-DATE
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:jar UP-TO-DATE
:assemble UP-TO-DATE
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses
:test
:check
:build

BUILD SUCCESSFUL

Total time: 15 mins 42.983 secs


15分43秒、マジ遅え。
しかも、最初の数個目までのテストは結構サクサク進んでいたけど、200を越えたあたりからテストの実行速度が落ちてきた感じがする。
こういう時はJVMの再起動などが必要なのかもしれん。

というわけで、さっそく、並列処理を試してみようと思う。

build.gradle

apply plugin: 'java'

repositories {
    mavenCentral()
}

dependencies {
    testCompile 'junit:junit:4.8.2'
}

test {
    maxParallelForks = 6
    forkEvery = 30
}



前回にも書きましたが、
  • maxParallelForksは並列するスレッド数。
  • forkEveryは指定した回数テストを実行するとJVMを再起動して、OutOfMemoryExceptionを回避する仕組みです。

では、実行開始です。


c:\Users\mike\IdeaProjects\TestingGradle>gradle build
:buildSrc:compileJava UP-TO-DATE
:buildSrc:compileGroovy UP-TO-DATE
:buildSrc:processResources UP-TO-DATE
:buildSrc:classes UP-TO-DATE
:buildSrc:jar UP-TO-DATE
:buildSrc:assemble UP-TO-DATE
:buildSrc:compileTestJava UP-TO-DATE
:buildSrc:compileTestGroovy UP-TO-DATE
:buildSrc:processTestResources UP-TO-DATE
:buildSrc:testClasses UP-TO-DATE
:buildSrc:test UP-TO-DATE
:buildSrc:check UP-TO-DATE
:buildSrc:build UP-TO-DATE
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test
:check
:build

BUILD SUCCESSFUL

Total time: 2 mins 42.544 secs




えっ、2分Σ(゚д゚lll



めちゃくちゃ早くなっていません?
元々15分43秒かかっていたのがたったの2分43秒。

単純に計算すると

15min43sec / 6(core) = 2min37sec


となるから、妥当な値ですね。

では、テストのサマリーを比較してみましょう。



並列化前



並列化後



これを見るとCPU時間は変わっていません。
つまり、CPU時間をすべて複数コアで実行したために早くすることが出来たという事になります。

というわけで、結論

GradlemaxParallelForksを使うともれなくスローテスト問題が解決できる。



0 件のコメント:

コメントを投稿