A seemingly ordinary list refresh routine can keep the CPU continuously active. It can also increase wakeups through overly frequent timers, duplicate network requests, or background tasks. Functional tests usually pass, and code coverage does not reveal the problem. To catch these changes before merging, use a consistent test environment on an OVPS cloud Mac, record the same sequence of user actions with xctrace, and compare the results against a validated baseline.
Define what the gate measures first
Energy use is not a single value independent of context. An idle home screen, continuous scrolling, image decoding, and background synchronization have entirely different resource profiles and must not share the same baseline. Start by selecting a reproducible core flow that lasts 60 to 120 seconds, such as launching the app, opening the message list, scrolling through three screens, opening an item, and returning.
Record at least the following conditions for each scenario:
- App commit revision and build configuration;
- Device model, OS version, and battery level range;
- Screen brightness, network type, and Low Power Mode status;
- Test account data volume;
- Sampling duration, warm-up count, and number of measured runs.
The simulator is useful for verifying that automation scripts and actions are stable, but it cannot represent energy use on physical hardware. The production gate should be tied to a dedicated physical device; simulator results should only serve as supporting evidence of process activity.
Do not use the first run as a measurement. The initial launch may include database migrations, shader preparation, or cache population. Perform one warm-up run, then record at least three measured runs and use the median to reduce noise from incidental system tasks.
Stabilize the cloud Mac and test device state
The execution node should use a fixed major Xcode version and select the developer directory explicitly. Do not rely on environment variables that happen to be set in an interactive shell.
set -euo pipefail
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
xcodebuild -version
xcrun xctrace version
xcrun xctrace list devices
xcrun xctrace list templates
The last two commands also serve as compatibility checks. Available templates and export structures can change between Xcode versions, so the script must not assume that Energy Log will always exist. If the template cannot be found, exit immediately and retain the version information instead of falling back to a meaningless empty sample.
Before testing, return the physical device to a consistent state: close unrelated apps, keep the battery within a similar range, confirm that the temperature has returned to normal, disable automatic updates, and stabilize network conditions. Do not run archiving, dependency downloads, or disk cleanup tasks during the test. Additional load on a shared node will contaminate CPU and I/O readings.
Add a preflight check
Write the device UDID, Bundle ID, scenario name, and commit identifier into the same run directory. The directory name should be unique, but tokens and signing materials must not appear in paths or logs.
RUN_ID="$(date -u +%Y%m%dT%H%M%SZ)-${GIT_COMMIT:-local}"
OUT_DIR="artifacts/energy/${RUN_ID}"
mkdir -p "$OUT_DIR"
xcrun simctl list devices > "$OUT_DIR/devices.txt"
xcrun xctrace list templates > "$OUT_DIR/templates.txt"
git rev-parse HEAD > "$OUT_DIR/commit.txt"
When using a connected physical device, the first command can be replaced with the team’s own device discovery command. The exact command is not important; what matters is stopping the measurement when discovery fails so that “device not connected” is not misinterpreted as “energy use decreased.”
Record a repeatable xctrace trace
Launch the process under test and complete the warm-up before attaching by process name. The following script requires the device, process, and template to be provided explicitly through CI parameters:
DEVICE_UDID="${DEVICE_UDID:?missing DEVICE_UDID}"
PROCESS_NAME="${PROCESS_NAME:?missing PROCESS_NAME}"
TRACE="$OUT_DIR/energy.trace"
xcrun xctrace record \
--template "Energy Log" \
--device "$DEVICE_UDID" \
--attach "$PROCESS_NAME" \
--time-limit 90s \
--output "$TRACE"
After recording starts, use UI automation to perform the fixed sequence of actions. The automation script should use accessibility identifiers rather than screen coordinates. Test data should be preloaded, and network requests should preferably target a stable test environment. If a run encounters a login failure, an obstructing dialog, or a page that never loads, mark that run as invalid and exclude it from the median.
A successful xctrace exit only means that a trace was generated; it does not confirm that the scenario ran correctly. For every recording, also retain the UI test results, start and end times, an application log summary, and a scenario completion marker. If any item is missing, the gate should report “invalid measurement” rather than a performance pass.
Export metrics and compare them with the baseline
First export the trace table of contents to determine which tables the current Xcode version provides, then maintain parsing rules for that version:
xcrun xctrace export \
--input "$TRACE" \
--toc \
--output "$OUT_DIR/toc.xml"
Do not parse a binary trace directly with a fragile regular expression. A more robust approach is to maintain XPath expressions or XML parsers for each major Xcode version and archive the original trace alongside the exported data. The gate can track four categories of change: sustained CPU activity, thread wakeups, timer firing frequency, and network transfer volume. These are diagnostic signals and should not be crudely combined into a single “energy score.”
The baseline should come from multiple normal runs using the same device, scenario, and build type. Store the median from the most recently validated version while retaining the raw value from every run. Compare both relative change and an absolute lower bound: doubling a very small value may have no practical engineering significance.
Handle anomalies in tiers
Minor deviations should initially produce a warning without blocking the merge. Fail only after the threshold is exceeded repeatedly or when CPU activity and wakeups regress together. Thresholds must be derived from the team’s historical samples rather than copied from a generic percentage.
A failure report should include at least the commit identifier, device and OS versions, all three sample values, the median, the baseline, the percentage change, the trace path, and the UI scenario result. This lets developers open the evidence directly instead of receiving only an “energy use too high” message.
Trace regressions back to the code
When CPU activity remains elevated, first inspect main-thread polling, image processing, repeated layout calculations, and background queues that never finish. When wakeups increase, focus on short-interval timers, frequent disk writes, duplicate notifications, and network retries that have not been consolidated. For abnormal network activity, verify pagination requests, cache hit rates, telemetry batching, and reconnection policies.
Common false positives must also be ruled out separately: temperature changes after the device was recently charged, cache building that continues during the first run, changes in test account data volume, system dialogs interrupting the flow, and other resource-intensive tasks running concurrently on the cloud Mac. After a fix, rerun all three measurements under the same conditions. Do not use one favorable result to overwrite a failed record.
Ultimately, the value of an energy regression gate is not in producing an attractive score. It comes from linking the test scenario, device conditions, raw traces, and code commit. Only when this evidence can be collected repeatedly can anomalies be attributed clearly and the gate avoid becoming a source of noise that teams routinely bypass.
Frequently asked questions
Can the iOS Simulator replace a physical device for energy checks?
No. The simulator can validate scripts and scenario stability, but the final energy baseline should come from a fixed physical device model and OS version.
Should one baseline breach block a merge?
No. Collect at least three runs, compare their median with the baseline, and block only when the increase persists and an inspectable trace is available.
Run your next build on a dedicated physical node
Choose from three Apple Silicon configurations and six available nodes. Actual availability is determined by the status returned in real time by the control panel.