Repository navigation
Feed parsing runs on the main actor, and the iOS package is built in debug #20
Description
Summary
A feed 200 is parsed synchronously inside Core.update, which is @MainActor, so the
UI is blocked for the length of the parse. Feed refresh (PR #18) makes this happen
repeatedly in the foreground (auto-refresh on launch and activation, Refresh all); before
it, it only happened once per user-initiated subscribe. Separately, make package builds
the Rust core unoptimized, so the cost is much higher than a release build would be.
Found while reviewing PR #18. The numbers below measure the Rust part only.
Measurements
Timed Pollux::update handling Event::FeedFetched with a 200 (parse plus building the
upsert effect) on synthetic feeds: 1.5 KB description plus content:encoded per episode,
about 3 KB each.
| Episodes | Feed size | Release build | Debug build |
|---|---|---|---|
| 100 | 0.3 MB | 3.5 ms | 23 ms |
| 500 | 1.6 MB | 3.3 ms | 36 ms |
| 2,000 | 6.3 MB | 12.5 ms | 110 ms |
| 5,000 | 30 MB | 40 ms | 313 ms |
A frame is about 16 ms at 60 Hz and 8 ms at 120 Hz. In a release build a typical 100 to
500 episode podcast is fine and a 2,000-episode archive feed is roughly one dropped
frame. In a debug build every size drops frames, and a large archive feed freezes the UI
for around a tenth of a second.
cargo swift package defaults to a debug build (-r, --release is opt-in) and the Makefile
package target does not pass it, so as built from this repo the app runs the debug
numbers. How any release or TestFlight build is produced is not documented here.
Not measured
- The Swift side of the same payload. The body goes into
core.updateas bincode, and the
resultingUpsertFeedWithEpisodescarrying every episode comes back out, both across the
FFI on the main actor. That could cost as much as the parse. - Behaviour on a device, as opposed to the simulator.
Options
- Build the Rust package with
--release. One flag, roughly a 6 to 10x speedup for the
whole app, not just refresh. Costs: slower builds and worse debugging, so probably a
separatepackage-releasetarget for device and distribution builds rather than the
default. Worth doing whatever else is decided. - Run
Core.updateoff the main actor. The core would live on its own serial executor
and publishviewto the main actor. Fixes the whole class of stalls, but touches every
call site in the shell and the@MainActorassumptions around it; needs a design
discussion. - Parse outside
update. An effect or a second FFI entry point parses in the shell,
off-main. Breaks the core's pure shape, and still pays the FFI copy cost. - Measure first. Profile with Instruments on a device, with a real large feed, and decide
from that.
Suggested plan
Option 1 plus option 4. Defer option 2 unless the device profile shows real hitches.
Where
shared/src/app.rs: theRefreshFetchedandFeedFetchedhandlers (both call
parse_feedinsideupdate), andiOS/Pollux/core.swift(@MainActorCore,
resolveAndDispatch).Makefile: thepackagetarget (cargo bin cargo-swift package ..., no--release).
Activity