GraalVMのインストゥルメントのスタート・ガイド
ツールは、GraalVMプラットフォーム内ではインストゥルメントと呼ばれることもあります。インストゥルメントAPIは、このようなインストゥルメントを実装するために使用されます。インストゥルメントは、非常に詳細なVMレベルのランタイム・イベントを追跡し、GraalVMで実行されているアプリケーションのランタイム動作をプロファイル、検査および分析できます。
Simple Tool
ツール開発者がより簡単に開始できるように、Simple Toolサンプル・プロジェクトを作成しました。これは、単純なコード・カバレッジ・ツールを実装するjavadoc対応のMavenプロジェクトです。
ツール開発の開始点として、リポジトリをクローニングし、ソース・コードを調べることをお薦めします。次の各項では、Simple Toolソース・コードを実行例として使用して、GraalVMツールのビルドと実行に必要なステップについて説明します。これらの項ではインストゥルメントAPIのすべての機能を網羅しているわけではないため、詳細はjavadocを確認することをお薦めします。
要件
前述のように、Simple Toolはコード・カバレッジ・ツールです。最終目標は、実行されたソース・コード行の割合およびどの行が実行されたかに関する情報を開発者に提供することです。このことを考慮して、このツールのおおまかな要件をいくつか定義できます:
- ツールは、ロードされたソース・コードを追跡します。
- ツールは、実行されたソース・コードを追跡します。
- アプリケーションが終了するとき、ツールは行単位のカバレッジ情報を計算して出力します。
インストゥルメントAPI
ツールの主な開始点は、TruffleInstrumentクラスのサブクラス化です。当然ながら、このことはsimple toolコード・ベースによって実行され、SimpleCoverageInstrumentクラスが作成されます。
クラスのRegistration注釈により、新しく作成されたインストゥルメントがフレームワークによって自動的に検出され、インストゥルメントAPIに確実に登録されます。また、インストゥルメントに関するメタデータ(ID、名前、バージョン、インストゥルメントが提供するサービス、およびインストゥルメントが内部かどうか)も提供されます。この注釈を有効にするには、DSLプロセッサでこのクラスを処理する必要があります。これは、Simple Toolの場合、DSLプロセッサをMaven構成の依存性として使用することで自動的に行われます。
ここで、SimpleCoverageInstrument
クラスの実装、つまり、オーバーライドされるTruffleInstrument
のメソッドを確認します。これらは、onCreate、onDisposeおよびgetOptionDescriptorsです。onCreate
メソッドとonDispose
メソッドは、その名のとおり、インストゥルメントが作成および破棄されるときにフレームワークによってコールされます。これらの実装については後で説明することとし、まず、残りのgetOptionDescriptors
の実装について説明します。
Truffle言語実装フレームワークには、コマンド行オプションを指定するための独自のシステムが付属しています。これらのオプションを使用すると、ツール・ユーザーは、コマンドラインから、またはポリグロット・コンテキストを作成するときにツールを制御できます。これは注釈ベースであり、このようなオプションの例には、SimpleCoverageInstrument
のENABLEDフィールドとPRINT_COVERAGEフィールドがあります。これらはどちらも、Registration
注釈と同様に、オプションのメタデータを提供するOptionで注釈付けされたOptionKey型のstatic finalフィールドです。ここでも、Registration
注釈と同様に、Option
注釈を有効にするには、OptionDescriptorsのサブクラス(この例ではSimpleCoverageInstrumentOptionDescriptors
)を生成するDSLプロセッサが必要です。インストゥルメントが提供するオプションをフレームワークに通知するには、このクラスのインスタンスをgetOptionDescriptors
メソッドから返す必要があります。
onCreate
メソッドに戻ると、引数としてEnvクラスのインスタンスを受け取ります。このオブジェクトは多くの有用な情報を提供しますが、onCreate
メソッドについては、ツールに渡されるオプションを読み取るために使用できるgetOptionsメソッドに着目します。これを使用してENABLED
オプションが設定されているかどうかを確認し、設定されている場合は、enableメソッドをコールしてツールを有効にします。同様に、onDispose
メソッドでは、PRINT_COVERAGE
オプションの状態を確認し、有効になっている場合は、結果を出力するprintResultsメソッドをコールします。
ツールを有効にするとはどういう意味でしょうか。一般的に、関心のあるイベントおよびその対応方法についてフレームワークに通知することを意味します。enable
メソッドでは、次のことを行っています:
- まず、SourceSectionFilterを定義します。このフィルタは、関心のあるソース・コードの一部の宣言定義です。この例では、式とみなされるすべてのノードを考慮し、内部言語部分は考慮しません。
- 次に、インストゥルメントするシステムの部分を指定できるオブジェクトである、Instrumenterクラスのインスタンスを取得します。
- 最後に、
Instrumenter
クラスを使用して、次の2つの項で説明するソース・セクション・リスナーと実行イベント・ファクトリを指定します。
ソース・セクション・リスナー
言語APIは、ソース・コード・ユニットであるSourceの概念と、Source
の1つの連続した部分であるSourceSection (たとえば、1つのメソッド、1つの文、1つの式など)を提供します。詳細は、対応するjavadocを参照してください。
Simple Toolの最初の要件は、ロードされたソース・コードを追跡することです。インストゥルメントAPIはLoadSourceSectionListenerを提供し、これがサブクラス化されてインストゥルメンタに登録されると、ユーザーが実行時ロード・ソース・セクションに対応できるようになります。これは、インストゥルメントのenableメソッドに登録されているGatherSourceSectionsListenerで行うこととまったく同じです。GatherSourceSectionsListener
の実装は非常に簡単です: onLoadメソッドをオーバーライドして、ロードされた各ソース・セクションのインストゥルメントに通知します。インストゥルメントは、各Source
から、各ソースのロードされた一連のソース・セクションを保持するCoverageオブジェクトへのマッピングを保持します。
実行イベント・ノード
ゲスト言語は、抽象構文ツリー(AST)インタプリタとして実装されます。言語実装者によって特定のノードにタグで注釈が付けられるため、前述のSourceSectionFilter
を使用して、関心のあるノードを言語に依存しない方法で選択できます。
インストゥルメントAPIの主な利点は、関心のあるノードをラップする特殊化されたノードをASTに挿入できることです。これらのノードは、言語開発者が使用するものと同じインフラストラクチャを使用してビルドされ、ランタイムの観点からは、言語ノードと区別できません。これは、ゲスト言語をこのような高パフォーマンスの言語実装に最適化するために使用されるすべての手法を、ツール開発者も使用できることを意味します。
これらの手法の詳細は、言語実装のドキュメントを参照してください。ここでは、Simple Toolが2番目の要件を満たすには、すべての式を、その式が実行されたときに通知する独自のノードを使用してインストゥルメントする必要があることを述べるにとどめます。
このタスクでは、CoverageNodeを使用します。これはExecutionEventNodeのサブクラスで、名前が示すように、実行中にイベントをインストゥルメントするために使用されます。ExecutionEventNode
にはオーバーライドするメソッドが多数用意されていますが、ここではonReturnValueにのみ着目します。このメソッドは、ラップされたノードが値を返したとき、つまり、正常に実行されたときに起動されます。実装はかなり簡単です。この特定のSourceSection
を持つノードが実行されたことをインストゥルメントに通知するのみで、インストゥルメントはカバレッジ・マップ内のCoverage
オブジェクトを更新します。
ロジックはフラグによって保護されるため、インストゥルメントはノードごとに1回のみ通知されます。このフラグにはCompilationFinalの注釈が付くこと、および、インストゥルメントをコールする前にtransferToInterpreterAndInvalidate()をコールすることがTruffleの標準的な手法であることから、このインストゥルメンテーションが必要でなくなる(つまり、ノードが実行される)と、このインストゥルメンテーションが以降のコンパイルから削除され、パフォーマンス・オーバーヘッドもなくなることが確実になります。
フレームワークが必要に応じてCoverageNode
をインスタンス化する方法を認識するように、ファクトリを提供する必要があります。ファクトリは、ExecutionEventNodeFactoryのサブクラスであるCoverageEventFactoryです。このクラスは、提供されたEventContext内を検索することによって、各CoverageNode
がインストゥルメントしているSourceSection
を認識していることを確認します。
最後に、インストゥルメントを有効にするときに、フィルタによって選択されたノードをラップするためにファクトリを使用するようにインストゥルメンタに指示します。
ユーザーとインストゥルメントとの対話
Simple Toolの3番目の最後の要件は、行カバレッジを標準出力に出力することによってユーザーと実際に対話することです。インストゥルメントは、インストゥルメントが破棄されるときに当然コールされるonDisposeメソッドをオーバーライドします。このメソッドでは、適切なオプションが設定されていることを確認し、設定されている場合は、Coverage
オブジェクトのマップによって記録されたカバレッジを計算して出力します。
この方法で、ユーザーに有用な情報を簡単に提供できますが、これが唯一の方法ではありません。ツールで、データをファイルに直接ダンプしたり、情報を表示するWebエンドポイントを実行することができます。インストゥルメントAPIがユーザーに提供するメカニズムの1つは、他のインストゥルメントによってルックアップされるサービスとしてインストゥルメントを登録することです。インストゥルメントのRegistration注釈を見ると、インストゥルメントが他のインストゥルメントに提供するサービスを指定できるservices
フィールドがあることがわかります。これらのサービスは、明示的に登録する必要があります。これにより、インストゥルメント間で対象をより適切に分離できるため、たとえば、SimpleCoverageInstrument
を使用してREST APIを介してオンデマンド・カバレッジ情報をユーザーに提供する「リアルタイム・カバレッジ」インストゥルメントと、カバレッジがしきい値を下回った場合に実行を停止する「低カバレッジで中止」インストゥルメント(どちらもサービスとしてSimpleCoverageInstrument
を使用)を使用できます。
ノート: 分離の理由から、インストゥルメント・サービスはアプリケーション・コードでは使用できず、他のインストゥルメントまたはゲスト言語からのみ使用できます。
GraalVMへのツールのインストール
これまでのところ、Simple Toolはすべての要件を満たしているようですが、どのように使用するかという問題が残っています。前述のように、Simple ToolはMavenプロジェクトです。JAVA_HOME
をGraalVMインストールに設定し、mvn package
を実行すると、target/simpletool-<version>.jar
が生成されます。これは、Simple Toolのディストリビューション形式です。
Truffleフレームワークでは、言語/ツール・コードとアプリケーション・コードが明確に分離されます。このため、JARをクラス・パスに配置しても、フレームワークは新しいツールが必要であるとは認識しません。これを実現するために、--vm.Dtruffle.class.path.append=/path/to/simpletool-<version>.jar
を使用しています(単純なツールのランチャ・スクリプトを参照)。このスクリプトは、Simple Toolに指定したCLIオプションを設定できることも示しています。つまり、./simpletool js example.js
を実行すると、GraalVMのjs
ランチャが起動し、ツールがフレームワーク・クラス・パスに追加され、含まれているexample.jsファイルがSimple Toolを有効にして実行され、次のように出力されます:
==
Coverage of /path/to/simpletool/example.js is 59.42%
+ var N = 2000;
+ var EXPECTED = 17393;
function Natural() {
+ x = 2;
+ return {
+ 'next' : function() { return x++; }
+ };
}
function Filter(number, filter) {
+ var self = this;
+ this.number = number;
+ this.filter = filter;
+ this.accept = function(n) {
+ var filter = self;
+ for (;;) {
+ if (n % filter.number === 0) {
+ return false;
+ }
+ filter = filter.filter;
+ if (filter === null) {
+ break;
+ }
+ }
+ return true;
+ };
+ return this;
}
function Primes(natural) {
+ var self = this;
+ this.natural = natural;
+ this.filter = null;
+ this.next = function() {
+ for (;;) {
+ var n = self.natural.next();
+ if (self.filter === null || self.filter.accept(n)) {
+ self.filter = new Filter(n, self.filter);
+ return n;
+ }
+ }
+ };
}
+ var holdsAFunctionThatIsNeverCalled = function(natural) {
- var self = this;
- this.natural = natural;
- this.filter = null;
- this.next = function() {
- for (;;) {
- var n = self.natural.next();
- if (self.filter === null || self.filter.accept(n)) {
- self.filter = new Filter(n, self.filter);
- return n;
- }
- }
- };
+ }
- var holdsAFunctionThatIsNeverCalledOneLine = function() {return null;}
function primesMain() {
+ var primes = new Primes(Natural());
+ var primArray = [];
+ for (var i=0;i<=N;i++) { primArray.push(primes.next()); }
- if (primArray[N] != EXPECTED) { throw new Error('wrong prime found: ' + primArray[N]); }
}
+ primesMain();
その他の例
次の例は、インストゥルメントAPIで解決できる一般的なユースケースを示すことを目的としています。
- カバレッジ・インストゥルメント: Simple Toolの構築に使用されたカバレッジ・ツールの例。必要に応じて、詳細テキストで実行例として使用されます。
- デバッガ・インストゥルメント: デバッガの実装方法の概説。インストゥルメントAPIには、直接使用できるデバッガ・インストゥルメントがすでに用意されています。
- 文プロファイラ: 文の実行をプロファイルできるプロファイラ。
インストゥルメンテーション・イベント・リスナー
インストゥルメントAPIは、com.oracle.truffle.api.instrumentation
パッケージで定義されています。インストゥルメンテーション・エージェントは、TruffleInstrument
クラスを拡張することで開発でき、Instrumenter
クラスを使用して実行中のGraalVMインスタンスにアタッチできます。インストゥルメンテーション・エージェントは、実行中の言語ランタイムにアタッチされると、言語ランタイムが破棄されないかぎり使用可能なままになります。GraalVMのインストゥルメンテーション・エージェントは、次のような様々なVMレベルのランタイム・イベントをモニターできます:
- ソース・コード関連イベント: モニター対象の言語ランタイムによって新しいSourceまたはSourceSection要素がロードされるたびにエージェントに通知できます。
- 割当てイベント: モニター対象の言語ランタイムのメモリー領域に新しいオブジェクトが割り当てられるたびにエージェントに通知できます。
- 言語ランタイムおよびスレッド作成イベント: 新しい実行コンテキストまたはモニター対象の言語ランタイムの新しいスレッドが作成されるとすぐにエージェントに通知できます。
- アプリケーション実行イベント: モニター対象のアプリケーションが特定の一連の言語操作を実行するたびにエージェントに通知します。このような操作の例には言語文や式が含まれるため、インストゥルメンテーション・エージェントは実行中のアプリケーションを非常に高い精度で検査できます。
インストゥルメンテーション・エージェントは、実行イベントごとに、関連する実行イベントのみをモニターするためにGraalVMインストゥルメンテーション・ランタイムで使用されるフィルタリング基準を定義できます。現在、GraalVMインストゥルメントは次の2つのいずれかのフィルタ・タイプを受け入れます:
AllocationEventFilter
: 割当てイベントを割当てタイプでフィルタします。SourceSectionFilter
: アプリケーション内のソース・コードの場所をフィルタします。
フィルタは、提供されたビルダー・オブジェクトを使用して作成できます。たとえば、次のビルダーはSourceSectionFilter
を作成します:
SourceSectionFilter.newBuilder()
.tagIs(StandardTag.StatementTag)
.mimeTypeIs("x-application/js")
.build()
この例のフィルタを使用すると、特定のアプリケーション内のすべてのJavaScript文の実行をモニターできます。行番号やファイル拡張子など、他のフィルタリング・オプションも指定できます。
例に含まれるようなソース・セクション・フィルタでは、タグを使用して、モニター対象の一連の実行イベントを指定できます。文や式などの言語に依存しないタグは、com.oracle.truffle.api.instrumentation.Tag
クラスで定義されており、すべてのGraalVM言語でサポートされます。GraalVM言語では、標準タグに加えて、言語固有のイベントをきめ細かくプロファイルできるように、他の言語固有のタグが提供される場合があります。(例として、GraalVM JavaScriptエンジンには、Array
、Map
、Math
などのECMA組込みオブジェクトの使用状況を追跡するためのJavaScript固有のタグが用意されています。)
実行イベントのモニタリング
アプリケーション実行イベントにより、非常に正確で詳細なモニタリングが可能になります。GraalVMでは、このようなイベントをプロファイルするために、次の2つの異なるタイプのインストゥルメンテーション・エージェントがサポートされています:
- 実行リスナー: 特定のランタイム・イベントが発生するたびに通知されるインストゥルメンテーション・エージェント。リスナーは
ExecutionEventListener
インタフェースを実装し、状態をソース・コードの場所に関連付けることはできません。 - 実行イベント・ノード: TruffleフレームワークASTノードを使用して表すことができるインストゥルメンテーション・エージェント。このようなエージェントは、
ExecutionEventNode
クラスを拡張し、実行リスナーと同じ機能を持ちますが、状態をソース・コードの場所に関連付けることができます。
単純なインストゥルメンテーション・エージェント
ランタイム・コード・カバレッジの実行に使用されるカスタム・インストゥルメンテーション・エージェントの簡単な例は、CoverageExample
クラスにあります。エージェント、その設計および機能の概要を次に示します。
すべてのインストゥルメントは、TruffleInstrument
抽象クラスを拡張し、@Registration
注釈を介してGraalVMランタイムに登録されます:
@Registration(id = CoverageExample.ID, services = Object.class)
public final class CoverageExample extends TruffleInstrument {
@Override
protected void onCreate(final Env env) {
}
/* Other methods omitted... */
}
インストゥルメントはonCreate(Env env)
メソッドをオーバーライドして、インストゥルメントのロード時にカスタム操作を実行します。通常、インストゥルメントはこのメソッドを使用して、既存のGraalVM実行環境に自身を登録します。たとえば、ASTノードを使用するインストゥルメントは、次の方法で登録できます:
@Override
protected void onCreate(final Env env) {
SourceSectionFilter.Builder builder = SourceSectionFilter.newBuilder();
SourceSectionFilter filter = builder.tagIs(EXPRESSION).build();
Instrumenter instrumenter = env.getInstrumenter();
instrumenter.attachExecutionEventFactory(filter, new CoverageEventFactory(env));
}
インストゥルメントは、次の2つの引数を指定して、attachExecutionEventFactory
メソッドを使用して自身を実行中のGraalVMに接続します:
SourceSectionFilter
: 追跡する特定のコード・セクションについてGraalVMに通知するために使用されるソース・セクション・フィルタ。ExecutionEventNodeFactory
: (ソース・フィルタで指定された)ランタイム・イベントが実行されるたびにエージェントによって実行されるインストゥルメンテーションASTノードを提供するTruffle ASTファクトリ。
アプリケーションのASTノードをインストゥルメントする基本的なExecutionEventNodeFactory
は、次の方法で実装できます:
public ExecutionEventNode create(final EventContext ec) {
return new ExecutionEventNode() {
@Override
public void onReturnValue(VirtualFrame vFrame, Object result) {
/*
* Code to be executed every time a filtered source code
* element is evaluated by the guest language.
*/
}
};
}
実行イベント・ノードは、ランタイム実行イベントをインターセプトする特定のコールバック・メソッドを実装できます。たとえば、次のものがあります:
onEnter
: フィルタされたソース・コード要素(言語文や式など)に対応するASTノードが評価される前に実行されます。onReturnValue
: ソース・コード要素が値を戻した後に実行されます。onReturnExceptional
: フィルタされたソース・コード要素が例外をスローした場合に実行されます。
実行イベント・ノードは、コードの場所ごとに作成されます。したがって、これらを使用して、インストゥルメントされたアプリケーションの特定のソース・コードの場所に固有のデータを格納できます。たとえば、インストゥルメンテーション・ノードは、ノードローカル・フラグを使用して、すでにアクセスされたすべてのコードの場所を追跡できます。このようなノードローカルboolean
フラグを使用すると、ASTノードの実行を次の方法で追跡できます:
// To keep track of all source code locations executed
private final Set<SourceSection> coverage = new HashSet<>();
public ExecutionEventNode create(final EventContext ec) {
return new ExecutionEventNode() {
// Per-node flag to keep track of execution for this node
@CompilationFinal private boolean visited = false;
@Override
public void onReturnValue(VirtualFrame vFrame, Object result) {
if (!visited) {
CompilerDirectives.transferToInterpreterAndInvalidate();
visited = true;
SourceSection src = ec.getInstrumentedSourceSection();
coverage.add(src);
}
}
};
}
前述のコードが示すように、ExecutionEventNode
は有効なASTノードです。これは、インストゥルメンテーション・コードがインストゥルメントされたアプリケーションとともにGraalVMランタイムによって最適化され、インストゥルメンテーション・オーバーヘッドが最小限になることを意味します。さらに、これにより、インストゥルメント開発者は、インストゥルメンテーション・ノードからTruffleフレームワーク・コンパイラ・ディレクティブを直接使用できます。この例では、コンパイラ・ディレクティブを使用して、visited
をコンパイル終了とみなすことができることをGraalコンパイラに通知します。
各インストゥルメンテーション・ノードは、特定のコードの場所にバインドされます。このような場所には、指定されたEventContext
オブジェクトを使用してエージェントがアクセスできます。コンテキスト・オブジェクトは、インストゥルメンテーション・ノードに、実行されている現在のASTノードに関する様々な情報へのアクセスを提供します。EventContext
を介してインストゥルメンテーション・エージェントが使用できる問合せAPIの例は次のとおりです:
hasTag
: インストゥルメントされたノードに特定のノードTag
を問い合せます(たとえば、文ノードが条件ノードでもあるかどうかを確認します)。getInstrumentedSourceSection
: 現在のノードに関連付けられたSourceSection
にアクセスします。getInstrumentedNode
: 現在のインストゥルメンテーション・イベントに対応するNode
にアクセスします。
きめ細かい式プロファイリング
インストゥルメンテーション・エージェントは、言語式などの部分イベントもプロファイルできます。このためには、2つのソース・セクション・フィルタを指定してエージェントを初期化する必要があります:
// What source sections are we interested in?
SourceSectionFilter sourceSectionFilter = SourceSectionFilter.newBuilder()
.tagIs(JSTags.BinaryOperation.class)
.build();
// What generates input data to track?
SourceSectionFilter inputGeneratingLocations = SourceSectionFilter.newBuilder()
.tagIs(StandardTags.ExpressionTag.class)
.build();
instrumenter.attachExecutionEventFactory(sourceSectionFilter, inputGeneratingLocations, factory);
最初のソース・セクション・フィルタ(この例ではsourceSectionFilter
)は前述の他のフィルタと同等の標準フィルタとなり、モニター対象のソース・コードの場所を識別するために使用されます。2番目のセクション・フィルタinputGeneratingLocations
は、特定のソース・セクションについてモニター対象とする中間値を指定するために、エージェントによって使用されます。中間値は、モニター対象のコード要素の実行に関連するすべての監視可能な値に対応し、onInputValue
コールバックによってインストゥルメンテーション・エージェントにレポートされます。たとえば、エージェントが、JavaScriptの合計操作(つまり、+
)に指定されたすべてのオペランド値をプロファイルする必要があるとします:
var a = 3;
var b = 4;
// the '+' expression is profiled
var c = a + b;
JavaScriptバイナリ式でフィルタすることで、インストゥルメンテーション・エージェントは前述のコード・スニペットの次のランタイム・イベントを検出できます:
onEnter()
: 3行目のバイナリ式。onInputValue()
: 3行目のバイナリ操作の最初のオペランド。コールバックによってレポートされる値は、3
、つまりa
ローカル変数の値になります。onInputValue()
: バイナリ操作の2番目のオペランド。コールバックによってレポートされる値は、4
、つまりb
ローカル変数の値になります。onReturnValue()
: バイナリ式。コールバックに指定された値は、式が評価を完了した後に戻される値、つまり値7
になります。
ソース・セクション・フィルタを可能性のあるすべてのイベントに拡張することで、インストゥルメンテーション・エージェントは次の実行トレース(擬似コード内)と同等のものを監視します:
// First variable declaration
onEnter - VariableWrite
onEnter - NumericLiteral
onReturnValue - NumericLiteral
onInputValue - (3)
onReturnValue - VariableWrite
// Second variable declaration
onEnter - VariableWrite
onEnter - NumericLiteral
onReturnValue - NumericLiteral
onInputValue - (4)
onReturnValue - VariableWrite
// Third variable declaration
onEnter - VariableWrite
onEnter - BinaryOperation
onEnter - VariableRead
onReturnValue - VariableRead
onInputValue - (3)
onEnter - VariableRead
onReturnValue - VariableRead
onInputValue - (4)
onReturnValue - BinaryOperation
onInputValue - (7)
onReturnValue - VariableWrite
onInputValue
メソッドをソース・セクション・フィルタと組み合せて使用すると、言語式で使用される中間値などの非常にきめ細かい実行イベントをインターセプトできます。インストゥルメンテーション・フレームワークにアクセスできる中間値は、各言語で提供されるインストゥルメンテーション・サポートによって大きく異なります。さらに、言語は、言語固有のTag
クラスに関連付けられた追加のメタデータを提供する場合があります。
アプリケーションの実行フローの変更
これまでに説明したインストゥルメンテーション機能により、ユーザーは実行中のアプリケーションの特定の側面を監視できます。インストゥルメントAPIは、アプリケーションの動作を受動的にモニタリングするだけでなく、実行時にアプリケーションの動作を能動的に変更するサポートも備えています。このような機能を使用すると、実行中のアプリケーションの動作に影響を与える複雑なインストゥルメンテーション・エージェントを作成して、特定のランタイム・セマンティクスを実現できます。たとえば、実行中のアプリケーションのセマンティクスを変更して、(コール時に例外をスローすることなどによって)特定のメソッドまたは関数が実行されないようにすることができます。
このような機能を持つインストゥルメンテーション・エージェントは、実行イベント・リスナーおよびファクトリでonUnwind
コールバックを利用することで実装できます。例として、次のJavaScriptコードを考えてみます:
function inc(x) {
return x + 1
}
var a = 10
var b = a;
// Let's call inc() with normal semantics
while (a == b && a < 100000) {
a = inc(a);
b = b + 1;
}
c = a;
// Run inc() and alter it's return type using the instrument
return inc(c)
inc
の戻り値を常に42
に変更するインストゥルメンテーション・エージェントは、次のようにExecutionEventListener
を使用して実装できます:
ExecutionEventListener myListener = new ExecutionEventListener() {
@Override
public void onReturnValue(EventContext context, VirtualFrame frame, Object result) {
String callSrc = context.getInstrumentedSourceSection().getCharacters();
// is this the function call that we want to modify?
if ("inc(c)".equals(callSrc)) {
CompilerDirectives.transferToInterpreter();
// notify the runtime that we will change the current execution flow
throw context.createUnwind(null);
}
}
@Override
public Object onUnwind(EventContext context, VirtualFrame frame, Object info) {
// just return 42 as the return value for this node
return 42;
}
}
イベント・リスナーは、たとえば次のインストゥルメントを使用して、すべての関数コールをインターセプトして実行できます:
@TruffleInstrument.Registration(id = "UniversalAnswer", services = UniversalAnswerInstrument.class)
public static class UniversalAnswerInstrument extends TruffleInstrument {
@Override
protected void onCreate(Env env) {
env.registerService(this);
env.getInstrumenter().attachListener(SourceSectionFilter.newBuilder().tagIs(CallTag.class).build(), myListener);
}
}
有効になっている場合、インストゥルメントは、関数コールが戻るたびに、そのonReturnValue
コールバックを実行します。コールバックは、関連するソース・セクションを(getInstrumentedSourceSection
を使用して)読み取り、特定のソース・コード・パターン(この場合は関数コールinc(c)
)を検索します。このようなコード・パターンが見つかるとすぐに、インストゥルメントはUnwindException
という特別な実行時例外をスローし、現在のアプリケーションの実行フローの変更についてインストゥルメンテーション・フレームワークに指示します。例外はインストゥルメンテーション・エージェントのonUnwind
コールバックによってインターセプトされ、これを使用して、任意の値をインストゥルメントされた元のアプリケーションに返すことができます。
この例では、inc(c)
に対するすべてのコールは、アプリケーション固有のデータに関係なく、42
を返します。より現実的なインストゥルメントは、アプリケーションのいくつかの側面にアクセスしてモニターし、ソース・コードの場所ではなく、オブジェクト・インスタンスやその他のアプリケーション固有のデータに依存する場合があります。