ax-energy
npx machina-cli add skill Kasempiternal/axiom-v2/ax-energy --openclawEnergy Optimization
Quick Patterns
Power Profiler Workflow (5 min)
1. Connect iPhone wirelessly to Xcode (wireless debugging)
2. Xcode > Product > Profile (Cmd+I)
3. Select Blank template
4. Click "+" > Add "Power Profiler" instrument
5. Optional: Add "CPU Profiler" for correlation
6. Click Record, use app normally for 2-3 minutes
7. Click Stop
8. Expand Power Profiler track, examine per-app lanes:
- CPU Power Impact: Computation, timers, parsing
- GPU Power Impact: Animations, blur, Metal
- Display Power Impact: Brightness, content type
- Network Power Impact: Requests, downloads, polling
Why wireless: When device is charging via cable, power metrics show 0.
Timer Tolerance (1 min fix)
// BAD: No tolerance, prevents system batching
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in self.updateUI() }
// GOOD: 10% tolerance for system batching
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in self.updateUI() }
timer.tolerance = 0.1
// BETTER: Combine Timer (auto-cleanup)
Timer.publish(every: 1.0, tolerance: 0.1, on: .main, in: .default)
.autoconnect()
.sink { [weak self] _ in self?.refresh() }
.store(in: &cancellables)
// BEST: Event-driven, no timer
NotificationCenter.default.publisher(for: .dataDidUpdate)
.sink { [weak self] _ in self?.updateUI() }
.store(in: &cancellables)
Push vs Poll (100x efficiency)
// BAD: Polling every 5 seconds (radio active 100% of time)
Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
self?.fetchLatestData()
}
// GOOD: Server pushes when data changes
func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
Task {
do {
let hasNewData = try await fetchLatestData()
completionHandler(hasNewData ? .newData : .noData)
} catch { completionHandler(.failed) }
}
}
Location Accuracy Reduction
// BAD: Best accuracy drains GPS constantly
locationManager.desiredAccuracy = kCLLocationAccuracyBest
locationManager.startUpdatingLocation()
// GOOD: Appropriate accuracy + distance filter
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
locationManager.distanceFilter = 100
locationManager.startMonitoringSignificantLocationChanges()
// BEST: iOS 26+ async with stationary detection
for try await update in CLLocationUpdate.liveUpdates() {
if update.stationary { break } // System pauses automatically
handleLocation(update.location)
}
Lazy Loading (eliminates CPU spikes)
// BAD: Creates ALL views upfront (CPU spike from 1 to 21)
VStack { ForEach(videos) { VideoCardView(video: $0) } }
// GOOD: Creates only visible views (CPU impact 4.3)
LazyVStack { ForEach(videos) { VideoCardView(video: $0) } }
Background Task Completion
// BAD: Long-running, never ends task
backgroundTask = application.beginBackgroundTask { }
performLongOperation()
// GOOD: End immediately when done
backgroundTask = application.beginBackgroundTask(withName: "Save State") { [weak self] in
self?.saveProgress()
if let task = self?.backgroundTask { application.endBackgroundTask(task) }
self?.backgroundTask = .invalid
}
saveEssentialState()
application.endBackgroundTask(backgroundTask)
backgroundTask = .invalid
Decision Tree
Identify Dominant Subsystem
User reports energy issue?
|
|-- Run Power Profiler (15 min)
| |
| |-- CPU Power Impact dominant?
| | |-- Continuous? -> Timer leak, polling, tight loop
| | |-- Spikes during actions? -> Eager loading, repeated parsing
| | +-- High background CPU? -> Location, BGTasks, audio session
| |
| |-- Network Power Impact dominant?
| | |-- Many small requests? -> Batch into fewer large requests
| | |-- Regular intervals? -> Polling pattern, convert to push
| | +-- Large foreground downloads? -> Use discretionary background URLSession
| |
| |-- GPU Power Impact dominant?
| | |-- Continuous animations? -> Stop when view not visible
| | |-- Blur effects? -> Remove or use solid colors
| | +-- High frame rate? -> Audit secondary frame rates
| |
| |-- Display Power Impact dominant?
| | +-- Light backgrounds on OLED? -> Dark Mode (up to 70% savings)
| |
| +-- Location (shown in CPU + location icon)?
| |-- Continuous updates? -> Significant-change monitoring
| |-- High accuracy? -> Reduce to hundredMeters
| +-- Background location? -> Evaluate if truly needed
Symptom: App at Top of Battery Settings
App at top of Battery Settings?
|
|-- Step 1: Run Power Profiler (15 min)
| |-- CPU high? -> Check timers, polling, eager loading
| |-- Network high? -> Check request frequency, batching
| |-- GPU high? -> Check animations, blur effects
| +-- Display high? -> Check Dark Mode support
|
+-- Step 2: Check background section
|-- High background time?
| |-- Location icon visible? -> Continuous location
| |-- Audio active? -> Session not deactivated
| +-- BGTasks running long? -> Not completing promptly
+-- Background time appropriate? -> Issue is foreground
Symptom: Device Gets Hot
Device gets hot?
|
|-- During specific action?
| |-- Video/camera? -> Check encoding efficiency, release session
| |-- Scroll/animation? -> GPU effects, frame rate too high
| +-- Data processing? -> Cache parsed results, move to background
|
|-- During normal use? -> Run Power Profiler
| |-- CPU high continuously -> Timer, polling, tight loop
| |-- GPU high continuously -> Animation leak
| +-- Network high continuously -> Polling pattern
|
+-- Only in background?
|-- Location continuous -> Reduce accuracy, stop when done
|-- Audio session active -> Deactivate when not playing
+-- BGTask too long -> Complete faster, requiresExternalPower
Symptom: Background Battery Drain
High background battery?
|
|-- Check Info.plist background modes
| |-- "location" enabled? -> Need it? Use significant-change, lowest accuracy
| |-- "audio" enabled? -> Not playing? Deactivate session
| |-- "fetch" enabled? -> earliestBeginDate reasonable?
| +-- "remote-notification" -> Check handler efficiency
|
|-- Check BGTaskScheduler
| |-- Refresh too frequent? -> Increase earliestBeginDate
| |-- Processing without power? -> Add requiresExternalPower = true
| +-- Not completing? -> Always call setTaskCompleted
|
+-- Check beginBackgroundTask
|-- endBackgroundTask called promptly?
+-- Multiple overlapping tasks? -> Track IDs, end each
Symptom: High Cellular Energy
High drain on cellular?
|
|-- URLSession configuration
| |-- allowsExpensiveNetworkAccess = true? -> Set false for non-urgent
| |-- isDiscretionary = false? -> Set true for background downloads
| +-- waitsForConnectivity = false? -> Set true to avoid retries
|
|-- Request patterns
| |-- Many small requests? -> Batch
| |-- Polling? -> Push notifications
| +-- Large foreground downloads? -> Background URLSession
|
+-- Low Data Mode
+-- Check allowsConstrainedNetworkAccess, isLowDataModeEnabled
Anti-Patterns
Polling instead of push. Polling every 5 seconds keeps radio active 100% of the time. Push notifications activate radio only when data changes. Polling uses 100x more energy.
Best accuracy for non-navigation. kCLLocationAccuracyBest uses GPS + WiFi + cellular triangulation. 95% of apps only need kCLLocationAccuracyHundredMeters.
Animations running when not visible. GPU continues rendering offscreen. Stop in viewWillDisappear, resume in viewWillAppear.
Timer without tolerance. Forces exact wake schedule, prevents system batching. Set tolerance to at least 10% of interval.
VStack with large ForEach. Creates ALL views upfront. Use LazyVStack for on-demand creation.
Repeated parsing in frequently-called methods. JSON parsed on every location update or timer fire. Cache with lazy var.
Audio session never deactivated. Hardware stays powered. Call setActive(false, options: .notifyOthersOnDeactivation) when playback stops.
beginBackgroundTask without prompt end. Running until expiration wastes energy. Call endBackgroundTask immediately when work completes.
Ship now, optimize later. Battery drain is immediately visible. Users see your app at top of Battery Settings on day one. A 15-minute Power Profiler session before launch catches major issues.
Deep Patterns
Background Execution (EMRCA)
Principles from WWDC25-227:
- Efficient: Lightweight, purpose-driven tasks
- Minimal: Keep background work to minimum
- Resilient: Save incremental progress, handle expiration
- Courteous: Honor user preferences and system conditions
- Adaptive: Work with system priorities, don't fight constraints
BGProcessingTask (Long Operations)
func scheduleBackgroundProcessing() {
let request = BGProcessingTaskRequest(identifier: "com.app.maintenance")
request.requiresNetworkConnectivity = true
request.requiresExternalPower = true // Only when charging
try? BGTaskScheduler.shared.submit(request)
}
BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.app.maintenance", using: nil) { task in
self.handleMaintenance(task: task as! BGProcessingTask)
}
iOS 26+ BGContinuedProcessingTask
Continue user-initiated tasks with progress UI:
let request = BGContinuedProcessingTaskRequest(
identifier: "com.app.export",
title: "Exporting Photos",
subtitle: "0 of 100 photos"
)
try? BGTaskScheduler.shared.submit(request)
BGTaskScheduler.shared.register("com.app.export") { task in
let continuedTask = task as! BGContinuedProcessingTask
continuedTask.progress.totalUnitCount = 100
var shouldContinue = true
continuedTask.expirationHandler = { shouldContinue = false }
for i in 0..<100 {
guard shouldContinue else { break }
performExportStep(i)
continuedTask.progress.completedUnitCount = Int64(i + 1)
}
continuedTask.setTaskCompleted(success: shouldContinue)
}
Frame Rate Auditing
Secondary animations at higher frame rates than needed waste GPU power. Up to 20% battery savings by aligning frame rates.
let displayLink = CADisplayLink(target: self, selector: #selector(updateAnimation))
displayLink.preferredFrameRateRange = CAFrameRateRange(
minimum: 10, maximum: 30, preferred: 30 // Match primary content
)
displayLink.add(to: .current, forMode: .default)
Low Power Mode Response
if ProcessInfo.processInfo.isLowPowerModeEnabled {
reduceEnergyUsage()
}
NotificationCenter.default.publisher(for: .NSProcessInfoPowerStateDidChange)
.sink { [weak self] _ in
if ProcessInfo.processInfo.isLowPowerModeEnabled {
self?.reduceEnergyUsage()
} else {
self?.restoreNormalOperation()
}
}
.store(in: &cancellables)
func reduceEnergyUsage() {
// Increase timer intervals
// Reduce animation frame rates
// Defer network requests
// Stop non-critical location updates
}
On-Device Power Profiler (Without Mac)
From WWDC25-226: Capture traces in real-world conditions.
1. Settings > Privacy & Security > Developer Mode > Enable
2. Settings > Developer > Performance Trace > Enable
3. Control Center > Add "Performance Trace" shortcut
4. Swipe down > Tap icon > Start (records up to 10 hours)
5. Share trace via AirDrop to Mac
Production Monitoring with MetricKit
class EnergyMetricsManager: NSObject, MXMetricManagerSubscriber {
func didReceive(_ payloads: [MXMetricPayload]) {
for payload in payloads {
if let cpu = payload.cpuMetrics {
logMetric("foreground_cpu", value: cpu.cumulativeCPUTime)
}
if let location = payload.locationActivityMetrics {
logMetric("background_location", value: location.cumulativeBackgroundLocationTime)
}
}
}
}
Xcode Organizer > Battery Usage: Foreground vs background energy breakdown, category breakdown, version comparison.
Real-World Examples
Video app eager loading (WWDC25-226): VStack creating all thumbnails upfront caused CPU power impact 21. Changed to LazyVStack, dropped to 4.3.
Location-based suggestions (WWDC25-226): JSON parsed on every location update during commute. Cached with lazy var, eliminated CPU spikes.
Music app audio session: Session never deactivated after stop. Added setActive(false, options: .notifyOthersOnDeactivation), eliminated background drain.
Diagnostics
30-Second Check
- Device plugged in? (Power metrics show 0)
- Debug build? (Less optimized than release)
- Low Power Mode on? (May affect measurements)
5-Minute Check (Power Profiler)
- Which subsystem is dominant? (CPU/GPU/Network/Display)
- Sustained or spiky?
- Foreground or background?
15-Minute Investigation
- If CPU: Run Time Profiler to identify function
- If Network: Check request frequency and size
- If GPU: Check animation frame rates
- If Background: Check Info.plist modes
Common Quick Fixes
| Finding | Fix | Time |
|---|---|---|
| Timer without tolerance | .tolerance = 0.1 | 1 min |
| VStack with large ForEach | Change to LazyVStack | 1 min |
| allowsExpensiveNetworkAccess = true | Set false | 1 min |
| Missing stopUpdatingLocation | Add stop call | 2 min |
| No Dark Mode | Add asset variants | 30 min |
| Audio session always active | setActive(false) | 5 min |
Background Drain Patterns
| Pattern | Power Profiler Signature | Fix |
|---|---|---|
| Continuous location | CPU lane + location icon | significant-change |
| Audio session leak | CPU lane steady | setActive(false) |
| Timer not invalidated | CPU spikes at intervals | invalidate in background |
| Polling from background | Network lane at intervals | Push notifications |
| BGTask too long | CPU sustained | Faster completion |
Audit Checklists
Timers: Tolerance >= 10%, invalidated when done, using Combine, no polling that could be push, stopped on background.
Network: Requests batched, discretionary for non-urgent, waitsForConnectivity, allowsExpensiveNetworkAccess=false for deferrable, push not poll.
Location: Appropriate accuracy (not Best unless navigation), distanceFilter set, stopped when done, significant-change for background, background justified.
Background: endBackgroundTask called promptly, BGProcessingTask uses requiresExternalPower, background modes limited, audio deactivated, EMRCA followed.
Display/GPU: Dark Mode supported, animations stop when hidden, frame rates appropriate, blur minimized, Metal has frame limiting.
Disk I/O: Writes batched, SQLite WAL mode, no rapid create/delete, SwiftData/Core Data for frequent updates.
Key Energy Savings
| Optimization | Potential Savings |
|---|---|
| Dark Mode on OLED | Up to 70% display power |
| Frame rate alignment | Up to 20% GPU power |
| Push vs poll | 100x network efficiency |
| Location accuracy reduction | 50-90% GPS power |
| Timer tolerance | Significant CPU savings |
| Lazy loading | Eliminates startup CPU spikes |
Related
WWDC: 2019-417, 2019-707, 2020-10095, 2022-10083, 2025-226, 2025-227
Docs: /documentation/backgroundtasks, /documentation/corelocation, /documentation/metrickit
Skills: ax-energy-ref, ax-performance, ax-networking
Source
git clone https://github.com/Kasempiternal/axiom-v2/blob/main/axiom-plugin/skills/ax-energy/SKILL.mdView on GitHub Overview
ax-energy helps diagnose power drains by profiling subsystems (CPU, GPU, Network, Location, Display) and observing background execution patterns. It guides engineers to reduce energy consumption with practical patterns like push-based updates, reduced location accuracy, and lazy loading.
How This Skill Works
Use Xcode's Power Profiler to measure per-app energy impact across subsystems (CPU, GPU, Display, Network). Combine profiling with established patterns (timer tolerance, push vs poll, lazy loading, proper background task handling) and follow the decision tree to identify the dominant energy drain.
When to Use It
- App drains battery or device gets hot
- Auditing power consumption in QA or production
- Investigating CPU/GPU/Network/Location/Display subsystems for energy impact
- Tuning timers, polling, and background tasks to reduce energy usage
- Optimizing location accuracy and background updates for efficiency
Quick Start
- Step 1: Connect iPhone wirelessly to Xcode and enable Power Profiler
- Step 2: In Xcode, Profile with the Power Profiler (and optionally CPU Profiler) and run the app for 2-3 minutes
- Step 3: Expand the Power Profiler track and examine per-app lanes: CPU, GPU, Display, and Network Power Impact
Best Practices
- Run Power Profiler and inspect per-app lanes (CPU, GPU, Display, Network) to identify dominant drains
- Apply timer tolerance and migrate from fixed timers to event-driven updates
- Prefer server push over polling to reduce radio activity and energy use
- Choose appropriate location accuracy and update frequency (e.g., significant-change or async updates)
- Use lazy loading to minimize CPU spikes and end background tasks promptly when done
Example Use Cases
- Power Profiler workflow to reveal CPU, GPU, Display, and Network power impact
- Implementing timer tolerance and replacing fixed timers with publishers
- Switching from periodic polling to server-push updates for efficiency
- Reducing location energy use with appropriate accuracy and distance filters
- Replacing bulk view creation with on-demand rendering (LazyVStack) and timely background completion