diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /media/libcubeb/test/test_deadlock.cpp | |
parent | Initial commit. (diff) | |
download | firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.tar.xz firefox-esr-36d22d82aa202bb199967e9512281e9a53db42c9.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
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.cpp | 262 |
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, + ¶ms, 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 |