WWDC Quick Look 💓 By SwiftGGTeam
Visualize and optimize Swift concurrency

Visualize and optimize Swift concurrency

元の動画を見る

ハイライト

Instruments 14 では、タスクとアクターの実行ステータスを視覚化できる新しい Swift Concurrency テンプレートが追加されています。 Mike と Harjas は、ファイル圧縮アプリを使用して、メイン アクターのブロック、アクターのシリアル競合、スレッド プールの枯渇という 3 つの主要なパフォーマンス問題を特定する方法を示し、コードをリファクタリングしてアプリを「UI スタック」から「マルチコア並列処理とスムーズなインターフェイス」に変換しました。

主要内容

Swift Concurrency を使用すると、同時実行コードを正しく書くことが容易になりますが、正しく書くことは、コードを速く書くことを意味するわけではありません。 Mike と Harjas は、3 種類の一般的なパフォーマンスの問題を指摘しました。1 つは UI フリーズの原因となるメイン アクターのブロック、並列処理の低下につながるアクターのシリアル競合、そしてリークやクラッシュにつながるコンティニュエーションの誤用です。 Instruments 14 の新しい Swift Concurrency テンプレートは、これらの問題を捉えて視覚化するように特別に設計されています。 (00:28)

デモ アプリは File Squeezer と呼ばれ、その機能はフォルダー内のファイルをバッチで圧縮することです。小さなファイルでは正常に動作しますが、大きなファイルでは UI が完全にフリーズします。 Harjas は Command-I を使用して Instruments を起動し、Swift Concurrency テンプレートを選択し、実行データを記録します。 (04:18)

まずトップレベルの統計を見てみましょう。実行中のタスクには同時に実行されたタスクの数が表示され、実行中のタスクには生き残っているタスクの数が表示され、合計タスクには作成されたタスクの累積数が表示されます。 Harjas は、ほとんどの場合、実行タスクが 1 つだけであることに気づきました。これは、すべての作業が強制的にシリアル化されることを示しています。彼は Pin 機能を使用して、最も長いタスクをタイムラインに固定しました。 Narrative View は、このタスクが最初は短期間バックグラウンド スレッドで実行され、その後メイン スレッドを長時間占有していることを示しました。これはメインアクターのブロックです。 (08:42)

問題の根本はCompressionState親切。クラス全体がマークされます@MainActor、そしてcompressFileメソッドは時間のかかる計算を実行します。計算は包まれていますが、Task {}ただし、タスクはメイン アクターのコンテキストで作成されるため、メイン アクターの分離ドメインを継承し、時間のかかる計算は依然としてメイン スレッドで実行されます。 (10:24)

解決策は状態を分割することです。filesプロパティは次のとおりです。@Published、Main Actor で更新する必要があります。しかしlogs内部状態のみで、メインスレッドは必要ありません。それで、新しいものを作成しますParallelCompressorアクター、ログステータスと圧縮ロジックをそこに移動します。CompressionStateUI関連を維持するfiles、合格await電話ParallelCompressor方法。 (11:49)

変更後、UI が動かなくなることはなくなりましたが、圧縮速度はまだ十分ではありません。 Harjas は再び Instruments を使用して分析したところ、タスク サマリー内の多数のタスクがキューに入れられ、入力を待っている状態であることがわかりました。ParallelCompressor俳優。その理由は、compressFileこのメソッドは完全にアクター分離ドメイン内で実行され、同時に圧縮を実行できるタスクは 1 つだけです。 (14:43)

2 番目の再構築: putcompressFileとしてマークされていますnonisolated、アクターの分離からそれを取り除きます。このようにして、圧縮計算はスレッド プール内の任意のスレッドで並行して実行でき、必要なのはアクセスのみです。logsアクターに短時間だけ入ります。同時にTask {}に変更しますTask.detached {}、新しいタスクが作成元のアクター コンテキストを継承しないようにします。 (17:46)

変更後、アプリはすべてのファイルを同時に圧縮でき、UI は応答性を維持します。機器検証ディスプレイParallelCompressorアクター上のタスクは短く、キューの長さは常に制御可能です。

詳細

Main Actor ブロックの診断と修復

(04:18) File Squeezer の最初のコードには、すべてのロジックが含まれています。@MainActorカテゴリ内:

@MainActor
class CompressionState: ObservableObject {
    @Published var files: [FileStatus] = []
    var logs: [String] = []
    
    func update(url: URL, progress: Double) {
        if let loc = files.firstIndex(where: {$0.url == url}) {
            files[loc].progress = progress
        }
    }
    
    func compressAllFiles() {
        for file in files {
            Task {
                let compressedData = compressFile(url: file.url)
                await save(compressedData, to: file.url)
            }
        }
    }
    
    func compressFile(url: URL) -> Data {
        log(update: "Starting for \(url)")
        let compressedData = CompressionUtils.compressDataInFile(at: url) { uncompressedSize in
            update(url: url, uncompressedSize: uncompressedSize)
        } progressNotification: { progress in
            update(url: url, progress: progress)
            log(update: "Progress for \(url): \(progress)")
        } finalNotificaton: { compressedSize in
            update(url: url, compressedSize: compressedSize)
        }
        log(update: "Ending for \(url)")
        return compressedData
    }
    
    func log(update: String) {
        logs.append(update)
    }
}

キーポイント:

  • @MainActorクラス全体をメインスレッドで実行させます
  • compressFile時間のかかる操作ですが、@MainActorクラスなので、メインスレッドでも実行されます
  • Task {}Main Actor コンテキストを継承するため、非同期タスクもメイン スレッドで実行されます。
  • インスツルメントのタスク状態サマリーにより、最も長いタスクを見つけることができます。タイムラインにピン留めした後、ナラティブ ビューには、タスクがメイン スレッドで長時間実行されていることが表示されます。

修正: 置くlogs圧縮ロジックは独立したアクターに分割されます。

actor ParallelCompressor {
    var logs: [String] = []
    unowned let status: CompressionState
    
    init(status: CompressionState) {
        self.status = status
    }
    
    func compressFile(url: URL) -> Data {
        log(update: "Starting for \(url)")
        let compressedData = CompressionUtils.compressDataInFile(at: url) { uncompressedSize in
            Task { @MainActor in
                status.update(url: url, uncompressedSize: uncompressedSize)
            }
        } progressNotification: { progress in
            Task { @MainActor in
                status.update(url: url, progress: progress)
                await log(update: "Progress for \(url): \(progress)")
            }
        } finalNotificaton: { compressedSize in
            Task { @MainActor in
                status.update(url: url, compressedSize: compressedSize)
            }
        }
        log(update: "Ending for \(url)")
        return compressedData
    }
    
    func log(update: String) {
        logs.append(update)
    }
}

@MainActor
class CompressionState: ObservableObject {
    @Published var files: [FileStatus] = []
    var compressor: ParallelCompressor!
    
    init() {
        self.compressor = ParallelCompressor(status: self)
    }
    
    func compressAllFiles() {
        for file in files {
            Task {
                let compressedData = await compressor.compressFile(url: file.url)
                await save(compressedData, to: file.url)
            }
        }
    }
}

キーポイント:

  • ParallelCompressor独立した俳優であり、保護していますlogs
  • 進行状況コールバックTask { @MainActor in ... }メインスレッドに戻って UI を更新します
  • CompressionStateだけ保管してください@PublishedUIの状態
  • compressAllFiles合格awaitActor メソッドの呼び出し

Actor シリアル コンペティションの診断と修復

(14:43) 最初のリファクタリングの後、Instruments は、多数のタスクが Enqueued 状態になるのを待っていることを示します。ParallelCompressor。その理由は、compressFileアクター分離ドメイン内で完全に実行され、同時に実行できるタスクは 1 つだけです。

修正: 置くcompressFileとしてマークされていますnonisolated、使用Task.detached独立したタスクを作成します。

actor ParallelCompressor {
    var logs: [String] = []
    unowned let status: CompressionState
    
    init(status: CompressionState) {
        self.status = status
    }
    
    nonisolated func compressFile(url: URL) async -> Data {
        await log(update: "Starting for \(url)")
        let compressedData = CompressionUtils.compressDataInFile(at: url) { uncompressedSize in
            Task { @MainActor in
                status.update(url: url, uncompressedSize: uncompressedSize)
            }
        } progressNotification: { progress in
            Task { @MainActor in
                status.update(url: url, progress: progress)
                await log(update: "Progress for \(url): \(progress)")
            }
        } finalNotificaton: { compressedSize in
            Task { @MainActor in
                status.update(url: url, compressedSize: compressedSize)
            }
        }
        await log(update: "Ending for \(url)")
        return compressedData
    }
    
    func log(update: String) {
        logs.append(update)
    }
}

@MainActor
class CompressionState: ObservableObject {
    @Published var files: [FileStatus] = []
    var compressor: ParallelCompressor!
    
    init() {
        self.compressor = ParallelCompressor(status: self)
    }
    
    func compressAllFiles() {
        for file in files {
            Task.detached {
                let compressedData = await self.compressor.compressFile(url: file.url)
                await save(compressedData, to: file.url)
            }
        }
    }
}

キーポイント:

  • nonisolatedメソッドをアクターの分離からエスケープし、任意のスレッドで実行できるようにします。
  • なぜならnonisolated、アクターの状態にアクセスします (例:log) を追加する必要がありますawait
  • Task.detached作成時にアクター コンテキストを継承せず、タスクがスレッド プール内で自由にスケジュールされるようにします。
  • self切り離されたタスクでは明示的なキャプチャが必要です
  • 圧縮計算を真に並列化できるようになりました。制限は CPU コアの数のみです。

Instruments の Swift Concurrency テンプレート機能

(04:58) Instruments 14 の Swift Concurrency テンプレートには、複数のビューが含まれています。

Swift タスクのインストゥルメント統計:

  • 実行中のタスク: 同時に実行されるタスクの数 -Alive Tasks: 現在の生きているタスクの数
  • 合計タスク: 作成されたタスクの累計数

Task Forest:

  • 構造化された同時実行でのタスクの親子関係をグラフィカルに表示します
  • ウィンドウの下半分にあります

Task Summary:

  • さまざまな状態(実行中、一時停止、キューに入れられているなど)での各タスクの消費時間を表示します。
  • タスクを右クリックしてタイムラインに固定します

タスクをピン留めした後のタイムラインには次の内容が含まれます:

  • タスクステータストラック
  • 拡張機能の詳細でコールスタックを作成する
  • ナラティブ ビュー: 「どのタスクが待機中か」など、タスクの現在のステータスを説明します。
  • ピンサブタスク、スレッド、またはアクターはナラティブビューから続行できます

Swift Actors Instrument:

  • 各アクターのキューの長さとタスクの実行時間を表示します
  • アクターが長時間占有されているかどうかを確認するために使用されます

スレッドプールの枯渇と継続の誤用

(19:46) Mike は、よくある質問を 2 つ追加しました。

スレッド プールが枯渇しました:

  • Swift Concurrency では、タスクを継続的に進める必要があります。
  • ブロック呼び出し (同期ファイル IO、ネットワーク要求、ロック待機) はスレッドを占有しますが、CPU は使用しません
  • スレッド プールは限られており、ブロックするタスクが多すぎると他のタスクが実行できなくなり、極端な場合にはデッドロックが発生します。
  • 解決策: 非同期 API を使用して、ブロッキング呼び出しを置き換えます。ブロッキング コールを使用する必要がある場合は、それらをディスパッチ キューに入れてから、継続を使用してブリッジします。

継続的な悪用:

  • withCheckedContinuationコールバックは呼び出す必要があり、一度しか呼び出すことができません
  • 呼び出し回数ゼロ: タスクが永久にハングし、メモリ リークが発生する
  • 2 回呼び出される: プログラムがクラッシュするか、異常な動作をする
  • 使用withCheckedContinuation(それよりもwithUnsafeContinuation) 実行時に誤用を検出できる
  • インストルメントは、タイムライン上でタスクを継続待機状態にマークします。

重要ポイント

  • 何をすべきか: 同時実行コードを通じて Instruments の Swift Concurrency テンプレートを実行します。 実行する価値がある理由: Harjas のデモは、タスクがどのスレッドで実行されているか、アクターのキューの長さを肉眼で判断するのが難しいことを示しています。 Instruments はタイムライン、タスク ステータス、アクター キューを視覚化し、位置決めの問題を「推測」から「視覚」に変えます。 開始方法: Xcode で Command-I を押し、Swift Concurrency テンプレートを選択し、典型的なワークロードを含む実行データを記録します。まず、実行中のタスクが CPU コアの数に近いかどうかを確認し、次に長時間エンキューされているタスクがないか確認します。

  • やるべきこと: コード内のすべてを確認します。@MainActor時間のかかる操作を削除するためにクラスをマークしました。 実行する価値がある理由: File Squeezer の問題の根本は、クラス全体がマークされていることです。@MainActorこれにより、すべてのメソッドがメインスレッドで実行されます。正しいアプローチは次のとおりです。@Publishedプロパティと UI の更新はメイン アクタに残り、他の状態は独立したアクタを使用するか、nonisolatedメソッドの保護。 開始方法: プロジェクト内を検索します@MainActor、マークされた各クラス/メソッドが時間のかかる操作 (ファイルの読み取りと書き込み、ネットワーク リクエスト、大量の計算) を実行しているかどうかを確認します。その場合は、時間のかかるロジックを独立したアクターまたは独立したタスクに分割します。

  • やるべきこと: アクターのメソッドに追加nonisolated計算ロジックをアクターのシリアル化から分離するためにマークします。 実行する価値がある理由: 最初の再構築後、File Squeezer の UI はスタックしなくなりましたが、圧縮はシリアルのままです。その理由は、compressFile完全にアクター分離ドメイン内にあります。マークnonisolated最後に、計算は通常のスレッド プールで並行して実行され、ログを書き込むときにアクターに短時間だけ入力されます。 開始方法: アクター メソッドを調べて、「このメソッドはほとんどの時間を計算やアクター状態へのアクセスに費やしているでしょうか?」と自問してください。前者の場合は追加しますnonisolated、ステータスアクセスをに変更しますawait

関連セッション

コメント

GitHub Issues · utterances