Implementing iOS Performance Monitoring: CPU, Memory, FPS, Startup Time, and Power Consumption
This article details the design and implementation of an iOS performance‑monitoring SDK that captures fundamental metrics such as CPU usage, memory consumption, frame rate, cold and hot startup times, and power draw, explains the underlying Mach APIs, provides sample Objective‑C code, and discusses practical considerations for accurate measurement and troubleshooting.
Why This Article Was Written
As mobile applications become more performance‑sensitive, developers need reliable ways to detect issues like connection timeouts, crashes, freezes, high CPU usage, memory leaks, and poor UI responsiveness. Traditional reactive debugging based on user feedback is inefficient, so a dedicated performance‑monitoring SDK is essential for proactive optimization.
Project Name Origin
The SDK is codenamed Wedjat (华狄特), referencing the Egyptian god Horus’s eye, symbolizing justice and the ability to see all actions—an apt metaphor for a tool that watches every performance‑related event.
CPU Monitoring
CPU is the most critical resource on a mobile device; excessive usage leads to stutter and battery drain. By enumerating all threads of the current process and aggregating their thread_basic_info , the SDK can compute the overall CPU usage percentage.
struct thread_basic_info {
time_value_t user_time; /* user run time */
time_value_t system_time; /* system run time */
integer_t cpu_usage; /* scaled cpu usage percentage */
policy_t policy; /* scheduling policy */
integer_t run_state; /* run state */
integer_t flags; /* various flags */
integer_t suspend_count; /* suspend count */
integer_t sleep_time; /* seconds thread has been sleeping */
};The implementation calls task_threads to obtain the thread list, then iterates with thread_info (THREAD_BASIC_INFO) to sum user and system times and calculate the percentage. The final step deallocates the thread list with vm_deallocate to avoid leaks.
#import
#import
float cpu_usage() {
kern_return_t kr;
task_info_data_t tinfo;
mach_msg_type_number_t task_info_count = TASK_INFO_MAX;
kr = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)tinfo, &task_info_count);
if (kr != KERN_SUCCESS) return -1;
task_basic_info_t basic_info = (task_basic_info_t)tinfo;
thread_array_t thread_list;
mach_msg_type_number_t thread_count;
kr = task_threads(mach_task_self(), &thread_list, &thread_count);
if (kr != KERN_SUCCESS) return -1;
float tot_cpu = 0;
for (int i = 0; i < (int)thread_count; i++) {
thread_info_data_t thinfo;
mach_msg_type_number_t thread_info_count = THREAD_INFO_MAX;
kr = thread_info(thread_list[i], THREAD_BASIC_INFO, (thread_info_t)thinfo, &thread_info_count);
if (kr != KERN_SUCCESS) return -1;
thread_basic_info_t basic_info_th = (thread_basic_info_t)thinfo;
if (!(basic_info_th->flags & TH_FLAGS_IDLE)) {
tot_cpu += basic_info_th->cpu_usage / (float)TH_USAGE_SCALE * 100.0;
}
}
vm_deallocate(mach_task_self(), (vm_offset_t)thread_list, thread_count * sizeof(thread_t));
assert(kr == KERN_SUCCESS);
return tot_cpu;
}Memory Monitoring
Memory is a scarce resource on iOS; the mach_task_basic_info structure provides resident_size (physical memory) and virtual_size . The SDK queries this via task_info with the MACH_TASK_BASIC_INFO flavor.
struct mach_task_basic_info {
mach_vm_size_t virtual_size; /* virtual memory size (bytes) */
mach_vm_size_t resident_size; /* resident memory size (bytes) */
mach_vm_size_t resident_size_max;
time_value_t user_time;
time_value_t system_time;
policy_t policy;
integer_t suspend_count;
}; NSUInteger getResidentMemory() {
struct mach_task_basic_info info;
mach_msg_type_number_t count = MACH_TASK_BASIC_INFO_COUNT;
int r = task_info(mach_task_self(), MACH_TASK_BASIC_INFO, (task_info_t)&info, &count);
if (r == KERN_SUCCESS) {
return info.resident_size;
} else {
return -1;
}
}Startup Time Measurement
Startup time is split into cold start (pre‑main loading) and hot start (post‑launch resume). The article shows two approaches: recording a timestamp at the beginning of main() and computing the delta in applicationDidFinishLaunching:withOptions: , or using mach_absolute_time() inside a +load method and listening for UIApplicationDidFinishLaunchingNotification to obtain a high‑precision measurement.
CFAbsoluteTime StartTime;
int main(int argc, char *argv[]) {
@autoreleasepool {
StartTime = CFAbsoluteTimeGetCurrent();
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
// In applicationDidFinishLaunching:
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"Launched in %f sec", CFAbsoluteTimeGetCurrent() - StartTime);
});FPS Monitoring
Frames‑per‑second is measured using a CADisplayLink that fires each screen refresh. The demo counts frames over a one‑second interval and computes FPS. This method reflects the run‑loop rate rather than the true Core Animation performance.
@implementation YYFPSLabel {
CADisplayLink *_link;
NSUInteger _count;
NSTimeInterval _lastTime;
}
- (id)init {
self = [super init];
if (self) {
_link = [CADisplayLink displayLinkWithTarget:[YYWeakProxy proxyWithTarget:self] selector:@selector(tick:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
return self;
}
- (void)tick:(CADisplayLink *)link {
if (_lastTime == 0) { _lastTime = link.timestamp; return; }
_count++;
NSTimeInterval delta = link.timestamp - _lastTime;
if (delta < 1) return;
float fps = _count / delta;
_count = 0;
_lastTime = link.timestamp;
// display fps …
}
@endFreezing / Lag Detection
Lag occurs when CPU or GPU cannot finish work before the next V‑Sync (≈16.67 ms). Two common detection strategies are FPS monitoring and run‑loop stall detection. The article presents a run‑loop observer that records kCFRunLoopBeforeSources and kCFRunLoopAfterWaiting intervals; if the interval exceeds a threshold repeatedly, a lag event is reported.
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
MyClass *object = (__bridge MyClass *)info;
object->activity = activity;
dispatch_semaphore_signal(object->semaphore);
}
- (void)registerObserver {
CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
self->semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0,0), ^{
while (YES) {
long st = dispatch_semaphore_wait(self->semaphore, dispatch_time(DISPATCH_TIME_NOW, 50 * NSEC_PER_MSEC));
if (st != 0 && (self->activity == kCFRunLoopBeforeSources || self->activity == kCFRunLoopAfterWaiting)) {
if (++self->timeoutCount >= 5) {
// report lag
}
} else {
self->timeoutCount = 0;
}
}
});
}Power‑Consumption Monitoring
Three approaches are compared: the public UIDevice API (simple but coarse), the private IOKit power‑source API (more precise, 1 % granularity), and a jailbreak‑only method using iOSDiagnosticsSupport to read hourly energy logs. Sample IOKit code extracts current and maximum capacity from the power‑source dictionary and computes a percentage.
- (double)getBatteryLevel {
CFTypeRef blob = IOPSCopyPowerSourcesInfo();
CFArrayRef sources = IOPSCopyPowerSourcesList(blob);
int num = CFArrayGetCount(sources);
if (num == 0) return -1.0;
for (int i = 0; i < num; i++) {
CFDictionaryRef pSource = IOPSGetPowerSourceDescription(blob, CFArrayGetValueAtIndex(sources, i));
if (!pSource) return -1.0;
int cur, max;
CFNumberGetValue(CFDictionaryGetValue(pSource, CFSTR(kIOPSCurrentCapacityKey)), kCFNumberSInt32Type, &cur);
CFNumberGetValue(CFDictionaryGetValue(pSource, CFSTR(kIOPSMaxCapacityKey)), kCFNumberSInt32Type, &max);
double percent = (double)cur / (double)max * 100.0;
NSLog(@"curCapacity : %d / maxCapacity: %d , percentage: %.1f ", cur, max, percent);
return percent;
}
return -1.0;
}Wrap Up
The first part of the series explains how to collect core iOS performance metrics—CPU usage, memory footprint, FPS, cold/hot launch times, and power consumption—using low‑level Mach APIs and public frameworks. The next article will focus on network‑request monitoring, and the author notes that power‑consumption measurement still lacks a perfect solution, leaving room for further research.
References
Mac OS X and iOS Internals: To the Apple’s Core
OS X and iOS Kernel Programming
Handling low memory conditions in iOS and Mavericks
iOS-System-Services
GT
Optimizing Facebook for iOS start time
Apple Documentation: Strategies for Handling App State Transitions
iOS time handling articles
StartupMeasurer
Frame rate (Wikipedia)
YYText
Mobile performance monitoring solution Hertz
iOS smooth UI techniques
WeChat Reader iOS performance summary
PLCrashReporter
iOS‑Diagnostics
Performance visualization practice
Hujiang Technology
We focus on the real-world challenges developers face, delivering authentic, practical content and a direct platform for technical networking among developers.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.