ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
Instrumentsによるハング分析
ユーザインターフェースの要素は、リアルタイムの応答など、現実世界でのインタラクションを模倣することがよくあります。ユーザーのインタラクションに顕著なハングがあるアプリは、その期待に添えず、フラストレーションを生み出します。Instrumentsを使って、すべてのAppleプラットフォームでアプリのハングを分析、理解、改善する方法を紹介します。Instrumentsのトレースドキュメントを効率的に操作し、トレースデータを解釈し、さらにデータプロファイリングを記録して、特定のハングをより深く理解する方法を紹介します。 Instrumentsの使い方に不慣れな場合は、まず「Getting Started with Instruments」をご覧ください。また、アプリのハングを発見するのに役立つ他のツールについては、「Track down hangs with Xcode and on-device detection.」をご覧ください。
関連する章
- 1:56 - What is a hang?
- 3:51 - What is instant?
- 4:39 - Event handling and rendering loop
- 8:25 - Keep main thread work below 100ms
- 9:15 - Busy main thread hang
- 14:26 - Too long or too often?
- 21:46 - LazyVGrid still hangs on iPad
- 24:31 - Fix: Use task modifier to load thumbnail asynchronously
- 25:52 - Asynchronous hangs
- 32:38 - Fix: Get off of the main actor
- 35:57 - Blocked Main Thread Hang
- 39:19 - Fix: Make shared property async
- 40:35 - Blocked Thread does not imply unresponsive app
リソース
関連ビデオ
WWDC23
WWDC22
WWDC21
WWDC20
Tech Talks
WWDC19
WWDC16
-
ダウンロード
♪ ♪
「Instrumentsによるハング分析」に ようこそ Joachim Kurzです Instrumentsチームのエンジニアです 今日はハングについて詳しく見ていきます まず ハングの概要を説明します そのために人間の知覚について説明します 次に イベント処理とレンダリングループに ついて簡単に説明します レンダリングループはハングの原因を 理解する基礎となります この理論的知識を武器に Instrumentsで 3つの異なるハングの例を見てみましょう ビジーなメインスレッドのハング 非同期のハング そして ブロックされた メインスレッドのハングです それぞれについて 見分け方を説明します 分析する際に何を見るべきか 他のInstrumentを追加するタイミングを 詳細に学ぶことができます セッションの一部を理解する上で Instrumentsついて 多少理解しておくと役に立ちます Instrumentsでアプリの プロファイリングをしたことがあれば 大丈夫です そうWWDC19「Getting Started with Instruments」をご覧ください
ハングに対処する場合 通常は3つのステップがあります ハングを見つけ 次にハングを分析し どのように発生するかを理解します そして修正します (実際に修正されたことをご確認ください) 今日はハングをすでに見つけたと仮定します 分析することに集中しましょう いくつかの修正点についても説明します ハングの見つけ方について 詳細を知りたい場合は WWDC22のセッション 「Track down hangs with Xcode and on-device detection」をご覧ください Instrumentsを含むハングを見つけるための ツールをすべて網羅しています デバイス上でのハング検出は iOSデベロッパ設定と Xcodeのオーガナイザで有効にできます 今日は Instrumentsを使って すでに見つけたハングを分析します ハングをより理解するために 人間の知覚について話します 電気をつけてみましょう
電球とケーブルが必要です ずっと良くなりました ケーブルをコンセントに差し込むと ランプのように点灯しました それをまた抜くと 一瞬で電源が切れます でも遅延があったらどうでしょうか? 差し込みます 電源が入るまで少し時間がかかりました さらに奇妙なことに もう一度ケーブルを抜いても同じです ケーブルが差し込まれてから 電気が点灯するまでの遅延は わずか500ミリ秒でした この箱の中で何が起こっているのか すでに気になります ランプが直接ついたり消えたりしないのは 違和感があります しかし 他の状況では500ミリ秒の 遅延は問題ないかもしれません どの程度の遅延が許容されるかは 状況によります 例えばこのような会話を耳にしたとします 「カメはどうやって コミュニケーションをとるの?」 「貝殻電話」 ここでは 質問と回答の間に 1秒の遅れがありました それが全く自然に感じられました しかし これは違います
なぜでしょう? 亀とユニコーンの会話は リクエスト・レスポンス形式の インタラクションです しかし ランプを差し込むことは 実際の物体を直接操作することになります 実際の物体は瞬時に反応します 本物をシミュレートするなら 瞬時に反応する必要もあります そうしないと 幻想が壊れてしまいます
ケーブルが差し込まれてから ランプがつくまでに何の遅延もないのに 実際のランプがここにあると主張しても 何の問題もなかったでしょう しかし 大幅な遅延が発生すると 脳は突然 「ちょっと待って そうはいかない」と言うのです 瞬時とはどのくらいでしょうか? どの程度の遅延なら気づかないのでしょう?
これが遅延なしの基準値です
100ミリ秒くらいでしょうか?
私には 電源を入れた時に わずかに遅れているように感じました ただし 電源を切る時ではなく よく見た時だけです 皆さんの経験とは違うかもしれません 100ミリ秒はおおよそのしきい値です 著しく小さい遅延は実際には知覚できません 250ミリ秒で試してみましょう
250ミリ秒はもはや瞬間ではありません 遅くはありませんが 遅延は明らかに目立ちます
このような知覚のしきい値は ハングレポートにも反映されます ボタンのタップなどの 個々の操作における遅延は 約100ミリ秒未満であれば 通常 瞬時に感じられます それ以下にしたい特殊なケースでは 目指すには良い目標です それ以上は状況によります 250ミリ秒までは何とかなるかもしれません それ以上長くなると目立ちます 少なくとも無意識のうちにです スケールは連続的なものですが 250ミリ秒を超えると 確かにもう瞬時ではありません ほとんどのツールはデフォルトで 250ミリ秒からハングを報告し始めます これは無視しやすいので 「マイクロハング」と呼んでいます コンテキストによっては問題ありませんが そうでないこともよくあります 500ミリ秒を超えるものは すべて適切なハングとみなします これに基づいて このしきい値を大まかに使用できます 何かを瞬時に感じたい場合は 遅延は100ミリ秒以下を目指してください リクエスト・レスポンス形式の インタラクションがある場合は 追加のフィードバックなしで 500ミリ秒で問題ないかもしれません しかし実際には インタラクションの中で 両方があることがよくあります 例を見てみましょう
このセッションの準備に 協力してくれた同僚全員に このメールを書き終え 送信する準備ができています マウスを送信ボタンに移動して クリックします しばらくして メールウインドウが表示され 送信中であることを示します ここで何が起こったかというと 2つのことが 起きていたことを実際に見たということです まず ボタンが強調表示され 500ミリ秒のわずかな遅延が発生し その後 メールウインドウが表示されました リクエストが届いたことはすでに知って いたので この遅延は問題ないと感じました ボタンが強調表示されていたからです ボタンを「リアル」として扱います そして 「リアル」タイムで 瞬時に更新されることを期待します
インターフェイスの実際のUI要素については 通常 この「瞬時」更新を目指します UI要素を「瞬時に」反応させるには メインスレッドをUI以外の作業から 解放しておくことが重要です その理由について イベント処理とレンダリングのループを 詳しく見てみましょう Appleのプラットフォームで イベントがどのように処理され ユーザー入力がどのように画面更新に つながるか見てみましょう
ある時点で 誰かがデバイスと やり取りします それがいつ起こるかを コントロールできません まず 通常は何らかの ハードウェアが関与します マウスやタッチスクリーンなどです インタラクションを検出し イベントを作成して オペレーティングシステムに送信します オペレーティングシステムはどのプロセスが イベントを処理するかを判断し そのプロセス 例えばアプリに転送します アプリでは メインスレッドがイベントを処理します ここでUIコードのほとんどが実行されます UIの更新を決定します 次に このUI更新が レンダリングサーバに送信されます これは個々のUIレイヤを合成する 別のプロセスで 次のフレームをレンダリングします ディスプレイドライバはレンダリング サーバが用意したビットマップを取得し それに応じて画面上のピクセルを更新します この仕組みついて詳しく知りたい場合は 「アプリの応答性を向上させる」で 取り上げています 何が起きているのかを理解するには この大まかな概要で十分です さて この間に別のイベントが入ると 通常は並行して処理できます しかし 1つのイベントがパイプラインを どのように通過するかを見ると すべてのステップを順番に 見ていく必要があります メインスレッドに到達する前の イベント処理のステップと レンダリングおよび更新表示後のステップは 通常 その時間はかなり予測可能です インタラクションに大幅な遅延が生じると ほとんどの場合 メインスレッドに 時間がかかりすぎたからです または イベント発生時に メインスレッドで他の何かが まだ実行されているためです 従って イベントを処理する前に イベントの終了を待つ必要があります UI要素を更新するたびに メインスレッドで時間が必要だと仮定すると 更新が100ミリ秒以内に行われ リアルに感じられるようにしたいです 理想的には メインスレッドでの作業が 100ミリ秒を超えないことです もっと速くできれば さらに良いです メインスレッドで長時間実行される作業も 不具合を引き起こす可能性があります 不具合を避けるために より低いしきい値が適用されます 不具合に関する詳細は 「UIアニメーションの不具合と レンダリングループを調査する」と 「アプリの応答性を向上させる」を ご覧ください 今日はハングに焦点を当てます 同僚の一人がアプリのBackyard Birdsで ハングに気づきました 新しい機能に取り組んでいた時です Instrumentsでアプリを プロファイリングしてみましょう
ここにアプリの Xcodeプロジェクトがあります Instrumentsでアプリの プロファイリングに必要なことはすべて 「プロダクト」メニューをクリックし 「プロファイル」をクリックします そして Xcodeがアプリをビルドして デバイスにインストールします しかし起動はしません
XcodeはInstrumentsも開き Xcodeで構成したのと同じアプリと デバイスをターゲットに設定します Instrumentsのテンプレート選択で Time Profilerテンプレートを選択します これは 何を探しているか まだ分からない場合や アプリが何をしているのかを より理解したい場合には 良い出発点になることがよくあります Time Profilerテンプレートから新しい Instrumentsのドキュメントが作成されます 特に この新しいドキュメントには Time Profilerとハングの instrumentsが含まれ どちらも分析に役立つでしょう ツールバーの左上にある「録画」ボタンを クリックして録画を開始します Instrumentsが設定されたアプリ を起動し データのキャプチャを開始します
ここにBackyard Birdsアプリがあります 最初の庭をタップすると 詳細図に移動します すぐに「背景を選択」ボタンをタップすると 下のシートが出て 背景写真の選択が表示されます 今それをやってみます ボタンは押されていますが動かないようです シートが表示されるまで かなり時間がかかりました 深刻なハングです
Instrumentsがすべてを記録しています ツールバーで「停止」ボタンをクリックして 録画を停止します Instrumentsもハングを検出しました ハング時間を測定し そして 重大度に応じて 対応する間隔にラベルを付けます この場合Instrumentsは 「重大なハング」が発生したことを示します これは アプリの使用中にも 同様に当てはまります Instrumentsが 応答しないメインスレッドを検出し 対応する間隔を ハングの可能性としてマークします この場合 確かにハングが発生しました メインスレッドが応答しない場合は 主に2つのケースが考えられます 最も単純なケースは メインスレッドが単に 他の仕事でまだ忙しい時です この場合 メインスレッドは 大量のCPUアクティビティを表示します もう1つのケースは メインスレッドが ブロックされている場合です これは通常 メインスレッドが 他の場所で行われる他の作業を 待っているためです スレッドがブロックされている場合 メインスレッド上のCPUアクティビティは 皆無かそれに近いです どのケースに該当するかによって 何が起こっているのかを判断するために 次に取るべきステップが決まります Instrumentsに戻り メインスレッドを見つけます ドキュメントの最後のトラックは ターゲットプロセスのトラックを表示します 左側に小さな開示インジケータがあり サブトラックがあることを示します これをクリックすると プロセス内の 各スレッドに個別のトラックが表示されます 次にここで メインスレッドの トラックを選択します これで 詳細エリアも更新され プロファイル表示が示されます これはすべての関数のコールツリーを示し 全記録時間中に メインスレッドで実行されます
しかし 興味があるのは ハング中に何が起こったかだけです タイムラインのハング間隔をセカンダリ クリックしコンテキストメニューを表示します ここで「Set Inspection Range」を 選択できますが ここでは オプションキーを押しながら 「Set Inspection Range and Zoom を代わりに選択します
これは間隔の範囲にズームインし 詳細ビューに表示されるデータを 選択された時間範囲にフィルタリングします
全ハング期間中のCPU使用率は 100%ではありませんが ほとんどの場合 60%から90%と 依然としてかなり高いです これは明らかにメインスレッドが ビジー状態である場合です このCPUの作業がどのようなものか 調べてみましょう
コードツリーの様々なノードを すべて詳しく見てみましょう しかし 右側には素晴らしい要約があります 最も重いスタックトレース表示です その最も重いスタックトレース表示の フレームをクリックすると コールツリービューが更新され このノードが表示されます この呼び出しメソッドはコールツリーの かなり深いところにあることもわかります
デフォルトで最も重いスタックトレースは ソースコードに由来しない 後続の関数呼び出しを非表示にし ソースコードがどこに関与しているかを 見やすくします 同様のフィルタを コールツリービューに適用できます 下のバーの 「コールツリー」ボタンをクリックし 「システムライブラリを非表示にする」 チェックボックスを有効にします これで システムライブラリから すべての関数が除外され コードに集中しやすくなります コールツリー表示では ほぼすべてのバックトレースに 「BackgroundThumbnailView. body.getter」呼び出しが含まれています bodyゲッタを 早くした方が良さそうですよね? 実はそうでもないのです メインスレッドが ビジー状態であることがわかります つまり CPUは 多くの作業をしているということです また CPUが多くの時間を費やしている メソッドも見つかりました しかし 今は2つの異なるケースがあります このメソッドでは多くのCPU時間を 費やしている可能性があります メソッド自体が長時間実行されるからです しかし 単に何度も呼び出されるから かもしれません それがここに表示される理由です メインスレッドの作業をいかに減らすかは どのケースを想定しているかによります
典型的なコールスタックは 次のような構造になっています メイン関数からの呼び出しがあり UIフレームワークや 他の多くのものを呼び出します そして ある時点でコードが呼び出されます この関数が1回だけ呼び出される場合 ここでのタートル関数のように 1回の呼び出しには長い時間がかかります 次に 何を呼び出すかを見てみましょう 多分 多くの作業をするでしょう 作業を減らすことができるかもしれません しかし 私たちが調査している方法が このユニコーンのように 何度も呼び出されている 可能性もあります もちろん その作業も 同様に何度も繰り返されます これは通常呼び出し元がいて ユニコーンという関数を 何度も呼び出しているからです 例えばループからです 焦点を当てた関数 ユニコーンが 行うことを最適化するよりも どうすれば呼び出す頻度を減らせるかを 調査する方が有益かもしれません
つまり 次に見るべき方向は 抱えているケース次第です タートルのケースのように 長く続く関数の場合 その実装と呼び出し先を見たいと思います もっと下を見る必要があります ただし ユニコーンのように 関数が何度も呼び出される場合 呼び出しているものを調べる方が有益です その頻度を減らすことができるかどうかを 判断します さらに上を見てみる必要があります しかし Time Profilerでは どのケースかわかりません ユニコーンとタートルへの呼び出しが 次々に起こったと仮定しましょう Time Profiler はCPU上で一定間隔で 何が実行されているかを確認して データを収集します そして サンプルごとにCPU上でどの関数が 現在実行されているかをチェックします この例では タートルとユニコーンの両方を 4回獲得します しかし これは非常に速い タートルである可能性もあり ユニコーンにはもっと時間がかかりますが あるいは 他の組み合わせもあります これらの状況はすべてTime Profilerに 同じデータを作成します
特定の関数の実行時間を測定するには os_signpostを使用します この方法についてはWWDC19の 「Getting Started with Instruments」 でも 様々な技術に特化した Instrumentsもあり 何が起きているかを 正確に知ることができます その1つがSwiftUIのView bodyの instrumentです SwiftUI本体のinstrumentを追加するには ツールバーの右上の プラスボタンをクリックします Instrumentsライブラリが表示されます これはInstrumentsアプリが 提供するすべてのInstrumentsのリストです たくさんあります 独自のカスタムinstrumentsを 表示することもできます
フィルタ欄に「SwiftUI」と入力すると 2つのinstrumentsが表示されます 「View body」instrumentsを選択します ドキュメントウインドウに ドラッグして追加します このinstrumentは前回記録した時には ドキュメントにはなかったので 表示するデータがありません でも問題はありません また記録すればいいだけです 時間を節約するために すでに実行しました SwiftUIの「View Body」instrumentを ドキュメントに記録したあとで View Bodyのトラックにも データが表示されるようになりました SwiftUIのView Bodyトラックには 多くの間隔があります 少し狭いので Ctrl+Plusを押して 高さを増やします SwiftUI View Bodyトラックは 間隔をグループ化し 実装されている ライブラリによって行われます 各間隔は1つのview bodyの実行です 再びハングにズームインしてみましょう
第2レーンはオレンジの間隔が多く すべて「BackgroundThumbnailView」の ラベルが付いています これで bodyが何回実行され 各回どれだけの時間がかかったのか 正確にわかります オレンジ色は 特定bodyの実行時間が SwiftUIで目指しているものよりも 少し時間がかかったことを示しています しかし より大きな問題は 間隔がいくつあるかということです 詳細表示では すべてのbody間隔の 概要が表示されます Backyard Birdsの横にある 開示インジケータをクリックすると Backyard Birdsで個々の表示タイプを 明らかにすることができます これはBackgroundThumbnailViewの bodyは 70回実行され 平均実行時間は 約50ミリ秒で 合計実行時間は 3秒を超えることを示しています これで ハング時間のほぼすべてが 説明されます しかし 70回は多すぎる気がします 6枚の画像を前面に表示するだけの場合です これは bodyを呼び出す頻度を 減らすべきケースです したがって bodyゲッタの 呼び出し元を調べて 頻繁に呼び出される理由を調べ それを減らす方法を考えるべきです 関連するコードに簡単に移動するために メインスレッドトラックを再度選択し BackgroundThumbnailView.body.getter ノードをセカンダリクリックし 呼び出しツリーでコンテキストメニューを 表示し「Reveal in Xcode」を選択します
これで Xcodeでbodyの実装が開きます タイプをセカンダリクリックして 表示が どのように使用されるか見てみましょう 次に 「Find」を選択し「Find Selected Symbol in Workspace」を選択します 検索ナビゲーターの最初の結果が すでに探しているものです
ここで 「BackgroundThumbnailView」は GridRow内のForEach内で使用され Grid内の別のForEach内で使用されています Gridは生成された時点で コンテンツ全体をくまなく計算し 全ての背景のサムネイルを計算します 必要なのは最初の数枚だけですが しかし 代替手段としてLazyVGridがあります これは1画面を満たすのに 必要な表示のみが計算されます SwiftUIの表示には多く 遅延バリアントがあり 必要な数の表示のみを計算します これはしばしば作業を減らす簡単な方法です しかし イーガーバリアントは 同じコンテンツをレンダリングする場合 メモリ使用量が大幅に少なくなります デフォルトで通常のイーガーバリアントを 使用し 遅延バリアントに切り替えましょう 事前に過剰な作業に関連した パフォーマンスの問題が見つかった場合です
WWDC20の「Stacks, Grids, and Outlines in SwiftUI」で この遅延バリアントを紹介し さらに詳しく説明しています この更新されたコードを プロファイリングしてみましょう 記録を開始します 「Choose Background」ボタンを もう1度タップしてハングを再現します これでかなり良くなりました わずかな遅延はありますが 前ほどひどくはありません Instrumentsはこれを裏付けています 今回記録したハング時間は 400ミリ秒未満でした マイクロハングです 「View Body」トラックでは BackgroundThumbnailのbody実行が 8回だけだったことが示されています これは予想通りです これで十分かもしれません マイクロハングはあまり目立ちません 他のデバイスでもうまく機能しているか 確認してみましょう iPadでBackyard Birds を プロファイリングして見ましょう
iPadでBackyard Birdsを実行しています すでに詳細表示になっています 「Choose Background」ボタンをタップします シートが表示されるのに時間がかかります 一度表示されれば その理由がわかります 画面が大きくなったので サムネイルがたくさんあり より多くのスペースがあります Instrumentsもこのハングを記録しました
検査範囲をハング間隔に焦点を合わせます BackgroundThumbnailViewのbodyが また増えています 理にかなっています 全画面表示には 約40枚のレンダリングが必要です さらに画面に適合させるためです 従って 同じコードでiPhoneでは ほぼ問題なく動作しても iPadでは 単に画面が大きいため 速度が遅かったのです これがマイクロハングも 修正すべき理由の1つです 自分のデスクでのテスト中に マイクロハングと思われるものが 様々な条件下では一部のユーザーにとって 重大なハングの可能性があります 現在 画面いっぱいに必要な数の表示しか レンダリングしないので これを呼び出す頻度を減らすという点で 最適化の可能性を使い果たしました 個々の実行をより早くするために 何ができるかを見てみましょう 検査範囲を単一のBackgroundThumbnailView の間隔に設定します そしてMain Threadトラックに 戻ります Instrumentsは 最も重いバックトレース表示 でview bodyゲッタを示し 「BackyardBackground.thumbnail」の プロパティゲッタを 呼び出していることを示しています これはサムネイル画像を提供する モデルオブジェクトで ビューに表示します このサムネイルゲッタは「UIImageimageBy PreparingThumbnailOfSize:」を呼び出します つまり ここではオンザフライで サムネイルを計算しているようです これには時間がかかります この場合は約150ミリ秒です これはむしろ バックグラウンドで 行うべき作業で メインスレッドを 忙しくさせるべきではありません よりよく理解するために どのような変更を行うことができるか サムネイルゲッタの呼び出され方を コンテキストで見てみましょう 「BackgroundThumbnailView.body.getter」 をセカンダリクリックし これは最も重い スタックトレース表示のフレームです そして「ソースビューアで開く」を 選択します これにより コールツリー表示が ソースビューアに置き換えられます これはbodyゲッタの実装を示しています 次に 実装の行に注釈を付けます Time Profilerサンプルでコードがどこに どれだけの時間を費やしたかを示します ここでのbodyの実装は本当に単純で 背景から返されたサムネイルで 新しい画像表示を作成するだけです しかし このサムネイルの呼び出しには 時間がかかります 別の書き方のアイデアがあります Xcodeにジャンプするには 右上のメニューボタンをクリックします そして「Open file in Xcode」を 選択します
先ほどと同様に Xcodeのソースコードを示し 変更を行う準備ができています 今やりたいことは サムネイルを バックグラウンドで読み込むことです 読み込み中に 進捗インジケータに表示します まず 読み込んだサムネイルを 保持するために状態変数が必要です
次に bodyに すでに読み込まれた画像があれば 画像表示で使用します そうでなければ 進捗表示が示されます
後は 実際のサムネイルを読み込むだけです 表示が見えたら読み込みを開始します これが「.task」修飾子の目的です
表示されると SwiftUIが タスクを開始します 「サムネイル」ゲッタを呼び出し 結果を「画像」に割り当てます これで表示が更新されます 試してみましょう! それでは Instrumentsの記録で 「Choose Background」ボタンをタップします そしてシートがすぐに表示されます! 素晴らしいです! 進捗インジケータが表示され 数秒後には サムネイルが表示されました うまくいきました!
しかし Instrumentsはまだ 2秒近くのハングを示しています ここで起こったことは ハングするのが少し遅くなりました Backyard Birdsアプリのどこで これが起こるのか説明しましょう すでに詳細表示になっています すぐに もう一度「Choose Background」ボタンをタップします その後 シートを直接閉じてみます 完了ボタンをタップしてください 「背景を選択」して「完了」です 何度もタップしてしまいましたが 読み込みが行われている間は 私のタップは無視されました これがInstrumentsが 教えてくれたハングです シートが表示されたあとに起こります
少し異なるタイプのハングです メインスレッドがビジー状態か ブロックされているかの違いについては すでに説明しました ハングを調べる別の方法もあります 何が原因で いつ起こるのか これらを同期ハングと 非同期ハングと呼んでいます
ここでは メインスレッドが 作業をしています イベントが発生した時 そのイベントの処理に時間がかかるなら それはハングです 仮にそれをコントロールでき イベントが素早く処理されるとします しかし メインスレッドで後でする作業を 遅らせただけかもしれません または 他のメインスレッドの作業の後 イベントが発生するかもしれません そのイベントは 処理される前に 前の作業が終わるまで 待たなければなりません それでも まだハングが発生します 個々のイベント処理のコードは すぐに終了しますが 私たちのプラットフォームでの ハング検出の仕組みは メインスレッド上のすべての作業項目を見て 長すぎないかを検査します もしそうなら ハングの可能性があると マークされます これはユーザー入力の有無に関係なく 行われます ユーザー入力がいつでも 入ってくる可能性があるため 実際にハングすることになります つまり ハング検出はこのような非同期や 遅延のケースも検出できるということです ただし 実際に発生した遅延ではなく 潜在的な遅延を測定するだけです
非同期ハングを非同期と呼ぶのは 頻繁に発生するためです メインキューでの 「dispatch_async」による作業や メインアクタ上で非同期に実行される SwiftConcurrencyタスクで発生します しかし メインスレッドでの作業を 引き起こすものであれば何でも起こり得ます 最初に見たハングは同期ハングでした ボタンをタップすると そのタップにより 長時間の作業が発生し 結果が表示されるのが遅くなります
この直近のハングは 非同期または遅延ハングです 完了ボタンをタップしても それ自体が 高額な作業をするわけではありません しかし メインスレッドには まだ作業が残っていて タップの処理を妨げています つまり アプリを使用している人は 気づかないかもしれませんが この間にアプリを操作しなければ そうした場合に備えて このようなケースを修正する必要があります 今すぐそうしましょう さて ここでInstrumentsに戻ります すでに選択範囲を非同期ハングに設定し ズームインしています ビューボディトラックの概要表示で Instrumentsは現在75件の 呼び出しがあったことを示しています BackgroundThumbnailViewの bodyゲッタにです これは ほとんどのサムネイルの bodyゲッタが2回実行されるからです SwiftUIは グリッドを満たす進捗 インジケータを持つ40枚の表示を作成します しかし 実際に表示されるのは 35件だけです その35枚に対して 画像の読み込みを開始します 画像が読み込まれると 表示が更新され bodyが再び呼び出されます 合計75回のbodyゲッタが実行されます
合計75回のbodyゲッタでさえ 1ミリ秒よりはるかに短いです つまり bodyゲッタは今では速くなりました その部分はうまくいきました しかし まだハングがあります もう一度 「メインスレッド」トラックを選択します そして最も重いスタックトレース表示で Instrumentsは まだサムネイルゲッタであることを示し メインスレッドで長い時間がかかっています 今度はクロージャで呼び出されます 「BackgroundThumbnailView. body.getter」内で呼び出されます 直接bodyゲッタではありません これをダブルクリックすると ソースビューアを開くショートカットです これはまさにバックグラウンドで 実行されることを期待していたコードです タスク修飾子クロージャ内にあるためです このコードは現時点で実行されるべきですが メインスレッドではそうではありません このような問題に対しては Swiftの並行処理タスクが 期待通りに実行されない場合は 別の便利なinstrumentがあります Swift Concurrency Tasks instrumentです すでに同じ動作を記録し Swift Concurrency task instrumentを 追加しました Swift Tasks instrumentは ドキュメントに概要トラックを追加します しかし 今回のケースでより興味深いのは 各スレッドトラックに寄与するデータです メインスレッドトラックでは Swift Tasks instrumentから 新しいグラフがあります 1つのトラックに 複数のグラフを表示できます スレッドトラックヘッダの 小さな下向き矢印をクリックすると どのグラフを表示するか設定できます Time ProfilerのCPU使用率 グラフのように 別のグラフを選択することもできます または Commandキーを押しながら クリックして複数を選択します これで InstrumentsはCPU使用率と このスレッドのSwift Tasksグラフを 一緒に表示します ハング間隔に再度ズームインします 「Swift Tasks」レーンには明確に表示され メインスレッドでは 大量のタスクが実行されます 検査範囲をいずれかに設定し プロファイル表示で 最も重いスタックトレースをチェックし このタスクがサムネイルの計算作業を ラップしていることを確認します
従って この作業は望んでいたように タスクに正しくラップされています しかし タスクはメインスレッドで 実行されていますが これは予期外です ここで何が起こっているのか説明しましょう まず bodyゲッタは @MainActor注釈を継承しています SwiftUIの表示プロトコルからです 「表示」プロトコルで「body」は 「@MainActor」と注釈されているので これを実装すると bodyゲッタも暗黙的に @MainActorとして注釈されます 次に 「.task」修飾子のクロージャは 周囲のコンテキストのアクタ分離を 継承するように注釈が付けられています つまり bodyゲッタはMainActorに 分離されているためです タスククロージャも同様です 従って このクロージャで 実行されるコードはすべて デフォルトではメインアクタ上で 実行されます 「サムネイル」ゲッタは同期しているため メインスレッド上で同期的に実行されます
Swiftの並行処理タスクは デフォルトで周囲のコンテキストの アクタの分離を継承します 同じ動作は SwiftUIの .task修飾子も同じです メインアクタから離れる方法は2つあります メインアクタにバインドされていない関数を 非同期的に呼び出し タスクがメインアクタから 離れることができます しかし それが不可能な場合もあるでしょう 次に 周囲のアクタのコンテキストから タスクを明示的に切り離すことができます 「Task.detached」を使用しますが それは強引なやり方です 単に既存のものを中断するよりも 別のタスクを作成するのは より高くつきます SwiftUIは タスク修飾子で作成された タスクも自動的にキャンセルします 対応する表示が消えた時です しかし このキャンセルは新しい非構造化 タスクにはTask.detachedのように 伝搬しません 詳細については WWDC22の 「Swiftの並行処理の可視化と最適化」と アプリの応答性の向上に関する ドキュメントをご覧ください この場合 すでに非同期コンテキストにいるため サムネイル関数を 非分離で非同期にするのは簡単です オプション1を選択します サムネイルの読み込みコードです 問題は このタスクが メインアクタで実行されることです bodyゲッタのメインアクタの 分離を継承するためです サムネイルゲッタは同期しているため メインアクタにも留まります 修正は簡単です サムネイルゲッタの定義に移ります ゲッタを非同期にし それから表示構造体に戻ります…
ゲッタは非同期になっているため その前にawaitを追加する必要があります
これで 「サムネイル」ゲッタは メインスレッドの代わりに Swift Concurrencyの並行スレッドプールで 実行されるようになります 試してみましょう 再び詳細表示にして 「背景を選択」をタップします すごく速いです! ハングがなかっただけでなく 全体的に 読み込みが速くなったように思われます 進捗表示はほとんど見ていません Instrumentsで ハングがないことが確認されました ここでは CPUの使用率が高いようです 拡大してみましょう ここでサムネイルの読み込みが行われます メインスレッドを確認すると すべてのタスク間隔が 非常に短くなっていることが確認できます 他のスレッドトラックまで スクロールダウンすると Swiftのタスクが 他のスレッドで連続的ではなく 並列に実行されていることがわかります これにより マルチコアCPUが より有効に活用されます これですべてのサムネイルを 約1.5秒ではなく 数百ミリ秒で計算できます この間も メインスレッドは 応答し続けています 従って これは完全に修正されました
応答しないメインスレッドを 調査し修正しました メインスレッドが ビジー状態であることが原因で これは ハング中にメインスレッドが大量の CPUを使用していることから特定できました ハングがユーザーとのインタラクションの 一部として直接発生する 同期的な場合と メインスレッドで先に予定されていた作業が 次のイベントの処理を遅れさせる 非同期的な場合があります そして Instrumentsがその両方のケースを 検出できることも経験しました 作業を減らし バックグラウンドで減らすことのできない 他の作業をすることでハングを修正しました メインスレッドに戻るのは UIを更新するためだけです しかし まだ見ていないケースが 1つあります ブロックされたメインスレッドで この場合 メインスレッドは CPUをほとんど使用しません 他の次元は ブロックされた メインスレッドにも同様に適用されますが このようなケースを分析するには 他のInstrumentsが必要です 例を見てみましょう
別のハングからのトレースファイルです このハングをズームインしました 数秒の長いものです 「メインスレッド」トラックで CPU使用率のグラフを見ると 初期のCPU使用率はありますが その後は何もありません これは明らかに メインスレッドがブロックされたケースです Time ProfilerがCPUで 実行されているものを見本として データを収集する方法について説明しました
拡大すると CPU使用率グラフに 個々の見本まで表示されます この各マーカーは Time Profilerが取得した見本です 右側にはいくつか見本がありますが そのあとは何もありません しかし 見本なしの時間範囲を選択すると Time Profilerでは 何が起こっているのかわかりません この間はデータが記録されなかったためです 従って別のツール つまり スレッドステイツのinstrumentが必要です これまでの他のinstrumentsと同様に Instrumentsライブラリから追加できます すでに同じハングを再度記録しましたが 今回は 「Thread State Trace」 instrumentが追加されました このinstrumentには 新しいトラックが追加されました しかし 「Swift Concurrency」 instrumentのように 興味深いデータは 実際には 「スレッド」トラックにあります つまり メインスレッドには 非常に長い「ブロックされた」間隔があり 6秒以上で ハング時間のほとんどが これで説明されます その真ん中をクリックするとInstrumentsの タイムカーソルがそこに移動し これにより 詳細領域の ナラティブ表示も更新され このブロックされた状態の エントリを表示します ナラティブ表示では スレッドのストーリーがわかります いつ なぜ 何をしていたのかを示します
選択された時間では スレッドは 6.64秒間ブロックされたことがわかります システムコールのmach_msg2_trapを 呼び出していたためです 右側には再び バックトレース表示があります しかし このバックトレースは 最も重いバックトレースではありません アグリゲーションでもありません これは mach_msg2_trapシステムコールの 正確なバックトレースです それが原因でスレッドがブロックされました 関数の呼び出しは 下部のリーフノードとして表示されます そのコールスタックが上に表示されます コールスタックは MLModelを割り当てた結果 システムコールが 発生したことを示しています これは「ColorizingService」型の オブジェクトを割り当てたために発生し このColorizingServiceの「shared」と呼ばれる シングルトンプロパティの一部として 呼び出され 次に bodyゲッタの クロージャによって呼び出されました そのクロージャをダブルクリックすると 再びソースビューアに移動し 呼び出したコードを見つけることができます この行は無害に見えますよね? 詳しく見てみましょう
ColorizingServiceの 共有プロパティにアクセスし それをローカル変数に格納します ただし 共有プロパティが生成するため 無害ではありません 共有ColorizingServiceインスタンスに 初めてアクセスすると 今度はモデル読み込み機構全体が起動し スレッドがブロックされます それで次のように思われるかもしれません 「これを『await』の後の 非同期部分内に移動しよう」 しかし 直観に反して これでは問題は解決されません 「await」キーワードは後続のコードでの 非同期関数呼び出しにのみ適用されます この例では 「colorize」関数は 「async」です しかし 「shared」プロパティは そうではありません 静的なletプロパティなので 最初にアクセスされた時に 遅延初期化され これは同期的に起こります awaitキーワードを使っても変わりません 従って 同期呼び出しは 引き続きメインスレッドで発生します 前の例と同じ方法でこれを修正できます sharedプロパティも「async」にし メインアクタから離れることで解決できます 自分のスレッドに代わって 他の場所で前進する作業を待っている場合は これは通常は問題ありません しかし スレッドがブロックされる他の 一般的な理由はロックまたはセマフォです 留意すべきベストプラクティスと Swiftの並行処理でロックとセマフォを 使う時に回避すべき事項については WWDC21の「Swift concurrency: Behind the scenes」をご覧ください
最後に 次のことをお話ししたいと思います ブロックされたメインスレッドに関連する もう1つのケースです これが先ほど見たトレースです 右が先ほど調べたハングです メインスレッドがブロックされています しかし その左側には他のケースもあります メインスレッドが 何秒もブロックされるケースです しかし Instrumentsはこれを ハングの可能性があるとは警告しません ここではメインスレッドは ユーザー入力が ないため単にスリープ状態になっています オペレーティングシステムの観点からは ブロックされています しかし 何もすることがない時に実行しない ことでリソースを節約しているだけです 入力データが入ると すぐに起動して処理します 従って ブロックされたスレッドが 応答性の問題かどうかを判断するには スレッド状態のinstrumentではなく ハングinstrumentを見てください
メインスレッドがブロックされていても 応答していないとは限りません 同様に CPU使用率が高いからといって メインスレッドが応答しないことを 意味するものではありません でもメインスレッドが応答しない場合は ブロックされているか メインスレッドが ビジーであることを意味します ハング検出では これらすべての詳細を考慮します メインスレッドが実際に 応答しなかった間隔にのみラベルを付け ハングする可能性があると表示します このセッションで覚えていることが1つだけ あるとすれば それは次のとおりです メインスレッドで行っている作業が何であれ 100ミリ秒未満で終了し メインスレッドを解放して再び イベント処理に使えるようにすべきです 短ければ短いほど良いです ハングを詳細に分析するには Instrumentsが最適です メインスレッドがビジーかブロックされた 状態かの区別を覚えておいてください ハングはメインスレッドでの非同期作業でも 発生することを覚えておいてください ハングを修正するには 作業を減らすか 作業をバックグラウンドに移すことです 時には 両方の場合もあります そして 作業量を減らすということは 作業に適切なAPIを使うことを意味します 一般的には まず測定し 最適化する前に 実際にハングがあるかどうかを確認します 確かにベストプラクティスはありますが ただし 並列と非同期コードの デバッグもさらに困難です すべてのことに驚くことがよくあります 実際にはとても速いです そして実は何が遅くなるのか すべてのハングを見つけて分析し 修正することを楽しんでください ご視聴ありがとうございました ♪ ♪
-
-
19:38 - BackgroundThumbnailView
struct BackgroundThumbnailView: View { static let thumbnailSize = CGSize(width:128, height:128) var background: BackyardBackground var body: some View { Image(uiImage: background.thumbnail) } }
-
19:58 - BackgroundSelectionView with Grid
var body: some View { ScrollView { Grid { ForEach(backgroundsGrid) { row in GridRow { ForEach(row.items) { background in BackgroundThumbnailView(background: background) .onTapGesture { selectedBackground = background } } } } } } }
-
20:03 - BackgroundSelectionView with Grid (simplified)
var body: some View { ScrollView { Grid { ForEach(backgroundsGrid) { row in GridRow { ForEach(row.items) { background in BackgroundThumbnailView(background: background) } } } } } }
-
20:26 - LazyVGrid variant
var body: some View { ScrollView { LazyVGrid(columns: [.init(.adaptive(minimum: BackgroundThumbnailView.thumbnailSize.width))]) { ForEach(BackyardBackground.allBackgrounds) { background in BackgroundThumbnailView(background: background) } } } }
-
24:05 - BackgroundThumbnailView
struct BackgroundThumbnailView: View { static let thumbnailSize = CGSize(width:128, height:128) var background: BackyardBackground var body: some View { Image(uiImage: background.thumbnail) } }
-
24:59 - BackgroundThumbnailView with progress (but without loading)
struct BackgroundThumbnailView: View { static let thumbnailSize = CGSize(width:128, height:128) var background: BackyardBackground @State private var image: UIImage? var body: some View { if let image { Image(uiImage: image) } else { ProgressView() .frame(width: Self.thumbnailSize.width, height: Self.thumbnailSize.height, alignment: .center) } } }
-
25:26 - BackgroundThumbnailView with async loading on main thread
struct BackgroundThumbnailView: View { static let thumbnailSize = CGSize(width:128, height:128) var background: BackyardBackground @State private var image: UIImage? var body: some View { if let image { Image(uiImage: image) } else { ProgressView() .frame(width: Self.thumbnailSize.width, height: Self.thumbnailSize.height, alignment: .center) .task { image = background.thumbnail } } } }
-
29:59 - BackgroundThumbnailView with async loading on main thread
struct BackgroundThumbnailView: View { static let thumbnailSize = CGSize(width:128, height:128) var background: BackyardBackground @State private var image: UIImage? var body: some View { if let image { Image(uiImage: image) } else { ProgressView() .frame(width: Self.thumbnailSize.width, height: Self.thumbnailSize.height, alignment: .center) .task { image = background.thumbnail } } } }
-
31:41 - BackgroundThumbnailView with async loading on main thread (simplified)
struct BackgroundThumbnailView: View { // [...] var body: some View { // [...] ProgressView() .task { image = background.thumbnail } // [...] } }
-
33:40 - BackgroundThumbnailView with async loading on main thread
struct BackgroundThumbnailView: View { static let thumbnailSize = CGSize(width:128, height:128) var background: BackyardBackground @State private var image: UIImage? var body: some View { if let image { Image(uiImage: image) } else { ProgressView() .frame(width: Self.thumbnailSize.width, height: Self.thumbnailSize.height, alignment: .center) .task { image = background.thumbnail } } } }
-
33:59 - synchronous thumbnail property
public var thumbnail: UIImage { get { // compute and cache thumbnail } }
-
34:03 - asynchronous thumbnail property
public var thumbnail: UIImage { get async { // compute and cache thumbnail } }
-
34:08 - BackgroundThumbnailView with async loading in background
struct BackgroundThumbnailView: View { static let thumbnailSize = CGSize(width:128, height:128) var background: BackyardBackground @State private var image: UIImage? var body: some View { if let image { Image(uiImage: image) } else { ProgressView() .frame(width: Self.thumbnailSize.width, height: Self.thumbnailSize.height, alignment: .center) .task { image = await background.thumbnail } } } }
-
38:52 - shared property causes blocked main thread
var body: some View { mainContent .task(id: imageMode) { defer { loading = false } do { var image = await background.thumbnail if imageMode == .colorized { let colorizer = ColorizingService.shared image = try await colorizer.colorize(image) } self.image = image } catch { self.error = error } } }
-
39:00 - shared property causes blocked main thread (simplified)
struct ImageTile: View { // [...] // implicit @MainActor var body: some View { mainContent .task() { // inherits @MainActor isolation // [...] let colorizer = ColorizingService.shared result = try await colorizer.colorize(image) } } }
-
39:10 - shared property causes blocked main thread + ColorizingService (simplified)
class ColorizingService { static let shared = ColorizingService() // [...] } struct ImageTile: View { // [...] // implicit @MainActor var body: some View { mainContent .task() { // inherits @MainActor isolation // [...] let colorizer = ColorizingService.shared result = try await colorizer.colorize(image) } } }
-
39:25 - shared synchronous property after await keyword still causes blocked main thread
class ColorizingService { static let shared = ColorizingService() // [...] } struct ImageTile: View { // [...] // implicit @MainActor var body: some View { mainContent .task() { // inherits @MainActor isolation // [...] result = try await ColorizingService.shared.colorize(image) } } }
-
class ColorizingService { static let shared = ColorizingService() func colorize(_ grayscaleImage: CGImage) async throws -> CGImage // [...] } struct ImageTile: View { // [...] // implicit @MainActor var body: some View { mainContent .task() { // inherits @MainActor isolation // [...] result = try await ColorizingService.shared.colorize(image) } } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。