blob: 458572d0dd2bb82ba98f58650b9cacaec485a023 [file] [log] [blame]
// Copyright 2016 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <lib/fit/defer.h>
#include <lib/zircon-internal/thread_annotations.h>
#include <lib/zx/clock.h>
#include <lib/zx/event.h>
#include <stddef.h>
#include <threads.h>
#include <zircon/syscalls.h>
#include <zircon/time.h>
#include <zircon/types.h>
#include <zircon/utc.h>
#include <array>
#include <cinttypes>
#include <cstdio>
#include <cstdlib>
#include <zxtest/zxtest.h>
namespace {
constexpr int* kNoReturnValue = nullptr;
class TestThread {
public:
TestThread(size_t number_tries, zx::duration delay, mtx_t* lock)
: number_tries_(number_tries), delay_(delay), lock_(lock) {
ZX_ASSERT(lock != nullptr);
}
void Start() {
int status = thrd_create(
&tid_,
[](void* ctx) -> int {
return reinterpret_cast<TestThread*>(ctx)->RepeatMutexLockAndUnlock();
},
this);
started_ = status == thrd_success;
ASSERT_TRUE(started_);
}
void Join() {
if (started_) {
thrd_join(tid_, kNoReturnValue);
started_ = false;
}
}
int RepeatMutexLockAndUnlock() {
for (uint64_t tries = 0; tries < number_tries_; tries++) {
mtx_lock(lock_);
zx::nanosleep(zx::deadline_after(delay_));
mtx_unlock(lock_);
}
return 0;
}
private:
uint64_t number_tries_;
zx::duration delay_;
mtx_t* lock_;
thrd_t tid_;
bool started_ = false;
};
TEST(C11MutexTest, MultiThreadedContention) {
// These tests all conditionally acquire the lock, by design. The
// thread safety analysis is not up to this, so disable it.
mtx_t lock;
ASSERT_EQ(thrd_success, mtx_init(&lock, mtx_timed));
std::array threads = {
TestThread{300, zx::usec(100), &lock},
TestThread{150, zx::usec(200), &lock},
TestThread{100, zx::usec(300), &lock},
};
for (auto& thread : threads) {
thread.Start();
ASSERT_NO_FAILURES();
}
for (auto& thread : threads) {
thread.Join();
ASSERT_NO_FAILURES();
}
}
// TODO(fxbug.dev/39408) Remove all TA_NO_THREAD_SAFETY_ANALYSIS annotations when we can
TEST(C11MutexTest, TryMutexMultiThreadedContention) TA_NO_THREAD_SAFETY_ANALYSIS {
struct MutexThreadArgs {
std::atomic<int> lock_acquired;
std::atomic<int> lock_released = thrd_error;
mtx_t* lock;
} args;
// This test conditionally acquires the lock, by design. The
// thread safety analysis is not up to this, so disable it.
auto TryGrabLock = [](void* arg) TA_NO_THREAD_SAFETY_ANALYSIS -> int {
MutexThreadArgs* args = static_cast<MutexThreadArgs*>(arg);
args->lock_acquired = mtx_trylock(args->lock);
if (args->lock_acquired == thrd_success) {
args->lock_released = mtx_unlock(args->lock);
}
return 0;
};
mtx_t lock;
bool lock_held = false;
ASSERT_EQ(thrd_success, mtx_init(&lock, mtx_plain));
args.lock = &lock;
auto cleanup_mutex = fit::defer([&]() TA_NO_THREAD_SAFETY_ANALYSIS {
if (lock_held) {
mtx_unlock(&lock);
}
mtx_destroy(&lock);
});
thrd_t thread_id;
ASSERT_EQ(thrd_success, mtx_lock(&lock));
lock_held = true;
ASSERT_EQ(thrd_success, thrd_create(&thread_id, TryGrabLock, &args));
ASSERT_EQ(thrd_success, thrd_join(thread_id, kNoReturnValue));
ASSERT_EQ(thrd_success, mtx_unlock(&lock));
lock_held = false;
ASSERT_EQ(thrd_busy, args.lock_acquired);
args.lock_acquired = thrd_error;
args.lock_released = thrd_error;
ASSERT_EQ(thrd_success, thrd_create(&thread_id, TryGrabLock, &args));
ASSERT_EQ(thrd_success, thrd_join(thread_id, kNoReturnValue));
ASSERT_EQ(thrd_success, args.lock_acquired);
ASSERT_EQ(thrd_success, args.lock_released);
}
TEST(C11MutexTest, InitalizeLocalMutex) {
mtx_t auto_mutex;
ASSERT_EQ(thrd_success, mtx_init(&auto_mutex, mtx_timed));
mtx_destroy(&auto_mutex);
}
TEST(C11MutexTest, StaticInitalizerSameBytesAsAuto) {
static mtx_t static_mutex = MTX_INIT;
mtx_t auto_mutex;
memset(&auto_mutex, 0xae, sizeof(auto_mutex));
mtx_init(&auto_mutex, mtx_plain);
auto cleanup_mutex = fit::defer([&]() { mtx_destroy(&auto_mutex); });
EXPECT_BYTES_EQ(reinterpret_cast<uint8_t*>(&static_mutex),
reinterpret_cast<uint8_t*>(&auto_mutex), sizeof(mtx_t),
"MTX_INIT and mtx_init differ!");
}
TEST(C11MutexTest, TimeoutElapsed) {
struct ThreadTimeoutArgs {
mtx_t lock;
zx::event start_event;
zx::event done_event;
std::atomic<bool> mtx_lock_result = false;
std::atomic<bool> mtx_unlock_result = false;
std::atomic<zx_status_t> signal_result = ZX_ERR_INTERNAL;
std::atomic<zx_status_t> wait_one_result = ZX_ERR_INTERNAL;
};
// Create and start a local kernel clock. This will be used as the test
// UTC clock.
zx::clock local_utc_clock;
ASSERT_OK(zx::clock::create(ZX_CLOCK_OPT_MONOTONIC, nullptr, &local_utc_clock));
ASSERT_OK(local_utc_clock.update(zx::clock::update_args().set_value(zx::time(0))));
// Install the local clock as the process-global UTC clock.
// NOTE: This will cause flakes if more than one test is doing this in the same process.
zx::clock prev_clock;
ASSERT_OK(zx_utc_reference_swap(local_utc_clock.release(), prev_clock.reset_and_get_address()));
auto reinstall_clock = fit::defer([&]() {
ASSERT_OK(zx_utc_reference_swap(prev_clock.release(), local_utc_clock.reset_and_get_address()));
});
auto TestTimeoutHelper = [](void* ctx) TA_NO_THREAD_SAFETY_ANALYSIS -> int {
ThreadTimeoutArgs* args = static_cast<ThreadTimeoutArgs*>(ctx);
if ((args->mtx_lock_result = mtx_lock(&args->lock)) != thrd_success) {
return -1;
}
// Inform the main thread that we have acquired the lock.
if ((args->signal_result = args->start_event.signal(0, ZX_EVENT_SIGNALED)) != ZX_OK) {
mtx_unlock(&args->lock);
return -1;
}
// Wait until the main thread has completed its test.
if ((args->wait_one_result = args->done_event.wait_one(ZX_EVENT_SIGNALED, zx::time::infinite(),
nullptr)) != ZX_OK) {
mtx_unlock(&args->lock);
return -1;
}
if ((args->mtx_unlock_result = mtx_unlock(&args->lock)) != thrd_success) {
return -1;
}
return 0;
};
constexpr zx::duration kRelativeDeadline = zx::msec(100);
ThreadTimeoutArgs args;
ASSERT_EQ(thrd_success, mtx_init(&args.lock, mtx_plain));
auto cleanup_mutex = fit::defer([&]() { mtx_destroy(&args.lock); });
ASSERT_OK(zx::event::create(0, &args.start_event));
ASSERT_OK(zx::event::create(0, &args.done_event));
thrd_t helper;
ASSERT_EQ(thrd_create(&helper, TestTimeoutHelper, &args), thrd_success);
cleanup_mutex.cancel();
auto cleanup_thread = fit::defer([&]() {
args.done_event.signal(0, ZX_EVENT_SIGNALED);
thrd_join(helper, kNoReturnValue);
mtx_destroy(&args.lock);
});
// Wait for the helper thread to acquire the lock.
ASSERT_OK(args.start_event.wait_one(ZX_EVENT_SIGNALED, zx::time::infinite(), nullptr));
zx::unowned_clock utc_clock(zx_utc_reference_get());
for (int i = 0; i < 5; ++i) {
zx_time_t now;
ASSERT_OK(utc_clock->read(&now));
struct timespec then = {
.tv_sec = now / ZX_SEC(1),
.tv_nsec = now % ZX_SEC(1),
};
then.tv_nsec += kRelativeDeadline.get();
if (then.tv_nsec > reinterpret_cast<long>(ZX_SEC(1))) {
then.tv_nsec -= ZX_SEC(1);
then.tv_sec += 1;
}
int rc = mtx_timedlock(&args.lock, &then);
ASSERT_EQ(rc, thrd_timedout, "wait should time out");
zx_time_t later;
ASSERT_OK(utc_clock->read(&later));
zx_duration_t elapsed = later - now;
// Since the wait is based on the UTC clock which can be adjusted while
// the wait proceeds, it is important to check that the mutex does not
// return early.
// TODO(fxbug.dev/34941): when the kernel UTC clock gets wired, the test framework should
// expose a way to do local modifications to the clock so we can properly
// test this behavior. Currently the UTC offset is global and mutating it here
// can create hard to diagnose flakes as seen with bug 34857.
EXPECT_GE(elapsed, kRelativeDeadline.get(), "wait returned early");
}
// Inform the helper thread that we are done.
ASSERT_OK(args.done_event.signal(0, ZX_EVENT_SIGNALED));
ASSERT_EQ(thrd_join(helper, kNoReturnValue), thrd_success);
cleanup_thread.cancel();
mtx_destroy(&args.lock);
ASSERT_EQ(args.mtx_lock_result, thrd_success);
ASSERT_OK(args.signal_result);
ASSERT_OK(args.wait_one_result);
ASSERT_EQ(args.mtx_unlock_result, thrd_success);
}
} // namespace