Merge pull request #6 from goetzc/fix/high-cpu-usage
Fix high CPU usage: optimize artwork fetches and idle wake-ups
This commit is contained in:
commit
49a75f8118
5 changed files with 45 additions and 16 deletions
|
|
@ -51,7 +51,7 @@ def main() -> None:
|
|||
asyncio.run(
|
||||
listen(config, listener, receivers),
|
||||
loop_factory=factory.make_loop,
|
||||
debug=True,
|
||||
debug=False,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -30,19 +30,28 @@ class MpdArtworkCache:
|
|||
mpd: MpdStateHandler
|
||||
album_cache: Cache[Artwork]
|
||||
track_cache: Cache[Artwork]
|
||||
pending_tracks: set[str]
|
||||
|
||||
def __init__(self, mpd: MpdStateHandler, cache_url: URL = MEMORY):
|
||||
self.mpd = mpd
|
||||
self.album_cache = make_cache(ArtworkSchema, cache_url, "album")
|
||||
self.track_cache = make_cache(ArtworkSchema, cache_url, "track")
|
||||
self.pending_tracks = set()
|
||||
|
||||
async def get_cached_artwork(self, song: CurrentSongResponse) -> bytes | None:
|
||||
art = await self.track_cache.get(calc_track_key(song))
|
||||
if art:
|
||||
return art.data
|
||||
track_key = calc_track_key(song)
|
||||
art = await self.track_cache.get(track_key)
|
||||
if art is not None:
|
||||
# NoArtwork is a valid cached value too: returning None here avoids
|
||||
# repeatedly re-querying MPD for files that have no embedded art.
|
||||
if art:
|
||||
return art.data
|
||||
return None
|
||||
|
||||
# If we don't have track artwork cached, go find some.
|
||||
run_background_task(self.cache_artwork(song))
|
||||
if track_key not in self.pending_tracks:
|
||||
self.pending_tracks.add(track_key)
|
||||
run_background_task(self.cache_artwork(song))
|
||||
|
||||
# Even if we don't have cached track art, we can try looking for cached album art.
|
||||
art = await self.album_cache.get(calc_album_key(song))
|
||||
|
|
@ -52,10 +61,16 @@ class MpdArtworkCache:
|
|||
return None
|
||||
|
||||
async def cache_artwork(self, song: CurrentSongResponse) -> None:
|
||||
art = to_artwork(await self.mpd.get_art(song["file"]))
|
||||
track_key = calc_track_key(song)
|
||||
try:
|
||||
await self.album_cache.add(calc_album_key(song), art, ttl=CACHE_TTL)
|
||||
except ValueError:
|
||||
pass
|
||||
await self.track_cache.set(calc_track_key(song), art, ttl=CACHE_TTL)
|
||||
await self.mpd.refresh()
|
||||
art = to_artwork(await self.mpd.get_art(song["file"]))
|
||||
try:
|
||||
await self.album_cache.add(calc_album_key(song), art, ttl=CACHE_TTL)
|
||||
except ValueError:
|
||||
pass
|
||||
await self.track_cache.set(track_key, art, ttl=CACHE_TTL)
|
||||
# Refresh receivers only when we discovered actual artwork.
|
||||
if art:
|
||||
await self.mpd.refresh()
|
||||
finally:
|
||||
self.pending_tracks.discard(track_key)
|
||||
|
|
|
|||
|
|
@ -17,10 +17,13 @@ def to_queue(mpd: MpdState) -> Queue:
|
|||
|
||||
def to_mixramp(mpd: MpdState) -> MixRamp:
|
||||
delay = mpd.status.get("mixrampdelay", 0)
|
||||
db = mpd.status.get("mixrampdb", 0)
|
||||
if delay == "nan":
|
||||
delay = 0
|
||||
if db == "nan":
|
||||
db = 0
|
||||
return MixRamp(
|
||||
db=float(mpd.status.get("mixrampdb", 0)),
|
||||
db=float(db),
|
||||
delay=float(delay),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@ from collections.abc import Iterable
|
|||
|
||||
from mpd.asyncio import MPDClient
|
||||
from mpd.base import CommandError
|
||||
from rich import print as rprint
|
||||
from yarl import URL
|
||||
|
||||
from ..config.model import MpdConfig
|
||||
|
|
@ -18,17 +17,23 @@ from .types import MpdState
|
|||
|
||||
|
||||
class MpdStateListener(Player):
|
||||
# Subsystems relevant to now-playing metadata and remote controls.
|
||||
# Listening to all MPD subsystems can cause noisy wakeups (e.g. database
|
||||
# updates), which drives unnecessary status/currentsong polling.
|
||||
WATCHED_SUBSYSTEMS = ("player", "mixer", "options", "playlist", "partition")
|
||||
config: MpdConfig
|
||||
client: MPDClient
|
||||
receivers: Iterable[Receiver]
|
||||
art_cache: MpdArtworkCache
|
||||
idle_count = 0
|
||||
last_playback: Playback | None
|
||||
|
||||
def __init__(self, cache: URL | None = None) -> None:
|
||||
self.client = MPDClient()
|
||||
self.art_cache = (
|
||||
MpdArtworkCache(self, cache) if cache else MpdArtworkCache(self)
|
||||
)
|
||||
self.last_playback = None
|
||||
|
||||
async def start(self, conf: MpdConfig) -> None:
|
||||
self.config = conf
|
||||
|
|
@ -53,9 +58,12 @@ class MpdStateListener(Player):
|
|||
# Notify our receivers of the initial state MPD is in when this script loads up.
|
||||
await self.update_receivers()
|
||||
# And then wait for stuff to change in MPD. :)
|
||||
async for subsystems in self.client.idle():
|
||||
async for subsystems in self.client.idle(self.WATCHED_SUBSYSTEMS):
|
||||
# If no subsystems actually changed, we don't need to update the receivers.
|
||||
if not subsystems:
|
||||
# MPD/python-mpd2 can occasionally wake idle() without reporting a
|
||||
# changed subsystem; avoid a hot loop if that happens repeatedly.
|
||||
await asyncio.sleep(0.1)
|
||||
continue
|
||||
self.idle_count += 1
|
||||
await self.update_receivers()
|
||||
|
|
@ -79,7 +87,9 @@ class MpdStateListener(Player):
|
|||
|
||||
state = MpdState(status, current, art)
|
||||
pb = to_playback(self.config, state)
|
||||
rprint(pb)
|
||||
if pb == self.last_playback:
|
||||
return
|
||||
self.last_playback = pb
|
||||
await self.update(pb)
|
||||
|
||||
async def update(self, playback: Playback) -> None:
|
||||
|
|
|
|||
|
|
@ -19,7 +19,8 @@ def playback_to_media_item(playback: Playback) -> NSMutableDictionary:
|
|||
if song := playback.active_song:
|
||||
nowplaying_info = song_to_media_item(song)
|
||||
nowplaying_info[MPNowPlayingInfoPropertyPlaybackQueueCount] = playback.queue.length
|
||||
nowplaying_info[MPNowPlayingInfoPropertyPlaybackQueueIndex] = playback.queue.current
|
||||
if playback.queue.current is not None:
|
||||
nowplaying_info[MPNowPlayingInfoPropertyPlaybackQueueIndex] = playback.queue.current
|
||||
return nowplaying_info
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue