Instruments 27でアプリの応答性はどう進化したか
WWDC 2026で、2人のAppleエンジニアがメモアプリをプロファイリングしました。このアプリには3つの異なるハングがありました。保存時にペンシルが反応しなくなる、スクロールがカクつく、そしてなげ縄ツール使用時に処理が止まる、という症状です。彼らはInstruments 27の新しいツールを各症状に1つずつ対応させて3つすべてを修正し、ベースラインと最適化後の比較によってそれぞれの修正を裏付けました。1 セッションの主張は、1つの診断フローに集約されます。ハング中のCPUを読み取れば、CPUが次に開くべきインストゥルメントを教えてくれる、というものです。CPUが高ければ、コードが遅すぎるということです。CPUがアイドルなら、コードがブロックされているということです。Instruments 27はその両方を解決するツールと、修正が実際に効いたかを確認する新しいビューを備えています。
この記事では、そのフローの軸となる3つのツールを順に見ていきます。CPUが飽和したときにself weightの大きい箇所を見つけるTop Functions、処理がMain Actorを奪い合うときにタスクがどのexecutorで実行されたかを見る新しいSwift executorsインストゥルメント、そしてスレッドがシステム待ちでアイドル状態になったときにsyscallの引数を読む新しいInspectorパネルです。Run Comparisonsは、各変更がトレースを改善したかを測定することで、これらを束ねます。以下の内容はすべてセッションから直接得たものです。
TL;DR
- Instruments 27は応答性のワークフローを1つの診断ルールを中心に再編成しました。まずTime Profilerを開き、ハング中のメインスレッドのCPUを確認し、その読み取り結果が適切なツールへ導きます。1
Top Functionsは新しい分析モードで、コール階層を破棄し、散らばったすべてのノードをself weightごとにマージします。フレームグラフが各ブランチに分散させてしまうランタイムのオーバーヘッドを浮かび上がらせます。1Run Comparisonsは「New in Instruments」であり、ベースライントレースと最適化トレースの正確なパフォーマンス差分を計算します。実行間で各関数を対応付け、リグレッションを赤、改善を緑で色分けします。1- 新しい
Swift executorsインストゥルメントは、Main Actor、グローバル並行executor、カスタムexecutorを可視化します。これにより、タスクがどのexecutorで実行されたかを確認し、Main Actorの競合を捉えられます。1 - 新しいInspectorパネルは、システムコールの正確な引数(ファイルディスクリプタ、バッファアドレス、書き込みサイズ)を提示し、on-core時間とoff-core時間を分離します。これにより、メインスレッドでの1.7 GBの書き込みのような同期的ブロッキングが明らかになります。1
Instruments 27が中心に据える診断フロー
新しいツールに入る前に、セッションはそれらを束ねるルールを示します。アプリがフレームを落としたりハングしたりしたとき、最初のステップはTime Profilerです。これは状況を把握するための高レベルの概観を与えてくれます。1 そこから、すべてを振り分ける問いはただ1つです。ハング中、CPUは何をしているのか、ということです。
CPU使用率が高ければ、スレッドはビジー状態であり、処理に時間がかかりすぎています。これはコードのパフォーマンスのボトルネックを示します。修正方法は2つあります。アルゴリズムをより速く動くようにリファクタリングするか、重い処理が避けられない場合は、バックグラウンドタスクへオフロードしてインターフェースの応答性を保つかです。1 一方、プロセッサがアイドルのままアプリがハングする場合、アルゴリズムの最適化は役に立ちません。メインスレッドがリソースの解放を待ってスタックしているからです。ファイルI/O、同期ロック、プロセス間通信のいずれかです。セッションはこう述べています。「Time ProfilerはアクティブなCPUサイクルしか監視しないため、これらのイベントを可視化できません」1
エンジニアたちはメモアプリのリリースビルドをプロファイリングしました。「デバッグビルドはランタイムのパフォーマンスをデバッグのしやすさと引き換えにするため、デバッグビルドのプロファイリングデータは誤解を招くおそれがある」からです。1 彼らはSwift Concurrencyテンプレートを選びました。これでもTime Profilerインストゥルメントは利用でき、3つのハングをすべて1つのベースライントレースに記録しました。さらに、なげ縄選択をOSSignposter型を使ったos_signpostインターバルでラップし、カテゴリをpoints of interestに設定しました。これによりInstrumentsがそのインターバルをpoints of interestトラックに表示します。このsignpostが、トレースをフィルタリングするためのアンカーとなり、後にはノイズのないrun comparisonのアンカーにもなります。1
ArtとHarjasがデモの前に、1:50から診断フローを説明します。
Instruments 27のウィンドウ自体がワークフローを枠組みづけます。上部のタイムラインには、タスク、アクター、executorの水平トラックが表示されます。その下の詳細エリアは、選択したトラックに応じて変化します。右側には「全く新しいInspectorパネル」があり、選択した内容に応じて追加の詳細とアクションを提示します。1 3つのツールがこの枠組みを埋め、それぞれが3つのハングの1つに対応します。
Top Functions:CPUが飽和したとき
なげ縄のハングはCPU高として現れます。トレースをなげ縄のos_signpostインターバルにフィルタリングしたところ、hangsインストゥルメントがそこに複数のハングを確認し、プロセストラックをメインスレッドまで展開すると、CPUが「この期間中、100%付近にとどまっている」ことが見えました。1 CPUが高いということは、コードは実行されているものの時間がかかりすぎている、ということです。そのためTime Profilerが適切なツールとなります。
セッションは、なぜここでフレームグラフだけでは不十分なのかを説明します。Time Profilerはハードウェアタイマーを使い、デフォルトで1ミリ秒ごとにコールスタックをサンプリングし、各コアの現在のスタックを記録します。サンプリングされた各関数には重みが与えられ、スタックの最下部にある関数にはself weight、すなわちその関数の内部で命令を直接実行するのに費やされた時間が与えられます。1 フレームグラフはそのコールツリーを空間的なブロックとして描画し、呼び出し元を上に、呼び出し先を下へと伸ばし、バーの幅はCPU総時間に比例します。1 問題はこうです。「Swiftランタイム関数やさまざまなヘルパーユーティリティのように、多くの場所から呼び出されるコード」では、フレームグラフが総コストをそれを呼び出すすべてのブランチに分散させてしまいます。実行時間が細かく断片化し、「どの特定の関数が全体で最も多くのサイクルを消費したかを答えるのが難しくなる」のです。1
そのギャップを埋めるのがTop Functionsです。セッションが新しいモードを説明するように、「この新しいモードはコール階層を破棄します。代わりに、散らばったすべてのノードを抽出して1つのブロックにまとめます」。これはselfメトリックで評価されます。1 なげ縄のフレームグラフを見ても、単一の元凶はなく、ただ「異なるコードパスが合算されてハングを引き起こすほどのコストになっている」だけでした。1 self weightでソートしたTop Functionsに切り替えると、最上位のエントリはswift_project_boxed_opaque_existentialでした。これは、コードが扱えるようにexistentialをアンラップするランタイム関数です。1
修正はタイムラインではなく型システムにありました。エンジニアはXcodeのコーディングアシスタントに、描画コードをexistentialの代わりに具象型とジェネリクスを使うように書き直すよう依頼しました。existentialはサイズが可変でアクセスに余分な処理を要するため、このユースケースでは高コストすぎることがわかったからです。1 このツールから得られる教訓はこうです。Top Functionsは、フレームグラフのどの単一ブランチも明らかにしない、散らばったソフトウェアのオーバーヘッドを捉えるために存在するのです。
Run Comparisons:修正が効いたことを証明する
かつて修正を確認するには、2つのトレースを別々のウィンドウで開き、Top Functionsのデータを並べて目で見比べる必要がありました。Instruments 27はそれをRun Comparisonsに置き換えます。セッションでは「New in Instruments」と説明されています。1 これは「ベースライントレースと最適化トレースのすべてのサンプルを相互参照することで、正確なパフォーマンス差分を計算」し、スタック内のすべてのノードを評価します。ベースライン実行の古いバージョンの関数を、最適化実行の新しいバージョンに対応付け、差分を計算し、パフォーマンスの差でソートします。赤いブロックはリグレッションを、緑のブロックは改善を示します。1
このワークフローはノイズの除去に意図的です。エンジニアたちはまず、両方の実行をなげ縄選択のまったく同じos_signpostインターバルにフィルタリングし、次にメインスレッドトラックを選択して比較ボタンをクリックし、ドロップダウンからベースライン実行を選びました。1 これによりサイドバーに比較タブが追加され、「複数の比較を作成でき、それらはドキュメントに保存されるためコラボレーションが容易になります」。1
比較は正直な物語を語りました。テキストのコールツリーは、なげ縄の全体的な実行時間が減少したことを示しました。フレームグラフは改善されたパスを緑で、リグレッションしたパスを赤で示し、そのリグレッションは「コーディングアシスタントがexistentialの使用を排除しようと作業する中で追加した新しい関数」でした。1 Top Functionsビューでは、リグレッションがデフォルトで最上位にソートされます。ソートを反転させると、swift_project_boxed_opaque_existentialが「完全に削除された」ことがわかり、「全体として、改善がリグレッションを上回って(out[weigh])いる」ことが明らかになりました。1 この差分こそが検証ステップです。Run Comparisonsは、修正を期待ではなく測定された結果にするために存在します。セッションが一貫して勧めるのは、「run comparisonのインターバルを確実なものにするために、os_signpostを活用する」ことです。1
Swift executorsインストゥルメント:タスクがMain Actorを奪い合うとき
スクロールのハングには、頼れるpoints-of-interestログがありませんでした。セッションは別種のコンテキストに手を伸ばします。「これらのハング中にMain Actor上にどのタスクがあるか」です。1 それが新しいSwift executorsインストゥルメントの役割であり、これは「プロセス内のMain Actor、グローバル並行executor、そしてあらゆるカスタムexecutorを可視化」します。1
各スクロールのハングについて、Main ActorトラックにはrenderThumbnailという名前のSwiftタスクが表示されました。トラックを選択するとMain Actor上のタスクが要約され、「Main Actor上で実行に数百ミリ秒かかっている複数のrender thumbnailタスク」が明らかになりました。これはカクついたスクロールと一致します。1 診断フローに従い、エンジニアは1つのハングにフィルタリングし、Inspectorを使ってメインスレッドをピン留めしました。Time ProfilerはCPUが「100%付近」だと報告し、システムリソース待ちの可能性を排除しました。タスクは単にMain Actor上で時間がかかりすぎていたのです。1
根本原因は、このインストゥルメントが可視化するコンテキスト継承の微妙な点です。Main ActorはすべてのUI更新と操作を処理します。アプリはサムネイルを非同期にレンダリングしていましたが、そのコードがSwiftUIから呼び出されていたため、「Main Actorのコンテキストを継承してしまい」、サムネイルタスクが重要なUI更新と競合していました。1 修正は処理をスレッドプールへ振り向けます。エンジニアはタスクのイニシャライザに@concurrent属性を追加しました。これによりレンダリングタスクがMain Actorからグローバルexecutorへ移り、その変更が競合状態を引き起こさないことをSwiftコンパイラが確認します。1 インストゥルメントはその移動を裏付けました。更新後のトレースでは、「サムネイルレンダリングタスクがMain Actorトラックからグローバルexecutorトラックへ移動」し、処理が並列で実行されるようになりました。1 このツールの要点はこうです。Swift executorsインストゥルメントは、どのexecutorがどのタスクを実行したかを正確に示すことで、アクターの混雑を特定するために存在するのです。
Inspectorパネル:スレッドがアイドルでブロックされているとき
保存のハングは、最初の2つとは逆です。Write to Fileのos_signpostインターバルにフィルタリングしてズームインすると、報告されたマイクロハングはCPUが「20%付近で推移している」ことを示しました。1 CPUが低いことは誤解を招きます。「それはコードが遅く実行されているという意味ではなく、スレッドが実行を停止しているという意味です」。1 CPUが低い状態でインターフェースがフリーズするとき、メインスレッドはシステムリソースを待ってブロックされており、アルゴリズムを最適化しても何も得られません。「最適化すべきコードが動いていないから」です。1
その症状に対して、セッションはSystem Traceテンプレートに切り替えます。これは「オペレーティングシステムがいつ、なぜアプリケーションを一時停止するかを正確に可視化するために作られた」ものです。1 セッションはスレッド状態のモデルをたどります。利用できないリソースに到達した実行中のスレッドはブロック状態に入り、カーネルがそれをプロセッサから追い出します。そしてリソースの準備ができたときにだけ実行可能(runnable)になり、スケジューラが空いているコアを割り当てるのを待ちます。リクエストの次の段階を調整するためのこの短い目覚めこそが、まさに「あの20%のCPU使用率を引き起こすもの」です。1
System Traceでは、保存のアクティビティレーンに「大量の空白」が表示され、スレッドがブロックされていることを示し、紫のインターバルが実行中のシステムコールを表していました。1 1つのインターバルを選択すると、クリックしたセグメント以上がハイライトされ、「on-coreとoff-coreの両方の時間にまたがる1つの連続したwriteシステムコール」が可視化されました。不透明なセグメントはon-coreの実行、半透明なセグメントはoff-coreのブロッキングです。1
新しいInspectorパネルは、それを判定に変えます。セッションが述べるように、「Inspectorは、このシステムコールに渡された正確な引数を示してくれます。対象のファイルディスクリプタ、バッファのメモリアドレス、そして最も重要なサイズが見えます」。1 サイズが決定的な証拠でした。アプリは「メインスレッドで1.7ギガバイトを超えるデータを書き込もうとしていた」のです。1 Inspectorはコストも示しました。この1つの操作は「500ミリ秒以上かかり、そのうち300ミリ秒近くはディスクを待つoff-coreに費やされていました」。1 同期的なdata.write呼び出しがボトルネックでした。エンコードと書き込みをSwiftタスクでラップして並行スレッドプールへ押し出すと、検証トレースは、writeシステムコールがメインスレッドではなくバックグラウンドスレッドに表示されるようになったことを確認しました。1 Inspectorパネルは、CPU専用のプロファイラでは見えない、ファイルI/Oのような同期的ブロッキングの振る舞いを露わにするために存在するのです。
重要なポイント
iOSとmacOSのエンジニアへ:
- まずTime Profilerを開き、ハング中のメインスレッドのCPUを読み取りましょう。CPUが高ければ
Top Functionsへ、CPUがアイドルならSystem TraceとInspectorへ向かいます。1 - フレームグラフが単一の元凶を示さないときは
Top Functionsを使いましょう。散らばったランタイムのオーバーヘッドをself weightごとにマージするので、最もコストの高い関数が浮かび上がります。1
パフォーマンス修正を出荷するチームへ:
- すべての変更を
Run Comparisonsで検証し、同じos_signpostインターバルにフィルタリングして、並べて目で見比べることを信じる代わりに赤/緑の差分を読みましょう。1 - 比較はドキュメントに保存され、複数の実行を積み重ねられるので、修正の証拠がレビューのためにトレースとともに移動します。1
並行処理と応答性の作業へ:
- 処理がMain Actorを奪い合うときは
Swift executorsインストゥルメントに手を伸ばしましょう。タスクがMain Actor、グローバル並行executor、カスタムexecutorのどれで実行されたかを示してくれます。1 - 常にリリースビルドをプロファイリングしましょう。デバッグビルドは誤解を招くデータを生むからです。1
FAQ
Instruments 27のTop Functionsモードとは何ですか?
Top FunctionsはTime Profilerの新しい分析モードです。コール階層を破棄し、関数の散らばったすべてのインスタンスを1つのブロックにまとめ、self weight、すなわちその関数の内部で命令を直接実行するのに費やされた時間でランク付けします。フレームグラフが苦手とする問いに答えます。Swiftランタイム関数やヘルパーユーティリティのように、コストが多くの呼び出しブランチに断片化しているとき、どの特定の関数が全体で最も多くのサイクルを消費したか、という問いです。1
InstrumentsのRun Comparisonsはどう機能しますか?
Run Comparisonsは、セッションでInstrumentsの新機能として説明されており、ベースライントレースと最適化トレースの正確なパフォーマンス差分を計算します。2つの実行間で各関数を対応付け、スタック内のすべてのノードの差分を計算し、パフォーマンスの差でソートして、リグレッションを赤、改善を緑で色分けします。クリーンな比較を得るには、両方の実行を同じos_signpostインターバルにフィルタリングし、トラックを選択し、ドロップダウンからベースラインを選びます。比較はドキュメントに保存されます。1
Swift executorsインストゥルメントは何を示しますか?
Instruments 27のSwift executorsインストゥルメントは、プロセス内のMain Actor、グローバル並行executor、そしてあらゆるカスタムexecutorを可視化します。これにより、特定のSwiftタスクがどのexecutorで実行されたかを確認でき、Main Actorの競合を捉えられます。セッションでは、SwiftUIから呼び出されたコードがMain Actorのコンテキストを継承したためにrenderThumbnailタスクがMain Actor上でスタックしていることが明らかになり、それらをグローバルexecutorへ移すことでハングが解消されました。1
ファイルI/Oが原因のハングはどう見つけますか?
インターフェースはフリーズしているのにメインスレッドのCPUが低い(セッションでは20%付近)とき、スレッドは遅いコードを実行しているのではなく、システムリソースを待ってブロックされています。System Traceテンプレートに切り替えてシステムコールのインターバルを選択しましょう。新しいInspectorパネルが、ファイルディスクリプタ、バッファアドレス、書き込みサイズを含むsyscallの正確な引数と、on-core対off-coreの時間を示します。デモでは、メインスレッド上での1.7 GBの同期書き込みが明らかになりました。1
このセッションが教える診断フローは、応答性のSwiftUI側を扱うiOS 27におけるSwiftUIのパフォーマンスと相互運用、そして測定のマインドセットを扱うパフォーマンスの盲点と対になります。Main Actorのハングを解消した並行処理の移動は、実践におけるSwift 6.2の並行処理で扱う、より広いモデルの中に位置づけられます。シリーズ全体のハブはApple Ecosystem Seriesです。
参考文献
-
Apple, WWDC 2026 session 268, Profile, fix, and verify: Improve app responsiveness with Instruments. 診断フロー(まずTime Profiler、メインスレッドのCPUの読み取りが調査を振り分ける)、リリースビルドとSwift Concurrencyテンプレートのガイダンス、
os_signpost/OSSignposterのpoints-of-interestインターバル、新しいInspectorパネル、コールツリーとフレームグラフのサンプリングモデル(デフォルト1ミリ秒のレート、self weight)、Top Functions分析モードとswift_project_boxed_opaque_existentialの発見、赤/緑の差分とドキュメントに保存される比較タブを備えたRun Comparisons(「New in Instruments」)、Swift executorsインストゥルメント(Main Actor、グローバル並行executor、カスタムexecutor)と@concurrent属性で解消したrenderThumbnailのMain Actor競合、そして1.7 GBの同期書き込みについてのSystem TraceとInspectorによる診断(ファイルディスクリプタ、バッファアドレス、書き込みサイズ、on-core対off-coreの時間、500ミリ秒以上のうち300ミリ秒近くがoff-core)の出典。 ↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩↩