summaryrefslogtreecommitdiffstats
path: root/media/libcubeb/test/test_deadlock.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /media/libcubeb/test/test_deadlock.cpp
parentInitial commit. (diff)
downloadfirefox-43a97878ce14b72f0981164f87f2e35e14151312.tar.xz
firefox-43a97878ce14b72f0981164f87f2e35e14151312.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'media/libcubeb/test/test_deadlock.cpp')
-rw-r--r--media/libcubeb/test/test_deadlock.cpp262
1 files changed, 262 insertions, 0 deletions
diff --git a/media/libcubeb/test/test_deadlock.cpp b/media/libcubeb/test/test_deadlock.cpp
new file mode 100644
index 0000000000..373ba6ad7e
--- /dev/null
+++ b/media/libcubeb/test/test_deadlock.cpp
@@ -0,0 +1,262 @@
+/*
+ * Copyright © 2017 Mozilla Foundation
+ *
+ * This program is made available under an ISC-style license. See the
+ * accompanying file LICENSE for details.
+ *
+ *
+ * Purpose
+ * =============================================================================
+ * In CoreAudio, the data callback will holds a mutex shared with AudioUnit
+ * (mutex_AU). Thus, if the callback request another mutex M held by the another
+ * function, without releasing mutex_AU, then it will cause a deadlock when the
+ * another function, which holds the mutex M, request to use AudioUnit.
+ *
+ * The following figure illustrates the deadlock in bug 1337805:
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1337805
+ * (The detail analysis can be found on bug 1350511:
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1350511)
+ *
+ * holds
+ * data_callback <---------- mutext_AudioUnit(mutex_AU)
+ * | ^
+ * | |
+ * | request | request
+ * | |
+ * v holds |
+ * mutex_cubeb ------------> get_channel_layout
+ *
+ * In this example, the "audiounit_get_channel_layout" in f4edfb8:
+ * https://github.com/kinetiknz/cubeb/blob/f4edfb8eea920887713325e44773f3a2d959860c/src/cubeb_audiounit.cpp#L2725
+ * requests the mutex_AU to create an AudioUnit, when it holds a mutex for cubeb
+ * context. Meanwhile, the data callback who holds the mutex_AU requests the
+ * mutex for cubeb context. As a result, it causes a deadlock.
+ *
+ * The problem is solve by pull 236: https://github.com/kinetiknz/cubeb/pull/236
+ * We store the latest channel layout and return it when there is an active
+ * AudioUnit, otherwise, we will create an AudioUnit to get it.
+ *
+ * Although the problem is solved, to prevent it happens again, we add the test
+ * here in case someone without such knowledge misuses the AudioUnit in
+ * get_channel_layout. Moreover, it's a good way to record the known issues
+ * to warn other developers.
+ */
+
+#include "gtest/gtest.h"
+//#define ENABLE_NORMAL_LOG
+//#define ENABLE_VERBOSE_LOG
+#include "common.h" // for layout_infos
+#include "cubeb/cubeb.h" // for cubeb utils
+#include "cubeb_utils.h" // for owned_critical_section, auto_lock
+#include <iostream> // for fprintf
+#include <pthread.h> // for pthread
+#include <signal.h> // for signal
+#include <stdexcept> // for std::logic_error
+#include <string> // for std::string
+#include <unistd.h> // for sleep, usleep
+#include <atomic> // for std::atomic
+
+// The signal alias for calling our thread killer.
+#define CALL_THREAD_KILLER SIGUSR1
+
+// This indicator will become true when our pending task thread is killed by
+// ourselves.
+bool killed = false;
+
+// This indicator will become true when the assigned task is done.
+std::atomic<bool> task_done{ false };
+
+// Indicating the data callback is fired or not.
+bool called = false;
+
+// Toggle to true when running data callback. Before data callback gets
+// the mutex for cubeb context, it toggles back to false.
+// The task to get channel layout should be executed when this is true.
+std::atomic<bool> callbacking_before_getting_context{ false };
+
+owned_critical_section context_mutex;
+cubeb * context = nullptr;
+
+cubeb * get_cubeb_context_unlocked()
+{
+ if (context) {
+ return context;
+ }
+
+ int r = CUBEB_OK;
+ r = common_init(&context, "Cubeb deadlock test");
+ if (r != CUBEB_OK) {
+ context = nullptr;
+ }
+
+ return context;
+}
+
+cubeb * get_cubeb_context()
+{
+ auto_lock lock(context_mutex);
+ return get_cubeb_context_unlocked();
+}
+
+void state_cb_audio(cubeb_stream * /*stream*/, void * /*user*/, cubeb_state /*state*/)
+{
+}
+
+// Fired by coreaudio's rendering mechanism. It holds a mutex shared with the
+// current used AudioUnit.
+template<typename T>
+long data_cb(cubeb_stream * /*stream*/, void * /*user*/,
+ const void * /*inputbuffer*/, void * outputbuffer, long nframes)
+{
+ called = true;
+
+ uint64_t tid; // Current thread id.
+ pthread_threadid_np(NULL, &tid);
+ fprintf(stderr, "Audio output is on thread %llu\n", tid);
+
+ if (!task_done) {
+ callbacking_before_getting_context = true;
+ fprintf(stderr, "[%llu] time to switch thread\n", tid);
+ // Force to switch threads by sleeping 10 ms. Notice that anything over
+ // 10ms would create a glitch. It's intended here for test, so the delay
+ // is ok.
+ usleep(10000);
+ callbacking_before_getting_context = false;
+ }
+
+ fprintf(stderr, "[%llu] try getting backend id ...\n", tid);
+
+ // Try requesting mutex for context by get_cubeb_context()
+ // when holding a mutex for AudioUnit.
+ char const * backend_id = cubeb_get_backend_id(get_cubeb_context());
+ fprintf(stderr, "[%llu] callback on %s\n", tid, backend_id);
+
+ // Mute the output (or get deaf)
+ memset(outputbuffer, 0, nframes * 2 * sizeof(float));
+ return nframes;
+}
+
+// Called by wait_to_get_layout, which is run out of main thread.
+void get_preferred_channel_layout()
+{
+ auto_lock lock(context_mutex);
+ cubeb * context = get_cubeb_context_unlocked();
+ ASSERT_TRUE(!!context);
+
+ // We will cause a deadlock if cubeb_get_preferred_channel_layout requests
+ // mutex for AudioUnit when it holds mutex for context.
+ cubeb_channel_layout layout;
+ int r = cubeb_get_preferred_channel_layout(context, &layout);
+ ASSERT_EQ(r == CUBEB_OK, layout != CUBEB_LAYOUT_UNDEFINED);
+ fprintf(stderr, "layout is %s\n", layout_infos[layout].name);
+}
+
+void * wait_to_get_layout(void *)
+{
+ uint64_t tid; // Current thread id.
+ pthread_threadid_np(NULL, &tid);
+
+ while(!callbacking_before_getting_context) {
+ fprintf(stderr, "[%llu] waiting for data callback ...\n", tid);
+ usleep(1000); // Force to switch threads by sleeping 1 ms.
+ }
+
+ fprintf(stderr, "[%llu] try getting channel layout ...\n", tid);
+ get_preferred_channel_layout(); // Deadlock checkpoint.
+ task_done = true;
+
+ return NULL;
+}
+
+void * watchdog(void * s)
+{
+ uint64_t tid; // Current thread id.
+ pthread_threadid_np(NULL, &tid);
+
+ pthread_t subject = *((pthread_t *) s);
+ uint64_t stid; // task thread id.
+ pthread_threadid_np(subject, &stid);
+
+ unsigned int sec = 2;
+ fprintf(stderr, "[%llu] sleep %d seconds before checking task for thread %llu\n", tid, sec, stid);
+ sleep(sec); // Force to switch threads.
+
+ fprintf(stderr, "[%llu] check task for thread %llu now\n", tid, stid);
+ if (!task_done) {
+ fprintf(stderr, "[%llu] kill the task thread %llu\n", tid, stid);
+ pthread_kill(subject, CALL_THREAD_KILLER);
+ pthread_detach(subject);
+ // pthread_kill doesn't release the mutex held by the killed thread,
+ // so we need to unlock it manually.
+ context_mutex.unlock();
+ }
+ fprintf(stderr, "[%llu] the assigned task for thread %llu is %sdone\n", tid, stid, (task_done) ? "" : "not ");
+
+ return NULL;
+}
+
+void thread_killer(int signal)
+{
+ ASSERT_EQ(signal, CALL_THREAD_KILLER);
+ fprintf(stderr, "task thread is killed!\n");
+ killed = true;
+}
+
+TEST(cubeb, run_deadlock_test)
+{
+#if !defined(__APPLE__)
+ FAIL() << "Deadlock test is only for OSX now";
+#endif
+
+ cubeb * ctx = get_cubeb_context();
+ ASSERT_TRUE(!!ctx);
+
+ std::unique_ptr<cubeb, decltype(&cubeb_destroy)>
+ cleanup_cubeb_at_exit(ctx, cubeb_destroy);
+
+ cubeb_stream_params params;
+ params.format = CUBEB_SAMPLE_FLOAT32NE;
+ params.rate = 44100;
+ params.channels = 2;
+ params.layout = CUBEB_LAYOUT_STEREO;
+ params.prefs = CUBEB_STREAM_PREF_NONE;
+
+ cubeb_stream * stream = NULL;
+ int r = cubeb_stream_init(ctx, &stream, "test deadlock", NULL, NULL, NULL,
+ &params, 512, &data_cb<float>, state_cb_audio, NULL);
+ ASSERT_EQ(r, CUBEB_OK);
+
+ std::unique_ptr<cubeb_stream, decltype(&cubeb_stream_destroy)>
+ cleanup_stream_at_exit(stream, cubeb_stream_destroy);
+
+ // Install signal handler.
+ signal(CALL_THREAD_KILLER, thread_killer);
+
+ pthread_t subject, detector;
+ pthread_create(&subject, NULL, wait_to_get_layout, NULL);
+ pthread_create(&detector, NULL, watchdog, (void *) &subject);
+
+ uint64_t stid, dtid;
+ pthread_threadid_np(subject, &stid);
+ pthread_threadid_np(detector, &dtid);
+ fprintf(stderr, "task thread %llu, monitor thread %llu are created\n", stid, dtid);
+
+ cubeb_stream_start(stream);
+
+ pthread_join(subject, NULL);
+ pthread_join(detector, NULL);
+
+ ASSERT_TRUE(called);
+
+ fprintf(stderr, "\n%sDeadlock detected!\n", (called && !task_done.load()) ? "" : "No ");
+
+ // Check the task is killed by ourselves if deadlock happends.
+ // Otherwise, thread_killer should not be triggered.
+ ASSERT_NE(task_done.load(), killed);
+
+ ASSERT_TRUE(task_done.load());
+
+ cubeb_stream_stop(stream);
+}
+
+#undef CALL_THREAD_KILLER