Skip to content

Perf: cut per-tick allocation and SwiftData churn on the 20 Hz playback path - #117

Merged
sanylax0 merged 1 commit into
mainfrom
claude/repo-review-improvements-yvk3t7-tick-perf
Jul 20, 2026
Merged

sanylax0 merged 1 commit into
mainfrom
claude/repo-review-improvements-yvk3t7-tick-perf

Conversation

@sanylax2

Copy link
Copy Markdown
Collaborator

Part 2 of the CPU/memory audit — the playback hot path, most directly tied to "random audio bugs under high CPU load". All changes are behavior-identical.

  • 20 Hz timer no longer allocates a Task per tick. The timer already fires on the main run loop, so tick() now runs synchronously via MainActor.assumeIsolated (the same pattern the audio-session handlers use). The old wrapper was ~1,200 heap allocations + actor-hop re-enqueues per minute, each pushing the tick to a later scheduling slot — worst exactly when the CPU is contended.
  • effectiveEndSeconds stops hitting SwiftData 20×/s. track.audibleEndSeconds is a managed-object read that contends with ingest writes; it's now served from a cache refreshed ~1 Hz in tick() (the value only ever changes when the silence scan backfills it, minutes before it matters at track end; a ≤1 s pickup delay is imperceptible). UI reads (secondsUntilTransition) share the cache.
  • Periodic persist (~5 s) stops re-reading the whole queue. queue.map(\.id) (one SwiftData read per queued track) is now cached and invalidated only when the queue actually mutates; JSONEncoder/JSONDecoder are static instead of per-call.
  • Lock-screen artwork fetch drops to .utility priority.

Device check

  • Play through a full track + auto-transition + manual skip: progress/scrubbing/blends identical; resume-on-relaunch still lands at the right song/position; lock-screen art still appears.
  • With silence trimming on, gapless early-advance still triggers at the same spot.

🤖 Generated with Claude Code

https://claude.ai/code/session_013TJoWkqg8bzkGdzxWhWjWP


Generated by Claude Code

Audit findings for audio glitches under high CPU load — all behavior-
identical:
- The 20 Hz timer wrapped every tick() in a fresh Task even though it
  already fires on the main run loop: ~1200 heap allocations + actor-hop
  requeues per minute, each deferring the tick to a later scheduling
  slot. Run it synchronously via MainActor.assumeIsolated (same pattern
  as the audio-session handlers).
- effectiveEndSeconds read track.audibleEndSeconds (a SwiftData managed
  read) 20x/s from tick + UI; now served from a ~1 Hz-refreshed cache
  (the value only moves when the silence scan backfills, minutes before
  it matters).
- The ~5 s periodic persist re-mapped queue.map(\.id) (one SwiftData
  read per queued track) and built a fresh JSONEncoder every call; the
  ID array is now invalidated only on queue mutation and the coders are
  static.
- Lock-screen artwork fetch drops to utility priority.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013TJoWkqg8bzkGdzxWhWjWP
@sanylax0
sanylax0 merged commit 632e0f3 into main Jul 20, 2026
4 checks passed
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.

3 participants