funkatron / funkatron/now-playing
Investigate memory growth in long-running LaunchAgent polling Apple Music/Spotify
- Dominant language
- Python
- Stars
- 0
- Forks
- 0
- PR merge metrics
- No merged PRs in 30d
Description
## Summary
A long-running `now-playing` LaunchAgent appears to accumulate a very large Activity Monitor memory footprint over several days. The process was still responsive, but the system had significant compressed memory and swap usage.
## Observed behavior
Running LaunchAgent:
```text
/Users/coj/Library/LaunchAgents/com.funkatron.now-playing.plist
/Users/coj/alt-sync/src/DeadAgent/now-playing/.venv/bin/python3 /Users/coj/alt-sync/src/DeadAgent/now-playing/np_service.py serve
```
Runtime snapshot:
```text
PID 958
started Tue Jul 7 15:37:51 2026
elapsed ~5 days 23 hours
rss ~1.2 GB by ps at the time of inspection
vsz ~528 GB
Activity Monitor process list showed ~43 GB memory
Activity Monitor process detail showed ~1.17 GB real memory / ~1.07 GB private memory
```
System context at the same time:
```text
swap: total 9216 MB, used 8406 MB, free 810 MB
compressed memory: ~60 GB
/System/Volumes/Data: 1.8 TiB total, 1.7 TiB used, 61 GiB available, 97% full
```
This may be a true leak, a compressed-memory footprint buildup, or a virtual-address/Objective-C bridge allocation issue, but the process footprint is high for a small now-playing helper.
## Likely hotspots
### Apple Music path
`now_playing/providers/apple.py` currently does the following on each poll:
- creates a new `ScriptingBridge.SBApplication.applicationWithBundleIdentifier_("com.apple.Music")`
- fetches `currentTrack()`
- fetches `track.artworks()`
- extracts artwork data
- converts artwork through `NSBitmapImageRep.imageRepWithData_()`
- creates PNG data via `representationUsingType_properties_()`
- writes the PNG path
The artwork conversion runs every poll while playing, even if the track has not changed. With `INTERVAL_SECONDS=5`, this is repeated for days in a PyObjC/AppKit/ScriptingBridge process.
Relevant file:
```text
now_playing/providers/apple.py
```
### Spotify path
`now_playing/providers/spotify.py` shells out to `osascript` for every Spotify check:
```text
subprocess.run(["osascript", "-e", script], ...)
```
With `NOW_PLAYING_SOURCE=auto`, `now_playing/providers/auto.py` checks Apple Music first and, when Apple Music is playing, still checks Spotify for diagnostics:
```python
if apple_track.state == "playing":
return apple_track, {
"apple_music": apple_snapshot.to_payload(),
"spotify": inspect_provider("spotify")[1].to_payload(),
}
```
So even Apple Music playback causes a Spotify AppleScript subprocess every polling interval.
### Duplicate Apple implementation in Spotify module
`now_playing/providers/spotify.py` also contains what looks like a stale duplicate `get_apple_music_track()` implementation, including references to names that are not imported in that module. It appears unused, but it is confusing and may be leftover from the split/refactor.
## Suggested fixes
- Cache the last Apple Music track fingerprint and only extract/convert artwork when the track changes.
- Consider reusing or explicitly releasing/autoreleasing PyObjC objects if needed around the Apple Music artwork path.
- In `auto` mode, avoid polling Spotify diagnostics every cycle when Apple Music is already playing, or throttle diagnostics separately.
- Remove the stale duplicate Apple Music implementation from `spotify.py` if unused.
- Add a lightweight long-running/poll-loop test around provider selection to assert that unchanged tracks do not trigger repeated artwork extraction.
## Notes
This issue is from a live machine inspection, not yet from a reduced repro. The immediate user preference was not to stop the LaunchAgent yet, so the goal here is to track and fix likely leak/bloat sources before restarting masks the evidence.
Contributor guide
No contributing guide indexed for this repository
Assessment
This issue has not been assessed yet.