Skip to content

[Performance] Combine AsyncImageView optimizations#83

Merged
NachoSoto merged 11 commits into
masterfrom
combine-image-performance-optimizations
Jul 12, 2026
Merged

[Performance] Combine AsyncImageView optimizations#83
NachoSoto merged 11 commits into
masterfrom
combine-image-performance-optimizations

Conversation

@NachoSoto

Copy link
Copy Markdown
Owner

Summary

This branch combines the independently reviewed changes from:

Component benchmarks

Change WatchChess workload Result
UIKit unchanged-size guard (#77) 128,000 origin-only mutations -32.24% mean, scheduler calls 128,000 → 0
SwiftUI geometry (#78) 100 real cells × 80 relayouts -2.86% mean, 95% CI for saving 9.053–45.532 ms
Scheduler hop (#80) 96 cold + 96 repeated requests scheduler operations -50%; wall time neutral
Lazy SwiftUI state (#81) 20,000 body reconstructions -4.44% mean, 12/12 paired wins
Multicast completed hits (#82) 50,000 real PieceView hits -89.3% / 9.33× faster
Multicast distinct keys (#82) 24 concurrent keys neutral: -1.4% mean, 95% CI spans zero

Combined WatchChess benchmark

Release WatchChess, iPhone 17 Pro Max / iOS 26.5 simulators, 10 samples per variant and flow. Negative CPU/hitch changes are improvements.

CPU

Flow CPU time, baseline → combined Change (paired-bootstrap 95% CI) Mean one-core-equivalent CPU
Tournament-list scrolling 2.230 → 2.264 s +1.54% (-2.79% to +6.09%): neutral 12.01% → 12.07%
Rounds scrolling 1.059 → 1.024 s -3.25% (-5.55% to -1.27%) 13.03% → 12.79%
Open game 0.452 → 0.439 s -2.89% (-5.36% to -0.12%) 13.47% → 13.11% (-2.63%; CI -4.37% to -0.87%)

FPS and hitches

FPS below is a main-run-loop CADisplayLink callback proxy over the exact UI-action window, not compositor presentation FPS.

Flow FPS, baseline → combined FPS change (95% CI) Hitches/min, baseline → combined Hitch change (95% CI)
Tournament-list scrolling 58.93 → 59.00 +0.12% (-0.20% to +0.48%): neutral 40.67 → 37.39 -8.06% (-29.75% to +14.24%): neutral
Rounds scrolling 58.53 → 58.98 +0.77% (+0.13% to +1.39%) 63.24 → 51.68 -18.28% (-35.36% to +5.33%): trend, CI spans zero
Open game 54.14 → 54.07 -0.14% (-1.20% to +0.91%): neutral 131.04 → 145.44 +10.99% (-5.26% to +29.68%): neutral

Missed-refresh rates were also neutral for tournaments (-3.62%, CI -21.79% to +14.17%) and game (+2.99%, CI -5.93% to +13.06%). Rounds trended lower (-26.69%, CI -47.25% to +4.29%).

Methodology and controls

  • two-pass, pass-outer crossover: pass 1 baseline on simulator A then candidate on B; pass 2 candidate on A then baseline on B
  • five measurements per pass, variant, and flow; 10 total per variant/flow
  • engine analysis forced off (currentEngineStrength = 1) and validated inside every app launch/report
  • exact 24-entry real API fixture in cache-only mode; any cache miss failed the run
  • identical 147-path warm AsyncImageView cache coverage per variant
  • exact AsyncImageView path/hash/mtime validation before sample 1 and after every five-sample block
  • API path/size/hash validation before and after every block
  • arithmetic-mean CPU headline; duration-weighted ratio-of-sums FPS/hitch rates
  • deterministic 100,000-resample, pass-stratified paired bootstrap confidence intervals
  • symmetric full cache hashing before each measured block; these are deliberately warm-cache measurements
  • hitches: clipped display-link interval at least 1.5× its target refresh period

All earlier measurements with uncontrolled engine state or unequal cache warmness were quarantined and excluded.

Rendering and cache regression checks

  • cold game and rounds screenshots were pixel-identical; cold tournament app content was pixel-identical outside dynamic system chrome
  • all four pass-1 seed/verify tournament screenshots were pixel-identical across the 1320×2590 app region; the only difference was 5,708 pixels in the translucent system search-field blur
  • six gated 8 FPS recordings covered tournament scrolling, rounds scrolling, and opening a game for both variants
  • manual contact-sheet review found no blank flashes, stale substitutions, missing portraits, or persistent placeholders; both game recordings showed the same normal transition placeholders and matching fully rendered end states
  • all six recordings passed pre/post AsyncImageView and API cache audits
  • baseline and candidate kept identical 147 relative cache paths; each variant's hashes and mtimes remained stable

The different cache bytes between variants are intentional: #79 stores image scale in a versioned envelope. Legacy raw-PNG entries become one normal miss and regenerate in the corrected format. Existing remote images may therefore re-download once; offline users can temporarily miss legacy cached images until connectivity returns.

#79 and #82 semantics

#79 and the #82 synchronous-subscriber review finding are different fixes, and both are included:

#82's lifetime.observeEnded intentionally holds the wrapper and in-flight entry only until producer completion or cancellation. Weak-reference tests verify release after both paths, while cancellation still disposes upstream work; no retain cycle or leak was observed.

Validation

  • swiftlint --config .swiftlint.yml --strict: 0 violations
  • full iOS simulator suite: 69 XCTest/Quick + 13 Swift Testing tests = 82 passed
  • focused multicast concurrency, synchronous, retry, memory-warning, cancellation, and weak-lifetime coverage
  • WatchChess Release cache preflights, 60 CPU samples, 60 frame reports, and six visual recordings passed their evidence checks
  • CI updated to macOS 26 / Xcode 26.5

@NachoSoto NachoSoto marked this pull request as ready for review July 12, 2026 02:57
@NachoSoto NachoSoto merged commit 2362f3b into master Jul 12, 2026
1 check passed
@NachoSoto NachoSoto deleted the combine-image-performance-optimizations branch July 12, 2026 03:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant