diff options
Diffstat (limited to 'src/tests')
-rw-r--r-- | src/tests/CMakeLists.txt | 2 | ||||
-rw-r--r-- | src/tests/common/fibers.cpp | 358 | ||||
-rw-r--r-- | src/tests/core/host_timing.cpp | 142 |
3 files changed, 502 insertions, 0 deletions
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index c7038b217..3f750b51c 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -1,12 +1,14 @@ add_executable(tests common/bit_field.cpp common/bit_utils.cpp + common/fibers.cpp common/multi_level_queue.cpp common/param_package.cpp common/ring_buffer.cpp core/arm/arm_test_common.cpp core/arm/arm_test_common.h core/core_timing.cpp + core/host_timing.cpp tests.cpp ) diff --git a/src/tests/common/fibers.cpp b/src/tests/common/fibers.cpp new file mode 100644 index 000000000..12536b6d8 --- /dev/null +++ b/src/tests/common/fibers.cpp @@ -0,0 +1,358 @@ +// Copyright 2020 yuzu Emulator Project +// Licensed under GPLv2 or any later version +// Refer to the license.txt file included. + +#include <atomic> +#include <cstdlib> +#include <functional> +#include <memory> +#include <thread> +#include <unordered_map> +#include <vector> + +#include <catch2/catch.hpp> +#include <math.h> +#include "common/common_types.h" +#include "common/fiber.h" +#include "common/spin_lock.h" + +namespace Common { + +class TestControl1 { +public: + TestControl1() = default; + + void DoWork(); + + void ExecuteThread(u32 id); + + std::unordered_map<std::thread::id, u32> ids; + std::vector<std::shared_ptr<Common::Fiber>> thread_fibers; + std::vector<std::shared_ptr<Common::Fiber>> work_fibers; + std::vector<u32> items; + std::vector<u32> results; +}; + +static void WorkControl1(void* control) { + auto* test_control = static_cast<TestControl1*>(control); + test_control->DoWork(); +} + +void TestControl1::DoWork() { + std::thread::id this_id = std::this_thread::get_id(); + u32 id = ids[this_id]; + u32 value = items[id]; + for (u32 i = 0; i < id; i++) { + value++; + } + results[id] = value; + Fiber::YieldTo(work_fibers[id], thread_fibers[id]); +} + +void TestControl1::ExecuteThread(u32 id) { + std::thread::id this_id = std::this_thread::get_id(); + ids[this_id] = id; + auto thread_fiber = Fiber::ThreadToFiber(); + thread_fibers[id] = thread_fiber; + work_fibers[id] = std::make_shared<Fiber>(std::function<void(void*)>{WorkControl1}, this); + items[id] = rand() % 256; + Fiber::YieldTo(thread_fibers[id], work_fibers[id]); + thread_fibers[id]->Exit(); +} + +static void ThreadStart1(u32 id, TestControl1& test_control) { + test_control.ExecuteThread(id); +} + +/** This test checks for fiber setup configuration and validates that fibers are + * doing all the work required. + */ +TEST_CASE("Fibers::Setup", "[common]") { + constexpr u32 num_threads = 7; + TestControl1 test_control{}; + test_control.thread_fibers.resize(num_threads); + test_control.work_fibers.resize(num_threads); + test_control.items.resize(num_threads, 0); + test_control.results.resize(num_threads, 0); + std::vector<std::thread> threads; + for (u32 i = 0; i < num_threads; i++) { + threads.emplace_back(ThreadStart1, i, std::ref(test_control)); + } + for (u32 i = 0; i < num_threads; i++) { + threads[i].join(); + } + for (u32 i = 0; i < num_threads; i++) { + REQUIRE(test_control.items[i] + i == test_control.results[i]); + } +} + +class TestControl2 { +public: + TestControl2() = default; + + void DoWork1() { + trap2 = false; + while (trap.load()) + ; + for (u32 i = 0; i < 12000; i++) { + value1 += i; + } + Fiber::YieldTo(fiber1, fiber3); + std::thread::id this_id = std::this_thread::get_id(); + u32 id = ids[this_id]; + assert1 = id == 1; + value2 += 5000; + Fiber::YieldTo(fiber1, thread_fibers[id]); + } + + void DoWork2() { + while (trap2.load()) + ; + value2 = 2000; + trap = false; + Fiber::YieldTo(fiber2, fiber1); + assert3 = false; + } + + void DoWork3() { + std::thread::id this_id = std::this_thread::get_id(); + u32 id = ids[this_id]; + assert2 = id == 0; + value1 += 1000; + Fiber::YieldTo(fiber3, thread_fibers[id]); + } + + void ExecuteThread(u32 id); + + void CallFiber1() { + std::thread::id this_id = std::this_thread::get_id(); + u32 id = ids[this_id]; + Fiber::YieldTo(thread_fibers[id], fiber1); + } + + void CallFiber2() { + std::thread::id this_id = std::this_thread::get_id(); + u32 id = ids[this_id]; + Fiber::YieldTo(thread_fibers[id], fiber2); + } + + void Exit(); + + bool assert1{}; + bool assert2{}; + bool assert3{true}; + u32 value1{}; + u32 value2{}; + std::atomic<bool> trap{true}; + std::atomic<bool> trap2{true}; + std::unordered_map<std::thread::id, u32> ids; + std::vector<std::shared_ptr<Common::Fiber>> thread_fibers; + std::shared_ptr<Common::Fiber> fiber1; + std::shared_ptr<Common::Fiber> fiber2; + std::shared_ptr<Common::Fiber> fiber3; +}; + +static void WorkControl2_1(void* control) { + auto* test_control = static_cast<TestControl2*>(control); + test_control->DoWork1(); +} + +static void WorkControl2_2(void* control) { + auto* test_control = static_cast<TestControl2*>(control); + test_control->DoWork2(); +} + +static void WorkControl2_3(void* control) { + auto* test_control = static_cast<TestControl2*>(control); + test_control->DoWork3(); +} + +void TestControl2::ExecuteThread(u32 id) { + std::thread::id this_id = std::this_thread::get_id(); + ids[this_id] = id; + auto thread_fiber = Fiber::ThreadToFiber(); + thread_fibers[id] = thread_fiber; +} + +void TestControl2::Exit() { + std::thread::id this_id = std::this_thread::get_id(); + u32 id = ids[this_id]; + thread_fibers[id]->Exit(); +} + +static void ThreadStart2_1(u32 id, TestControl2& test_control) { + test_control.ExecuteThread(id); + test_control.CallFiber1(); + test_control.Exit(); +} + +static void ThreadStart2_2(u32 id, TestControl2& test_control) { + test_control.ExecuteThread(id); + test_control.CallFiber2(); + test_control.Exit(); +} + +/** This test checks for fiber thread exchange configuration and validates that fibers are + * that a fiber has been succesfully transfered from one thread to another and that the TLS + * region of the thread is kept while changing fibers. + */ +TEST_CASE("Fibers::InterExchange", "[common]") { + TestControl2 test_control{}; + test_control.thread_fibers.resize(2); + test_control.fiber1 = + std::make_shared<Fiber>(std::function<void(void*)>{WorkControl2_1}, &test_control); + test_control.fiber2 = + std::make_shared<Fiber>(std::function<void(void*)>{WorkControl2_2}, &test_control); + test_control.fiber3 = + std::make_shared<Fiber>(std::function<void(void*)>{WorkControl2_3}, &test_control); + std::thread thread1(ThreadStart2_1, 0, std::ref(test_control)); + std::thread thread2(ThreadStart2_2, 1, std::ref(test_control)); + thread1.join(); + thread2.join(); + REQUIRE(test_control.assert1); + REQUIRE(test_control.assert2); + REQUIRE(test_control.assert3); + REQUIRE(test_control.value2 == 7000); + u32 cal_value = 0; + for (u32 i = 0; i < 12000; i++) { + cal_value += i; + } + cal_value += 1000; + REQUIRE(test_control.value1 == cal_value); +} + +class TestControl3 { +public: + TestControl3() = default; + + void DoWork1() { + value1 += 1; + Fiber::YieldTo(fiber1, fiber2); + std::thread::id this_id = std::this_thread::get_id(); + u32 id = ids[this_id]; + value3 += 1; + Fiber::YieldTo(fiber1, thread_fibers[id]); + } + + void DoWork2() { + value2 += 1; + std::thread::id this_id = std::this_thread::get_id(); + u32 id = ids[this_id]; + Fiber::YieldTo(fiber2, thread_fibers[id]); + } + + void ExecuteThread(u32 id); + + void CallFiber1() { + std::thread::id this_id = std::this_thread::get_id(); + u32 id = ids[this_id]; + Fiber::YieldTo(thread_fibers[id], fiber1); + } + + void Exit(); + + u32 value1{}; + u32 value2{}; + u32 value3{}; + std::unordered_map<std::thread::id, u32> ids; + std::vector<std::shared_ptr<Common::Fiber>> thread_fibers; + std::shared_ptr<Common::Fiber> fiber1; + std::shared_ptr<Common::Fiber> fiber2; +}; + +static void WorkControl3_1(void* control) { + auto* test_control = static_cast<TestControl3*>(control); + test_control->DoWork1(); +} + +static void WorkControl3_2(void* control) { + auto* test_control = static_cast<TestControl3*>(control); + test_control->DoWork2(); +} + +void TestControl3::ExecuteThread(u32 id) { + std::thread::id this_id = std::this_thread::get_id(); + ids[this_id] = id; + auto thread_fiber = Fiber::ThreadToFiber(); + thread_fibers[id] = thread_fiber; +} + +void TestControl3::Exit() { + std::thread::id this_id = std::this_thread::get_id(); + u32 id = ids[this_id]; + thread_fibers[id]->Exit(); +} + +static void ThreadStart3(u32 id, TestControl3& test_control) { + test_control.ExecuteThread(id); + test_control.CallFiber1(); + test_control.Exit(); +} + +/** This test checks for one two threads racing for starting the same fiber. + * It checks execution occured in an ordered manner and by no time there were + * two contexts at the same time. + */ +TEST_CASE("Fibers::StartRace", "[common]") { + TestControl3 test_control{}; + test_control.thread_fibers.resize(2); + test_control.fiber1 = + std::make_shared<Fiber>(std::function<void(void*)>{WorkControl3_1}, &test_control); + test_control.fiber2 = + std::make_shared<Fiber>(std::function<void(void*)>{WorkControl3_2}, &test_control); + std::thread thread1(ThreadStart3, 0, std::ref(test_control)); + std::thread thread2(ThreadStart3, 1, std::ref(test_control)); + thread1.join(); + thread2.join(); + REQUIRE(test_control.value1 == 1); + REQUIRE(test_control.value2 == 1); + REQUIRE(test_control.value3 == 1); +} + +class TestControl4; + +static void WorkControl4(void* control); + +class TestControl4 { +public: + TestControl4() { + fiber1 = std::make_shared<Fiber>(std::function<void(void*)>{WorkControl4}, this); + goal_reached = false; + rewinded = false; + } + + void Execute() { + thread_fiber = Fiber::ThreadToFiber(); + Fiber::YieldTo(thread_fiber, fiber1); + thread_fiber->Exit(); + } + + void DoWork() { + fiber1->SetRewindPoint(std::function<void(void*)>{WorkControl4}, this); + if (rewinded) { + goal_reached = true; + Fiber::YieldTo(fiber1, thread_fiber); + } + rewinded = true; + fiber1->Rewind(); + } + + std::shared_ptr<Common::Fiber> fiber1; + std::shared_ptr<Common::Fiber> thread_fiber; + bool goal_reached; + bool rewinded; +}; + +static void WorkControl4(void* control) { + auto* test_control = static_cast<TestControl4*>(control); + test_control->DoWork(); +} + +TEST_CASE("Fibers::Rewind", "[common]") { + TestControl4 test_control{}; + test_control.Execute(); + REQUIRE(test_control.goal_reached); + REQUIRE(test_control.rewinded); +} + +} // namespace Common diff --git a/src/tests/core/host_timing.cpp b/src/tests/core/host_timing.cpp new file mode 100644 index 000000000..556254098 --- /dev/null +++ b/src/tests/core/host_timing.cpp @@ -0,0 +1,142 @@ +// Copyright 2016 Dolphin Emulator Project / 2017 Dolphin Emulator Project +// Licensed under GPLv2+ +// Refer to the license.txt file included. + +#include <catch2/catch.hpp> + +#include <array> +#include <bitset> +#include <cstdlib> +#include <memory> +#include <string> + +#include "common/file_util.h" +#include "core/core.h" +#include "core/host_timing.h" + +// Numbers are chosen randomly to make sure the correct one is given. +static constexpr std::array<u64, 5> CB_IDS{{42, 144, 93, 1026, UINT64_C(0xFFFF7FFFF7FFFF)}}; +static constexpr int MAX_SLICE_LENGTH = 10000; // Copied from CoreTiming internals +static constexpr std::array<u64, 5> calls_order{{2, 0, 1, 4, 3}}; +static std::array<s64, 5> delays{}; + +static std::bitset<CB_IDS.size()> callbacks_ran_flags; +static u64 expected_callback = 0; + +template <unsigned int IDX> +void HostCallbackTemplate(u64 userdata, s64 nanoseconds_late) { + static_assert(IDX < CB_IDS.size(), "IDX out of range"); + callbacks_ran_flags.set(IDX); + REQUIRE(CB_IDS[IDX] == userdata); + REQUIRE(CB_IDS[IDX] == CB_IDS[calls_order[expected_callback]]); + delays[IDX] = nanoseconds_late; + ++expected_callback; +} + +struct ScopeInit final { + ScopeInit() { + core_timing.Initialize(); + } + ~ScopeInit() { + core_timing.Shutdown(); + } + + Core::HostTiming::CoreTiming core_timing; +}; + +#pragma optimize("", off) + +static u64 TestTimerSpeed(Core::HostTiming::CoreTiming& core_timing) { + u64 start = core_timing.GetGlobalTimeNs().count(); + u64 placebo = 0; + for (std::size_t i = 0; i < 1000; i++) { + placebo += core_timing.GetGlobalTimeNs().count(); + } + u64 end = core_timing.GetGlobalTimeNs().count(); + return (end - start); +} + +#pragma optimize("", on) + +TEST_CASE("HostTiming[BasicOrder]", "[core]") { + ScopeInit guard; + auto& core_timing = guard.core_timing; + std::vector<std::shared_ptr<Core::HostTiming::EventType>> events{ + Core::HostTiming::CreateEvent("callbackA", HostCallbackTemplate<0>), + Core::HostTiming::CreateEvent("callbackB", HostCallbackTemplate<1>), + Core::HostTiming::CreateEvent("callbackC", HostCallbackTemplate<2>), + Core::HostTiming::CreateEvent("callbackD", HostCallbackTemplate<3>), + Core::HostTiming::CreateEvent("callbackE", HostCallbackTemplate<4>), + }; + + expected_callback = 0; + + core_timing.SyncPause(true); + + u64 one_micro = 1000U; + for (std::size_t i = 0; i < events.size(); i++) { + u64 order = calls_order[i]; + core_timing.ScheduleEvent(i * one_micro + 100U, events[order], CB_IDS[order]); + } + /// test pause + REQUIRE(callbacks_ran_flags.none()); + + core_timing.Pause(false); // No need to sync + + while (core_timing.HasPendingEvents()) + ; + + REQUIRE(callbacks_ran_flags.all()); + + for (std::size_t i = 0; i < delays.size(); i++) { + const double delay = static_cast<double>(delays[i]); + const double micro = delay / 1000.0f; + const double mili = micro / 1000.0f; + printf("HostTimer Pausing Delay[%zu]: %.3f %.6f\n", i, micro, mili); + } +} + +TEST_CASE("HostTiming[BasicOrderNoPausing]", "[core]") { + ScopeInit guard; + auto& core_timing = guard.core_timing; + std::vector<std::shared_ptr<Core::HostTiming::EventType>> events{ + Core::HostTiming::CreateEvent("callbackA", HostCallbackTemplate<0>), + Core::HostTiming::CreateEvent("callbackB", HostCallbackTemplate<1>), + Core::HostTiming::CreateEvent("callbackC", HostCallbackTemplate<2>), + Core::HostTiming::CreateEvent("callbackD", HostCallbackTemplate<3>), + Core::HostTiming::CreateEvent("callbackE", HostCallbackTemplate<4>), + }; + + core_timing.SyncPause(true); + core_timing.SyncPause(false); + + expected_callback = 0; + + u64 start = core_timing.GetGlobalTimeNs().count(); + u64 one_micro = 1000U; + for (std::size_t i = 0; i < events.size(); i++) { + u64 order = calls_order[i]; + core_timing.ScheduleEvent(i * one_micro + 100U, events[order], CB_IDS[order]); + } + u64 end = core_timing.GetGlobalTimeNs().count(); + const double scheduling_time = static_cast<double>(end - start); + const double timer_time = static_cast<double>(TestTimerSpeed(core_timing)); + + while (core_timing.HasPendingEvents()) + ; + + REQUIRE(callbacks_ran_flags.all()); + + for (std::size_t i = 0; i < delays.size(); i++) { + const double delay = static_cast<double>(delays[i]); + const double micro = delay / 1000.0f; + const double mili = micro / 1000.0f; + printf("HostTimer No Pausing Delay[%zu]: %.3f %.6f\n", i, micro, mili); + } + + const double micro = scheduling_time / 1000.0f; + const double mili = micro / 1000.0f; + printf("HostTimer No Pausing Scheduling Time: %.3f %.6f\n", micro, mili); + printf("HostTimer No Pausing Timer Time: %.3f %.6f\n", timer_time / 1000.f, + timer_time / 1000000.f); +} |