続・JUnit4でテストクラスの並列実行(Concurrent Util版)

さっき書いたJUnit4でテストクラスの並列実行(Concurrent Util版) - royzumiの日記ですが、id:cactusmanさんの

それぞれのテストクラス内のメソッドは逐次実行されるため、並列化したいところはクラスに分けないといけません。
この辺もなんとかしたいところですね。

をみて、なんとかしてみました。

package org.cactusman;

import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.junit.runner.Runner;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.Suite;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.RunnerBuilder;
import org.junit.runners.model.Statement;

public class ParallelSuite extends Suite implements Work {

    private ExecutorService _es = new ThreadPoolExecutor(0, 
            Runtime.getRuntime().availableProcessors() * 5, 60L,
            TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
    
    public ParallelSuite(Class<?> klass, RunnerBuilder builder) throws InitializationError {
        super(new ParallelBuilder(), klass, getAnnotatedClasses(klass));
    }

    private static Class<?>[] getAnnotatedClasses(Class<?> klass) throws InitializationError {
        SuiteClasses annotation= klass.getAnnotation(SuiteClasses.class);
        if (annotation == null)
            throw new InitializationError(String.format("class '%s' must have a SuiteClasses annotation", klass.getName()));
        return annotation.value();
    }

    @Override
    protected Statement childrenInvoker(final RunNotifier notifier) {
        return new Statement() {
            @Override
            public void evaluate() {
                new CompletionServiceUtil(getChildren(), _es).execute(notifier, ParallelSuite.this);
            }
        };
    }

    @Override
    public void execute(Object obj, RunNotifier notifier) {
        runChild((Runner)obj, notifier);
    }
}

class ParallelBuilder extends RunnerBuilder {

    @Override
    public Runner runnerForClass(Class<?> testClass) throws Throwable {
        return new ParallelJUnit4ClassRunner(testClass);
    }
}

class ParallelJUnit4ClassRunner extends BlockJUnit4ClassRunner implements Work {
    private ExecutorService _es = new ThreadPoolExecutor(0, 
            Runtime.getRuntime().availableProcessors() * 5, 60L,
            TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
    ParallelJUnit4ClassRunner(Class<?> klass) throws InitializationError {
        super(klass);
    }

    @Override
    protected Statement childrenInvoker(final RunNotifier notifier) {
        return new Statement() {
            @Override
            public void evaluate() {
                new CompletionServiceUtil(getFilteredChildren(), _es).execute(notifier, ParallelJUnit4ClassRunner.this);
            }
        };
    }

    @Override
    public void execute(Object obj, RunNotifier notifier) {
        runChild((FrameworkMethod)obj, notifier);
    }
}

interface Work {
    void execute(Object obj, RunNotifier notifier);
}

class CompletionServiceUtil {
    private final List<Object> _list;
    private final ExecutorService _es;
    CompletionServiceUtil(List<?> list, ExecutorService es) {
        _list = (List<Object>)list;
        _es = es;
    }

    void execute(final RunNotifier notifier, final Work run) {
        CompletionService<Object> completionService = new ExecutorCompletionService<Object>(_es);
        for (final Object each : _list) {
            completionService.submit(new Callable<Object>() {
                @Override
                public Object call() {
                    run.execute(each,notifier);
                    return null;
                }
            });
        }
        int n = _list.size();
        for (int i = 0; i < n; i++) {
            try {
                completionService.take().get();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

なるべくきれいに書いたつもりだけど、インナークラスが多いので、見にくいですねやっぱり。でも、しょうがないことにする。
工夫した点は、前の実装だとクラスが多いとあっという間にスレッドが沢山生成されてしまう(っと思う。試していないけど)。このため、スレッド数の上限をプロセッサ×5になるようにした(5に根拠はない)。
きっと動くとは思うんだけど、手元に大きなテストがないので誰か試して貰えないでしょうか?

追記:クラスとメソッドの並列実行は分けるコメントもらったのでそのようにしました。

JUnit4でテストクラスの並列実行(Concurrent Util版)

id:cactusmanさんの下記のblogがとても良いと思いました。

最近、スローテスト問題というのが深刻になっています。
JUnitは基本的に逐次実行されるため、高性能なPCでも待たされる処理があるとどうしても時間がかかってしまいます。
JUnit3では川口さんが作成した「Parallel Junit」があるので並列実行することができるのですが、JUnit4対応はされていません。
なので、JUnit4で何かないかと調べてみましたが、使えそうなものはなさそうでした。
なければ作るというわけで、とりあえずSuiteクラスを継承して実装してみました。

で、

この辺はConcurrency Utilを使ったほうがいいですね。
それはおいおいと。

っとあり、今日は時間があったので、Concurrent Utilを使用したものを実装してみました。

package org.cactusman;

import java.util.concurrent.Callable;
import java.util.concurrent.CompletionService;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

import org.junit.runner.Runner;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.Suite;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.RunnerBuilder;
import org.junit.runners.model.Statement;

public class ParallelSuite extends Suite{

    public ParallelSuite(Class<?> klass, RunnerBuilder builder) throws InitializationError {
        super(builder, klass, getAnnotatedClasses(klass));
    }

    private static Class<?>[] getAnnotatedClasses(Class<?> klass) throws InitializationError {
        SuiteClasses annotation= klass.getAnnotation(SuiteClasses.class);
        if (annotation == null)
            throw new InitializationError(String.format("class '%s' must have a SuiteClasses annotation", klass.getName()));
        return annotation.value();
    }

    protected Statement childrenInvoker(final RunNotifier notifier) {
        return new Statement() {
            @Override
            public void evaluate() {
                runChildren(notifier);
            }
        };
    }

    private void runChildren(final RunNotifier notifier) {
        ExecutorService es = Executors.newCachedThreadPool();
        CompletionService<Object> completionService =
            new ExecutorCompletionService<Object>(es);
        for (final Runner each : getChildren()) {
            completionService.submit(new Callable<Object>(){
                @Override
                public Object call() {
                    runChild(each, notifier);
                    return null;
                }
            });
        }
        int n = getChildren().size();
        try {
            for (int i = 0; i < n; i++) {
                try {
                    completionService.take().get();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        } finally {
            es.shutdown();
        }
    }
}

いや〜、仕事絡まない実装は楽しいですね。

追記:shutdownを忘れてた。

java.lang.Comparableもありますよ

Javaで文字列ソートを行う方法

  • Collections.sortを使う
  • TreeSetを使う
  • Arrays.sort + Comparatorを使う

http://d.hatena.ne.jp/bluerabbit/20090617

Comparableを使用する方法もあるので紹介します。
あおうささんの日記のJoSQLを使用したサンプルを少し改造しました。

package sample;
public class Bean implements Comparable<Bean> {
  private String value;
  public String getValue() {
    return value;
  }
  public void setValue(String value) {
    this.value = value;
  }
  @Override
  public int compareTo(Bean bean) {
    return value.compareTo(bean.value);
  }
  @Override
  public String toString() {
	  return value;
  }
}

実行はこちら

public static void main(String[] args) {
  Set<Bean> set = new TreeSet<Bean>();
  Bean bean1 = new Bean();
  bean1.setValue("x");
  Bean bean2 = new Bean();
  bean2.setValue("z");
  Bean bean3 = new Bean();
  bean3.setValue("1");
  Bean bean4 = new Bean();
  bean4.setValue("2");
  set.add(bean1);
  set.add(bean2);
  set.add(bean3);
  set.add(bean4);
  System.out.println(set);
}

Comparableの場合、Beanのようなクラスに対して比較処理を書くことができるので、TreeSetなどをそのまましよう可能です。Comparatorとの違いは、比較のロジックを外部に持つ(Comparator)のか、内部つまりクラス自身に持つ(Comparable)もつのかの違いです。
あと、ここでは実装しませんでしたが、Comparableを実装するような場合は、equalsやhasocodeなどもオーバーライドした方がよいのだと思います。

これからの開発のこと

久しぶりに書いてみる。とりとめもなく、まとまっていないけど。

昔のCPUは1コアで、周波数を上げる方向に改良させていた。最近はこの周波数を上げると言うことが限界になってきたから、コアを沢山のせて速度の改善をしている。

でも、家のPCではQuadコアを使用しているけど、あまり早さを感じない。重いアプリケーションを使用しているときのタスクマネージャーを見ると、1つのコアの使用率は100%だけど、他のコアはほとんど使用していないと言う状態。希望としては全てのコアが使用され、使用率が80%位の状態。
現状ではコアが増えても、それを使い切れていない状態。コアが増えても意味がないんだな。

OS(アプリケーション?)が、もっとマルチコアを生かすようにならないといけないな。
Windowsではだめだったけど、Linuxなどではうまくいくのかな?MacにのせたEclipseがCPUをがんがん使ってとても快適と聞いたことがある。本当なのかな?

OSレベルの話は手がでないけど、アプリケーションレベルでこれらを改善する方法を考えていかないといけないんじゃないかな?これからの開発者は。

1プロセスが1コアに限定されてしまうアーキテクチャであれば、この問題は解決できない。でも、1プロセスで複数のスレッドを使用しているアプリケーションは多いのだから、このスレッドをうまくコアに分散させればいいと思うんだけど。そういうことは実際やられていると考えるんだけど(実際にどうかは知らないけど、すごい人たちが開発しているんだからきっとできているはず・・・)。

こう考えてくると、これからの開発者は、マルチスレッドプログラミングを当たり前に使用する必要が生じるのかな。
Scalaが注目されているのも関係有るのかな。

追伸:APサーバとかでコアが増えたから、インスタンスを複数立ち上げた方が性能が良かったと言う結果があるけど、これはCPUがうまくさばけていないからなのか?まだOSは対応していないのかな?

BOOK-OFFっていいんじゃね?

小遣いはアルコールとなって蒸発してしまうので、書籍購入になかなかお金をかけられない(タクシー代か?)。こんな私の友達は図書館なのですが、良書はやはり手元において読み返したいもの。
かと言って、新品で買うとビールが買えない・・・
そんなときは!
Amazonの中古商品サイトですよね。私も何度か使用しています。でも、ここってまとめて買っても配送料は別途かかるんですよね(だって、書籍を販売してくれる人が一緒になることはほとんどないから・・・)。
そ・こ・で!
BOOK-OFFです。
でも、家の近くの店舗がどこにあるかわからない・・・っと思ってググってみると、なんと、BOOF-OFF Onlineなるものがあるではないですか!
http://www.bookoffonline.co.jp/top/CSfTop.jsp

まだ使用していないけど、BOOK-OFFからの配送だから、複数の本で合計が1500円以上になっても配送料無料でいけるんですよね?

もうしばらくしたら、買ってみようと思っている。