Add scan flow MVP and local Axiom skill workspace
This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
This commit is contained in:
7
.claude/skills/axiom-energy/.openskills.json
Normal file
7
.claude/skills/axiom-energy/.openskills.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"source": "CharlesWiltgen/Axiom",
|
||||
"sourceType": "git",
|
||||
"repoUrl": "https://github.com/CharlesWiltgen/Axiom",
|
||||
"subpath": "axiom-codex/skills/axiom-energy",
|
||||
"installedAt": "2026-04-12T08:06:12.950Z"
|
||||
}
|
||||
851
.claude/skills/axiom-energy/SKILL.md
Normal file
851
.claude/skills/axiom-energy/SKILL.md
Normal file
@@ -0,0 +1,851 @@
|
||||
---
|
||||
name: axiom-energy
|
||||
description: Use when app drains battery, device gets hot, users report energy issues, or auditing power consumption - systematic Power Profiler diagnosis, subsystem identification (CPU/GPU/Network/Location/Display), anti-pattern fixes for iOS/iPadOS
|
||||
license: MIT
|
||||
metadata:
|
||||
version: "1.0.0"
|
||||
---
|
||||
|
||||
# Energy Optimization
|
||||
|
||||
## Overview
|
||||
|
||||
Energy issues manifest as battery drain, hot devices, and poor App Store reviews. **Core principle**: Measure before optimizing. Use Power Profiler to identify the dominant subsystem (CPU/GPU/Network/Location/Display), then apply targeted fixes.
|
||||
|
||||
**Key insight**: Developers often don't know where to START auditing. This skill provides systematic diagnosis, not guesswork.
|
||||
|
||||
**Requirements**: iOS 26+, Xcode 26+, Power Profiler in Instruments
|
||||
|
||||
## Example Prompts
|
||||
|
||||
Real questions developers ask that this skill answers:
|
||||
|
||||
#### 1. "My app is always at the top of Battery Settings. How do I find what's draining power?"
|
||||
→ The skill covers Power Profiler workflow to identify dominant subsystem and targeted fixes
|
||||
|
||||
#### 2. "Users report my app makes their phone hot. Where do I start debugging?"
|
||||
→ The skill provides decision tree: CPU vs GPU vs Network diagnosis with specific patterns
|
||||
|
||||
#### 3. "I have timers and location updates. Are they causing battery drain?"
|
||||
→ The skill covers timer tolerance, location accuracy trade-offs, and audit checklists
|
||||
|
||||
#### 4. "My app drains battery in the background even when users aren't using it."
|
||||
→ The skill covers background execution patterns, BGTasks, and EMRCA principles
|
||||
|
||||
#### 5. "How do I measure if my optimization actually improved battery life?"
|
||||
→ The skill demonstrates before/after Power Profiler comparison workflow
|
||||
|
||||
---
|
||||
|
||||
## Red Flags — High Energy Likely
|
||||
|
||||
If you see ANY of these, suspect energy inefficiency:
|
||||
|
||||
- **Battery Settings**: Your app consistently at top of battery consumers
|
||||
- **Device temperature**: Phone gets warm during normal app use
|
||||
- **User reviews**: Mentions of "battery drain", "hot phone", "kills my battery"
|
||||
- **Xcode Energy Gauge**: Shows sustained high or very high impact
|
||||
- **Background runtime**: App runs longer than expected when not visible
|
||||
- **Network activity**: Frequent small requests instead of batched operations
|
||||
- **Location icon**: Appears in status bar when app shouldn't need location
|
||||
|
||||
#### Difference from normal energy use
|
||||
- **Normal**: App uses energy during active use, minimal when backgrounded
|
||||
- **Problem**: App uses significant energy even when user isn't interacting
|
||||
|
||||
## Mandatory First Steps
|
||||
|
||||
**ALWAYS run Power Profiler FIRST** before optimizing code:
|
||||
|
||||
### Step 1: Record a Power Trace (5 minutes)
|
||||
|
||||
```
|
||||
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
|
||||
7. Use your app normally for 2-3 minutes
|
||||
8. Click Stop
|
||||
```
|
||||
|
||||
**Why wireless**: When device is charging via cable, power metrics show 0. Use wireless debugging for accurate readings.
|
||||
|
||||
### Step 2: Identify Dominant Subsystem
|
||||
|
||||
Expand the Power Profiler track and examine per-app metrics:
|
||||
|
||||
| Lane | Meaning | High Value Indicates |
|
||||
|------|---------|---------------------|
|
||||
| CPU Power Impact | Processor activity | Computation, timers, parsing |
|
||||
| GPU Power Impact | Graphics rendering | Animations, blur, Metal |
|
||||
| Display Power Impact | Screen usage | Brightness, always-on content |
|
||||
| Network Power Impact | Radio activity | Requests, downloads, polling |
|
||||
|
||||
**Look for**: Which subsystem shows highest sustained values during your app's usage.
|
||||
|
||||
### Step 3: Branch to Subsystem-Specific Fixes
|
||||
|
||||
Once you identify the dominant subsystem, use the decision trees below.
|
||||
|
||||
#### What this tells you
|
||||
- **CPU dominant** → Check timers, polling, JSON parsing, eager loading
|
||||
- **GPU dominant** → Check animations, blur effects, frame rates
|
||||
- **Network dominant** → Check request frequency, polling vs push
|
||||
- **Display dominant** → Check Dark Mode, brightness, screen-on time
|
||||
- **Location** (shown in CPU) → Check accuracy, update frequency
|
||||
|
||||
#### Why diagnostics first
|
||||
- Finding root cause with Power Profiler: **15-20 minutes**
|
||||
- Guessing and testing random optimizations: **4+ hours, often wrong subsystem**
|
||||
|
||||
---
|
||||
|
||||
## Energy Decision Tree
|
||||
|
||||
```
|
||||
User reports energy issue?
|
||||
│
|
||||
├─ CPU Power Impact dominant?
|
||||
│ ├─ Continuous high impact?
|
||||
│ │ ├─ Timers running? → Pattern 1: Timer Efficiency
|
||||
│ │ ├─ Polling data? → Pattern 2: Push vs Poll
|
||||
│ │ └─ Processing in loop? → Pattern 3: Lazy Loading
|
||||
│ ├─ Spikes during specific actions?
|
||||
│ │ ├─ JSON parsing? → Cache parsed results
|
||||
│ │ ├─ Image processing? → Move to background, cache
|
||||
│ │ └─ Database queries? → Index, batch, prefetch
|
||||
│ └─ High background CPU?
|
||||
│ ├─ Location updates? → Pattern 4: Location Efficiency
|
||||
│ ├─ BGTasks running too long? → Pattern 5: Background Execution
|
||||
│ └─ Audio session active? → Stop when not playing
|
||||
│
|
||||
├─ Network Power Impact dominant?
|
||||
│ ├─ Many small requests?
|
||||
│ │ └─ Batch into fewer large requests
|
||||
│ ├─ Polling pattern detected?
|
||||
│ │ └─ Convert to push notifications → Pattern 2
|
||||
│ ├─ Downloads in foreground?
|
||||
│ │ └─ Use discretionary background URLSession
|
||||
│ └─ High cellular usage?
|
||||
│ └─ Defer to WiFi when possible
|
||||
│
|
||||
├─ GPU Power Impact dominant?
|
||||
│ ├─ Continuous animations?
|
||||
│ │ └─ Stop when view not visible
|
||||
│ ├─ Blur effects (UIVisualEffectView)?
|
||||
│ │ └─ Reduce or remove, use solid colors
|
||||
│ ├─ High frame rate animations?
|
||||
│ │ └─ Audit secondary frame rates → Pattern 6
|
||||
│ └─ Metal rendering?
|
||||
│ └─ Implement frame limiting
|
||||
│
|
||||
├─ Display Power Impact dominant?
|
||||
│ ├─ Light backgrounds on OLED?
|
||||
│ │ └─ Implement Dark Mode (up to 70% savings)
|
||||
│ ├─ High brightness content?
|
||||
│ │ └─ Use darker UI elements
|
||||
│ └─ Screen always on?
|
||||
│ └─ Allow screen to sleep when appropriate
|
||||
│
|
||||
└─ Location causing drain? (check CPU lane + location icon)
|
||||
├─ Continuous updates?
|
||||
│ └─ Switch to significant-change monitoring
|
||||
├─ High accuracy (kCLLocationAccuracyBest)?
|
||||
│ └─ Reduce to kCLLocationAccuracyHundredMeters
|
||||
└─ Background location?
|
||||
└─ Evaluate if truly needed → Pattern 4
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Common Energy Patterns (With Fixes)
|
||||
|
||||
### Pattern 1: Timer Efficiency
|
||||
|
||||
**Problem**: Timers wake the CPU from idle states, consuming significant energy.
|
||||
|
||||
#### ❌ Anti-Pattern — Timer without tolerance
|
||||
```swift
|
||||
// BAD: Timer fires exactly every 1.0 seconds
|
||||
// Prevents system from batching with other timers
|
||||
Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
|
||||
self.updateUI()
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ Fix — Set tolerance for timer batching
|
||||
```swift
|
||||
// GOOD: 10% tolerance allows system to batch timers
|
||||
let timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in
|
||||
self.updateUI()
|
||||
}
|
||||
timer.tolerance = 0.1 // 10% tolerance minimum
|
||||
|
||||
// BETTER: Use Combine Timer with tolerance
|
||||
Timer.publish(every: 1.0, tolerance: 0.1, on: .main, in: .default)
|
||||
.autoconnect()
|
||||
.sink { [weak self] _ in
|
||||
self?.updateUI()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
```
|
||||
|
||||
#### ✅ Best — Use event-driven instead of polling
|
||||
```swift
|
||||
// BEST: Don't use timer at all — react to events
|
||||
NotificationCenter.default.publisher(for: .dataDidUpdate)
|
||||
.sink { [weak self] _ in
|
||||
self?.updateUI()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
```
|
||||
|
||||
**Key points**:
|
||||
- Set tolerance to **at least 10%** of interval
|
||||
- Timer tolerance allows system to batch multiple timers into single wake
|
||||
- Prefer event-driven patterns over polling timers
|
||||
- Always invalidate timers when no longer needed
|
||||
|
||||
---
|
||||
|
||||
### Pattern 2: Push vs Poll
|
||||
|
||||
**Problem**: Polling (checking server every N seconds) keeps radios active and drains battery.
|
||||
|
||||
#### ❌ Anti-Pattern — Polling every 5 seconds
|
||||
```swift
|
||||
// BAD: Polls server every 5 seconds
|
||||
// Radio stays active, massive battery drain
|
||||
Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true) { [weak self] _ in
|
||||
self?.fetchLatestData() // Network request every 5 seconds
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ Fix — Use background push notifications
|
||||
```swift
|
||||
// GOOD: Server pushes when data changes
|
||||
// Radio only active when there's actual new data
|
||||
|
||||
// 1. Register for remote notifications
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
|
||||
// 2. Handle background notification
|
||||
func application(_ application: UIApplication,
|
||||
didReceiveRemoteNotification userInfo: [AnyHashable: Any],
|
||||
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
|
||||
|
||||
guard let _ = userInfo["content-available"] else {
|
||||
completionHandler(.noData)
|
||||
return
|
||||
}
|
||||
|
||||
Task {
|
||||
do {
|
||||
let hasNewData = try await fetchLatestData()
|
||||
completionHandler(hasNewData ? .newData : .noData)
|
||||
} catch {
|
||||
completionHandler(.failed)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Server payload for background push**:
|
||||
```json
|
||||
{
|
||||
"aps": {
|
||||
"content-available": 1
|
||||
},
|
||||
"custom-data": "your-payload"
|
||||
}
|
||||
```
|
||||
|
||||
**Key points**:
|
||||
- Background pushes are **discretionary** — system delivers at optimal time
|
||||
- Use `apns-priority: 5` for non-urgent updates (energy efficient)
|
||||
- Use `apns-priority: 10` only for time-sensitive alerts
|
||||
- Polling every 5 seconds uses **100x more energy** than push
|
||||
|
||||
---
|
||||
|
||||
### Pattern 3: Lazy Loading & Caching
|
||||
|
||||
**Problem**: Loading all data upfront causes CPU spikes and memory pressure.
|
||||
|
||||
#### ❌ Anti-Pattern — Eager loading (from WWDC25-226)
|
||||
```swift
|
||||
// BAD: Creates and renders ALL views upfront
|
||||
// From WWDC25-226: This caused CPU spike and hang
|
||||
VStack {
|
||||
ForEach(videos) { video in
|
||||
VideoCardView(video: video) // Creates ALL thumbnails immediately
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ Fix — Lazy loading
|
||||
```swift
|
||||
// GOOD: Only creates visible views
|
||||
// From WWDC25-226: Reduced CPU power impact from 21 to 4.3
|
||||
LazyVStack {
|
||||
ForEach(videos) { video in
|
||||
VideoCardView(video: video) // Creates on-demand
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### ❌ Anti-Pattern — Repeated parsing (from WWDC25-226)
|
||||
```swift
|
||||
// BAD: Parses JSON file on every location update
|
||||
// From WWDC25-226: Caused continuous CPU drain during commute
|
||||
func videoSuggestionsForLocation(_ location: CLLocation) -> [Video] {
|
||||
// Called every location change!
|
||||
let data = try? Data(contentsOf: rulesFileURL)
|
||||
let rules = try? JSONDecoder().decode([RecommendationRule].self, from: data)
|
||||
return filteredVideos(using: rules)
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ Fix — Cache parsed data
|
||||
```swift
|
||||
// GOOD: Parse once, reuse cached result
|
||||
// From WWDC25-226: Eliminated CPU drain
|
||||
private lazy var cachedRules: [RecommendationRule] = {
|
||||
let data = try? Data(contentsOf: rulesFileURL)
|
||||
return (try? JSONDecoder().decode([RecommendationRule].self, from: data)) ?? []
|
||||
}()
|
||||
|
||||
func videoSuggestionsForLocation(_ location: CLLocation) -> [Video] {
|
||||
return filteredVideos(using: cachedRules) // No parsing!
|
||||
}
|
||||
```
|
||||
|
||||
**Key points**:
|
||||
- Use `LazyVStack`, `LazyHStack`, `LazyVGrid` for large collections
|
||||
- Cache parsed JSON, decoded data, computed results
|
||||
- Move expensive operations out of frequently-called methods
|
||||
|
||||
---
|
||||
|
||||
### Pattern 4: Location Efficiency
|
||||
|
||||
**Problem**: Continuous location updates keep GPS active, draining battery rapidly.
|
||||
|
||||
#### ❌ Anti-Pattern — Continuous high-accuracy updates
|
||||
```swift
|
||||
// BAD: Continuous updates with best accuracy
|
||||
// GPS stays active constantly, massive battery drain
|
||||
let locationManager = CLLocationManager()
|
||||
locationManager.desiredAccuracy = kCLLocationAccuracyBest
|
||||
locationManager.startUpdatingLocation() // Never stops!
|
||||
```
|
||||
|
||||
#### ✅ Fix — Appropriate accuracy and significant-change
|
||||
```swift
|
||||
// GOOD: Reduced accuracy, significant-change monitoring
|
||||
let locationManager = CLLocationManager()
|
||||
|
||||
// Use appropriate accuracy (100m is fine for most apps)
|
||||
locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters
|
||||
|
||||
// Use distance filter to reduce updates
|
||||
locationManager.distanceFilter = 100 // Only update every 100 meters
|
||||
|
||||
// For background: Use significant-change monitoring
|
||||
locationManager.startMonitoringSignificantLocationChanges()
|
||||
|
||||
// Stop when done
|
||||
func stopTracking() {
|
||||
locationManager.stopUpdatingLocation()
|
||||
locationManager.stopMonitoringSignificantLocationChanges()
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ Better — iOS 26+ CLLocationUpdate with stationary detection
|
||||
```swift
|
||||
// BEST: Modern async API with automatic stationary detection
|
||||
for try await update in CLLocationUpdate.liveUpdates() {
|
||||
if update.stationary {
|
||||
// Device stopped moving — system pauses updates automatically
|
||||
// Switch to CLMonitor for region monitoring
|
||||
break
|
||||
}
|
||||
handleLocation(update.location)
|
||||
}
|
||||
```
|
||||
|
||||
**Accuracy comparison (battery impact)**:
|
||||
| Accuracy | Battery Impact | Use Case |
|
||||
|----------|---------------|----------|
|
||||
| `kCLLocationAccuracyBest` | Very High | Navigation apps only |
|
||||
| `kCLLocationAccuracyNearestTenMeters` | High | Fitness tracking |
|
||||
| `kCLLocationAccuracyHundredMeters` | Medium | Store locators |
|
||||
| `kCLLocationAccuracyKilometer` | Low | Weather apps |
|
||||
| Significant-change | Very Low | Background updates |
|
||||
|
||||
---
|
||||
|
||||
### Pattern 5: Background Execution (EMRCA)
|
||||
|
||||
**Problem**: Background tasks that run too long or too often drain battery.
|
||||
|
||||
#### EMRCA Principles (from WWDC25-227)
|
||||
|
||||
Your background work must be:
|
||||
- **E**fficient — Design lightweight, purpose-driven tasks
|
||||
- **M**inimal — Keep background work to a minimum
|
||||
- **R**esilient — Save incremental progress; respond to expiration signals
|
||||
- **C**ourteous — Honor user preferences and system conditions
|
||||
- **A**daptive — Understand and adapt to system priorities
|
||||
|
||||
#### ❌ Anti-Pattern — Long-running background task
|
||||
```swift
|
||||
// BAD: Requests unlimited background time
|
||||
// System will terminate after ~30 seconds anyway
|
||||
var backgroundTask: UIBackgroundTaskIdentifier = .invalid
|
||||
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
backgroundTask = application.beginBackgroundTask {
|
||||
// Expiration handler — but task runs too long
|
||||
}
|
||||
|
||||
// Long operation that may not complete
|
||||
performLongOperation()
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ Fix — Proper background task handling
|
||||
```swift
|
||||
// GOOD: Finish quickly, save progress, notify system
|
||||
var backgroundTask: UIBackgroundTaskIdentifier = .invalid
|
||||
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
backgroundTask = application.beginBackgroundTask(withName: "Save State") { [weak self] in
|
||||
// Expiration handler — clean up immediately
|
||||
self?.saveProgress()
|
||||
if let task = self?.backgroundTask {
|
||||
application.endBackgroundTask(task)
|
||||
}
|
||||
self?.backgroundTask = .invalid
|
||||
}
|
||||
|
||||
// Quick operation
|
||||
saveEssentialState()
|
||||
|
||||
// End task as soon as done — don't wait for expiration
|
||||
application.endBackgroundTask(backgroundTask)
|
||||
backgroundTask = .invalid
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ For Long Operations — Use BGProcessingTask
|
||||
```swift
|
||||
// BEST: Let system schedule at optimal time (charging, WiFi)
|
||||
func scheduleBackgroundProcessing() {
|
||||
let request = BGProcessingTaskRequest(identifier: "com.app.maintenance")
|
||||
request.requiresNetworkConnectivity = true
|
||||
request.requiresExternalPower = true // Only when charging
|
||||
|
||||
try? BGTaskScheduler.shared.submit(request)
|
||||
}
|
||||
|
||||
// Register handler at app launch
|
||||
BGTaskScheduler.shared.register(
|
||||
forTaskWithIdentifier: "com.app.maintenance",
|
||||
using: nil
|
||||
) { task in
|
||||
self.handleMaintenance(task: task as! BGProcessingTask)
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ iOS 26+ — BGContinuedProcessingTask for user-initiated work
|
||||
```swift
|
||||
// NEW iOS 26: Continue user-initiated tasks with progress UI
|
||||
let request = BGContinuedProcessingTaskRequest(
|
||||
identifier: "com.app.export",
|
||||
title: "Exporting Photos",
|
||||
subtitle: "23 of 100 photos"
|
||||
)
|
||||
|
||||
try? BGTaskScheduler.shared.submit(request)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Pattern 6: Frame Rate Auditing
|
||||
|
||||
**Problem**: Secondary animations running at higher frame rates than needed increase GPU power.
|
||||
|
||||
#### ❌ Anti-Pattern — Uncontrolled frame rates
|
||||
```swift
|
||||
// BAD: Secondary animation runs at 60fps
|
||||
// When primary content only needs 30fps, this wastes power
|
||||
UIView.animate(withDuration: 2.0, delay: 0, options: [.repeat]) {
|
||||
self.subtitleLabel.alpha = 0.5
|
||||
} completion: { _ in
|
||||
self.subtitleLabel.alpha = 1.0
|
||||
}
|
||||
```
|
||||
|
||||
#### ✅ Fix — Control frame rate with CADisplayLink
|
||||
```swift
|
||||
// GOOD: Explicitly set preferred frame rate
|
||||
let displayLink = CADisplayLink(target: self, selector: #selector(updateAnimation))
|
||||
displayLink.preferredFrameRateRange = CAFrameRateRange(
|
||||
minimum: 10,
|
||||
maximum: 30, // Match primary content
|
||||
preferred: 30
|
||||
)
|
||||
displayLink.add(to: .current, forMode: .default)
|
||||
```
|
||||
|
||||
**From WWDC22-10083**: Up to **20% battery savings** by aligning secondary animation frame rates with primary content.
|
||||
|
||||
---
|
||||
|
||||
## Audit Checklists
|
||||
|
||||
### Timer Audit
|
||||
- [ ] All timers have tolerance set (≥10% of interval)?
|
||||
- [ ] Timers invalidated when no longer needed?
|
||||
- [ ] Using Combine Timer instead of NSTimer where possible?
|
||||
- [ ] No polling patterns that could use push notifications?
|
||||
- [ ] Timers stopped when app enters background?
|
||||
|
||||
### Network Audit
|
||||
- [ ] Requests batched instead of many small requests?
|
||||
- [ ] Using discretionary URLSession for non-urgent downloads?
|
||||
- [ ] `waitsForConnectivity` set to avoid failed connection attempts?
|
||||
- [ ] `allowsExpensiveNetworkAccess` set to false for deferrable work?
|
||||
- [ ] Push notifications instead of polling?
|
||||
|
||||
### Location Audit
|
||||
- [ ] Using appropriate accuracy (not `kCLLocationAccuracyBest` unless navigation)?
|
||||
- [ ] `distanceFilter` set to reduce update frequency?
|
||||
- [ ] Stopping updates when no longer needed?
|
||||
- [ ] Using significant-change for background updates?
|
||||
- [ ] Background location justified and explained to users?
|
||||
|
||||
### Background Execution Audit
|
||||
- [ ] `endBackgroundTask` called promptly when work completes?
|
||||
- [ ] Long operations use `BGProcessingTask` with `requiresExternalPower`?
|
||||
- [ ] Background modes in Info.plist limited to what's actually needed?
|
||||
- [ ] Audio session deactivated when not playing?
|
||||
- [ ] EMRCA principles followed?
|
||||
|
||||
### Display/GPU Audit
|
||||
- [ ] Dark Mode supported (70% OLED power savings)?
|
||||
- [ ] Animations stopped when view not visible?
|
||||
- [ ] Secondary animations use appropriate frame rates?
|
||||
- [ ] Blur effects minimized or removed?
|
||||
- [ ] Metal rendering has frame limiting?
|
||||
|
||||
### Disk I/O Audit
|
||||
- [ ] Writes batched instead of frequent small writes?
|
||||
- [ ] SQLite using WAL journaling mode?
|
||||
- [ ] Avoiding rapid file creation/deletion?
|
||||
- [ ] Using SwiftData/Core Data instead of serialized files for frequent updates?
|
||||
|
||||
---
|
||||
|
||||
## Pressure Scenarios
|
||||
|
||||
### Scenario 1: "Just poll every 5 seconds for real-time updates"
|
||||
|
||||
**The temptation**: "Push notifications are complex. Polling is simpler."
|
||||
|
||||
**The reality**:
|
||||
- Polling every 5 seconds: Radio active **100% of time**
|
||||
- Push notifications: Radio active **only when data changes**
|
||||
- Users WILL see your app at top of Battery Settings
|
||||
- App Store reviews WILL mention "battery hog"
|
||||
|
||||
**Time cost comparison**:
|
||||
- Implement polling: 30 minutes
|
||||
- Implement push: 2-4 hours
|
||||
- Fix bad reviews + reputation damage: Weeks
|
||||
|
||||
**Pushback template**: "Push notification setup takes a few hours, but polling will guarantee we're at the top of Battery Settings. Users actively uninstall apps that drain battery. The 2-hour investment prevents ongoing reputation damage."
|
||||
|
||||
---
|
||||
|
||||
### Scenario 2: "Use continuous location for best accuracy"
|
||||
|
||||
**The temptation**: "Users expect accurate location. Let's use `kCLLocationAccuracyBest`."
|
||||
|
||||
**The reality**:
|
||||
- `kCLLocationAccuracyBest`: GPS + WiFi + Cellular triangulation = **massive drain**
|
||||
- `kCLLocationAccuracyHundredMeters`: Good enough for 95% of use cases
|
||||
- Location icon in status bar = users checking Battery Settings
|
||||
|
||||
**Time cost comparison**:
|
||||
- Implement high accuracy: 10 minutes
|
||||
- Debug "why does my app drain battery" complaints: Hours
|
||||
- Refactor to appropriate accuracy: 30 minutes
|
||||
|
||||
**Pushback template**: "100-meter accuracy is sufficient for [use case]. Navigation apps like Google Maps need best accuracy, but we're showing [store locations / weather / general area]. The accuracy difference is imperceptible to users, but battery difference is massive."
|
||||
|
||||
---
|
||||
|
||||
### Scenario 3: "Keep animations running, users expect smooth UI"
|
||||
|
||||
**The temptation**: "Animations make the app feel alive and polished."
|
||||
|
||||
**The reality**:
|
||||
- Animations running when view not visible = pure waste
|
||||
- High frame rate secondary animations = GPU drain
|
||||
- GPU power is significant portion of total device power
|
||||
|
||||
**Time cost comparison**:
|
||||
- Add animation: 15 minutes
|
||||
- Add visibility checks: 5 minutes extra
|
||||
- Debug "phone gets hot" reports: Hours
|
||||
|
||||
**Pushback template**: "We can keep the animation, but should pause it when the view isn't visible. This is a 5-minute change that prevents GPU drain when users aren't looking at the screen."
|
||||
|
||||
---
|
||||
|
||||
### Scenario 4: "Ship now, optimize later"
|
||||
|
||||
**The temptation**: "Energy optimization is polish. We can do it in v1.1."
|
||||
|
||||
**The reality**:
|
||||
- Battery drain is **immediately visible** to users
|
||||
- First impressions drive reviews
|
||||
- "Battery hog" reputation is hard to shake
|
||||
- Power Profiler baseline takes **15 minutes**
|
||||
|
||||
**Time cost comparison**:
|
||||
- Power Profiler check before launch: 15 minutes
|
||||
- Fix energy issues post-launch: Days (plus reputation damage)
|
||||
- Regain user trust: Months
|
||||
|
||||
**Pushback template**: "A 15-minute Power Profiler session before launch catches major energy issues. If we ship with battery problems, users will see us at top of Battery Settings on day one and leave 1-star reviews. Let me do a quick check — it's faster than damage control."
|
||||
|
||||
---
|
||||
|
||||
## Real-World Examples
|
||||
|
||||
### Example 1: Video Streaming App with Eager Loading (WWDC25-226)
|
||||
|
||||
**Symptom**: CPU power impact jumped from 1 to 21 when opening Library pane. UI hung.
|
||||
|
||||
**Diagnosis using Power Profiler**:
|
||||
1. Recorded trace while opening Library pane
|
||||
2. CPU Power Impact lane showed massive spike
|
||||
3. Time Profiler showed `VideoCardView` body called hundreds of times
|
||||
4. Root cause: `VStack` creating ALL video thumbnails upfront
|
||||
|
||||
**Fix**:
|
||||
```swift
|
||||
// Before: VStack (eager)
|
||||
VStack {
|
||||
ForEach(videos) { video in
|
||||
VideoCardView(video: video)
|
||||
}
|
||||
}
|
||||
|
||||
// After: LazyVStack (on-demand)
|
||||
LazyVStack {
|
||||
ForEach(videos) { video in
|
||||
VideoCardView(video: video)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Result**: CPU power impact dropped from 21 to 4.3. UI no longer hung.
|
||||
|
||||
---
|
||||
|
||||
### Example 2: Location-Based Suggestions with Repeated Parsing (WWDC25-226)
|
||||
|
||||
**Symptom**: User commuting reported massive battery drain. Developer couldn't reproduce at desk.
|
||||
|
||||
**Diagnosis using on-device Power Profiler**:
|
||||
1. User collected trace during commute (Settings → Developer → Performance Trace)
|
||||
2. Trace showed periodic CPU spikes correlating with movement
|
||||
3. Time Profiler showed `videoSuggestionsForLocation` consuming CPU
|
||||
4. Root cause: JSON file parsed on EVERY location update
|
||||
|
||||
**Fix**:
|
||||
```swift
|
||||
// Before: Parse on every call
|
||||
func videoSuggestionsForLocation(_ location: CLLocation) -> [Video] {
|
||||
let data = try? Data(contentsOf: rulesFileURL)
|
||||
let rules = try? JSONDecoder().decode([RecommendationRule].self, from: data)
|
||||
return filteredVideos(using: rules)
|
||||
}
|
||||
|
||||
// After: Parse once, cache
|
||||
private lazy var cachedRules: [RecommendationRule] = {
|
||||
let data = try? Data(contentsOf: rulesFileURL)
|
||||
return (try? JSONDecoder().decode([RecommendationRule].self, from: data)) ?? []
|
||||
}()
|
||||
|
||||
func videoSuggestionsForLocation(_ location: CLLocation) -> [Video] {
|
||||
return filteredVideos(using: cachedRules)
|
||||
}
|
||||
```
|
||||
|
||||
**Result**: Eliminated CPU spikes during movement. Battery drain resolved.
|
||||
|
||||
---
|
||||
|
||||
### Example 3: Music App with Always-Active Audio Session
|
||||
|
||||
**Symptom**: App drains battery even when not playing music.
|
||||
|
||||
**Diagnosis**:
|
||||
1. Power Profiler showed sustained background CPU activity
|
||||
2. Audio session remained active after playback stopped
|
||||
3. System kept audio hardware powered on
|
||||
|
||||
**Fix**:
|
||||
```swift
|
||||
// Before: Never deactivate
|
||||
func playTrack(_ track: Track) {
|
||||
try? AVAudioSession.sharedInstance().setActive(true)
|
||||
player.play()
|
||||
}
|
||||
|
||||
func stopPlayback() {
|
||||
player.stop()
|
||||
// Audio session still active!
|
||||
}
|
||||
|
||||
// After: Deactivate when done
|
||||
func stopPlayback() {
|
||||
player.stop()
|
||||
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
||||
}
|
||||
|
||||
// Even better: Use AVAudioEngine auto-shutdown
|
||||
let engine = AVAudioEngine()
|
||||
engine.isAutoShutdownEnabled = true // Automatically powers down when idle
|
||||
```
|
||||
|
||||
**Result**: Background audio hardware powered down. Battery drain eliminated.
|
||||
|
||||
---
|
||||
|
||||
## Responding to Low Power Mode
|
||||
|
||||
Detect and adapt when user enables Low Power Mode:
|
||||
|
||||
```swift
|
||||
// Check current state
|
||||
if ProcessInfo.processInfo.isLowPowerModeEnabled {
|
||||
reduceEnergyUsage()
|
||||
}
|
||||
|
||||
// React to changes
|
||||
NotificationCenter.default.publisher(for: .NSProcessInfoPowerStateDidChange)
|
||||
.sink { [weak self] _ in
|
||||
if ProcessInfo.processInfo.isLowPowerModeEnabled {
|
||||
self?.reduceEnergyUsage()
|
||||
} else {
|
||||
self?.restoreNormalOperation()
|
||||
}
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
|
||||
func reduceEnergyUsage() {
|
||||
// Pause optional activities
|
||||
// Reduce animation frame rates
|
||||
// Increase timer intervals
|
||||
// Defer network requests
|
||||
// Stop location updates if not critical
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Monitoring Energy in Production
|
||||
|
||||
### MetricKit Setup
|
||||
|
||||
```swift
|
||||
import MetricKit
|
||||
|
||||
class EnergyMetricsManager: NSObject, MXMetricManagerSubscriber {
|
||||
static let shared = EnergyMetricsManager()
|
||||
|
||||
func startMonitoring() {
|
||||
MXMetricManager.shared.add(self)
|
||||
}
|
||||
|
||||
func didReceive(_ payloads: [MXMetricPayload]) {
|
||||
for payload in payloads {
|
||||
if let cpuMetrics = payload.cpuMetrics {
|
||||
// Monitor CPU time
|
||||
let foregroundCPU = cpuMetrics.cumulativeCPUTime
|
||||
logMetric("foreground_cpu", value: foregroundCPU)
|
||||
}
|
||||
|
||||
if let locationMetrics = payload.locationActivityMetrics {
|
||||
// Monitor location usage
|
||||
let backgroundLocation = locationMetrics.cumulativeBackgroundLocationTime
|
||||
logMetric("background_location", value: backgroundLocation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Xcode Organizer
|
||||
|
||||
Check **Battery Usage** pane in Xcode Organizer for field data:
|
||||
- Foreground vs background energy breakdown
|
||||
- Category breakdown (Audio, Networking, Processing, Display, etc.)
|
||||
- Version comparison to detect regressions
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Power Profiler Workflow
|
||||
```
|
||||
1. Connect device wirelessly
|
||||
2. Product → Profile → Blank → Add Power Profiler
|
||||
3. Record 2-3 minutes of usage
|
||||
4. Identify dominant subsystem (CPU/GPU/Network/Display)
|
||||
5. Apply targeted fix from patterns above
|
||||
6. Record again to verify improvement
|
||||
```
|
||||
|
||||
### 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 Skills
|
||||
- `axiom-energy-ref` — Complete API reference with all code examples
|
||||
- `axiom-energy-diag` — Symptom-based troubleshooting decision trees
|
||||
- `axiom-background-processing` — Background task mechanics (why tasks don't run)
|
||||
- `axiom-performance-profiling` — General Instruments workflows
|
||||
- `axiom-memory-debugging` — Memory leak diagnosis (often related to energy)
|
||||
- `axiom-networking` — Network optimization patterns
|
||||
- `axiom-timer-patterns` — Timer crash prevention and lifecycle safety
|
||||
|
||||
---
|
||||
|
||||
## WWDC Sessions
|
||||
|
||||
- **WWDC25-226** "Profile and optimize power usage in your app" — Power Profiler workflow
|
||||
- **WWDC25-227** "Finish tasks in the background" — BGContinuedProcessingTask, EMRCA
|
||||
- **WWDC22-10083** "Power down: Improve battery consumption" — Dark Mode, frame rates, deferral
|
||||
- **WWDC20-10095** "The Push Notifications primer" — Push vs poll
|
||||
- **WWDC19-417** "Improving Battery Life and Performance" — MetricKit
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-26
|
||||
**Platforms**: iOS 26+, iPadOS 26+
|
||||
**Status**: Production-ready energy optimization patterns
|
||||
3
.claude/skills/axiom-energy/agents/openai.yaml
Normal file
3
.claude/skills/axiom-energy/agents/openai.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
interface:
|
||||
display_name: "Energy"
|
||||
short_description: "App drains battery, device gets hot, users report energy issues, or auditing power consumption"
|
||||
Reference in New Issue
Block a user