From e0f8f657197acb473ff2bb8dff84b7760f599413 Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 27 Oct 2026 22:56:47 -0400 Subject: [PATCH 1/2] A pool worker sleeps until work or its deadline wakes it, not a tick at a time Every pool thread on Windows ran sleepy_robin, whose wait was a Sleep(1) and whose wake did nothing: an idle worker woke every tick of the system timer to look for work, and picked work up, or came back from a fiber's timed wait, only at a tick. The viewer's window asks for 1 ms ticks, so each idle pool thread woke about a thousand times a second; Windows 11 gives a process whose window is hidden the whole 15.6 ms tick whatever it asked for. sleepy_robin now waits on an auto-reset event, which a fiber made ready from another thread sets, and where a fiber's timed wait sets a deadline, on a high resolution waitable timer, which the tick does not round. Short of either handle it uses round_robin's own wait and wake, so no path sleeps a tick. Only a timer that will not be set falls back to a wait the tick rounds, which a wake still ends at once. Measured in threadpool_test at Windows' default tick, before and after: an idle worker ran 758,090 cycles in a quarter second, now 0; a 2 ms timed wait on a worker overran by a median 13.4 ms, now 0.4 ms. rye/aldb has an event-based sleepy_robin of its own, whose deadlines go through millisecond WaitForSingleObject timeouts the tick rounds, and its own tests/threadpool_test.cpp: whichever lands second merges the two, and this scheduler is the one to keep. Co-Authored-By: Claude Opus 5.5 --- indra/llcommon/CMakeLists.txt | 1 + indra/llcommon/tests/threadpool_test.cpp | 141 +++++++++++++++++++++++ indra/llcommon/threadpool.cpp | 116 ++++++++++++++++--- 3 files changed, 240 insertions(+), 18 deletions(-) create mode 100644 indra/llcommon/tests/threadpool_test.cpp diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt index 603e414953..f1db872d08 100644 --- a/indra/llcommon/CMakeLists.txt +++ b/indra/llcommon/CMakeLists.txt @@ -368,6 +368,7 @@ if(AL_BUILD_TESTS) tests/lluri_test.cpp tests/lluuid_test.cpp tests/stringize_test.cpp + tests/threadpool_test.cpp tests/threadsafeschedule_test.cpp tests/tuple_test.cpp tests/workqueue_test.cpp diff --git a/indra/llcommon/tests/threadpool_test.cpp b/indra/llcommon/tests/threadpool_test.cpp new file mode 100644 index 0000000000..5d4653b434 --- /dev/null +++ b/indra/llcommon/tests/threadpool_test.cpp @@ -0,0 +1,141 @@ +/** + * @file threadpool_test.cpp + * @brief A pool's worker sleeps until work or a deadline wakes it. + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Alchemy Viewer Source Code + * Copyright (C) 2026, Rye + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "../threadpool.h" + +#if LL_WINDOWS +#include "../llwin32headers.h" +#endif + +#include + +#include + +#include +#include +#include +#include +#include +#include + +namespace +{ + using namespace std::chrono_literals; + + // Whether done() comes true within a few seconds. + template + bool waitFor(DONE&& done) + { + const auto deadline = std::chrono::steady_clock::now() + 5s; + while (!done()) + { + if (std::chrono::steady_clock::now() > deadline) + { + return false; + } + std::this_thread::sleep_for(1ms); + } + return true; + } + + // How long a timed wait on the pool's worker overran its deadline, each + // of `count` times. The worker holds what it writes into as well, since a + // wait that never ends outlives the check that gave up on it. + std::vector> overruns(LL::ThreadPool& pool, std::chrono::milliseconds wait, size_t count) + { + struct Overruns + { + std::vector> mOverran; + std::atomic mDone{ 0 }; + }; + auto measured = std::make_shared(); + measured->mOverran.resize(count); + for (size_t i = 0; i < count; ++i) + { + const bool posted = pool.getQueue().post([measured, wait, i]() + { + const auto start = std::chrono::steady_clock::now(); + boost::this_fiber::sleep_for(wait); + measured->mOverran[i] = std::chrono::steady_clock::now() - start - wait; + measured->mDone.store(i + 1, std::memory_order_release); + }); + REQUIRE(posted); + REQUIRE(waitFor([&measured, i]() { return measured->mDone.load(std::memory_order_acquire) > i; })); + } + return measured->mOverran; + } + + TEST_CASE("threadpool: a worker's timed wait comes back once its deadline passes", "[threadpool]") + { + LL::ThreadPool pool("threadpool_test_deadline", 1, 1024, false, true); + pool.start(); + for (const auto& overran : overruns(pool, 20ms, 5)) + { + CHECK(overran.count() >= 0.0); + } + } + +#if LL_WINDOWS + TEST_CASE("threadpool: an idle worker runs nothing until work comes", "[threadpool]") + { + std::atomic worker{ nullptr }; + LL::ThreadPool pool("threadpool_test_idle", 1, 1024, false, true); + pool.start(); + REQUIRE(pool.getQueue().post([&worker]() + { + worker = OpenThread(THREAD_QUERY_LIMITED_INFORMATION, FALSE, GetCurrentThreadId()); + })); + REQUIRE(waitFor([&worker]() { return worker.load() != nullptr; })); + // Back to waiting for the next. + std::this_thread::sleep_for(50ms); + + ULONG64 before = 0; + ULONG64 after = 0; + REQUIRE(QueryThreadCycleTime(worker, &before)); + std::this_thread::sleep_for(250ms); + REQUIRE(QueryThreadCycleTime(worker, &after)); + CloseHandle(worker); + INFO("cycles while idle: " << (after - before)); + // A worker that looks for work every tick of the system timer runs + // sixteen times or more in a quarter second, a thousand at the 1 ms + // the viewer's window asks for, and thousands of cycles each time. + CHECK(after - before < 20000); + } + + TEST_CASE("threadpool: a worker's timed wait is not rounded to the system timer's tick", "[threadpool]") + { + LL::ThreadPool pool("threadpool_test_tick", 1, 1024, false, true); + pool.start(); + std::vector> overran = overruns(pool, 2ms, 21); + std::nth_element(overran.begin(), overran.begin() + overran.size() / 2, overran.end()); + const double median = overran[overran.size() / 2].count(); + INFO("median overrun: " << median * 1000.0 << " ms"); + // A wait the tick rounds wakes at the next tick: up to 15.6 ms late + // at Windows' default. + CHECK(median < 0.0015); + } +#endif +} diff --git a/indra/llcommon/threadpool.cpp b/indra/llcommon/threadpool.cpp index c72a58cc4c..546127e994 100644 --- a/indra/llcommon/threadpool.cpp +++ b/indra/llcommon/threadpool.cpp @@ -25,37 +25,118 @@ #include +#include +#include + /***************************************************************************** * Custom fiber scheduler for worker threads *****************************************************************************/ // As of 2022-12-06, each of our worker threads only runs a single (default) // fiber: we don't launch explicit fibers within worker threads, nor do we // anticipate doing so. So a worker thread that's simply waiting for incoming -// tasks should really sleep a little. Override the default fiber scheduler to -// implement that. +// tasks should sleep until there is one. Override the default fiber scheduler +// to implement that. +#if LL_WINDOWS +#ifndef CREATE_WAITABLE_TIMER_HIGH_RESOLUTION +#define CREATE_WAITABLE_TIMER_HIGH_RESOLUTION 0x00000002 +#endif + struct sleepy_robin: public boost::fibers::algo::round_robin { - virtual void suspend_until( std::chrono::steady_clock::time_point const&) noexcept + sleepy_robin(): + mWake(CreateEventW(nullptr, FALSE, FALSE, nullptr)), + mTimer(CreateWaitableTimerExW(nullptr, nullptr, CREATE_WAITABLE_TIMER_HIGH_RESOLUTION, TIMER_ALL_ACCESS)) { -#if LL_WINDOWS - // round_robin holds a std::condition_variable, and - // round_robin::suspend_until() calls - // std::condition_variable::wait_until(). On Windows, that call seems - // busier than it ought to be. Try just sleeping. - Sleep(1); -#else - // currently unused other than windows, but might as well have something here - // different units than Sleep(), but we actually just want to sleep for any de-minimis duration - usleep(1); -#endif + if (!mWake || !mTimer) + { + // Short of either, round_robin's own wait, for the waits and the + // wakes alike. + closeHandles(); + } + } + + ~sleepy_robin() override + { + closeHandles(); + } + + // round_robin holds a std::condition_variable, and + // round_robin::suspend_until() calls + // std::condition_variable::wait_until(). On Windows, that call seems + // busier than it ought to be. This waits on an event instead, which a + // fiber made ready from another thread sets; and, where a fiber's timed + // wait sets a deadline, on a high resolution waitable timer, which the + // system timer's tick does not round as it does Sleep() and a wait's + // timeout -- to a whole 15.6 ms on Windows 11 for a process whose window + // is hidden, whatever resolution it asked for. + void suspend_until(std::chrono::steady_clock::time_point const& until) noexcept override + { + if (!mWake) + { + round_robin::suspend_until(until); + return; + } + if (until == (std::chrono::steady_clock::time_point::max)()) + { + WaitForSingleObject(mWake, INFINITE); + return; + } + const auto left = until - std::chrono::steady_clock::now(); + if (left <= left.zero()) + { + return; + } + // Relative, in the timer's 100 ns units, rounded up: a wait that + // ended early would only come back here. + LARGE_INTEGER due; + due.QuadPart = -(std::max)(LONGLONG(1), + LONGLONG(std::chrono::ceil>>(left).count())); + if (!SetWaitableTimerEx(mTimer, &due, 0, nullptr, nullptr, nullptr, 0)) + { + // A timer that would not be set: a wait the tick rounds, which a + // wake still ends at once. + WaitForSingleObject(mWake, DWORD((std::min)(std::chrono::ceil(left).count(), + static_cast(INFINITE - 1)))); + return; + } + const HANDLE waits[] = { mWake, mTimer }; + if (WaitForMultipleObjects(DWORD(std::size(waits)), waits, FALSE, INFINITE) == WAIT_OBJECT_0) + { + // Woken before the deadline, which nothing waits for now. + CancelWaitableTimer(mTimer); + } + } + + // Auto-reset: a notify that comes before the wait is kept for it. + void notify() noexcept override + { + if (!mWake) + { + round_robin::notify(); + return; + } + SetEvent(mWake); } - virtual void notify() noexcept +private: + void closeHandles() { - // Since our Sleep() call above will wake up on its own, we need not - // take any special action to wake it. + if (mTimer) + { + CloseHandle(mTimer); + mTimer = nullptr; + } + if (mWake) + { + CloseHandle(mWake); + mWake = nullptr; + } } + + HANDLE mWake; + HANDLE mTimer; }; +#endif // LL_WINDOWS /***************************************************************************** * ThreadPoolBase @@ -156,7 +237,6 @@ void LL::ThreadPoolBase::close() void LL::ThreadPoolBase::run(const std::string& name) { #if LL_WINDOWS - // Try using sleepy_robin fiber scheduler. boost::fibers::use_scheduling_algorithm(); #endif // LL_WINDOWS From 9ebf45f009210a8e1aea1a17528efd922b55fc0c Mon Sep 17 00:00:00 2001 From: Rye Date: Tue, 27 Oct 2026 22:56:49 -0400 Subject: [PATCH 2/2] Generated UUIDs are random v4, from the system's generator, on any thread without a lock LLUUID::generate made a version 1 UUID -- the time, a clock sequence and the network card's address -- and handed out its MD5. The time and the clock sequence lived in statics no lock guarded, so threads generating at once read the same tick and made the same UUID: eight threads making 25,000 each made about 140,000 distinct of 200,000, every run. Its one lock, an LLMutex on the branch where the clock had not moved, asserted if a coroutine reached it, and was made lazily on whichever thread got there first. A UUID is now 122 random bits with RFC 9562's version 4 and variant, from the system's cryptographic generator (BCryptGenRandom, getrandom, arc4random_buf) through a buffer each thread keeps: nothing shared between threads, and a call into the system for every sixteen UUIDs. uuid_time_t, cmpTime, getSystemTime, getCurrentTime and LLUUID::mMutex are gone; the machine ID and the hashed unique ID still read getNodeID. bcrypt joins the Windows system libraries. combine and the name-based generate(string) keep their MD5: combine is what the server makes an upload's asset ID with, and every caller of generate(string) names files or keys from it. Co-Authored-By: Claude Opus 5.5 --- indra/cmake/Linking.cmake | 1 + indra/llcommon/lluuid.cpp | 261 ++++++++------------------- indra/llcommon/lluuid.h | 16 +- indra/llcommon/tests/lluuid_test.cpp | 50 +++++ 4 files changed, 125 insertions(+), 203 deletions(-) diff --git a/indra/cmake/Linking.cmake b/indra/cmake/Linking.cmake index 66f7e01359..04cad31e96 100644 --- a/indra/cmake/Linking.cmake +++ b/indra/cmake/Linking.cmake @@ -56,6 +56,7 @@ elseif(WINDOWS) ll::oslibraries INTERFACE advapi32 + bcrypt shlwapi shell32 wbemuuid diff --git a/indra/llcommon/lluuid.cpp b/indra/llcommon/lluuid.cpp index e5b81260d3..e3d9b72e31 100644 --- a/indra/llcommon/lluuid.cpp +++ b/indra/llcommon/lluuid.cpp @@ -30,10 +30,15 @@ #if LL_WINDOWS #include "llwin32headers.h" +#include #include +#elif LL_DARWIN +#include +#else +#include +#include #endif -#include "llapp.h" #include "lldefs.h" #include "llerror.h" @@ -42,15 +47,69 @@ #include "llerror.h" #include "llrand.h" #include "llstring.h" -#include "lltimer.h" -#include "llthread.h" -#include "llmutex.h" #include "llmd5.h" #include -// static -LLMutex* LLUUID::mMutex = NULL; +namespace +{ + // Bytes from the system's cryptographic generator, a thread's buffer + // at a time: nothing shared between threads to lock, and a call into + // the system for every sixteen UUIDs rather than every one. + struct RandomBuffer + { + std::array mBytes{}; + size_t mUsed = UUID_BYTES * 16; + }; + thread_local RandomBuffer tRandom; + + bool systemRandom(U8* out, size_t count) + { +#if LL_WINDOWS + return BCRYPT_SUCCESS(BCryptGenRandom(nullptr, out, static_cast(count), BCRYPT_USE_SYSTEM_PREFERRED_RNG)); +#elif LL_DARWIN + arc4random_buf(out, count); + return true; +#else + while (count > 0) + { + const ssize_t got = getrandom(out, count, 0); + if (got < 0) + { + if (errno == EINTR) + { + continue; + } + return false; + } + out += got; + count -= static_cast(got); + } + return true; +#endif + } + + // At most the buffer's size. + void randomBytes(U8* out, size_t count) + { + RandomBuffer& buffer = tRandom; + if (buffer.mBytes.size() - buffer.mUsed < count) + { + if (!systemRandom(buffer.mBytes.data(), buffer.mBytes.size())) + { + LL_WARNS_ONCE("LLUUID") << "The system's random generator failed; UUIDs come from ll_rand" << LL_ENDL; + for (size_t i = 0; i < buffer.mBytes.size(); i += sizeof(U32)) + { + const U32 bits = ll_rand_u32(); + memcpy(buffer.mBytes.data() + i, &bits, sizeof(bits)); /* Flawfinder: ignore */ + } + } + buffer.mUsed = 0; + } + memcpy(out, buffer.mBytes.data() + buffer.mUsed, count); /* Flawfinder: ignore */ + buffer.mUsed += count; + } +} // Defined here rather than as an inline constexpr in lluuid.h: LLUUID / // LLTransactionID are exported (LL_COMMON_API), so these static members are @@ -426,17 +485,6 @@ std::istream& operator>>(std::istream& s, LLUUID& uuid) return s; } -static void get_random_bytes(void* buf, int nbytes) -{ - U8* cp = static_cast(buf); - const size_t count = static_cast(nbytes); - for (size_t i = 0; i < count; i += sizeof(U32)) - { - const U32 bits = ll_rand_u32(); - memcpy(cp + i, &bits, std::min(sizeof(bits), count - i)); /* Flawfinder: ignore */ - } -} - #if LL_WINDOWS // static @@ -699,181 +747,14 @@ S32 LLUUID::getNodeID(unsigned char* node_id) #endif -S32 LLUUID::cmpTime(uuid_time_t* t1, uuid_time_t* t2) -{ - // Compare two time values. - - if (t1->high < t2->high) return -1; - if (t1->high > t2->high) return 1; - if (t1->low < t2->low) return -1; - if (t1->low > t2->low) return 1; - return 0; -} - -void LLUUID::getSystemTime(uuid_time_t* timestamp) -{ - // Get system time with 100ns precision. Time is since Oct 15, 1582. -#if LL_WINDOWS - ULARGE_INTEGER time; - GetSystemTimeAsFileTime((FILETIME*)&time); - // NT keeps time in FILETIME format which is 100ns ticks since - // Jan 1, 1601. UUIDs use time in 100ns ticks since Oct 15, 1582. - // The difference is 17 Days in Oct + 30 (Nov) + 31 (Dec) - // + 18 years and 5 leap days. - time.QuadPart += - (unsigned __int64)(1000 * 1000 * 10) // seconds - * (unsigned __int64)(60 * 60 * 24) // days - * (unsigned __int64)(17 + 30 + 31 + 365 * 18 + 5); // # of days - - timestamp->high = time.HighPart; - timestamp->low = time.LowPart; -#else - struct timeval tp; - gettimeofday(&tp, 0); - - // Offset between UUID formatted times and Unix formatted times. - // UUID UTC base time is October 15, 1582. - // Unix base time is January 1, 1970. - U64 uuid_time = ((U64)tp.tv_sec * 10000000) + (tp.tv_usec * 10) + - U64L(0x01B21DD213814000); - timestamp->high = (U32)(uuid_time >> 32); - timestamp->low = (U32)(uuid_time & 0xFFFFFFFF); -#endif -} - -void LLUUID::getCurrentTime(uuid_time_t* timestamp) -{ - // Get current time as 60 bit 100ns ticks since whenever. - // Compensate for the fact that real clock resolution is less - // than 100ns. - - const U32 uuids_per_tick = 1024; - - static uuid_time_t time_last; - static U32 uuids_this_tick; - static bool init = false; - - if (!init) { - getSystemTime(&time_last); - uuids_this_tick = uuids_per_tick; - init = true; - mMutex = new LLMutex(); - } - - uuid_time_t time_now = { 0,0 }; - - while (1) { - getSystemTime(&time_now); - - // if clock reading changed since last UUID generated - if (cmpTime(&time_last, &time_now)) { - // reset count of uuid's generated with this clock reading - uuids_this_tick = 0; - break; - } - if (uuids_this_tick < uuids_per_tick) { - uuids_this_tick++; - break; - } - // going too fast for our clock; spin - } - - time_last = time_now; - - if (uuids_this_tick != 0) { - if (time_now.low & 0x80000000) { - time_now.low += uuids_this_tick; - if (!(time_now.low & 0x80000000)) - time_now.high++; - } - else - time_now.low += uuids_this_tick; - } - - timestamp->high = time_now.high; - timestamp->low = time_now.low; -} - void LLUUID::generate() { - // Create a UUID. - uuid_time_t timestamp; - - static unsigned char node_id[6]; /* Flawfinder: ignore */ - static int has_init = 0; - - // Create a UUID. - static uuid_time_t time_last = { 0,0 }; - static U16 clock_seq = 0; - - if (!has_init) - { - has_init = 1; - if (getNodeID(node_id) <= 0) - { - get_random_bytes(node_id, 6); - /* - * Set multicast bit, to prevent conflicts - * with IEEE 802 addresses obtained from - * network cards: the least significant bit - * of the first octet (RFC 4122, 4.5). - */ - node_id[0] |= 0x01; - } - - getCurrentTime(&time_last); - - clock_seq = (U16)ll_rand(0x4000); - } - - // get current time - getCurrentTime(×tamp); - U16 our_clock_seq = clock_seq; - - // if clock hasn't changed or went backward, change clockseq - if (cmpTime(×tamp, &time_last) != 1) - { - LLMutexLock lock(mMutex); - clock_seq = (clock_seq + 1) & 0x3FFF; - if (clock_seq == 0) - clock_seq++; - our_clock_seq = clock_seq; // Ensure we're using a different clock_seq value from previous time - } - - time_last = timestamp; - - memcpy(mData + 10, node_id, 6); /* Flawfinder: ignore */ - U32 tmp; - tmp = timestamp.low; - mData[3] = (unsigned char)tmp; - tmp >>= 8; - mData[2] = (unsigned char)tmp; - tmp >>= 8; - mData[1] = (unsigned char)tmp; - tmp >>= 8; - mData[0] = (unsigned char)tmp; - - tmp = (U16)timestamp.high; - mData[5] = (unsigned char)tmp; - tmp >>= 8; - mData[4] = (unsigned char)tmp; - - tmp = (timestamp.high >> 16) | 0x1000; - mData[7] = (unsigned char)tmp; - tmp >>= 8; - mData[6] = (unsigned char)tmp; - - tmp = our_clock_seq; - - mData[9] = (unsigned char)tmp; - tmp >>= 8; - mData[8] = (unsigned char)tmp; - - LLMD5 md5_uuid; - - md5_uuid.update(mData, 16); - md5_uuid.finalize(); - md5_uuid.raw_digest(mData); + // 122 random bits, with the version and variant RFC 9562 gives a random + // UUID. Nothing of the time or the machine is in it, and nothing is + // shared between threads that make them. + randomBytes(mData, UUID_BYTES); + mData[6] = (mData[6] & 0x0F) | 0x40; + mData[8] = (mData[8] & 0x3F) | 0x80; } void LLUUID::generate(const std::string& hash_string) diff --git a/indra/llcommon/lluuid.h b/indra/llcommon/lluuid.h index a330b5921d..50edd01a1c 100644 --- a/indra/llcommon/lluuid.h +++ b/indra/llcommon/lluuid.h @@ -41,19 +41,12 @@ #include // __umulh #endif -class LLMutex; - const S32 UUID_BYTES = 16; const S32 UUID_WORDS = 4; const S32 UUID_STR_LENGTH = 37; // number of bytes needed to store a UUID as a string const S32 UUID_STR_SIZE = 36; // .size() of a UUID in a std::string const S32 UUID_BASE85_LENGTH = 21; // including the trailing NULL. -struct uuid_time_t { - U32 high; - U32 low; - }; - class LL_COMMON_API LLUUID { public: @@ -83,7 +76,9 @@ class LL_COMMON_API LLUUID // // MANIPULATORS // - void generate(); // Generate a new UUID + // A random UUID, version 4 as RFC 9562 lays it out, on any thread + // without a lock. + void generate(); void generate(const std::string& stream); //Generate a new UUID based on hash of input stream //static versions of above for use in initializer expressions such as constructor params, etc. @@ -94,10 +89,6 @@ class LL_COMMON_API LLUUID bool set(const std::string& in_string, bool emit = true); // Convert from string, if emit is false, do not emit warnings void setNull(); // Faster than setting to LLUUID::null. - S32 cmpTime(uuid_time_t *t1, uuid_time_t *t2); - static void getSystemTime(uuid_time_t *timestamp); - void getCurrentTime(uuid_time_t *timestamp); - // // ACCESSORS // @@ -142,7 +133,6 @@ class LL_COMMON_API LLUUID static bool validate(const std::string& in_string); // Validate that the UUID string is legal. static const LLUUID null; - static LLMutex * mMutex; static S32 getNodeID(unsigned char * node_id); diff --git a/indra/llcommon/tests/lluuid_test.cpp b/indra/llcommon/tests/lluuid_test.cpp index e07b521b47..40adfcbf31 100644 --- a/indra/llcommon/tests/lluuid_test.cpp +++ b/indra/llcommon/tests/lluuid_test.cpp @@ -19,14 +19,17 @@ #include "linden_common.h" +#include #include #include #include #include +#include #include #include #include +#include #include "../lluuid.h" @@ -376,6 +379,53 @@ namespace CHECK(LLUUID::validate(a.asString())); } + TEST_CASE("LLUUID: a generated UUID is version 4 with RFC 9562's variant", "[LLUUID]") + { + // More than the generator's buffer holds, so it is filled again. + for (int i = 0; i < 64; ++i) + { + const LLUUID id = LLUUID::generateNewID(); + INFO(id.asString()); + CHECK((id.mData[6] >> 4) == 4); + CHECK((id.mData[8] & 0xC0) == 0x80); + } + } + + TEST_CASE("LLUUID: threads generating at once make no UUID twice", "[LLUUID]") + { + constexpr size_t THREADS = 8; + constexpr size_t EACH = 25000; + std::vector> made(THREADS); + std::atomic go{ false }; + std::vector threads; + for (size_t t = 0; t < THREADS; ++t) + { + threads.emplace_back([&made, &go, t]() + { + while (!go) + { + std::this_thread::yield(); + } + made[t].reserve(EACH); + for (size_t i = 0; i < EACH; ++i) + { + made[t].push_back(LLUUID::generateNewID()); + } + }); + } + go = true; + for (std::thread& thread : threads) + { + thread.join(); + } + boost::unordered_flat_set all; + for (const std::vector& one : made) + { + all.insert(one.begin(), one.end()); + } + CHECK(all.size() == THREADS * EACH); + } + // to_chars emits exactly UUID_STR_LENGTH-1 chars in canonical 8-4-4-4-12 form. TEST_CASE("LLUUID: to_chars emits UUID_STR_LENGTH-1 chars in canonical 8-4-4-4-12 form", "[LLUUID]") {