mcpField
A
axiom-hang-diagnostics

axiom-hang-diagnostics

github.com/charleswiltgen開発1.2k170 回インストール今日更新
リポジトリ

axiom-hang-diagnostics について

Swiftアプリ開発者向けのハング診断スキルです。メインスレッドの処理過多とブロックを見分け、Instruments、Xcode Organizer、MetricKitで原因を調べ、無応答を防ぐ方法を説明します。

主な機能

  1. 01ハング、ヒッチ、遅延の区別
  2. 02Time Profilerによるメインスレッド分析
  3. 03System Traceによるブロック調査
  4. 04OrganizerとMetricKitの診断情報の確認
  5. 05一般的な原因と非同期修正例

活用シーン

  1. 01ユーザー報告の画面フリーズ調査
  2. 02本番環境のハングレポート分析
  3. 03アプリ遷移中のウォッチドッグ終了の診断

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/charleswiltgen/axiom --skill axiom-hang-diagnostics

インストール後、Claude Code で/axiom-hang-diagnosticsを入力して読み込まれたか確認します。

詳細

ハングの診断

アプリのハングを体系的に診断し、解決します。ハングとは、メインスレッドが1秒を超えてブロックされ、アプリがユーザー入力に応答しなくなる状態です。

注意すべき兆候 — 次の場合はこのスキルを確認してください

症状このスキルの対象
使用中にアプリが一時的にフリーズするはい — ハングの可能性があります
UIがタッチに反応しないはい — メインスレッドがブロックされています
「App not responding」というシステムダイアログが表示されるはい — 深刻なハングです
Xcode Organizerにハング診断が表示されるはい — 実機でのハングレポートです
MetricKit MXHangDiagnosticを受信したはい — 集計されたハングデータです
アニメーションがカクついたり、途中で止まったりする場合によります — ハングではなく、ヒッチの可能性があります
アプリの動作が遅く感じるが、応答はあるいいえ — パフォーマンスの問題であり、ハングではありません

ハングとは

ハングとは、メインのランループが1秒を超えてイベントを処理できない状態です。ユーザーがタップしても何も起きません。

User taps → Main thread busy/blocked → Event queued → 1+ second delay → HANG

重要な違い:メインスレッドは、すべてのユーザー入力を処理します。メインスレッドがビジーまたはブロックされると、UI全体がフリーズします。

ハング、ヒッチ、遅延の違い
問題継続時間ユーザーエクスペリエンスツール
ハング1秒超アプリがフリーズし、応答しないTime Profiler、System Trace
ヒッチ1~3フレーム(16~50ms)アニメーションがカクつくAnimation Hitches instrument
遅延100~500ms遅く感じるが、応答はあるTime Profiler

このスキルではハングを扱います。 ヒッチについては axiom-swiftui-performance をご覧ください。一般的な遅延については axiom-performance-profiling をご覧ください。

ハングの2つの原因

ハングの根本原因は、次の2つのいずれかです。

1. メインスレッドがビジー

メインスレッドがイベントを処理せず、何らかの作業をしています。

サブカテゴリ:

種類例修正方法
先回りした作業ユーザーが要求していないデータを事前計算する遅延初期化し、必要になったときに計算する
不要な作業関連する通知だけでなく、すべての通知を処理する通知をフィルタリングし、対象を絞ったオブザーバーを使う
最適でないAPI非同期APIがあるのにブロッキングAPIを使う非同期APIに切り替える
2. メインスレッドがブロックされている

メインスレッドが、別の処理を待っています。

サブカテゴリ:

種類例修正方法
同期IPCシステムサービスを同期的に呼び出す非同期API版を使う
ファイルI/Oメインスレッドで Data(contentsOf:) を実行するバックグラウンドキューに移す
ネットワーク同期的なURLリクエストURLSessionの非同期APIを使う
ロック競合バックグラウンドスレッドが保持するロックを待つクリティカルセクションを短縮し、アクターを使う
セマフォ/dispatch_syncバックグラウンド処理をブロッキングして待つ非同期の完了処理を使う構成に変更する

意思決定ツリー — ハングの診断

START: App hangs reported
  │
  ├─→ Do you have hang diagnostics from Organizer or MetricKit?
  │     │
  │     ├─→ YES: Examine stack trace
  │     │     │
  │     │     ├─→ Stack shows your code running
  │     │     │     → BUSY: Main thread doing work
  │     │     │     → Profile with Time Profiler
  │     │     │
  │     │     └─→ Stack shows waiting (semaphore, lock, dispatch_sync)
  │     │           → BLOCKED: Main thread waiting
  │     │           → Profile with System Trace
  │     │
  │     └─→ NO: Can you reproduce?
  │           │
  │           ├─→ YES: Profile with Time Profiler first
  │           │     │
  │           │     ├─→ High CPU on main thread
  │           │     │     → BUSY: Optimize the work
  │           │     │
  │           │     └─→ Low CPU, thread blocked
  │           │           → Use System Trace to find what's blocking
  │           │
  │           └─→ NO: Enable MetricKit in app
  │                 → Wait for field reports
  │                 → Check Organizer > Hangs

ツールの選択

シナリオ主なツール理由
ローカルで再現できるTime Profilerメインスレッドが何をしているか正確に確認できます
スレッドのブロックが疑われるSystem Traceスレッドの状態やロック競合を確認できます
実機でのレポートのみXcode Organizer集計されたハング診断を確認できます
アプリ内のデータが必要MetricKitコールスタックを含むMXHangDiagnosticを取得できます
正確なタイミングが必要System Traceナノ秒レベルのスレッド解析ができます

ハングを調べるためのTime Profilerワークフロー

  1. Instrumentsを起動する → Time Profilerテンプレートを選択する
  2. ハング中に記録する → フリーズを再現する
  3. 記録を停止する → タイムラインでハングしていた期間を見つける
  4. ハング領域を選択する → フリーズしていた時間範囲をドラッグして選択する
  5. コールツリーを調べる → メインスレッドの処理を確認する

確認する項目:

  • メインスレッドで「Self Time」が長い関数
  • 予想外に深いコールスタック
  • メインスレッドで実行すべきでないシステムコール

ハングがブロックされた場合のシステムトレースワークフロー

  1. Instruments を起動 → System Trace テンプレートを選択します
  2. ハング中に記録 → スレッドの状態をキャプチャします
  3. メインスレッドを見つける → メインスレッドに絞り込みます
  4. 赤色/オレンジ色を確認 → ブロック状態を確認します
  5. ブロックの原因を調べる → ロック、セマフォ、IPC

スレッドの状態:

  • 実行中(青):コードを実行中です
  • プリエンプト(オレンジ):実行可能ですが、スケジュールされていません
  • ブロック中(赤):リソースを待機中です

よくあるハングのパターンと修正方法

パターン 1:同期ファイル I/O

修正前(ハングします):

// Main thread blocks on file read
func loadUserData() {
    let data = try! Data(contentsOf: largeFileURL)  // BLOCKS
    processData(data)
}

修正後(非同期):

func loadUserData() {
    Task.detached {
        let data = try Data(contentsOf: largeFileURL)
        await MainActor.run {
            self.processData(data)
        }
    }
}
パターン 2:フィルタリングされていない通知オブザーバー

修正前(すべてを処理します):

NotificationCenter.default.addObserver(
    self,
    selector: #selector(handleChange),
    name: .NSManagedObjectContextObjectsDidChange,
    object: nil  // Receives ALL contexts
)

修正後(フィルタリング済み):

NotificationCenter.default.addObserver(
    self,
    selector: #selector(handleChange),
    name: .NSManagedObjectContextObjectsDidChange,
    object: relevantContext  // Only this context
)
パターン 3:コストの高いフォーマッターの作成

修正前(毎回作成します):

func formatDate(_ date: Date) -> String {
    let formatter = DateFormatter()  // EXPENSIVE
    formatter.dateStyle = .medium
    return formatter.string(from: date)
}

修正後(キャッシュ済み):

private static let dateFormatter: DateFormatter = {
    let formatter = DateFormatter()
    formatter.dateStyle = .medium
    return formatter
}()

func formatDate(_ date: Date) -> String {
    Self.dateFormatter.string(from: date)
}
パターン 4:メインスレッドへの dispatch_sync

修正前(デッドロックの危険性):

// From background thread
DispatchQueue.main.sync {  // BLOCKS if main is blocked
    updateUI()
}

修正後(非同期):

DispatchQueue.main.async {
    self.updateUI()
}
パターン 5:非同期処理の結果をセマフォで待機

修正前(メインスレッドをブロックします):

func fetchDataSync() -> Data {
    let semaphore = DispatchSemaphore(value: 0)
    var result: Data?

    URLSession.shared.dataTask(with: url) { data, _, _ in
        result = data
        semaphore.signal()
    }.resume()

    semaphore.wait()  // BLOCKS MAIN THREAD
    return result!
}

修正後(async/await):

func fetchData() async throws -> Data {
    let (data, _) = try await URLSession.shared.data(from: url)
    return data
}
パターン 6:ロックの競合

修正前(共有ロック):

class DataManager {
    private let lock = NSLock()
    private var cache: [String: Data] = [:]

    func getData(for key: String) -> Data? {
        lock.lock()  // Main thread waits for background
        defer { lock.unlock() }
        return cache[key]
    }
}

修正後(アクター):

actor DataManager {
    private var cache: [String: Data] = [:]

    func getData(for key: String) -> Data? {
        cache[key]  // Actor serializes access safely
    }
}
パターン 7:アプリ起動時のハング(Watchdog)

修正前(処理が多すぎます):

func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    loadAllUserData()      // Expensive
    setupAnalytics()       // Network calls
    precomputeLayouts()    // CPU intensive
    return true
}

修正後(後回しにする):

func application(_ application: UIApplication,
                 didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // Only essential setup
    setupMinimalUI()
    return true
}

func applicationDidBecomeActive(_ application: UIApplication) {
    // Defer non-essential work
    Task {
        await loadUserDataInBackground()
    }
}
パターン 8:メインスレッドでの画像処理

修正前(UI をブロックします):

func processImage(_ image: UIImage) {
    let filtered = applyExpensiveFilter(image)  // BLOCKS
    imageView.image = filtered
}

修正後(バックグラウンド処理):

func processImage(_ image: UIImage) {
    imageView.image = placeholder

    Task.detached(priority: .userInitiated) {
        let filtered = applyExpensiveFilter(image)
        await MainActor.run {
            self.imageView.image = filtered
        }
    }
}

Xcode Organizer によるハング診断

Window > Organizer > Select App > Hangs

Organizer には、診断情報の共有に同意したユーザーから集約されたハングデータが表示されます。

レポートの読み方:

  1. ハング率:デバイス 1 台あたり 1 日に発生するハング数
  2. コールスタック:ハングが発生した場所
  3. デバイス/OS の内訳:影響を受けた構成

コールスタックの解釈:

  • 先頭に自分のコードがある場合:メインスレッドがアプリの処理でビジー状態です
  • 先頭にシステム API がある場合:メインスレッドでブロッキング API を呼び出しています
  • pthread_mutex/semaphore:ロックの競合、または明示的な待機です

MetricKit によるハング診断

アプリでハング診断を受け取るには、MetricKit を導入します。

import MetricKit

class MetricsSubscriber: NSObject, MXMetricManagerSubscriber {
    func didReceive(_ payloads: [MXDiagnosticPayload]) {
        for payload in payloads {
            if let hangDiagnostics = payload.hangDiagnostics {
                for diagnostic in hangDiagnostics {
                    analyzeHang(diagnostic)
                }
            }
        }
    }

    private func analyzeHang(_ diagnostic: MXHangDiagnostic) {
        // Duration of the hang
        let duration = diagnostic.hangDuration

        // Call stack tree (needs symbolication)
        let callStack = diagnostic.callStackTree

        // Send to your analytics
        uploadHangDiagnostic(duration: duration, callStack: callStack)
    }
}

主な MXHangDiagnostic プロパティ:

  • hangDuration:ハングが継続した時間
  • callStackTree:フレームを含む MXCallStackTree
  • signatureIdentifier:類似したハングをグループ化するための識別子

Watchdog による終了

Watchdog は、重要な遷移中にハングするアプリを強制終了します。

遷移制限時間結果
アプリの起動約 20 秒アプリが強制終了され、クラッシュが記録されます
バックグラウンドへの遷移約 5 秒アプリが強制終了されます
フォアグラウンドへの遷移約 10 秒アプリが強制終了されます

Watchdog が無効になる環境:

  • シミュレーター
  • デバッガーが接続されている場合
  • 開発ビルド(場合によります)

Watchdog による強制終了はクラッシュとして記録されます。例外タイプは EXC_CRASH (SIGKILL)、終了理由は Namespace RUNNINGBOARD, Code 3735883980(16 進数の 0xDEAD10CC — アプリが一時停止中にファイルロックまたは SQLite データベースロックを保持していたことを示します)です。

プレッシャーシナリオ

シナリオ 1:マネージャーが「ローディングスピナーを追加するだけでいい」と言う

状況:データの読み込み中にアプリがハングします。マネージャーは「修正」するためにスピナーを追加するよう提案します。

これでは解決しない理由:スピナーを追加してもハングは防げません。UI はフリーズしたままで、スピナーもアニメーションせず、アプリは応答しない状態が続きます。

適切な対応:「ハング中はメインスレッドがブロックされるため、スピナーはアニメーションしません。スピナーが実際に回転し、アプリが応答し続けるように、この処理をメインスレッド以外に移す必要があります。」

シナリオ 2:「テストでは問題なく動作します」

状況:QA ではハングを再現できません。ログには、本番環境で発生していることが示されています。

分析:

  1. 実環境のデバイスではデータサイズが異なります
  2. ネットワーク環境は変動します(接続が遅いほど同期に時間がかかります)
  3. バックグラウンドアプリがメモリ/CPU を消費します
  4. デバッグビルドでは Watchdog が無効です

対応:

  • 実環境の診断情報を取得するために MetricKit を追加します
  • 本番環境と同等のサイズのデータセットでテストします
  • デバッガーを接続せずにテストします
  • Organizer でハングレポートを確認します
シナリオ 3:「ずっとこのやり方でやってきた」

状況:レガシーコードがメインスレッドで同期 API を呼び出しています。リファクタリングは「リスクが高すぎる」とされています。

重要な理由:以前は問題なく動作していたとしても、次のような変化が起きている可能性があります。

  • データが大きくなっている
  • OS のアップデートによってタイミングが変わっている
  • 新しいデバイスの特性が異なる
  • アプリの動作が速くなるほど、ユーザーが問題に気付きやすくなる

アプローチ:

  1. メトリクスを追加し、現在のハング率を測定する
  2. フィーチャーフラグを使って段階的にリファクタリングする
  3. A/B テストで改善を示す
  4. 修正しない場合のリスクを文書化する

避けるべきアンチパターン

アンチパターン問題点代わりに
バックグラウンドからの DispatchQueue.main.syncデッドロックする可能性があり、常にブロックします.async を使う
非同期処理を同期処理に変換するためのセマフォ呼び出し元のスレッドをブロックします完了ハンドラー/await を使い、非同期処理のままにする
メインスレッドでのファイル I/Oレイテンシーが予測できませんバックグラウンドキューを使う
フィルターされていない通知オブザーバー関係のないイベントを処理しますオブジェクト/名前でフィルターする
ループ内でのフォーマッターの作成初期化にコストがかかりますキャッシュして再利用する
同期ネットワークリクエストネットワークのレイテンシーでブロックしますURLSession の非同期 API を使う

ハング防止チェックリスト

リリース前に、次の項目を確認してください。

  • メインスレッドで Data(contentsOf:) やファイル読み込みを行っていない
  • バックグラウンドスレッドから DispatchQueue.main.sync を呼び出していない
  • メインスレッドで semaphore.wait() を呼び出していない
  • フォーマッター(DateFormatter、NumberFormatter)がキャッシュされている
  • 通知オブザーバーが適切にフィルターされている
  • 起動時の処理が最小限になっている(必須でない処理は後回しにする)
  • 画像処理がメインスレッド以外で行われている
  • データベースクエリがメインスレッドで実行されていない
  • フィールド診断に MetricKit が採用されている

リソース

WWDC:2021-10258、2022-10082

ドキュメント:/xcode/analyzing-responsiveness-issues-in-your-shipping-app、/metrickit/mxhangdiagnostic

スキル:axiom-metrickit-ref、axiom-performance-profiling、axiom-swift-concurrency、axiom-lldb(フリーズ発生時点での対話的なスレッド検査)

コメント