Skip to content

Feed parsing runs on the main actor, and the iOS package is built in debug #20

Open

Description

@AlexAxthelm

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.update as bincode, and the
    resulting UpsertFeedWithEpisodes carrying 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

  1. 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
    separate package-release target for device and distribution builds rather than the
    default. Worth doing whatever else is decided.
  2. Run Core.update off the main actor. The core would live on its own serial executor
    and publish view to the main actor. Fixes the whole class of stalls, but touches every
    call site in the shell and the @MainActor assumptions around it; needs a design
    discussion.
  3. 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.
  4. 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: the RefreshFetched and FeedFetched handlers (both call
    parse_feed inside update), and iOS/Pollux/core.swift (@MainActor Core,
    resolveAndDispatch).
  • Makefile: the package target (cargo bin cargo-swift package ..., no --release).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions