GraalVMのInstruments入門

GraalVMプラットフォームでは、ツールはInstrumentsと呼ばれることがあります。Instrument APIは、そのようなInstrumentsを実装するために使用されます。Instrumentsは、非常に細かい粒度のVMレベルのランタイムイベントを追跡して、GraalVM上で実行されているアプリケーションのランタイム動作をプロファイル、検査、および分析できます。

シンプルなツール #

ツール開発者にとってより簡単な出発点を提供するために、シンプルなツールのサンプルプロジェクトを作成しました。これは、シンプルなコードカバレッジツールを実装した、javadocが豊富なMavenプロジェクトです。

ツール開発の出発点として、リポジトリをクローンしてソースコードを調べてみることをお勧めします。以下のセクションでは、シンプルなツールのソースコードを実行例として使用し、GraalVMツールをビルドして実行するために必要な手順をガイドします。これらのセクションでは、Instrument APIのすべての機能を網羅しているわけではないため、詳細についてはjavadocを確認することをお勧めします。

要件 #

前述のように、シンプルなツールはコードカバレッジツールです。最終的には、ソースコードの何パーセントの行が実行されたか、そして正確にどの行が実行されたかについての情報を開発者に提供する必要があります。それを念頭に置いて、ツールからのいくつかの高レベルの要件を定義できます。

  1. ツールは、ロードされたソースコードを追跡します。
  2. ツールは、実行されたソースコードを追跡します。
  3. アプリケーション終了時に、ツールは行ごとのカバレッジ情報を計算して出力します。

Instrument API #

ツールの主な出発点は、TruffleInstrumentクラスをサブクラス化することです。当然のことながら、シンプルなツールのコードベースはまさにこれを行い、SimpleCoverageInstrumentクラスを作成します。

クラスのRegistrationアノテーションは、新しく作成されたInstrumentがInstrument APIに登録されることを保証します。つまり、フレームワークによって自動的に検出されます。また、Instrumentに関するメタデータ(ID、名前、バージョン、Instrumentが提供するサービス、Instrumentが内部か外部か)も提供します。このアノテーションを有効にするには、DSLプロセッサがこのクラスを処理する必要があります。これは、シンプルなツールの場合、Maven設定でDSLプロセッサを依存関係として持つことで自動的に行われます。

ここで、`SimpleCoverageInstrument`クラスの実装、つまり`TruffleInstrument`からオーバーライドするメソッドについて振り返ってみましょう。これらは、onCreateonDisposegetOptionDescriptorsです。`onCreate`メソッドと`onDispose`メソッドは、その名前が示す通り、Instrumentの作成時と破棄時にフレームワークによって呼び出されます。これらの実装については後ほど説明しますが、まずは残りの1つである`getOptionDescriptors`について説明します。

Truffle言語実装フレームワークには、コマンドラインオプションを指定するための独自のシステムが備わっています。これらのオプションを使用すると、ツールユーザーはコマンドラインから、またはポリグロットコンテキストを作成するときにツールを制御できます。これはアノテーションベースであり、そのようなオプションの例としては、`SimpleCoverageInstrument`のENABLEDフィールドとPRINT_COVERAGEフィールドがあります。これらの両方は、OptionKey型の静的最終フィールドであり、Optionでアノテーションが付けられています。これは、`Registration`アノテーションと同様に、オプションのメタデータを提供します。繰り返しますが、`Registration`アノテーションと同様に、`Option`アノテーションを有効にするにはDSLプロセッサが必要であり、DSLプロセッサはOptionDescriptorsのサブクラスを生成します(この場合は`SimpleCoverageInstrumentOptionDescriptors`という名前)。フレームワークにInstrumentが提供するオプションを知らせるために、このクラスのインスタンスを`getOptionDescriptors`メソッドから返す必要があります。

`onCreate`メソッドに戻ると、引数としてEnvクラスのインスタンスを受け取ります。このオブジェクトは多くの有用な情報を提供しますが、`onCreate`メソッドでは、主にツールに渡されるオプションを読み取るために使用できるgetOptionsメソッドに興味があります。これを使用して、`ENABLED`オプションが設定されているかどうかを確認し、設定されている場合はenableメソッドを呼び出すことによってツールを有効にします。同様に、`onDispose`メソッドでは、`PRINT_COVERAGE`オプションの状態を確認し、有効になっている場合は結果を出力するprintResultsメソッドを呼び出します。

「ツールを有効にする」とはどういう意味でしょうか?一般に、これは、興味のあるイベントとそれらにどのように反応したいかをフレームワークに伝えることを意味します。`enable`メソッドを見ると、次のことが行われています。

  • まず、SourceSectionFilterを定義します。このフィルターは、興味のあるソースコードの部分を宣言的に定義したものです。この例では、式と consideredれるすべてのノードに関心があり、内部言語の部分には関心がありません。
  • 次に、システムのどの部分をインストゥルメント化したいかを指定できるオブジェクトであるInstrumenterクラスのインスタンスを取得します。
  • 最後に、`Instrumenter`クラスを使用して、ソースセクションリスナーと実行イベントファクトリを指定します。これらの両方は、次の2つのセクションで説明します。

ソースセクションリスナー #

言語APIは、ソースコード単位であるSourceと、`Source`の1つの連続した部分(例:1つのメソッド、1つのステートメント、1つの式など)であるSourceSectionの概念を提供します。詳細は、それぞれのjavadocを参照してください。

シンプルなツールの最初の要件は、ロードされたソースコードを追跡することです。Instrument APIは、サブクラス化してインストゥルメンターに登録すると、ユーザーがソースセクションのランタイムロードに反応できるLoadSourceSectionListenerを提供します。これは、Instrumentのenableメソッドに登録されているGatherSourceSectionsListenerでまさに私たちが行っていることです。`GatherSourceSectionsListener`の実装は非常にシンプルです。onLoadメソッドをオーバーライドして、ロードされた各ソースセクションをInstrumentに通知します。Instrumentは、各`Source`からCoverageオブジェクトへのマッピングを保持します。このオブジェクトは、各ソースのロードされたソースセクションのセットを保持します。

実行イベントノード #

ゲスト言語は、抽象構文木(AST)インタープリターとして実装されます。言語実装者は、特定のノードにタグを付けます。これにより、前述の`SourceSectionFilter`を使用して、言語に依存しない方法で、どのノードに関心があるかを選択できます。

Instrument APIの主な強みは、ASTに特殊なノードを挿入して、対象のノードを「ラップ」できることです。これらのノードは、言語開発者が使用するのと同じインフラストラクチャを使用して構築されており、ランタイムの観点からは言語ノードと区別できません。これは、ゲスト言語をそのような高性能言語実装に最適化するために使用されるすべての技術が、ツール開発者にも利用できることを意味します。

これらの手法の詳細については、言語実装ドキュメントを参照してください。シンプルなツールが2番目の要件を満たすためには、すべての式を、その式が実行されたときに通知する独自のノードでインストゥルメント化する必要があると言うだけで十分です。

このタスクでは、CoverageNodeを使用します。これは、名前が示すように、実行中のイベントを計測するために使用されるExecutionEventNodeのサブクラスです。ExecutionEventNodeはオーバーライド可能なメソッドを多数提供しますが、ここではonReturnValueのみに注目します。このメソッドは、「ラップされた」ノードが値を返したとき、つまり正常に実行されたときに呼び出されます。実装は非常にシンプルです。この特定のSourceSectionを持つノードが実行されたことをインストゥルメントに通知するだけで、インストゥルメントはカバレッジマップ内のCoverageオブジェクトを更新します。

ロジックはフラグによってガードされているため、インストゥルメントへの通知はノードごとに一度だけ行われます。このフラグがCompilationFinalでアノテーションされており、インストゥルメントの呼び出しの前にtransferToInterpreterAndInvalidate()の呼び出しが先行していることは、Truffleの標準的な手法です。これにより、この計測が不要になった場合(ノードが実行された場合)、計測はパフォーマンスのオーバーヘッドとともに、以降のコンパイルから削除されます。

フレームワークが必要なときにCoverageNodeをインスタンス化する方法を知るためには、そのためのファクトリを提供する必要があります。ファクトリは、ExecutionEventNodeFactoryのサブクラスであるCoverageEventFactoryです。このクラスは、提供されたEventContextで検索することにより、各CoverageNodeが計測しているSourceSectionを認識することを保証します。

最後に、インストゥルメントを有効にする際に、インストゥルメンターに、フィルターによって選択されたノードを「ラップする」ためにファクトリを使用するように指示します。

ユーザーとインストゥルメント間の相互作用 #

Simple Toolの3番目で最後の要件は、行カバレッジを標準出力に出力することで、実際にユーザーと対話することです。インストゥルメントは、インストゥルメントが破棄されるときに呼び出されるonDisposeメソッドをオーバーライドします。このメソッドでは、適切なオプションが設定されているかどうかを確認し、設定されている場合は、Coverageオブジェクトのマップによって記録されたカバレッジを計算して出力します。

これはユーザーに役立つ情報を提供する簡単な方法ですが、決して唯一の方法ではありません。ツールはデータをファイルに直接ダンプしたり、情報を表示するWebエンドポイントを実行したりすることもできます。Instrument 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ファイルをクラスパスに配置しても、フレームワークは新しいツールが必要であることを認識しません。これを達成するために、simple toolのランチャースクリプトに示されているように、--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();

その他の例 #

以下の例は、Instrument APIで解決できる一般的なユースケースを示すことを目的としています。

計測イベントリスナー #

Instrument APIは、com.oracle.truffle.api.instrumentationパッケージで定義されています。計測エージェントは、TruffleInstrumentクラスを拡張することで開発でき、Instrumenterクラスを使用して実行中のGraalVMインスタンスにアタッチできます。実行中の言語ランタイムにアタッチされると、言語ランタイムが破棄されない限り、計測エージェントは使用可能なままになります。GraalVMの計測エージェントは、次のようなさまざまなVMレベルのランタイムイベントを監視できます。

  1. ソースコード関連のイベント:監視対象の言語ランタイムによって新しいSourceまたはSourceSection要素がロードされるたびに、エージェントに通知されます。
  2. 割り当てイベント:監視対象の言語ランタイムのメモリ空間に新しいオブジェクトが割り当てられるたびに、エージェントに通知されます。
  3. 言語ランタイムとスレッド作成イベント:監視対象の言語ランタイムの新しい実行コンテキストまたは新しいスレッドが作成されるとすぐに、エージェントに通知されます。
  4. アプリケーション実行イベント:監視対象のアプリケーションが特定の言語操作セットを実行するたびに、エージェントに通知されます。このような操作の例には、言語ステートメントや式が含まれるため、計測エージェントは実行中のアプリケーションを非常に高い精度で検査できます。

実行イベントごとに、計測エージェントは、GraalVM計測ランタイムが関連する実行イベントのみを監視するために使用する_フィルタリング_条件を定義できます。現在、GraalVMインストゥルメントは、次の2つのフィルタータイプのいずれかを受け入れます。

  1. AllocationEventFilter:割り当てタイプ別に割り当てイベントをフィルタリングします。
  2. SourceSectionFilter:アプリケーション内のソースコードの場所をフィルタリングします。

フィルターは、提供されているビルダーオブジェクトを使用して作成できます。たとえば、次のビルダーはSourceSectionFilterを作成します。

SourceSectionFilter.newBuilder()
                   .tagIs(StandardTag.StatementTag)
                   .mimeTypeIs("x-application/js")
                   .build()

この例のフィルターを使用して、特定のアプリケーションのすべてのJavaScriptステートメントの実行を監視できます。行番号やファイル拡張子などの他のフィルタリングオプションも提供できます。

この例のソースセクションフィルターは、_タグ_を使用して、監視する実行イベントのセットを指定できます。ステートメントや式などの言語に依存しないタグは、com.oracle.truffle.api.instrumentation.Tagクラスで定義されており、すべてのGraalVM言語でサポートされています。標準タグに加えて、GraalVM言語は、言語固有のイベントの詳細なプロファイリングを可能にするために、他の言語固有のタグを提供する場合があります。(例として、GraalVM JavaScriptエンジンは、ArrayMapMathなどのECMA組み込みオブジェクトの使用状況を追跡するためのJavaScript固有のタグを提供します。)

実行イベントの監視 #

アプリケーション実行イベントにより、非常に正確で詳細な監視が可能になります。GraalVMは、このようなイベントをプロファイルするために、2つの異なるタイプの計測エージェント、すなわち

  1. _実行リスナー_:特定のランタイムイベントが発生するたびに通知される計測エージェント。リスナーは、ExecutionEventListenerインターフェースを実装し、ソースコードの場所に_状態_を関連付けることはできません。
  2. _実行イベントノード_:Truffle Framework 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));
}

インストゥルメントは、attachExecutionEventFactoryメソッドを使用して、実行中のGraalVMに自身を接続し、次の2つの引数を指定します。

  1. SourceSectionFilter:追跡する特定のコードセクションについてGraalVMに通知するために使用されるソースセクションフィルター。
  2. 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.
       */
    }
  };
}

実行イベントノードは、ランタイム実行イベントをインターセプトするために、特定のコールバックメソッドを実装できます。例としては、

  1. onEnter:フィルターされたソースコード要素(たとえば、言語ステートメントまたは式)に対応するASTノードが評価される_前_に実行されます。
  2. onReturnValue:ソースコード要素が値を返した_後_に実行されます。
  3. 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 の例としては、以下が挙げられます。

  1. hasTag: インストゥルメントされたノードに特定のノード Tag があるかどうかをクエリします(たとえば、ステートメントノードが条件付きノードでもあるかどうかを確認するため)。
  2. getInstrumentedSourceSection: 現在のノードに関連付けられた SourceSection にアクセスします。
  3. 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 の二項式をフィルタリングすることにより、インストゥルメンテーションエージェントは、上記のコードスニペットに対して以下のランタイムイベントを検出できます。

  1. onEnter(): 3行目の二項式に対して。
  2. onInputValue(): 3行目の二項演算の最初のオペランドに対して。コールバックによって報告される値は、a ローカル変数の値である 3 になります。
  3. onInputValue(): 二項演算の2番目のオペランドに対して。コールバックによって報告される値は、b ローカル変数の値である 4 になります。
  4. 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 メソッドは、ソースセクションフィルタと組み合わせて使用​​することで、言語式で使用される中間値など、非常にきめ細かい実行イベントをインターセプトできます。Instrumentation フレームワークにアクセスできる中間値は、各言語によって提供されるインストゥルメンテーションサポートに大きく依存します。さらに、言語は、言語固有の Tag クラスに関連付けられた追加のメタデータを提供する場合があります。

アプリケーションの実行フローの変更 #

これまでに紹介したインストゥルメンテーション機能により、ユーザーは実行中のアプリケーションの特定の側面を*観察*できます。アプリケーションの動作のパッシブモニタリングに加えて、Instrument 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 と呼ばれる特別なランタイム例外をスローします。これは、現在のアプリケーションの実行フローの変更について Instrumentation フレームワークに指示します。例外は、インストゥルメンテーションエージェントの onUnwind コールバックによってインターセプトされます。これは、*任意*の値を元のインストゥルメントされたアプリケーションに返すために使用できます。

この例では、inc(c) へのすべての呼び出しは、アプリケーション固有のデータに関係なく 42 を返します。より現実的なインストゥルメントは、アプリケーションの複数の側面にアクセスして監視する可能性があり、ソースコードの場所ではなく、オブジェクトインスタンスまたは他のアプリケーション固有のデータに依存する可能性があります。

お問い合わせ