Experimental feature in GraalVM

Insight マニュアル

GraalVM Insight は、信頼性の高いアプリケーションを作成するための多目的で柔軟なツールです。このツールの動的な性質により、既存のアプリケーションにトレースポイントカットを選択的に適用でき、パフォーマンスの低下もありません。

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

目次 #

クイックスタート #

必須の **HelloWorld** の例から始めましょう。次の内容で *source-tracing.js* という名前のスクリプトを作成します。

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

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

./bin/node --js.print --experimental-options --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 オブジェクトを使用して、ランタイムに **source** リスナーをアタッチしました。そのため、node がスクリプトをロードするたびに、リスナーはそれを通知され、アクションを実行できました(この場合は、処理されたスクリプトの長さと名前を出力します)。

Hotness Top 10 の例 #

洞察情報の収集は、print 文に限定されません。言語でチューリング完全な計算を実行できます。たとえば、すべてのメソッド呼び出しをカウントし、実行が終了したときに最も頻繁に呼び出されたメソッドをダンプするプログラムです。

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

var map = new Map();

function dumpHotness() {
    print("==== Hotness Top 10 ====");
    var count = 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 (count-- > 0) 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) を介して登録されます)。プログラムを実行します。

./bin/node --js.print --experimental-options --insight=function-hotness-tracing.js -e "print('The result: ' + 6 * 7)"
The result: 42
==== Hotness Top 10 ====
516 calls to isPosixPathSeparator
311 calls to :=>
269 calls to E
263 calls to makeNodeErrorWithCode
159 calls to :anonymous
157 calls to :program
 58 calls to getOptionValue
 58 calls to getCLIOptionsFromBinding
 48 calls to validateString
 43 calls to hideStackFrames
========================

node プロセスが終了すると、関数呼び出しの名前とカウントを含むテーブルが出力されます。

あらゆる GraalVM 言語に Insight を適用する #

前の例は 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!'

注:Ruby サポートが有効になっていることを確認してください。多言語プログラミングガイドを参照してください。

JavaScript インストラメントを Ruby プログラムに適用します。表示される内容は次のとおりです。

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

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

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* の上で実行します。これは、エラトステネスのふるいの変種を使用して 100,000 個の素数を計算するサンプルスクリプトです。

./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 の例を示します。

GraalVM Insight スクリプトを Python で記述できます。このような洞察は、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* に適用します。

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

注:Python サポートが有効になっていることを確認してください。多言語プログラミングガイドを参照してください。

Ruby での Insight #

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

注:Ruby サポートが有効になっていることを確認してください。多言語プログラミングガイドを参照してください。

*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 スクリプトでインストルメントします。

./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 の値を出力します。

./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* プログラムをトレースします。

./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 実装を使用すると、**C**、**C++**、**Fortran**、**Rust** などで記述された静的にコンパイルされたプログラムでさえ混在させることができます。

たとえば、 *sieve.c* のような長時間実行されるプログラムを考えてみましょう。これは、main メソッドに無限の for ループが含まれています。 これに実行クォータを与えたいとします。

まず、GraalVM でプログラムを実行します。

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

GraalVM clang ラッパーは、通常の clang に LLVM ビットコード情報を通常のネイティブコードとともに sieve 実行可能ファイルに保持するように指示する特別なオプションを追加します。GraalVM の lli インタプリタは、ビットコードを使用してプログラムをフルスピードで解釈できます。ちなみに、./sieve による直接ネイティブ実行の結果と ./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 実行を停止します。次のようにプログラムを実行します。

./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'
});

新しい素数がフィルターリストに追加されるたびにメッセージを出力します。

./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* に格納されている場合、次のコマンドを呼び出すと、プログラムの実行と関数呼び出しの間で渡されるパラメーターに関する詳細情報が得られます。

./bin/node --js.print --experimental-options --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 フックは、既存の変数を変更することのみ可能です。新しい変数を導入することはできません。そうしようとすると例外が発生します。

特定の場所への Insight #

特定のコードの場所にある変数にアクセスするには、at オブジェクトに必須のソース指定のいずれか 1 つだけを含めることができます。ソースファイルパスと一致する正規表現を持つ 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
    }
});

それは私たちに与えます

./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関数を呼び出して、すべてのノードモジュールにアクセスしながら実際の初期化を実行します。スクリプトは以下のように実行できます。

./bin/node --js.print --experimental-options --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関数への呼び出しを待機し、その時点で独自の例外を発行して、ユーザープログラムの実行を効果的に中断します。その結果、以下が得られます。

./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関数の呼び出し元に戻ります。インサイトon('enter', ...)が適用されたため、たとえば、関数の実際の本体が実行される前に、plusメソッドの本体はまったく実行されません。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回繰り返すと、ランタイムはウォームアップして適切に最適化することができます。最適な実行は次のとおりです。

./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スクリプトを有効にして実行した場合の実行時間と比較します。

./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回呼び出して、最後のラウンドが完了するまでの時間を測定することで、動作をテストします。

./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万個の素数があることを意味します)、結果が出力されます。

次に、以下を試してください。

./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フックがトリガーされるたびに、関数のnamesource.namelinecolumnを含む現在の 実行スタックが出力されます。さらに、各フレームですべてのローカルvarsの値も出力します。既存の変数の値に新しい値を代入することで、値を変更することもできます。vars.n = 42。スタック全体へのアクセスは柔軟ですが、現在の 実行フレーム内のローカル変数へのアクセスとは異なり、高速な操作ではないため、プログラムをフルスピードで実行し続けたい場合は、賢明に使用してください。

ヒープダンプ #

GraalVM Insightを使用して、実行中にプログラムヒープの領域のスナップショットを作成できます。通常の--insightオプションと一緒に--heap.dump=/path/to/output.hprofオプションを使用します。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ファイルを取得し、次のように起動します。

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

Heap Stack

dump.hprofファイルは、measure関数の最後に作成され、プログラムメモリの状態がキャプチャされます。生成された.hprofファイルは、VisualVMNetBeansなどの通常のツールで検査します。

Heap Inspect

前の図は、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タグが付いています。

お問い合わせ