From 04e1962d48e36f9055f0d893fc1b7a97d9e334c6 Mon Sep 17 00:00:00 2001 From: "Brad Payne (Vir Linden)" Date: Wed, 15 Sep 2021 20:06:56 +0100 Subject: SL-15742 - python 3 support for integration test script --- indra/llcommon/tests/llprocess_test.cpp | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) (limited to 'indra/llcommon/tests') diff --git a/indra/llcommon/tests/llprocess_test.cpp b/indra/llcommon/tests/llprocess_test.cpp index f0eafa8201..447c7f50f2 100644 --- a/indra/llcommon/tests/llprocess_test.cpp +++ b/indra/llcommon/tests/llprocess_test.cpp @@ -356,14 +356,15 @@ namespace tut // Create a script file in a temporary place. NamedTempFile script("py", + "from __future__ import print_function" EOL "import sys" EOL "import time" EOL EOL "time.sleep(2)" EOL - "print >>sys.stdout, 'stdout after wait'" EOL + "print('stdout after wait',file=sys.stdout)" EOL "sys.stdout.flush()" EOL "time.sleep(2)" EOL - "print >>sys.stderr, 'stderr after wait'" EOL + "print('stderr after wait',file=sys.stderr)" EOL "sys.stderr.flush()" EOL ); @@ -568,12 +569,12 @@ namespace tut { set_test_name("arguments"); PythonProcessLauncher py(get_test_name(), - "from __future__ import with_statement\n" + "from __future__ import with_statement, print_function\n" "import sys\n" // note nonstandard output-file arg! "with open(sys.argv[3], 'w') as f:\n" " for arg in sys.argv[1:]:\n" - " print >>f, arg\n"); + " print(arg,file=f)\n"); // We expect that PythonProcessLauncher has already appended // its own NamedTempFile to mParams.args (sys.argv[0]). py.mParams.args.add("first arg"); // sys.argv[1] @@ -857,7 +858,8 @@ namespace tut set_test_name("'bogus' test"); CaptureLog recorder; PythonProcessLauncher py(get_test_name(), - "print 'Hello world'\n"); + "from __future__ import print_function\n" + "print('Hello world')\n"); py.mParams.files.add(LLProcess::FileParam("bogus")); py.mPy = LLProcess::create(py.mParams); ensure("should have rejected 'bogus'", ! py.mPy); @@ -872,7 +874,8 @@ namespace tut // Replace this test with one or more real 'file' tests when we // implement 'file' support PythonProcessLauncher py(get_test_name(), - "print 'Hello world'\n"); + "from __future__ import print_function\n" + "print('Hello world')\n"); py.mParams.files.add(LLProcess::FileParam()); py.mParams.files.add(LLProcess::FileParam("file")); py.mPy = LLProcess::create(py.mParams); @@ -887,7 +890,8 @@ namespace tut // implement 'tpipe' support CaptureLog recorder; PythonProcessLauncher py(get_test_name(), - "print 'Hello world'\n"); + "from __future__ import print_function\n" + "print('Hello world')\n"); py.mParams.files.add(LLProcess::FileParam()); py.mParams.files.add(LLProcess::FileParam("tpipe")); py.mPy = LLProcess::create(py.mParams); @@ -904,7 +908,8 @@ namespace tut // implement 'npipe' support CaptureLog recorder; PythonProcessLauncher py(get_test_name(), - "print 'Hello world'\n"); + "from __future__ import print_function\n" + "print('Hello world')\n"); py.mParams.files.add(LLProcess::FileParam()); py.mParams.files.add(LLProcess::FileParam()); py.mParams.files.add(LLProcess::FileParam("npipe")); @@ -980,7 +985,8 @@ namespace tut { set_test_name("get*Pipe() validation"); PythonProcessLauncher py(get_test_name(), - "print 'this output is expected'\n"); + "from __future__ import print_function\n" + "print('this output is expected')\n"); py.mParams.files.add(LLProcess::FileParam("pipe")); // pipe for stdin py.mParams.files.add(LLProcess::FileParam()); // inherit stdout py.mParams.files.add(LLProcess::FileParam("pipe")); // pipe for stderr @@ -1000,14 +1006,15 @@ namespace tut { set_test_name("talk to stdin/stdout"); PythonProcessLauncher py(get_test_name(), + "from __future__ import print_function\n" "import sys, time\n" - "print 'ok'\n" + "print('ok')\n" "sys.stdout.flush()\n" "# wait for 'go' from test program\n" "go = sys.stdin.readline()\n" "if go != 'go\\n':\n" " sys.exit('expected \"go\", saw %r' % go)\n" - "print 'ack'\n"); + "print('ack')\n"); py.mParams.files.add(LLProcess::FileParam("pipe")); // stdin py.mParams.files.add(LLProcess::FileParam("pipe")); // stdout py.launch(); @@ -1118,7 +1125,8 @@ namespace tut { set_test_name("ReadPipe \"eof\" event"); PythonProcessLauncher py(get_test_name(), - "print 'Hello from Python!'\n"); + "from __future__ import print_function\n" + "print('Hello from Python!')\n"); py.mParams.files.add(LLProcess::FileParam()); // stdin py.mParams.files.add(LLProcess::FileParam("pipe")); // stdout py.launch(); -- cgit v1.3 From 1b1ebdf183e50c6a751493570ee6e643c33c4eda Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Mon, 4 Oct 2021 11:48:58 -0400 Subject: SL-16024: Introduce tuple.h with tuple_cons(), tuple_cdr(). These functions allow prepending or removing an item at the left end of an arbitrary tuple -- for instance, to add a sequence key to a caller's data, then remove it again when delivering the original tuple. --- indra/llcommon/CMakeLists.txt | 2 + indra/llcommon/tests/tuple_test.cpp | 47 +++++++++++++++++++++ indra/llcommon/tuple.h | 84 +++++++++++++++++++++++++++++++++++++ 3 files changed, 133 insertions(+) create mode 100644 indra/llcommon/tests/tuple_test.cpp create mode 100644 indra/llcommon/tuple.h (limited to 'indra/llcommon/tests') diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt index dd266630ea..6558219462 100644 --- a/indra/llcommon/CMakeLists.txt +++ b/indra/llcommon/CMakeLists.txt @@ -254,6 +254,7 @@ set(llcommon_HEADER_FILES stdtypes.h stringize.h timer.h + tuple.h u64.h StackWalker.h ) @@ -358,6 +359,7 @@ if (LL_TESTS) LL_ADD_INTEGRATION_TEST(lluri "" "${test_libs}") LL_ADD_INTEGRATION_TEST(llunits "" "${test_libs}") LL_ADD_INTEGRATION_TEST(stringize "" "${test_libs}") + LL_ADD_INTEGRATION_TEST(tuple "" "${test_libs}") ## llexception_test.cpp isn't a regression test, and doesn't need to be run ## every build. It's to help a developer make implementation choices about diff --git a/indra/llcommon/tests/tuple_test.cpp b/indra/llcommon/tests/tuple_test.cpp new file mode 100644 index 0000000000..af94e2086c --- /dev/null +++ b/indra/llcommon/tests/tuple_test.cpp @@ -0,0 +1,47 @@ +/** + * @file tuple_test.cpp + * @author Nat Goodspeed + * @date 2021-10-04 + * @brief Test for tuple. + * + * $LicenseInfo:firstyear=2021&license=viewerlgpl$ + * Copyright (c) 2021, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "tuple.h" +// STL headers +// std headers +// external library headers +// other Linden headers +#include "../test/lltut.h" + +/***************************************************************************** +* TUT +*****************************************************************************/ +namespace tut +{ + struct tuple_data + { + }; + typedef test_group tuple_group; + typedef tuple_group::object object; + tuple_group tuplegrp("tuple"); + + template<> template<> + void object::test<1>() + { + set_test_name("tuple"); + std::tuple tup{ "abc", 17 }; + std::tuple ptup{ tuple_cons(34, tup) }; + std::tuple tup2; + int i; + std::tie(i, tup2) = tuple_split(ptup); + ensure_equals("tuple_car() fail", i, 34); + ensure_equals("tuple_cdr() (0) fail", std::get<0>(tup2), "abc"); + ensure_equals("tuple_cdr() (1) fail", std::get<1>(tup2), 17); + } +} // namespace tut diff --git a/indra/llcommon/tuple.h b/indra/llcommon/tuple.h new file mode 100644 index 0000000000..bfe7e3c2ba --- /dev/null +++ b/indra/llcommon/tuple.h @@ -0,0 +1,84 @@ +/** + * @file tuple.h + * @author Nat Goodspeed + * @date 2021-10-04 + * @brief A couple tuple utilities + * + * $LicenseInfo:firstyear=2021&license=viewerlgpl$ + * Copyright (c) 2021, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_TUPLE_H) +#define LL_TUPLE_H + +#include +#include // std::remove_reference +#include // std::pair + +/** + * tuple_cons() behaves like LISP cons: it uses std::tuple_cat() to prepend a + * new item of arbitrary type to an existing std::tuple. + */ +template > +auto tuple_cons(First&& first, Tuple_&& rest) +{ + // All we need to do is make a tuple containing 'first', and let + // tuple_cat() do the hard part. + return std::tuple_cat(std::tuple(std::forward(first)), + std::forward(rest)); +} + +/** + * tuple_car() behaves like LISP car: it extracts the first item from a + * std::tuple. + */ +template > +auto tuple_car(Tuple_&& tuple) +{ + return std::get<0>(std::forward(tuple)); +} + +/** + * tuple_cdr() behaves like LISP cdr: it returns a new tuple containing + * everything BUT the first item. + */ +// derived from https://stackoverflow.com/a/24046437 +template +auto tuple_cdr_(Tuple&& tuple, const std::index_sequence) +{ + // Given an index sequence from [0..N-1), extract tuple items [1..N) + return std::make_tuple(std::get(std::forward(tuple))...); +} + +template +auto tuple_cdr(Tuple&& tuple) +{ + return tuple_cdr_( + std::forward(tuple), + // Pass helper function an index sequence one item shorter than tuple + std::make_index_sequence< + std::tuple_size< + // tuple_size doesn't like reference types + typename std::remove_reference::type + >::value - 1u> + ()); +} + +/** + * tuple_split(), the opposite of tuple_cons(), has no direct analog in LISP. + * It returns a std::pair of tuple_car(), tuple_cdr(). We could call this + * function tuple_car_cdr(), or tuple_slice() or some such. But tuple_split() + * feels more descriptive. + */ +template > +auto tuple_split(Tuple_&& tuple) +{ + // We're not really worried about forwarding multiple times a tuple that + // might contain move-only items, because the implementation above only + // applies std::get() exactly once to each item. + return std::make_pair(tuple_car(std::forward(tuple)), + tuple_cdr(std::forward(tuple))); +} + +#endif /* ! defined(LL_TUPLE_H) */ -- cgit v1.3 From 955b967623983cb50ba09f7b82e5f01f2c6bcebb Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Tue, 5 Oct 2021 17:31:53 -0400 Subject: SL-16024: Add ThreadSafeSchedule, a timestamped LLThreadSafeQueue. ThreadSafeSchedule orders its items by timestamp, which can be passed either implicitly or explicitly. The timestamp specifies earliest delivery time: an item cannot be popped until that time. Add initial tests. Tweak the LLThreadSafeQueue base class to support ThreadSafeSchedule: introduce virtual canPop() method to report whether the current head item is available to pop. The base class unconditionally says yes, ThreadSafeSchedule says it depends on whether its timestamp is still in the future. This replaces the protected pop_() overload accepting a predicate. Rather than explicitly passing a predicate through a couple levels of function call, use canPop() at the level it matters. Runtime behavior that varies depending on an object's leaf class is what virtual functions were invented for. Give pop_() a three-state enum return so pop() can distinguish between "closed and empty" (throws exception) versus "closed, not yet drained because we're not yet ready to pop the head item" (waits). Also break out protected tryPopUntil_() method, the body logic of tryPopUntil(). The public method locks the data structure, the protected method requires that its caller has already done so. Add chrono.h with a more full-featured LL::time_point_cast() function than the one found in , which only converts between time_point durations, not between time_points based on different clocks. --- indra/llcommon/CMakeLists.txt | 3 + indra/llcommon/chrono.h | 65 +++++ indra/llcommon/llthreadsafequeue.h | 121 ++++---- indra/llcommon/tests/threadsafeschedule_test.cpp | 65 +++++ indra/llcommon/threadsafeschedule.h | 334 +++++++++++++++++++++++ 5 files changed, 535 insertions(+), 53 deletions(-) create mode 100644 indra/llcommon/chrono.h create mode 100644 indra/llcommon/tests/threadsafeschedule_test.cpp create mode 100644 indra/llcommon/threadsafeschedule.h (limited to 'indra/llcommon/tests') diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt index 6558219462..5efcfabf24 100644 --- a/indra/llcommon/CMakeLists.txt +++ b/indra/llcommon/CMakeLists.txt @@ -127,6 +127,7 @@ set(llcommon_SOURCE_FILES set(llcommon_HEADER_FILES CMakeLists.txt + chrono.h ctype_workaround.h fix_macros.h indra_constants.h @@ -253,6 +254,7 @@ set(llcommon_HEADER_FILES lockstatic.h stdtypes.h stringize.h + threadsafeschedule.h timer.h tuple.h u64.h @@ -359,6 +361,7 @@ if (LL_TESTS) LL_ADD_INTEGRATION_TEST(lluri "" "${test_libs}") LL_ADD_INTEGRATION_TEST(llunits "" "${test_libs}") LL_ADD_INTEGRATION_TEST(stringize "" "${test_libs}") + LL_ADD_INTEGRATION_TEST(threadsafeschedule "" "${test_libs}") LL_ADD_INTEGRATION_TEST(tuple "" "${test_libs}") ## llexception_test.cpp isn't a regression test, and doesn't need to be run diff --git a/indra/llcommon/chrono.h b/indra/llcommon/chrono.h new file mode 100644 index 0000000000..806e871892 --- /dev/null +++ b/indra/llcommon/chrono.h @@ -0,0 +1,65 @@ +/** + * @file chrono.h + * @author Nat Goodspeed + * @date 2021-10-05 + * @brief supplement with utility functions + * + * $LicenseInfo:firstyear=2021&license=viewerlgpl$ + * Copyright (c) 2021, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_CHRONO_H) +#define LL_CHRONO_H + +#include +#include // std::enable_if + +namespace LL +{ + +// time_point_cast() is derived from https://stackoverflow.com/a/35293183 +// without the iteration: we think errors in the ~1 microsecond range are +// probably acceptable. + +// This variant is for the optimal case when the source and dest use the same +// clock: that case is handled by std::chrono. +template ::value, + bool>::type = true> +DestTimePoint time_point_cast(const SrcTimePoint& time) +{ + return std::chrono::time_point_cast(time); +} + +// This variant is for when the source and dest use different clocks -- see +// the linked StackOverflow answer, also Howard Hinnant's, for more context. +template ::value, + bool>::type = true> +DestTimePoint time_point_cast(const SrcTimePoint& time) +{ + // The basic idea is that we must adjust the passed time_point by the + // difference between the clocks' epochs. But since time_point doesn't + // expose its epoch, we fall back on what each of them thinks is now(). + // However, since we necessarily make sequential calls to those now() + // functions, the answers differ not only by the cycles spent executing + // those calls, but by potential OS interruptions between them. Try to + // reduce that error by capturing the source clock time both before and + // after the dest clock, and splitting the difference. Of course an + // interruption between two of these now() calls without a comparable + // interruption between the other two will skew the result, but better is + // more expensive. + const auto src_before = typename SrcTimePoint::clock::now(); + const auto dest_now = typename DestTimePoint::clock::now(); + const auto src_after = typename SrcTimePoint::clock::now(); + const auto src_diff = src_after - src_before; + const auto src_now = src_before + src_diff / 2; + return dest_now + (time - src_now); +} + +} // namespace LL + +#endif /* ! defined(LL_CHRONO_H) */ diff --git a/indra/llcommon/llthreadsafequeue.h b/indra/llcommon/llthreadsafequeue.h index 1dffad6b89..bd2d82d4c3 100644 --- a/indra/llcommon/llthreadsafequeue.h +++ b/indra/llcommon/llthreadsafequeue.h @@ -83,6 +83,7 @@ public: // Limiting the number of pending items prevents unbounded growth of the // underlying queue. LLThreadSafeQueue(U32 capacity = 1024); + virtual ~LLThreadSafeQueue() {} // Add an element to the queue (will block if the queue has // reached capacity). @@ -162,10 +163,10 @@ public: // then every subsequent tryPop() call will return false void close(); - // detect closed state + // producer end: are we prevented from pushing any additional items? bool isClosed(); - // inverse of isClosed() - explicit operator bool(); + // consumer end: are we done, is the queue entirely drained? + bool done(); protected: typedef QueueT queue_type; @@ -178,6 +179,11 @@ protected: boost::fibers::condition_variable_any mCapacityCond; boost::fibers::condition_variable_any mEmptyCond; + // implementation logic, suitable for passing to tryLockUntil() + template + bool tryPopUntil_(lock_t& lock, + const std::chrono::time_point& until, + ElementT& element); // if we're able to lock immediately, do so and run the passed callable, // which must accept lock_t& and return bool template @@ -191,11 +197,11 @@ protected: template bool push_(lock_t& lock, T&& element); // while lock is locked, really pop the head element, if we can - bool pop_(lock_t& lock, ElementT& element); - // pop_() with an explicit predicate indicating whether the head element - // is ready to be popped - template - bool pop_(lock_t& lock, ElementT& element, PRED&& pred); + enum pop_result { EMPTY, WAITING, POPPED }; + pop_result pop_(lock_t& lock, ElementT& element); + // Is the current head element ready to pop? We say yes; subclass can + // override as needed. + virtual bool canPop(const ElementT& head) const { return true; } }; /***************************************************************************** @@ -387,26 +393,16 @@ bool LLThreadSafeQueue::tryPushUntil( // while lock is locked, really pop the head element, if we can template -bool LLThreadSafeQueue::pop_(lock_t& lock, ElementT& element) -{ - // default predicate: head element, if present, is always ready to pop - return pop_(lock, element, [](const ElementT&){ return true; }); -} - - -// pop_() with an explicit predicate indicating whether the head element -// is ready to be popped -template -template -bool LLThreadSafeQueue::pop_( - lock_t& lock, ElementT& element, PRED&& pred) +typename LLThreadSafeQueue::pop_result +LLThreadSafeQueue::pop_(lock_t& lock, ElementT& element) { // If mStorage is empty, there's no head element. - // If there's a head element, pass it to the predicate to see if caller - // considers it ready to pop. - // Unless both are satisfied, no point in continuing. - if (mStorage.empty() || ! std::forward(pred)(mStorage.front())) - return false; + if (mStorage.empty()) + return EMPTY; + + // If there's a head element, pass it to canPop() to see if it's ready to pop. + if (! canPop(mStorage.front())) + return WAITING; // std::queue::front() is the element about to pop() element = mStorage.front(); @@ -414,7 +410,7 @@ bool LLThreadSafeQueue::pop_( lock.unlock(); // now that we've popped, if somebody's been waiting to push, signal them mCapacityCond.notify_one(); - return true; + return POPPED; } @@ -422,17 +418,20 @@ template ElementT LLThreadSafeQueue::pop(void) { lock_t lock1(mLock); + ElementT value; while (true) { // On the consumer side, we always try to pop before checking mClosed // so we can finish draining the queue. - ElementT value; - if (pop_(lock1, value)) + pop_result popped = pop_(lock1, value); + if (popped == POPPED) return std::move(value); // Once the queue is empty, mClosed lets us know if there will ever be - // any more coming. - if (mClosed) + // any more coming. If we didn't pop because WAITING, i.e. canPop() + // returned false, then even if the producer end has been closed, + // there's still at least one item to drain: wait for it. + if (popped == EMPTY && mClosed) { LLTHROW(LLThreadSafeQueueInterrupt()); } @@ -452,7 +451,7 @@ bool LLThreadSafeQueue::tryPop(ElementT & element) // no need to check mClosed: tryPop() behavior when the queue is // closed is implemented by simple inability to push any new // elements - return pop_(lock, element); + return pop_(lock, element) == POPPED; }); } @@ -479,26 +478,38 @@ bool LLThreadSafeQueue::tryPopUntil( until, [this, until, &element](lock_t& lock) { - while (true) - { - if (pop_(lock, element)) - return true; + return tryPopUntil_(lock, until, element); + }); +} - if (mClosed) - { - return false; - } - // Storage empty. Wait for signal. - if (LLCoros::cv_status::timeout == mEmptyCond.wait_until(lock, until)) - { - // timed out -- formally we might recheck both conditions above - return false; - } - // If we didn't time out, we were notified for some reason. Loop back - // to check. - } - }); +// body of tryPopUntil(), called once we have the lock +template +template +bool LLThreadSafeQueue::tryPopUntil_( + lock_t& lock, + const std::chrono::time_point& until, + ElementT& element) +{ + while (true) + { + if (pop_(lock, element) == POPPED) + return true; + + if (mClosed) + { + return false; + } + + // Storage empty. Wait for signal. + if (LLCoros::cv_status::timeout == mEmptyCond.wait_until(lock, until)) + { + // timed out -- formally we might recheck both conditions above + return false; + } + // If we didn't time out, we were notified for some reason. Loop back + // to check. + } } @@ -509,6 +520,7 @@ size_t LLThreadSafeQueue::size(void) return mStorage.size(); } + template void LLThreadSafeQueue::close() { @@ -521,17 +533,20 @@ void LLThreadSafeQueue::close() mCapacityCond.notify_all(); } + template bool LLThreadSafeQueue::isClosed() { lock_t lock(mLock); - return mClosed && mStorage.size() == 0; + return mClosed; } + template -LLThreadSafeQueue::operator bool() +bool LLThreadSafeQueue::done() { - return ! isClosed(); + lock_t lock(mLock); + return mClosed && mStorage.size() == 0; } #endif diff --git a/indra/llcommon/tests/threadsafeschedule_test.cpp b/indra/llcommon/tests/threadsafeschedule_test.cpp new file mode 100644 index 0000000000..ec0fa0c928 --- /dev/null +++ b/indra/llcommon/tests/threadsafeschedule_test.cpp @@ -0,0 +1,65 @@ +/** + * @file threadsafeschedule_test.cpp + * @author Nat Goodspeed + * @date 2021-10-04 + * @brief Test for threadsafeschedule. + * + * $LicenseInfo:firstyear=2021&license=viewerlgpl$ + * Copyright (c) 2021, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "threadsafeschedule.h" +// STL headers +// std headers +#include +// external library headers +// other Linden headers +#include "../test/lltut.h" + +using namespace std::literals::chrono_literals; // ms suffix +using namespace std::literals::string_literals; // s suffix +using Queue = LL::ThreadSafeSchedule; + +/***************************************************************************** +* TUT +*****************************************************************************/ +namespace tut +{ + struct threadsafeschedule_data + { + Queue queue; + }; + typedef test_group threadsafeschedule_group; + typedef threadsafeschedule_group::object object; + threadsafeschedule_group threadsafeschedulegrp("threadsafeschedule"); + + template<> template<> + void object::test<1>() + { + set_test_name("push"); + // Simply calling push() a few times might result in indeterminate + // delivery order if the resolution of steady_clock is coarser than + // the real time required for each push() call. Explicitly increment + // the timestamp for each one -- but since we're passing explicit + // timestamps, make the queue reorder them. + queue.push(Queue::TimeTuple(Queue::Clock::now() + 20ms, "ghi")); + queue.push("abc"s); + queue.push(Queue::Clock::now() + 10ms, "def"); + queue.close(); + auto entry = queue.pop(); + ensure_equals("failed to pop first", std::get<0>(entry), "abc"s); + entry = queue.pop(); + ensure_equals("failed to pop second", std::get<0>(entry), "def"s); + ensure("queue not closed", queue.isClosed()); + ensure("queue prematurely done", ! queue.done()); + entry = queue.pop(); + ensure_equals("failed to pop third", std::get<0>(entry), "ghi"s); + bool popped = queue.tryPop(entry); + ensure("queue not empty", ! popped); + ensure("queue not done", queue.done()); + } +} // namespace tut diff --git a/indra/llcommon/threadsafeschedule.h b/indra/llcommon/threadsafeschedule.h new file mode 100644 index 0000000000..545c820f53 --- /dev/null +++ b/indra/llcommon/threadsafeschedule.h @@ -0,0 +1,334 @@ +/** + * @file threadsafeschedule.h + * @author Nat Goodspeed + * @date 2021-10-02 + * @brief ThreadSafeSchedule is an ordered queue in which every item has an + * associated timestamp. + * + * $LicenseInfo:firstyear=2021&license=viewerlgpl$ + * Copyright (c) 2021, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_THREADSAFESCHEDULE_H) +#define LL_THREADSAFESCHEDULE_H + +#include "chrono.h" +#include "llexception.h" +#include "llthreadsafequeue.h" +#include "tuple.h" +#include +#include + +namespace LL +{ + namespace ThreadSafeSchedulePrivate + { + using TimePoint = std::chrono::steady_clock::time_point; + // Bundle consumer's data with a TimePoint to order items by timestamp. + template + using TimestampedTuple = std::tuple; + + // comparison functor for TimedTuples -- see TimedQueue comments + struct ReverseTupleOrder + { + template + bool operator()(const Tuple& left, const Tuple& right) const + { + return std::get<0>(left) > std::get<0>(right); + } + }; + + template + using TimedQueue = PriorityQueueAdapter< + TimestampedTuple, + // std::vector is the default storage for std::priority_queue, + // have to restate to specify comparison template parameter + std::vector>, + // std::priority_queue uses a counterintuitive comparison + // behavior: the default std::less comparator is used to present + // the *highest* value as top(). So to sort by earliest timestamp, + // we must invert by using >. + ReverseTupleOrder>; + } // namespace ThreadSafeSchedulePrivate + + /** + * ThreadSafeSchedule is an ordered LLThreadSafeQueue in which every item + * is given an associated timestamp. That is, TimePoint is implicitly + * prepended to the std::tuple with the specified types. + * + * Items are popped in increasing chronological order. Moreover, any item + * with a timestamp in the future is held back until + * std::chrono::steady_clock reaches that timestamp. + */ + template + class ThreadSafeSchedule: + public LLThreadSafeQueue, + ThreadSafeSchedulePrivate::TimedQueue> + { + public: + using DataTuple = std::tuple; + using TimeTuple = ThreadSafeSchedulePrivate::TimestampedTuple; + + private: + using super = LLThreadSafeQueue>; + using lock_t = typename super::lock_t; + using super::pop_; + using super::push_; + using super::mClosed; + using super::mEmptyCond; + using super::mCapacityCond; + + public: + using TimePoint = ThreadSafeSchedulePrivate::TimePoint; + using Clock = TimePoint::clock; + + ThreadSafeSchedule(U32 capacity=1024): + super(capacity) + {} + + /*----------------------------- push() -----------------------------*/ + /// explicitly pass TimeTuple + using super::push; + + /// pass DataTuple with implicit now + void push(const DataTuple& tuple) + { + push(tuple_cons(Clock::now(), tuple)); + } + + /// individually pass each component of the TimeTuple + void push(const TimePoint& time, Args&&... args) + { + push(TimeTuple(time, std::forward(args)...)); + } + + /// individually pass every component except the TimePoint (implies + /// now) -- could be ambiguous if the first specified template + /// parameter type is also TimePoint -- we could try to disambiguate, + /// but a simpler approach would be for the caller to explicitly + /// construct DataTuple and call that overload + void push(Args&&... args) + { + push(Clock::now(), std::forward(args)...); + } + + /*--------------------------- tryPush() ----------------------------*/ + /// explicit TimeTuple + using super::tryPush; + + /// DataTuple with implicit now + bool tryPush(const DataTuple& tuple) + { + return tryPush(tuple_cons(Clock::now(), tuple)); + } + + /// individually pass components + bool tryPush(const TimePoint& time, Args&&... args) + { + return tryPush(TimeTuple(time, std::forward(args)...)); + } + + /// individually pass components with implicit now + bool tryPush(Args&&... args) + { + return tryPush(Clock::now(), std::forward(args)...); + } + + /*-------------------------- tryPushFor() --------------------------*/ + /// explicit TimeTuple + using super::tryPushFor; + + /// DataTuple with implicit now + template + bool tryPushFor(const std::chrono::duration& timeout, + const DataTuple& tuple) + { + return tryPushFor(timeout, tuple_cons(Clock::now(), tuple)); + } + + /// individually pass components + template + bool tryPushFor(const std::chrono::duration& timeout, + const TimePoint& time, Args&&... args) + { + return tryPushFor(TimeTuple(time, std::forward(args)...)); + } + + /// individually pass components with implicit now + template + bool tryPushFor(const std::chrono::duration& timeout, + Args&&... args) + { + return tryPushFor(Clock::now(), std::forward(args)...); + } + + /*------------------------- tryPushUntil() -------------------------*/ + /// explicit TimeTuple + using super::tryPushUntil; + + /// DataTuple with implicit now + template + bool tryPushUntil(const std::chrono::time_point& until, + const DataTuple& tuple) + { + return tryPushUntil(until, tuple_cons(Clock::now(), tuple)); + } + + /// individually pass components + template + bool tryPushUntil(const std::chrono::time_point& until, + const TimePoint& time, Args&&... args) + { + return tryPushUntil(until, TimeTuple(time, std::forward(args)...)); + } + + /// individually pass components with implicit now + template + bool tryPushUntil(const std::chrono::time_point& until, + Args&&... args) + { + return tryPushUntil(until, Clock::now(), std::forward(args)...); + } + + /*----------------------------- pop() ------------------------------*/ + // Our consumer may or may not care about the timestamp associated + // with each popped item, so we allow retrieving either DataTuple or + // TimeTuple. One potential use would be to observe, and possibly + // adjust for, the time lag between the item time and the actual + // current time. + + /// pop DataTuple by value + DataTuple pop() + { + return tuple_cdr(popWithTime()); + } + + /// pop TimeTuple by value + TimeTuple popWithTime() + { + lock_t lock(super::mLock); + // We can't just sit around waiting forever, given that there may + // be items in the queue that are not yet ready but will *become* + // ready in the near future. So in fact, with this class, every + // pop() becomes a tryPopUntil(), constrained to the timestamp of + // the head item. It almost doesn't matter what we specify for the + // caller's time constraint -- all we really care about is the + // head item's timestamp. Since pop() and popWithTime() are + // defined to wait until either an item becomes available or the + // queue is closed, loop until one of those things happens. The + // constraint we pass just determines how often we'll loop while + // waiting. + TimeTuple tt; + while (true) + { + // Pick a point suitably far into the future. + TimePoint until = TimePoint::clock::now() + std::chrono::hours(24); + if (tryPopUntil_(lock, until, tt)) + return std::move(tt); + + // empty and closed: throw, just as super::pop() does + if (super::mStorage.empty() && super::mClosed) + { + LLTHROW(LLThreadSafeQueueInterrupt()); + } + // If not empty, we've still got items to drain. + // If not closed, it's worth waiting for more items. + // Either way, loop back to wait. + } + } + + // We can use tryPop(TimeTuple&) just as it stands; the only behavior + // difference is in our canPop() override method. + using super::tryPop; + + /// tryPop(DataTuple&) + bool tryPop(DataTuple& tuple) + { + TimeTuple tt; + if (! super::tryPop(tt)) + return false; + tuple = tuple_cdr(std::move(tt)); + return true; + } + + /// tryPopFor() + template + bool tryPopFor(const std::chrono::duration& timeout, Tuple& tuple) + { + // It's important to use OUR tryPopUntil() implementation, rather + // than delegating immediately to our base class. + return tryPopUntil(Clock::now() + timeout, tuple); + } + + /// tryPopUntil(TimeTuple&) + template + bool tryPopUntil(const std::chrono::time_point& until, + TimeTuple& tuple) + { + // super::tryPopUntil() wakes up when an item becomes available or + // we hit 'until', whichever comes first. Thing is, the current + // head of the queue could become ready sooner than either of + // those events, and we need to deliver it as soon as it does. + // Don't wait past the TimePoint of the head item. + // Naturally, lock the queue before peeking at mStorage. + return super::tryLockUntil( + until, + [this, until, &tuple](lock_t& lock) + { + // Use our time_point_cast to allow for 'until' that's a + // time_point type other than TimePoint. + return tryPopUntil_(lock, time_point_cast(until), tuple); + }); + } + + bool tryPopUntil_(lock_t& lock, const TimePoint& until, TimeTuple& tuple) + { + TimePoint adjusted = until; + if (! super::mStorage.empty()) + { + // use whichever is earlier: the head item's timestamp, or + // the caller's limit + adjusted = min(std::get<0>(super::mStorage.front()), adjusted); + } + // now delegate to base-class tryPopUntil_() + return super::tryPopUntil_(lock, adjusted, tuple); + } + + /// tryPopUntil(DataTuple&) + template + bool tryPopUntil(const std::chrono::time_point& until, + DataTuple& tuple) + { + TimeTuple tt; + if (! tryPopUntil(until, tt)) + return false; + tuple = tuple_cdr(std::move(tt)); + return true; + } + + /*------------------------------ etc. ------------------------------*/ + // We can't hide items that aren't yet ready because we can't traverse + // the underlying priority_queue: it has no iterators, only top(). So + // a consumer could observe size() > 0 and yet tryPop() returns false. + // Shrug, in a multi-consumer scenario that would be expected behavior. + using super::size; + // open/closed state + using super::close; + using super::isClosed; + using super::done; + + private: + // this method is called by base class pop_() every time we're + // considering whether to deliver the current head element + bool canPop(const TimeTuple& head) const override + { + // an item with a future timestamp isn't yet ready to pop + // (should we add some slop for overhead?) + return std::get<0>(head) <= Clock::now(); + } + }; + +} // namespace LL + +#endif /* ! defined(LL_THREADSAFESCHEDULE_H) */ -- cgit v1.3 From cf70766b4504f7ee745822926c526ed9c86c9339 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 6 Oct 2021 12:54:29 -0400 Subject: SL-16024: Fix ThreadSafeSchedule::tryPopFor(), tryPopUntil(). ThreadSafeSchedule::tryPopUntil() (and therefore tryPopFor()) was simply delegating to LLThreadSafeQueue::tryPopUntil(), with an adjusted timeout since we want to wake up as soon as the head item, if any, becomes ready. But then we have to loop back to retry the pop to actually deal with that head item. In addition, ThreadSafeSchedule::popWithTime() was spinning rather than properly blocking on a timed condition variable. Fixed. --- indra/llcommon/llthreadsafequeue.h | 51 +++++++++-------- indra/llcommon/tests/threadsafeschedule_test.cpp | 10 +++- indra/llcommon/threadsafeschedule.h | 72 ++++++++++++++++++------ 3 files changed, 88 insertions(+), 45 deletions(-) (limited to 'indra/llcommon/tests') diff --git a/indra/llcommon/llthreadsafequeue.h b/indra/llcommon/llthreadsafequeue.h index bd2d82d4c3..719edcd579 100644 --- a/indra/llcommon/llthreadsafequeue.h +++ b/indra/llcommon/llthreadsafequeue.h @@ -179,11 +179,12 @@ protected: boost::fibers::condition_variable_any mCapacityCond; boost::fibers::condition_variable_any mEmptyCond; + enum pop_result { EMPTY, DONE, WAITING, POPPED }; // implementation logic, suitable for passing to tryLockUntil() template - bool tryPopUntil_(lock_t& lock, - const std::chrono::time_point& until, - ElementT& element); + pop_result tryPopUntil_(lock_t& lock, + const std::chrono::time_point& until, + ElementT& element); // if we're able to lock immediately, do so and run the passed callable, // which must accept lock_t& and return bool template @@ -197,7 +198,6 @@ protected: template bool push_(lock_t& lock, T&& element); // while lock is locked, really pop the head element, if we can - enum pop_result { EMPTY, WAITING, POPPED }; pop_result pop_(lock_t& lock, ElementT& element); // Is the current head element ready to pop? We say yes; subclass can // override as needed. @@ -398,7 +398,7 @@ LLThreadSafeQueue::pop_(lock_t& lock, ElementT& element) { // If mStorage is empty, there's no head element. if (mStorage.empty()) - return EMPTY; + return mClosed? DONE : EMPTY; // If there's a head element, pass it to canPop() to see if it's ready to pop. if (! canPop(mStorage.front())) @@ -427,16 +427,16 @@ ElementT LLThreadSafeQueue::pop(void) if (popped == POPPED) return std::move(value); - // Once the queue is empty, mClosed lets us know if there will ever be - // any more coming. If we didn't pop because WAITING, i.e. canPop() - // returned false, then even if the producer end has been closed, - // there's still at least one item to drain: wait for it. - if (popped == EMPTY && mClosed) + // Once the queue is DONE, there will never be any more coming. + if (popped == DONE) { LLTHROW(LLThreadSafeQueueInterrupt()); } - // Storage empty, queue still open. Wait for signal. + // If we didn't pop because WAITING, i.e. canPop() returned false, + // then even if the producer end has been closed, there's still at + // least one item to drain: wait for it. Or we might be EMPTY, with + // the queue still open. Either way, wait for signal. mEmptyCond.wait(lock1); } } @@ -448,8 +448,8 @@ bool LLThreadSafeQueue::tryPop(ElementT & element) return tryLock( [this, &element](lock_t& lock) { - // no need to check mClosed: tryPop() behavior when the queue is - // closed is implemented by simple inability to push any new + // conflate EMPTY, DONE, WAITING: tryPop() behavior when the queue + // is closed is implemented by simple inability to push any new // elements return pop_(lock, element) == POPPED; }); @@ -478,7 +478,8 @@ bool LLThreadSafeQueue::tryPopUntil( until, [this, until, &element](lock_t& lock) { - return tryPopUntil_(lock, until, element); + // conflate EMPTY, DONE, WAITING + return tryPopUntil_(lock, until, element) == POPPED; }); } @@ -486,26 +487,28 @@ bool LLThreadSafeQueue::tryPopUntil( // body of tryPopUntil(), called once we have the lock template template -bool LLThreadSafeQueue::tryPopUntil_( +typename LLThreadSafeQueue::pop_result +LLThreadSafeQueue::tryPopUntil_( lock_t& lock, const std::chrono::time_point& until, ElementT& element) { while (true) { - if (pop_(lock, element) == POPPED) - return true; - - if (mClosed) + pop_result popped = pop_(lock, element); + if (popped == POPPED || popped == DONE) { - return false; + // If we succeeded, great! If we've drained the last item, so be + // it. Either way, break the loop and tell caller. + return popped; } - // Storage empty. Wait for signal. + // EMPTY or WAITING: wait for signal. if (LLCoros::cv_status::timeout == mEmptyCond.wait_until(lock, until)) { - // timed out -- formally we might recheck both conditions above - return false; + // timed out -- formally we might recheck + // as it is, break loop + return popped; } // If we didn't time out, we were notified for some reason. Loop back // to check. @@ -546,7 +549,7 @@ template bool LLThreadSafeQueue::done() { lock_t lock(mLock); - return mClosed && mStorage.size() == 0; + return mClosed && mStorage.empty(); } #endif diff --git a/indra/llcommon/tests/threadsafeschedule_test.cpp b/indra/llcommon/tests/threadsafeschedule_test.cpp index ec0fa0c928..af67b9f492 100644 --- a/indra/llcommon/tests/threadsafeschedule_test.cpp +++ b/indra/llcommon/tests/threadsafeschedule_test.cpp @@ -47,6 +47,8 @@ namespace tut // the timestamp for each one -- but since we're passing explicit // timestamps, make the queue reorder them. queue.push(Queue::TimeTuple(Queue::Clock::now() + 20ms, "ghi")); + // Given the various push() overloads, you have to match the type + // exactly: conversions are ambiguous. queue.push("abc"s); queue.push(Queue::Clock::now() + 10ms, "def"); queue.close(); @@ -56,9 +58,11 @@ namespace tut ensure_equals("failed to pop second", std::get<0>(entry), "def"s); ensure("queue not closed", queue.isClosed()); ensure("queue prematurely done", ! queue.done()); - entry = queue.pop(); - ensure_equals("failed to pop third", std::get<0>(entry), "ghi"s); - bool popped = queue.tryPop(entry); + std::string s; + bool popped = queue.tryPopFor(1s, s); + ensure("failed to pop third", popped); + ensure_equals("third is wrong", s, "ghi"s); + popped = queue.tryPop(s); ensure("queue not empty", ! popped); ensure("queue not done", queue.done()); } diff --git a/indra/llcommon/threadsafeschedule.h b/indra/llcommon/threadsafeschedule.h index 545c820f53..8ab4311ca1 100644 --- a/indra/llcommon/threadsafeschedule.h +++ b/indra/llcommon/threadsafeschedule.h @@ -73,11 +73,7 @@ namespace LL private: using super = LLThreadSafeQueue>; using lock_t = typename super::lock_t; - using super::pop_; - using super::push_; - using super::mClosed; - using super::mEmptyCond; - using super::mCapacityCond; + using pop_result = typename super::pop_result; public: using TimePoint = ThreadSafeSchedulePrivate::TimePoint; @@ -92,6 +88,11 @@ namespace LL using super::push; /// pass DataTuple with implicit now + // This could be ambiguous for Args with a single type. Unfortunately + // we can't enable_if an individual method with a condition based on + // the *class* template arguments, only on that method's template + // arguments. We could specialize this class for the single-Args case; + // we could minimize redundancy by breaking out a common base class... void push(const DataTuple& tuple) { push(tuple_cons(Clock::now(), tuple)); @@ -103,11 +104,11 @@ namespace LL push(TimeTuple(time, std::forward(args)...)); } - /// individually pass every component except the TimePoint (implies - /// now) -- could be ambiguous if the first specified template - /// parameter type is also TimePoint -- we could try to disambiguate, - /// but a simpler approach would be for the caller to explicitly - /// construct DataTuple and call that overload + /// individually pass every component except the TimePoint (implies now) + // This could be ambiguous if the first specified template parameter + // type is also TimePoint. We could try to disambiguate, but a simpler + // approach would be for the caller to explicitly construct DataTuple + // and call that overload. void push(Args&&... args) { push(Clock::now(), std::forward(args)...); @@ -199,6 +200,10 @@ namespace LL // current time. /// pop DataTuple by value + // It would be great to notice when sizeof...(Args) == 1 and directly + // return the first (only) value, instead of making pop()'s caller + // call std::get<0>(value). See push(DataTuple) remarks for why we + // haven't yet jumped through those hoops. DataTuple pop() { return tuple_cdr(popWithTime()); @@ -224,16 +229,17 @@ namespace LL { // Pick a point suitably far into the future. TimePoint until = TimePoint::clock::now() + std::chrono::hours(24); - if (tryPopUntil_(lock, until, tt)) + pop_result popped = tryPopUntil_(lock, until, tt); + if (popped == super::POPPED) return std::move(tt); - // empty and closed: throw, just as super::pop() does - if (super::mStorage.empty() && super::mClosed) + // DONE: throw, just as super::pop() does + if (popped == super::DONE) { LLTHROW(LLThreadSafeQueueInterrupt()); } - // If not empty, we've still got items to drain. - // If not closed, it's worth waiting for more items. + // WAITING: we've still got items to drain. + // EMPTY: not closed, so it's worth waiting for more items. // Either way, loop back to wait. } } @@ -252,6 +258,16 @@ namespace LL return true; } + /// for when Args has exactly one type + bool tryPop(typename std::tuple_element<1, TimeTuple>::type& value) + { + TimeTuple tt; + if (! super::tryPop(tt)) + return false; + value = std::get<1>(std::move(tt)); + return true; + } + /// tryPopFor() template bool tryPopFor(const std::chrono::duration& timeout, Tuple& tuple) @@ -278,11 +294,12 @@ namespace LL { // Use our time_point_cast to allow for 'until' that's a // time_point type other than TimePoint. - return tryPopUntil_(lock, time_point_cast(until), tuple); + return super::POPPED == + tryPopUntil_(lock, LL::time_point_cast(until), tuple); }); } - bool tryPopUntil_(lock_t& lock, const TimePoint& until, TimeTuple& tuple) + pop_result tryPopUntil_(lock_t& lock, const TimePoint& until, TimeTuple& tuple) { TimePoint adjusted = until; if (! super::mStorage.empty()) @@ -292,7 +309,14 @@ namespace LL adjusted = min(std::get<0>(super::mStorage.front()), adjusted); } // now delegate to base-class tryPopUntil_() - return super::tryPopUntil_(lock, adjusted, tuple); + pop_result popped; + while ((popped = super::tryPopUntil_(lock, adjusted, tuple)) == super::WAITING) + { + // If super::tryPopUntil_() returns WAITING, it means there's + // a head item, but it's not yet time. But it's worth looping + // back to recheck. + } + return popped; } /// tryPopUntil(DataTuple&) @@ -307,6 +331,18 @@ namespace LL return true; } + /// for when Args has exactly one type + template + bool tryPopUntil(const std::chrono::time_point& until, + typename std::tuple_element<1, TimeTuple>::type& value) + { + TimeTuple tt; + if (! tryPopUntil(until, tt)) + return false; + value = std::get<1>(std::move(tt)); + return true; + } + /*------------------------------ etc. ------------------------------*/ // We can't hide items that aren't yet ready because we can't traverse // the underlying priority_queue: it has no iterators, only top(). So -- cgit v1.3 From b554c9eaf45c83500e6b65e295cc507b9a3d537b Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 7 Oct 2021 14:00:39 -0400 Subject: SL-16024: Adapt llinstancetracker_test.cpp to getInstance() change. --- indra/llcommon/tests/llinstancetracker_test.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) (limited to 'indra/llcommon/tests') diff --git a/indra/llcommon/tests/llinstancetracker_test.cpp b/indra/llcommon/tests/llinstancetracker_test.cpp index 9b89159625..5daa29adf4 100644 --- a/indra/llcommon/tests/llinstancetracker_test.cpp +++ b/indra/llcommon/tests/llinstancetracker_test.cpp @@ -90,19 +90,19 @@ namespace tut { Keyed one("one"); ensure_equals(Keyed::instanceCount(), 1); - Keyed* found = Keyed::getInstance("one"); - ensure("couldn't find stack Keyed", found); - ensure_equals("found wrong Keyed instance", found, &one); + auto found = Keyed::getInstance("one"); + ensure("couldn't find stack Keyed", bool(found)); + ensure_equals("found wrong Keyed instance", found.get(), &one); { boost::scoped_ptr two(new Keyed("two")); ensure_equals(Keyed::instanceCount(), 2); - Keyed* found = Keyed::getInstance("two"); - ensure("couldn't find heap Keyed", found); - ensure_equals("found wrong Keyed instance", found, two.get()); + auto found = Keyed::getInstance("two"); + ensure("couldn't find heap Keyed", bool(found)); + ensure_equals("found wrong Keyed instance", found.get(), two.get()); } ensure_equals(Keyed::instanceCount(), 1); } - Keyed* found = Keyed::getInstance("one"); + auto found = Keyed::getInstance("one"); ensure("Keyed key lives too long", ! found); ensure_equals(Keyed::instanceCount(), 0); } -- cgit v1.3 From 623ac79120a417ec445ce5c106a907fe46734309 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 7 Oct 2021 15:32:51 -0400 Subject: SL-16024: Add LL::WorkQueue for passing work items between threads. A typical WorkQueue has a string name, which can be used to find it to post work to it. "Work" is a nullary callable. WorkQueue is a multi-producer, multi-consumer thread-safe queue: multiple threads can service the WorkQueue, multiple threads can post work to it. Work can be scheduled in the future by submitting with a timestamp. In addition, a given work item can be scheduled to run on a recurring basis. A requesting thread servicing a WorkQueue of its own, such as the viewer's main thread, can submit work to another WorkQueue along with a callback to be passed the result (of arbitrary type) of the first work item. The callback is posted to the originating WorkQueue, permitting safe data exchange between participating threads. Methods are provided for different kinds of servicing threads. runUntilClose() is useful for a simple worker thread. runFor(duration) devotes no more than a specified time slice to that WorkQueue, e.g. for use by the main thread. --- indra/llcommon/CMakeLists.txt | 3 + indra/llcommon/tests/workqueue_test.cpp | 158 ++++++++++++++++ indra/llcommon/threadsafeschedule.h | 1 + indra/llcommon/workqueue.cpp | 114 +++++++++++ indra/llcommon/workqueue.h | 325 ++++++++++++++++++++++++++++++++ 5 files changed, 601 insertions(+) create mode 100644 indra/llcommon/tests/workqueue_test.cpp create mode 100644 indra/llcommon/workqueue.cpp create mode 100644 indra/llcommon/workqueue.h (limited to 'indra/llcommon/tests') diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt index 5efcfabf24..a3dbb6d9d0 100644 --- a/indra/llcommon/CMakeLists.txt +++ b/indra/llcommon/CMakeLists.txt @@ -121,6 +121,7 @@ set(llcommon_SOURCE_FILES llworkerthread.cpp timing.cpp u64.cpp + workqueue.cpp StackWalker.cpp ) @@ -258,6 +259,7 @@ set(llcommon_HEADER_FILES timer.h tuple.h u64.h + workqueue.h StackWalker.h ) @@ -363,6 +365,7 @@ if (LL_TESTS) LL_ADD_INTEGRATION_TEST(stringize "" "${test_libs}") LL_ADD_INTEGRATION_TEST(threadsafeschedule "" "${test_libs}") LL_ADD_INTEGRATION_TEST(tuple "" "${test_libs}") + LL_ADD_INTEGRATION_TEST(workqueue "" "${test_libs}") ## llexception_test.cpp isn't a regression test, and doesn't need to be run ## every build. It's to help a developer make implementation choices about diff --git a/indra/llcommon/tests/workqueue_test.cpp b/indra/llcommon/tests/workqueue_test.cpp new file mode 100644 index 0000000000..ab1cae6c14 --- /dev/null +++ b/indra/llcommon/tests/workqueue_test.cpp @@ -0,0 +1,158 @@ +/** + * @file workqueue_test.cpp + * @author Nat Goodspeed + * @date 2021-10-07 + * @brief Test for workqueue. + * + * $LicenseInfo:firstyear=2021&license=viewerlgpl$ + * Copyright (c) 2021, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "workqueue.h" +// STL headers +// std headers +#include +#include +// external library headers +// other Linden headers +#include "../test/lltut.h" +#include "llcond.h" +#include "llstring.h" +#include "stringize.h" + +using namespace LL; +using namespace std::literals::chrono_literals; // ms suffix +using namespace std::literals::string_literals; // s suffix + +/***************************************************************************** +* TUT +*****************************************************************************/ +namespace tut +{ + struct workqueue_data + { + WorkQueue queue{"queue"}; + }; + typedef test_group workqueue_group; + typedef workqueue_group::object object; + workqueue_group workqueuegrp("workqueue"); + + template<> template<> + void object::test<1>() + { + set_test_name("name"); + ensure_equals("didn't capture name", queue.getKey(), "queue"); + ensure("not findable", WorkQueue::getInstance("queue") == queue.getWeak().lock()); + WorkQueue q2; + ensure("has no name", LLStringUtil::startsWith(q2.getKey(), "WorkQueue")); + } + + template<> template<> + void object::test<2>() + { + set_test_name("post"); + bool wasRun{ false }; + // We only get away with binding a simple bool because we're running + // the work on the same thread. + queue.post([&wasRun](){ wasRun = true; }); + queue.close(); + ensure("ran too soon", ! wasRun); + queue.runUntilClose(); + ensure("didn't run", wasRun); + } + + template<> template<> + void object::test<3>() + { + set_test_name("postEvery"); + // record of runs + using Shared = std::deque; + // This is an example of how to share data between the originator of + // postEvery(work) and the work item itself, since usually a WorkQueue + // is used to dispatch work to a different thread. Neither of them + // should call any of LLCond's wait methods: you don't want to stall + // either the worker thread or the originating thread (conventionally + // main). Use LLCond or a subclass even if all you want to do is + // signal the work item that it can quit; consider LLOneShotCond. + LLCond data; + auto start = WorkQueue::TimePoint::clock::now(); + auto interval = 100ms; + queue.postEvery( + interval, + [&data, count = 0] + () mutable + { + // record the timestamp at which this instance is running + data.update_one( + [](Shared& data) + { + data.push_back(WorkQueue::TimePoint::clock::now()); + }); + // by the 3rd call, return false to stop + return (++count < 3); + }); + // no convenient way to close() our queue while we've got a + // postEvery() running, so run until we think we should have exhausted + // the iterations + queue.runFor(10*interval); + // Take a copy of the captured deque. + Shared result = data.get(); + ensure_equals("called wrong number of times", result.size(), 3); + // postEvery() assumes you want the first call to happen right away. + // Inject a fake start time that's (interval) earlier than that, to + // make our too early/too late tests uniform for all entries. + result.push_front(start - interval); + for (size_t i = 1; i < result.size(); ++i) + { + auto diff = (result[i] - result[i-1]); + try + { + ensure(STRINGIZE("call " << i << " too soon"), diff >= interval); + ensure(STRINGIZE("call " << i << " too late"), diff < interval*1.5); + } + catch (const tut::failure&) + { + auto interval_ms = interval / 1ms; + auto diff_ms = diff / 1ms; + std::cerr << "interval " << interval_ms + << "ms; diff " << diff_ms << "ms" << std::endl; + throw; + } + } + } + + template<> template<> + void object::test<4>() + { + set_test_name("postTo"); + WorkQueue main("main"); + auto qptr = WorkQueue::getInstance("queue"); + int result = 0; + main.postTo( + qptr, + [](){ return 17; }, + // Note that a postTo() *callback* can safely bind a reference to + // a variable on the invoking thread, because the callback is run + // on the invoking thread. + [&result](int i){ result = i; }); + // this should post the callback to main + qptr->runOne(); + // this should run the callback + main.runOne(); + ensure_equals("failed to run int callback", result, 17); + + std::string alpha; + // postTo() handles arbitrary return types + main.postTo( + qptr, + [](){ return "abc"s; }, + [&alpha](const std::string& s){ alpha = s; }); + qptr->runPending(); + main.runPending(); + ensure_equals("failed to run string callback", alpha, "abc"); + } +} // namespace tut diff --git a/indra/llcommon/threadsafeschedule.h b/indra/llcommon/threadsafeschedule.h index 0e70d30714..c8ad23532b 100644 --- a/indra/llcommon/threadsafeschedule.h +++ b/indra/llcommon/threadsafeschedule.h @@ -78,6 +78,7 @@ namespace LL enum pop_result { EMPTY=super::EMPTY, DONE=super::DONE, WAITING=super::WAITING, POPPED=super::POPPED }; public: + using Closed = LLThreadSafeQueueInterrupt; using TimePoint = ThreadSafeSchedulePrivate::TimePoint; using Clock = TimePoint::clock; diff --git a/indra/llcommon/workqueue.cpp b/indra/llcommon/workqueue.cpp new file mode 100644 index 0000000000..15e292fb43 --- /dev/null +++ b/indra/llcommon/workqueue.cpp @@ -0,0 +1,114 @@ +/** + * @file workqueue.cpp + * @author Nat Goodspeed + * @date 2021-10-06 + * @brief Implementation for WorkQueue. + * + * $LicenseInfo:firstyear=2021&license=viewerlgpl$ + * Copyright (c) 2021, Linden Research, Inc. + * $/LicenseInfo$ + */ + +// Precompiled header +#include "linden_common.h" +// associated header +#include "workqueue.h" +// STL headers +// std headers +// external library headers +// other Linden headers +#include "llerror.h" +#include "llexception.h" +#include "stringize.h" + +LL::WorkQueue::WorkQueue(const std::string& name): + super(makeName(name)) +{ + // TODO: register for "LLApp" events so we can implicitly close() on + // viewer shutdown. +} + +void LL::WorkQueue::close() +{ + mQueue.close(); +} + +void LL::WorkQueue::runUntilClose() +{ + try + { + for (;;) + { + callWork(mQueue.pop()); + } + } + catch (const Queue::Closed&) + { + } +} + +bool LL::WorkQueue::runPending() +{ + for (Work work; mQueue.tryPop(work); ) + { + callWork(work); + } + return ! mQueue.done(); +} + +bool LL::WorkQueue::runOne() +{ + Work work; + if (mQueue.tryPop(work)) + { + callWork(work); + } + return ! mQueue.done(); +} + +bool LL::WorkQueue::runUntil(const TimePoint& until) +{ + // Should we subtract some slop to allow for typical Work execution time? + // How much slop? + Work work; + while (TimePoint::clock::now() < until && mQueue.tryPopUntil(until, work)) + { + callWork(work); + } + return ! mQueue.done(); +} + +std::string LL::WorkQueue::makeName(const std::string& name) +{ + if (! name.empty()) + return name; + + static thread_local U32 discriminator = 0; + return STRINGIZE("WorkQueue" << discriminator++); +} + +void LL::WorkQueue::callWork(const Queue::DataTuple& work) +{ + // ThreadSafeSchedule::pop() always delivers a tuple, even when + // there's only one data field per item, as for us. + callWork(std::get<0>(work)); +} + +void LL::WorkQueue::callWork(const Work& work) +{ + try + { + work(); + } + catch (...) + { + // No matter what goes wrong with any individual work item, the worker + // thread must go on! Log our own instance name with the exception. + LOG_UNHANDLED_EXCEPTION(getKey()); + } +} + +void LL::WorkQueue::error(const std::string& msg) +{ + LL_ERRS("WorkQueue") << msg << LL_ENDL; +} diff --git a/indra/llcommon/workqueue.h b/indra/llcommon/workqueue.h new file mode 100644 index 0000000000..a52f7b0e26 --- /dev/null +++ b/indra/llcommon/workqueue.h @@ -0,0 +1,325 @@ +/** + * @file workqueue.h + * @author Nat Goodspeed + * @date 2021-09-30 + * @brief Queue used for inter-thread work passing. + * + * $LicenseInfo:firstyear=2021&license=viewerlgpl$ + * Copyright (c) 2021, Linden Research, Inc. + * $/LicenseInfo$ + */ + +#if ! defined(LL_WORKQUEUE_H) +#define LL_WORKQUEUE_H + +#include "llinstancetracker.h" +#include "threadsafeschedule.h" +#include +#include // std::function +#include +#include +#include // std::pair +#include + +namespace LL +{ + /** + * A typical WorkQueue has a string name that can be used to find it. + */ + class WorkQueue: public LLInstanceTracker + { + private: + using super = LLInstanceTracker; + + public: + using Work = std::function; + + private: + using Queue = ThreadSafeSchedule; + // helper for postEvery() + template + class BackJack; + + public: + using TimePoint = Queue::TimePoint; + using TimedWork = Queue::TimeTuple; + using Closed = Queue::Closed; + + /** + * You may omit the WorkQueue name, in which case a unique name is + * synthesized; for practical purposes that makes it anonymous. + */ + WorkQueue(const std::string& name = std::string()); + + /** + * Since the point of WorkQueue is to pass work to some other worker + * thread(s) asynchronously, it's important that the WorkQueue continue + * to exist until the worker thread(s) have drained it. To communicate + * that it's time for them to quit, close() the queue. + */ + void close(); + + /*---------------------- fire and forget API -----------------------*/ + + /// fire-and-forget, but at a particular (future?) time + template + void post(const TimePoint& time, CALLABLE&& callable) + { + // Defer reifying an arbitrary CALLABLE until we hit this method. + // All other methods should accept CALLABLEs of arbitrary type to + // avoid multiple levels of std::function indirection. + mQueue.push(TimedWork(time, std::move(callable))); + } + + /// fire-and-forget + template + void post(CALLABLE&& callable) + { + // We use TimePoint::clock::now() instead of TimePoint's + // representation of the epoch because this WorkQueue may contain + // a mix of past-due TimedWork items and TimedWork items scheduled + // for the future. Sift this new item into the correct place. + post(TimePoint::clock::now(), std::move(callable)); + } + + /** + * Launch a callable returning bool that will trigger repeatedly at + * specified interval, until the callable returns false. + * + * If you need to signal that callable from outside, DO NOT bind a + * reference to a simple bool! That's not thread-safe. Instead, bind + * an LLCond variant, e.g. LLOneShotCond or LLBoolCond. + */ + template + void postEvery(const std::chrono::duration& interval, + CALLABLE&& callable); + + /*------------------------- handshake API --------------------------*/ + + /** + * Post work to another WorkQueue to be run at a specified time, + * requesting a specific callback to be run on this WorkQueue on + * completion. + * + * Returns true if able to post, false if the other WorkQueue is + * inaccessible. + */ + template + bool postTo(std::weak_ptr target, + const TimePoint& time, CALLABLE&& callable, CALLBACK&& callback) + { + // We're being asked to post to the WorkQueue at target. + // target is a weak_ptr: have to lock it to check it. + auto tptr = target.lock(); + if (! tptr) + // can't post() if the target WorkQueue has been destroyed + return false; + + // Here we believe target WorkQueue still exists. Post to it a + // lambda that packages our callable, our callback and a weak_ptr + // to this originating WorkQueue. + tptr->post( + time, + [reply = super::getWeak(), + callable = std::move(callable), + callback = std::move(callback)] + () + { + // Call the callable in any case -- but to minimize + // copying the result, immediately bind it into a reply + // lambda. The reply lambda also binds the original + // callback, so that when we, the originating WorkQueue, + // finally receive and process the reply lambda, we'll + // call the bound callback with the bound result -- on the + // same thread that originally called postTo(). + auto rlambda = + [result = callable(), + callback = std::move(callback)] + () + { callback(std::move(result)); }; + // Check if this originating WorkQueue still exists. + // Remember, the outer lambda is now running on a thread + // servicing the target WorkQueue, and real time has + // elapsed since postTo()'s tptr->post() call. + // reply is a weak_ptr: have to lock it to check it. + auto rptr = reply.lock(); + if (rptr) + { + // Only post reply lambda if the originating WorkQueue + // still exists. If not -- who would we tell? Log it? + try + { + rptr->post(std::move(rlambda)); + } + catch (const Closed&) + { + // Originating WorkQueue might still exist, but + // might be Closed. Same thing: just discard the + // callback. + } + } + }); + // looks like we were able to post() + return true; + } + + /** + * Post work to another WorkQueue, requesting a specific callback to + * be run on this WorkQueue on completion. + * + * Returns true if able to post, false if the other WorkQueue is + * inaccessible. + */ + template + bool postTo(std::weak_ptr target, + CALLABLE&& callable, CALLBACK&& callback) + { + return postTo(target, TimePoint::clock::now(), std::move(callable), std::move(callback)); + } + + /*--------------------------- worker API ---------------------------*/ + + /** + * runUntilClose() pulls TimedWork items off this WorkQueue until the + * queue is closed, at which point it returns. This would be the + * typical entry point for a simple worker thread. + */ + void runUntilClose(); + + /** + * runPending() runs all TimedWork items that are ready to run. It + * returns true if the queue remains open, false if the queue has been + * closed. This could be used by a thread whose primary purpose is to + * serve the queue, but also wants to do other things with its idle time. + */ + bool runPending(); + + /** + * runOne() runs at most one ready TimedWork item -- zero if none are + * ready. It returns true if the queue remains open, false if the + * queue has been closed. + */ + bool runOne(); + + /** + * runFor() runs a subset of ready TimedWork items, until the + * timeslice has been exceeded. It returns true if the queue remains + * open, false if the queue has been closed. This could be used by a + * busy main thread to lend a bounded few CPU cycles to this WorkQueue + * without risking the WorkQueue blowing out the length of any one + * frame. + */ + template + bool runFor(const std::chrono::duration& timeslice) + { + return runUntil(TimePoint::clock::now() + timeslice); + } + + /** + * runUntil() is just like runFor(), only with a specific end time + * instead of a timeslice duration. + */ + bool runUntil(const TimePoint& until); + + private: + static void error(const std::string& msg); + static std::string makeName(const std::string& name); + void callWork(const Queue::DataTuple& work); + void callWork(const Work& work); + Queue mQueue; + }; + + /** + * BackJack is, in effect, a hand-rolled lambda, binding a WorkQueue, a + * CALLABLE that returns bool, a TimePoint and an interval at which to + * relaunch it. As long as the callable continues returning true, BackJack + * keeps resubmitting it to the target WorkQueue. + */ + // Why is BackJack a class and not a lambda? Because, unlike a lambda, a + // class method gets its own 'this' pointer -- which we need to resubmit + // the whole BackJack callable. + template + class WorkQueue::BackJack + { + public: + // bind the desired data + BackJack(std::weak_ptr target, + const WorkQueue::TimePoint& start, + const std::chrono::duration& interval, + CALLABLE&& callable): + mTarget(target), + mStart(start), + mInterval(interval), + mCallable(std::move(callable)) + {} + + // Call by target WorkQueue -- note that although WE require a + // callable returning bool, WorkQueue wants a void callable. We + // consume the bool. + void operator()() + { + // If mCallable() throws an exception, don't catch it here: if it + // throws once, it's likely to throw every time, so it's a waste + // of time to arrange to call it again. + if (mCallable()) + { + // Modify mStart to the new start time we desire. If we simply + // added mInterval to now, we'd get actual timings of + // (mInterval + slop), where 'slop' is the latency between the + // previous mStart and the WorkQueue actually calling us. + // Instead, add mInterval to mStart so that at least we + // register our intent to fire at exact mIntervals. + mStart += mInterval; + + // We're being called at this moment by the target WorkQueue. + // Assume it still exists, rather than checking the result of + // lock(). + // Resubmit the whole *this callable: that's why we're a class + // rather than a lambda. Allow moving *this so we can carry a + // move-only callable; but naturally this statement must be + // the last time we reference this instance, which may become + // moved-from. + try + { + mTarget.lock()->post(mStart, std::move(*this)); + } + catch (const Closed&) + { + // Once this queue is closed, oh well, just stop + } + } + } + + private: + std::weak_ptr mTarget; + WorkQueue::TimePoint mStart; + std::chrono::duration mInterval; + CALLABLE mCallable; + }; + + template + void WorkQueue::postEvery(const std::chrono::duration& interval, + CALLABLE&& callable) + { + if (interval.count() <= 0) + { + // It's essential that postEvery() be called with a positive + // interval, since each call to BackJack posts another instance of + // itself at (start + interval) and we order by target time. A + // zero or negative interval would result in that BackJack + // instance going to the head of the queue every time, immediately + // ready to run. Effectively that would produce an infinite loop, + // a denial of service on this WorkQueue. + error("postEvery(interval) may not be 0"); + } + // Instantiate and post a suitable BackJack, binding a weak_ptr to + // self, the current time, the desired interval and the desired + // callable. + post( + BackJack( + getWeak(), TimePoint::clock::now(), interval, std::move(callable))); + } + +} // namespace LL + +#endif /* ! defined(LL_WORKQUEUE_H) */ -- cgit v1.3 From c585ddb75e383cdd994d0d99fed8f2de8f955e3c Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 7 Oct 2021 16:45:15 -0400 Subject: SL-16024: Defend against two threads making "anonymous" WorkQueues. Also make workqueue_test.cpp more robust. --- indra/llcommon/tests/workqueue_test.cpp | 11 ++++++----- indra/llcommon/workqueue.cpp | 18 ++++++++++++++++-- 2 files changed, 22 insertions(+), 7 deletions(-) (limited to 'indra/llcommon/tests') diff --git a/indra/llcommon/tests/workqueue_test.cpp b/indra/llcommon/tests/workqueue_test.cpp index ab1cae6c14..d5405400fd 100644 --- a/indra/llcommon/tests/workqueue_test.cpp +++ b/indra/llcommon/tests/workqueue_test.cpp @@ -103,12 +103,13 @@ namespace tut Shared result = data.get(); ensure_equals("called wrong number of times", result.size(), 3); // postEvery() assumes you want the first call to happen right away. - // Inject a fake start time that's (interval) earlier than that, to - // make our too early/too late tests uniform for all entries. - result.push_front(start - interval); - for (size_t i = 1; i < result.size(); ++i) + // Pretend our start time was (interval) earlier than that, to make + // our too early/too late tests uniform for all entries. + start -= interval; + for (size_t i = 0; i < result.size(); ++i) { - auto diff = (result[i] - result[i-1]); + auto diff = result[i] - start; + start += interval; try { ensure(STRINGIZE("call " << i << " too soon"), diff >= interval); diff --git a/indra/llcommon/workqueue.cpp b/indra/llcommon/workqueue.cpp index 15e292fb43..ffc9a97dc0 100644 --- a/indra/llcommon/workqueue.cpp +++ b/indra/llcommon/workqueue.cpp @@ -17,10 +17,15 @@ // std headers // external library headers // other Linden headers +#include "llcoros.h" +#include LLCOROS_MUTEX_HEADER #include "llerror.h" #include "llexception.h" #include "stringize.h" +using Mutex = LLCoros::Mutex; +using Lock = LLCoros::LockType; + LL::WorkQueue::WorkQueue(const std::string& name): super(makeName(name)) { @@ -83,8 +88,17 @@ std::string LL::WorkQueue::makeName(const std::string& name) if (! name.empty()) return name; - static thread_local U32 discriminator = 0; - return STRINGIZE("WorkQueue" << discriminator++); + static U32 discriminator = 0; + static Mutex mutex; + U32 num; + { + // Protect discriminator from concurrent access by different threads. + // It can't be thread_local, else two racing threads will come up with + // the same name. + Lock lk(mutex); + num = discriminator++; + } + return STRINGIZE("WorkQueue" << num); } void LL::WorkQueue::callWork(const Queue::DataTuple& work) -- cgit v1.3 From e7b8c27741201528bf78f95c96ba820833923dab Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Mon, 25 Oct 2021 15:55:49 -0400 Subject: SL-16220: Specialize WorkQueue for callable with void return. Add a test exercising this feature. --- indra/llcommon/tests/threadsafeschedule_test.cpp | 4 +- indra/llcommon/tests/workqueue_test.cpp | 23 +++- indra/llcommon/workqueue.h | 167 +++++++++++++++-------- 3 files changed, 134 insertions(+), 60 deletions(-) (limited to 'indra/llcommon/tests') diff --git a/indra/llcommon/tests/threadsafeschedule_test.cpp b/indra/llcommon/tests/threadsafeschedule_test.cpp index af67b9f492..c421cc7b1c 100644 --- a/indra/llcommon/tests/threadsafeschedule_test.cpp +++ b/indra/llcommon/tests/threadsafeschedule_test.cpp @@ -46,11 +46,11 @@ namespace tut // the real time required for each push() call. Explicitly increment // the timestamp for each one -- but since we're passing explicit // timestamps, make the queue reorder them. - queue.push(Queue::TimeTuple(Queue::Clock::now() + 20ms, "ghi")); + queue.push(Queue::TimeTuple(Queue::Clock::now() + 200ms, "ghi")); // Given the various push() overloads, you have to match the type // exactly: conversions are ambiguous. queue.push("abc"s); - queue.push(Queue::Clock::now() + 10ms, "def"); + queue.push(Queue::Clock::now() + 100ms, "def"); queue.close(); auto entry = queue.pop(); ensure_equals("failed to pop first", std::get<0>(entry), "abc"s); diff --git a/indra/llcommon/tests/workqueue_test.cpp b/indra/llcommon/tests/workqueue_test.cpp index d5405400fd..b69df49d33 100644 --- a/indra/llcommon/tests/workqueue_test.cpp +++ b/indra/llcommon/tests/workqueue_test.cpp @@ -138,7 +138,8 @@ namespace tut [](){ return 17; }, // Note that a postTo() *callback* can safely bind a reference to // a variable on the invoking thread, because the callback is run - // on the invoking thread. + // on the invoking thread. (Of course the bound variable must + // survive until the callback is called.) [&result](int i){ result = i; }); // this should post the callback to main qptr->runOne(); @@ -156,4 +157,24 @@ namespace tut main.runPending(); ensure_equals("failed to run string callback", alpha, "abc"); } + + template<> template<> + void object::test<5>() + { + set_test_name("postTo with void return"); + WorkQueue main("main"); + auto qptr = WorkQueue::getInstance("queue"); + std::string observe; + main.postTo( + qptr, + // The ONLY reason we can get away with binding a reference to + // 'observe' in our work callable is because we're directly + // calling qptr->runOne() on this same thread. It would be a + // mistake to do that if some other thread were servicing 'queue'. + [&observe](){ observe = "queue"; }, + [&observe](){ observe.append(";main"); }); + qptr->runOne(); + main.runOne(); + ensure_equals("failed to run both lambdas", observe, "queue;main"); + } } // namespace tut diff --git a/indra/llcommon/workqueue.h b/indra/llcommon/workqueue.h index cfae2019dc..deef3c8e84 100644 --- a/indra/llcommon/workqueue.h +++ b/indra/llcommon/workqueue.h @@ -115,62 +115,7 @@ namespace LL // code. template bool postTo(WorkQueue::weak_t target, - const TimePoint& time, CALLABLE&& callable, FOLLOWUP&& callback) - { - // We're being asked to post to the WorkQueue at target. - // target is a weak_ptr: have to lock it to check it. - auto tptr = target.lock(); - if (! tptr) - // can't post() if the target WorkQueue has been destroyed - return false; - - // Here we believe target WorkQueue still exists. Post to it a - // lambda that packages our callable, our callback and a weak_ptr - // to this originating WorkQueue. - tptr->post( - time, - [reply = super::getWeak(), - callable = std::move(callable), - callback = std::move(callback)] - () - { - // Call the callable in any case -- but to minimize - // copying the result, immediately bind it into a reply - // lambda. The reply lambda also binds the original - // callback, so that when we, the originating WorkQueue, - // finally receive and process the reply lambda, we'll - // call the bound callback with the bound result -- on the - // same thread that originally called postTo(). - auto rlambda = - [result = callable(), - callback = std::move(callback)] - () - { callback(std::move(result)); }; - // Check if this originating WorkQueue still exists. - // Remember, the outer lambda is now running on a thread - // servicing the target WorkQueue, and real time has - // elapsed since postTo()'s tptr->post() call. - // reply is a weak_ptr: have to lock it to check it. - auto rptr = reply.lock(); - if (rptr) - { - // Only post reply lambda if the originating WorkQueue - // still exists. If not -- who would we tell? Log it? - try - { - rptr->post(std::move(rlambda)); - } - catch (const Closed&) - { - // Originating WorkQueue might still exist, but - // might be Closed. Same thing: just discard the - // callback. - } - } - }); - // looks like we were able to post() - return true; - } + const TimePoint& time, CALLABLE&& callable, FOLLOWUP&& callback); /** * Post work to another WorkQueue, requesting a specific callback to @@ -183,7 +128,8 @@ namespace LL bool postTo(WorkQueue::weak_t target, CALLABLE&& callable, FOLLOWUP&& callback) { - return postTo(target, TimePoint::clock::now(), std::move(callable), std::move(callback)); + return postTo(target, TimePoint::clock::now(), + std::move(callable), std::move(callback)); } /*--------------------------- worker API ---------------------------*/ @@ -231,6 +177,17 @@ namespace LL bool runUntil(const TimePoint& until); private: + template + static auto makeReplyLambda(CALLABLE&& callable, FOLLOWUP&& callback); + + /// general case: arbitrary C++ return type + template + struct MakeReplyLambda; + + /// specialize for CALLABLE returning void + template + struct MakeReplyLambda; + static void error(const std::string& msg); static std::string makeName(const std::string& name); void callWork(const Queue::DataTuple& work); @@ -329,6 +286,102 @@ namespace LL getWeak(), TimePoint::clock::now(), interval, std::move(callable))); } + template + struct WorkQueue::MakeReplyLambda + { + auto operator()(CALLABLE&& callable, FOLLOWUP&& callback) + { + // Call the callable in any case -- but to minimize + // copying the result, immediately bind it into the reply + // lambda. The reply lambda also binds the original + // callback, so that when we, the originating WorkQueue, + // finally receive and process the reply lambda, we'll + // call the bound callback with the bound result -- on the + // same thread that originally called postTo(). + return + [result = std::forward(callable)(), + callback = std::move(callback)] + () + { callback(std::move(result)); }; + } + }; + + /// specialize for CALLABLE returning void + template + struct WorkQueue::MakeReplyLambda + { + auto operator()(CALLABLE&& callable, FOLLOWUP&& callback) + { + // Call the callable, which produces no result. + std::forward(callable)(); + // This reply lambda binds the original callback, so + // that when we, the originating WorkQueue, finally + // receive and process the reply lambda, we'll call + // the bound callback -- on the same thread that + // originally called postTo(). + return [callback = std::move(callback)](){ callback(); }; + } + }; + + template + auto WorkQueue::makeReplyLambda(CALLABLE&& callable, FOLLOWUP&& callback) + { + return MakeReplyLambda(callable)())>() + (std::move(callable), std::move(callback)); + } + + template + bool WorkQueue::postTo(WorkQueue::weak_t target, + const TimePoint& time, CALLABLE&& callable, FOLLOWUP&& callback) + { + // We're being asked to post to the WorkQueue at target. + // target is a weak_ptr: have to lock it to check it. + auto tptr = target.lock(); + if (! tptr) + // can't post() if the target WorkQueue has been destroyed + return false; + + // Here we believe target WorkQueue still exists. Post to it a + // lambda that packages our callable, our callback and a weak_ptr + // to this originating WorkQueue. + tptr->post( + time, + [reply = super::getWeak(), + callable = std::move(callable), + callback = std::move(callback)] + () + { + // Make a reply lambda to repost to THIS WorkQueue. + // Delegate to makeReplyLambda() so we can partially + // specialize on void return. + auto rlambda = makeReplyLambda(std::move(callable), std::move(callback)); + // Check if this originating WorkQueue still exists. + // Remember, the outer lambda is now running on a thread + // servicing the target WorkQueue, and real time has + // elapsed since postTo()'s tptr->post() call. + // reply is a weak_ptr: have to lock it to check it. + auto rptr = reply.lock(); + if (rptr) + { + // Only post reply lambda if the originating WorkQueue + // still exists. If not -- who would we tell? Log it? + try + { + rptr->post(std::move(rlambda)); + } + catch (const Closed&) + { + // Originating WorkQueue might still exist, but + // might be Closed. Same thing: just discard the + // callback. + } + } + }); + // looks like we were able to post() + return true; + } + } // namespace LL #endif /* ! defined(LL_WORKQUEUE_H) */ -- cgit v1.3 From 8b16ecb9cfb4917fe38e4e5b0e4f40a23dd4ffbf Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 27 Oct 2021 15:31:54 -0400 Subject: SL-16220: Add tests for WorkQueue::waitForResult(), void & non-void. --- indra/llcommon/tests/workqueue_test.cpp | 49 +++++++++++++++++++++++++++++++++ indra/llcommon/workqueue.h | 38 ++++++++++++------------- 2 files changed, 68 insertions(+), 19 deletions(-) (limited to 'indra/llcommon/tests') diff --git a/indra/llcommon/tests/workqueue_test.cpp b/indra/llcommon/tests/workqueue_test.cpp index b69df49d33..bea3ad911b 100644 --- a/indra/llcommon/tests/workqueue_test.cpp +++ b/indra/llcommon/tests/workqueue_test.cpp @@ -20,7 +20,10 @@ // external library headers // other Linden headers #include "../test/lltut.h" +#include "../test/catch_and_store_what_in.h" #include "llcond.h" +#include "llcoros.h" +#include "lleventcoro.h" #include "llstring.h" #include "stringize.h" @@ -177,4 +180,50 @@ namespace tut main.runOne(); ensure_equals("failed to run both lambdas", observe, "queue;main"); } + + template<> template<> + void object::test<6>() + { + set_test_name("waitForResult"); + std::string stored; + // Try to call waitForResult() on this thread's main coroutine. It + // should throw because the main coroutine must service the queue. + auto what{ catch_what( + [this, &stored](){ stored = queue.waitForResult( + [](){ return "should throw"; }); }) }; + ensure("lambda should not have run", stored.empty()); + ensure_not("waitForResult() should have thrown", what.empty()); + ensure(STRINGIZE("should mention waitForResult: " << what), + what.find("waitForResult") != std::string::npos); + + // Call waitForResult() on a coroutine, with a string result. + LLCoros::instance().launch( + "waitForResult string", + [this, &stored]() + { stored = queue.waitForResult( + [](){ return "string result"; }); }); + llcoro::suspend(); + // Nothing will have happened yet because, even if the coroutine did + // run immediately, all it did was to queue the inner lambda on + // 'queue'. Service it. + queue.runOne(); + llcoro::suspend(); + ensure_equals("bad waitForResult return", stored, "string result"); + + // Call waitForResult() on a coroutine, with a void callable. + stored.clear(); + bool done = false; + LLCoros::instance().launch( + "waitForResult void", + [this, &stored, &done]() + { + queue.waitForResult([&stored](){ stored = "ran"; }); + done = true; + }); + llcoro::suspend(); + queue.runOne(); + llcoro::suspend(); + ensure_equals("didn't run coroutine", stored, "ran"); + ensure("void waitForResult() didn't return", done); + } } // namespace tut diff --git a/indra/llcommon/workqueue.h b/indra/llcommon/workqueue.h index 42f5d78ba3..7dbc735c6d 100644 --- a/indra/llcommon/workqueue.h +++ b/indra/llcommon/workqueue.h @@ -92,6 +92,25 @@ namespace LL post(TimePoint::clock::now(), std::move(callable)); } + /** + * Post work to be run at a specified time to another WorkQueue, which + * may or may not still exist and be open. Return true if we were able + * to post. + */ + template + static bool postMaybe(weak_t target, const TimePoint& time, CALLABLE&& callable); + + /** + * Post work to another WorkQueue, which may or may not still exist + * and be open. Return true if we were able to post. + */ + template + static bool postMaybe(weak_t target, CALLABLE&& callable) + { + return postMaybe(target, TimePoint::clock::now(), + std::forward(callable)); + } + /** * Launch a callable returning bool that will trigger repeatedly at * specified interval, until the callable returns false. @@ -136,25 +155,6 @@ namespace LL std::move(callable), std::move(callback)); } - /** - * Post work to be run at a specified time to another WorkQueue, which - * may or may not still exist and be open. Return true if we were able - * to post. - */ - template - static bool postMaybe(weak_t target, const TimePoint& time, CALLABLE&& callable); - - /** - * Post work to another WorkQueue, which may or may not still exist - * and be open. Return true if we were able to post. - */ - template - static bool postMaybe(weak_t target, CALLABLE&& callable) - { - return postMaybe(target, TimePoint::clock::now(), - std::forward(callable)); - } - /** * Post work to another WorkQueue to be run at a specified time, * blocking the calling coroutine until then, returning the result to -- cgit v1.3 From 029b41c0419e975bbb28454538b46dc69ce5d2ba Mon Sep 17 00:00:00 2001 From: Dave Houlton Date: Mon, 15 Nov 2021 09:25:35 -0700 Subject: Revert "SL-16220: Merge branch 'origin/DRTVWR-546' into glthread" This reverts commit 5188a26a8521251dda07ac0140bb129f28417e49, reversing changes made to 819088563e13f1d75e048311fbaf0df4a79b7e19. --- indra/llcommon/CMakeLists.txt | 3 +- indra/llcommon/llsingleton.h | 24 +- indra/llcommon/llthreadsafequeue.h | 30 +- indra/llcommon/tests/threadsafeschedule_test.cpp | 4 +- indra/llcommon/tests/workqueue_test.cpp | 72 +--- indra/llcommon/threadpool.cpp | 80 ----- indra/llcommon/threadpool.h | 62 ---- indra/llcommon/timing.cpp | 25 ++ indra/llcommon/workqueue.cpp | 30 +- indra/llcommon/workqueue.h | 378 ++++---------------- indra/llrender/llimagegl.cpp | 91 ++++- indra/llrender/llimagegl.h | 27 +- indra/llui/CMakeLists.txt | 6 +- indra/llui/llviewereventrecorder.cpp | 2 + indra/llwindow/llwindowwin32.cpp | 426 ++++++++++++++++------- indra/llwindow/llwindowwin32.h | 48 ++- indra/newview/CMakeLists.txt | 3 +- indra/newview/app_settings/settings.xml | 25 -- indra/newview/llappviewer.cpp | 66 ++-- indra/newview/llenvironment.cpp | 1 + indra/newview/llmainlooprepeater.cpp | 88 +++++ indra/newview/llmainlooprepeater.h | 64 ++++ indra/newview/llselectmgr.cpp | 2 + indra/newview/llstartup.cpp | 20 -- indra/newview/llviewercamera.cpp | 2 + indra/newview/llviewertexture.cpp | 34 +- indra/newview/llviewertexture.h | 4 - indra/newview/llworld.cpp | 2 + 28 files changed, 770 insertions(+), 849 deletions(-) delete mode 100644 indra/llcommon/threadpool.cpp delete mode 100644 indra/llcommon/threadpool.h create mode 100644 indra/llcommon/timing.cpp create mode 100644 indra/newview/llmainlooprepeater.cpp create mode 100644 indra/newview/llmainlooprepeater.h (limited to 'indra/llcommon/tests') diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt index 78d6ea3090..ad6d3a5049 100644 --- a/indra/llcommon/CMakeLists.txt +++ b/indra/llcommon/CMakeLists.txt @@ -119,8 +119,8 @@ set(llcommon_SOURCE_FILES lluriparser.cpp lluuid.cpp llworkerthread.cpp + timing.cpp u64.cpp - threadpool.cpp workqueue.cpp StackWalker.cpp ) @@ -256,7 +256,6 @@ set(llcommon_HEADER_FILES lockstatic.h stdtypes.h stringize.h - threadpool.h threadsafeschedule.h timer.h tuple.h diff --git a/indra/llcommon/llsingleton.h b/indra/llcommon/llsingleton.h index 6042c0906c..10a8ecfedb 100644 --- a/indra/llcommon/llsingleton.h +++ b/indra/llcommon/llsingleton.h @@ -847,28 +847,22 @@ template class LLSimpleton { public: - template - static void createInstance(ARGS&&... args) - { + static T* sInstance; + + static void createInstance() + { llassert(sInstance == nullptr); - sInstance = new T(std::forward(args)...); + sInstance = new T(); } - + static inline T* getInstance() { return sInstance; } static inline T& instance() { return *getInstance(); } static inline bool instanceExists() { return sInstance != nullptr; } - static void deleteSingleton() - { - delete sInstance; - sInstance = nullptr; + static void deleteSingleton() { + delete sInstance; + sInstance = nullptr; } - -private: - static T* sInstance; }; -template -T* LLSimpleton::sInstance{ nullptr }; - #endif diff --git a/indra/llcommon/llthreadsafequeue.h b/indra/llcommon/llthreadsafequeue.h index 5c934791fe..06e8d8f609 100644 --- a/indra/llcommon/llthreadsafequeue.h +++ b/indra/llcommon/llthreadsafequeue.h @@ -85,8 +85,8 @@ public: LLThreadSafeQueue(U32 capacity = 1024); virtual ~LLThreadSafeQueue() {} - // Add an element to the queue (will block if the queue has reached - // capacity). + // Add an element to the queue (will block if the queue has + // reached capacity). // // This call will raise an interrupt error if the queue is closed while // the caller is blocked. @@ -95,11 +95,6 @@ public: // legacy name void pushFront(ElementT const & element) { return push(element); } - // Add an element to the queue (will block if the queue has reached - // capacity). Return false if the queue is closed before push is possible. - template - bool pushIfOpen(T&& element); - // Try to add an element to the queue without blocking. Returns // true only if the element was actually added. template @@ -316,8 +311,8 @@ bool LLThreadSafeQueue::push_(lock_t& lock, T&& element) template -template -bool LLThreadSafeQueue::pushIfOpen(T&& element) +template +void LLThreadSafeQueue::push(T&& element) { lock_t lock1(mLock); while (true) @@ -326,10 +321,12 @@ bool LLThreadSafeQueue::pushIfOpen(T&& element) // drained or not: the moment either end calls close(), further push() // operations will fail. if (mClosed) - return false; + { + LLTHROW(LLThreadSafeQueueInterrupt()); + } if (push_(lock1, std::forward(element))) - return true; + return; // Storage Full. Wait for signal. mCapacityCond.wait(lock1); @@ -337,17 +334,6 @@ bool LLThreadSafeQueue::pushIfOpen(T&& element) } -template -template -void LLThreadSafeQueue::push(T&& element) -{ - if (! pushIfOpen(std::forward(element))) - { - LLTHROW(LLThreadSafeQueueInterrupt()); - } -} - - template template bool LLThreadSafeQueue::tryPush(T&& element) diff --git a/indra/llcommon/tests/threadsafeschedule_test.cpp b/indra/llcommon/tests/threadsafeschedule_test.cpp index c421cc7b1c..af67b9f492 100644 --- a/indra/llcommon/tests/threadsafeschedule_test.cpp +++ b/indra/llcommon/tests/threadsafeschedule_test.cpp @@ -46,11 +46,11 @@ namespace tut // the real time required for each push() call. Explicitly increment // the timestamp for each one -- but since we're passing explicit // timestamps, make the queue reorder them. - queue.push(Queue::TimeTuple(Queue::Clock::now() + 200ms, "ghi")); + queue.push(Queue::TimeTuple(Queue::Clock::now() + 20ms, "ghi")); // Given the various push() overloads, you have to match the type // exactly: conversions are ambiguous. queue.push("abc"s); - queue.push(Queue::Clock::now() + 100ms, "def"); + queue.push(Queue::Clock::now() + 10ms, "def"); queue.close(); auto entry = queue.pop(); ensure_equals("failed to pop first", std::get<0>(entry), "abc"s); diff --git a/indra/llcommon/tests/workqueue_test.cpp b/indra/llcommon/tests/workqueue_test.cpp index bea3ad911b..d5405400fd 100644 --- a/indra/llcommon/tests/workqueue_test.cpp +++ b/indra/llcommon/tests/workqueue_test.cpp @@ -20,10 +20,7 @@ // external library headers // other Linden headers #include "../test/lltut.h" -#include "../test/catch_and_store_what_in.h" #include "llcond.h" -#include "llcoros.h" -#include "lleventcoro.h" #include "llstring.h" #include "stringize.h" @@ -141,8 +138,7 @@ namespace tut [](){ return 17; }, // Note that a postTo() *callback* can safely bind a reference to // a variable on the invoking thread, because the callback is run - // on the invoking thread. (Of course the bound variable must - // survive until the callback is called.) + // on the invoking thread. [&result](int i){ result = i; }); // this should post the callback to main qptr->runOne(); @@ -160,70 +156,4 @@ namespace tut main.runPending(); ensure_equals("failed to run string callback", alpha, "abc"); } - - template<> template<> - void object::test<5>() - { - set_test_name("postTo with void return"); - WorkQueue main("main"); - auto qptr = WorkQueue::getInstance("queue"); - std::string observe; - main.postTo( - qptr, - // The ONLY reason we can get away with binding a reference to - // 'observe' in our work callable is because we're directly - // calling qptr->runOne() on this same thread. It would be a - // mistake to do that if some other thread were servicing 'queue'. - [&observe](){ observe = "queue"; }, - [&observe](){ observe.append(";main"); }); - qptr->runOne(); - main.runOne(); - ensure_equals("failed to run both lambdas", observe, "queue;main"); - } - - template<> template<> - void object::test<6>() - { - set_test_name("waitForResult"); - std::string stored; - // Try to call waitForResult() on this thread's main coroutine. It - // should throw because the main coroutine must service the queue. - auto what{ catch_what( - [this, &stored](){ stored = queue.waitForResult( - [](){ return "should throw"; }); }) }; - ensure("lambda should not have run", stored.empty()); - ensure_not("waitForResult() should have thrown", what.empty()); - ensure(STRINGIZE("should mention waitForResult: " << what), - what.find("waitForResult") != std::string::npos); - - // Call waitForResult() on a coroutine, with a string result. - LLCoros::instance().launch( - "waitForResult string", - [this, &stored]() - { stored = queue.waitForResult( - [](){ return "string result"; }); }); - llcoro::suspend(); - // Nothing will have happened yet because, even if the coroutine did - // run immediately, all it did was to queue the inner lambda on - // 'queue'. Service it. - queue.runOne(); - llcoro::suspend(); - ensure_equals("bad waitForResult return", stored, "string result"); - - // Call waitForResult() on a coroutine, with a void callable. - stored.clear(); - bool done = false; - LLCoros::instance().launch( - "waitForResult void", - [this, &stored, &done]() - { - queue.waitForResult([&stored](){ stored = "ran"; }); - done = true; - }); - llcoro::suspend(); - queue.runOne(); - llcoro::suspend(); - ensure_equals("didn't run coroutine", stored, "ran"); - ensure("void waitForResult() didn't return", done); - } } // namespace tut diff --git a/indra/llcommon/threadpool.cpp b/indra/llcommon/threadpool.cpp deleted file mode 100644 index cf25cc838e..0000000000 --- a/indra/llcommon/threadpool.cpp +++ /dev/null @@ -1,80 +0,0 @@ -/** - * @file threadpool.cpp - * @author Nat Goodspeed - * @date 2021-10-21 - * @brief Implementation for threadpool. - * - * $LicenseInfo:firstyear=2021&license=viewerlgpl$ - * Copyright (c) 2021, Linden Research, Inc. - * $/LicenseInfo$ - */ - -// Precompiled header -#include "linden_common.h" -// associated header -#include "threadpool.h" -// STL headers -// std headers -// external library headers -// other Linden headers -#include "llerror.h" -#include "llevents.h" -#include "stringize.h" - -LL::ThreadPool::ThreadPool(const std::string& name, size_t threads, size_t capacity): - mQueue(name, capacity), - mName("ThreadPool:" + name) -{ - for (size_t i = 0; i < threads; ++i) - { - std::string tname{ STRINGIZE(mName << ':' << (i+1) << '/' << threads) }; - mThreads.emplace_back(tname, [this, tname](){ run(tname); }); - } - // Listen on "LLApp", and when the app is shutting down, close the queue - // and join the workers. - LLEventPumps::instance().obtain("LLApp").listen( - mName, - [this](const LLSD& stat) - { - std::string status(stat["status"]); - if (status != "running") - { - // viewer is starting shutdown -- proclaim the end is nigh! - LL_DEBUGS("ThreadPool") << mName << " saw " << status << LL_ENDL; - close(); - } - return false; - }); -} - -LL::ThreadPool::~ThreadPool() -{ - close(); -} - -void LL::ThreadPool::close() -{ - if (! mQueue.isClosed()) - { - LL_DEBUGS("ThreadPool") << mName << " closing queue and joining threads" << LL_ENDL; - mQueue.close(); - for (auto& pair: mThreads) - { - LL_DEBUGS("ThreadPool") << mName << " waiting on thread " << pair.first << LL_ENDL; - pair.second.join(); - } - LL_DEBUGS("ThreadPool") << mName << " shutdown complete" << LL_ENDL; - } -} - -void LL::ThreadPool::run(const std::string& name) -{ - LL_DEBUGS("ThreadPool") << name << " starting" << LL_ENDL; - run(); - LL_DEBUGS("ThreadPool") << name << " stopping" << LL_ENDL; -} - -void LL::ThreadPool::run() -{ - mQueue.runUntilClose(); -} diff --git a/indra/llcommon/threadpool.h b/indra/llcommon/threadpool.h deleted file mode 100644 index 1ca24aec58..0000000000 --- a/indra/llcommon/threadpool.h +++ /dev/null @@ -1,62 +0,0 @@ -/** - * @file threadpool.h - * @author Nat Goodspeed - * @date 2021-10-21 - * @brief ThreadPool configures a WorkQueue along with a pool of threads to - * service it. - * - * $LicenseInfo:firstyear=2021&license=viewerlgpl$ - * Copyright (c) 2021, Linden Research, Inc. - * $/LicenseInfo$ - */ - -#if ! defined(LL_THREADPOOL_H) -#define LL_THREADPOOL_H - -#include "workqueue.h" -#include -#include -#include // std::pair -#include - -namespace LL -{ - - class ThreadPool - { - public: - /** - * Pass ThreadPool a string name. This can be used to look up the - * relevant WorkQueue. - */ - ThreadPool(const std::string& name, size_t threads=1, size_t capacity=1024); - virtual ~ThreadPool(); - - /** - * ThreadPool listens for application shutdown messages on the "LLApp" - * LLEventPump. Call close() to shut down this ThreadPool early. - */ - void close(); - - std::string getName() const { return mName; } - size_t getWidth() const { return mThreads.size(); } - /// obtain a non-const reference to the WorkQueue to post work to it - WorkQueue& getQueue() { return mQueue; } - - /** - * Override run() if you need special processing. The default run() - * implementation simply calls WorkQueue::runUntilClose(). - */ - virtual void run(); - - private: - void run(const std::string& name); - - WorkQueue mQueue; - std::string mName; - std::vector> mThreads; - }; - -} // namespace LL - -#endif /* ! defined(LL_THREADPOOL_H) */ diff --git a/indra/llcommon/timing.cpp b/indra/llcommon/timing.cpp new file mode 100644 index 0000000000..c2dc695ef3 --- /dev/null +++ b/indra/llcommon/timing.cpp @@ -0,0 +1,25 @@ +/** + * @file timing.cpp + * @brief This file will be deprecated in the future. + * + * $LicenseInfo:firstyear=2000&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2010, Linden Research, Inc. + * + * 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 + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ diff --git a/indra/llcommon/workqueue.cpp b/indra/llcommon/workqueue.cpp index 633594ceea..b32357e832 100644 --- a/indra/llcommon/workqueue.cpp +++ b/indra/llcommon/workqueue.cpp @@ -26,9 +26,8 @@ using Mutex = LLCoros::Mutex; using Lock = LLCoros::LockType; -LL::WorkQueue::WorkQueue(const std::string& name, size_t capacity): - super(makeName(name)), - mQueue(capacity) +LL::WorkQueue::WorkQueue(const std::string& name): + super(makeName(name)) { // TODO: register for "LLApp" events so we can implicitly close() on // viewer shutdown. @@ -39,21 +38,6 @@ void LL::WorkQueue::close() mQueue.close(); } -size_t LL::WorkQueue::size() -{ - return mQueue.size(); -} - -bool LL::WorkQueue::isClosed() -{ - return mQueue.isClosed(); -} - -bool LL::WorkQueue::done() -{ - return mQueue.done(); -} - void LL::WorkQueue::runUntilClose() { try @@ -144,13 +128,3 @@ void LL::WorkQueue::error(const std::string& msg) { LL_ERRS("WorkQueue") << msg << LL_ENDL; } - -void LL::WorkQueue::checkCoroutine(const std::string& method) -{ - // By convention, the default coroutine on each thread has an empty name - // string. See also LLCoros::logname(). - if (LLCoros::getName().empty()) - { - LLTHROW(Error("Do not call " + method + " from a thread's default coroutine")); - } -} diff --git a/indra/llcommon/workqueue.h b/indra/llcommon/workqueue.h index c25d787425..5ec790da79 100644 --- a/indra/llcommon/workqueue.h +++ b/indra/llcommon/workqueue.h @@ -12,14 +12,14 @@ #if ! defined(LL_WORKQUEUE_H) #define LL_WORKQUEUE_H -#include "llcoros.h" -#include "llexception.h" #include "llinstancetracker.h" #include "threadsafeschedule.h" #include -#include // std::current_exception #include // std::function +#include #include +#include // std::pair +#include namespace LL { @@ -45,16 +45,11 @@ namespace LL using TimedWork = Queue::TimeTuple; using Closed = Queue::Closed; - struct Error: public LLException - { - Error(const std::string& what): LLException(what) {} - }; - /** * You may omit the WorkQueue name, in which case a unique name is * synthesized; for practical purposes that makes it anonymous. */ - WorkQueue(const std::string& name = std::string(), size_t capacity=1024); + WorkQueue(const std::string& name = std::string()); /** * Since the point of WorkQueue is to pass work to some other worker @@ -64,36 +59,15 @@ namespace LL */ void close(); - /** - * WorkQueue supports multiple producers and multiple consumers. In - * the general case it's misleading to test size(), since any other - * thread might change it the nanosecond the lock is released. On that - * basis, some might argue against publishing a size() method at all. - * - * But there are two specific cases in which a test based on size() - * might be reasonable: - * - * * If you're the only producer, noticing that size() == 0 is - * meaningful. - * * If you're the only consumer, noticing that size() > 0 is - * meaningful. - */ - size_t size(); - /// producer end: are we prevented from pushing any additional items? - bool isClosed(); - /// consumer end: are we done, is the queue entirely drained? - bool done(); - /*---------------------- fire and forget API -----------------------*/ /// fire-and-forget, but at a particular (future?) time template void post(const TimePoint& time, CALLABLE&& callable) { - // Defer reifying an arbitrary CALLABLE until we hit this or - // postIfOpen(). All other methods should accept CALLABLEs of - // arbitrary type to avoid multiple levels of std::function - // indirection. + // Defer reifying an arbitrary CALLABLE until we hit this method. + // All other methods should accept CALLABLEs of arbitrary type to + // avoid multiple levels of std::function indirection. mQueue.push(TimedWork(time, std::move(callable))); } @@ -108,47 +82,6 @@ namespace LL post(TimePoint::clock::now(), std::move(callable)); } - /** - * post work for a particular time, unless the queue is closed before - * we can post - */ - template - bool postIfOpen(const TimePoint& time, CALLABLE&& callable) - { - // Defer reifying an arbitrary CALLABLE until we hit this or - // post(). All other methods should accept CALLABLEs of arbitrary - // type to avoid multiple levels of std::function indirection. - return mQueue.pushIfOpen(TimedWork(time, std::move(callable))); - } - - /** - * post work, unless the queue is closed before we can post - */ - template - bool postIfOpen(CALLABLE&& callable) - { - return postIfOpen(TimePoint::clock::now(), std::move(callable)); - } - - /** - * Post work to be run at a specified time to another WorkQueue, which - * may or may not still exist and be open. Return true if we were able - * to post. - */ - template - static bool postMaybe(weak_t target, const TimePoint& time, CALLABLE&& callable); - - /** - * Post work to another WorkQueue, which may or may not still exist - * and be open. Return true if we were able to post. - */ - template - static bool postMaybe(weak_t target, CALLABLE&& callable) - { - return postMaybe(target, TimePoint::clock::now(), - std::forward(callable)); - } - /** * Launch a callable returning bool that will trigger repeatedly at * specified interval, until the callable returns false. @@ -182,8 +115,63 @@ namespace LL // Studio compile errors that seem utterly unrelated to this source // code. template - bool postTo(weak_t target, - const TimePoint& time, CALLABLE&& callable, FOLLOWUP&& callback); + bool postTo(WorkQueue::weak_t target, + const TimePoint& time, CALLABLE&& callable, FOLLOWUP&& callback) + { + // We're being asked to post to the WorkQueue at target. + // target is a weak_ptr: have to lock it to check it. + auto tptr = target.lock(); + if (! tptr) + // can't post() if the target WorkQueue has been destroyed + return false; + + // Here we believe target WorkQueue still exists. Post to it a + // lambda that packages our callable, our callback and a weak_ptr + // to this originating WorkQueue. + tptr->post( + time, + [reply = super::getWeak(), + callable = std::move(callable), + callback = std::move(callback)] + () + { + // Call the callable in any case -- but to minimize + // copying the result, immediately bind it into a reply + // lambda. The reply lambda also binds the original + // callback, so that when we, the originating WorkQueue, + // finally receive and process the reply lambda, we'll + // call the bound callback with the bound result -- on the + // same thread that originally called postTo(). + auto rlambda = + [result = callable(), + callback = std::move(callback)] + () + { callback(std::move(result)); }; + // Check if this originating WorkQueue still exists. + // Remember, the outer lambda is now running on a thread + // servicing the target WorkQueue, and real time has + // elapsed since postTo()'s tptr->post() call. + // reply is a weak_ptr: have to lock it to check it. + auto rptr = reply.lock(); + if (rptr) + { + // Only post reply lambda if the originating WorkQueue + // still exists. If not -- who would we tell? Log it? + try + { + rptr->post(std::move(rlambda)); + } + catch (const Closed&) + { + // Originating WorkQueue might still exist, but + // might be Closed. Same thing: just discard the + // callback. + } + } + }); + // looks like we were able to post() + return true; + } /** * Post work to another WorkQueue, requesting a specific callback to @@ -193,36 +181,10 @@ namespace LL * inaccessible. */ template - bool postTo(weak_t target, CALLABLE&& callable, FOLLOWUP&& callback) + bool postTo(WorkQueue::weak_t target, + CALLABLE&& callable, FOLLOWUP&& callback) { - return postTo(target, TimePoint::clock::now(), - std::move(callable), std::move(callback)); - } - - /** - * Post work to another WorkQueue to be run at a specified time, - * blocking the calling coroutine until then, returning the result to - * caller on completion. - * - * In general, we assume that each thread's default coroutine is busy - * servicing its WorkQueue or whatever. To try to prevent mistakes, we - * forbid calling waitForResult() from a thread's default coroutine. - */ - template - auto waitForResult(const TimePoint& time, CALLABLE&& callable); - - /** - * Post work to another WorkQueue, blocking the calling coroutine - * until then, returning the result to caller on completion. - * - * In general, we assume that each thread's default coroutine is busy - * servicing its WorkQueue or whatever. To try to prevent mistakes, we - * forbid calling waitForResult() from a thread's default coroutine. - */ - template - auto waitForResult(CALLABLE&& callable) - { - return waitForResult(TimePoint::clock::now(), std::move(callable)); + return postTo(target, TimePoint::clock::now(), std::move(callable), std::move(callback)); } /*--------------------------- worker API ---------------------------*/ @@ -270,23 +232,6 @@ namespace LL bool runUntil(const TimePoint& until); private: - template - static auto makeReplyLambda(CALLABLE&& callable, FOLLOWUP&& callback); - /// general case: arbitrary C++ return type - template - struct MakeReplyLambda; - /// specialize for CALLABLE returning void - template - struct MakeReplyLambda; - - /// general case: arbitrary C++ return type - template - struct WaitForResult; - /// specialize for CALLABLE returning void - template - struct WaitForResult; - - static void checkCoroutine(const std::string& method); static void error(const std::string& msg); static std::string makeName(const std::string& name); void callWork(const Queue::DataTuple& work); @@ -308,8 +253,8 @@ namespace LL { public: // bind the desired data - BackJack(weak_t target, - const TimePoint& start, + BackJack(WorkQueue::weak_t target, + const WorkQueue::TimePoint& start, const std::chrono::duration& interval, CALLABLE&& callable): mTarget(target), @@ -356,8 +301,8 @@ namespace LL } private: - weak_t mTarget; - TimePoint mStart; + WorkQueue::weak_t mTarget; + WorkQueue::TimePoint mStart; std::chrono::duration mInterval; CALLABLE mCallable; }; @@ -385,187 +330,6 @@ namespace LL getWeak(), TimePoint::clock::now(), interval, std::move(callable))); } - /// general case: arbitrary C++ return type - template - struct WorkQueue::MakeReplyLambda - { - auto operator()(CALLABLE&& callable, FOLLOWUP&& callback) - { - // Call the callable in any case -- but to minimize - // copying the result, immediately bind it into the reply - // lambda. The reply lambda also binds the original - // callback, so that when we, the originating WorkQueue, - // finally receive and process the reply lambda, we'll - // call the bound callback with the bound result -- on the - // same thread that originally called postTo(). - return - [result = std::forward(callable)(), - callback = std::move(callback)] - () - { callback(std::move(result)); }; - } - }; - - /// specialize for CALLABLE returning void - template - struct WorkQueue::MakeReplyLambda - { - auto operator()(CALLABLE&& callable, FOLLOWUP&& callback) - { - // Call the callable, which produces no result. - std::forward(callable)(); - // Our completion callback is simply the caller's callback. - return std::move(callback); - } - }; - - template - auto WorkQueue::makeReplyLambda(CALLABLE&& callable, FOLLOWUP&& callback) - { - return MakeReplyLambda(callable)())>() - (std::move(callable), std::move(callback)); - } - - template - bool WorkQueue::postTo(weak_t target, - const TimePoint& time, CALLABLE&& callable, FOLLOWUP&& callback) - { - // We're being asked to post to the WorkQueue at target. - // target is a weak_ptr: have to lock it to check it. - auto tptr = target.lock(); - if (! tptr) - // can't post() if the target WorkQueue has been destroyed - return false; - - // Here we believe target WorkQueue still exists. Post to it a - // lambda that packages our callable, our callback and a weak_ptr - // to this originating WorkQueue. - tptr->post( - time, - [reply = super::getWeak(), - callable = std::move(callable), - callback = std::move(callback)] - () - { - // Use postMaybe() below in case this originating WorkQueue - // has been closed or destroyed. Remember, the outer lambda is - // now running on a thread servicing the target WorkQueue, and - // real time has elapsed since postTo()'s tptr->post() call. - try - { - // Make a reply lambda to repost to THIS WorkQueue. - // Delegate to makeReplyLambda() so we can partially - // specialize on void return. - postMaybe(reply, makeReplyLambda(std::move(callable), std::move(callback))); - } - catch (...) - { - // Either variant of makeReplyLambda() is responsible for - // calling the caller's callable. If that throws, return - // the exception to the originating thread. - postMaybe( - reply, - // Bind the current exception to transport back to the - // originating WorkQueue. Once there, rethrow it. - [exc = std::current_exception()](){ std::rethrow_exception(exc); }); - } - }); - - // looks like we were able to post() - return true; - } - - template - bool WorkQueue::postMaybe(weak_t target, const TimePoint& time, CALLABLE&& callable) - { - // target is a weak_ptr: have to lock it to check it - auto tptr = target.lock(); - if (tptr) - { - try - { - tptr->post(time, std::forward(callable)); - // we were able to post() - return true; - } - catch (const Closed&) - { - // target WorkQueue still exists, but is Closed - } - } - // either target no longer exists, or its WorkQueue is Closed - return false; - } - - /// general case: arbitrary C++ return type - template - struct WorkQueue::WaitForResult - { - auto operator()(WorkQueue* self, const TimePoint& time, CALLABLE&& callable) - { - LLCoros::Promise promise; - self->post( - time, - // We dare to bind a reference to Promise because it's - // specifically designed for cross-thread communication. - [&promise, callable = std::move(callable)]() - { - try - { - // call the caller's callable and trigger promise with result - promise.set_value(callable()); - } - catch (...) - { - promise.set_exception(std::current_exception()); - } - }); - auto future{ LLCoros::getFuture(promise) }; - // now, on the calling thread, wait for that result - LLCoros::TempStatus st("waiting for WorkQueue::waitForResult()"); - return future.get(); - } - }; - - /// specialize for CALLABLE returning void - template - struct WorkQueue::WaitForResult - { - void operator()(WorkQueue* self, const TimePoint& time, CALLABLE&& callable) - { - LLCoros::Promise promise; - self->post( - time, - // &promise is designed for cross-thread access - [&promise, callable = std::move(callable)]() - { - try - { - callable(); - promise.set_value(); - } - catch (...) - { - promise.set_exception(std::current_exception()); - } - }); - auto future{ LLCoros::getFuture(promise) }; - // block until set_value() - LLCoros::TempStatus st("waiting for void WorkQueue::waitForResult()"); - future.get(); - } - }; - - template - auto WorkQueue::waitForResult(const TimePoint& time, CALLABLE&& callable) - { - checkCoroutine("waitForResult()"); - // derive callable's return type so we can specialize for void - return WaitForResult(callable)())>() - (this, time, std::forward(callable)); - } - } // namespace LL #endif /* ! defined(LL_WORKQUEUE_H) */ diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index 1b6920fe3b..cbc5392882 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -172,19 +172,31 @@ BOOL is_little_endian() return (*c == 0x78) ; } +LLImageGLThread* LLImageGLThread::sInstance = nullptr; + //static void LLImageGL::initClass(LLWindow* window, S32 num_catagories, BOOL skip_analyze_alpha /* = false */) { LL_PROFILE_ZONE_SCOPED; sSkipAnalyzeAlpha = skip_analyze_alpha; - LLImageGLThread::createInstance(window); + LLImageGLThread::sInstance = new LLImageGLThread(window); + LLImageGLThread::sInstance->start(); +} + +//static +void LLImageGL::updateClass() +{ + LL_PROFILE_ZONE_SCOPED; + LLImageGLThread::sInstance->executeCallbacks(); } //static void LLImageGL::cleanupClass() { LL_PROFILE_ZONE_SCOPED; - LLImageGLThread::deleteSingleton(); + LLImageGLThread::sInstance->mFunctionQueue.close(); + delete LLImageGLThread::sInstance; + LLImageGLThread::sInstance = nullptr; } //static @@ -492,9 +504,6 @@ void LLImageGL::init(BOOL usemipmaps) #endif mCategory = -1; - - // Sometimes we have to post work for the main thread. - mMainQueue = LL::WorkQueue::getInstance("mainloop"); } void LLImageGL::cleanup() @@ -1527,7 +1536,8 @@ BOOL LLImageGL::createGLTexture(S32 discard_level, const U8* data_in, BOOL data_ } //if we're on the image loading thread, be sure to delete old_texname and update mTexName on the main thread - if (! on_main_thread()) + if (LLImageGLThread::sInstance != nullptr && + LLThread::currentID() == LLImageGLThread::sInstance->getID()) { { LL_PROFILE_ZONE_NAMED("cglt - sync"); @@ -1544,9 +1554,7 @@ BOOL LLImageGL::createGLTexture(S32 discard_level, const U8* data_in, BOOL data_ } ref(); - LL::WorkQueue::postMaybe( - mMainQueue, - [=]() + LLImageGLThread::sInstance->postCallback([=]() { LL_PROFILE_ZONE_NAMED("cglt - delete callback"); if (old_texname != 0) @@ -2251,24 +2259,73 @@ void LLImageGL::resetCurTexSizebar() */ LLImageGLThread::LLImageGLThread(LLWindow* window) - // We want exactly one thread, but a very large capacity: we never want - // anyone, especially inner-loop render code, to have to block on post() - // because we're full. - : ThreadPool("LLImageGL", 1, 1024*1024) - , mWindow(window) + : LLThread("LLImageGL"), mWindow(window) { mFinished = false; mContext = mWindow->createSharedContext(); } +// post a function to be executed on the LLImageGL background thread + +bool LLImageGLThread::post(const std::function& func) +{ + try + { + mFunctionQueue.post(func); + } + catch (LLThreadSafeQueueInterrupt e) + { + return false; + } + + return true; +} + +//post a callback to be executed on the main thread + +bool LLImageGLThread::postCallback(const std::function& callback) +{ + try + { + if (!mCallbackQueue.tryPost(callback)) + { + mPendingCallbackQ.push(callback); + } + } + catch (LLThreadSafeQueueInterrupt e) + { + //thread is closing, drop request + return false; + } + + return true; +} + +void LLImageGLThread::executeCallbacks() +{ + LL_PROFILE_ZONE_SCOPED; + //executed from main thread + mCallbackQueue.runPending(); + + while (!mPendingCallbackQ.empty()) + { + if (mCallbackQueue.tryPost(mPendingCallbackQ.front())) + { + mPendingCallbackQ.pop(); + } + else + { + break; + } + } +} + void LLImageGLThread::run() { - // We must perform setup on this thread before actually servicing our - // WorkQueue, likewise cleanup afterwards. mWindow->makeContextCurrent(mContext); gGL.init(); - ThreadPool::run(); + mFunctionQueue.runUntilClose(); gGL.shutdown(); mWindow->destroySharedContext(mContext); } diff --git a/indra/llrender/llimagegl.h b/indra/llrender/llimagegl.h index 27496def1d..8264e4a5f2 100644 --- a/indra/llrender/llimagegl.h +++ b/indra/llrender/llimagegl.h @@ -37,7 +37,6 @@ #include "llunits.h" #include "llthreadsafequeue.h" #include "llrender.h" -#include "threadpool.h" #include "workqueue.h" class LLTextureAtlas ; @@ -199,7 +198,6 @@ private: void freePickMask(); LLPointer mSaveData; // used for destroyGL/restoreGL - LL::WorkQueue::weak_t mMainQueue; U8* mPickMask; //downsampled bitmap approximation of alpha channel. NULL if no alpha channel U16 mPickMaskWidth; U16 mPickMaskHeight; @@ -273,6 +271,7 @@ public: public: static void initClass(LLWindow* window, S32 num_catagories, BOOL skip_analyze_alpha = false); + static void updateClass(); static void cleanupClass() ; private: @@ -308,24 +307,34 @@ public: }; -class LLImageGLThread : public LLSimpleton, LL::ThreadPool +class LLImageGLThread : public LLThread { public: LLImageGLThread(LLWindow* window); // post a function to be executed on the LLImageGL background thread - template - bool post(CALLABLE&& func) - { - return getQueue().postIfOpen(std::forward(func)); - } + bool post(const std::function& func); + + //post a callback to be executed on the main thread + bool postCallback(const std::function& callback); + + void executeCallbacks(); void run() override; -private: + // Work Queue for background thread + LL::WorkQueue mFunctionQueue; + + // Work Queue for main thread (run from updateClass) + LL::WorkQueue mCallbackQueue; + LLWindow* mWindow; void* mContext; LLAtomicBool mFinished; + + std::queue> mPendingCallbackQ; + + static LLImageGLThread* sInstance; }; diff --git a/indra/llui/CMakeLists.txt b/indra/llui/CMakeLists.txt index 55c1655d7b..f781ff4110 100644 --- a/indra/llui/CMakeLists.txt +++ b/indra/llui/CMakeLists.txt @@ -308,10 +308,6 @@ if(LL_TESTS) ${BOOST_FIBER_LIBRARY} ${BOOST_CONTEXT_LIBRARY} ${BOOST_SYSTEM_LIBRARY} ${WINDOWS_LIBRARIES}) if(NOT LINUX) - if(WINDOWS) - LL_ADD_INTEGRATION_TEST(llurlentry llurlentry.cpp "imm32;${test_libs}") - else(WINDOWS) - LL_ADD_INTEGRATION_TEST(llurlentry llurlentry.cpp "${test_libs}") - endif(WINDOWS) + LL_ADD_INTEGRATION_TEST(llurlentry llurlentry.cpp "${test_libs}") endif(NOT LINUX) endif(LL_TESTS) diff --git a/indra/llui/llviewereventrecorder.cpp b/indra/llui/llviewereventrecorder.cpp index cb000aef74..5a44ec947a 100644 --- a/indra/llui/llviewereventrecorder.cpp +++ b/indra/llui/llviewereventrecorder.cpp @@ -28,6 +28,8 @@ #include "llui.h" #include "llleap.h" +LLViewerEventRecorder* LLSimpleton::sInstance = nullptr; + LLViewerEventRecorder::LLViewerEventRecorder() { clear(UNDEFINED); diff --git a/indra/llwindow/llwindowwin32.cpp b/indra/llwindow/llwindowwin32.cpp index 3f3dd43daf..e52624d66a 100644 --- a/indra/llwindow/llwindowwin32.cpp +++ b/indra/llwindow/llwindowwin32.cpp @@ -55,8 +55,6 @@ #include #include #include -#include -#include // std::pair // Require DirectInput version 8 #define DIRECTINPUT_VERSION 0x0800 @@ -176,19 +174,23 @@ DWORD LLWindowWin32::sWinIMESentenceMode = IME_SMODE_AUTOMATIC; LLCoordWindow LLWindowWin32::sWinIMEWindowPosition(-1,-1); // The following class LLWinImm delegates Windows IMM APIs. -// It was originally introduced to support US Windows XP, on which we needed -// to dynamically load IMM32.DLL and use GetProcAddress to resolve its entry -// points. Now that that's moot, we retain this wrapper only for hooks for -// metrics. +// We need this because some language versions of Windows, +// e.g., US version of Windows XP, doesn't install IMM32.DLL +// as a default, and we can't link against imm32.lib statically. +// I believe DLL loading of this type is best suited to do +// in a static initialization of a class. What I'm not sure is +// whether it follows the Linden Conding Standard... +// See http://wiki.secondlife.com/wiki/Coding_standards#Static_Members class LLWinImm { public: - static bool isAvailable() { return true; } + static bool isAvailable() { return sTheInstance.mHImmDll != NULL; } public: // Wrappers for IMM API. static BOOL isIME(HKL hkl); + static HWND getDefaultIMEWnd(HWND hwnd); static HIMC getContext(HWND hwnd); static BOOL releaseContext(HWND hwnd, HIMC himc); static BOOL getOpenStatus(HIMC himc); @@ -202,96 +204,236 @@ public: static BOOL setCompositionFont(HIMC himc, LPLOGFONTW logfont); static BOOL setCandidateWindow(HIMC himc, LPCANDIDATEFORM candidate_form); static BOOL notifyIME(HIMC himc, DWORD action, DWORD index, DWORD value); + +private: + LLWinImm(); + ~LLWinImm(); + +private: + // Pointers to IMM API. + BOOL (WINAPI *mImmIsIME)(HKL); + HWND (WINAPI *mImmGetDefaultIMEWnd)(HWND); + HIMC (WINAPI *mImmGetContext)(HWND); + BOOL (WINAPI *mImmReleaseContext)(HWND, HIMC); + BOOL (WINAPI *mImmGetOpenStatus)(HIMC); + BOOL (WINAPI *mImmSetOpenStatus)(HIMC, BOOL); + BOOL (WINAPI *mImmGetConversionStatus)(HIMC, LPDWORD, LPDWORD); + BOOL (WINAPI *mImmSetConversionStatus)(HIMC, DWORD, DWORD); + BOOL (WINAPI *mImmGetCompostitionWindow)(HIMC, LPCOMPOSITIONFORM); + BOOL (WINAPI *mImmSetCompostitionWindow)(HIMC, LPCOMPOSITIONFORM); + LONG (WINAPI *mImmGetCompositionString)(HIMC, DWORD, LPVOID, DWORD); + BOOL (WINAPI *mImmSetCompositionString)(HIMC, DWORD, LPVOID, DWORD, LPVOID, DWORD); + BOOL (WINAPI *mImmSetCompositionFont)(HIMC, LPLOGFONTW); + BOOL (WINAPI *mImmSetCandidateWindow)(HIMC, LPCANDIDATEFORM); + BOOL (WINAPI *mImmNotifyIME)(HIMC, DWORD, DWORD, DWORD); + +private: + HMODULE mHImmDll; + static LLWinImm sTheInstance; }; +LLWinImm LLWinImm::sTheInstance; + +LLWinImm::LLWinImm() : mHImmDll(NULL) +{ + // Check system metrics + if ( !GetSystemMetrics( SM_IMMENABLED ) ) + return; + + mHImmDll = LoadLibraryA("Imm32"); + if (mHImmDll != NULL) + { + mImmIsIME = (BOOL (WINAPI *)(HKL)) GetProcAddress(mHImmDll, "ImmIsIME"); + mImmGetDefaultIMEWnd = (HWND (WINAPI *)(HWND)) GetProcAddress(mHImmDll, "ImmGetDefaultIMEWnd"); + mImmGetContext = (HIMC (WINAPI *)(HWND)) GetProcAddress(mHImmDll, "ImmGetContext"); + mImmReleaseContext = (BOOL (WINAPI *)(HWND, HIMC)) GetProcAddress(mHImmDll, "ImmReleaseContext"); + mImmGetOpenStatus = (BOOL (WINAPI *)(HIMC)) GetProcAddress(mHImmDll, "ImmGetOpenStatus"); + mImmSetOpenStatus = (BOOL (WINAPI *)(HIMC, BOOL)) GetProcAddress(mHImmDll, "ImmSetOpenStatus"); + mImmGetConversionStatus = (BOOL (WINAPI *)(HIMC, LPDWORD, LPDWORD)) GetProcAddress(mHImmDll, "ImmGetConversionStatus"); + mImmSetConversionStatus = (BOOL (WINAPI *)(HIMC, DWORD, DWORD)) GetProcAddress(mHImmDll, "ImmSetConversionStatus"); + mImmGetCompostitionWindow = (BOOL (WINAPI *)(HIMC, LPCOMPOSITIONFORM)) GetProcAddress(mHImmDll, "ImmGetCompositionWindow"); + mImmSetCompostitionWindow = (BOOL (WINAPI *)(HIMC, LPCOMPOSITIONFORM)) GetProcAddress(mHImmDll, "ImmSetCompositionWindow"); + mImmGetCompositionString= (LONG (WINAPI *)(HIMC, DWORD, LPVOID, DWORD)) GetProcAddress(mHImmDll, "ImmGetCompositionStringW"); + mImmSetCompositionString= (BOOL (WINAPI *)(HIMC, DWORD, LPVOID, DWORD, LPVOID, DWORD)) GetProcAddress(mHImmDll, "ImmSetCompositionStringW"); + mImmSetCompositionFont = (BOOL (WINAPI *)(HIMC, LPLOGFONTW)) GetProcAddress(mHImmDll, "ImmSetCompositionFontW"); + mImmSetCandidateWindow = (BOOL (WINAPI *)(HIMC, LPCANDIDATEFORM)) GetProcAddress(mHImmDll, "ImmSetCandidateWindow"); + mImmNotifyIME = (BOOL (WINAPI *)(HIMC, DWORD, DWORD, DWORD)) GetProcAddress(mHImmDll, "ImmNotifyIME"); + + if (mImmIsIME == NULL || + mImmGetDefaultIMEWnd == NULL || + mImmGetContext == NULL || + mImmReleaseContext == NULL || + mImmGetOpenStatus == NULL || + mImmSetOpenStatus == NULL || + mImmGetConversionStatus == NULL || + mImmSetConversionStatus == NULL || + mImmGetCompostitionWindow == NULL || + mImmSetCompostitionWindow == NULL || + mImmGetCompositionString == NULL || + mImmSetCompositionString == NULL || + mImmSetCompositionFont == NULL || + mImmSetCandidateWindow == NULL || + mImmNotifyIME == NULL) + { + // If any of the above API entires are not found, we can't use IMM API. + // So, turn off the IMM support. We should log some warning message in + // the case, since it is very unusual; these APIs are available from + // the beginning, and all versions of IMM32.DLL should have them all. + // Unfortunately, this code may be executed before initialization of + // the logging channel (LL_WARNS()), and we can't do it here... Yes, this + // is one of disadvantages to use static constraction to DLL loading. + FreeLibrary(mHImmDll); + mHImmDll = NULL; + + // If we unload the library, make sure all the function pointers are cleared + mImmIsIME = NULL; + mImmGetDefaultIMEWnd = NULL; + mImmGetContext = NULL; + mImmReleaseContext = NULL; + mImmGetOpenStatus = NULL; + mImmSetOpenStatus = NULL; + mImmGetConversionStatus = NULL; + mImmSetConversionStatus = NULL; + mImmGetCompostitionWindow = NULL; + mImmSetCompostitionWindow = NULL; + mImmGetCompositionString = NULL; + mImmSetCompositionString = NULL; + mImmSetCompositionFont = NULL; + mImmSetCandidateWindow = NULL; + mImmNotifyIME = NULL; + } + } +} + + // static BOOL LLWinImm::isIME(HKL hkl) { - return ImmIsIME(hkl); + if ( sTheInstance.mImmIsIME ) + return sTheInstance.mImmIsIME(hkl); + return FALSE; } // static HIMC LLWinImm::getContext(HWND hwnd) { - return ImmGetContext(hwnd); + if ( sTheInstance.mImmGetContext ) + return sTheInstance.mImmGetContext(hwnd); + return 0; } //static BOOL LLWinImm::releaseContext(HWND hwnd, HIMC himc) { - return ImmReleaseContext(hwnd, himc); + if ( sTheInstance.mImmIsIME ) + return sTheInstance.mImmReleaseContext(hwnd, himc); + return FALSE; } // static BOOL LLWinImm::getOpenStatus(HIMC himc) { - return ImmGetOpenStatus(himc); + if ( sTheInstance.mImmGetOpenStatus ) + return sTheInstance.mImmGetOpenStatus(himc); + return FALSE; } // static BOOL LLWinImm::setOpenStatus(HIMC himc, BOOL status) { - return ImmSetOpenStatus(himc, status); + if ( sTheInstance.mImmSetOpenStatus ) + return sTheInstance.mImmSetOpenStatus(himc, status); + return FALSE; } // static BOOL LLWinImm::getConversionStatus(HIMC himc, LPDWORD conversion, LPDWORD sentence) { - return ImmGetConversionStatus(himc, conversion, sentence); + if ( sTheInstance.mImmGetConversionStatus ) + return sTheInstance.mImmGetConversionStatus(himc, conversion, sentence); + return FALSE; } // static BOOL LLWinImm::setConversionStatus(HIMC himc, DWORD conversion, DWORD sentence) { - return ImmSetConversionStatus(himc, conversion, sentence); + if ( sTheInstance.mImmSetConversionStatus ) + return sTheInstance.mImmSetConversionStatus(himc, conversion, sentence); + return FALSE; } // static BOOL LLWinImm::getCompositionWindow(HIMC himc, LPCOMPOSITIONFORM form) { - return ImmGetCompositionWindow(himc, form); + if ( sTheInstance.mImmGetCompostitionWindow ) + return sTheInstance.mImmGetCompostitionWindow(himc, form); + return FALSE; } // static BOOL LLWinImm::setCompositionWindow(HIMC himc, LPCOMPOSITIONFORM form) { - return ImmSetCompositionWindow(himc, form); + if ( sTheInstance.mImmSetCompostitionWindow ) + return sTheInstance.mImmSetCompostitionWindow(himc, form); + return FALSE; } // static LONG LLWinImm::getCompositionString(HIMC himc, DWORD index, LPVOID data, DWORD length) { - return ImmGetCompositionString(himc, index, data, length); + if ( sTheInstance.mImmGetCompositionString ) + return sTheInstance.mImmGetCompositionString(himc, index, data, length); + return FALSE; } // static BOOL LLWinImm::setCompositionString(HIMC himc, DWORD index, LPVOID pComp, DWORD compLength, LPVOID pRead, DWORD readLength) { - return ImmSetCompositionString(himc, index, pComp, compLength, pRead, readLength); + if ( sTheInstance.mImmSetCompositionString ) + return sTheInstance.mImmSetCompositionString(himc, index, pComp, compLength, pRead, readLength); + return FALSE; } // static BOOL LLWinImm::setCompositionFont(HIMC himc, LPLOGFONTW pFont) { - return ImmSetCompositionFont(himc, pFont); + if ( sTheInstance.mImmSetCompositionFont ) + return sTheInstance.mImmSetCompositionFont(himc, pFont); + return FALSE; } // static BOOL LLWinImm::setCandidateWindow(HIMC himc, LPCANDIDATEFORM form) { - return ImmSetCandidateWindow(himc, form); + if ( sTheInstance.mImmSetCandidateWindow ) + return sTheInstance.mImmSetCandidateWindow(himc, form); + return FALSE; } // static BOOL LLWinImm::notifyIME(HIMC himc, DWORD action, DWORD index, DWORD value) { - return ImmNotifyIME(himc, action, index, value); + if ( sTheInstance.mImmNotifyIME ) + return sTheInstance.mImmNotifyIME(himc, action, index, value); + return FALSE; } + +// ---------------------------------------------------------------------------------------- +LLWinImm::~LLWinImm() +{ + if (mHImmDll != NULL) + { + FreeLibrary(mHImmDll); + mHImmDll = NULL; + } +} + + class LLMonitorInfo { public: @@ -326,32 +468,6 @@ private: static LLMonitorInfo sMonitorInfo; -// Thread that owns the Window Handle -// This whole struct is private to LLWindowWin32, which needs to mess with its -// members, which is why it's a struct rather than a class. In effect, we make -// the containing class a friend. -struct LLWindowWin32::LLWindowWin32Thread : public LL::ThreadPool -{ - static const int MAX_QUEUE_SIZE = 2048; - - LLThreadSafeQueue mMessageQueue; - - LLWindowWin32Thread(); - - void run() override; - - template - void post(CALLABLE&& func) - { - getQueue().post(std::forward(func)); - } - - // call PeekMessage and pull enqueue messages for later processing - void gatherInput(); - HWND mWindowHandle = NULL; - HDC mhDC = 0; -}; - LLWindowWin32::LLWindowWin32(LLWindowCallbacks* callbacks, const std::string& title, const std::string& name, S32 x, S32 y, S32 width, @@ -363,7 +479,8 @@ LLWindowWin32::LLWindowWin32(LLWindowCallbacks* callbacks, : LLWindow(callbacks, fullscreen, flags) { sMainThreadId = LLThread::currentID(); - mWindowThread = new LLWindowWin32Thread(); + mWindowThread = new LLWindowWin32Thread(this); + mWindowThread->start(); //MAINT-516 -- force a load of opengl32.dll just in case windows went sideways LoadLibrary(L"opengl32.dll"); @@ -434,6 +551,7 @@ LLWindowWin32::LLWindowWin32(LLWindowCallbacks* callbacks, // Make an instance of our window then define the window class mhInstance = GetModuleHandle(NULL); + mWndProc = NULL; // Init Direct Input - needed for joystick / Spacemouse @@ -857,13 +975,17 @@ void LLWindowWin32::close() // Something killed the window while we were busy destroying gl or handle somehow got broken LL_WARNS("Window") << "Failed to destroy Window, invalid handle!" << LL_ENDL; } + mWindowHandle = NULL; + mWindowThread->mFinished = true; }); - // Even though the above lambda might not yet have run, we've already - // bound mWindowHandle into it by value, which should suffice for the - // operations we're asking. That's the last time WE should touch it. - mWindowHandle = NULL; - mWindowThread->close(); + + while (!mWindowThread->isStopped()) + { + //nudge window thread + PostMessage(mWindowHandle, WM_USER + 0x0017, 0xB0B0, 0x1337); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } } BOOL LLWindowWin32::isValid() @@ -1156,7 +1278,51 @@ BOOL LLWindowWin32::switchContext(BOOL fullscreen, const LLCoordScreen& size, BO << " Fullscreen: " << mFullscreen << LL_ENDL; - recreateWindow(window_rect, dw_ex_style, dw_style); + auto oldHandle = mWindowHandle; + + //zero out mWindowHandle and mhDC before destroying window so window thread falls back to peekmessage + mWindowHandle = 0; + mhDC = 0; + + if (oldHandle && !destroy_window_handler(oldHandle)) + { + LL_WARNS("Window") << "Failed to properly close window before recreating it!" << LL_ENDL; + } + + mWindowHandle = NULL; + mhDC = 0; + + mWindowThread->post( + [this, window_rect, dw_ex_style, dw_style]() + { + mWindowHandle = CreateWindowEx(dw_ex_style, + mWindowClassName, + mWindowTitle, + WS_CLIPSIBLINGS | WS_CLIPCHILDREN | dw_style, + window_rect.left, // x pos + window_rect.top, // y pos + window_rect.right - window_rect.left, // width + window_rect.bottom - window_rect.top, // height + NULL, + NULL, + mhInstance, + NULL); + + if (mWindowHandle) + { + mhDC = GetDC(mWindowHandle); + } + } + ); + + // HACK wait for above handle to become populated + // TODO: use a future + int count = 1024; + while (!mhDC && count > 0) + { + Sleep(10); + --count; + } if (mWindowHandle) { @@ -1484,7 +1650,48 @@ const S32 max_format = (S32)num_formats - 1; mhDC = 0; // Zero The Device Context } - recreateWindow(window_rect, dw_ex_style, dw_style); + auto oldHandle = mWindowHandle; + mWindowHandle = NULL; + mhDC = 0; + + // Destroy The Window + if (oldHandle && !destroy_window_handler(oldHandle)) + { + LL_WARNS("Window") << "Failed to properly close window!" << LL_ENDL; + } + + mWindowThread->post( + [this, window_rect, dw_ex_style, dw_style]() + { + mWindowHandle = CreateWindowEx(dw_ex_style, + mWindowClassName, + mWindowTitle, + WS_CLIPSIBLINGS | WS_CLIPCHILDREN | dw_style, + window_rect.left, // x pos + window_rect.top, // y pos + window_rect.right - window_rect.left, // width + window_rect.bottom - window_rect.top, // height + NULL, + NULL, + mhInstance, + NULL); + + if (mWindowHandle) + { + mhDC = GetDC(mWindowHandle); + } + } + ); + + // HACK wait for above handle to become populated + // TODO: use a future + int count = 1024; + while (!mhDC && count > 0) + { + PostMessage(oldHandle, WM_USER + 8, 0x1717, 0x3b3b); + Sleep(10); + --count; + } if (mWindowHandle) { @@ -1621,64 +1828,6 @@ const S32 max_format = (S32)num_formats - 1; return TRUE; } -void LLWindowWin32::recreateWindow(RECT window_rect, DWORD dw_ex_style, DWORD dw_style) -{ - auto oldHandle = mWindowHandle; - - // zero out mWindowHandle and mhDC before destroying window so window - // thread falls back to peekmessage - mWindowHandle = 0; - mhDC = 0; - - if (oldHandle && !destroy_window_handler(oldHandle)) - { - LL_WARNS("Window") << "Failed to properly close window before recreating it!" << LL_ENDL; - } - - std::promise> promise; - mWindowThread->post( - [this, window_rect, dw_ex_style, dw_style, &promise]() - { - auto handle = CreateWindowEx(dw_ex_style, - mWindowClassName, - mWindowTitle, - WS_CLIPSIBLINGS | WS_CLIPCHILDREN | dw_style, - window_rect.left, // x pos - window_rect.top, // y pos - window_rect.right - window_rect.left, // width - window_rect.bottom - window_rect.top, // height - NULL, - NULL, - mhInstance, - NULL); - - if (! handle) - { - // Failed to create window: clear the variables. This - // assignment is valid because we're running on mWindowThread. - mWindowThread->mWindowHandle = NULL; - mWindowThread->mhDC = 0; - } - else - { - // Update mWindowThread's own mWindowHandle and mhDC. - mWindowThread->mWindowHandle = handle; - mWindowThread->mhDC = GetDC(handle); - } - - // It's important to wake up the future either way. - promise.set_value(std::make_pair(mWindowThread->mWindowHandle, mWindowThread->mhDC)); - } - ); - - auto future = promise.get_future(); - // This blocks until mWindowThread processes CreateWindowEx() and calls - // promise.set_value(). - auto pair = future.get(); - mWindowHandle = pair.first; - mhDC = pair.second; -} - void* LLWindowWin32::createSharedContext() { S32 attribs[] = @@ -2032,14 +2181,12 @@ void LLWindowWin32::gatherInput() } - if (mWindowThread->getQueue().size()) + if (mWindowThread->mFunctionQueue.size() > 0) { LL_PROFILE_ZONE_NAMED("gi - PostMessage"); if (mWindowHandle) - { - // post a nonsense user message to wake up the Window Thread in - // case any functions are pending and no windows events came - // through this frame + { // post a nonsense user message to wake up the Window Thread in case any functions are pending + // and no windows events came through this frame PostMessage(mWindowHandle, WM_USER + 0x0017, 0xB0B0, 0x1337); } } @@ -2129,6 +2276,17 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ if (NULL != window_imp) { + // Has user provided their own window callback? + if (NULL != window_imp->mWndProc) + { + LL_PROFILE_ZONE_NAMED("mwp - WndProc"); + if (!window_imp->mWndProc(h_wnd, u_msg, w_param, l_param)) + { + // user has handled window message + return 0; + } + } + // Juggle to make sure we can get negative positions for when // mouse is outside window. LLCoordWindow window_coord((S32)(S16)LOWORD(l_param), (S32)(S16)HIWORD(l_param)); @@ -4409,32 +4567,35 @@ std::vector LLWindowWin32::getDynamicFallbackFontList() #endif // LL_WINDOWS -inline LLWindowWin32::LLWindowWin32Thread::LLWindowWin32Thread() - : ThreadPool("Window Thread", 1, MAX_QUEUE_SIZE) +inline LLWindowWin32Thread::LLWindowWin32Thread(LLWindowWin32* window) + : LLThread("Window Thread"), + mWindow(window), + mFunctionQueue(MAX_QUEUE_SIZE) { + } -void LLWindowWin32::LLWindowWin32Thread::run() +inline void LLWindowWin32Thread::run() { - sWindowThreadId = std::this_thread::get_id(); - while (! getQueue().done()) + sWindowThreadId = getID(); + while (!mFinished) { LL_PROFILE_ZONE_SCOPED; - if (mWindowHandle != 0) + if (mWindow && mWindow->mWindowHandle != 0) { MSG msg; BOOL status; - if (mhDC == 0) + if (mWindow->mhDC == 0) { LL_PROFILE_ZONE_NAMED("w32t - PeekMessage"); - status = PeekMessage(&msg, mWindowHandle, 0, 0, PM_REMOVE); + status = PeekMessage(&msg, mWindow->mWindowHandle, 0, 0, PM_REMOVE); } else { LL_PROFILE_ZONE_NAMED("w32t - GetMessage"); - status = GetMessage(&msg, mWindowHandle, 0, 0); + status = GetMessage(&msg, mWindow->mWindowHandle, 0, 0); } if (status > 0) { @@ -4448,7 +4609,11 @@ void LLWindowWin32::LLWindowWin32Thread::run() { LL_PROFILE_ZONE_NAMED("w32t - Function Queue"); //process any pending functions - getQueue().runPending(); + std::function curFunc; + while (mFunctionQueue.tryPopBack(curFunc)) + { + curFunc(); + } } #if 0 @@ -4460,6 +4625,11 @@ void LLWindowWin32::LLWindowWin32Thread::run() } } +void LLWindowWin32Thread::post(const std::function& func) +{ + mFunctionQueue.pushFront(func); +} + void LLWindowWin32::post(const std::function& func) { mFunctionQueue.pushFront(func); diff --git a/indra/llwindow/llwindowwin32.h b/indra/llwindow/llwindowwin32.h index 5966061177..d082080807 100644 --- a/indra/llwindow/llwindowwin32.h +++ b/indra/llwindow/llwindowwin32.h @@ -36,12 +36,44 @@ #include "llthread.h" #include "llthreadsafequeue.h" #include "llmutex.h" -#include "workqueue.h" // Hack for async host by name #define LL_WM_HOST_RESOLVED (WM_APP + 1) typedef void (*LLW32MsgCallback)(const MSG &msg); +class LLWindowWin32; + +// Thread that owns the Window Handle +class LLWindowWin32Thread : public LLThread +{ +public: + class Message + { + public: + LRESULT mMsg; + }; + + static const int MAX_QUEUE_SIZE = 2048; + + LLThreadSafeQueue mMessageQueue; + LLThreadSafeQueue> mFunctionQueue; + + bool mFinished = false; + + LLWindowWin32Thread(LLWindowWin32* window); + + void run() override; + + void post(const std::function& func); + +private: + + // call PeekMessage and pull enqueue messages for later processing + void gatherInput(); + LLWindowWin32* mWindow = nullptr; + +}; + class LLWindowWin32 : public LLWindow { public: @@ -186,6 +218,7 @@ protected: HGLRC mhRC = 0; // OpenGL rendering context HDC mhDC = 0; // Windows Device context handle HINSTANCE mhInstance; // handle to application instance + WNDPROC mWndProc; // user-installable window proc RECT mOldMouseClip; // Screen rect to which the mouse cursor was globally constrained before we changed it in clipMouse() WPARAM mLastSizeWParam; F32 mOverrideAspectRatio; @@ -237,15 +270,14 @@ protected: BOOL mMouseVanish; - struct LLWindowWin32Thread; - LLWindowWin32Thread* mWindowThread = nullptr; - LLThreadSafeQueue> mFunctionQueue; - LLThreadSafeQueue> mMouseQueue; - void post(const std::function& func); - void postMouseButtonEvent(const std::function& func); - void recreateWindow(RECT window_rect, DWORD dw_ex_style, DWORD dw_style); + LLWindowWin32Thread* mWindowThread = nullptr; + LLThreadSafeQueue> mFunctionQueue; + LLThreadSafeQueue> mMouseQueue; + void post(const std::function& func); + void postMouseButtonEvent(const std::function& func); friend class LLWindowManager; + friend class LLWindowWin32Thread; }; class LLSplashScreenWin32 : public LLSplashScreen diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 6d090be33a..631089f6ce 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -393,6 +393,7 @@ set(viewer_SOURCE_FILES llloginhandler.cpp lllogininstance.cpp llmachineid.cpp + llmainlooprepeater.cpp llmanip.cpp llmaniprotate.cpp llmanipscale.cpp @@ -1031,6 +1032,7 @@ set(viewer_HEADER_FILES llloginhandler.h lllogininstance.h llmachineid.h + llmainlooprepeater.h llmanip.h llmaniprotate.h llmanipscale.h @@ -1602,7 +1604,6 @@ if (WINDOWS) ${WINDOWS_LIBRARIES} comdlg32 dxguid - imm32 kernel32 odbc32 odbccp32 diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 607e531e62..f15b5d0981 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -3858,17 +3858,6 @@ Value 1 - MainWorkTime - - Comment - Max time per frame devoted to mainloop work queue (in milliseconds) - Persist - 1 - Type - F32 - Value - 0.1 - QueueInventoryFetchTimeout Comment @@ -12674,20 +12663,6 @@ Value 50 - ThreadPoolSizes - - Comment - Map of size overrides for specific thread pools. - Persist - 1 - Type - LLSD - Value - - General - 4 - - ThrottleBandwidthKBPS Comment diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 93e5c2e341..89756d0881 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -233,12 +233,11 @@ #include "llavatariconctrl.h" #include "llgroupiconctrl.h" #include "llviewerassetstats.h" -#include "workqueue.h" -using namespace LL; // Include for security api initialization #include "llsecapi.h" #include "llmachineid.h" +#include "llmainlooprepeater.h" #include "llcleanup.h" #include "llcoproceduremanager.h" @@ -367,10 +366,6 @@ BOOL gLogoutInProgress = FALSE; BOOL gSimulateMemLeak = FALSE; -// We don't want anyone, especially threads working on the graphics pipeline, -// to have to block due to this WorkQueue being full. -WorkQueue gMainloopWork("mainloop", 1024*1024); - //////////////////////////////////////////////////////////// // Internal globals... that should be removed. static std::string gArgs; @@ -386,6 +381,42 @@ static std::string gLaunchFileOnQuit; // Used on Win32 for other apps to identify our window (eg, win_setup) const char* const VIEWER_WINDOW_CLASSNAME = "Second Life"; +//-- LLDeferredTaskList ------------------------------------------------------ + +/** + * A list of deferred tasks. + * + * We sometimes need to defer execution of some code until the viewer gets idle, + * e.g. removing an inventory item from within notifyObservers() may not work out. + * + * Tasks added to this list will be executed in the next LLAppViewer::idle() iteration. + * All tasks are executed only once. + */ +class LLDeferredTaskList: public LLSingleton +{ + LLSINGLETON_EMPTY_CTOR(LLDeferredTaskList); + LOG_CLASS(LLDeferredTaskList); + + friend class LLAppViewer; + typedef boost::signals2::signal signal_t; + + void addTask(const signal_t::slot_type& cb) + { + mSignal.connect(cb); + } + + void run() + { + if (!mSignal.empty()) + { + mSignal(); + mSignal.disconnect_all_slots(); + } + } + + signal_t mSignal; +}; + //---------------------------------------------------------------------------- // List of entries from strings.xml to always replace @@ -943,6 +974,9 @@ bool LLAppViewer::init() } LL_INFOS("InitInfo") << "Cache initialization is done." << LL_ENDL ; + // Initialize the repeater service. + LLMainLoopRepeater::instance().start(); + // Initialize event recorder LLViewerEventRecorder::createInstance(); @@ -2158,6 +2192,8 @@ bool LLAppViewer::cleanup() SUBSYSTEM_CLEANUP(LLProxy); LLCore::LLHttp::cleanup(); + LLMainLoopRepeater::instance().stop(); + ll_close_fail_log(); LLError::LLCallStacks::cleanup(); @@ -4452,7 +4488,7 @@ bool LLAppViewer::initCache() void LLAppViewer::addOnIdleCallback(const boost::function& cb) { - gMainloopWork.post(cb); + LLDeferredTaskList::instance().addTask(cb); } void LLAppViewer::loadKeyBindings() @@ -4850,6 +4886,7 @@ void LLAppViewer::idle() LLNotificationsUI::LLToast::updateClass(); LLSmoothInterpolation::updateInterpolants(); LLMortician::updateClass(); + LLImageGL::updateClass(); LLFilePickerThread::clearDead(); //calls LLFilePickerThread::notify() LLDirPickerThread::clearDead(); F32 dt_raw = idle_timer.getElapsedTimeAndResetF32(); @@ -5226,19 +5263,8 @@ void LLAppViewer::idle() } } - // Service the WorkQueue we use for replies from worker threads. - // Use function statics for the timeslice setting so we only have to fetch - // and convert MainWorkTime once. - static F32 MainWorkTimeRaw = gSavedSettings.getF32("MainWorkTime"); - static F32Milliseconds MainWorkTimeMs(MainWorkTimeRaw); - // MainWorkTime is specified in fractional milliseconds, but std::chrono - // uses integer representations. What if we want less than a microsecond? - // Use nanoseconds. We're very sure we will never need to specify a - // MainWorkTime that would be larger than we could express in - // std::chrono::nanoseconds. - static std::chrono::nanoseconds MainWorkTimeNanoSec{ - std::chrono::nanoseconds::rep(MainWorkTimeMs.value() * 1000000)}; - gMainloopWork.runFor(MainWorkTimeNanoSec); + // Execute deferred tasks. + LLDeferredTaskList::instance().run(); // Handle shutdown process, for example, // wait for floaters to close, send quit message, diff --git a/indra/newview/llenvironment.cpp b/indra/newview/llenvironment.cpp index 1a66f10b8f..dba24b3d02 100644 --- a/indra/newview/llenvironment.cpp +++ b/indra/newview/llenvironment.cpp @@ -824,6 +824,7 @@ std::string env_selection_to_string(LLEnvironment::EnvSelection_t sel) #undef RTNENUM } +LLEnvironment* LLSimpleton::sInstance = nullptr; //------------------------------------------------------------------------- LLEnvironment::LLEnvironment(): mCloudScrollDelta(), diff --git a/indra/newview/llmainlooprepeater.cpp b/indra/newview/llmainlooprepeater.cpp new file mode 100644 index 0000000000..6736e9a950 --- /dev/null +++ b/indra/newview/llmainlooprepeater.cpp @@ -0,0 +1,88 @@ +/** + * @file llmachineid.cpp + * @brief retrieves unique machine ids + * + * $LicenseInfo:firstyear=2009&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2010, Linden Research, Inc. + * + * 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 + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "llviewerprecompiledheaders.h" +#include "llapr.h" +#include "llevents.h" +#include "llmainlooprepeater.h" + + + +// LLMainLoopRepeater +//----------------------------------------------------------------------------- + + +LLMainLoopRepeater::LLMainLoopRepeater(void): + mQueue(0) +{ + ; // No op. +} + + +void LLMainLoopRepeater::start(void) +{ + if(mQueue != 0) return; + + mQueue = new LLThreadSafeQueue(1024); + mMainLoopConnection = LLEventPumps::instance(). + obtain("mainloop").listen(LLEventPump::inventName(), boost::bind(&LLMainLoopRepeater::onMainLoop, this, _1)); + mRepeaterConnection = LLEventPumps::instance(). + obtain("mainlooprepeater").listen(LLEventPump::inventName(), boost::bind(&LLMainLoopRepeater::onMessage, this, _1)); +} + + +void LLMainLoopRepeater::stop(void) +{ + mMainLoopConnection.release(); + mRepeaterConnection.release(); + + delete mQueue; + mQueue = 0; +} + + +bool LLMainLoopRepeater::onMainLoop(LLSD const &) +{ + LLSD message; + while(mQueue->tryPopBack(message)) { + std::string pump = message["pump"].asString(); + if(pump.length() == 0 ) continue; // No pump. + LLEventPumps::instance().obtain(pump).post(message["payload"]); + } + return false; +} + + +bool LLMainLoopRepeater::onMessage(LLSD const & event) +{ + try { + mQueue->pushFront(event); + } catch(LLThreadSafeQueueError & e) { + LL_WARNS() << "could not repeat message (" << e.what() << ")" << + event.asString() << LL_ENDL; + } + return false; +} diff --git a/indra/newview/llmainlooprepeater.h b/indra/newview/llmainlooprepeater.h new file mode 100644 index 0000000000..2ec3a74e4a --- /dev/null +++ b/indra/newview/llmainlooprepeater.h @@ -0,0 +1,64 @@ +/** + * @file llmainlooprepeater.h + * @brief a service for repeating messages on the main loop. + * + * $LicenseInfo:firstyear=2010&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2010, Linden Research, Inc. + * + * 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 + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#ifndef LL_LLMAINLOOPREPEATER_H +#define LL_LLMAINLOOPREPEATER_H + + +#include "llsd.h" +#include "llthreadsafequeue.h" + + +// +// A service which creates the pump 'mainlooprepeater' to which any thread can +// post a message that will be re-posted on the main loop. +// +// The posted message should contain two map elements: pump and payload. The +// pump value is a string naming the pump to which the message should be +// re-posted. The payload value is what will be posted to the designated pump. +// +class LLMainLoopRepeater: + public LLSingleton +{ + LLSINGLETON(LLMainLoopRepeater); +public: + // Start the repeater service. + void start(void); + + // Stop the repeater service. + void stop(void); + +private: + LLTempBoundListener mMainLoopConnection; + LLTempBoundListener mRepeaterConnection; + LLThreadSafeQueue * mQueue; + + bool onMainLoop(LLSD const &); + bool onMessage(LLSD const & event); +}; + + +#endif diff --git a/indra/newview/llselectmgr.cpp b/indra/newview/llselectmgr.cpp index bc00c518e9..53247031b4 100644 --- a/indra/newview/llselectmgr.cpp +++ b/indra/newview/llselectmgr.cpp @@ -97,6 +97,8 @@ #include "llglheaders.h" #include "llinventoryobserver.h" +LLSelectMgr* LLSimpleton::sInstance = nullptr; + LLViewerObject* getSelectedParentObject(LLViewerObject *object) ; // // Consts diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp index 9a4149948c..57c5074804 100644 --- a/indra/newview/llstartup.cpp +++ b/indra/newview/llstartup.cpp @@ -205,9 +205,6 @@ #include "llstacktrace.h" -#include "threadpool.h" - - #if LL_WINDOWS #include "lldxhardware.h" #endif @@ -304,20 +301,6 @@ void callback_cache_name(const LLUUID& id, const std::string& full_name, bool is // local classes // -void launchThreadPool() -{ - LLSD poolSizes{ gSavedSettings.getLLSD("ThreadPoolSizes") }; - LLSD sizeSpec{ poolSizes["General"] }; - LLSD::Integer size{ sizeSpec.isInteger()? sizeSpec.asInteger() : 3 }; - LL_DEBUGS("ThreadPool") << "Instantiating General pool with " - << size << " threads" << LL_ENDL; - // Use a function-static ThreadPool: static duration, but instantiated - // only on demand. - // We don't want anyone, especially the main thread, to have to block - // due to this ThreadPool being full. - static LL::ThreadPool pool("General", size, 1024*1024); -} - void update_texture_fetch() { LLAppViewer::getTextureCache()->update(1); // unpauses the texture cache thread @@ -1506,9 +1489,6 @@ bool idle_startup() gAgentCamera.resetCamera(); display_startup(); - // start up the ThreadPool we'll use for textures et al. - launchThreadPool(); - // Initialize global class data needed for surfaces (i.e. textures) LL_DEBUGS("AppInit") << "Initializing sky..." << LL_ENDL; // Initialize all of the viewer object classes for the first time (doing things like texture fetches. diff --git a/indra/newview/llviewercamera.cpp b/indra/newview/llviewercamera.cpp index 5d8e80cc41..5ebce115f6 100644 --- a/indra/newview/llviewercamera.cpp +++ b/indra/newview/llviewercamera.cpp @@ -54,6 +54,8 @@ // System includes #include // for setprecision +LLViewerCamera* LLSimpleton::sInstance = nullptr; + LLTrace::CountStatHandle<> LLViewerCamera::sVelocityStat("camera_velocity"); LLTrace::CountStatHandle<> LLViewerCamera::sAngularVelocityStat("camera_angular_velocity"); diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp index 498e4ef8bc..fbc5830a5c 100644 --- a/indra/newview/llviewertexture.cpp +++ b/indra/newview/llviewertexture.cpp @@ -679,9 +679,6 @@ void LLViewerTexture::init(bool firstinit) mVolumeList[LLRender::LIGHT_TEX].clear(); mVolumeList[LLRender::SCULPT_TEX].clear(); - - mMainQueue = LL::WorkQueue::getInstance("mainloop"); - mImageQueue = LL::WorkQueue::getInstance("LLImageGL"); } //virtual @@ -1625,26 +1622,17 @@ void LLViewerFetchedTexture::scheduleCreateTexture() { mNeedsCreateTexture = TRUE; #if LL_WINDOWS //flip to 0 to revert to single-threaded OpenGL texture uploads - auto mainq = mMainQueue.lock(); - if (mainq) - { - mainq->postTo( - mImageQueue, - // work to be done on LLImageGL worker thread - [this]() - { - //actually create the texture on a background thread - createTexture(); - }, - // callback to be run on main thread - [this]() - { - //finalize on main thread - postCreateTexture(); - unref(); - }); - } - else + if (!LLImageGLThread::sInstance->post([this]() + { + //actually create the texture on a background thread + createTexture(); + LLImageGLThread::sInstance->postCallback([this]() + { + //finalize on main thread + postCreateTexture(); + unref(); + }); + })) #endif { gTextureList.mCreateTextureList.insert(this); diff --git a/indra/newview/llviewertexture.h b/indra/newview/llviewertexture.h index 4cd4c7cd39..f9f1bfef44 100644 --- a/indra/newview/llviewertexture.h +++ b/indra/newview/llviewertexture.h @@ -35,7 +35,6 @@ #include "llrender.h" #include "llmetricperformancetester.h" #include "httpcommon.h" -#include "workqueue.h" #include #include @@ -214,9 +213,6 @@ protected: //do not use LLPointer here. LLViewerMediaTexture* mParcelMedia ; - LL::WorkQueue::weak_t mMainQueue; - LL::WorkQueue::weak_t mImageQueue; - static F32 sTexelPixelRatio; public: static const U32 sCurrentFileVersion; diff --git a/indra/newview/llworld.cpp b/indra/newview/llworld.cpp index 5f62908009..0a8457eb2c 100644 --- a/indra/newview/llworld.cpp +++ b/indra/newview/llworld.cpp @@ -62,6 +62,8 @@ #include +LLWorld* LLSimpleton::sInstance = nullptr; + // // Globals // -- cgit v1.3 From 0b066539fe68dc5750900c3452189645c40adb45 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 24 Nov 2021 10:47:54 -0500 Subject: DRTVWR-546, SL-16220, SL-16094: Undo previous glthread branch revert. Reverting a merge is sticky: it tells git you never want to see that branch again. Merging the DRTVWR-546 branch, which contained the revert, into the glthread branch undid much of the development work on that branch. To restore it we must revert the revert. This reverts commit 029b41c0419e975bbb28454538b46dc69ce5d2ba. --- indra/llcommon/CMakeLists.txt | 3 +- indra/llcommon/llsingleton.h | 14 +- indra/llcommon/tests/threadsafeschedule_test.cpp | 4 +- indra/llcommon/tests/workqueue_test.cpp | 72 ++++- indra/llcommon/timing.cpp | 25 -- indra/llcommon/workqueue.cpp | 30 ++- indra/llcommon/workqueue.h | 197 +++++++++----- indra/llrender/llimagegl.cpp | 89 ++----- indra/llrender/llimagegl.h | 27 +- indra/llwindow/llwindowwin32.cpp | 321 +++-------------------- indra/llwindow/llwindowwin32.h | 36 +-- indra/newview/CMakeLists.txt | 3 +- indra/newview/app_settings/settings.xml | 25 ++ indra/newview/llappviewer.cpp | 51 +--- indra/newview/llmainlooprepeater.cpp | 88 ------- indra/newview/llmainlooprepeater.h | 64 ----- indra/newview/llstartup.cpp | 17 ++ indra/newview/llviewertexture.cpp | 34 ++- indra/newview/llviewertexture.h | 4 + 19 files changed, 371 insertions(+), 733 deletions(-) delete mode 100644 indra/llcommon/timing.cpp delete mode 100644 indra/newview/llmainlooprepeater.cpp delete mode 100644 indra/newview/llmainlooprepeater.h (limited to 'indra/llcommon/tests') diff --git a/indra/llcommon/CMakeLists.txt b/indra/llcommon/CMakeLists.txt index 9defa6b6c1..782f656406 100644 --- a/indra/llcommon/CMakeLists.txt +++ b/indra/llcommon/CMakeLists.txt @@ -119,8 +119,8 @@ set(llcommon_SOURCE_FILES lluriparser.cpp lluuid.cpp llworkerthread.cpp - timing.cpp u64.cpp + threadpool.cpp workqueue.cpp StackWalker.cpp ) @@ -256,6 +256,7 @@ set(llcommon_HEADER_FILES lockstatic.h stdtypes.h stringize.h + threadpool.h threadsafeschedule.h timer.h tuple.h diff --git a/indra/llcommon/llsingleton.h b/indra/llcommon/llsingleton.h index f85f961287..6042c0906c 100644 --- a/indra/llcommon/llsingleton.h +++ b/indra/llcommon/llsingleton.h @@ -847,14 +847,13 @@ template class LLSimpleton { public: - static T* sInstance; - - static void createInstance() - { + template + static void createInstance(ARGS&&... args) + { llassert(sInstance == nullptr); - sInstance = new T(); + sInstance = new T(std::forward(args)...); } - + static inline T* getInstance() { return sInstance; } static inline T& instance() { return *getInstance(); } static inline bool instanceExists() { return sInstance != nullptr; } @@ -864,6 +863,9 @@ public: delete sInstance; sInstance = nullptr; } + +private: + static T* sInstance; }; template diff --git a/indra/llcommon/tests/threadsafeschedule_test.cpp b/indra/llcommon/tests/threadsafeschedule_test.cpp index af67b9f492..c421cc7b1c 100644 --- a/indra/llcommon/tests/threadsafeschedule_test.cpp +++ b/indra/llcommon/tests/threadsafeschedule_test.cpp @@ -46,11 +46,11 @@ namespace tut // the real time required for each push() call. Explicitly increment // the timestamp for each one -- but since we're passing explicit // timestamps, make the queue reorder them. - queue.push(Queue::TimeTuple(Queue::Clock::now() + 20ms, "ghi")); + queue.push(Queue::TimeTuple(Queue::Clock::now() + 200ms, "ghi")); // Given the various push() overloads, you have to match the type // exactly: conversions are ambiguous. queue.push("abc"s); - queue.push(Queue::Clock::now() + 10ms, "def"); + queue.push(Queue::Clock::now() + 100ms, "def"); queue.close(); auto entry = queue.pop(); ensure_equals("failed to pop first", std::get<0>(entry), "abc"s); diff --git a/indra/llcommon/tests/workqueue_test.cpp b/indra/llcommon/tests/workqueue_test.cpp index d5405400fd..bea3ad911b 100644 --- a/indra/llcommon/tests/workqueue_test.cpp +++ b/indra/llcommon/tests/workqueue_test.cpp @@ -20,7 +20,10 @@ // external library headers // other Linden headers #include "../test/lltut.h" +#include "../test/catch_and_store_what_in.h" #include "llcond.h" +#include "llcoros.h" +#include "lleventcoro.h" #include "llstring.h" #include "stringize.h" @@ -138,7 +141,8 @@ namespace tut [](){ return 17; }, // Note that a postTo() *callback* can safely bind a reference to // a variable on the invoking thread, because the callback is run - // on the invoking thread. + // on the invoking thread. (Of course the bound variable must + // survive until the callback is called.) [&result](int i){ result = i; }); // this should post the callback to main qptr->runOne(); @@ -156,4 +160,70 @@ namespace tut main.runPending(); ensure_equals("failed to run string callback", alpha, "abc"); } + + template<> template<> + void object::test<5>() + { + set_test_name("postTo with void return"); + WorkQueue main("main"); + auto qptr = WorkQueue::getInstance("queue"); + std::string observe; + main.postTo( + qptr, + // The ONLY reason we can get away with binding a reference to + // 'observe' in our work callable is because we're directly + // calling qptr->runOne() on this same thread. It would be a + // mistake to do that if some other thread were servicing 'queue'. + [&observe](){ observe = "queue"; }, + [&observe](){ observe.append(";main"); }); + qptr->runOne(); + main.runOne(); + ensure_equals("failed to run both lambdas", observe, "queue;main"); + } + + template<> template<> + void object::test<6>() + { + set_test_name("waitForResult"); + std::string stored; + // Try to call waitForResult() on this thread's main coroutine. It + // should throw because the main coroutine must service the queue. + auto what{ catch_what( + [this, &stored](){ stored = queue.waitForResult( + [](){ return "should throw"; }); }) }; + ensure("lambda should not have run", stored.empty()); + ensure_not("waitForResult() should have thrown", what.empty()); + ensure(STRINGIZE("should mention waitForResult: " << what), + what.find("waitForResult") != std::string::npos); + + // Call waitForResult() on a coroutine, with a string result. + LLCoros::instance().launch( + "waitForResult string", + [this, &stored]() + { stored = queue.waitForResult( + [](){ return "string result"; }); }); + llcoro::suspend(); + // Nothing will have happened yet because, even if the coroutine did + // run immediately, all it did was to queue the inner lambda on + // 'queue'. Service it. + queue.runOne(); + llcoro::suspend(); + ensure_equals("bad waitForResult return", stored, "string result"); + + // Call waitForResult() on a coroutine, with a void callable. + stored.clear(); + bool done = false; + LLCoros::instance().launch( + "waitForResult void", + [this, &stored, &done]() + { + queue.waitForResult([&stored](){ stored = "ran"; }); + done = true; + }); + llcoro::suspend(); + queue.runOne(); + llcoro::suspend(); + ensure_equals("didn't run coroutine", stored, "ran"); + ensure("void waitForResult() didn't return", done); + } } // namespace tut diff --git a/indra/llcommon/timing.cpp b/indra/llcommon/timing.cpp deleted file mode 100644 index c2dc695ef3..0000000000 --- a/indra/llcommon/timing.cpp +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @file timing.cpp - * @brief This file will be deprecated in the future. - * - * $LicenseInfo:firstyear=2000&license=viewerlgpl$ - * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. - * - * 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 - * - * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA - * $/LicenseInfo$ - */ diff --git a/indra/llcommon/workqueue.cpp b/indra/llcommon/workqueue.cpp index e7d40354aa..c74dada2e4 100644 --- a/indra/llcommon/workqueue.cpp +++ b/indra/llcommon/workqueue.cpp @@ -26,8 +26,9 @@ using Mutex = LLCoros::Mutex; using Lock = LLCoros::LockType; -LL::WorkQueue::WorkQueue(const std::string& name): - super(makeName(name)) +LL::WorkQueue::WorkQueue(const std::string& name, size_t capacity): + super(makeName(name)), + mQueue(capacity) { // TODO: register for "LLApp" events so we can implicitly close() on // viewer shutdown. @@ -38,6 +39,21 @@ void LL::WorkQueue::close() mQueue.close(); } +size_t LL::WorkQueue::size() +{ + return mQueue.size(); +} + +bool LL::WorkQueue::isClosed() +{ + return mQueue.isClosed(); +} + +bool LL::WorkQueue::done() +{ + return mQueue.done(); +} + void LL::WorkQueue::runUntilClose() { try @@ -130,3 +146,13 @@ void LL::WorkQueue::error(const std::string& msg) { LL_ERRS("WorkQueue") << msg << LL_ENDL; } + +void LL::WorkQueue::checkCoroutine(const std::string& method) +{ + // By convention, the default coroutine on each thread has an empty name + // string. See also LLCoros::logname(). + if (LLCoros::getName().empty()) + { + LLTHROW(Error("Do not call " + method + " from a thread's default coroutine")); + } +} diff --git a/indra/llcommon/workqueue.h b/indra/llcommon/workqueue.h index 8e4b38c2f3..96574a18b9 100644 --- a/indra/llcommon/workqueue.h +++ b/indra/llcommon/workqueue.h @@ -12,14 +12,14 @@ #if ! defined(LL_WORKQUEUE_H) #define LL_WORKQUEUE_H +#include "llcoros.h" +#include "llexception.h" #include "llinstancetracker.h" #include "threadsafeschedule.h" #include +#include // std::current_exception #include // std::function -#include #include -#include // std::pair -#include namespace LL { @@ -45,11 +45,16 @@ namespace LL using TimedWork = Queue::TimeTuple; using Closed = Queue::Closed; + struct Error: public LLException + { + Error(const std::string& what): LLException(what) {} + }; + /** * You may omit the WorkQueue name, in which case a unique name is * synthesized; for practical purposes that makes it anonymous. */ - WorkQueue(const std::string& name = std::string()); + WorkQueue(const std::string& name = std::string(), size_t capacity=1024); /** * Since the point of WorkQueue is to pass work to some other worker @@ -59,15 +64,36 @@ namespace LL */ void close(); + /** + * WorkQueue supports multiple producers and multiple consumers. In + * the general case it's misleading to test size(), since any other + * thread might change it the nanosecond the lock is released. On that + * basis, some might argue against publishing a size() method at all. + * + * But there are two specific cases in which a test based on size() + * might be reasonable: + * + * * If you're the only producer, noticing that size() == 0 is + * meaningful. + * * If you're the only consumer, noticing that size() > 0 is + * meaningful. + */ + size_t size(); + /// producer end: are we prevented from pushing any additional items? + bool isClosed(); + /// consumer end: are we done, is the queue entirely drained? + bool done(); + /*---------------------- fire and forget API -----------------------*/ /// fire-and-forget, but at a particular (future?) time template void post(const TimePoint& time, CALLABLE&& callable) { - // Defer reifying an arbitrary CALLABLE until we hit this method. - // All other methods should accept CALLABLEs of arbitrary type to - // avoid multiple levels of std::function indirection. + // Defer reifying an arbitrary CALLABLE until we hit this or + // postIfOpen(). All other methods should accept CALLABLEs of + // arbitrary type to avoid multiple levels of std::function + // indirection. mQueue.push(TimedWork(time, std::move(callable))); } @@ -82,6 +108,47 @@ namespace LL post(TimePoint::clock::now(), std::move(callable)); } + /** + * post work for a particular time, unless the queue is closed before + * we can post + */ + template + bool postIfOpen(const TimePoint& time, CALLABLE&& callable) + { + // Defer reifying an arbitrary CALLABLE until we hit this or + // post(). All other methods should accept CALLABLEs of arbitrary + // type to avoid multiple levels of std::function indirection. + return mQueue.pushIfOpen(TimedWork(time, std::move(callable))); + } + + /** + * post work, unless the queue is closed before we can post + */ + template + bool postIfOpen(CALLABLE&& callable) + { + return postIfOpen(TimePoint::clock::now(), std::move(callable)); + } + + /** + * Post work to be run at a specified time to another WorkQueue, which + * may or may not still exist and be open. Return true if we were able + * to post. + */ + template + static bool postMaybe(weak_t target, const TimePoint& time, CALLABLE&& callable); + + /** + * Post work to another WorkQueue, which may or may not still exist + * and be open. Return true if we were able to post. + */ + template + static bool postMaybe(weak_t target, CALLABLE&& callable) + { + return postMaybe(target, TimePoint::clock::now(), + std::forward(callable)); + } + /** * Launch a callable returning bool that will trigger repeatedly at * specified interval, until the callable returns false. @@ -115,63 +182,8 @@ namespace LL // Studio compile errors that seem utterly unrelated to this source // code. template - bool postTo(WorkQueue::weak_t target, - const TimePoint& time, CALLABLE&& callable, FOLLOWUP&& callback) - { - // We're being asked to post to the WorkQueue at target. - // target is a weak_ptr: have to lock it to check it. - auto tptr = target.lock(); - if (! tptr) - // can't post() if the target WorkQueue has been destroyed - return false; - - // Here we believe target WorkQueue still exists. Post to it a - // lambda that packages our callable, our callback and a weak_ptr - // to this originating WorkQueue. - tptr->post( - time, - [reply = super::getWeak(), - callable = std::move(callable), - callback = std::move(callback)] - () - { - // Call the callable in any case -- but to minimize - // copying the result, immediately bind it into a reply - // lambda. The reply lambda also binds the original - // callback, so that when we, the originating WorkQueue, - // finally receive and process the reply lambda, we'll - // call the bound callback with the bound result -- on the - // same thread that originally called postTo(). - auto rlambda = - [result = callable(), - callback = std::move(callback)] - () - { callback(std::move(result)); }; - // Check if this originating WorkQueue still exists. - // Remember, the outer lambda is now running on a thread - // servicing the target WorkQueue, and real time has - // elapsed since postTo()'s tptr->post() call. - // reply is a weak_ptr: have to lock it to check it. - auto rptr = reply.lock(); - if (rptr) - { - // Only post reply lambda if the originating WorkQueue - // still exists. If not -- who would we tell? Log it? - try - { - rptr->post(std::move(rlambda)); - } - catch (const Closed&) - { - // Originating WorkQueue might still exist, but - // might be Closed. Same thing: just discard the - // callback. - } - } - }); - // looks like we were able to post() - return true; - } + bool postTo(weak_t target, + const TimePoint& time, CALLABLE&& callable, FOLLOWUP&& callback); /** * Post work to another WorkQueue, requesting a specific callback to @@ -181,10 +193,36 @@ namespace LL * inaccessible. */ template - bool postTo(WorkQueue::weak_t target, - CALLABLE&& callable, FOLLOWUP&& callback) + bool postTo(weak_t target, CALLABLE&& callable, FOLLOWUP&& callback) + { + return postTo(target, TimePoint::clock::now(), + std::move(callable), std::move(callback)); + } + + /** + * Post work to another WorkQueue to be run at a specified time, + * blocking the calling coroutine until then, returning the result to + * caller on completion. + * + * In general, we assume that each thread's default coroutine is busy + * servicing its WorkQueue or whatever. To try to prevent mistakes, we + * forbid calling waitForResult() from a thread's default coroutine. + */ + template + auto waitForResult(const TimePoint& time, CALLABLE&& callable); + + /** + * Post work to another WorkQueue, blocking the calling coroutine + * until then, returning the result to caller on completion. + * + * In general, we assume that each thread's default coroutine is busy + * servicing its WorkQueue or whatever. To try to prevent mistakes, we + * forbid calling waitForResult() from a thread's default coroutine. + */ + template + auto waitForResult(CALLABLE&& callable) { - return postTo(target, TimePoint::clock::now(), std::move(callable), std::move(callback)); + return waitForResult(TimePoint::clock::now(), std::move(callable)); } /*--------------------------- worker API ---------------------------*/ @@ -233,6 +271,23 @@ namespace LL bool runUntil(const TimePoint& until); private: + template + static auto makeReplyLambda(CALLABLE&& callable, FOLLOWUP&& callback); + /// general case: arbitrary C++ return type + template + struct MakeReplyLambda; + /// specialize for CALLABLE returning void + template + struct MakeReplyLambda; + + /// general case: arbitrary C++ return type + template + struct WaitForResult; + /// specialize for CALLABLE returning void + template + struct WaitForResult; + + static void checkCoroutine(const std::string& method); static void error(const std::string& msg); static std::string makeName(const std::string& name); void callWork(const Queue::DataTuple& work); @@ -254,8 +309,8 @@ namespace LL { public: // bind the desired data - BackJack(WorkQueue::weak_t target, - const WorkQueue::TimePoint& start, + BackJack(weak_t target, + const TimePoint& start, const std::chrono::duration& interval, CALLABLE&& callable): mTarget(target), @@ -302,8 +357,8 @@ namespace LL } private: - WorkQueue::weak_t mTarget; - WorkQueue::TimePoint mStart; + weak_t mTarget; + TimePoint mStart; std::chrono::duration mInterval; CALLABLE mCallable; }; diff --git a/indra/llrender/llimagegl.cpp b/indra/llrender/llimagegl.cpp index ed0e3fb345..894eb8c773 100644 --- a/indra/llrender/llimagegl.cpp +++ b/indra/llrender/llimagegl.cpp @@ -172,31 +172,19 @@ BOOL is_little_endian() return (*c == 0x78) ; } -LLImageGLThread* LLImageGLThread::sInstance = nullptr; - //static void LLImageGL::initClass(LLWindow* window, S32 num_catagories, BOOL skip_analyze_alpha /* = false */) { LL_PROFILE_ZONE_SCOPED; sSkipAnalyzeAlpha = skip_analyze_alpha; - LLImageGLThread::sInstance = new LLImageGLThread(window); - LLImageGLThread::sInstance->start(); -} - -//static -void LLImageGL::updateClass() -{ - LL_PROFILE_ZONE_SCOPED; - LLImageGLThread::sInstance->executeCallbacks(); + LLImageGLThread::createInstance(window); } //static void LLImageGL::cleanupClass() { LL_PROFILE_ZONE_SCOPED; - LLImageGLThread::sInstance->mFunctionQueue.close(); - delete LLImageGLThread::sInstance; - LLImageGLThread::sInstance = nullptr; + LLImageGLThread::deleteSingleton(); } //static @@ -504,6 +492,9 @@ void LLImageGL::init(BOOL usemipmaps) #endif mCategory = -1; + + // Sometimes we have to post work for the main thread. + mMainQueue = LL::WorkQueue::getInstance("mainloop"); } void LLImageGL::cleanup() @@ -1536,8 +1527,7 @@ BOOL LLImageGL::createGLTexture(S32 discard_level, const U8* data_in, BOOL data_ } //if we're on the image loading thread, be sure to delete old_texname and update mTexName on the main thread - if (LLImageGLThread::sInstance != nullptr && - LLThread::currentID() == LLImageGLThread::sInstance->getID()) + if (! on_main_thread()) { { LL_PROFILE_ZONE_NAMED("cglt - sync"); @@ -1554,7 +1544,9 @@ BOOL LLImageGL::createGLTexture(S32 discard_level, const U8* data_in, BOOL data_ } ref(); - LLImageGLThread::sInstance->postCallback([=]() + LL::WorkQueue::postMaybe( + mMainQueue, + [=]() { LL_PROFILE_ZONE_NAMED("cglt - delete callback"); if (old_texname != 0) @@ -2259,7 +2251,11 @@ void LLImageGL::resetCurTexSizebar() */ LLImageGLThread::LLImageGLThread(LLWindow* window) - : LLThread("LLImageGL"), mWindow(window) + // We want exactly one thread, but a very large capacity: we never want + // anyone, especially inner-loop render code, to have to block on post() + // because we're full. + : ThreadPool("LLImageGL", 1, 1024*1024) + , mWindow(window) { LL_PROFILE_ZONE_SCOPED; mFinished = false; @@ -2268,61 +2264,6 @@ LLImageGLThread::LLImageGLThread(LLWindow* window) ThreadPool::start(); } -// post a function to be executed on the LLImageGL background thread - -bool LLImageGLThread::post(const std::function& func) -{ - try - { - mFunctionQueue.post(func); - } - catch (LLThreadSafeQueueInterrupt e) - { - return false; - } - - return true; -} - -//post a callback to be executed on the main thread - -bool LLImageGLThread::postCallback(const std::function& callback) -{ - try - { - if (!mCallbackQueue.tryPost(callback)) - { - mPendingCallbackQ.push(callback); - } - } - catch (LLThreadSafeQueueInterrupt e) - { - //thread is closing, drop request - return false; - } - - return true; -} - -void LLImageGLThread::executeCallbacks() -{ - LL_PROFILE_ZONE_SCOPED; - //executed from main thread - mCallbackQueue.runPending(); - - while (!mPendingCallbackQ.empty()) - { - if (mCallbackQueue.tryPost(mPendingCallbackQ.front())) - { - mPendingCallbackQ.pop(); - } - else - { - break; - } - } -} - void LLImageGLThread::run() { LL_PROFILE_ZONE_SCOPED; @@ -2330,7 +2271,7 @@ void LLImageGLThread::run() // WorkQueue, likewise cleanup afterwards. mWindow->makeContextCurrent(mContext); gGL.init(); - mFunctionQueue.runUntilClose(); + ThreadPool::run(); gGL.shutdown(); mWindow->destroySharedContext(mContext); } diff --git a/indra/llrender/llimagegl.h b/indra/llrender/llimagegl.h index bb46dbc639..ae773bb362 100644 --- a/indra/llrender/llimagegl.h +++ b/indra/llrender/llimagegl.h @@ -37,6 +37,7 @@ #include "llunits.h" #include "llthreadsafequeue.h" #include "llrender.h" +#include "threadpool.h" #include "workqueue.h" class LLTextureAtlas ; @@ -198,6 +199,7 @@ private: void freePickMask(); LLPointer mSaveData; // used for destroyGL/restoreGL + LL::WorkQueue::weak_t mMainQueue; U8* mPickMask; //downsampled bitmap approximation of alpha channel. NULL if no alpha channel U16 mPickMaskWidth; U16 mPickMaskHeight; @@ -271,7 +273,6 @@ public: public: static void initClass(LLWindow* window, S32 num_catagories, BOOL skip_analyze_alpha = false); - static void updateClass(); static void cleanupClass() ; private: @@ -307,34 +308,24 @@ public: }; -class LLImageGLThread : public LLThread +class LLImageGLThread : public LLSimpleton, LL::ThreadPool { public: LLImageGLThread(LLWindow* window); // post a function to be executed on the LLImageGL background thread - bool post(const std::function& func); - - //post a callback to be executed on the main thread - bool postCallback(const std::function& callback); - - void executeCallbacks(); + template + bool post(CALLABLE&& func) + { + return getQueue().postIfOpen(std::forward(func)); + } void run() override; - // Work Queue for background thread - LL::WorkQueue mFunctionQueue; - - // Work Queue for main thread (run from updateClass) - LL::WorkQueue mCallbackQueue; - +private: LLWindow* mWindow; void* mContext = nullptr; LLAtomicBool mFinished; - - std::queue> mPendingCallbackQ; - - static LLImageGLThread* sInstance; }; diff --git a/indra/llwindow/llwindowwin32.cpp b/indra/llwindow/llwindowwin32.cpp index 149a92ffff..062dd02903 100644 --- a/indra/llwindow/llwindowwin32.cpp +++ b/indra/llwindow/llwindowwin32.cpp @@ -183,23 +183,19 @@ DWORD LLWindowWin32::sWinIMESentenceMode = IME_SMODE_AUTOMATIC; LLCoordWindow LLWindowWin32::sWinIMEWindowPosition(-1,-1); // The following class LLWinImm delegates Windows IMM APIs. -// We need this because some language versions of Windows, -// e.g., US version of Windows XP, doesn't install IMM32.DLL -// as a default, and we can't link against imm32.lib statically. -// I believe DLL loading of this type is best suited to do -// in a static initialization of a class. What I'm not sure is -// whether it follows the Linden Conding Standard... -// See http://wiki.secondlife.com/wiki/Coding_standards#Static_Members +// It was originally introduced to support US Windows XP, on which we needed +// to dynamically load IMM32.DLL and use GetProcAddress to resolve its entry +// points. Now that that's moot, we retain this wrapper only for hooks for +// metrics. class LLWinImm { public: - static bool isAvailable() { return sTheInstance.mHImmDll != NULL; } + static bool isAvailable() { return true; } public: // Wrappers for IMM API. static BOOL isIME(HKL hkl); - static HWND getDefaultIMEWnd(HWND hwnd); static HIMC getContext(HWND hwnd); static BOOL releaseContext(HWND hwnd, HIMC himc); static BOOL getOpenStatus(HIMC himc); @@ -213,236 +209,96 @@ public: static BOOL setCompositionFont(HIMC himc, LPLOGFONTW logfont); static BOOL setCandidateWindow(HIMC himc, LPCANDIDATEFORM candidate_form); static BOOL notifyIME(HIMC himc, DWORD action, DWORD index, DWORD value); - -private: - LLWinImm(); - ~LLWinImm(); - -private: - // Pointers to IMM API. - BOOL (WINAPI *mImmIsIME)(HKL); - HWND (WINAPI *mImmGetDefaultIMEWnd)(HWND); - HIMC (WINAPI *mImmGetContext)(HWND); - BOOL (WINAPI *mImmReleaseContext)(HWND, HIMC); - BOOL (WINAPI *mImmGetOpenStatus)(HIMC); - BOOL (WINAPI *mImmSetOpenStatus)(HIMC, BOOL); - BOOL (WINAPI *mImmGetConversionStatus)(HIMC, LPDWORD, LPDWORD); - BOOL (WINAPI *mImmSetConversionStatus)(HIMC, DWORD, DWORD); - BOOL (WINAPI *mImmGetCompostitionWindow)(HIMC, LPCOMPOSITIONFORM); - BOOL (WINAPI *mImmSetCompostitionWindow)(HIMC, LPCOMPOSITIONFORM); - LONG (WINAPI *mImmGetCompositionString)(HIMC, DWORD, LPVOID, DWORD); - BOOL (WINAPI *mImmSetCompositionString)(HIMC, DWORD, LPVOID, DWORD, LPVOID, DWORD); - BOOL (WINAPI *mImmSetCompositionFont)(HIMC, LPLOGFONTW); - BOOL (WINAPI *mImmSetCandidateWindow)(HIMC, LPCANDIDATEFORM); - BOOL (WINAPI *mImmNotifyIME)(HIMC, DWORD, DWORD, DWORD); - -private: - HMODULE mHImmDll; - static LLWinImm sTheInstance; }; -LLWinImm LLWinImm::sTheInstance; - -LLWinImm::LLWinImm() : mHImmDll(NULL) -{ - // Check system metrics - if ( !GetSystemMetrics( SM_IMMENABLED ) ) - return; - - mHImmDll = LoadLibraryA("Imm32"); - if (mHImmDll != NULL) - { - mImmIsIME = (BOOL (WINAPI *)(HKL)) GetProcAddress(mHImmDll, "ImmIsIME"); - mImmGetDefaultIMEWnd = (HWND (WINAPI *)(HWND)) GetProcAddress(mHImmDll, "ImmGetDefaultIMEWnd"); - mImmGetContext = (HIMC (WINAPI *)(HWND)) GetProcAddress(mHImmDll, "ImmGetContext"); - mImmReleaseContext = (BOOL (WINAPI *)(HWND, HIMC)) GetProcAddress(mHImmDll, "ImmReleaseContext"); - mImmGetOpenStatus = (BOOL (WINAPI *)(HIMC)) GetProcAddress(mHImmDll, "ImmGetOpenStatus"); - mImmSetOpenStatus = (BOOL (WINAPI *)(HIMC, BOOL)) GetProcAddress(mHImmDll, "ImmSetOpenStatus"); - mImmGetConversionStatus = (BOOL (WINAPI *)(HIMC, LPDWORD, LPDWORD)) GetProcAddress(mHImmDll, "ImmGetConversionStatus"); - mImmSetConversionStatus = (BOOL (WINAPI *)(HIMC, DWORD, DWORD)) GetProcAddress(mHImmDll, "ImmSetConversionStatus"); - mImmGetCompostitionWindow = (BOOL (WINAPI *)(HIMC, LPCOMPOSITIONFORM)) GetProcAddress(mHImmDll, "ImmGetCompositionWindow"); - mImmSetCompostitionWindow = (BOOL (WINAPI *)(HIMC, LPCOMPOSITIONFORM)) GetProcAddress(mHImmDll, "ImmSetCompositionWindow"); - mImmGetCompositionString= (LONG (WINAPI *)(HIMC, DWORD, LPVOID, DWORD)) GetProcAddress(mHImmDll, "ImmGetCompositionStringW"); - mImmSetCompositionString= (BOOL (WINAPI *)(HIMC, DWORD, LPVOID, DWORD, LPVOID, DWORD)) GetProcAddress(mHImmDll, "ImmSetCompositionStringW"); - mImmSetCompositionFont = (BOOL (WINAPI *)(HIMC, LPLOGFONTW)) GetProcAddress(mHImmDll, "ImmSetCompositionFontW"); - mImmSetCandidateWindow = (BOOL (WINAPI *)(HIMC, LPCANDIDATEFORM)) GetProcAddress(mHImmDll, "ImmSetCandidateWindow"); - mImmNotifyIME = (BOOL (WINAPI *)(HIMC, DWORD, DWORD, DWORD)) GetProcAddress(mHImmDll, "ImmNotifyIME"); - - if (mImmIsIME == NULL || - mImmGetDefaultIMEWnd == NULL || - mImmGetContext == NULL || - mImmReleaseContext == NULL || - mImmGetOpenStatus == NULL || - mImmSetOpenStatus == NULL || - mImmGetConversionStatus == NULL || - mImmSetConversionStatus == NULL || - mImmGetCompostitionWindow == NULL || - mImmSetCompostitionWindow == NULL || - mImmGetCompositionString == NULL || - mImmSetCompositionString == NULL || - mImmSetCompositionFont == NULL || - mImmSetCandidateWindow == NULL || - mImmNotifyIME == NULL) - { - // If any of the above API entires are not found, we can't use IMM API. - // So, turn off the IMM support. We should log some warning message in - // the case, since it is very unusual; these APIs are available from - // the beginning, and all versions of IMM32.DLL should have them all. - // Unfortunately, this code may be executed before initialization of - // the logging channel (LL_WARNS()), and we can't do it here... Yes, this - // is one of disadvantages to use static constraction to DLL loading. - FreeLibrary(mHImmDll); - mHImmDll = NULL; - - // If we unload the library, make sure all the function pointers are cleared - mImmIsIME = NULL; - mImmGetDefaultIMEWnd = NULL; - mImmGetContext = NULL; - mImmReleaseContext = NULL; - mImmGetOpenStatus = NULL; - mImmSetOpenStatus = NULL; - mImmGetConversionStatus = NULL; - mImmSetConversionStatus = NULL; - mImmGetCompostitionWindow = NULL; - mImmSetCompostitionWindow = NULL; - mImmGetCompositionString = NULL; - mImmSetCompositionString = NULL; - mImmSetCompositionFont = NULL; - mImmSetCandidateWindow = NULL; - mImmNotifyIME = NULL; - } - } -} - - // static BOOL LLWinImm::isIME(HKL hkl) { - if ( sTheInstance.mImmIsIME ) - return sTheInstance.mImmIsIME(hkl); - return FALSE; + return ImmIsIME(hkl); } // static HIMC LLWinImm::getContext(HWND hwnd) { - if ( sTheInstance.mImmGetContext ) - return sTheInstance.mImmGetContext(hwnd); - return 0; + return ImmGetContext(hwnd); } //static BOOL LLWinImm::releaseContext(HWND hwnd, HIMC himc) { - if ( sTheInstance.mImmIsIME ) - return sTheInstance.mImmReleaseContext(hwnd, himc); - return FALSE; + return ImmReleaseContext(hwnd, himc); } // static BOOL LLWinImm::getOpenStatus(HIMC himc) { - if ( sTheInstance.mImmGetOpenStatus ) - return sTheInstance.mImmGetOpenStatus(himc); - return FALSE; + return ImmGetOpenStatus(himc); } // static BOOL LLWinImm::setOpenStatus(HIMC himc, BOOL status) { - if ( sTheInstance.mImmSetOpenStatus ) - return sTheInstance.mImmSetOpenStatus(himc, status); - return FALSE; + return ImmSetOpenStatus(himc, status); } // static BOOL LLWinImm::getConversionStatus(HIMC himc, LPDWORD conversion, LPDWORD sentence) { - if ( sTheInstance.mImmGetConversionStatus ) - return sTheInstance.mImmGetConversionStatus(himc, conversion, sentence); - return FALSE; + return ImmGetConversionStatus(himc, conversion, sentence); } // static BOOL LLWinImm::setConversionStatus(HIMC himc, DWORD conversion, DWORD sentence) { - if ( sTheInstance.mImmSetConversionStatus ) - return sTheInstance.mImmSetConversionStatus(himc, conversion, sentence); - return FALSE; + return ImmSetConversionStatus(himc, conversion, sentence); } // static BOOL LLWinImm::getCompositionWindow(HIMC himc, LPCOMPOSITIONFORM form) { - if ( sTheInstance.mImmGetCompostitionWindow ) - return sTheInstance.mImmGetCompostitionWindow(himc, form); - return FALSE; + return ImmGetCompositionWindow(himc, form); } // static BOOL LLWinImm::setCompositionWindow(HIMC himc, LPCOMPOSITIONFORM form) { - if ( sTheInstance.mImmSetCompostitionWindow ) - return sTheInstance.mImmSetCompostitionWindow(himc, form); - return FALSE; + return ImmSetCompositionWindow(himc, form); } // static LONG LLWinImm::getCompositionString(HIMC himc, DWORD index, LPVOID data, DWORD length) { - if ( sTheInstance.mImmGetCompositionString ) - return sTheInstance.mImmGetCompositionString(himc, index, data, length); - return FALSE; + return ImmGetCompositionString(himc, index, data, length); } // static BOOL LLWinImm::setCompositionString(HIMC himc, DWORD index, LPVOID pComp, DWORD compLength, LPVOID pRead, DWORD readLength) { - if ( sTheInstance.mImmSetCompositionString ) - return sTheInstance.mImmSetCompositionString(himc, index, pComp, compLength, pRead, readLength); - return FALSE; + return ImmSetCompositionString(himc, index, pComp, compLength, pRead, readLength); } // static BOOL LLWinImm::setCompositionFont(HIMC himc, LPLOGFONTW pFont) { - if ( sTheInstance.mImmSetCompositionFont ) - return sTheInstance.mImmSetCompositionFont(himc, pFont); - return FALSE; + return ImmSetCompositionFont(himc, pFont); } // static BOOL LLWinImm::setCandidateWindow(HIMC himc, LPCANDIDATEFORM form) { - if ( sTheInstance.mImmSetCandidateWindow ) - return sTheInstance.mImmSetCandidateWindow(himc, form); - return FALSE; + return ImmSetCandidateWindow(himc, form); } // static BOOL LLWinImm::notifyIME(HIMC himc, DWORD action, DWORD index, DWORD value) { - if ( sTheInstance.mImmNotifyIME ) - return sTheInstance.mImmNotifyIME(himc, action, index, value); - return FALSE; + return ImmNotifyIME(himc, action, index, value); } - -// ---------------------------------------------------------------------------------------- -LLWinImm::~LLWinImm() -{ - if (mHImmDll != NULL) - { - FreeLibrary(mHImmDll); - mHImmDll = NULL; - } -} - - class LLMonitorInfo { public: @@ -552,8 +408,7 @@ LLWindowWin32::LLWindowWin32(LLWindowCallbacks* callbacks, : LLWindow(callbacks, fullscreen, flags) { sMainThreadId = LLThread::currentID(); - mWindowThread = new LLWindowWin32Thread(this); - mWindowThread->start(); + mWindowThread = new LLWindowWin32Thread(); //MAINT-516 -- force a load of opengl32.dll just in case windows went sideways LoadLibrary(L"opengl32.dll"); @@ -624,7 +479,6 @@ LLWindowWin32::LLWindowWin32(LLWindowCallbacks* callbacks, // Make an instance of our window then define the window class mhInstance = GetModuleHandle(NULL); - mWndProc = NULL; // Init Direct Input - needed for joystick / Spacemouse @@ -1048,17 +902,13 @@ void LLWindowWin32::close() // Something killed the window while we were busy destroying gl or handle somehow got broken LL_WARNS("Window") << "Failed to destroy Window, invalid handle!" << LL_ENDL; } - mWindowHandle = NULL; - mWindowThread->mFinished = true; }); - - while (!mWindowThread->isStopped()) - { - //nudge window thread - PostMessage(mWindowHandle, WM_USER + 0x0017, 0xB0B0, 0x1337); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } + // Even though the above lambda might not yet have run, we've already + // bound mWindowHandle into it by value, which should suffice for the + // operations we're asking. That's the last time WE should touch it. + mWindowHandle = NULL; + mWindowThread->close(); } BOOL LLWindowWin32::isValid() @@ -1351,51 +1201,7 @@ BOOL LLWindowWin32::switchContext(BOOL fullscreen, const LLCoordScreen& size, BO << " Fullscreen: " << mFullscreen << LL_ENDL; - auto oldHandle = mWindowHandle; - - //zero out mWindowHandle and mhDC before destroying window so window thread falls back to peekmessage - mWindowHandle = 0; - mhDC = 0; - - if (oldHandle && !destroy_window_handler(oldHandle)) - { - LL_WARNS("Window") << "Failed to properly close window before recreating it!" << LL_ENDL; - } - - mWindowHandle = NULL; - mhDC = 0; - - mWindowThread->post( - [this, window_rect, dw_ex_style, dw_style]() - { - mWindowHandle = CreateWindowEx(dw_ex_style, - mWindowClassName, - mWindowTitle, - WS_CLIPSIBLINGS | WS_CLIPCHILDREN | dw_style, - window_rect.left, // x pos - window_rect.top, // y pos - window_rect.right - window_rect.left, // width - window_rect.bottom - window_rect.top, // height - NULL, - NULL, - mhInstance, - NULL); - - if (mWindowHandle) - { - mhDC = GetDC(mWindowHandle); - } - } - ); - - // HACK wait for above handle to become populated - // TODO: use a future - int count = 1024; - while (!mhDC && count > 0) - { - Sleep(10); - --count; - } + recreateWindow(window_rect, dw_ex_style, dw_style); if (mWindowHandle) { @@ -1723,48 +1529,7 @@ const S32 max_format = (S32)num_formats - 1; mhDC = 0; // Zero The Device Context } - auto oldHandle = mWindowHandle; - mWindowHandle = NULL; - mhDC = 0; - - // Destroy The Window - if (oldHandle && !destroy_window_handler(oldHandle)) - { - LL_WARNS("Window") << "Failed to properly close window!" << LL_ENDL; - } - - mWindowThread->post( - [this, window_rect, dw_ex_style, dw_style]() - { - mWindowHandle = CreateWindowEx(dw_ex_style, - mWindowClassName, - mWindowTitle, - WS_CLIPSIBLINGS | WS_CLIPCHILDREN | dw_style, - window_rect.left, // x pos - window_rect.top, // y pos - window_rect.right - window_rect.left, // width - window_rect.bottom - window_rect.top, // height - NULL, - NULL, - mhInstance, - NULL); - - if (mWindowHandle) - { - mhDC = GetDC(mWindowHandle); - } - } - ); - - // HACK wait for above handle to become populated - // TODO: use a future - int count = 1024; - while (!mhDC && count > 0) - { - PostMessage(oldHandle, WM_USER + 8, 0x1717, 0x3b3b); - Sleep(10); - --count; - } + recreateWindow(window_rect, dw_ex_style, dw_style); if (mWindowHandle) { @@ -2347,7 +2112,7 @@ void LLWindowWin32::gatherInput() } - if (mWindowThread->mFunctionQueue.size() > 0) + if (mWindowThread->getQueue().size()) { LL_PROFILE_ZONE_NAMED("gi - PostMessage"); kickWindowThread(); @@ -2455,17 +2220,6 @@ LRESULT CALLBACK LLWindowWin32::mainWindowProc(HWND h_wnd, UINT u_msg, WPARAM w_ if (NULL != window_imp) { - // Has user provided their own window callback? - if (NULL != window_imp->mWndProc) - { - LL_PROFILE_ZONE_NAMED("mwp - WndProc"); - if (!window_imp->mWndProc(h_wnd, u_msg, w_param, l_param)) - { - // user has handled window message - return 0; - } - } - // Juggle to make sure we can get negative positions for when // mouse is outside window. LLCoordWindow window_coord((S32)(S16)LOWORD(l_param), (S32)(S16)HIWORD(l_param)); @@ -4746,10 +4500,8 @@ std::vector LLWindowWin32::getDynamicFallbackFontList() #endif // LL_WINDOWS -inline LLWindowWin32Thread::LLWindowWin32Thread(LLWindowWin32* window) - : LLThread("Window Thread"), - mWindow(window), - mFunctionQueue(MAX_QUEUE_SIZE) +inline LLWindowWin32::LLWindowWin32Thread::LLWindowWin32Thread() + : ThreadPool("Window Thread", 1, MAX_QUEUE_SIZE) { ThreadPool::start(); } @@ -4813,7 +4565,7 @@ void LLWindowWin32::LLWindowWin32Thread::run() { MSG msg; BOOL status; - if (mWindow->mhDC == 0) + if (mhDC == 0) { LL_PROFILE_ZONE_NAMED("w32t - PeekMessage"); logger.onChange("PeekMessage(", std::hex, mWindowHandle, ")"); @@ -4840,11 +4592,7 @@ void LLWindowWin32::LLWindowWin32Thread::run() LL_PROFILE_ZONE_NAMED("w32t - Function Queue"); logger.onChange("runPending()"); //process any pending functions - std::function curFunc; - while (mFunctionQueue.tryPopBack(curFunc)) - { - curFunc(); - } + getQueue().runPending(); } #if 0 @@ -4857,11 +4605,6 @@ void LLWindowWin32::LLWindowWin32Thread::run() } } -void LLWindowWin32Thread::post(const std::function& func) -{ - mFunctionQueue.pushFront(func); -} - void LLWindowWin32::post(const std::function& func) { mFunctionQueue.pushFront(func); diff --git a/indra/llwindow/llwindowwin32.h b/indra/llwindow/llwindowwin32.h index 8d0193abc8..b02815e990 100644 --- a/indra/llwindow/llwindowwin32.h +++ b/indra/llwindow/llwindowwin32.h @@ -36,44 +36,12 @@ #include "llthread.h" #include "llthreadsafequeue.h" #include "llmutex.h" +#include "workqueue.h" // Hack for async host by name #define LL_WM_HOST_RESOLVED (WM_APP + 1) typedef void (*LLW32MsgCallback)(const MSG &msg); -class LLWindowWin32; - -// Thread that owns the Window Handle -class LLWindowWin32Thread : public LLThread -{ -public: - class Message - { - public: - LRESULT mMsg; - }; - - static const int MAX_QUEUE_SIZE = 2048; - - LLThreadSafeQueue mMessageQueue; - LLThreadSafeQueue> mFunctionQueue; - - bool mFinished = false; - - LLWindowWin32Thread(LLWindowWin32* window); - - void run() override; - - void post(const std::function& func); - -private: - - // call PeekMessage and pull enqueue messages for later processing - void gatherInput(); - LLWindowWin32* mWindow = nullptr; - -}; - class LLWindowWin32 : public LLWindow { public: @@ -218,7 +186,6 @@ protected: HGLRC mhRC = 0; // OpenGL rendering context HDC mhDC = 0; // Windows Device context handle HINSTANCE mhInstance; // handle to application instance - WNDPROC mWndProc; // user-installable window proc RECT mOldMouseClip; // Screen rect to which the mouse cursor was globally constrained before we changed it in clipMouse() WPARAM mLastSizeWParam; F32 mOverrideAspectRatio; @@ -280,7 +247,6 @@ protected: void kickWindowThread(HWND windowHandle=0); friend class LLWindowManager; - friend class LLWindowWin32Thread; }; class LLSplashScreenWin32 : public LLSplashScreen diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 9b636e5e5d..5f085bb9ad 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -393,7 +393,6 @@ set(viewer_SOURCE_FILES llloginhandler.cpp lllogininstance.cpp llmachineid.cpp - llmainlooprepeater.cpp llmanip.cpp llmaniprotate.cpp llmanipscale.cpp @@ -1032,7 +1031,6 @@ set(viewer_HEADER_FILES llloginhandler.h lllogininstance.h llmachineid.h - llmainlooprepeater.h llmanip.h llmaniprotate.h llmanipscale.h @@ -1604,6 +1602,7 @@ if (WINDOWS) ${WINDOWS_LIBRARIES} comdlg32 dxguid + imm32 kernel32 odbc32 odbccp32 diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index 2d821b7451..058da4b66d 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -3871,6 +3871,17 @@ Value 1 + MainWorkTime + + Comment + Max time per frame devoted to mainloop work queue (in milliseconds) + Persist + 1 + Type + F32 + Value + 0.1 + QueueInventoryFetchTimeout Comment @@ -12667,6 +12678,20 @@ Value + ThreadPoolSizes + + Comment + Map of size overrides for specific thread pools. + Persist + 1 + Type + LLSD + Value + + General + 4 + + ThrottleBandwidthKBPS Comment diff --git a/indra/newview/llappviewer.cpp b/indra/newview/llappviewer.cpp index 5b3cbff9b8..e13c0a2472 100644 --- a/indra/newview/llappviewer.cpp +++ b/indra/newview/llappviewer.cpp @@ -233,11 +233,12 @@ #include "llavatariconctrl.h" #include "llgroupiconctrl.h" #include "llviewerassetstats.h" +#include "workqueue.h" +using namespace LL; // Include for security api initialization #include "llsecapi.h" #include "llmachineid.h" -#include "llmainlooprepeater.h" #include "llcleanup.h" #include "llcoproceduremanager.h" @@ -366,6 +367,10 @@ BOOL gLogoutInProgress = FALSE; BOOL gSimulateMemLeak = FALSE; +// We don't want anyone, especially threads working on the graphics pipeline, +// to have to block due to this WorkQueue being full. +WorkQueue gMainloopWork("mainloop", 1024*1024); + //////////////////////////////////////////////////////////// // Internal globals... that should be removed. static std::string gArgs; @@ -381,42 +386,6 @@ static std::string gLaunchFileOnQuit; // Used on Win32 for other apps to identify our window (eg, win_setup) const char* const VIEWER_WINDOW_CLASSNAME = "Second Life"; -//-- LLDeferredTaskList ------------------------------------------------------ - -/** - * A list of deferred tasks. - * - * We sometimes need to defer execution of some code until the viewer gets idle, - * e.g. removing an inventory item from within notifyObservers() may not work out. - * - * Tasks added to this list will be executed in the next LLAppViewer::idle() iteration. - * All tasks are executed only once. - */ -class LLDeferredTaskList: public LLSingleton -{ - LLSINGLETON_EMPTY_CTOR(LLDeferredTaskList); - LOG_CLASS(LLDeferredTaskList); - - friend class LLAppViewer; - typedef boost::signals2::signal signal_t; - - void addTask(const signal_t::slot_type& cb) - { - mSignal.connect(cb); - } - - void run() - { - if (!mSignal.empty()) - { - mSignal(); - mSignal.disconnect_all_slots(); - } - } - - signal_t mSignal; -}; - //---------------------------------------------------------------------------- // List of entries from strings.xml to always replace @@ -973,9 +942,6 @@ bool LLAppViewer::init() } LL_INFOS("InitInfo") << "Cache initialization is done." << LL_ENDL ; - // Initialize the repeater service. - LLMainLoopRepeater::instance().start(); - // Initialize event recorder LLViewerEventRecorder::createInstance(); @@ -2217,8 +2183,6 @@ bool LLAppViewer::cleanup() SUBSYSTEM_CLEANUP(LLProxy); LLCore::LLHttp::cleanup(); - LLMainLoopRepeater::instance().stop(); - ll_close_fail_log(); LLError::LLCallStacks::cleanup(); @@ -4550,7 +4514,7 @@ bool LLAppViewer::initCache() void LLAppViewer::addOnIdleCallback(const boost::function& cb) { - LLDeferredTaskList::instance().addTask(cb); + gMainloopWork.post(cb); } void LLAppViewer::loadKeyBindings() @@ -4948,7 +4912,6 @@ void LLAppViewer::idle() LLNotificationsUI::LLToast::updateClass(); LLSmoothInterpolation::updateInterpolants(); LLMortician::updateClass(); - LLImageGL::updateClass(); LLFilePickerThread::clearDead(); //calls LLFilePickerThread::notify() LLDirPickerThread::clearDead(); F32 dt_raw = idle_timer.getElapsedTimeAndResetF32(); diff --git a/indra/newview/llmainlooprepeater.cpp b/indra/newview/llmainlooprepeater.cpp deleted file mode 100644 index 6736e9a950..0000000000 --- a/indra/newview/llmainlooprepeater.cpp +++ /dev/null @@ -1,88 +0,0 @@ -/** - * @file llmachineid.cpp - * @brief retrieves unique machine ids - * - * $LicenseInfo:firstyear=2009&license=viewerlgpl$ - * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. - * - * 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 - * - * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA - * $/LicenseInfo$ - */ - -#include "llviewerprecompiledheaders.h" -#include "llapr.h" -#include "llevents.h" -#include "llmainlooprepeater.h" - - - -// LLMainLoopRepeater -//----------------------------------------------------------------------------- - - -LLMainLoopRepeater::LLMainLoopRepeater(void): - mQueue(0) -{ - ; // No op. -} - - -void LLMainLoopRepeater::start(void) -{ - if(mQueue != 0) return; - - mQueue = new LLThreadSafeQueue(1024); - mMainLoopConnection = LLEventPumps::instance(). - obtain("mainloop").listen(LLEventPump::inventName(), boost::bind(&LLMainLoopRepeater::onMainLoop, this, _1)); - mRepeaterConnection = LLEventPumps::instance(). - obtain("mainlooprepeater").listen(LLEventPump::inventName(), boost::bind(&LLMainLoopRepeater::onMessage, this, _1)); -} - - -void LLMainLoopRepeater::stop(void) -{ - mMainLoopConnection.release(); - mRepeaterConnection.release(); - - delete mQueue; - mQueue = 0; -} - - -bool LLMainLoopRepeater::onMainLoop(LLSD const &) -{ - LLSD message; - while(mQueue->tryPopBack(message)) { - std::string pump = message["pump"].asString(); - if(pump.length() == 0 ) continue; // No pump. - LLEventPumps::instance().obtain(pump).post(message["payload"]); - } - return false; -} - - -bool LLMainLoopRepeater::onMessage(LLSD const & event) -{ - try { - mQueue->pushFront(event); - } catch(LLThreadSafeQueueError & e) { - LL_WARNS() << "could not repeat message (" << e.what() << ")" << - event.asString() << LL_ENDL; - } - return false; -} diff --git a/indra/newview/llmainlooprepeater.h b/indra/newview/llmainlooprepeater.h deleted file mode 100644 index 2ec3a74e4a..0000000000 --- a/indra/newview/llmainlooprepeater.h +++ /dev/null @@ -1,64 +0,0 @@ -/** - * @file llmainlooprepeater.h - * @brief a service for repeating messages on the main loop. - * - * $LicenseInfo:firstyear=2010&license=viewerlgpl$ - * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. - * - * 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 - * - * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA - * $/LicenseInfo$ - */ - -#ifndef LL_LLMAINLOOPREPEATER_H -#define LL_LLMAINLOOPREPEATER_H - - -#include "llsd.h" -#include "llthreadsafequeue.h" - - -// -// A service which creates the pump 'mainlooprepeater' to which any thread can -// post a message that will be re-posted on the main loop. -// -// The posted message should contain two map elements: pump and payload. The -// pump value is a string naming the pump to which the message should be -// re-posted. The payload value is what will be posted to the designated pump. -// -class LLMainLoopRepeater: - public LLSingleton -{ - LLSINGLETON(LLMainLoopRepeater); -public: - // Start the repeater service. - void start(void); - - // Stop the repeater service. - void stop(void); - -private: - LLTempBoundListener mMainLoopConnection; - LLTempBoundListener mRepeaterConnection; - LLThreadSafeQueue * mQueue; - - bool onMainLoop(LLSD const &); - bool onMessage(LLSD const & event); -}; - - -#endif diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp index 8d21b04511..df066fb7ed 100644 --- a/indra/newview/llstartup.cpp +++ b/indra/newview/llstartup.cpp @@ -206,6 +206,9 @@ #include "llstacktrace.h" +#include "threadpool.h" + + #if LL_WINDOWS #include "lldxhardware.h" #endif @@ -303,6 +306,20 @@ void callback_cache_name(const LLUUID& id, const std::string& full_name, bool is // local classes // +void launchThreadPool() +{ + LLSD poolSizes{ gSavedSettings.getLLSD("ThreadPoolSizes") }; + LLSD sizeSpec{ poolSizes["General"] }; + LLSD::Integer size{ sizeSpec.isInteger()? sizeSpec.asInteger() : 3 }; + LL_DEBUGS("ThreadPool") << "Instantiating General pool with " + << size << " threads" << LL_ENDL; + // Use a function-static ThreadPool: static duration, but instantiated + // only on demand. + // We don't want anyone, especially the main thread, to have to block + // due to this ThreadPool being full. + static LL::ThreadPool pool("General", size, 1024*1024); +} + void update_texture_fetch() { LLAppViewer::getTextureCache()->update(1); // unpauses the texture cache thread diff --git a/indra/newview/llviewertexture.cpp b/indra/newview/llviewertexture.cpp index e6ac701644..f932acd48c 100644 --- a/indra/newview/llviewertexture.cpp +++ b/indra/newview/llviewertexture.cpp @@ -679,6 +679,9 @@ void LLViewerTexture::init(bool firstinit) mVolumeList[LLRender::LIGHT_TEX].clear(); mVolumeList[LLRender::SCULPT_TEX].clear(); + + mMainQueue = LL::WorkQueue::getInstance("mainloop"); + mImageQueue = LL::WorkQueue::getInstance("LLImageGL"); } //virtual @@ -1622,17 +1625,26 @@ void LLViewerFetchedTexture::scheduleCreateTexture() { mNeedsCreateTexture = TRUE; #if LL_WINDOWS //flip to 0 to revert to single-threaded OpenGL texture uploads - if (!LLImageGLThread::sInstance->post([this]() - { - //actually create the texture on a background thread - createTexture(); - LLImageGLThread::sInstance->postCallback([this]() - { - //finalize on main thread - postCreateTexture(); - unref(); - }); - })) + auto mainq = mMainQueue.lock(); + if (mainq) + { + mainq->postTo( + mImageQueue, + // work to be done on LLImageGL worker thread + [this]() + { + //actually create the texture on a background thread + createTexture(); + }, + // callback to be run on main thread + [this]() + { + //finalize on main thread + postCreateTexture(); + unref(); + }); + } + else #endif { gTextureList.mCreateTextureList.insert(this); diff --git a/indra/newview/llviewertexture.h b/indra/newview/llviewertexture.h index f9f1bfef44..4cd4c7cd39 100644 --- a/indra/newview/llviewertexture.h +++ b/indra/newview/llviewertexture.h @@ -35,6 +35,7 @@ #include "llrender.h" #include "llmetricperformancetester.h" #include "httpcommon.h" +#include "workqueue.h" #include #include @@ -213,6 +214,9 @@ protected: //do not use LLPointer here. LLViewerMediaTexture* mParcelMedia ; + LL::WorkQueue::weak_t mMainQueue; + LL::WorkQueue::weak_t mImageQueue; + static F32 sTexelPixelRatio; public: static const U32 sCurrentFileVersion; -- cgit v1.3 From 04ebc11a2d8a2e59abda5061e35e504fc30504d2 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Wed, 24 Nov 2021 12:56:48 -0500 Subject: SL-16094: Fix WorkQueue test for correct behavior of runFor(). Turns out that one of our WorkQueue integration tests was relying on the incorrect runFor() behavior that we just fixed, so the test broke. Now that runFor() doesn't wait around for work to be posted, use an explicit wait loop instead. To support this, add LLCond::get(functor), where functor must accept a const reference to the stored data. This new get() returns whatever the functor returns, allowing a caller to peek at the stored data. Also use universal references for all remaining LLCond functor arguments. --- indra/llcommon/llcond.h | 52 +++++++++++++++++++++++---------- indra/llcommon/tests/workqueue_test.cpp | 12 ++++++-- 2 files changed, 45 insertions(+), 19 deletions(-) (limited to 'indra/llcommon/tests') diff --git a/indra/llcommon/llcond.h b/indra/llcommon/llcond.h index c08acb66a1..da6e6affe1 100644 --- a/indra/llcommon/llcond.h +++ b/indra/llcommon/llcond.h @@ -67,15 +67,30 @@ public: LLCond(const LLCond&) = delete; LLCond& operator=(const LLCond&) = delete; - /// get() returns the stored DATA by value -- so to use get(), DATA must - /// be copyable. The only way to get a non-const reference -- to modify - /// the stored DATA -- is via update_one() or update_all(). + /** + * get() returns the stored DATA by value -- so to use get(), DATA must + * be copyable. The only way to get a non-const reference -- to modify + * the stored DATA -- is via update_one() or update_all(). + */ value_type get() { LockType lk(mMutex); return mData; } + /** + * get(functor) returns whatever the functor returns. It allows us to peek + * at the stored DATA without copying the whole thing. The functor must + * accept a const reference to DATA. If you want to modify DATA, call + * update_one() or update_all() instead. + */ + template + auto get(FUNC&& func) + { + LockType lk(mMutex); + return std::forward(func)(const_data()); + } + /** * Pass update_one() an invocable accepting non-const (DATA&). The * invocable will presumably modify the referenced DATA. update_one() @@ -86,11 +101,11 @@ public: * update_one() when DATA is a struct or class. */ template - void update_one(MODIFY modify) + void update_one(MODIFY&& modify) { { // scope of lock can/should end before notify_one() LockType lk(mMutex); - modify(mData); + std::forward(modify)(mData); } mCond.notify_one(); } @@ -105,11 +120,11 @@ public: * update_all() when DATA is a struct or class. */ template - void update_all(MODIFY modify) + void update_all(MODIFY&& modify) { { // scope of lock can/should end before notify_all() LockType lk(mMutex); - modify(mData); + std::forward(modify)(mData); } mCond.notify_all(); } @@ -122,7 +137,7 @@ public: * wait() on the condition_variable. */ template - void wait(Pred pred) + void wait(Pred&& pred) { LockType lk(mMutex); // We must iterate explicitly since the predicate accepted by @@ -133,7 +148,7 @@ public: // But what if they instead pass a predicate accepting non-const // (DATA&)? Such a predicate could modify mData, which would be Bad. // Forbid that. - while (! pred(const_cast(mData))) + while (! std::forward(pred)(const_data())) { mCond.wait(lk); } @@ -150,7 +165,7 @@ public: * returning true. */ template - bool wait_for(const std::chrono::duration& timeout_duration, Pred pred) + bool wait_for(const std::chrono::duration& timeout_duration, Pred&& pred) { // Instead of replicating wait_until() logic, convert duration to // time_point and just call wait_until(). @@ -159,7 +174,8 @@ public: // wrong! We'd keep pushing the timeout time farther and farther into // the future. This way, we establish a definite timeout time and // stick to it. - return wait_until(std::chrono::steady_clock::now() + timeout_duration, pred); + return wait_until(std::chrono::steady_clock::now() + timeout_duration, + std::forward(pred)); } /** @@ -169,9 +185,9 @@ public: * generic wait_for() method. */ template - bool wait_for(F32Milliseconds timeout_duration, Pred pred) + bool wait_for(F32Milliseconds timeout_duration, Pred&& pred) { - return wait_for(convert(timeout_duration), pred); + return wait_for(convert(timeout_duration), std::forward(pred)); } protected: @@ -189,6 +205,10 @@ protected: } private: + // It's important to pass a const ref to certain user-specified functors + // that aren't supposed to be able to modify mData. + const value_type& const_data() const { return mData; } + /** * Pass wait_until() a chrono::time_point, indicating the time at which we * should stop waiting, and a predicate accepting (const DATA&), returning @@ -209,21 +229,21 @@ private: * honoring a fixed timeout. */ template - bool wait_until(const std::chrono::time_point& timeout_time, Pred pred) + bool wait_until(const std::chrono::time_point& timeout_time, Pred&& pred) { LockType lk(mMutex); // We advise the caller to pass a predicate accepting (const DATA&). // But what if they instead pass a predicate accepting non-const // (DATA&)? Such a predicate could modify mData, which would be Bad. // Forbid that. - while (! pred(const_cast(mData))) + while (! std::forward(pred)(const_data())) { if (cv_status::timeout == mCond.wait_until(lk, timeout_time)) { // It's possible that wait_until() timed out AND the predicate // became true more or less simultaneously. Even though // wait_until() timed out, check the predicate one more time. - return pred(const_cast(mData)); + return std::forward(pred)(const_data()); } } return true; diff --git a/indra/llcommon/tests/workqueue_test.cpp b/indra/llcommon/tests/workqueue_test.cpp index bea3ad911b..1d73f7aa0d 100644 --- a/indra/llcommon/tests/workqueue_test.cpp +++ b/indra/llcommon/tests/workqueue_test.cpp @@ -99,9 +99,15 @@ namespace tut return (++count < 3); }); // no convenient way to close() our queue while we've got a - // postEvery() running, so run until we think we should have exhausted - // the iterations - queue.runFor(10*interval); + // postEvery() running, so run until we have exhausted the iterations + // or we time out waiting + for (auto finish = start + 10*interval; + WorkQueue::TimePoint::clock::now() < finish && + data.get([](const Shared& data){ return data.size(); }) < 3; ) + { + queue.runPending(); + std::this_thread::sleep_for(interval/10); + } // Take a copy of the captured deque. Shared result = data.get(); ensure_equals("called wrong number of times", result.size(), 3); -- cgit v1.3