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/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/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]") { 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