debugging-instruments について
Appleプラットフォームの開発者向けに、LLDB、メモリ問題、ハング、ビルド失敗を扱うデバッグとInstrumentsのスキルガイドです。プロファイリングツールの選び方とxctraceによるコマンドライン記録も説明します。
主な機能
- 01LLDBのブレークポイントと状態確認
- 02メモリグラフとリーク調査
- 03メインスレッドのハング診断
- 04ビルドと依存関係の問題調査
- 05Instrumentsテンプレートの選択
- 06xctraceによるコマンドライン計測
活用シーン
- 01クラッシュや予期しない状態の追跡
- 02リークやメモリ増加の調査
- 03操作時のハングや処理負荷の分析
- 04ビルド失敗と性能低下の調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ dpearson2699/ swift-ios-skills --skill debugging-instrumentsインストール後、Claude Code で/debugging-instrumentsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
evals/evals.jsonreferences/instruments-guide.mdlldb-patterns.md詳細
デバッグと Instruments
対話型グラフと Instruments によるトリアージはここで行います。詳細な .memgraph
コマンドラインによる所有関係・増加分析と ETTrace の作業は、それぞれ専用のスキルに振り分けてください。
目次
LLDB によるデバッグ
まずは、簡潔で再現性の高いワークフローから始めます。
- Debug ビルドで問題を再現し、必要な範囲に絞ったブレークポイントで停止します。
- コードを実行せずにローカル変数を確認し、現在のスタックを記録します。
- 関連するフレームまたはスレッドに移動して、問題が発生している状態を確認します。
- 問題のある状態遷移がまだ不明な場合に限り、条件またはウォッチポイントを追加します。
(lldb) br set -f ViewModel.swift -l 42 # Stop at file and line
(lldb) v myLocal # Inspect without executing code
(lldb) po myObject # Use debugDescription when needed
(lldb) bt all # Capture every thread's backtrace
(lldb) frame select 3 # Inspect a relevant frame
(lldb) br modify 1 -c "count > 10" # Narrow a noisy breakpoint
(lldb) w set v self.score # Stop on an unexpected write
ローカル変数の値だけを確認する場合は、po ではなく v を使います。コードを実行せず、副作用を引き起こすこともありません。式の評価はプログラムの状態を実行または変更する可能性があり、ハードウェアウォッチポイントも限られているため、どちらも必要な場合に限って使用してください。
確認、ブレークポイント/ログポイント、式、ウォッチポイント、スレッドの移動、シンボリックブレークポイントに関するコマンド表の全体については、references/lldb-patterns.md を参照してください。
メモリのデバッグ
Memory Graph Debugger のワークフロー
- Debug 構成でアプリを実行します。
- リークの疑いがある状況を再現します(画面に移動してから戻ります)。
- Xcode のデバッグバーにある Memory Graph ボタンをタップします。
- 紫色の警告アイコンを探します。これはオブジェクトのリークを示しています。
- リークしたオブジェクトを選択し、参照グラフとバックトレースを確認します。
実行前に Malloc Stack Logging(Scheme > Diagnostics)を有効にしておくと、Memory Graph に割り当てのバックトレースが表示されます。
よくある保持サイクルのパターン
self を強参照でキャプチャするクロージャ:
// LEAK — closure holds strong reference to self
class ProfileViewModel {
var onUpdate: (() -> Void)?
func startObserving() {
onUpdate = {
self.refresh() // strong capture of self
}
}
}
// FIXED — use [weak self]
func startObserving() {
onUpdate = { [weak self] in
self?.refresh()
}
}
デリゲートへの強参照:
// LEAK — strong delegate creates a cycle
protocol DataDelegate: AnyObject {
func didUpdate()
}
class DataManager {
var delegate: DataDelegate? // should be weak
}
// FIXED — weak delegate
class DataManager {
weak var delegate: DataDelegate?
}
ターゲットを保持するタイマー:
// LEAK — Timer.scheduledTimer retains its target
timer = Timer.scheduledTimer(
timeInterval: 1.0, target: self,
selector: #selector(tick), userInfo: nil, repeats: true
)
// FIXED — use closure-based API with [weak self]
timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in
self?.tick()
}
Instruments:Allocations と Leaks
- Allocations テンプレート:時間経過に伴うメモリの増加を追跡します。「Mark Generation」機能を使うと、ユーザー操作(例:画面の表示/終了)の間に作成された割り当てを切り分けられます。
- Leaks テンプレート:プロセスから到達できなくなった孤立した保持サイクルを含め、リークした割り当てを検出します。全体像を把握するため、Allocations と併せて実行してください。
- アプリのモジュール名でフィルタリングし、システムの割り当てを除外します。
リークやメモリ増加のトリアージでは、ツールを組み合わせます。再現の前後に Allocations の Mark Generation を使用して保持された増加を確認し、次に Memory Graph Debugger でオブジェクトの所有関係を調べ、Malloc Stack Logging で割り当て時のコールスタックを取得します。
Malloc Stack Logging
Scheme > Run > Diagnostics > Malloc Stack Logging で有効にします。これにより割り当てのバックトレースが記録され、Memory Graph Debugger、Allocations インストゥルメント、エクスポートした .memgraph ファイルでオブジェクトの作成場所を確認できます。
# Inspect an exported memory graph from Xcode or Instruments
leaks MyApp.memgraph
ハングの診断
メインスレッドのハングを特定する
個別の操作では、100 ms 未満の遅延はほとんど気になりません。Apple の開発者向けツールでは、メインランループのビジー状態が 250 ms を超えると一般的に報告されますが、この報告しきい値は製品の目標値ではありません。数百ミリ秒でも反応が遅いと感じることがあります。一般的な検出ツールは次のとおりです。
- Thread Checker(Xcode Diagnostics):メインスレッド以外からの UI 呼び出しを警告します
- Thread Performance Checker:デバッグ中に優先度の逆転を報告します
- デバイス上のハング検出:Developer Settings で、デバイス使用中に発生したハングを報告します
- Time Profiler / CPU Profiler / Hitches:再現可能なハングをプロファイルします
- os_signpost と
OSSignposter:Instruments 用に区間を記録します - MetricKit のハング診断:本番環境でのハングを検出します(
HangDiagnosticと iOS 26 との互換性についてはmetrickitスキルを参照)
import os
let signposter = OSSignposter(subsystem: "com.example.app", category: "DataLoad")
func loadData() async {
let state = signposter.beginInterval("loadData")
let result = await fetchFromNetwork()
signposter.endInterval("loadData", state)
process(result)
}
Time Profiler の使用
- Product > Profile(Cmd+I)を選択して Instruments を起動します。
- Time Profiler テンプレートを選択します。
- 動作が遅い操作を再現しながら記録します。
- メインスレッドに注目します。「Weight」で並べ替えて、負荷の高いコードパスを見つけます。
- 「Hide System Libraries」をオンにして、自分のコードだけを表示します。
- 負荷の高いフレームをダブルクリックすると、ソースコードに移動できます。
ハングの一般的な原因
| 原因 | 症状 | 対処法 |
|---|---|---|
| メインスレッドでの同期 I/O | ネットワーク/ファイルの読み込みで UI がブロックされる | Task { } またはバックグラウンドアクターに移動する |
| ロック競合 | バックグラウンド処理が保持するロックをメインスレッドが待機する | アクターを使用するか、ロックの適用範囲を小さくする |
| レイアウトの頻繁な再計算 | layoutSubviews が繰り返し呼び出される | レイアウトの変更をまとめ、強制的なレイアウトを避ける |
| 大きなペイロードの JSON 解析 | データ読み込み中に UI がフリーズする | バックグラウンドスレッドで解析する |
| 同期的な画像デコード | 画像の多いリストでスクロールがカクつく | AsyncImage を使用するか、メインスレッド以外でデコードする |
ビルド失敗のトリアージ
コンパイラ診断の読み方
- 最初のエラーから確認します。後続のエラーは、最初のエラーに連鎖していることがよくあります。
- ビルドログでエラーコード(例:
error: cannot convert)を検索します。 - Report Navigator(Cmd+9)を使うと、タイムスタンプ付きのビルドログ全体を確認できます。
SPM の依存関係の解決
# Common: version conflict
error: Dependencies could not be resolved because root depends on 'Package' 1.0.0..<2.0.0
# Fix: check Package.resolved and update version ranges
# Reset package caches if needed:
rm -rf ~/Library/Caches/org.swift.swiftpm
rm -rf .build
swift package resolve
モジュールが見つからない/リンカーエラー
| エラー | 確認項目 |
|---|---|
No such module 'Foo' | ターゲットメンバーシップ、インポートパス、フレームワーク検索パス |
Undefined symbol | リンクフェーズでのフレームワークの指定漏れ、アーキテクチャの誤り |
duplicate symbol | 2 つのターゲットが同じシンボルを定義していないか。Objective-C の命名衝突を確認 |
最初に確認するビルド設定:
FRAMEWORK_SEARCH_PATHSOTHER_LDFLAGSSWIFT_INCLUDE_PATHSBUILD_LIBRARY_FOR_DISTRIBUTION(XCFrameworks 用)
Instruments の概要
テンプレートの選び方
| テンプレート | 使用する状況 |
|---|---|
| Time Profiler | CPU 使用率が高い、UI の動作が遅い、負荷の高いコードパスを見つけたい場合 |
| Allocations | 時間の経過とともにメモリ使用量が増える、オブジェクトのライフタイムを追跡したい場合 |
| Leaks | retain cycle や解放されていないオブジェクトが疑われる場合 |
| Network | HTTP リクエスト/レスポンスのタイミングとペイロードを調べる場合 |
| SwiftUI | ビューの body 評価と更新頻度をプロファイリングする場合 |
| Animation Hitches / Core Animation instruments | フレーム落ち、カクつき、ブレンディング、コミット/レンダリング処理を調べる場合 |
| Power Profiler | バッテリー消費、熱負荷、バックグラウンドでの電力への影響を調べる場合 |
| File Activity | ディスク I/O が過剰、ファイル操作が遅い場合 |
| System Trace | スレッドのスケジューリング、システムコール、仮想メモリのページフォールトを調べる場合 |
CI プロファイリング用の xctrace CLI
# Record a trace from the command line
xcrun xctrace record --device "My iPhone" \
--template "Time Profiler" \
--instrument "Allocations" \
--output profile.trace \
--launch -- /path/to/MyApp.app
# Export trace data as XML for automated analysis
xcrun xctrace export --input profile.trace --xpath '/trace-toc/run/data/table'
# List available templates
xcrun xctrace list templates
# List connected devices
xcrun xctrace list devices
記録ごとに --template は 1 つ指定します。追加のインスツルメントは
--instrument で指定します。CI パイプラインで xctrace を使うと、パフォーマンスのリグレッションを
自動的に検出できます。ビルド間でエクスポートしたメトリクスを比較します。
よくある間違い
やってはいけないこと:デバッグに os.Logger ではなく print() を使う
Logger を使うと、ログレベル、プライバシーメタデータ、サブシステム/カテゴリによるフィルタリングを利用できます。
.debug はメモリには保持されますが、リリースビルドでは永続化されません。
// WRONG — unstructured and not filterable by subsystem/category
print("user tapped button, state: \(viewModel.state)")
print("network response: \(data)")
// CORRECT — structured logging with Logger
import os
let logger = Logger(subsystem: "com.example.app", category: "UI")
logger.debug("Button tapped, state: \(viewModel.state, privacy: .public)")
logger.info("Network response received, bytes: \(data.count)")
やってはいけないこと:メモリのデバッグ前に Malloc Stack Logging を有効にし忘れる
// WRONG — open Memory Graph without enabling Malloc Stack Logging
// Result: leaked objects visible but no allocation backtrace
// CORRECT — enable BEFORE running:
// Scheme > Run > Diagnostics > check "Malloc Stack Logging: All Allocations"
// Then run, reproduce the leak, and open Memory Graph
やってはいけないこと:最適化されたコードで、すべての変数を確認できると思ってデバッグする
// WRONG — profiling with Debug build, debugging with Release build
// Debug builds: extra runtime checks distort perf measurements
// Release builds: variables show as "<optimized out>" in debugger
// CORRECT approach:
// Debugging: use Debug configuration (full symbols, no optimization)
// Profiling: use Release configuration (realistic performance)
やってはいけないこと:条件付きブレークポイントを使わずに、ループのすべての反復で停止する
// WRONG — breakpoint on line inside loop, stops 10,000 times
for item in items {
process(item) // breakpoint here stops on EVERY item
}
// CORRECT — use a conditional breakpoint:
// (lldb) br set -f MyFile.swift -l 42 -c "item.id == targetID"
// Or in Xcode: right-click breakpoint > Edit > add Condition
やってはいけないこと:Thread Sanitizer の警告を無視する
Thread Sanitizer(TSan)の警告は、断続的にしかクラッシュしない可能性のあるデータ競合を示しています。ツールの問題だと切り分けられていない限り、実際のバグとして扱ってください。
// WRONG — ignoring TSan warning about concurrent access
var cache: [String: Data] = [:] // accessed from multiple threads
// CORRECT — protect shared mutable state
actor CacheActor {
var cache: [String: Data] = [:]
func get(_ key: String) -> Data? { cache[key] }
func set(_ key: String, _ value: Data) { cache[key] = value }
}
TSan を有効にするには、Scheme > Run > Diagnostics > Thread Sanitizer の順に選択します。iOS、iPadOS、 tvOS、visionOS、watchOS のアプリでは、Simulator 上で TSan を実行してください。Apple のドキュメントでは、デバイスのサポートは 64 ビット macOS アプリのみとされています。
レビューチェックリスト
- 診断出力には
print()ではなくos.Loggerを使用する - メモリデバッグセッションの前に Malloc Stack Logging を有効にする
- dismiss/dealloc フローの後に Memory Graph Debugger を確認する
- 保持サイクルを防ぐため、デリゲートを
weak varとして宣言する - プロパティとして保持するクロージャでは、キャプチャリストに
[weak self]を使用する - タイマーでは
[weak self]を使用したクロージャベースの API を使う - 競合の切り分けに備え、Simulator のテストスキームで Thread Sanitizer を有効にする
- メインスレッドで同期 I/O や負荷の高い計算を行わない
- パフォーマンスの基準値を把握するため、Release ビルドで Time Profiler を実行する
- ビルド失敗は、ビルドログの最初のエラーから切り分ける
- カスタムのパフォーマンス計測区間には
OSSignposterを使用する - ループやコレクションのデバッグには条件付きブレークポイントを使用する
参考資料
- ロギング(統合ログシステム)
- Logger
- OSSignposter
- コードからログメッセージを生成する
- パフォーマンスデータの記録(サインポスト)
- メモリ、スレッド、クラッシュの問題を早期に診断する
- データ競合
- アプリのメモリ使用量を削減する
- Instruments を使用してアプリをプロファイリングする
- アプリの応答性を向上させる
- アプリのバッテリー使用状況を分析する
- リリース済みアプリのパフォーマンスを分析する
- LLDB コマンドリファレンス:references/lldb-patterns.md
- Instruments テンプレートガイド:references/instruments-guide.md