funkatron / funkatron/now-playing

Investigate memory growth in long-running LaunchAgent polling Apple Music/Spotify

Open
#5 0 comments 0 reactions 1 assignee Claimed by @funkatron View on GitHub
bug
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.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.