Skip to content

Fix AA Background Processing to not Monopolize CPU#3873

Merged
chrisuthe merged 8 commits into
music-assistant:devfrom
chrisuthe:feat/aa-realtime-pacing
May 11, 2026
Merged

Fix AA Background Processing to not Monopolize CPU#3873
chrisuthe merged 8 commits into
music-assistant:devfrom
chrisuthe:feat/aa-realtime-pacing

Conversation

@chrisuthe
Copy link
Copy Markdown
Member

Currently background processing goes As Fast As Possible, using as much CPU as it can while it's running (default midnight to 4 AM). This PR shifts the AFAP to be throttled to at MOST 4x real time, also enforcing that for real-time analysis during playback.

The background process will run one song at a time unless 8+ cores of CPU are detected, then it will run 2 songs until 16+ are detected then it will run 4.

chrisuthe added 6 commits May 11, 2026 13:00
Add a one-second minimum interval between consecutive PCM chunk
dispatches in both the live and background paths. Each chunk
represents one audio-second, so pacing at 1.0s dispatches at
real-time, naturally bounding analysis CPU to "what providers need
to keep up with playback" without explicit measurement or
per-provider throttling.

The live path's producer paces automatically via existing
chunk_callback backpressure; the audio buffer hovers near
ready_threshold rather than racing ahead. Background scan duration
becomes the sum of track durations; CONF_BACKGROUND_SCAN_CONCURRENCY
remains as the speedup knob for capable hardware.
Add min_interval and max_interval parameters to _chunk_worker,
_run_background_streaming_for_track, and _run_background_streaming_inner.
The two production call sites now pass values explicitly, demonstrating
the per-call pattern.

min_interval is the floor on wall-seconds between dispatches (i.e. the
speed ceiling); max_interval is the ceiling on wall-seconds between
dispatches (the speed floor) and is reserved for future adaptive logic.

Behavior is unchanged: both call sites pass REAL_TIME_PACE_INTERVAL_SECONDS
for both bounds. The API surface is in place for future config-driven
or adaptive pacing without further structural changes.
…u_count

DEFAULT_BACKGROUND_SCAN_CONCURRENCY now scales with the host's CPU count
via _default_background_scan_concurrency(): 1 on hosts with <8 cores,
2 on 8–15 cores, 4 on 16+. Mirrors the pattern used for buffer size
selection by total RAM (streams/constants.py _get_default_buffer_size).

CONF_BACKGROUND_SCAN_CONCURRENCY remains the user override; only the
default value changes. The existing _get_scan_concurrency() clamping to
[1, 8] is unaffected.

Update test_get_scan_concurrency_returns_default_on_unset to assert
against DEFAULT_BACKGROUND_SCAN_CONCURRENCY directly rather than the
literal 1, so it passes on every host regardless of cpu_count.
…adroom

Pacing at exactly 1.0s left the live audio buffer running dead-even with
playback rate — no margin if the producer hiccups. 0.9s gives ffmpeg
roughly 11% headroom to keep the buffer slightly ahead of the player,
while still being well under the 1s per-chunk timeout that bounds
provider work.

Background scans also complete ~11% faster as a side effect.
…e CEILING as per-chunk timeout

Replace REAL_TIME_PACE_INTERVAL_SECONDS with two related constants:

  REAL_TIME_PACE_INTERVAL_SECONDS_FLOOR    = 0.5  (fastest dispatch)
  REAL_TIME_PACE_INTERVAL_SECONDS_CEILING  = 1.0  (slowest dispatch / timeout)

The floor is the fastest pace we'll ever dispatch at (when providers can
keep up); the ceiling is now both the slowest pace and the per-chunk
processing timeout that evicts unresponsive providers. This collapses
two previously-separate concepts (interval and CHUNK_PROCESS_TIMEOUT_SECONDS)
into one parameterized bound and eliminates the redundant constant.

_distribute_chunk now accepts max_interval as a parameter so callers can
control the eviction threshold per session. The live and background
dispatchers thread their max_interval through to _distribute_chunk so
patching the ceiling constant (or passing different values per call)
propagates correctly. The live _on_chunk queue.put timeout also uses
the ceiling directly, since both are bounded by the same "how long do
we wait on one chunk" semantic.
…ime throughput

The 0.5s floor still left half a wall-second of idle between dispatches
when providers complete quickly. 0.25s gives capable hardware up to 4×
real-time throughput on chunks the providers can actually keep up with,
while the 1.0s ceiling still bounds slow-provider eviction.
Copy link
Copy Markdown
Member

@marcelveldt marcelveldt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, @chrisuthe !

chrisuthe added 2 commits May 11, 2026 13:59
The pacing-params commit added min_interval and max_interval kwargs to
_run_background_streaming_for_track and _run_background_streaming_inner.
Four test stubs that monkeypatch those methods don't accept the kwargs
and were raising TypeError on call.

Adding **_kwargs: object to each stub absorbs the new arguments without
caring about their values — matches the existing _hang stub pattern in
the same file. Resolves three CI failures:

  test_run_background_scan_uses_union_candidate_query
  test_run_background_scan_concurrency_semaphore
  test_background_streaming_cancellation_cleans_up
@chrisuthe chrisuthe enabled auto-merge (squash) May 11, 2026 19:08
@chrisuthe chrisuthe merged commit 5274a13 into music-assistant:dev May 11, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants