1
8
Fork
You've already forked nup
0

Reduce nup-headless CPU usage when idle #139

Closed
opened 2026年03月15日 01:42:26 +01:00 by derat · 9 comments
Owner
Copy link

nup-headless looks like it still takes ~4.3% CPU on the crappy Celeron where I have it running while playback is paused (compared to ~5.3% while playing). I suspect that this is from the audio library that I'm using (maybe I looked at it before?), but I should still do some profiling and see if there's some way to prevent this.

`nup-headless` looks like it still takes ~4.3% CPU on the crappy Celeron where I have it running while playback is paused (compared to ~5.3% while playing). I suspect that this is from the audio library that I'm using (maybe I looked at it before?), but I should still do some profiling and see if there's some way to prevent this.
Author
Owner
Copy link

I think that this is probably from github.com/hajimehoshi/ebiten/v2/audio.NewContext.func4, which calls github.com/hajimehoshi/ebiten/v2/audio.(*Context).updatePlayers every 100 ms.

This sounds like it's done for macOS game engine reasons that aren't relevant to me ("In the current Ebitengine implementation, update might not be called when the window is in background"). There's probably nothing I can do about this, and I'm still not aware of any other audio playback libraries for Go. Maybe I could use Cgo to wrap some other library (miniaudio?), but I'm not sure that it's worth the effort since the current gapless playback code is very tied to Ebitengine/Oto's needs.

I think that this is probably from [github.com/hajimehoshi/ebiten/v2/audio.NewContext.func4](https://github.com/hajimehoshi/ebiten/blob/f65118d0bf2d4cdc35b18d661e0a9f2bf9f3e81e/audio/audio.go#L149-L163), which calls [github.com/hajimehoshi/ebiten/v2/audio.(*Context).updatePlayers](https://github.com/hajimehoshi/ebiten/blob/f65118d0bf2d4cdc35b18d661e0a9f2bf9f3e81e/audio/audio.go#L266-L300) every 100 ms. This sounds like it's done for macOS game engine reasons that aren't relevant to me ("In the current Ebitengine implementation, update might not be called when the window is in background"). There's probably nothing I can do about this, and I'm still not aware of any other audio playback libraries for Go. Maybe I could use Cgo to wrap some other library ([miniaudio](https://miniaud.io/)?), but I'm not sure that it's worth the effort since the current gapless playback code is very tied to Ebitengine/Oto's needs.
derat changed title from (削除) Debug nup-headless CPU usage when idle (削除ここまで) to Reduce nup-headless CPU usage when idle 2026年03月15日 14:56:02 +01:00
Author
Owner
Copy link

Maybe I could close the audio.Player when the playlist is empty/ended or playback has been paused for a while and then create a new one when needed. I should test first that this actually reduces the idle CPU usage.

Maybe I could close the [audio.Player](https://pkg.go.dev/github.com/hajimehoshi/ebiten/v2@v2.9.9/audio#Player) when the playlist is empty/ended or playback has been paused for a while and then create a new one when needed. I should test first that this actually reduces the idle CPU usage.
Author
Owner
Copy link

I don't think that will help. Even just the following uses ~5% CPU:

packagemainimport("time""github.com/hajimehoshi/ebiten/v2/audio")funcmain(){audio.NewContext(44_100)time.Sleep(time.Minute)}
I don't think that will help. Even just the following uses ~5% CPU: ```go package main import ( "time" "github.com/hajimehoshi/ebiten/v2/audio" ) func main() { audio.NewContext(44_100) time.Sleep(time.Minute) } ```
Author
Owner
Copy link

Go bindings for miniaudio: https://github.com/gen2brain/malgo

Go bindings for miniaudio: https://github.com/gen2brain/malgo
Author
Owner
Copy link

Hmm. Audio playback seems to be far more performant with miniaudio (via malgo) than with Ebitengine. In a container on a crappy Chromebook, a trivial program that decodes an MP3 using minimp3 and plays it consumes 2-3% CPU with miniaudio, compared to 20-30% with Ebitengine. When I stop the miniaudio device that drops to 0%; when I pause the Ebitengine player it remains at 20-30%.

(Note that this device is slower than the one in the initial comment -- the 20-30% CPU here corresponds to the ~5% in the original comment.)

Ebitengine gives me the current playback position and lets me seek to new positions, and I don't think that there's anything similar in miniaudio (at least exposed by the malgo bindings); it just calls a read callback as long as the device is active. I could probably build my own abstraction on top of a miniaudio device that implements the audioPlayer interface used by gaplessPlayer:

typeaudioPlayerinterface{Position()time.DurationIsPlaying()boolPlay()Pause()SetPosition(time.Duration)error}

I'm not completely sure how to get the current playback position rather than what's been buffered, though. At least on the Chromebook, the buffer size passed to the read callback is just 3306 samples at 44.1 kHz stereo (30 ms, if I'm getting the math right), so maybe it doesn't really matter.

I'm also using Ebitengine for resampling on the rare occasion when it's needed, so I'd probably need to either keep using Ebitengine for that, extend malgo to wrap miniaudio's ma_resampler, or find some other Go audio resampling library.

Hmm. Audio playback seems to be far more performant with miniaudio (via malgo) than with Ebitengine. In a container on a crappy Chromebook, a trivial program that decodes an MP3 using minimp3 and plays it consumes 2-3% CPU with miniaudio, compared to 20-30% with Ebitengine. When I stop the miniaudio device that drops to 0%; when I pause the Ebitengine player it remains at 20-30%. (Note that this device is slower than the one in the initial comment -- the 20-30% CPU here corresponds to the ~5% in the original comment.) Ebitengine gives me the current playback position and lets me seek to new positions, and I don't think that there's anything similar in miniaudio (at least exposed by the malgo bindings); it just calls a read callback as long as the device is active. I could probably build my own abstraction on top of a miniaudio device that implements the `audioPlayer` interface used by `gaplessPlayer`: ```go type audioPlayer interface { Position() time.Duration IsPlaying() bool Play() Pause() SetPosition(time.Duration) error } ``` I'm not completely sure how to get the current playback position rather than what's been buffered, though. At least on the Chromebook, the buffer size passed to the read callback is just 3306 samples at 44.1 kHz stereo (30 ms, if I'm getting the math right), so maybe it doesn't really matter. I'm also using Ebitengine for resampling on the rare occasion when it's needed, so I'd probably need to either keep using Ebitengine for that, extend malgo to wrap miniaudio's `ma_resampler`, or find some other Go audio resampling library.
Author
Owner
Copy link

I... think I mostly have this working? It was much easier than I expected.

I... think I mostly have this working? It was much easier than I expected.
Author
Owner
Copy link

There's some trickiness due to synchronization differences between Ebitengine and miniaudio. Ebitengine allows calling play/pause from anywhere (I think) and automatically pauses when it gets an EOF. miniaudio keeps reading until you stop the device, which deadlocks if you do it from within the read callback.

There's some trickiness due to synchronization differences between Ebitengine and miniaudio. Ebitengine allows calling play/pause from anywhere (I think) and automatically pauses when it gets an EOF. miniaudio keeps reading until you stop the device, which deadlocks if you do it from within the read callback.
Author
Owner
Copy link

I added support for selecting a particular miniaudio backend, but I'm not sure that it was worthwhile.

The "null" and OSS backends look like they aren't compiled in.

The ALSA backend makes nup-headless consume close to 100% CPU and frequently logs EPIPE (write) messages.

The JACK backend doesn't work on my Chromebook; I think I don't have the server (or PipeWire emulation) running.

So I guess the PulseAudio backend is my only option, at least in my dev environment. I don't think there's a native PipeWire backend (or if one would be more efficient).

I added support for selecting a particular miniaudio backend, but I'm not sure that it was worthwhile. The "null" and OSS backends look like they aren't compiled in. The ALSA backend makes `nup-headless` consume close to 100% CPU and frequently logs `EPIPE (write)` messages. The JACK backend doesn't work on my Chromebook; I think I don't have the server (or PipeWire emulation) running. So I guess the PulseAudio backend is my only option, at least in my dev environment. I don't think there's a native PipeWire backend (or if one would be more efficient).
Author
Owner
Copy link

I was too cocky. This is a racy mess and I need to replace gaplessPlayer with something that's designed with miniaudio's API in mind.

I was too cocky. This is a racy mess and I need to replace `gaplessPlayer` with something that's designed with miniaudio's API in mind.
Sign in to join this conversation.
No Branch/Tag specified
main
headless
picture
datastore
No results found.
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
derat/nup#139
Reference in a new issue
derat/nup
No description provided.
Delete branch "%!s()"

Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?