diff --git a/clients/audio/backend.cpp b/clients/audio/backend.cpp index 59d8de7..09f3b4e 100644 --- a/clients/audio/backend.cpp +++ b/clients/audio/backend.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -249,6 +250,16 @@ const pw_metadata_events Backend::metadataEvents = [] { return events; }(); +// The last backend's loop and context, for the next one on the thread. A +// context's modules allocate on load what unloading them never frees, so a +// page that comes and goes would otherwise leak with each context. +struct SpareContext +{ + pw_loop* loop = nullptr; + pw_context* context = nullptr; +}; +static thread_local SpareContext s_spare; + std::shared_ptr Backend::shared() { static std::weak_ptr instance; @@ -263,7 +274,9 @@ std::shared_ptr Backend::shared() Backend::Backend() : m_eventLoop(base::EventLoop::current()) { - pw_init(nullptr, nullptr); + // Once per process, never undone: the spare context outlives every backend. + static std::once_flag initialized; + std::call_once(initialized, [] { pw_init(nullptr, nullptr); }); m_retry.setSingleShot(true); m_retry.onTimeout = [this] { connectToDaemon(); }; m_failure.setSingleShot(true); @@ -278,8 +291,11 @@ Backend::~Backend() m_retry.stop(); m_failure.stop(); disconnectFromDaemon(); + if (m_context && !s_spare.context) { + s_spare = {std::exchange(m_loop, nullptr), std::exchange(m_context, nullptr)}; + return; + } releaseContext(); - pw_deinit(); } std::vector> Backend::objects() const @@ -313,6 +329,10 @@ void Backend::connectToDaemon() if (m_core) return; m_failurePending = false; + if (!m_loop && s_spare.context) { + m_loop = std::exchange(s_spare.loop, nullptr); + m_context = std::exchange(s_spare.context, nullptr); + } // A context loads its modules and configuration when created; kept across // attempts, a retry while the daemon is away costs only the connect. if (!m_loop) { diff --git a/clients/audio/tests/audio_test.cpp b/clients/audio/tests/audio_test.cpp index 4d37ce0..e9bdb6f 100644 --- a/clients/audio/tests/audio_test.cpp +++ b/clients/audio/tests/audio_test.cpp @@ -530,3 +530,18 @@ TEST_F(AudioTest, Reconnect) sink->setVolume(0.3); ASSERT_TRUE(until([&] { return near(sink->volume(), 0.3); })); } + +// As a page that shows sound opens and closes: each backend reads the graph +// and is the last one to go. +TEST_F(AudioTest, LastBackendComesAndGoes) +{ + drivers.clear(); + backend.reset(); + for (int i = 0; i < 5; ++i) { + backend = Backend::shared(); + ASSERT_TRUE(waitFor(*loop, [] { return backend->isConnected() && find("test-output-a"); }, 10s)); + backend.reset(); + } + backend = Backend::shared(); + ASSERT_TRUE(waitFor(*loop, [] { return backend->isConnected() && find("test-output-a"); }, 10s)); +}