axiom-hang-diagnostics by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-hang-diagnostics系统性地诊断和解决应用卡顿问题。当主线程被阻塞超过 1 秒,导致应用对用户输入无响应时,即发生卡顿。
| 症状 | 此技能适用 |
|---|---|
| 使用过程中应用短暂冻结 | 是 — 可能是卡顿 |
| UI 不响应触摸 | 是 — 主线程被阻塞 |
| 系统弹出“应用无响应”对话框 | 是 — 严重卡顿 |
| Xcode Organizer 显示卡顿诊断 | 是 — 现场卡顿报告 |
| 收到 MetricKit MXHangDiagnostic | 是 — 聚合的卡顿数据 |
| 动画卡顿或跳帧 | 可能 — 可能是卡顿,而非卡死 |
| 应用感觉慢但有响应 | 否 — 性能问题,非卡顿 |
卡顿 是指主运行循环超过 1 秒无法处理事件。用户点击,但没有任何反应。
User taps → Main thread busy/blocked → Event queued → 1+ second delay → HANG
关键区别:主线程处理所有用户输入。如果它繁忙或被阻塞,整个 UI 都会冻结。
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
| 问题 | 持续时间 | 用户体验 | 工具 |
|---|---|---|---|
| 卡顿 | >1 秒 | 应用冻结,无响应 | Time Profiler, System Trace |
| 卡顿 | 1-3 帧 (16-50ms) | 动画卡顿 | Animation Hitches instrument |
| 延迟 | 100-500ms | 感觉慢但有响应 | Time Profiler |
此技能涵盖卡顿。 关于卡顿,请参阅 axiom-swiftui-performance。关于一般延迟,请参阅 axiom-performance-profiling。
每个卡顿都有以下两种根本原因之一:
主线程正在执行工作,而不是处理事件。
子类别:
| 类型 | 示例 | 修复方法 |
|---|---|---|
| 主动工作 | 预计算用户未请求的数据 | 延迟初始化,按需计算 |
| 无关工作 | 处理所有通知,而不仅仅是相关通知 | 过滤通知,针对性观察者 |
| 次优 API | 存在异步 API 时使用阻塞 API | 切换到异步 API |
主线程正在等待其他东西。
子类别:
| 类型 | 示例 | 修复方法 |
|---|---|---|
| 同步 IPC | 同步调用系统服务 | 使用异步 API 变体 |
| 文件 I/O | 在主线程上使用 Data(contentsOf:) | 移到后台队列 |
| 网络 | 同步 URL 请求 | 使用 URLSession 异步 |
| 锁竞争 | 等待后台线程持有的锁 | 减少临界区,使用 actor |
| 信号量/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 | 纳秒级线程分析 |
需要关注的内容:
线程状态:
修复前 (卡顿) :
// 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)
}
}
}
修复前 (处理所有通知) :
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
)
修复前 (每次都创建) :
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)
}
修复前 (死锁风险) :
// From background thread
DispatchQueue.main.sync { // BLOCKS if main is blocked
updateUI()
}
修复后 (异步) :
DispatchQueue.main.async {
self.updateUI()
}
修复前 (阻塞主线程) :
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
}
修复前 (共享锁) :
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) :
actor DataManager {
private var cache: [String: Data] = [:]
func getData(for key: String) -> Data? {
cache[key] // Actor serializes access safely
}
}
修复前 (工作太多) :
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()
}
}
修复前 (阻塞 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
}
}
}
Window > Organizer > Select App > Hangs
Organizer 显示来自选择共享诊断信息的用户的聚合卡顿数据。
阅读报告:
解释调用堆栈:
采用 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: 包含帧的 MXCallStackTreesignatureIdentifier: 用于分组相似的卡顿看门狗会杀死在关键转换期间卡顿的应用:
| 转换 | 时间限制 | 后果 |
|---|---|---|
| 应用启动 | ~20 秒 | 应用被杀死,记录崩溃 |
| 后台转换 | ~5 秒 | 应用被杀死 |
| 前台转换 | ~10 秒 | 应用被杀死 |
看门狗在以下情况被禁用:
看门狗杀死记录为崩溃,异常类型为 EXC_CRASH (SIGKILL),终止原因为 Namespace RUNNINGBOARD, Code 3735883980(十六进制 0xDEAD10CC — 表示应用在挂起时持有文件锁或 SQLite 数据库锁)。
情况 : 应用在数据加载时卡顿。经理建议添加加载动画来“修复”它。
为何失败 : 添加加载动画并不能防止卡顿 — UI 仍然会冻结,加载动画不会动,应用仍然无响应。
正确回应 : “在卡顿期间加载动画不会动,因为主线程被阻塞了。我们需要把这个工作移出主线程,这样加载动画才能真正旋转,应用才能保持响应。”
情况 : QA 无法复现卡顿。日志显示它发生在生产环境。
分析 :
行动 :
情况 : 遗留代码在主线程上调用同步 API。重构“风险太大”。
为何重要 : 即使以前能工作:
方法 :
| 反模式 | 为何错误 | 替代方案 |
|---|---|---|
从后台线程使用 DispatchQueue.main.sync | 可能导致死锁,总是阻塞 | 使用 .async |
| 使用信号量将异步转换为同步 | 阻塞调用线程 | 保持异步,使用 completion/await |
| 在主线程上进行文件 I/O | 不可预测的延迟 | 后台队列 |
| 未过滤的通知观察者 | 处理无关事件 | 按对象/名称过滤 |
| 在循环中创建格式化器 | 昂贵的初始化 | 缓存并重用 |
| 同步网络请求 | 阻塞于网络延迟 | URLSession 异步 |
发布前,请验证:
Data(contentsOf:) 或文件读取DispatchQueue.main.syncsemaphore.wait()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 (在冻结点进行交互式线程检查)
每周安装量
94
代码仓库
GitHub 星标数
610
首次出现
2026年1月21日
安全审计
安装于
opencode78
claude-code74
codex72
gemini-cli71
cursor71
github-copilot67
Systematic diagnosis and resolution of app hangs. A hang occurs when the main thread is blocked for more than 1 second, making the app unresponsive to user input.
| Symptom | This Skill Applies |
|---|---|
| App freezes briefly during use | Yes — likely hang |
| UI doesn't respond to touches | Yes — main thread blocked |
| "App not responding" system dialog | Yes — severe hang |
| Xcode Organizer shows hang diagnostics | Yes — field hang reports |
| MetricKit MXHangDiagnostic received | Yes — aggregated hang data |
| Animations stutter or skip | Maybe — could be hitch, not hang |
| App feels slow but responsive | No — performance issue, not hang |
A hang is when the main runloop cannot process events for more than 1 second. The user taps, but nothing happens.
User taps → Main thread busy/blocked → Event queued → 1+ second delay → HANG
Key distinction : The main thread handles ALL user input. If it's busy or blocked, the entire UI freezes.
| Issue | Duration | User Experience | Tool |
|---|---|---|---|
| Hang | >1 second | App frozen, unresponsive | Time Profiler, System Trace |
| Hitch | 1-3 frames (16-50ms) | Animation stutters | Animation Hitches instrument |
| Lag | 100-500ms | Feels slow but responsive | Time Profiler |
This skill covers hangs. For hitches, see axiom-swiftui-performance. For general lag, see axiom-performance-profiling.
Every hang has one of two root causes:
The main thread is doing work instead of processing events.
Subcategories :
| Type | Example | Fix |
|---|---|---|
| Proactive work | Pre-computing data user hasn't requested | Lazy initialization, compute on demand |
| Irrelevant work | Processing all notifications, not just relevant ones | Filter notifications, targeted observers |
| Suboptimal API | Using blocking API when async exists | Switch to async API |
The main thread is waiting for something else.
Subcategories :
| Type | Example | Fix |
|---|---|---|
| Synchronous IPC | Calling system service synchronously | Use async API variant |
| File I/O | Data(contentsOf:) on main thread | Move to background queue |
| Network | Synchronous URL request | Use URLSession async |
| Lock contention | Waiting for lock held by background thread | Reduce critical section, use actors |
| Semaphore/dispatch_sync | Blocking on background work | Restructure to async completion |
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
| Scenario | Primary Tool | Why |
|---|---|---|
| Reproduces locally | Time Profiler | See exactly what main thread is doing |
| Blocked thread suspected | System Trace | Shows thread state, lock contention |
| Field reports only | Xcode Organizer | Aggregated hang diagnostics |
| Want in-app data | MetricKit | MXHangDiagnostic with call stacks |
| Need precise timing | System Trace | Nanosecond-level thread analysis |
What to look for :
Thread states :
Before (hangs) :
// Main thread blocks on file read
func loadUserData() {
let data = try! Data(contentsOf: largeFileURL) // BLOCKS
processData(data)
}
After (async) :
func loadUserData() {
Task.detached {
let data = try Data(contentsOf: largeFileURL)
await MainActor.run {
self.processData(data)
}
}
}
Before (processes all) :
NotificationCenter.default.addObserver(
self,
selector: #selector(handleChange),
name: .NSManagedObjectContextObjectsDidChange,
object: nil // Receives ALL contexts
)
After (filtered) :
NotificationCenter.default.addObserver(
self,
selector: #selector(handleChange),
name: .NSManagedObjectContextObjectsDidChange,
object: relevantContext // Only this context
)
Before (creates each time) :
func formatDate(_ date: Date) -> String {
let formatter = DateFormatter() // EXPENSIVE
formatter.dateStyle = .medium
return formatter.string(from: date)
}
After (cached) :
private static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
return formatter
}()
func formatDate(_ date: Date) -> String {
Self.dateFormatter.string(from: date)
}
Before (deadlock risk) :
// From background thread
DispatchQueue.main.sync { // BLOCKS if main is blocked
updateUI()
}
After (async) :
DispatchQueue.main.async {
self.updateUI()
}
Before (blocks main thread) :
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!
}
After (async/await) :
func fetchData() async throws -> Data {
let (data, _) = try await URLSession.shared.data(from: url)
return data
}
Before (shared lock) :
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]
}
}
After (actor) :
actor DataManager {
private var cache: [String: Data] = [:]
func getData(for key: String) -> Data? {
cache[key] // Actor serializes access safely
}
}
Before (too much work) :
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
loadAllUserData() // Expensive
setupAnalytics() // Network calls
precomputeLayouts() // CPU intensive
return true
}
After (deferred) :
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()
}
}
Before (blocks UI) :
func processImage(_ image: UIImage) {
let filtered = applyExpensiveFilter(image) // BLOCKS
imageView.image = filtered
}
After (background processing) :
func processImage(_ image: UIImage) {
imageView.image = placeholder
Task.detached(priority: .userInitiated) {
let filtered = applyExpensiveFilter(image)
await MainActor.run {
self.imageView.image = filtered
}
}
}
Window > Organizer > Select App > Hangs
The Organizer shows aggregated hang data from users who opted into sharing diagnostics.
Reading the report :
Interpreting call stacks :
Adopt MetricKit to receive hang diagnostics in your app:
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)
}
}
Key MXHangDiagnostic properties :
hangDuration: How long the hang lastedcallStackTree: MXCallStackTree with framessignatureIdentifier: For grouping similar hangsThe watchdog kills apps that hang during key transitions:
| Transition | Time Limit | Consequence |
|---|---|---|
| App launch | ~20 seconds | App killed, crash logged |
| Background transition | ~5 seconds | App killed |
| Foreground transition | ~10 seconds | App killed |
Watchdog disabled in :
Watchdog kills are logged as crashes with exception type EXC_CRASH (SIGKILL) and termination reason Namespace RUNNINGBOARD, Code 3735883980 (hex 0xDEAD10CC — indicates app held a file lock or SQLite database lock while being suspended).
Situation : App hangs during data load. Manager suggests adding spinner to "fix" it.
Why this fails : Adding a spinner doesn't prevent the hang—the UI still freezes, the spinner won't animate, and the app remains unresponsive.
Correct response : "A spinner won't animate during a hang because the main thread is blocked. We need to move this work off the main thread so the spinner can actually spin and the app stays responsive."
Situation : QA can't reproduce the hang. Logs show it happens in production.
Analysis :
Action :
Situation : Legacy code calls synchronous API on main thread. Refactoring is "too risky."
Why it matters : Even if it worked before:
Approach :
| Anti-Pattern | Why It's Wrong | Instead |
|---|---|---|
DispatchQueue.main.sync from background | Can deadlock, always blocks | Use .async |
| Semaphore to convert async to sync | Blocks calling thread | Stay async with completion/await |
| File I/O on main thread | Unpredictable latency | Background queue |
| Unfiltered notification observer | Processes irrelevant events | Filter by object/name |
| Creating formatters in loops | Expensive initialization | Cache and reuse |
| Synchronous network request | Blocks on network latency | URLSession async |
Before shipping, verify:
Data(contentsOf:) or file reads on main threadDispatchQueue.main.sync from background threadsWWDC : 2021-10258, 2022-10082
Docs : /xcode/analyzing-responsiveness-issues-in-your-shipping-app, /metrickit/mxhangdiagnostic
Skills : axiom-metrickit-ref, axiom-performance-profiling, axiom-swift-concurrency, axiom-lldb (interactive thread inspection at freeze point)
Weekly Installs
94
Repository
GitHub Stars
610
First Seen
Jan 21, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
opencode78
claude-code74
codex72
gemini-cli71
cursor71
github-copilot67
TanStack Query v5 完全指南:React 数据管理、乐观更新、离线支持
2,500 周安装