Summary
- A sysdiagnose carries BatteryUI property lists that hold per-app foreground and background runtime and energy values, by time bucket. Before this contribution no iLEAPP artifact read them.
- The pull request adds three artifacts: one row per app entry of each bucket, one row per bucket, and one row per file with the query time and, for an upgrade file, the old and new build.
- The unit of the runtimes was tested on the files, not assumed. The stored bucket width differs between the iOS 26.6 and the iOS 27.0 file, so the report shows the width as stored on every row.
- A row is a runtime figure the file records for a bucket. It does not show that a person opened the app, and an app without a row is not evidence that it did not run.
- 3artifacts added
- 699per-app rows from the iOS 27.0 image (72 buckets, 2 files)
- 272per-app rows from the iOS 26.6 image (36 buckets, 1 file)
- 12 of 12registered test cases passing
Cite as
Addai, D., SectraTEK Lab (2026). Per-app runtime from the BatteryUI property lists: an iLEAPP contribution. iLEAPP pull request #2521, merged 20 October 2026. https://github.com/abrignoni/iLEAPP/pull/2521
Methodology note
Drafted with AI assistance and reviewed by the author before publication.
The issue
On a phone, the battery screen answers a plain question: which apps used the device, and for how long? A sysdiagnose keeps the data behind that screen in property lists under logs/BatteryUIPlist. After an operating system update it also keeps an upgrade file whose name carries the old and the new build. Until this contribution, the only mention of BatteryUI in iLEAPP was a table name in the power log reader; the files themselves were not read.
Opening one of them by hand shows why. The numbers carry no units. A foreground runtime of 1200.0 could be seconds or minutes. Bucket boundaries are bare epoch values. The file stores a bucket width, but as Figure 4 shows, the width is not the same from one file to the next, so a reader who assumes an hour per bucket can be wrong by a factor of 24. Figure 1 shows one entry as stored and as reported.
Show this figure as a table
| Stored key | Report column |
|---|---|
| PLBatteryUIBucketStartKey | Bucket Start (UTC), read as Unix epoch seconds |
| PLBatteryUIBucketEndKey | Bucket End (UTC); Bucket Seconds is end minus start |
| PLBatteryUIAppBundleIDKey | App Identifier (as stored) |
| PLBatteryUIAppForegroundRuntimeKey | Foreground Runtime (s) |
| PLBatteryUIAppEnergyValueKey | Energy Value (as stored) |
The risk is practical. A runtime read as minutes understates an hour of use sixty-fold. A runtime read against the wrong bucket width places activity in the wrong period. Neither mistake raises an error, so the report would look sound.
The essence
The three artifacts read the BatteryUI lists and report what is stored, with the unit stated only where we proved it. The first has a row for each app entry in each bucket of each of the four range entries, with the bucket's start and end as UTC times in the first columns so the rows reach the timeline. The second has a row for each bucket, including buckets with no app entries. The third has one row per file.
| Artifact | A row is | Main columns |
|---|---|---|
| Battery UI - Per-App Runtime | One app entry of one bucket of one range entry | Bucket start and end (UTC); bucket seconds; range; app identifier and name; foreground and background runtime (s); plugged-in runtimes and call off-screen time as stored; energy values as stored |
| Battery UI - Time Buckets | One bucket | Foreground and background time; energy totals; error code; number of app entries, all as stored |
| Battery UI - Snapshots and Upgrade Marker | One file | Query time; last charge time and level; for an upgrade file, the old and new build taken from the file name; data duration as stored |
The notes shipped with the artifacts say what each column is. Energy values are labeled "as stored" because no unit is established. The range entries overlap, so rows of different ranges are not added together. An upgrade file is a lead that the build changed, not a statement of when.
Our methodology
The reader was written from the layout of the files. A Breakdown section holds four range entries; two hold one bucket each and two hold a list. Every bucket has stored start and end keys, and a list of app entries. The reader walks that tree and yields a row at each level (Figure 2). Where a key is missing or not a number, the cell stays blank rather than receiving a guess.
The unit was the hard part, so we tested it against the data instead of assuming it. Four tests, run on the iOS 27.0 and the iOS 26.6 files:
- no foreground runtime in a tap bucket exceeds the bucket's stored width in seconds (on the wider corpus, one value does, by 8.6 seconds in an upgrade file);
- in the iOS 27.0 file, ten values equal exactly 3,600.0, the signature of an hour cap on a value in seconds;
- in the iOS 26.6 file the largest value in an hour-wide bucket is 2,635.4, far more than an hour holds in minutes;
- each bucket's foreground and background time equals the sum of its app values.
Figure 3 puts the first three on one scale. Together they support seconds. They do not prove it for every file, and the notes say which files were read. Time values are read as Unix epoch seconds; in both source files the bucket boundaries fall on whole hours, and the main file's last bucket ends at the local midnight after the date in the sysdiagnose folder name.
Show this figure as a table
| Observation | Value |
|---|---|
| Stored width of the iOS 26.6 DayTapKey bucket | 3,600 s |
| Largest value in an hour-wide bucket, iOS 26.6 file | 2,635.4 |
| Values equal to exactly 3,600.0, iOS 27.0 file | 10; none larger |
| What one hour holds if the unit were minutes | 60 |
Counts were checked with separate code. On the iOS 27.0 image the reader gave 699 per-app rows in 72 buckets from two files, 2 snapshot rows and 701 timeline rows; on the iOS 26.6 image, 272 rows in 36 buckets from one file. Each count equals one taken straight from the property lists with no iLEAPP code, and the HTML, TSV, LAVA and timeline outputs match.
During review the maintainer asked for more data, so the reader was run on five older sysdiagnoses from a public test corpus (iOS 13.3.1 to 26.6). Four gave rows: 443 per-app rows in 36 buckets on iOS 14.3, 173 in 36 on iOS 17.3, 1,473 in 144 on one iOS 26 image and 156 in 108 on another. The iOS 13.3.1 image holds only an older BatteryUI.plist, which the reader names as not read. That run also showed where the files differ: no stored bucket times on iOS 14.3 and boundaries off the hour on iOS 17.3. The notes were extended to say so. On one image the figures did not line up with the power log for the same hours (of 39 app-hours, 5 were shared), so the notes warn that one does not stand in for the other.
Test data is synthetic or sanitized. The unit tests build a tree with invented com.example.* apps and formula-built numbers in both bucket shapes. The registered cases add two copies of the real structure with every identifying value replaced, all times shifted by one constant offset of whole weeks, and runtimes and energy values scaled by fixed factors while the stored relations are kept. The replacement mapping is not in the repository.
The value
The artifact lets an examiner ask which apps accounted for foreground and background runtime in a given period, from a sysdiagnose alone, without a full file system extraction. The time columns put the rows on the same timeline as other sources. The bucket width and range are on every row, so the reader can see the resolution of what they are looking at.
Figure 4 is the main caution. In the iOS 27.0 file the DayTapKey and WeekKey buckets are stored 86,400 seconds wide, the same as the other entries, while ten DayTapKey values still sit at the one-hour cap and the WeekKey values exceed the stored width. The stored times of those entries therefore do not bound the values. We report the width as stored and say so in the notes; what produces the day-wide shape is not established.
Show this figure as a table
| Range entry | iOS 26.6 file | iOS 27.0 file |
|---|---|---|
| DayKey | 86,400 | 86,400 |
| DayTapKey | 3,600 | 86,400 |
| WeekKey | 864,000 | 86,400 |
| WeekTapKey | 86,400 | 86,400 |
A row is a runtime figure kept for a bucket. It shows that the power-logging service summarised that much runtime for the app, not what the app did, and not when within a wide bucket. The file covers a limited window, and its stored data duration has no stated unit (twelve days if it is seconds). An empty result is not evidence of no activity. Only two source files were available for the unit tests, so the findings are about those files and the corpus runs, not about every device.
Where this sits in the Lab's research
This contribution supports LAB-TR-0011, Log Compression and Archive Structure in the iOS 27.0 Sysdiagnose: An Early Observation Tested Against Ten Archives (version 1.2, preprint, not peer reviewed; doi:10.5281/zenodo.23720069). Section 4.3 tests the unit of the runtimes again across ten archives and shows the day-wide bucket shape on the iOS 27.0 archive only among them. The report is on the Lab site at LAB-TR-0011.
LAB-TR-0010, the report on reading new sysdiagnose files, is in preparation and has no public page yet. The method above rests on the pull request.
Sources
Every number and claim above, with where it came from. PR means the pull request text, its diff or its review thread.
| Number or claim | Where it came from |
|---|---|
| Three artifacts; four range entries; three file levels | PR text; scripts/artifacts/batteryUISysdiagnose.py |
| 699 per-app rows, 72 buckets, 2 files, 2 snapshot rows, 701 timeline rows (iOS 27.0); 272 rows, 36 buckets, 1 file (iOS 26.6) | PR text, section Tested |
| Counts equal an independent count from the plists; HTML, TSV, LAVA and timeline match | PR text, section Tested |
| 12 of 12 registered test cases pass | PR text, section Tested |
| Ten values equal 3,600.0 (iOS 27.0); largest value 2,635.4 in an hour-wide bucket (iOS 26.6) | PR text, paragraph on the unit check; LAB-TR-0011 section 4.3 |
| Bucket widths 3,600 / 864,000 (iOS 26.6) and 86,400 everywhere (iOS 27.0) | PR text; artifact notes |
| One corpus value exceeds its width by 8.6 s (3,608.6 in a 3,600 s bucket) | Artifact notes in the diff |
| Corpus runs 443/36, 173/36, 1,473/144, 156/108 (rows/buckets); iOS 13.3.1 gives none | PR text, section Corpus run (added in review) |
| 5 app-hours shared with the power log of 39 | Artifact notes in the diff |
| Stored data duration: twelve days if it is in seconds | PR text, section What the data does and does not show |
| Sanitized copies: times shifted by one offset of whole weeks, values scaled, mapping not in the repository | PR text, section Real-structure test fixtures |
| Merged 20 October 2026 | GitHub pull request metadata (mergedAt) |