Insightのマニュアル

GraalVM Insightは、信頼性のあるアプリケーションを記述するための柔軟な汎用ツールです。このツールの動的な性質により、パフォーマンスを損なうことなく、すでにデプロイされているアプリケーションにトレース・ポイントカットを選択的に適用できます。

中程度のスキルを持つハッカーであれば、いわゆるInsightスニペットを簡単に作成し、実際のプログラムに動的に適用できます。これにより、実行速度を損なうことなく、アプリケーションの実行と動作に関する究極のインサイトが提供されます。

目次

クイック・スタート

必須のHelloWorldの例から始めます。次のコンテンツを含むsource-tracing.jsスクリプトを作成します:

insight.on('source', function(ev) {
    print(`Loading ${ev.characters.length} characters from ${ev.name}`);
});

GraalVMのnodeランチャで--insightインストゥルメント・オプションを追加して実行します。ロードおよび評価されるスクリプトを確認します:

graalvm/bin/node --js.print --insight=source-tracing.js -e "print('The result: ' + 6 * 7)" | tail -n 10
Loading 29938 characters from url.js
Loading 345 characters from internal/idna.js
Loading 12642 characters from punycode.js
Loading 33678 characters from internal/modules/cjs/loader.js
Loading 13058 characters from vm.js
Loading 52408 characters from fs.js
Loading 15920 characters from internal/fs/utils.js
Loading 505 characters from [eval]-wrapper
Loading 29 characters from [eval]
The result: 42

どのようになりましたか。GraalVM Insightのsource-tracing.jsスクリプトにより、指定されたinsightオブジェクトを使用して、ソース・リスナーがランタイムにアタッチされました。そのため、nodeでスクリプトがロードされるたびに、リスナーは通知を受け取り、(この場合は処理されたスクリプトの長さと名前を出力する)アクションを実行できます。

ホット度上位10の例

インサイト情報の収集は、印刷物に限定されません。ご使用の言語でチューリング完全計算を実行できます。たとえば、すべてのメソッド呼出しをカウントし、実行が終了した時点で最も頻度が高かったメソッド呼出しをダンプするプログラムです。

次のコードをfunction-hotness-tracing.jsに保存します:

var map = new Map();

function dumpHotness() {
    print("==== Hotness Top 10 ====");
    var digits = 3;
    Array.from(map.entries()).sort((one, two) => two[1] - one[1]).forEach(function (entry) {
        var number = entry[1].toString();
        if (number.length >= digits) {
            digits = number.length;
        } else {
            number = Array(digits - number.length + 1).join(' ') + number;
        }
        if (number > 10) print(`${number} calls to ${entry[0]}`);
    });
    print("========================");
}

insight.on('enter', function(ev) {
    var cnt = map.get(ev.name);
    if (cnt) {
        cnt = cnt + 1;
    } else {
        cnt = 1;
    }
    map.set(ev.name, cnt);
}, {
    roots: true
});

insight.on('close', dumpHotness);

mapは、Insightスクリプト全体に表示されるグローバル変数であり、insight.on('enter')関数とdumpHotness関数の間で、コードによるデータの共有を許可します。後者は、nodeプロセスの実行が終了すると実行されます(insight.on('close', dumpHotness)を介して登録されます)。プログラムを起動します:

graalvm/bin/node --js.print --insight=function-hotness-tracing.js -e "print('The result: ' + 6 * 7)"
The result: 42
==== Hotness Top 10 ====
543 calls to isPosixPathSeparator
211 calls to E
211 calls to makeNodeErrorWithCode
205 calls to NativeModule
198 calls to uncurryThis
154 calls to :=>
147 calls to nativeModuleRequire
145 calls to NativeModule.compile
 55 calls to internalBinding
 53 calls to :anonymous
 49 calls to :program
 37 calls to getOptionValue
 24 calls to copyProps
 18 calls to validateString
 13 calls to copyPrototype
 13 calls to hideStackFrames
 13 calls to addReadOnlyProcessAlias
========================

nodeプロセスの終了時に、関数呼出しの名前と数を含む表が出力されます。

Insightの任意のGraalVM言語への適用

前述の例はJavaScriptで記述され、nodeが使用されていましたが、GraalVMのポリグロットの性質上、同じインストゥルメントを取得して、GraalVMがサポートする任意の言語に適用できます。たとえば、GraalVM InsightでRuby言語をテストします。

開始するには、source-trace.jsファイルにインストゥルメントを作成します:

insight.on('source', function(ev) {
   if (ev.uri.indexOf('gems') === -1) {
     let n = ev.uri.substring(ev.uri.lastIndexOf('/') + 1);
     print('JavaScript instrument observed load of ' + n);
   }
});

helloworld.rbファイルでRubyプログラムを準備します:

puts 'Hello from GraalVM Ruby!'

ノート: gu install rubyでRubyのサポートがGraalVMに追加されていることを確認してください。

JavaScriptインストゥルメントをRubyプログラムに適用します次に、表示される内容を示します:

graalvm/bin/ruby --polyglot --insight=source-trace.js helloworld.rb
JavaScript instrument observed load of helloworld.rb
Hello from GraalVM Ruby!

source-tracing.jsスクリプトはJavaScriptで記述されたままであるため、--polyglotパラメータを使用してGraalVMのRubyランチャを起動する必要があります。

JavaScriptを使用したInsight

前の項で説明したように、GraalVM InsightはNode.jsのみに限定されるものではありません。GraalVMが提供するすべての言語ランタイムで使用できます。GraalVMに付属するJavaScriptの実装を試してください。

function-tracing.jsスクリプトを作成します:

var count = 0;
var next = 8;

insight.on('enter', function(ev) {
    if (count++ % next === 0) {
        print(`Just called ${ev.name} as ${count} function invocation`);
        next *= 2;
    }
}, {
    roots: true
});

sieve.jsの上で実行します。これは、エラトステネスの篩のバリアントを使用して10万個の素数を計算するサンプル・スクリプトです:

graalvm/bin/js --insight=function-tracing.js sieve.js | grep -v Computed
Just called :program as 1 function invocation
Just called Natural.next as 17 function invocation
Just called Natural.next as 33 function invocation
Just called Natural.next as 65 function invocation
Just called Natural.next as 129 function invocation
Just called Filter as 257 function invocation
Just called Natural.next as 513 function invocation
Just called Natural.next as 1025 function invocation
Just called Natural.next as 2049 function invocation
Just called Natural.next as 4097 function invocation

Pythonを使用したInsight

GraalVM言語をインストゥルメントできるだけでなく、Insightスクリプトをその言語で記述することもできます。この項では、Pythonの例を紹介します。

PythonでGraalVM Insightスクリプトを記述できます。このようなインサイトは、Pythonまたはその他の言語で記述されたプログラムに適用できます。

次に、関数minusOneがコールされると変数nの値を出力するスクリプトの例を示します。次のコードをagent.pyファイルに保存します:

def onEnter(ctx, frame):
    print(f"minusOne {frame.n}")

class At:
    sourcePath = ".*agent-fib.js"

class Roots:
    roots = True
    at = At()
    rootNameFilter = "minusOne"

insight.on("enter", onEnter, Roots())

次のコードは、GraalVM 22.2で導入されたソースの場所の宣言仕様を使用します。古いGraalVMバージョンで動的sourceFilterを使用します:

def onEnter(ctx, frame):
    print(f"minusOne {frame.n}")

class Roots:
    roots = True
    rootNameFilter = "minusOne"

    def sourceFilter(self, src):
        return src.name == "agent-fib.js"

insight.on("enter", onEnter, Roots())

次のコマンドを使用して、このスクリプトをagent-fib.jsに適用します:

`js --polyglot --insight=agent.py agent-fib.js`

ノート: gu install pythonでPythonのサポートがGraalVMに追加されていることを確認してください。

Rubyを使用したInsight

RubyでGraalVM Insightスクリプトを記述できます。このようなインサイトは、Rubyまたはその他の言語で記述されたプログラムに適用できます。

ノート: gu install rubyでRubyのサポートがGraalVMに追加されていることを確認してください。

source-tracing.rbスクリプトを作成します:

puts("Ruby: Insight version #{insight.version} is launching")

insight.on("source", -> (env) {
  puts "Ruby: observed loading of #{env.name}"
})
puts("Ruby: Hooks are ready!")

Node.jsアプリケーションを起動し、Rubyスクリプトを使用してインストゥルメントします:

graalvm/bin/node --js.print --experimental-options --polyglot --insight=source-tracing.rb agent-fib.js
Ruby: Initializing GraalVM Insight script
Ruby: Hooks are ready!
Ruby: observed loading of node:internal/errors
Ruby: observed loading of node:internal/util
Ruby: observed loading of node:events
....
Ruby: observed loading of node:internal/modules/run_main
Ruby: observed loading of <...>/agent-fib.js
Three is the result 3

変数値を追跡するには、agent.rbスクリプトを作成します:

insight.on("enter", -> (ctx, frame) {
    puts("minusOne #{frame.n}")
}, {
  roots: true,
  rootNameFilter: "minusOne",
  at: {
    sourcePath: ".*agent-fib.js"
  }
})

次のコードは、GraalVM 22.2で導入されたソースの場所の宣言仕様を使用します。古いGraalVMバージョンで動的sourceFilterを使用します:

insight.on("enter", -> (ctx, frame) {
    puts("minusOne #{frame.n}")
}, {
  roots: true,
  rootNameFilter: "minusOne",
  sourceFilter: -> (src) {
    return src.name == Dir.pwd+"/agent-fib.js"
  }
})

前述のRubyスクリプトの例では、agent-fib.jsプログラムの関数minusOneがコールされると、変数nの値が出力されます:

graalvm/bin/node --js.print --experimental-options --polyglot --insight=agent.rb agent-fib.js
minusOne 4
minusOne 3
minusOne 2
minusOne 2
Three is the result 3

Rを使用したInsight

同じインストゥルメントをR言語で記述できます。

agent-r.Rスクリプトを作成します:

cat("R: Initializing GraalVM Insight script\n")

insight@on('source', function(env) {
    cat("R: observed loading of ", env$name, "\n")
})

cat("R: Hooks are ready!\n")

これを使用して、test.Rプログラムをトレースします:

graalvm/bin/Rscript --insight=agent-r.R test.R
R: Initializing GraalVM Insight script
R: Hooks are ready!
R: observed loading of test.R

唯一の変更点はR言語であることです。他のすべてのGraalVM Insightの機能およびAPIは同じままです。

CコードへのInsightの追加

動的言語を解釈できるだけでなく、GraalVMのLLI実装により、CC++FortranRustなどで記述された、静的にコンパイルされたプログラムも混在させることができます。

たとえば、sieve.cなどのプログラムが長時間実行されているとします。ここには、mainメソッドに終了しないforループが含まれています。それに実行割当てを行います。

最初に、GraalVMでプログラムを実行します:

export TOOLCHAIN_PATH=`graalvm/bin/lli --print-toolchain-path`
${TOOLCHAIN_PATH}/clang agent-sieve.c -lm -o sieve
graalvm/bin/lli sieve

GraalVMのclangラッパーは、通常のネイティブ・コードに沿って、sieve実行ファイルにLLVMビットコード情報を保持するように通常のclangに指示する特別なオプションを追加します。その後、GraalVMのlliインタプリタは、ビットコードを使用して、プログラムをフル・スピードで解釈できます。ちなみに、./sieveによる直接ネイティブ実行の結果と、graalvm/bin/lli sieveのインタプリタ速度を比較してください。インタプリタに関しては、かなり良い結果になるはずです。

ここで、無限ループを断ち切ることに焦点を当てます。次のJavaScript agent-limit.js Insightスクリプトを使用して実行できます:

var counter = 0;

insight.on('enter', function(ctx, frame) {
    if (++counter === 1000) {
        throw `GraalVM Insight: ${ctx.name} method called ${counter} times. enough!`;
    }
}, {
    roots: true,
    rootNameFilter: 'nextNatural'
});

このスクリプトは、CのnextNatural関数の呼出し数をカウントし、関数が1000回呼び出されると、エラーを発行してsieveの実行を終了します。次のようにプログラムを実行します:

graalvm/bin/lli --polyglot --insight=agent-limit.js sieve
Computed 97 primes in 181 ms. Last one is 509
GraalVM Insight: nextNatural method called 1000 times. enough!
        at <js> :anonymous(<eval>:7:117-185)
        at <llvm> nextNatural(agent-sieve.c:14:186-221)
        at <llvm> nextPrime(agent-sieve.c:74:1409)
        at <llvm> measure(agent-sieve.c:104:1955)
        at <llvm> main(agent-sieve.c:123:2452)

ネイティブ・コードからプリミティブ・ローカル変数にアクセスできます。前述のInsightスクリプトを次のように置き換えます:

insight.on('enter', function(ctx, frame) {
    print(`found new prime number ${frame.n}`);
}, {
    roots: true,
    rootNameFilter: (n) => n === 'newFilter'
});

新しいプライムがフィルタ・リストに追加されるたびに、メッセージを出力します:

graalvm/bin/lli --polyglot --insight=agent-limit.js sieve | head -n 3
found new prime number 2
found new prime number 3
found new prime number 5

lli、ポリグロットおよびGraalVM Insightを組み合せると、ネイティブ・プログラムのトレース、制御、および対話型デバッグまたはバッチ・デバッグに大きな可能性が広がります。

値の検査

GraalVM Insightでは、プログラムが実行されている場所をトレースできるだけでなく、実行中にローカル変数および関数の引数の値にアクセスすることもできます。たとえば、関数fibの引数nの値を示すインストゥルメントを作成できます:

insight.on('enter', function(ctx, frame) {
   print('fib for ' + frame.n);
}, {
   roots: true,
   rootNameFilter: 'fib'
});

このインストゥルメントは、2番目の関数の引数frameを使用して、インストゥルメントされたすべての関数内のローカル変数の値にアクセスします。前述のInsightスクリプトでは、rootNameFilterを使用して、そのフックをfibという名前の関数にのみ適用しています:

function fib(n) {
  if (n < 1) return 0;
  if (n < 2) return 1;
  else return fib(n - 1) + fib(n - 2);
}
print("Two is the result " + fib(3));

インストゥルメントがfib-trace.jsファイルに格納されており、実際のコードがfib.jsに格納されている場合、次のコマンドを起動すると、プログラムの実行および関数の呼出し間で渡されるパラメータに関する詳細情報が生成されます:

graalvm/bin/node --js.print --insight=fib-trace.js fib.js
fib for 3
fib for 2
fib for 1
fib for 0
fib for 1
Two is the result 2

この項をまとめると、GraalVM Insightは、ポリグロットで言語に依存しないアスペクト指向プログラミングに役立つツールです。

ローカル変数の変更

GraalVM Insightは、ローカル変数にアクセスできるだけでなく、それを変更することもできます。たとえば、配列を合計する次のプログラムを考えてみます:

function plus(a, b) {
  return a + b;
}

var sum = 0;
[1, 2, 3, 4, 5, 6, 7, 8, 9].forEach((n) => sum = plus(sum, n));
print(sum);

数値45が出力されます。次のInsightスクリプトを適用して、偶数でない数値を加算する前に消去します:

insight.on('enter', function zeroNonEvenNumbers(ctx, frame) {
    if (frame.b % 2 === 1) {
        frame.b = 0;
    }
}, {
    roots: true,
    rootNameFilter: 'plus'
});

js --insight=erase.js sumarray.jsで起動すると、値20のみが出力されます。

GraalVM Insightのenterおよびreturnフックは、既存の変数のみを変更できます。これらは、新しいものを導入することはできません。そうしようとすると、例外が発生します。

特定の場所へのインサイト

特定のコードの場所の変数を取得するために、atオブジェクトには、必須のソース指定(ソース・ファイル・パスに一致する正規表現を持つsourcePathプロパティ、またはソースURIの文字列表現を持つsourceURIプロパティ)のいずれか以外も含めることができます。オプションのlineまたはcolumn(あるいはその両方)を指定することもできます。distance.jsソース・ファイルを見てみましょう:

(function(x, y) {
    let x2 = x*x;
    let y2 = y*y;
    let d = Math.sqrt(x2 + y2);
    for (let i = 0; i < d; i++) {
        // ...
    }
    return d;
})(3, 4);

次に、次のdistance-trace.jsインサイト・スクリプトを適用して、変数の値を取得します:

insight.on('enter', function(ctx, frame) {
    print("Squares: " + frame.x2 + ", " + frame.y2);
}, {
    statements: true,
    at: {
        sourcePath: ".*distance.js",
        line: 4
    }
});

insight.on('enter', function(ctx, frame) {
    print("Loop var i = " + frame.i);
}, {
    expressions: true,
    at: {
        sourcePath: ".*distance.js",
        line: 5,
        column: 21
    }
});

出力は次のとおりです:

graalvm/bin/js --insight=distance-trace.js distance.js
Squares: 9, 16
Loop var i = 0
Loop var i = 1
Loop var i = 2
Loop var i = 3
Loop var i = 4
Loop var i = 5

Node.JSでのInsight初期化の遅延

GraalVM Insightは、node実装を含む任意のGraalVM言語ランタイムで使用できます。ただし、nodeには、プレーンなInsightスクリプトを記述することを望んでいません。モジュールを含めて、nodeエコシステムを最大限に使用することを推奨します。これを行うagent-require.jsスクリプトのサンプルを次に示します:

let initialize = function (require) {
    let http = require("http");
    print(`${typeof http.createServer} http.createServer is available to the agent`);
}

let waitForRequire = function (event) {
  if (typeof process === 'object' && process.mainModule && process.mainModule.require) {
    insight.off('source', waitForRequire);
    initialize(process.mainModule.require.bind(process.mainModule));
  }
};

insight.on('source', waitForRequire, { roots: true });

Insightスクリプトはできるかぎり早く初期化され、その時点でrequire関数はまだ準備ができていません。そのため、このスクリプトは、まずロードされたスクリプトにリスナーをアタッチし、メイン・ユーザー・スクリプトがロードされているときに、そのprocess.mainModule.require関数を取得します。次に、insight.offを使用してプローブを削除し、実際のinitialize関数を呼び出して、すべてのノード・モジュールにアクセスしながら実際の初期化を実行します。スクリプトは、次を使用して実行できます:

graalvm/bin/node --js.print --insight=agent-require.js yourScript.js

この初期化シーケンスは、メインのyourScript.jsパラメータを使用して起動されたGraalVMのnodeバージョン12.10.0で動作することが知られています。

例外の処理

GraalVM Insightインストゥルメントは、例外をスローでき、その例外は周囲のユーザー・スクリプトに伝播されます。様々なメッセージを記録するプログラムseq.jsがあるとします:

function log(msg) {
    print(msg);
}

log('Hello GraalVM Insight!');
log('How');
log('are');
log('You?');

記録されたメッセージを監視して、インストゥルメントterm.jsを登録し、seq.jsプログラム実行の途中で実行を終了できます:

insight.on('enter', (ev, frame) => {
    if (frame.msg === 'are') {
        throw 'great you are!';
    }
}, {
    roots: true,
    rootNameFilter: 'log'
});

term.jsインストゥルメントは、メッセージareを含むlog関数のコールを待機し、その時点で、ユーザー・プログラムの実行を効果的に中断して独自の例外を生成します。その結果、次のようになります:

graalvm/bin/js --polyglot --insight=term.js seq.js
Hello GraalVM Insight!
How
great you are!
        at <js> :=>(term.js:3:75-97)
        at <js> log(seq.js:1-3:18-36)
        at <js> :program(seq.js:7:74-83)

Insightインストゥルメントによって生成された例外は、通常の言語例外として扱われます。seq.jsプログラムは、通常のtry { ... } catch (e) { ... }ブロックを使用してそれらを捕捉し、通常のユーザー・コードによって生成されたかのように処理できます。

実行のインターセプトおよび変更

GraalVM Insightでは、プログラムの実行を変更できます。特定の計算をスキップし、独自の代替計算に置き換えることができます。例として、次のplus関数を示します:

function plus(a, b) {
    return a + b;
}

plusメソッドの動作は簡単に変更できます。次のInsightスクリプトは、ctx.returnNow機能を使用して、+演算を乗算に置き換えます:

insight.on('enter', function(ctx, frame) {
    ctx.returnNow(frame.a * frame.b);
}, {
    roots: true,
    rootNameFilter: 'plus'
});

returnNowメソッドは、すぐに実行を停止し、plus関数のコール元に戻ります。plusメソッドの本体がまったく実行されないのは、たとえば実際の関数の本体が実行される前にインサイトon('enter', ...)が適用されたためです。2つの数値を加算するかわりに乗算することは、あまり魅力的ではないと思われるかもしれませんが、同じアプローチは、関数呼出しを繰り返すアドオン・キャッシュ(メモ化など)の提供に役立ちます。

また、元の関数コードを実行して、その結果の変更のみを行うことも可能です。たとえば、plus関数の結果が常に非負数になるように変更します:

insight.on('return', function(ctx, frame) {
    let result = ctx.returnValue(frame);
    ctx.returnNow(Math.abs(result));
}, {
    roots: true,
    rootNameFilter: 'plus'
});

Insightフックは、plus関数の戻り時に実行され、returnValueヘルパー関数を使用して、現在のframeオブジェクトから計算された戻り値を取得します。その後、値を変更でき、かわりにreturnNowによって新しい結果が返されます。returnValue関数は、提供されたctxオブジェクトで常に使用できますが、on('return', ...)フックで使用される場合にのみ意味のある値を返します。

最小限のオーバヘッド

スクリプトの適用時にGraalVM Insightでパフォーマンスのオーバーヘッドが発生するかというと、答えは「いいえ」または「最小限」になります。オーバーヘッドはスクリプトの動作によって異なります。複雑な計算をコード・ベース全体に追加して分散すると、その計算の代価を支払います。ただし、これはインストゥルメンテーションではなくコードのオーバーヘッドになります。単純なfunction-count.jsスクリプトを使用して、オーバーヘッドを測定します。

var count = 0;
function dumpCount() {
    print(`${count} functions have been executed`);
}

insight.on('enter', function(ev) {
    count++;
}, {
    roots: true
});

insight.on('close', dumpCount);

sieve.jsサンプルの50回の反復でスクリプトを使用します。このサンプルは、エラトステネスの篩のバリアントを使用して10万個の素数を計算します。計算を50回繰り返すと、ランタイムがウォームアップし、適切に最適化できます。最適な実行は次のとおりです:

graalvm/bin/js sieve.js | grep -v Computed
Hundred thousand prime numbers in 75 ms
Hundred thousand prime numbers in 73 ms
Hundred thousand prime numbers in 73 ms

ここで、GraalVM Insightスクリプトを有効にして実行した場合の実行時間と比較します:

graalvm/bin/js --insight=function-count.js sieve.js  | grep -v Computed
Hundred thousand prime numbers in 74 ms
Hundred thousand prime numbers in 74 ms
Hundred thousand prime numbers in 75 ms
72784921 functions have been executed

違いは2ミリ秒です。GraalVM Insightでは、アプリケーション・コードとインサイト収集スクリプトの違いが融合され、すべてのコードが1つのものとして機能します。count++の呼出しは、アプリケーション関数のROOTを表すすべての場所で、アプリケーションに自然に含まれるものとなります。

ローカルへのアクセス時の最小限のオーバヘッド

GraalVM Insightは、ほぼ無料でローカル変数にアクセスできます。ローカル変数にアクセスするGraalVM Insightコードは、変数を定義する実際の関数コードと融合され、目に見える速度低下はありません。

このことは、10万個の素数を計算する次のsieve.jsアルゴリズムで示すことができます。見つかった素数は、次の関数を使用して作成されたリンク・リストに保持されます:

function Filter(number) {
    this.number = number;
    this.next = null;
    this.last = this;
}

まず、計算を50回呼び出し、最後のラウンドを終了するまでの時間を測定して、動作をテストします:

graalvm/bin/js -e "var count=50" --file sieve.js | grep Hundred | tail -n 1
Hundred thousand prime numbers in 73 ms

次に、new Filterコンストラクタのコールなど、新しい素数スロットの各割当てを監視することにより、システムを「困らせ」ます:

var sum = 0;
var max = 0;

insight.on('enter', (ctx, frame) => {
    sum += frame.number;
    if (frame.number > max) {
        max = frame.number;
    }
}, {
  roots: true,
  rootNameFilter: 'Filter'
});

insight.on('return', (ctx, frame) => {
    log(`Hundred thousand prime numbers from 2 to ${max} has sum ${sum}`);
    sum = 0;
    max = 0;
}, {
    roots: true,
    rootNameFilter: 'measure'
});

new Filter(number)が割り当てられるたびに、numberの最大値(見つかった最大素数など)が取得され、これまでに見つかったすべての素数のsumも取得されます。measureのメイン・ループが終了すると、たとえば10万個の素数がある場合、その結果が出力されます。

ここで、次を試します:

graalvm/bin/js  -e "var count=50" --insight=sieve-filter1.js --file sieve.js | grep Hundred | tail -n 2
Hundred thousand prime numbers from 2 to 1299709 has sum 62260698721
Hundred thousand prime numbers in 74 ms

速度低下はまったくありません。GraalVM Insightは、GraalVMコンパイラのインライン化アルゴリズムと組み合せると、パフォーマンスをほとんど低下させることなく、優れたインストゥルメンテーション機能を実現します。

実行スタックへのアクセス

GraalVM Insightで実行スタック全体にアクセスする方法があります。次のコード・スニペットは、その方法を示しています:

insight.on("return", function(ctx, frame) {
  print("dumping locals");
  ctx.iterateFrames((at, vars) => {
      for (let p in vars) {
          print(`    at ${at.name} (${at.source.name}:${at.line}:${at.column}) ${p} has value ${vars[p]}`);
      }
  });
  print("end of locals");
}, {
  roots: true
});

Insightフックがトリガーされるたびに、関数source.namelineおよびcolumnnameを使用して現在の実行スタックが出力されます。さらに、各フレームですべてのローカルvarsの値も出力されます。また、次のように既存の変数に新しい値を割り当てることで、既存の変数の値を変更することもできます: vars.n = 42。スタック全体へのアクセスは柔軟ですが、現在の実行フレーム内のローカルへのアクセスとは異なり、高速な操作ではないため、プログラムを引き続きフル・スピードで実行する場合は、よく考えて使用してください。

ヒープ・ダンプ

GraalVM Insightを使用すると、実行中にプログラム・ヒープのリージョンのスナップショットを作成できます。--heap.dump=/path/to/output.hprofオプションを通常の --insightオプションと一緒に使用します。Insightスクリプトは、dump関数を使用してheapオブジェクトにアクセスします。必要な場所にフックを配置し、適切なタイミングでヒープをダンプします:

insight.on('return', (ctx, frame) => {
    heap.dump({
        format: '1.0',
        depth: 50, // set max depth for traversing object references
        events: [
            {
                stack : [
                    {
                        at : ctx, // location of dump sieve.js:73
                        frame : {
                            // assemble frame content as you want
                            primes : frame.primes, // capture primes object
                            cnt : frame.cnt, // capture cnt value
                        },
                        depth : 10 // optionally override depth to ten references
                    }, // there can be more stack elements like this one
                ]
            },
            // there can be multiple events like the previous one
        ],
    });
    throw 'Heap dump written!';
}, {
    roots: true,
    rootNameFilter: 'measure'
});

コード・スニペットをdump.jsファイルとして保存します。sieve.jsファイルを取得し、次のように起動します:

graalvm/bin/js --insight=dump.js --heap.dump=dump.hprof --file sieve.js

ヒープ・スタック

dump.hprofファイルは、プログラム・メモリーの状態を取得するmeasure関数の最後に作成されます。生成された.hprofファイルをVisualVMNetBeansなどの通常のツールで調べます:

ヒープ検査

前の図は、sieve.jsスクリプトのmeasure関数の最後で取得されたヒープ・ダンプを示しています。この関数は、10万個(変数cntで使用可能な数)の素数を計算しました。この図は、2から17までの素数を保持するリンク・リストFilterを示しています。リンク・リストの残りの部分は、unreachableオブジェクトの背後に隠されています(深さ10までの参照のみがリクエストされました)。最後の変数xは、すべての素数を計算するために検索された自然数の数を示しています。

ヒープ・ダンプ・キャッシュ

ヒープ・ダンプ・プロセスを高速化し、結果のダンプを最適化するために、メモリー・キャッシュを有効にできます。キャッシュへのダンプ間でプロパティが変更されないオブジェクトは1回のみ格納され、結果として生成されるヒープ・ダンプ・サイズが削減されます。たとえば、--heap.cacheSize=1000オプションを追加すると、1000イベントのメモリー・キャッシュが使用されます。デフォルトでは、キャッシュはファイルにダンプされ、いっぱいになると消去されます。このポリシーは、--heap.cacheReplacement=lruオプションによって変更できます。これにより、キャッシュ・サイズ制限に達すると、キャッシュ内の最新のダンプ・イベントは保持され、最も古いものが削除されます。

キャッシュをヒープ・ダンプ・ファイルにフラッシュするには、heap.flush()を明示的にコールする必要があります。

GraalVM Insight APIに関するノート

insightオブジェクトを介して公開されるGraalVM Insight APIは、互換性のある方法で実装されます。GraalVM Insight APIは、このリンクで確認できます。insightオブジェクトのプロパティおよび関数は、そのjavadocの一部として提供されています。

将来のバージョンでは新機能が追加されますが、一度公開されたものはすべて引き続き機能します。スクリプトがいくつかの新機能に依存している場合、公開されているAPIのバージョンを確認することがあります:

print(`GraalVM Insight version is ${insight.version}`);

API内の新しい要素には、関連する@sinceタグが付けられ、関連付けられた機能が使用可能になる最小バージョンが記述されます。