S4パフォーマンス向上作戦

最低限の実装は行ったので次はパフォーマンスを実用レベルにするが目標です。今日はパフォーマンスを上げるために幾つかテストしてみたのでそれを書いてみようと思います。
パフォーマンスアップの方針としては、画像を分割して変更のあったパーツのみを配信するような方針です。

パフォーマンス問題

正直なところ自分はあまりパフォーマンスを意識したプログラミングは行いませんし、ほとんど行ってきていません。分野としてはWebアプリ、しかも業務アプリが多かったため、パフォーマンスよりもどれだけ仕様変更に耐えうるかを意識した設計・コーディングを行ってきたからです。
これまでのプロジェクトでパフォーマンスの問題が発生したのは1回のみです。その時は、聞いていた想定データが1万件ということで10万件のレコードに対し集計が3〜4秒の速度と「まあ十分でしょう」という形で結合テストに入ったのですが、その時想定データが1,000万件以上という事を知らされました。サンプルデータの投入だけで1週間とかかかったのですが、当然それらの統計SQLが返ってくるはずがありません。こうなってくると設計自体がおかしいのでパフォーマンス問題とはいいませんが…
そんな自分なので久しぶりにパフォーマンスを意識したプログラミングをやる事になりました。まずは方針を立て、幾つかの測定値を元に実装していくことにします。

分割してスクリーンショットを撮る

概ね800x600〜1024x768程度の領域のスクリーンショットをRobot#createScreenCaptureで作るわけですが、分割した場合何度も取得する必要があります。これはパフォーマンスにどう影響するのかを確認してみました。

    public static void main(String[] args) throws AWTException {
        List<Long> results = new ArrayList<Long>();
        results.add(capture(1024, 768, 1));
        results.add(capture(1024, 768, 2));
        results.add(capture(1024, 768, 3));
        results.add(capture(1024, 768, 4));
        results.add(capture(1024, 768, 5));
        results.add(capture(1024, 768, 10));
        results.add(capture(1024, 768, 20));
        for (Long time : results) {
            System.out.println(time / 100);
        }
    }

    static long capture(int width, int height, int divided) throws AWTException {
        Robot robot = new Robot();
        int w = width / divided;
        int h = height / divided;
        Rectangle[] rects = new Rectangle[divided * divided];
        for (int i = 0; i < divided; i++) {
            for (int j = 0; j < divided; j++) {
                rects[i * divided + j] = new Rectangle(w * i, h * i, w, h);
            }
        }
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100; i++) {
            for (int j = 0; j < rects.length; j++) {
                robot.createScreenCapture(rects[j]);
            }
        }
        return System.currentTimeMillis() - start;
    }

結果

25
11
10
12
13
23
58

意外なことに大きな領域を確保するよりも細かく撮る方がパフォーマンスが良いという結果です。とはいえ、100分割以上してしまうとむしろ遅くなり、4〜60分割程度する方が良いだろうという結果になりました。
おそらくはキャプチャした後にイメージが作られるわけですが、内部で持つバイト配列の大きさに関係するのではと思われます。適度な粒度のバイト配列の方がGCもかかりやすいのかもしれません。

BufferedImageの比較

今回のパフォーマンスアップの方針は、キャプチャしたBufferedImageが前回のものと一致している場合、画像を転送せず、Appletでも更新しないという処理です。しかし、画像の比較に時間がかかりすぎてアップロードできなくなるのでは意味がありません。分割をしていった場合にどの程度のパフォーマンスになるかを測定しました。

    public static void main(String[] args) throws AWTException {

        System.out.println(compare(1));
        System.out.println(compare(2));
        System.out.println(compare(3));
        System.out.println(compare(4));
        System.out.println(compare(5));
        System.out.println(compare(10));
        System.out.println(compare(20));
    }

    static long compare(int divied) throws AWTException {
        Robot robot = new Robot();
        BufferedImage img1 = robot.createScreenCapture(new Rectangle(1024 / divied, 768 / divied));
        BufferedImage img2 = robot.createScreenCapture(new Rectangle(1024 / divied, 768 / divied));
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100; i++) {
            for (int j = 0; j < divied * divied; j++) {
                equals(img1, img2);
            }
        }
        return System.currentTimeMillis() - start;
    }

    static boolean equals(BufferedImage img1, BufferedImage img2) {
        int[] data1 = ((DataBufferInt) img1.getRaster().getDataBuffer()).getData();
        int[] data2 = ((DataBufferInt) img2.getRaster().getDataBuffer()).getData();
        return Arrays.equals(data1, data2);
    }

結果。

203
172
172
172
172
172
172

はい、これもほとんど変わりません。大きな配列を使う方がむしろ遅く、分割自体はそれほど影響はないと考えていいでしょう。結果的に同じ領域をキャプチャしているので、比較する量(バイト数)には差はなく、誤差の範囲なんだろうと推測できます。

マルチスレッドで処理したら早くなる?

最後にせっかくキャプチャが分割されるわけで、スレッドをわけて処理してみればぐっと早くなるかも?と思いテストしてみました。自分の環境は4coreです。

   public static void main(String[] args) throws AWTException, InterruptedException, ExecutionException {
        Robot robot = new Robot();
        Rectangle rect = new Rectangle(0, 0, 1024, 768 / 16);
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100; i++) {
            for (int j = 0; j < 16; j++) {
                robot.createScreenCapture(rect);
            }
        }
        System.out.println(System.currentTimeMillis() - start);
        doMultiThred(1);
        doMultiThred(2);
        doMultiThred(3);
        doMultiThred(4);
        doMultiThred(8);
        doMultiThred(16);
    }

    static void doMultiThred(int num) throws AWTException, InterruptedException, ExecutionException {
        List<Callable<BufferedImage>> tasks = new ArrayList<Callable<BufferedImage>>();
        for (int j = 0; j < 16; j++) {
            tasks.add(new Callable<BufferedImage>() {
                Robot robot = new Robot();
                Rectangle rect = new Rectangle(0, 0, 1024, 768 / 16);

                @Override
                public BufferedImage call() throws Exception {
                    return robot.createScreenCapture(rect);
                }
            });
        }
        ExecutorService service = Executors.newFixedThreadPool(num);
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100; i++) {
            List<Future<BufferedImage>> futures = service.invokeAll(tasks);
            for (Future<BufferedImage> future : futures) {
                future.get();
            }
        }
        System.out.println(System.currentTimeMillis() - start);

    }

結果。

1672
2719
2625
2562
2672
2688
2687

並列処理でむしろ遅くなりました。これはキャプチャ処理自体が一瞬であり、スレッドを使う時のオーバーヘッドの方が大きいのが理由です。特に実行結果を待つfuture.get()は大きなロスになっているのかと思います。また、Robot#createScreenCaptureはマルチスレッドで使用しても、OSレベルやVMレベルで並列処理できないのかもしれません。結局は1枚撮るまで後続が待つため、順次処理する方が一番効率がいいのかもしれません。

というわけで16枚にスライスする

というわけで、実装方針は固まりました。

  • とりあえず16枚にスライス
  • 16枚を1ブロックでサーバに転送
  • ただし前回と同じ画像の場合は転送しない(0バイトと送る)
  • サーバからAppletにも同じ画像は送らない
  • Appletでは前回から変更のあったエリアのみを再描画する

実装後

かなり改善して転送量は激減しました。少なくとも画面に変更がない限りは転送されません*1
現時点では光回線であれば3FPS程度でなめらかに動きます。ゲーム画面などもなんとなく雰囲気が伝わる程度には配信可能です。
とはいえ、イーモバでの配信ですと1FPSでもまだまだ厳しいところです。アップロードの速度も安定しないので表示がカクカクしますし、ロストも多いようです。

協力者募集!

実はこの辺りになってくるとあまり得意な分野ではありません。どなたかアイディアとご協力をいただけると非常に助かります。興味があればTwitterなどでお声かけてください。ソース一式はプロジェクトサイトに全部ありますのでご自由にどうぞ。

*1:Appletが途中から画像を取得したときに表示する画像がダウンロードできないなどバグも多く残っていますがw