axiom-performance-profiling by charleswiltgen/axiom
npx skills add https://github.com/charleswiltgen/axiom --skill axiom-performance-profilingiOS 应用性能问题分为不同的类别,每个类别都有特定的诊断工具。本技能帮助您在压力下选择正确的工具、有效使用它并正确解读结果。
核心原则:先测量,后优化。猜测性能问题比剖析浪费更多时间。
要求:Xcode 15+, iOS 14+ 相关技能:axiom-swiftui-performance(使用 Instruments 26 进行 SwiftUI 特定剖析),axiom-memory-debugging(内存泄漏诊断)
axiom-memory-debuggingaxiom-swiftui-performance广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
在打开 Instruments 之前,先缩小您实际要调查的范围。
应用性能问题?
├─ 应用感觉缓慢或卡顿(UI 交互停滞,滚动卡顿)
│ └─ → 使用 Time Profiler(测量 CPU 使用率)
├─ 内存随时间增长(Xcode 显示内存增加)
│ └─ → 使用 Allocations(测量对象创建)
├─ 数据加载缓慢(解析、数据库查询、API 调用)
│ └─ → 使用 Core Data instrument(如果使用 Core Data)
│ └─ → 使用 Time Profiler(如果是计算问题)
└─ 电池消耗快(设备发热,几小时内耗尽)
└─ → 使用 Energy Impact(测量功耗)
能 – 使用 Instruments 进行测量(剖析最准确)
不能 – 主动进行剖析
Time Profiler – 缓慢、UI 卡顿、CPU 峰值 Allocations – 内存增长、内存压力、对象计数 Core Data – 查询性能、获取时间、故障触发 Energy Impact – 电池消耗、持续功耗 Network Link Conditioner – 连接相关的缓慢 System Trace – 线程阻塞、主线程阻塞、调度
当您的应用感觉缓慢或卡顿时,使用 Time Profiler。它测量每个函数花费的 CPU 时间。
open -a Instruments
选择 "Time Profiler" 模板。
顶部面板显示随时间变化的 CPU 使用率时间线。查找:
在调用树中,点击 "Heaviest Stack Trace" 查看哪些函数使用最多的 CPU:
Time Profiler 结果
MyViewController.viewDidLoad() – 500ms (占总时间的 40%)
├─ DataParser.parse() – 350ms
│ └─ JSONDecoder.decode() – 320ms
└─ UITableView.reloadData() – 150ms
自身时间 = 在该函数内花费的时间(不包括它调用的函数) 总时间 = 在该函数内花费的时间 + 它调用的所有内容
// ❌ 错误:剖析显示 DataParser.parse() 占用 80% CPU
// 结论:"DataParser 很慢,我来优化它"
// ✅ 正确:检查 DataParser 调用了什么
// 如果 JSONDecoder.decode() 做了 99% 的工作,
// 优化 JSON 解码,而不是 DataParser
问题:总时间高的函数可能正在调用慢速代码,而不是自身在做慢速工作。
修复:查看自身时间,而不是总时间。深入查看每个函数调用了什么。
// ❌ 错误:在模拟器中剖析应用
// 模拟器 CPU 与真实设备不同
// 结果不能反映实际设备性能
// ✅ 正确:在实际设备上剖析
// 设备设置:启用开发者模式,连接 Xcode
修复:始终在实际设备上剖析以获得准确的 CPU 测量结果。
// ❌ 错误:剖析整个应用启动过程
// 看到 2000ms 启动时间,涉及许多函数
// ✅ 正确:仅剖析缓慢的部分
// "滚动时应用感觉缓慢" → 仅剖析滚动
// 分离关注点:启动缓慢 vs 交互缓慢
修复:复现特定的缓慢操作,而不是整个应用。
诱惑:"我必须优化函数 X!"
现实:函数 X 可能是:
应该做什么:
检查自身时间,而不是总时间
深入一层
检查时间线
提问:用户会注意到吗?
时间成本:5 分钟(读取结果)+ 2 分钟(深入分析)= 7 分钟理解
猜测成本:2 小时优化错误函数 + 1 小时意识到没有帮助 + 回到起点 = 浪费 3+ 小时
当内存随时间增长或您怀疑内存压力问题时,使用 Allocations。
open -a Instruments
选择 "Allocations" 模板。
查看主图表:
在 "Statistics" 下:
按 "Persistent" 排序(仍然存活的对象)
查找数量异常大的对象:
UIImage: 500 个实例 (300MB) – 正常应用应 <50
NSString: 50000 个实例 – 应 <1000
CustomDataModel: 10000 个实例 – 应 <100
// ❌ 错误:内存从 100MB 增长到 500MB
// 结论:"有泄漏,内存一直在增长!"
// ✅ 正确:检查是什么导致了增长
// 加载了 1000 张图像(正常)
// 缓存了 API 响应(正常)
// 用户有 5000 个联系人(正常)
// 内存被正确使用
问题:内存增长 ≠ 泄漏。应用在加载数据时合法地使用更多内存。
修复:在 Allocations 中检查对象计数。如果图像/数据计数与您加载的匹配,这是正常的。如果对象计数在没有操作的情况下持续增长,那就是泄漏。
// ❌ 错误:Allocations 显示内存中有 1000 个 UIImage
// 结论:"内存泄漏,图像太多了!"
// ✅ 正确:检查这是否是故意的缓存
// ImageCache 设计为最多保存 1000 张图像
// 当内存压力发生时,缓存被清除
// 正常行为
修复:区分有意的缓存和实际的泄漏。泄漏在内存压力下不会释放。
// ❌ 错误:记录 5 秒,看到 200MB
// 结论:"应用使用 200MB,优化内存"
// ✅ 正确:记录 2-3 分钟,查看完整生命周期
// 加载数据:200MB
// 导航离开:180MB(20MB 仍被缓存)
// 导航返回:190MB(缓存被重用)
// 真实基准:稳态时约 190MB
修复:剖析足够长的时间以看到内存稳定。短时间记录捕获的是瞬时尖峰。
诱惑:"删除缓存,减少对象创建,优化数据结构"
现实:500MB 真的很大吗?
应该做什么:
在实际设备上建立基准
# 在设备上,打开 Xcode 中的 Memory 视图
Xcode → Debug → Memory Debugger → 检查应用启动时的 "Real Memory"
检查对象计数,而不是总内存
在内存压力下测试
剖析真实用户旅程
时间成本:5 分钟(启动 Allocations)+ 3 分钟(记录应用使用)+ 2 分钟(分析)= 10 分钟
猜测成本:删除缓存以"减少内存" → 应用在每个屏幕重新加载数据 → 应用变慢 → 用户抱怨 → 恢复更改 = 浪费 2+ 小时
当您的应用使用 Core Data 且数据加载缓慢时,使用 Core Data instrument。
在 Xcode 的启动参数中添加:
Edit Scheme → Run → Arguments Passed On Launch
添加:-com.apple.CoreData.SQLDebug 1
现在 SQLite 查询会打印到控制台:
CoreData: sql: SELECT ... FROM tracks WHERE artist = ? (time: 0.015s)
CoreData: sql: SELECT ... FROM albums WHERE id = ? (time: 0.002s)
在典型的用户操作期间(加载列表、滚动、过滤)观察控制台:
❌ 错误:加载 100 条音轨,然后为每条查询专辑
SELECT * FROM tracks (time: 0.050s) → 100 条音轨
SELECT * FROM albums WHERE id = 1 (time: 0.005s)
SELECT * FROM albums WHERE id = 2 (time: 0.005s)
SELECT * FROM albums WHERE id = 3 (time: 0.005s)
... 还有 97 个查询
总计:0.050s + (100 × 0.005s) = 0.550s
✅ 正确:使用专辑关系获取音轨(预加载)
SELECT tracks.*, albums.* FROM tracks
LEFT JOIN albums ON tracks.albumId = albums.id
(time: 0.050s)
总计:0.050s
open -a Instruments
选择 "Core Data" 模板。
在执行缓慢操作时记录:
Core Data 结果
Fetch Requests: 102
Average Fetch Time: 12ms
Slow Fetch: "SELECT * FROM tracks" (180ms)
Fault Fires: 5000
→ 对象被访问,需要从数据库获取
→ 应使用预取
// ❌ 错误:获取音轨,然后为每条访问专辑
let tracks = try context.fetch(Track.fetchRequest())
for track in tracks {
print(track.album.title) // 为每条触发单独查询
}
// 总计:1 + N 个查询
// ✅ 正确:使用关系预取进行获取
let request = Track.fetchRequest()
request.returnsObjectsAsFaults = false
request.relationshipKeyPathsForPrefetching = ["album"]
let tracks = try context.fetch(request)
for track in tracks {
print(track.album.title) // 已加载
}
// 总计:1 个查询
修复:使用 relationshipKeyPathsForPrefetching 预先加载相关对象。
// ❌ 错误:一次性获取 50,000 条记录
let request = Track.fetchRequest()
let allTracks = try context.fetch(request) // 巨大的内存尖峰
// ✅ 正确:分块批量获取
let request = Track.fetchRequest()
request.fetchBatchSize = 500 // 每次获取 500 条
let allTracks = try context.fetch(request) // 内存高效
修复:对大型数据集使用 fetchBatchSize。
// ❌ 错误:将所有对象保存在内存中
let request = Track.fetchRequest()
request.returnsObjectsAsFaults = false // 将所有保存在内存中
let allTracks = try context.fetch(request) // 50,000 个对象
// 如果您不使用所有对象,会出现内存尖峰
// ✅ 正确:使用故障(延迟加载)
let request = Track.fetchRequest()
// request.returnsObjectsAsFaults = true (默认)
let allTracks = try context.fetch(request) // 仅引用
// 仅加载您实际访问的对象
修复:除非您需要所有对象预先加载,否则保持 returnsObjectsAsFaults 为默认值(true)。
诱惑:"模式错了,我需要重构一切"
现实:99% 的"Core Data 缓慢"是由于:
重新设计模式是最后尝试的事情。
应该做什么:
启用 SQL 调试(2 分钟)
-com.apple.CoreData.SQLDebug 1 启动参数查找 N+1 模式(3 分钟)
如果需要,添加索引(5 分钟)
@NSManaged var artist: String?@Index测试改进(2 分钟)
只有此时才考虑模式更改(30+ 分钟)
时间成本:12 分钟诊断 + 修复 = 12 分钟
模式重新设计成本:8 小时设计 + 4 小时迁移 + 2 小时测试 + 1 小时回滚 = 总计 15 小时
何时使用:应用电池消耗快,设备发热
工作流程:
关键指标:
常见问题:
何时使用:应用在 4G 上似乎缓慢,希望无需旅行即可测试
设置:
关键配置文件:
注意:在 ui-testing 中也涵盖了网络依赖的测试场景。
何时使用:UI 冻结或卡顿,但 Time Profiler 显示低 CPU
常见原因:主线程被等待锁的后台任务阻塞
工作流程:
关键指标:
虽然 Time Profiler 显示 CPU 时间的一般去向,但 OSSignposter 允许您测量您定义的特定操作。它是 Apple 平台上自定义性能检测的主要工具。
import os
let signposter = OSSignposter(subsystem: "com.app", category: "DataLoad")
// 区间测量(开始 → 结束)
func loadData() async throws -> [Item] {
let signpostID = signposter.makeSignpostID()
let state = signposter.beginInterval("Load Items", id: signpostID)
defer { signposter.endInterval("Load Items", state) }
return try await fetchItems()
}
// 兴趣点(单个事件)
func cacheHit(for key: String) {
signposter.emitEvent("Cache Hit")
}
| 需求 | 工具 |
|---|---|
| 通用 CPU 热点 | Time Profiler |
| 特定操作持续时间 | OSSignposter |
| 跨线程操作计时 | OSSignposter |
| 自动化回归测试 | OSSignposter + XCTOSSignpostMetric |
问题:您运行 Time Profiler 3 次,得到 200ms、150ms、280ms。哪个是正确的?
您可能认为的危险信号:
现实:差异是正常的。不同的运行会遇到不同的:
应该做什么:
预热缓存(第一次运行总是较慢)
控制系统负载
寻找模式
相信最慢的运行(最坏情况场景)
时间成本:10 分钟(运行剖析器 3 次)+ 2 分钟(解读)= 12 分钟
忽略差异的成本:错过间歇性性能问题 → 用户偶尔看到冻结 → 差评
问题:Time Profiler 显示 JSON 解析缓慢。Allocations 显示内存使用正常。修复哪个?
答案:两者都是真实的,优先级不同。
Time Profiler: JSONDecoder.decode() = 500ms
Allocations: 内存 = 250MB(对于应用大小正常)
结果:应用缓慢且内存正常
操作:优化 JSON 解码(不是内存)
常见冲突:
| Time Profiler | Allocations | 操作 |
|---|---|---|
| 高 CPU | 内存正常 | 优化计算(减少 CPU) |
| 低 CPU | 内存增长 | 查找泄漏或减少对象创建 |
| 两者都高 | 两者都高 | 先剖析哪个对用户可见 |
应该做什么:
按用户影响确定优先级
检查它们是否相关
按影响顺序修复
时间成本:5 分钟(分析两个结果)= 5 分钟
修复错误问题的成本:花费 4 小时优化正常的内存 → 对用户体验没有改善
情况:经理说"我们 2 小时后发布。性能可接受吗?"
您可能认为的危险信号:
现实:剖析总共需要 15-20 分钟。这是您剩余时间的 1%。
应该做什么:
剖析关键路径(3 分钟)
进行一次适当的记录(5 分钟)
快速解读(5 分钟)
有信心地发布(2 分钟)
时间成本:15 分钟剖析 + 5 分钟分析 = 20 分钟
不剖析的成本:发布未知性能 → 用户遇到缓慢 → 差评 → 2 周后紧急热修复
数学:现在 20 分钟的剖析 << 2+ 周的发布后支持
// Time Profiler:启动 Instruments
open -a Instruments
// Core Data:启用 SQL 日志记录
// Edit Scheme → Run → Arguments Passed On Launch
-com.apple.CoreData.SQLDebug 1
// Allocations:检查持久对象
Instruments → Allocations → Statistics → 按 "Persistent" 排序
// Memory warning:模拟压力
Xcode → Debug → Simulate Memory Warning
// Energy Impact:剖析电池消耗
Instruments → Energy Impact 模板
// Network Link Conditioner:模拟 3G
System Preferences → Network Link Conditioner → 3G 配置文件
性能问题?
├─ 应用感觉缓慢/卡顿?
│ └─ → Time Profiler(测量 CPU)
├─ 内存随时间增长?
│ └─ → Allocations(查找对象增长)
├─ 数据加载缓慢?
│ └─ → Core Data instrument(如果使用 Core Data)
│ └─ → Time Profiler(如果计算缓慢)
└─ 电池消耗快?
└─ → Energy Impact(测量功耗)
场景:您的应用加载带有艺术家姓名的专辑列表。它很慢(100 张专辑需要 5 秒以上)。您怀疑 Core Data。
设置:首先启用 SQL 日志记录
# Edit Scheme → Run → Arguments Passed On Launch
-com.apple.CoreData.SQLDebug 1
您在控制台中看到的:
CoreData: sql: SELECT ... FROM albums WHERE ... (time: 0.050s)
CoreData: sql: SELECT ... FROM artists WHERE id = 1 (time: 0.003s)
CoreData: sql: SELECT ... FROM artists WHERE id = 2 (time: 0.003s)
... 还有 98 个单独查询
总计:0.050s + (100 × 0.003s) = 0.350s
使用技能进行诊断:
修复:
// ❌ 错误:每次专辑访问触发单独的艺术家查询
let request = Album.fetchRequest()
let albums = try context.fetch(request)
for album in albums {
print(album.artist.name) // 每条额外查询
}
// ✅ 正确:预取关系
let request = Album.fetchRequest()
request.returnsObjectsAsFaults = false
request.relationshipKeyPathsForPrefetching = ["artist"]
let albums = try context.fetch(request)
for album in albums {
print(album.artist.name) // 已加载
}
结果:0.350s → 0.050s(快 7 倍)
场景:您的应用 UI 在加载视图时停滞 1-2 秒。您的共同负责人说"在所有地方添加后台线程。"您想先测量。
使用技能的工作流程(Time Profiler 深入解析,第 82-118 行):
open -a Instruments
# 选择 "Time Profiler"
2. 记录卡顿:
应用启动
Time Profiler 记录
视图加载
发生卡顿(在 Time Profiler 中观察尖峰)
停止记录
3. 检查结果:
调用栈显示:
viewDidLoad() – 1500ms
├─ loadJSON() – 1200ms (自身时间:50ms)
│ └─ loadImages() – 1150ms (自身时间:1150ms) ← 这是罪魁祸首
├─ parseData() – 200ms
└─ layoutUI() – 100ms
4. 应用技能(第 173-175 行):
loadJSON() 自身时间:50ms,总时间:1200ms
→ loadJSON() 不慢,它调用的东西慢
→ loadImages() 自身时间:1150ms
→ loadImages() 是实际的瓶颈
5. 修复正确的东西:
// ❌ 错误:将所有内容线程化
DispatchQueue.global().async { loadJSON() }
// ✅ 正确:仅线程化缓慢的部分
func loadJSON() {
let data = parseJSON() // 50ms,在主线程上正常
// 仅将缓慢部分移到后台
DispatchQueue.global().async {
let images = loadImages() // 1150ms,现在在后台
DispatchQueue.main.async {
updateUI(with: images)
}
}
}
结果:1500ms → 350ms(快 4 倍,主线程解除阻塞)
为什么这很重要:您修复了实际的瓶颈(1150ms),而不是盲目猜测线程化。
场景:Allocations 显示在 30 分钟的应用使用过程中,内存从 150MB 增长到 600MB。您的经理说"内存泄漏!"您需要知道是否真实。
使用技能的工作流程(Allocations 深入解析,第 199-277 行):
在 Instruments 中启动 Allocations
记录正常应用使用 3 分钟:
用户加载数据 → 内存增长到 400MB
用户四处导航 → 内存保持在 400MB
用户转到设置 → 内存 400MB
用户返回 → 内存 400MB
3. 检查 Allocations 统计信息:
持久对象:
- UIImage: 1200 个实例 (300MB) ← 数量大
- NSString: 5000 个实例 (4MB)
- CustomDataModel: 800 个实例 (15MB)
4. 提问技能问题(第 220-240 行):
诊断:不是泄漏。这是正常缓存(第 235-248 行)
内存增长 = 应用使用用户请求的数据
在压力下内存下降 = 缓存正常工作
内存无限期保持高位 = 可能泄漏
5. 结论:
// ✅ 这工作正常
let imageCache = NSCache<NSString, UIImage>()
// 设计为最多保存 1200 张图像
// 在系统内存压力发生时清除
// 没有泄漏
结果:无需操作。"泄漏"实际上是缓存正在工作。
性能工作不会在修复发布后就完成。没有回归检测,优化会随着时间的推移悄悄退化。三阶段管道在每个阶段捕获回归。
| 阶段 | 工具 | 何时 | 捕获 |
|---|---|---|---|
| 开发 | OSSignposter | 编写代码时 | 特定操作计时 |
| CI | XCTest 性能测试 | 每个 PR | 与基线的回归 |
| 生产 | MetricKit | 发布后 | 真实世界退化 |
参见上面的 OSSignposter 部分。将 signpost 区间添加到性能关键的代码路径。
func testDataLoadPerformance() throws {
let options = XCTMeasureOptions()
options.iterationCount = 10
measure(metrics: [
XCTClockMetric(), // 挂钟时间
XCTCPUMetric(), // CPU 时间和周期
XCTMemoryMetric(), // 峰值物理内存
], options: options) {
loadData()
}
}
运行
iOS app performance problems fall into distinct categories, each with a specific diagnosis tool. This skill helps you choose the right tool , use it effectively , and interpret results correctly under pressure.
Core principle : Measure before optimizing. Guessing about performance wastes more time than profiling.
Requires : Xcode 15+, iOS 14+ Related skills : axiom-swiftui-performance (SwiftUI-specific profiling with Instruments 26), axiom-memory-debugging (memory leak diagnosis)
axiom-memory-debugging instead whenaxiom-swiftui-performance instead whenBefore opening Instruments, narrow down what you're actually investigating.
App performance problem?
├─ App feels slow or lags (UI interactions stall, scrolling stutters)
│ └─ → Use Time Profiler (measure CPU usage)
├─ Memory grows over time (Xcode shows increasing memory)
│ └─ → Use Allocations (measure object creation)
├─ Data loading is slow (parsing, database queries, API calls)
│ └─ → Use Core Data instrument (if using Core Data)
│ └─ → Use Time Profiler (if it's computation)
└─ Battery drains fast (device gets hot, depletes in hours)
└─ → Use Energy Impact (measure power consumption)
YES – Use Instruments to measure it (profiling is most accurate)
NO – Use profiling proactively
Time Profiler – Slowness, UI lag, CPU spikes Allocations – Memory growth, memory pressure, object counts Core Data – Query performance, fetch times, fault fires Energy Impact – Battery drain, sustained power draw Network Link Conditioner – Connection-related slowness System Trace – Thread blocking, main thread blocking, scheduling
Use Time Profiler when your app feels slow or laggy. It measures CPU time spent in each function.
open -a Instruments
Select "Time Profiler" template.
The top panel shows a timeline of CPU usage over time. Look for:
In the call tree, click "Heaviest Stack Trace" to see which functions use the most CPU:
Time Profiler Results
MyViewController.viewDidLoad() – 500ms (40% of total)
├─ DataParser.parse() – 350ms
│ └─ JSONDecoder.decode() – 320ms
└─ UITableView.reloadData() – 150ms
Self Time = Time spent IN that function (not in functions it calls) Total Time = Time spent in that function + everything it calls
// ❌ WRONG: Profile shows DataParser.parse() is 80% CPU
// Conclusion: "DataParser is slow, let me optimize it"
// ✅ RIGHT: Check what DataParser is calling
// If JSONDecoder.decode() is doing 99% of the work,
// optimize JSON decoding, not DataParser
The issue : A function with high Total Time might be calling slow code, not doing slow work itself.
Fix : Look at Self Time, not Total Time. Drill down to see what each function calls.
// ❌ WRONG: Profile app in Simulator
// Simulator CPU is different than real device
// Results don't reflect actual device performance
// ✅ RIGHT: Profile on actual device
// Device settings: Developer Mode enabled, Xcode attached
Fix : Always profile on actual device for accurate CPU measurements.
// ❌ WRONG: Profile entire app startup
// Sees 2000ms startup time, many functions involved
// ✅ RIGHT: Profile just the slow part
// "App feels slow when scrolling" → profile only scrolling
// Separate concerns: startup slow vs interaction slow
Fix : Reproduce the specific slow operation, not the entire app.
The temptation : "I must optimize function X!"
The reality : Function X might be:
What to do instead :
Check Self Time, not Total Time
Drill down one level
Check the timeline
Ask: Will users notice?
Time cost : 5 min (read results) + 2 min (drill down) = 7 minutes to understand
Cost of guessing : 2 hours optimizing wrong function + 1 hour realizing it didn't help + back to square one = 3+ hours wasted
Use Allocations when memory grows over time or you suspect memory pressure issues.
open -a Instruments
Select "Allocations" template.
Look at the main chart:
Under "Statistics":
Sort by "Persistent" (objects still alive)
Look for surprisingly large object counts:
UIImage: 500 instances (300MB) – Should be <50 for normal app
NSString: 50000 instances – Should be <1000
CustomDataModel: 10000 instances – Should be <100
// ❌ WRONG: Memory went from 100MB to 500MB
// Conclusion: "There's a leak, memory keeps growing!"
// ✅ RIGHT: Check what caused the growth
// Loaded 1000 images (normal)
// Cached API responses (normal)
// User has 5000 contacts (normal)
// Memory is being used correctly
The issue : Growing memory ≠ leak. Apps legitimately use more memory when loading data.
Fix : Check Allocations for object counts. If images/data count matches what you loaded, it's normal. If object count keeps growing without actions, that's a leak.
// ❌ WRONG: Allocations shows 1000 UIImages in memory
// Conclusion: "Memory leak, too many images!"
// ✅ RIGHT: Check if this is intentional caching
// ImageCache holds up to 1000 images by design
// When memory pressure happens, cache is cleared
// Normal behavior
Fix : Distinguish between intended caching and actual leaks. Leaks don't release under memory pressure.
// ❌ WRONG: Record for 5 seconds, see 200MB
// Conclusion: "App uses 200MB, optimize memory"
// ✅ RIGHT: Record for 2-3 minutes, see full lifecycle
// Load data: 200MB
// Navigate away: 180MB (20MB still cached)
// Navigate back: 190MB (cache reused)
// Real baseline: ~190MB at steady state
Fix : Profile long enough to see memory stabilize. Short recordings capture transient spikes.
The temptation : "Delete caching, reduce object creation, optimize data structures"
The reality : Is 500MB actually large?
What to do instead :
Establish baseline on real device
# On device, open Memory view in Xcode
Xcode → Debug → Memory Debugger → Check "Real Memory" at app launch
Check object counts, not total memory
Test under memory pressure
Profile real user journey
Time cost : 5 min (launch Allocations) + 3 min (record app usage) + 2 min (analyze) = 10 minutes
Cost of guessing : Delete caching to "reduce memory" → app reloads data every screen → slower app → users complain → revert changes = 2+ hours wasted
Use Core Data instrument when your app uses Core Data and data loading is slow.
Add to your launch arguments in Xcode:
Edit Scheme → Run → Arguments Passed On Launch
Add: -com.apple.CoreData.SQLDebug 1
Now SQLite queries print to console:
CoreData: sql: SELECT ... FROM tracks WHERE artist = ? (time: 0.015s)
CoreData: sql: SELECT ... FROM albums WHERE id = ? (time: 0.002s)
Watch the console during a typical user action (load list, scroll, filter):
❌ BAD: Loading 100 tracks, then querying album for each
SELECT * FROM tracks (time: 0.050s) → 100 tracks
SELECT * FROM albums WHERE id = 1 (time: 0.005s)
SELECT * FROM albums WHERE id = 2 (time: 0.005s)
SELECT * FROM albums WHERE id = 3 (time: 0.005s)
... 97 more queries
Total: 0.050s + (100 × 0.005s) = 0.550s
✅ GOOD: Fetch tracks WITH album relationship (eager loading)
SELECT tracks.*, albums.* FROM tracks
LEFT JOIN albums ON tracks.albumId = albums.id
(time: 0.050s)
Total: 0.050s
open -a Instruments
Select "Core Data" template.
Record while performing slow action:
Core Data Results
Fetch Requests: 102
Average Fetch Time: 12ms
Slow Fetch: "SELECT * FROM tracks" (180ms)
Fault Fires: 5000
→ Object accessed, requires fetch from database
→ Should use prefetching
// ❌ WRONG: Fetch tracks, then access album for each
let tracks = try context.fetch(Track.fetchRequest())
for track in tracks {
print(track.album.title) // Fires individual query for each
}
// Total: 1 + N queries
// ✅ RIGHT: Fetch with relationship prefetching
let request = Track.fetchRequest()
request.returnsObjectsAsFaults = false
request.relationshipKeyPathsForPrefetching = ["album"]
let tracks = try context.fetch(request)
for track in tracks {
print(track.album.title) // Already loaded
}
// Total: 1 query
Fix : Use relationshipKeyPathsForPrefetching to load related objects upfront.
// ❌ WRONG: Fetch 50,000 records all at once
let request = Track.fetchRequest()
let allTracks = try context.fetch(request) // Huge memory spike
// ✅ RIGHT: Batch fetch in chunks
let request = Track.fetchRequest()
request.fetchBatchSize = 500 // Fetch 500 at a time
let allTracks = try context.fetch(request) // Memory efficient
Fix : Use fetchBatchSize for large datasets.
// ❌ WRONG: Keep all objects in memory
let request = Track.fetchRequest()
request.returnsObjectsAsFaults = false // Keep all in memory
let allTracks = try context.fetch(request) // 50,000 objects
// Memory spike if you don't use all of them
// ✅ RIGHT: Use faults (lazy loading)
let request = Track.fetchRequest()
// request.returnsObjectsAsFaults = true (default)
let allTracks = try context.fetch(request) // Just references
// Only load objects you actually access
Fix : Leave returnsObjectsAsFaults as default (true) unless you need all objects upfront.
The temptation : "The schema is wrong, I need to restructure everything"
The reality : 99% of "slow Core Data" is due to:
Redesigning the schema is the LAST thing to try.
What to do instead :
Enable SQL debugging (2 min)
-com.apple.CoreData.SQLDebug 1 launch argumentLook for N+1 pattern (3 min)
Add indexes if needed (5 min)
@NSManaged var artist: String with frequent filtering?@Index in schemaTest improvement (2 min)
Only THEN consider schema changes (30+ min)
Time cost : 12 minutes to diagnose + fix = 12 minutes
Cost of schema redesign : 8 hours design + 4 hours migration + 2 hours testing + 1 hour rollback = 15 hours total
When to use : App drains battery fast, device gets hot
Workflow :
Key metrics :
Common issues :
When to use : App seems slow on 4G, want to test without traveling
Setup :
Key profiles :
Note : Also covered in ui-testing for network-dependent test scenarios.
When to use : UI freezes or is janky, but Time Profiler shows low CPU
Common cause : Main thread blocked by background task waiting on lock
Workflow :
Key metrics :
While Time Profiler shows where CPU time goes generally, OSSignposter lets you measure specific operations you define. It's the primary tool for custom performance instrumentation on Apple platforms.
import os
let signposter = OSSignposter(subsystem: "com.app", category: "DataLoad")
// Interval measurement (start → end)
func loadData() async throws -> [Item] {
let signpostID = signposter.makeSignpostID()
let state = signposter.beginInterval("Load Items", id: signpostID)
defer { signposter.endInterval("Load Items", state) }
return try await fetchItems()
}
// Point of interest (single event)
func cacheHit(for key: String) {
signposter.emitEvent("Cache Hit")
}
| Need | Tool |
|---|---|
| General CPU hotspots | Time Profiler |
| Specific operation duration | OSSignposter |
| Cross-thread operation timing | OSSignposter |
| Automated regression testing | OSSignposter + XCTOSSignpostMetric |
The problem : You run Time Profiler 3 times, get 200ms, 150ms, 280ms. Which is correct?
Red flags you might think :
The reality : Variance is NORMAL. Different runs hit different:
What to do instead :
Warm up the cache (first run always slower)
Control system load
Look for the pattern
Trust the slowest run (worst case scenario)
Time cost : 10 min (run profiler 3x) + 2 min (interpret) = 12 minutes
Cost of ignoring variance : Miss intermittent performance issue → users see occasional freezes → bad reviews
The problem : Time Profiler shows JSON parsing is slow. Allocations show memory use is normal. Which to fix?
The answer : Both are real, prioritize differently.
Time Profiler: JSONDecoder.decode() = 500ms
Allocations: Memory = 250MB (normal for app size)
Result: App is slow AND memory is fine
Action: Optimize JSON decoding (not memory)
Common conflicts :
| Time Profiler | Allocations | Action |
|---|---|---|
| High CPU | Normal memory | Optimize computation (reduce CPU) |
| Low CPU | Memory growing | Find leak or reduce object creation |
| Both high | Both high | Profile which is user-visible first |
What to do :
Prioritize by user impact
Check if they're related
Fix in order of impact
Time cost : 5 min (analyze both results) = 5 minutes
Cost of fixing wrong problem : Spend 4 hours optimizing memory that's fine → no improvement to user experience
The situation : Manager says "We ship in 2 hours. Is performance acceptable?"
Red flags you might think :
The reality : Profiling takes 15-20 minutes total. That's 1% of your remaining time.
What to do instead :
Profile the critical path (3 min)
Record one proper run (5 min)
Interpret quickly (5 min)
Ship with confidence (2 min)
Time cost : 15 min profiling + 5 min analysis = 20 minutes
Cost of not profiling : Ship with unknown performance → Users hit slowness → Bad reviews → Emergency hotfix 2 weeks later
Math : 20 minutes of profiling now << 2+ weeks of post-launch support
// Time Profiler: Launch Instruments
open -a Instruments
// Core Data: Enable SQL logging
// Edit Scheme → Run → Arguments Passed On Launch
-com.apple.CoreData.SQLDebug 1
// Allocations: Check persistent objects
Instruments → Allocations → Statistics → sort "Persistent"
// Memory warning: Simulate pressure
Xcode → Debug → Simulate Memory Warning
// Energy Impact: Profile battery drain
Instruments → Energy Impact template
// Network Link Conditioner: Simulate 3G
System Preferences → Network Link Conditioner → 3G profile
Performance problem?
├─ App feels slow/laggy?
│ └─ → Time Profiler (measure CPU)
├─ Memory grows over time?
│ └─ → Allocations (find object growth)
├─ Data loading is slow?
│ └─ → Core Data instrument (if using Core Data)
│ └─ → Time Profiler (if computation slow)
└─ Battery drains fast?
└─ → Energy Impact (measure power)
Scenario : Your app loads a list of albums with artist names. It's slow (5+ seconds for 100 albums). You suspect Core Data.
Setup : Enable SQL logging first
# Edit Scheme → Run → Arguments Passed On Launch
-com.apple.CoreData.SQLDebug 1
What you see in console :
CoreData: sql: SELECT ... FROM albums WHERE ... (time: 0.050s)
CoreData: sql: SELECT ... FROM artists WHERE id = 1 (time: 0.003s)
CoreData: sql: SELECT ... FROM artists WHERE id = 2 (time: 0.003s)
... 98 more individual queries
Total: 0.050s + (100 × 0.003s) = 0.350s
Diagnosis using the skill :
Fix :
// ❌ WRONG: Each album access triggers separate artist query
let request = Album.fetchRequest()
let albums = try context.fetch(request)
for album in albums {
print(album.artist.name) // Extra query for each
}
// ✅ RIGHT: Prefetch the relationship
let request = Album.fetchRequest()
request.returnsObjectsAsFaults = false
request.relationshipKeyPathsForPrefetching = ["artist"]
let albums = try context.fetch(request)
for album in albums {
print(album.artist.name) // Already loaded
}
Result : 0.350s → 0.050s (7x faster)
Scenario : Your app UI stalls for 1-2 seconds when loading a view. Your co-lead says "Add background threading everywhere." You want to measure first.
Workflow using the skill (Time Profiler Deep Dive, lines 82-118):
open -a Instruments
# Select "Time Profiler"
2. Record the stall :
App launches
Time Profiler records
View loads
Stall happens (observe the spike in Time Profiler)
Stop recording
3. Examine results :
Call Stack shows:
viewDidLoad() – 1500ms
├─ loadJSON() – 1200ms (Self Time: 50ms)
│ └─ loadImages() – 1150ms (Self Time: 1150ms) ← HERE'S THE CULPRIT
├─ parseData() – 200ms
└─ layoutUI() – 100ms
4. Apply the skill (lines 173-175):
loadJSON() has Self Time: 50ms, Total Time: 1200ms
→ loadJSON() isn't slow, something it CALLS is slow
→ loadImages() has Self Time: 1150ms
→ loadImages() is the actual bottleneck
5. Fix the right thing :
// ❌ WRONG: Thread everything
DispatchQueue.global().async { loadJSON() }
// ✅ RIGHT: Thread only the slow part
func loadJSON() {
let data = parseJSON() // 50ms, fine on main
// Move ONLY the slow part to background
DispatchQueue.global().async {
let images = loadImages() // 1150ms, now background
DispatchQueue.main.async {
updateUI(with: images)
}
}
}
Result : 1500ms → 350ms (4x faster, main thread unblocked)
Why this matters : You fixed the ACTUAL bottleneck (1150ms), not guessing blindly about threading.
Scenario : Allocations shows memory growing from 150MB to 600MB over 30 minutes of app use. Your manager says "Memory leak!" You need to know if it's real.
Workflow using the skill (Allocations Deep Dive, lines 199-277):
Launch Allocations in Instruments
Record normal app usage for 3 minutes :
User loads data → memory grows to 400MB
User navigates around → memory stays at 400MB
User goes to Settings → memory at 400MB
User comes back → memory at 400MB
3. Check Allocations Statistics :
Persistent Objects:
- UIImage: 1200 instances (300MB) ← Large count
- NSString: 5000 instances (4MB)
- CustomDataModel: 800 instances (15MB)
4. Ask the skill questions (lines 220-240):
Diagnosis : NOT a leak. This is normal caching (lines 235-248)
Memory growing = apps using data users asked for
Memory dropping under pressure = cache working correctly
Memory staying high indefinitely = possible leak
5. Conclusion :
// ✅ This is working correctly
let imageCache = NSCache<NSString, UIImage>()
// Holds up to 1200 images by design
// Clears when system memory pressure happens
// No leak
Result : No action needed. The "leak" is actually the cache doing its job.
Performance work isn't done when the fix ships. Without regression detection, optimizations quietly degrade over time. The three-stage pipeline catches regressions at every phase.
| Stage | Tool | When | Catches |
|---|---|---|---|
| Dev | OSSignposter | Writing code | Specific operation timing |
| CI | XCTest performance tests | Every PR | Regression vs baseline |
| Production | MetricKit | After release | Real-world degradation |
See OSSignposter section above. Add signpost intervals to performance-critical code paths.
func testDataLoadPerformance() throws {
let options = XCTMeasureOptions()
options.iterationCount = 10
measure(metrics: [
XCTClockMetric(), // Wall clock time
XCTCPUMetric(), // CPU time and cycles
XCTMemoryMetric(), // Peak physical memory
], options: options) {
loadData()
}
}
After running once, click the value in Xcode's test results → "Set Baseline". Subsequent runs compare against baseline and fail if regression exceeds tolerance (default 10%).
// ❌ Test always passes — no baseline set
func testPerformance() {
measure { doWork() }
}
// ✅ Set baseline in Xcode after first run
// Tests fail when performance regresses beyond tolerance
// In production code
let signposter = OSSignposter(subsystem: "com.app", category: "Sync")
func syncData() {
let id = signposter.makeSignpostID()
let state = signposter.beginInterval("Full Sync", id: id)
defer { signposter.endInterval("Full Sync", state) }
// ... sync logic
}
// In test
func testSyncPerformance() {
let metric = XCTOSSignpostMetric(
subsystem: "com.app",
category: "Sync",
name: "Full Sync"
)
measure(metrics: [metric]) {
syncData()
}
}
See axiom-metrickit-ref for comprehensive MetricKit integration. Key metrics to monitor:
MXAppLaunchMetric — Launch time regressionMXAppResponsivenessMetric — Hang rate increaseMXCPUMetric — CPU time per foreground sessionMXMemoryMetric — Peak memory growth across versionsWWDC : 2023-10160, 2024-10217, 2025-308, 2025-312
Docs : /library/archive/documentation/cocoa/conceptual/coredataperformance, /library/archive/technotes/tn2224, /os/ossignposter, /xctest/xctestcase/measure
Skills : axiom-memory-debugging, axiom-swiftui-performance, axiom-swift-concurrency, axiom-metrickit-ref
Targets: iOS 14+, Swift 5.5+ Tools: Instruments, Core Data History: See git log for changes
Weekly Installs
97
Repository
GitHub Stars
601
First Seen
Jan 21, 2026
Security Audits
Gen Agent Trust HubFailSocketPassSnykPass
Installed on
opencode82
codex76
claude-code76
gemini-cli75
cursor73
github-copilot71
ESLint迁移到Oxlint完整指南:JavaScript/TypeScript项目性能优化工具
1,600 周安装
gstack工作流助手:Claude Code专家团队,AI驱动开发规划、代码评审与自动化测试
664 周安装
find-skills 技能查找工具:快速发现并安装智能体技能,扩展AI能力
669 周安装
专业转化文案撰写指南:提升营销效果与SEO优化的文案技巧
656 周安装
WordPress Elementor 页面编辑与模板管理指南:WP-CLI 与浏览器自动化
674 周安装
Skill Forge 技能开发指南:Claude AI 专用技能创建与优化工作流程
668 周安装
HubSpot CRM 集成指南:使用 Membrane CLI 自动化销售、营销与客户服务
665 周安装