これは、なにをしたくて書いたもの?
前に、Virtual Threadsに関するエントリーを書きました。
Java 21で正式版になったJEP 444(Virtual Threads)に関するAPIを試す - CLOVER🍀
Virtual Threadsを使ってHTTPサーバー/クライアントを書いて、スレッドまわりの動きを確認してみる(スレッドダンプの取得付き) - CLOVER🍀
この中で、pinning(ピン留め)についての確認を少し忘れていたので続きをやろうと思います。
Virtual Threadsのpinning(ピン留め)
JEP 444(Virtual Threads)についてはこちら。
Virtual Threadsについては、あらためては説明しません。
こちらのエントリーを参照、ということで。
Java 21で正式版になったJEP 444(Virtual Threads)に関するAPIを試す - CLOVER🍀
pinning(ピン留め)は、プラットフォームスレッドから仮想スレッドをアンマウントできない状態のことを言います。
以下のケースがあるようです。
- When it executes code inside a synchronized block or method(
synchronized
ブロックやsynchronized
メソッド内のコードを実行している時) - When it executes a native method or a foreign function(ネイティブメソッドやJEP 424のForeign Functionを実行している時)
pinning(ピン留め)が発生してもアプリケーションがおかしくなることはありませんが、スケーラビリティが代償になる可能性があります。 数に限りのあるプラットフォームスレッドがひとつの仮想スレッドに縛られて、他の仮想スレッドを動かせず数が減ってしまうからですね。
これを回避するには、synchronized
ブロックおよびsynchronized
メソッドを使わなくて済むようにすることはもちろんですが、
ロックが必要な場合はReentrantLock
を使うように修正することです。
そして、synchronized
ブロックでアンマウントできなくなることは以下のエントリーで確認しています。
Virtual Threadsを使ってHTTPサーバー/クライアントを書いて、スレッドまわりの動きを確認してみる(スレッドダンプの取得付き) - CLOVER🍀
ReentrantLock
の利用であれば問題ないことも確認しています。
pinning(ピン留め)が発生したことは、以下の方法で記録できるようです。
- A JDK Flight Recorder (JFR) event is emitted when a thread blocks while pinned (see JDK Flight Recorder).
- The system property jdk.tracePinnedThreads triggers a stack trace when a thread blocks while pinned. Running with -Djdk.tracePinnedThreads=full prints a complete stack trace when a thread blocks while pinned, highlighting native frames and frames holding monitors. Running with -Djdk.tracePinnedThreads=short limits the output to just the problematic frames.
今回は、jdk.tracePinnedThreads
システムプロパティを試してみたいと思います。これは、pinning(ピン留め)が発生した時に
スタックトレースを出力するものです。
指定方法には2種類あるようです。
-Djdk.tracePinnedThreads=full
… pinning(ピン留め)が発生した時に、完全なスタックトレースとネイティブフレームおよびモニターを保持しているフレームが強調表示されるDjdk.tracePinnedThreads=short
… 問題のあるフレームのみが出力される
こちらのドキュメントにも記載があります。
コア・ライブラリ / 並行処理 / 仮想スレッド / 仮想スレッド: 採用ガイド / 長時間かつ頻繁な固定の回避
動作確認には、こちらで書いたHTTPサーバーを使うことにします。
Virtual Threadsを使ってHTTPサーバー/クライアントを書いて、スレッドまわりの動きを確認してみる(スレッドダンプの取得付き) - CLOVER🍀
では、試してみましょう。
環境
今回の環境はこちら。
$ java --version openjdk 21.0.2 2024-01-16 OpenJDK Runtime Environment (build 21.0.2+13-Ubuntu-122.04.1) OpenJDK 64-Bit Server VM (build 21.0.2+13-Ubuntu-122.04.1, mixed mode, sharing) $ mvn --version Apache Maven 3.9.6 (bc0240f3c744dd6b6ec2920b3cd08dcc295161ae) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 21.0.2, vendor: Private Build, runtime: /usr/lib/jvm/java-21-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.15.0-101-generic", arch: "amd64", family: "unix"
準備
Mavenの設定ですが、特に依存関係などは使いません。
<properties> <maven.compiler.release>21</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties>
ソースコードはこちら。
src/main/java/org/littlewings/virtualthreads/SimpleHttpServer.java
package org.littlewings.virtualthreads; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; import java.io.IOException; import java.io.OutputStream; import java.io.UncheckedIOException; import java.net.InetSocketAddress; import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Duration; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; public class SimpleHttpServer { private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("uuuu-MM-dd HH:mm:ss"); private HttpServer httpServer; SimpleHttpServer(HttpServer httpServer) { this.httpServer = httpServer; } public static void main(String... args) { String host; int port; if (args.length > 1) { host = args[0]; port = Integer.parseInt(args[1]); } else if (args.length > 0) { host = "localhost"; port = Integer.parseInt(args[0]); } else { host = "localhost"; port = 8080; } SimpleHttpServer simpleHttpServer = SimpleHttpServer.create(host, port); simpleHttpServer.start(); Runtime.getRuntime().addShutdownHook(Thread.ofPlatform().unstarted(simpleHttpServer::stop)); } private static void log(String message) { Thread currentThread = Thread.currentThread(); String threadName = currentThread.getName(); System.out.printf("[%s] - %s - %s%n", LocalDateTime.now().format(FORMATTER), threadName, message); } public static SimpleHttpServer create(String host, int port) { try { HttpServer httpServer = HttpServer.create(new InetSocketAddress(host, port), 0); log(String.format("jdk.virtualThreadScheduler.parallelism = %s", System.getProperty("jdk.virtualThreadScheduler.parallelism", ""))); log(String.format("jdk.virtualThreadScheduler.maxPoolSize = %s", System.getProperty("jdk.virtualThreadScheduler.maxPoolSize", ""))); log(String.format("jdk.tracePinnedThreads = %s", System.getProperty("jdk.tracePinnedThreads", ""))); // httpServer.setExecutor(Executors.newVirtualThreadPerTaskExecutor()); httpServer.setExecutor(Executors.newThreadPerTaskExecutor(Thread.ofVirtual().name("handler-", 1).factory())); httpServer.createContext("/", createHandler()); return new SimpleHttpServer(httpServer); } catch (IOException e) { throw new UncheckedIOException(e); } } public static SimpleHttpServer create(int port) { return create("localhost", port); } static HttpHandler createHandler() { ReentrantLock lock = new ReentrantLock(); ReentrantLock lock2 = new ReentrantLock(); Object synchronizedLockObject = new Object(); Object synchronizedLockObject2 = new Object(); return httpExchange -> { URI requestUri = httpExchange.getRequestURI(); String method = httpExchange.getRequestMethod(); String requestPath = requestUri.getPath(); log(String.format("access[%s:%s] start", method, requestPath)); Consumer<String> writeResponse = responseString -> { byte[] binary = responseString.getBytes(StandardCharsets.UTF_8); try { httpExchange.sendResponseHeaders(200, binary.length); try (OutputStream os = httpExchange.getResponseBody()) { os.write(binary); } } catch (IOException e) { throw new UncheckedIOException(e); } }; Duration sleepTime = Duration.ofSeconds(3L); switch (requestPath) { case "/sleep" -> { try { TimeUnit.SECONDS.sleep(sleepTime.toSeconds()); } catch (InterruptedException e) { // ignore } writeResponse.accept("sleep."); } case "/heavy" -> { long startTime = System.currentTimeMillis(); while (true) { for (int i = 0; i < 100000; i++) { // loop } long elapsedTime = System.currentTimeMillis() - startTime; if (elapsedTime > sleepTime.toMillis()) { break; } } writeResponse.accept("heavy."); } case "/lock" -> { try { lock.lock(); TimeUnit.SECONDS.sleep(sleepTime.toSeconds()); } catch (InterruptedException e) { // ignore } finally { lock.unlock(); } writeResponse.accept("lock."); } case "/lock2" -> { try { lock2.lock(); TimeUnit.SECONDS.sleep(sleepTime.toSeconds()); } catch (InterruptedException e) { // ignore } finally { lock2.unlock(); } writeResponse.accept("lock2."); } case "/synchronized-lock" -> { synchronized (synchronizedLockObject) { try { TimeUnit.SECONDS.sleep(sleepTime.toSeconds()); } catch (InterruptedException e) { // ignore } writeResponse.accept("synchronized lock."); } } case "/synchronized-lock2" -> { synchronized (synchronizedLockObject2) { try { TimeUnit.SECONDS.sleep(sleepTime.toSeconds()); } catch (InterruptedException e) { // ignore } writeResponse.accept("synchronized lock2."); } } default -> writeResponse.accept("Hello World."); } log(String.format("access[%s:%s] end", method, requestPath)); }; } public void start() { httpServer.start(); log(String.format("simple http server[%s:%d], started.", httpServer.getAddress().getHostString(), httpServer.getAddress().getPort())); } public void stop() { httpServer.stop(1); log(String.format("simple http server[%s:%d], shutdown.", httpServer.getAddress().getHostString(), httpServer.getAddress().getPort())); } }
基本的に、もとのエントリーと内容はほぼ変えていません。JDKのHttpServer
を使って、Virtual Threadsを使った簡単なHTTPサーバーを
書いています。
public static SimpleHttpServer create(String host, int port) { try { HttpServer httpServer = HttpServer.create(new InetSocketAddress(host, port), 0); log(String.format("jdk.virtualThreadScheduler.parallelism = %s", System.getProperty("jdk.virtualThreadScheduler.parallelism", ""))); log(String.format("jdk.virtualThreadScheduler.maxPoolSize = %s", System.getProperty("jdk.virtualThreadScheduler.maxPoolSize", ""))); log(String.format("jdk.tracePinnedThreads = %s", System.getProperty("jdk.tracePinnedThreads", ""))); // httpServer.setExecutor(Executors.newVirtualThreadPerTaskExecutor()); httpServer.setExecutor(Executors.newThreadPerTaskExecutor(Thread.ofVirtual().name("handler-", 1).factory())); httpServer.createContext("/", createHandler()); return new SimpleHttpServer(httpServer); } catch (IOException e) { throw new UncheckedIOException(e); } }
アクセスパスに応じて、以下の5種類の処理を行います。
/sleep
… 指定した秒数だけTimeUnit#sleep
でスリープ(ブロック操作の代わり)/heavy
… 指定した秒数だけループ(CPUを消費する処理)/lock
、/lock2
… それぞれ異なるReentrantLock
のインスタンスを使ってロックを取得し、指定した秒数だけTimeUnit#sleep
でスリープ/synchronized-lock
、/synchronized-lock2
… それぞれ異なるインスタンスに対してsynchronized
でロックを取得し、指定した秒数だけTimeUnit#sleepでスリープ- それ以外のパス … 即座に
Hello World.
を返す
アクセスパスごとの処理は、case
になっています。
この中でpinning(ピン留め)が発生しそうなのは、/synchronized-lock
、/synchronized-lock2
かなと思います。/heavy
も仮想スレッドが
束縛され続けますが、どうなるんでしょうね?
では、動かしてみましょう。
確認してみる
まずはコンパイル。
$ mvn compile
起動は、以下のコマンドで行います。
$ java \ -Djdk.virtualThreadScheduler.parallelism=1 \ -Djdk.virtualThreadScheduler.maxPoolSize=1 \ -Djdk.tracePinnedThreads=[full or short] \ -cp target/classes \ org.littlewings.virtualthreads.SimpleHttpServer
システムプロパティjdk.virtualThreadScheduler.parallelism
とjdk.virtualThreadScheduler.maxPoolSize
を
1にして並列度および利用可能なプラットフォームスレッドの数を1にしています。
-Djdk.tracePinnedThreads=full
と-Djdk.tracePinnedThreads=short
でそれぞれ確認してみましょう。
-Djdk.tracePinnedThreads=full
最初は、-Djdk.tracePinnedThreads=full
で確認してみましょう。
$ java \ -Djdk.virtualThreadScheduler.parallelism=1 \ -Djdk.virtualThreadScheduler.maxPoolSize=1 \ -Djdk.tracePinnedThreads=full \ -cp target/classes \ org.littlewings.virtualthreads.SimpleHttpServer
各アクセスパスごとに確認してみます。
/sleep
TimeUnit#sleep
でスリープする、/sleep
にアクセス。2つのリクエストは、別々のターミナルで連続で実行しています。
## ひとつ目 $ time curl localhost:8080/sleep sleep. real 0m3.277s user 0m0.014s sys 0m0.000s ## 2つ目 $ time curl localhost:8080/sleep sleep. real 0m3.013s user 0m0.008s sys 0m0.004s
HTTPサーバー側のログ上は、変わったところはありません。
[2024-04-03 23:09:24] - handler-1 - access[GET:/sleep] start [2024-04-03 23:09:24] - handler-2 - access[GET:/sleep] start [2024-04-03 23:09:27] - handler-1 - access[GET:/sleep] end [2024-04-03 23:09:27] - handler-2 - access[GET:/sleep] end
/heavy
空ループを回してCPUを消費する/heavy
。
## ひとつ目 $ time curl localhost:8080/heavy heavy. real 0m3.014s user 0m0.006s sys 0m0.005s ## 2つ目 $ time curl localhost:8080/heavy heavy. real 0m5.642s user 0m0.004s sys 0m0.003s
こちらのログも、変わったところはありませんでした。
[2024-04-03 23:13:26] - handler-5 - access[GET:/heavy] start [2024-04-03 23:13:29] - handler-5 - access[GET:/heavy] end [2024-04-03 23:13:29] - handler-6 - access[GET:/heavy] start [2024-04-03 23:13:32] - handler-6 - access[GET:/heavy] end
/lock
ReentrantLock
を使う/lock
。
## ひとつ目 $ time curl localhost:8080/lock lock. real 0m3.012s user 0m0.001s sys 0m0.009s ## 2つ目 $ time curl localhost:8080/lock lock. real 0m5.662s user 0m0.006s sys 0m0.005s
ログ。
[2024-04-03 23:26:15] - handler-9 - access[GET:/lock] start [2024-04-03 23:26:16] - handler-10 - access[GET:/lock] start [2024-04-03 23:26:18] - handler-9 - access[GET:/lock] end [2024-04-03 23:26:21] - handler-10 - access[GET:/lock] end
ReentrantLock
を使っているので問題ないですね。/lock2
はもう飛ばすことにします。
/synchronized-lock
synchronized
でロックを取る、/synchronized-lock
を試してみます。
## ひとつ目 $ time curl localhost:8080/synchronized-lock synchronized lock. real 0m3.013s user 0m0.008s sys 0m0.003s ## 2つ目 $ time curl localhost:8080/synchronized-lock synchronized lock. real 0m5.543s user 0m0.001s sys 0m0.009s
ログはこうなりました。
[2024-04-03 23:27:36] - handler-13 - access[GET:/synchronized-lock] start Thread[#39,ForkJoinPool-1-worker-2,5,CarrierThreads] java.base/java.lang.VirtualThread$VThreadContinuation.onPinned(VirtualThread.java:183) java.base/jdk.internal.vm.Continuation.onPinned0(Continuation.java:393) java.base/java.lang.VirtualThread.parkNanos(VirtualThread.java:621) java.base/java.lang.VirtualThread.sleepNanos(VirtualThread.java:791) java.base/java.lang.Thread.sleep(Thread.java:556) java.base/java.util.concurrent.TimeUnit.sleep(TimeUnit.java:446) org.littlewings.virtualthreads.SimpleHttpServer.lambda$createHandler$1(SimpleHttpServer.java:160) <== monitors:1 jdk.httpserver/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:98) jdk.httpserver/sun.net.httpserver.AuthFilter.doFilter(AuthFilter.java:82) jdk.httpserver/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:101) jdk.httpserver/sun.net.httpserver.ServerImpl$Exchange$LinkHandler.handle(ServerImpl.java:871) jdk.httpserver/com.sun.net.httpserver.Filter$Chain.doFilter(Filter.java:98) jdk.httpserver/sun.net.httpserver.ServerImpl$Exchange.run(ServerImpl.java:847) java.base/java.util.concurrent.ThreadPerTaskExecutor$TaskRunner.run(ThreadPerTaskExecutor.java:314) java.base/java.lang.VirtualThread.run(VirtualThread.java:309) [2024-04-03 23:27:39] - handler-13 - access[GET:/synchronized-lock] end [2024-04-03 23:27:39] - handler-14 - access[GET:/synchronized-lock] start [2024-04-03 23:27:42] - handler-14 - access[GET:/synchronized-lock] end
なるほど、確かにスタックトレースが出力されています。しかも、どこでsynchronized
に入っているかがわかるんですね(monitors
)。
一定時間が経過してから出力される、というより即出力されるようですが、連続で実行した時の2回目の方はスタックトレースが出力されて
いないんですよね。さらに追加で実行しても出力されませんでした。これはどうしてなのでしょう?
この理由は、あとで調べてみました。
/synchronized-lock2
も飛ばすことにします。
-Djdk.tracePinnedThreads=short
次はHTTPサーバーを再起動して、-Djdk.tracePinnedThreads=short
で確認してみましょう。
$ java \ -Djdk.virtualThreadScheduler.parallelism=1 \ -Djdk.virtualThreadScheduler.maxPoolSize=1 \ -Djdk.tracePinnedThreads=short \ -cp target/classes \ org.littlewings.virtualthreads.SimpleHttpServer
/synchronized-lock
もう結果がわかっているので、synchronized
でロックを取る/synchronized-lock
のみで試します。
## ひとつ目 $ time curl localhost:8080/synchronized-lock synchronized lock. real 0m3.090s user 0m0.005s sys 0m0.004s ## 2つ目 $ time curl localhost:8080/synchronized-lock synchronized lock. real 0m5.700s user 0m0.008s sys 0m0.003s
ログはこのようになりました。スタックトレースが非常にシンプルというか、問題になった箇所のみが出力されています。
[2024-04-03 23:31:08] - handler-1 - access[GET:/synchronized-lock] start Thread[#25,ForkJoinPool-1-worker-1,5,CarrierThreads] org.littlewings.virtualthreads.SimpleHttpServer.lambda$createHandler$1(SimpleHttpServer.java:160) <== monitors:1 [2024-04-03 23:31:11] - handler-1 - access[GET:/synchronized-lock] end [2024-04-03 23:31:11] - handler-2 - access[GET:/synchronized-lock] start [2024-04-03 23:31:14] - handler-2 - access[GET:/synchronized-lock] end
確かにshort
です。
これで、システムプロパティjdk.tracePinnedThreads
の効果がわかりましたね。
もう少し
ここからは、少し実装を見てみましょう。
システムプロパティjdk.tracePinnedThreadsの参照箇所
こちらで見ているようです。
private static int tracePinningMode() { String propValue = GetPropertyAction.privilegedGetProperty("jdk.tracePinnedThreads"); if (propValue != null) { if (propValue.length() == 0 || "full".equalsIgnoreCase(propValue)) return 1; if ("short".equalsIgnoreCase(propValue)) return 2; } return 0; }
これを見ると、システムプロパティjdk.tracePinnedThreads
のみを指定した場合はfull
を指定した場合と同義になるようですね。
この結果は定数として保持され
private static final int TRACE_PINNING_MODE = tracePinningMode();
VThreadContinuation#onPinned
でpinning(ピン留め)を検出した時に、システムプロパティjdk.tracePinnedThreads
が指定されていれば
即座に出力されます。
@Override protected void onPinned(Continuation.Pinned reason) { if (TRACE_PINNING_MODE > 0) { boolean printAll = (TRACE_PINNING_MODE == 1); PinnedThreadPrinter.printStackTrace(System.out, printAll); } }
TRACE_PINNING_MODE
が1の場合はfull
ですね。
pinning(ピン留め)の状態かどうかは、こちらで検出します。
@Hidden private boolean yield0(ContinuationScope scope, Continuation child) { preempted = false; if (scope != this.scope) this.yieldInfo = scope; int res = doYield(); U.storeFence(); // needed to prevent certain transformations by the compiler assert scope != this.scope || yieldInfo == null : "scope: " + scope + " this.scope: " + this.scope + " yieldInfo: " + yieldInfo + " res: " + res; assert yieldInfo == null || scope == this.scope || yieldInfo instanceof Integer : "scope: " + scope + " this.scope: " + this.scope + " yieldInfo: " + yieldInfo + " res: " + res; if (child != null) { // TODO: ugly if (res != 0) { child.yieldInfo = res; } else if (yieldInfo != null) { assert yieldInfo instanceof Integer; child.yieldInfo = yieldInfo; } else { child.yieldInfo = res; } this.yieldInfo = null; } else { if (res == 0 && yieldInfo != null) { res = (Integer)yieldInfo; } this.yieldInfo = null; if (res == 0) onContinue(); else onPinned0(res); } assert yieldInfo == null; return res == 0; }
Continuation#doYield
メソッドはnative
メソッドなので詳細は見ていません。
int res = doYield();
そして、この結果が0でない場合はpinning(ピン留め)と判定します。
if (res == 0) onContinue(); else onPinned0(res);
ここからVThreadContinuation#onPinned
を呼び出すわけですね。
private void onPinned0(int reason) { onPinned(pinnedReason(reason)); }
Coninuation#pinnedReason
の内容はこちら。
private static Pinned pinnedReason(int reason) { return switch (reason) { case 2 -> Pinned.CRITICAL_SECTION; case 3 -> Pinned.NATIVE; case 4 -> Pinned.MONITOR; default -> throw new AssertionError("Unknown pinned reason: " + reason); }; }
Pinned
というのは列挙型で、pinning(ピン留め)の理由を表します。
/** Reason for pinning */ public enum Pinned { /** Native frame on stack */ NATIVE, /** Monitor held */ MONITOR, /** In critical section */ CRITICAL_SECTION }
あとは先ほどのシステムプロパティjdk.tracePinnedThreads
が指定されている時に、VThreadContinuation#onPinned
が呼び出している
PinnedThreadPrinter#printStackTrace
で、スタックトレースを出力するということになります。
/** * Prints the continuation stack trace. * * @param printAll true to print all stack frames, false to only print the * frames that are native or holding a monitor */ static void printStackTrace(PrintStream out, boolean printAll) { List<LiveStackFrame> stack = STACK_WALKER.walk(s -> s.map(f -> (LiveStackFrame) f) .filter(f -> f.getDeclaringClass() != PinnedThreadPrinter.class) .collect(Collectors.toList()) ); // find the closest frame that is causing the thread to be pinned stack.stream() .filter(f -> (f.isNativeMethod() || f.getMonitors().length > 0)) .map(LiveStackFrame::getDeclaringClass) .findFirst() .ifPresentOrElse(klass -> { int hash = hash(stack); Hashes hashes = HASHES.get(klass); synchronized (hashes) { // print the stack trace if not already seen if (hashes.add(hash)) { printStackTrace(stack, out, printAll); } } }, () -> printStackTrace(stack, out, true)); // not found }
スタックトレースが1度しか出力されなかったのは?
先ほど記載した箇所に答えが載っているのですが、ここですね。
int hash = hash(stack); Hashes hashes = HASHES.get(klass); synchronized (hashes) { // print the stack trace if not already seen if (hashes.add(hash)) { printStackTrace(stack, out, printAll); } }
スタックフレームのハッシュ値を取り、同じスタックフレームからは2度出力しないようになっています。
/** * Returns a hash of the given stack trace. The hash is based on the class, * method and bytecode index. */ private static int hash(List<LiveStackFrame> stack) { int hash = 0; for (LiveStackFrame frame : stack) { hash = (31 * hash) + Objects.hash(frame.getDeclaringClass(), frame.getMethodName(), frame.getByteCodeIndex()); } return hash; }
synchronized
ブロックを実行するパスに2回アクセスしたのに、1度しか出力されなかったのはこれが理由ですね。
おわりに
JEP 444(Virtual Threads)のpinning(ピン留め)が起こった際に、システムプロパティjdk.tracePinnedThreads
による
スタックトレースが出力されることを確認してみました。
pinning(ピン留め)自体については前回も確認していましたが、この機能のことを忘れていたので今回確認しておいてよかったです。
pinning(ピン留め)というものがどういうものか、どういう条件でスタックトレースを出力させることができるかも確認できましたしね。