目次


リッチ・クライアント・アプリケーションのパフォーマンス

第 1 回 パフォーマンス分析のツール、手法、そしてヒント

Comments

コンテンツシリーズ

このコンテンツは全#シリーズのパート#です: リッチ・クライアント・アプリケーションのパフォーマンス

このシリーズの続きに乞うご期待。

このコンテンツはシリーズの一部分です:リッチ・クライアント・アプリケーションのパフォーマンス

このシリーズの続きに乞うご期待。

事前に十分計画されたアプリケーションでさえも、重大なパフォーマンス問題が発生することは珍しくありません。この 2 回の連載記事では、Windows で動作する Eclipse ベースのリッチ・クライアント・アプリケーションに重点を置いて、パフォーマンス問題を分析する手法をいくつか紹介します。第 1 回で説明するのは、Eclipse ベースの RCP (Rich Client Platform) アプリケーションのパフォーマンスを測定して、速度低下のボトルネックとなっている原因が CPU または I/O のどちらにあるのかを判断する方法、そして UI スレッドをアイドル状態にして応答性を維持する方法です。また、誤ったスレッド化を避け、アプリケーション起動時のパフォーマンスを改善するためのヒントも紹介します。続く第 2 回では、メモリー問題を突き止める方法を紹介します。この連載で紹介する手法の多くは、Eclipse アプリケーションに限らず適用することができます。

重要なコンセプト

パフォーマンス問題を調査する際に必ず最初に片付けなければならない課題は、問題になっているタスクが主に CPU バウンドなのか、あるいは I/O バウンドなのかを判断することです。

CPU バウンドということは、CPU がジョブの完了を妨げるボトルネックになっているということです。つまり、CPU が速ければ速いほどアクションの所要時間が短縮されます。例えば、100MHz の CPU で 100,000 件の E メールの一覧をソートするのにかかる時間が 50 秒だとすると、1GHz の CPU では 5 秒に短縮できるはずです。

ただし、CPU が速くなれば必ずタスクの動作が速くなるとは限りません。I/O バウンドのタスクでは、I/O 処理がボトルネックとなります。そのよい例が、大きなサイズのファイルをディスクから読み取ったり、Web サイトからファイルをダウンロードするような場合です。ファイルの読み取りを扱うのは I/O サブシステムなので、I/O バウンドのタスクには一般に CPU の処理速度は関係しません。送信元のデバイスは CPU をビジー状態にしておくだけの転送速度を維持できないのが常です。データを待っている間、CPU は何もすることがないためスリープ状態に入ります。

つまり速度低下の原因には、CPU がビジー状態であること、アプリケーションが過剰な I/O を実行していること、アプリケーションが I/O の完了を待機中であること、あるいはそれらの組み合わせなどが考えられます。しかし、原因を推測してみたところでほとんど意味はありません。原因の究明にはツールのほうが長けているからです。次のセクションでは、タスクが CPU バウンドまたは I/O バウンドのどちらであるかを判別するのに役立つツールをいくつか紹介します。

Windows 対応のモニタリング・ツール

Windows オペレーティング・システムの場合、私は通常、Performance Monitor (Perfmon) と Sysinternals Process Explorer を組み合わせて使用します (「参考文献」を参照)。Process Explorer は Windows に組み込まれているタスク マネージャの代わりとして使うことができるものなので、私はタスクマネージャをめったに使用しません。Process Explorer の使用に際しては、すべてのモニタリング・ツールの場合と同じく、慎重さが求められます (「アプリケーション・モニタリングの負荷」を参照)。Process Explorer のビュー更新間隔を短くしすぎると、マシンへの負荷が高くなりますので注意してください。

私がツールの選択基準としているのは、パフォーマンス分析を臨時的に行うのか、あるいは長期的に行うのかということです。アプリケーションを長期的にモニタリングしたり、パフォーマンス・データをアプリケーション・ログに相関させる必要がある場合は通常、Perfmon を使います。Perfmon では、他のログと相関できるようにするために、カンマ区切り (CSV) ファイルにタイム・スタンプ付きでログを記録するようなセットアップを簡単に行うことができるからです。

Perfmon を起動する手順は以下のとおりです。

  1. Start > Run > perfmon の順にクリックします。
  2. Perfmon の+ ボタンをクリックして Add Counters ダイアログを起動します (図 1を参照)。
  3. 各カウンターの詳細説明を参照するには、Explain ボタンをクリックします。
図 1. Perfmon の Add Counters ダイアログ
Perfmon の Add Counters ダイアログ
Perfmon の Add Counters ダイアログ

私は通常、Performance object には Process を選択し、以下のカウンターを追加します。

  • % User Time: プロセスが処理中の作業量。
  • Handle count: プロセスによってオープンされたハンドル数。数あるカウンターの中で、ハンドル数はアプリケーションがオープンしたファイルまたはソケットの数を表しています。
  • IO Data Bytes/sec:プロセスが行っているディスク I/O、ネットワーク I/O、またはデバイス I/O の量。
  • Private Bytes: プロセスに関連付けられた共有不可能なメモリー量。アプリケーションのおおよそのサイズがわかります (この値はタスク マネージャの VM Size に相当します)。
  • Thread Count: プロセスに関連付けられたスレッドの数。

一方、再現可能な問題を「リアルタイム」で観察したい場合には、Sysinternals Process Explorer を使用します。Process Explorer の利点は、マシン全体ではなく単一のプロセスに焦点を絞れることです。特定の問題を調べるときには、該当するアプリケーションだけを調べるほうが有効な場合も多くあります。

Process Explorer 内で、モニタリングを行いたいアプリケーションをダブルクリックすると、そのプロセスの Properties ダイアログが開きます (図 2を参照)。

図 2. javaw プロセスのプロパティー
javaw プロセスのプロパティー
javaw プロセスのプロパティー

図 2 の javaw プロセスは、JEdit のものです。この例では、ディスクから 14MB のテキスト・ファイルを開いています。図 2 の 3 つのグラフからわかる内容は以下のとおりです (下のグラフから順に説明)。

  • I/O Bytes History グラフに現れている鋭い山形は、14MB のファイルを読み取るための所要ディスク I/O を表します。線を辿ると、14MB 読み取られたことがわかります。
  • Private Bytes は 33MB 急激に増加しています。14MB のテキスト・ファイルは Java ヒープでは 28MB になりますが、これは主に、Java 言語が 16 ビットの Unicode 文字を使用するためです。残りの 5MB は、Swing オブジェクトと JEdit オブジェクトが編集を管理するために必要なバイト数です。
  • CPU Usage History の大きな山形は、ファイルがメモリーに読み取られた後に行われた処理を示します。この例では、JEdit による表示の更新、ファイルの構文強調表示などの処理に該当します。

動作の遅さが I/O バウンドによる場合は、アプリケーションのどの部分が I/O に関連しているのかを判断してください。一方、CPU バウンドによる場合は、プロファイラーが登場する番です。

プロファイラーのセットアップ

RCP アプリケーション用プロファイラーのセットアップは、他の多くのタイプのアプリケーションを対象としたセットアップとは異なります。なぜなら、RCP アプリケーションの起動には、Java ランタイムを直接起動する方法ではなく、大抵は実行可能プログラムまたはシェル・スクリプトを使用した起動方法がとられるからです。また RCP ランチャーが Java プロセッサーのコマンド・ラインの引数を作成して、Java プロセッサーを起動するため、事はさらにややこしくなります。このような間接的な手段がとられていることが、プロファイルを作成したり、あるいは JVM 呼び出し引数を正確に制御する上での障害となる場合があります。そこで私がよく使うのは、アプリケーションのランチャーに頼って Java ランタイムを起動する代わりに、Java コマンド・ラインを抽出して直接起動させるという方法です。以下は、その手順の一例です。

  1. 通常の方法でアプリケーションを起動します。
  2. アプリケーションが立ち上がったら、Process Explorer を起動して javaw または Java プロセスを見つけます。プロセスのプロパティーを開き、表示されている詳細情報の中からコマンド・ラインの引数をコピーします (図 3を参照)。
  3. このコマンド・ラインの引数をバッチ・ファイルに貼り付け、必要に応じて変更します (このようにして、コアとなるバッチ・ファイル、そして VM 引数、クラスパス・エントリーなどを追加または削除したさまざまなバージョンを作成します)。
図 3. Java プロセスのコマンド・ラインの引数
Java プロセスのコマンド・ラインの引数
Java プロセスのコマンド・ラインの引数

UI スレッドで長時間実行されるアクションの特定

最近では、ほとんどのオペレーティング・システムの UI スレッドは 1 つです。同様に、SWT (Standard Widget Toolkit) の UI スレッドも 1 つです。そのため、この単一スレッドからは、過剰なディスク I/O やネットワーク呼び出しなどの長時間実行される操作を行ったり、あるいは旧来のごく普通の作業を数多く実行したりしないように注意しなければなりません。

その理由を知る手段として、ボタンのあるアプリケーションを想像してください。ボタンのクリックによって何らかの作業が行われるようにするためは、このボタンにイベント・ハンドラーを追加します。ユーザーがボタンをクリックすると OS が GUI ツールキットを呼び出し、このツールキットがイベント・ハンドラーを呼び出すという仕組みです。こうしてイベント・ハンドラーのコードは UI スレッドで実行されるようになりますが、このコードが実行されている間、UI スレッドは他の UI イベントに応答することができません。つまり UI がフリーズしているように見え、ユーザーを当惑させることになります。ここでの要点は、コードが UI スレッドで実行されていると、アプリケーションは OS からの UI イベントを処理できないということです。長時間実行される操作をキャンセルするためのボタンがアプリケーションにあったとしても、UI スレッドで作業を行っている場合、その操作が完了するまではキャンセル・イベントは発生しません (ただし、UI スレッドでのコードの実行が長引くと、OS が割り込み、ユーザーに通知してアプリケーションを強制終了するオプションを提示します)。

このような理由から、時間がかかり、処理完了までの時間が予測不可能な I/O は UI スレッドでは問題になります。I/O のタイプによってその特性は大きく異なります。たとえば、ディスク I/O は傾向として、待ち時間+転送速度×データ量という線形モデルに従います。それに比べ、ネットワーク I/O にはあまり規則性がありません。ネットワーク I/O はエンドポイント間の (おそらく一時的な) ネットワーク輻輳に影響されるため、ディスク I/O よりも時間がかかると同時に、信頼性も劣ります。

大抵は高速で遅延が少ないネットワークが使用されることになると思うので、UI スレッドにおけるネットワーク I/O の影響は開発段階では見過ごされがちです。そのような環境では、UI スレッドで不用意にネットワーク呼び出しが行われてしまっていても、速度または信頼性の劣ったネットワークを使用している顧客が、毎回ネットワークに接続するたびに UI がフリーズすることに気付くまで、事態が発覚しないということにもなりかねません。これにソケット・タイムアウトが重なったりしようものなら、「死のホワイト・スクリーン」 (アプリケーションが UI イベントに対し 5 秒間以上応答しない場合に Windows で表示される画面) に遭遇する可能性も十分あるのです。

表 1 に、UI スレッドで長時間実行される操作を見つける手法をその利点と欠点と併せて記載します。

表 1. UI スレッドで長時間実行される操作の検出手法
手法利点欠点
プロファイラーの使用既存のものがある場合、セットアップに手間がかかりません。通常は費用がかかります。
実行時のオーバーヘッドが高くなりすぎる場合があります。
JDK のインスツルメンテーション一度セットアップしてしまえば、JDK をアップグレードするまでアプリケーションで有効に機能します。
実行時のオーバーヘッドがわずかで済みます。
他人と共有する場合は厄介です。
コードのインスツルメンテーションカスタマー、QA、開発者などがこのインスツルメンテーションを有効にして実行できるので、多数のユーザーに拡張できます。ネットワーク呼び出しを行う場所をすべて見つけるには、アプリケーションの再設計が必要になる可能性があります。
インスツルメンテーションを行っていない新しいメソッドを追加しないよう徹底しなければなりません。
ログの事後処理が必要です。ログ・ファイルが大きくなる可能性があります。

インスツルメンテーションの手法

アプリケーションの実行内容を詳細に調べるには、さまざまな手法があります。このセクションでは、そのうちの一部を紹介します。

アスペクトの使用

インスツルメンテーション対象のクラスに変更を「織り込む」には、アスペクト指向の手法を使用することができます。例えば、SocketInputStreamSocketOutputStream に対し、ストリームが UI スレッドでアクセスされているかどうかを調べるコードを織り込むのは簡単なことです (アスペクト指向の手法とツールについての詳細は、「参考文献」のリンクを参照)。

ブレークポイントの使用

デバッガーでアプリケーションを実行できる場合、条件付きブレークポイントを用いると JDK のインスツルメンテーションが簡単になることがあります。以前に私が取り組んだ大きなアプリケーションは、UI スレッドでネットワーク呼び出しを行っていましたが、アプリケーションの構造 (サード・パーティーのコードを多く含む) のせいで、誰がネットワーク呼び出しに関与しているのかを特定しにくくしていました。図 4 のように、Eclipse で SocketInputStream クラスに条件付きブレークポイントを設定することで、違反者を特定するのが簡単になりました。

図 4. 条件付きブレークポイント
条件付きブレークポイント
条件付きブレークポイント

セキュリティー・マネージャーの使用

アプリケーションのセキュリティー・マネージャーをインスツルメンテーション化したセキュリティー・マネージャーに置き換えるというのも、私が使用して有効だった選択肢のひとつです。注目に値する呼び出しが数多くセキュリティー・マネージャーを通過します。例えばリスト 1 のセキュリティー・マネージャーは、GUI スレッドでソケットをオープンしようとするとメッセージをログに記録します。

リスト 1. UI スレッドでソケットをオープンする際のエラーのログへの記録
SecurityManager securityManager = new SecurityManager() {
    public void checkPermission(Permission perm) {
        if(perm instanceof java.net.SocketPermission) {
            if(Thread.currentThread().getName().equals("main&")) {
                logger.log(Level.SEVERE, "Network call on UI thread&");
                new Error().printStackTrace();
            }
        }
    }
};
System.setSecurityManager(securityManager);

コードのインスツルメンテーション

アプリケーションがきちんと階層化されていて、ネットワーク呼び出しが 1 箇所 (あるいは少数の場所) しか通過しない場合、ネットワーク呼び出しの前にアプリケーション・コードに現行のスレッドをチェックさせるという手段があります (リスト 2を参照)。スレッドのチェックは負荷が軽いので、私はこのようなコードを実動ビルドでもそのまま有効にしています。例外の生成とロギングはある程度オーバーヘッドを招きますが、問題の原因が誰にあるのかを突き止めるにはスタック・トレースが極めて有益だからです。

リスト 2. UI スレッドでのネットワーク呼び出しによるエラーのログへの記録
if(Thread.currentThread().getName().equals("main")) {
    logger.log(Level.SEVERE, "Network call on UI thread");
    new Error().printStackTrace();
}

JDK クラスの変更

最後の手段は、JDK のクラスを変更して JDK のインスツルメンテーションを行うことです。このような手段はサポートされてはおらず、厄介でライセンスに違反する可能性もありますが、前述した手法のいずれかが役に立たないというまれな状況では貴重なオプションとなります。この手法の大まかな要領は、JDK のクラスを再コンパイルしてから、-Xbootclasspath/p: を使ってブート・クラスパスの前に JAR またはディレクトリーを追加するというものです。

UI スレッドでアクションを長時間実行させないようにするための対策

ここからは、UI スレッドでアクションを長時間実行させないようにするための対策をいくつか紹介します。一般的な例として、データベース・クエリー、ネットワーク呼び出し、またはディスクから取得した何らかの内容を、表またはツリーとして表示する作業を元に説明します。

妥当な対策

表へのデータの取り込み作業を、UI スレッド上で行おうとしないこと。数百程度の項目数であれば正常に機能しますが、数千となるとその限りではありません。

望ましい対策

ユーザーに表やツリーの一部を最初の結果として表示するにあたり、残りの部分の内容もすべて取り込まれている必要があるという考えは捨てること。例えば、E メール・クライアントを開発している場合、メール・メッセージ一覧の、ある「ページ」をユーザーに表示するのに、その他すべてのフォルダーのメール・メッセージもロードし、表を完成させておく必要はない、ということです。

さらに望ましい対策

SWT/JFace 仮想ウィジェットを活用すること。使用できる手法はいくつかありますが、いずれも要するに「可能な限り作業を先延ばしにする」という手法になります。UI スレッドでは、表またはツリーにプレースホルダー値を入力しておき、バックグラウンド Job として実際の値の検索を行い、取得した値でツリーを更新していきます。

最善の対策

イベント・ハンドラーで行う作業の量に注意すること。とりわけ SWT 選択ハンドラーに注意し、中でも表、ツリー、リストに関連付けられているものについては慎重になってください。この点を取り違えたコードをよく目にしますが、その一例がリスト 3 のメール・アプリケーションの選択リスナーです。この例では、メッセージが選択されるたびにデータベース・クエリーが実行され、メール詳細の読み取りと UI の更新が行われています。

リスト 3. 選択が変更されるごとに応答する選択リスナー
viewer.addSelectionChangedListener(new ISelectionChangedListener() {
    public void selectionChanged(SelectionChangedEvent event) {
        new Job("go to db") {
            protected IStatus run(IProgressMonitor monitor) {
                //do expensive work here
                return Status.OK_STATUS; 
            }
        }.schedule();
    }
});

リスト 3 の開発者は、この手法が負荷が高くなることをわかっていて、バックグラウンド Job で作業を行っています。問題は、ユーザーが受信箱の最初のメッセージを選択してからキーボードの下矢印を数秒間押し続けるという可能性をこの開発者が予期していなかったことです。このような場合には通常、選択が変更されるたびに新しいバックグラウンドJob が実行され、JobManager がすぐに Job で溢れてしまうことになります。

選択ハンドラーよりも適切なのは、postSelection ハンドラーです (リスト 4を参照)。JFace が提供する PostSelection ハンドラーは、アプリケーションにとっておそらく意味のない一連の選択をいちいち通知するのではなく、イベントを合体させて最後の選択のみをアプリケーションに通知します。つまり、こまごまとした煩わしいイベントは無視し、代わりに主なイベントに焦点を合わせていると考えてください。

リスト 4. 最後の選択変更のみに応答する選択リスナー
viewer.addPostSelectionChangedListener(new ISelectionChangedListener() {
    public void selectionChanged(SelectionChangedEvent event) {
        new Job("go to db") {
            protected IStatus run(IProgressMonitor monitor) {
                //do expensive work here
                return Status.OK_STATUS; 
            }
        }.schedule();
    }
});

ディスク I/O への対処

皆さんも、メモリーの読み取り/書き込みはナノ秒単位で測定され、ディスクの読み取り/書き込みはミリ秒単位で測定される、というようなことを耳にしたことがあるでしょう。私が今までに取り組んだ RCP アプリケーションのほとんどでは、ディスク I/O ではなく CPU がボトルネックとなっていました。とは言うものの、ディスク I/O も問題になる可能性はあるので、無視することはできません。RCP アプリケーションで問題となりやすいのは、ディスクからの非効率的なイメージ読み取りです。

アプリケーションがどのようにファイルを使用しているかを詳細に調べるときに私がよく使うのは、Sysinternals Process Monitor です。例えば図 5では、ファイルの読み取りでバッファーが使用されていないことが簡単にわかります。

図 5. バッファーを使用しない読み取り I/O を示す Process Monitor
バッファーを使用しない読み取り I/O を示す Process Monitor
バッファーを使用しない読み取り I/O を示す Process Monitor

図 5 の各行を見れば、big.txt という名前のファイルの読み取りが行われていることがわかります。ところが、各行の最後の列が示すとおり、このファイルは一度に 1 バイトずつしか読み取りが行われていません。リスト 5 に、その原因となっているコードを記載します。

リスト 5. バッファーを使用しない I/O (悪い例)
InputStream in = new FileInputStream(args[0]);
int c;
while ((c = in.read()) != -1) {
    //stuff characters in buffer, etc
}

7200RPM のディスク・ドライブを搭載した私の ThinkPad T60p では、7MB のファイルを読み取るのに 24 秒かかることになります。これを BufferedInputStream に切り替えると、時間は 350 ミリ秒に短縮されます。この高速化は主にハード・ディスクの使用率改善によるものです。大抵のプログラムではこれほどまで劇的な改善にはならないかもしれませんが、バッファーを使用しない I/O については BufferedInputStream などのバッファー付きストリームで修正する価値があります。

RCP アプリケーションに見られるもう 1 つの問題は、私がイメージ・バーン (image burn) と呼んでいるもので、これはイメージがディスクから頻繁に読み取られて処理されると発生します。その発生頻度によっては、イメージをキャッシング対象にすると有効かもしれません。

スレッド化と Eclipse のジョブ

アプリケーションはコンピューターのリソースを有効に利用しなければなりません。CPU を最大限有効に使用するには、アプリケーション・スレッドの数をプロセッサー数、そしてスレッドが実行する作業のタイプに応じて調整する必要があります。それぞれの Java スレッドには特定の大きさのネイティブ・メモリーが関連付けられ、またスレッド間でコンテキスト・スイッチが行われる際にはある程度の CPU オーバーヘッドが生じるため、スレッド数を多くすることが常によい策とは限りません。

クライアント・アプリケーションでは、スレッドが乱用されているケースがよく見受けられます。従来 UI スレッドをブロックしていた長時間の操作にスレッドを使用するのはよいことですが、RCP ベースのアプリケーションの場合、大抵はスレッドより Job を使用する方がよいでしょう。

JobRunnable に似ていて、実行の場となるスレッドではなくタスクを記述します。Job は (当然のことながら) JobManager が管理し、この JobManager がもつワーカー・プールを使って多数の Job を処理することができます。Job がスレッドに勝る利点としてはもう 1 つ、Jobは簡単にインスツルメンテーション化できるという点が挙げられます。いくつかのフラグを設定してアプリケーションを実行すれば、JobManagerJob の生成、スケジュール、実行、そして完了をもれなく通知してくれます。JobManager はワーカー・プールの管理状況も通知するので、バックグラウンドJob が実行された時点とその実行期間を把握したいときには大きなメリットとなります。

このサポートを有効にするには、リスト 6 の行をファイルに追加してください (この情報は、org.eclipse.core.jobs バンドルの .options ファイルにも記載されています)。その上で、-debug Path_to_debug_fileのオプションを付けて RCP アプリケーションを起動します。

リスト 6. Job デバッグ情報の有効化
# Prints debug information on running background jobs
org.eclipse.core.jobs/jobs=true
# Includes current date and time in job debug information
org.eclipse.core.jobs/jobs/timing=true
# Computes location of error on mismatched IJobManager.beginRule/endRule
org.eclipse.core.jobs/jobs/beginend=true
# Pedantic assertion checking on locks and deadlock reporting
org.eclipse.core.jobs/jobs/locks=true
# Throws an IllegalStateException when deadlock occurs
org.eclipse.core.jobs/jobs/errorondeadlock=true
# Debug shutdown behaviour
org.eclipse.core.jobs/jobs/shutdown=true

Job は高機能なスケジューリング・ルールもサポートしています。Job が実行されるタイミングを制御するためのスケジューリング・ルールを、単純なものから複雑なものまで作成することができます。リスト 7では、2 つの Jobが同時に実行されないようにするためのルールを作成する方法の 1 つを例示しています。

リスト 7. 2 つの Job の同時実行の防止
ISchedulingRule onlyOne = new ISchedulingRule() {
    public boolean isConflicting(ISchedulingRule rule) {
        return rule == this;
    }
    public boolean contains(ISchedulingRule rule) {
        return rule == this;
    }
};
Job job1 = new LongRunningJob();
Job job2 = new LongRunningJob();
job1.setRule(onlyOne);
job2.setRule(onlyOne);
job1.schedule();
job2.schedule();
return onlyOne;

リスト 7 のコードのポイントは、Job が実行される前にスケジューリング・ルールの isConflicting() メソッドが呼び出されているところにあります。job1 が実行されている間は、ルールは job1 に「所有」されます。その他の例については、ISchedulingRule の作成者の情報を参照してください (「参考文献」を参照)。

java.util.Timer も、スレッドと同じく乱用されがちです。最終的にTimer をいくつも生成しているアプリケーションが多くありますが、こうするとそれぞれの Timer に対し、これを管理する専用スレッドが生成されるのです。一般に、アプリケーションは生成する Timer を 1 つにとどめ、複数の java.util.TimerTask を指定して管理させるようにしなければなりません。ですが、多くの開発者が、個々に作業を行った場合などは特に、独自の Timer を生成してスレッドを無駄に使用しています。RCP アプリケーションでは、ある特定の時点で実行するようにスケジュールされた Job を使用することで java.util.Timer を使用せずに済むケースがほとんどです。

1.4 よりも後のバージョンの JVM を使用している場合は、java.util.concurrent ExecutorScheduledThreadPoolExecutor、そして Task を使用すれば同様のメリットを得ることができます。java.util.Timer クラスは非推奨になってはいませんが、実際には大筋で ScheduledExecutorService に置き換えられています。

スレッドについて最後に言っておくべき重要な点は、絶対に必要不可欠でない限り、ビジー・スリープは実行しないことです。リスト 8 に、避けるべき例を示します。

リスト 8. ビジー・スリープ
while(someCondition) {
    ...more code here...
    Thread.sleep(aFewMilliseconds);
    ...more code here...
}

このようなアプリケーションでは大抵、該当するコード・ブロックに多くの不要なガーベッジが作り出され、コンテキスト・スイッチが頻繁に行われます。ビジー・スリープではなく、java.util.concurrent に用意された上位レベルの同期クラス (BlockingQueueSemaphoreFutureTask、または CountDownLatch など) を使用してください。これらの並行クラスを使用することで、条件が true になるまで待機する間、CPU を消費せずに済みます。この手段は、モニターを使用しないサード・パーティーのコードを呼び出している場合は使用できないこともありますので、そのようなときは、ポーリング中に生成されるガーベッジの量を極力抑えるようにしてください。

起動パフォーマンスの分析と改善

起動パフォーマンスの改善は、RCP アプリケーションでは難題となる場合があります。一般に、起動パフォーマンスに影響を与えるのは、ディスク I/O、クラスロード、そしてバイトコード検証です。もちろんバンドル内で非常に多くの作業をすることで、バンドルの立ち上げに時間がかかることもありますが、それが起動の大部分を占めることは通常ありません。どちらかというと、起動では小さな作業がとにかく多数行われます。大抵は単独で長時間かかるような作業はありませんが、すべての作業に少しずつかかる時間が蓄積されると、結局は長時間になってしまうのです。

RCP アプリケーションは、Java 言語に基づく動的モジュール・システム (Dynamic Module System) である OSGi の上に構築されます。OSGi はクラスロードをグローバル・フックにする簡単な手段を提供しており、このクラスロードのフックを利用して Java クラスのキャッシュを生成し、ディスクに頻繁にアクセスしないようにすることで起動パフォーマンスを改善する、ということを行っている開発者もいます。これは見込みのある手法なので、さらに調査を進めてその有効性を判断する必要があります。

起動パフォーマンスを改善するために Eclipse が奨励しているもう 1 つの手法は、遅延バンドル起動です。この手法では、バンドルは必要となるまでロードおよび起動されません。通常、私は起動パフォーマンスを分析する際に、起動されたすべてのバンドルのリストと各バンドルが起動された理由を把握するためのスタック・トレースを集めます。これを見ながら、バンドルが本当に起動される必要があるのかどうかを判断します。起動が不要と思われるバンドルがあれば削除してみて、改善の傾向 (およびどんな支障がでるのか) を明らかにします。該当するバンドルを削除するとどの程度の改善につながるかがわかったところで、コードを所有する開発者に連絡して、バンドルの削除あるいはバンドルの起動の遅延について話し合うというわけです。

バンドルの起動とクラスロードに関する情報を集めるには、リスト 9 のデバッグ・オプション (org.eclipse.osgi バンドルの .options ファイルに記載) を使用してください。または、CVS 形式の最新バージョンもあります (「参考文献」を参照)。

リスト 9. OSGi デバッグ・オプションの有効化
org.eclipse.osgi/debug=true
org.eclipse.osgi/debug/bundleTime=true
org.eclipse.osgi/debug/monitorbundles=true
org.eclipse.osgi/monitor/activation=true
org.eclipse.osgi/monitor/classes=true

一方、プラグイン開発者は無理してでも遅延ロードを阻止しようとするかもしれません。私が以前に取り組んだ製品を例に挙げると、この製品には重ねて表示することができる一連のビューがあり、拡張機能では独自のビューをそこに追加できるように定義されていました。起動時にはビューのいずれか 1 つが表示されるか、あるいは 1 つも表示されない可能性もありましたが、拡張機能の開発者は、表示されることがないとしてもビューを作成しました。製品に変更が加えられ、拡張機能はビューのタイトルとアイコンのみを表示する一方、エンド・ユーザーがビューを表示しようとするまでは関連するバンドルが起動されないようになりました。この変更によって起動されるバンドルが少なくなったため、大幅な改善が達成されたというわけです。

別の例として、ログイン・ダイアログのあるアプリケーションを構築しているとしましょう。このような場合は、このログイン・ダイアログを表示するために必要なバンドルだけを起動することを目標としなければなりません。今まで私は、ログイン・ダイアログを表示するためだけに全バンドルの 7 割が起動されるようなアプリケーションを見てきたからです。

アプリケーションを起動するタイミングが分散されても、起動に要する合計時間はそのまま変わらないという仕組みなので、ある意味ごまかしているだけの対策と言えなくもありません。しかしながら、ユーザーは、使用しない製品機能にまで「代価を払う」必要はないのです。ここでの意図は、最初に時間をかけて一気に行ってしまうのではなく、その負荷を全体的に分散させるということです。実際のパフォーマンスを改善するために可能な対策をすべて取ったにもかかわらず、それでもアプリケーションの速度が満足できるものにならない場合は、感覚として認識されるパフォーマンスの改善案も試してみてください。

まとめ

アプリケーションのアーキテクチャーと設計フェーズには、パフォーマンスについて理解している人の参加が不可欠であることを、ここで改めて強調したいと思います。少なくとも、アーキテクトあるいは開発リーダーが、基本的な次数解析または時間計算量 (Big O など) を把握し、アプリケーションが拡大するに従ってストレージ要件または実行時間がどのように変化するかを理解していなければなりません。最後の段階で大きなパフォーマンス問題を修正しようと全力を挙げても、後になってアーキテクチャーを大幅に変更するのはほとんど不可能なため事後対策になりがちです。しかも、大きな効果は望めません。

しかしながら、極めて優れた設計のアプリケーションでもパフォーマンスのボトルネックはあるものなので、これらのボトルネックを判断し、対処するためのツールと手法は必要になります。この記事を読んで、RCP アプリケーションのパフォーマンスを測定する方法、速度を低下させているボトルネックが CPU または I/O のどちらであるかを判断する方法、インスツルメンテーション手法を使う方法、UI スレッドの応答性を維持する方法、Job を使用してスレッドの乱用を回避する方法、そして起動パフォーマンスを改善する方法がわかったはずです。次回の記事では、メモリー使用率を把握し、メモリー・リークを突き止める手法を説明します。

謝辞

この記事のレビュー段階でさまざまな提案を出してくれた Brian Goetz 氏に感謝します。


ダウンロード可能なリソース


関連トピック


コメント

コメントを登録するにはサインインあるいは登録してください。

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology, Open source
ArticleID=257929
ArticleTitle=リッチ・クライアント・アプリケーションのパフォーマンス: 第 1 回 パフォーマンス分析のツール、手法、そしてヒント
publish-date=07312007