summaryrefslogtreecommitdiff
path: root/indra/llcommon
diff options
context:
space:
mode:
Diffstat (limited to 'indra/llcommon')
-rw-r--r--indra/llcommon/CMakeLists.txt3
-rw-r--r--indra/llcommon/llcond.h52
-rw-r--r--indra/llcommon/llpreprocessor.h4
-rw-r--r--indra/llcommon/llsingleton.h14
-rw-r--r--indra/llcommon/llstring.cpp104
-rw-r--r--indra/llcommon/llstring.h225
-rw-r--r--indra/llcommon/llthreadsafequeue.h48
-rw-r--r--indra/llcommon/stdtypes.h7
-rw-r--r--indra/llcommon/stringize.h117
-rw-r--r--indra/llcommon/tests/threadsafeschedule_test.cpp4
-rw-r--r--indra/llcommon/tests/workqueue_test.cpp84
-rw-r--r--indra/llcommon/threadpool.cpp88
-rw-r--r--indra/llcommon/threadpool.h71
-rw-r--r--indra/llcommon/threadsafeschedule.h34
-rw-r--r--indra/llcommon/timing.cpp25
-rw-r--r--indra/llcommon/workqueue.cpp36
-rw-r--r--indra/llcommon/workqueue.h381
17 files changed, 968 insertions, 329 deletions
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/llcond.h b/indra/llcommon/llcond.h
index c08acb66a1..da6e6affe1 100644
--- a/indra/llcommon/llcond.h
+++ b/indra/llcommon/llcond.h
@@ -67,9 +67,11 @@ 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);
@@ -77,6 +79,19 @@ public:
}
/**
+ * 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 <typename FUNC>
+ auto get(FUNC&& func)
+ {
+ LockType lk(mMutex);
+ return std::forward<FUNC>(func)(const_data());
+ }
+
+ /**
* Pass update_one() an invocable accepting non-const (DATA&). The
* invocable will presumably modify the referenced DATA. update_one()
* will lock the mutex, call the invocable and then call notify_one() on
@@ -86,11 +101,11 @@ public:
* update_one() when DATA is a struct or class.
*/
template <typename MODIFY>
- 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>(modify)(mData);
}
mCond.notify_one();
}
@@ -105,11 +120,11 @@ public:
* update_all() when DATA is a struct or class.
*/
template <typename MODIFY>
- 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>(modify)(mData);
}
mCond.notify_all();
}
@@ -122,7 +137,7 @@ public:
* wait() on the condition_variable.
*/
template <typename Pred>
- 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<const value_type&>(mData)))
+ while (! std::forward<Pred>(pred)(const_data()))
{
mCond.wait(lk);
}
@@ -150,7 +165,7 @@ public:
* returning true.
*/
template <typename Rep, typename Period, typename Pred>
- bool wait_for(const std::chrono::duration<Rep, Period>& timeout_duration, Pred pred)
+ bool wait_for(const std::chrono::duration<Rep, Period>& 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>(pred));
}
/**
@@ -169,9 +185,9 @@ public:
* generic wait_for() method.
*/
template <typename Pred>
- 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>(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 <typename Clock, typename Duration, typename Pred>
- bool wait_until(const std::chrono::time_point<Clock, Duration>& timeout_time, Pred pred)
+ bool wait_until(const std::chrono::time_point<Clock, Duration>& 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<const value_type&>(mData)))
+ while (! std::forward<Pred>(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<const value_type&>(mData));
+ return std::forward<Pred>(pred)(const_data());
}
}
return true;
diff --git a/indra/llcommon/llpreprocessor.h b/indra/llcommon/llpreprocessor.h
index b17a8e761a..dc586b0008 100644
--- a/indra/llcommon/llpreprocessor.h
+++ b/indra/llcommon/llpreprocessor.h
@@ -171,7 +171,9 @@
#define LL_DLLIMPORT
#endif // LL_WINDOWS
-#if ! defined(LL_WINDOWS)
+#if __clang__ || ! defined(LL_WINDOWS)
+// Only on Windows, and only with the Microsoft compiler (vs. clang) is
+// wchar_t potentially not a distinct type.
#define LL_WCHAR_T_NATIVE 1
#else // LL_WINDOWS
// https://docs.microsoft.com/en-us/cpp/preprocessor/predefined-macros
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 T>
class LLSimpleton
{
public:
- static T* sInstance;
-
- static void createInstance()
- {
+ template <typename... ARGS>
+ static void createInstance(ARGS&&... args)
+ {
llassert(sInstance == nullptr);
- sInstance = new T();
+ sInstance = new T(std::forward<ARGS>(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 <class T>
diff --git a/indra/llcommon/llstring.cpp b/indra/llcommon/llstring.cpp
index f6f9f97809..bdea1e76ea 100644
--- a/indra/llcommon/llstring.cpp
+++ b/indra/llcommon/llstring.cpp
@@ -212,7 +212,7 @@ S32 utf16chars_to_wchar(const U16* inchars, llwchar* outchar)
return inchars - base;
}
-llutf16string wstring_to_utf16str(const LLWString &utf32str, S32 len)
+llutf16string wstring_to_utf16str(const llwchar* utf32str, size_t len)
{
llutf16string out;
@@ -234,27 +234,19 @@ llutf16string wstring_to_utf16str(const LLWString &utf32str, S32 len)
return out;
}
-llutf16string wstring_to_utf16str(const LLWString &utf32str)
+llutf16string utf8str_to_utf16str( const char* utf8str, size_t len )
{
- const S32 len = (S32)utf32str.length();
- return wstring_to_utf16str(utf32str, len);
-}
-
-llutf16string utf8str_to_utf16str ( const std::string& utf8str )
-{
- LLWString wstr = utf8str_to_wstring ( utf8str );
+ LLWString wstr = utf8str_to_wstring ( utf8str, len );
return wstring_to_utf16str ( wstr );
}
-
-LLWString utf16str_to_wstring(const llutf16string &utf16str, S32 len)
+LLWString utf16str_to_wstring(const U16* utf16str, size_t len)
{
LLWString wout;
- if((len <= 0) || utf16str.empty()) return wout;
+ if (len == 0) return wout;
S32 i = 0;
- // craziness to make gcc happy (llutf16string.c_str() is tweaked on linux):
- const U16* chars16 = &(*(utf16str.begin()));
+ const U16* chars16 = utf16str;
while (i < len)
{
llwchar cur_char;
@@ -264,12 +256,6 @@ LLWString utf16str_to_wstring(const llutf16string &utf16str, S32 len)
return wout;
}
-LLWString utf16str_to_wstring(const llutf16string &utf16str)
-{
- const S32 len = (S32)utf16str.length();
- return utf16str_to_wstring(utf16str, len);
-}
-
// Length in llwchar (UTF-32) of the first len units (16 bits) of the given UTF-16 string.
S32 utf16str_wstring_length(const llutf16string &utf16str, const S32 utf16_len)
{
@@ -389,8 +375,7 @@ S32 wstring_utf8_length(const LLWString& wstr)
return len;
}
-
-LLWString utf8str_to_wstring(const std::string& utf8str, S32 len)
+LLWString utf8str_to_wstring(const char* utf8str, size_t len)
{
LLWString wout;
@@ -478,13 +463,7 @@ LLWString utf8str_to_wstring(const std::string& utf8str, S32 len)
return wout;
}
-LLWString utf8str_to_wstring(const std::string& utf8str)
-{
- const S32 len = (S32)utf8str.length();
- return utf8str_to_wstring(utf8str, len);
-}
-
-std::string wstring_to_utf8str(const LLWString& utf32str, S32 len)
+std::string wstring_to_utf8str(const llwchar* utf32str, size_t len)
{
std::string out;
@@ -500,20 +479,9 @@ std::string wstring_to_utf8str(const LLWString& utf32str, S32 len)
return out;
}
-std::string wstring_to_utf8str(const LLWString& utf32str)
-{
- const S32 len = (S32)utf32str.length();
- return wstring_to_utf8str(utf32str, len);
-}
-
-std::string utf16str_to_utf8str(const llutf16string& utf16str)
+std::string utf16str_to_utf8str(const U16* utf16str, size_t len)
{
- return wstring_to_utf8str(utf16str_to_wstring(utf16str));
-}
-
-std::string utf16str_to_utf8str(const llutf16string& utf16str, S32 len)
-{
- return wstring_to_utf8str(utf16str_to_wstring(utf16str, len), len);
+ return wstring_to_utf8str(utf16str_to_wstring(utf16str, len));
}
std::string utf8str_trim(const std::string& utf8str)
@@ -654,17 +622,16 @@ std::string utf8str_removeCRLF(const std::string& utf8str)
}
#if LL_WINDOWS
-std::string ll_convert_wide_to_string(const wchar_t* in)
+unsigned int ll_wstring_default_code_page()
{
- return ll_convert_wide_to_string(in, CP_UTF8);
+ return CP_UTF8;
}
-std::string ll_convert_wide_to_string(const wchar_t* in, unsigned int code_page)
+std::string ll_convert_wide_to_string(const wchar_t* in, size_t len_in, unsigned int code_page)
{
std::string out;
if(in)
{
- int len_in = wcslen(in);
int len_out = WideCharToMultiByte(
code_page,
0,
@@ -696,12 +663,7 @@ std::string ll_convert_wide_to_string(const wchar_t* in, unsigned int code_page)
return out;
}
-std::wstring ll_convert_string_to_wide(const std::string& in)
-{
- return ll_convert_string_to_wide(in, CP_UTF8);
-}
-
-std::wstring ll_convert_string_to_wide(const std::string& in, unsigned int code_page)
+std::wstring ll_convert_string_to_wide(const char* in, size_t len, unsigned int code_page)
{
// From review:
// We can preallocate a wide char buffer that is the same length (in wchar_t elements) as the utf8 input,
@@ -713,10 +675,10 @@ std::wstring ll_convert_string_to_wide(const std::string& in, unsigned int code_
// reserve an output buffer that will be destroyed on exit, with a place
// to put NULL terminator
- std::vector<wchar_t> w_out(in.length() + 1);
+ std::vector<wchar_t> w_out(len + 1);
memset(&w_out[0], 0, w_out.size());
- int real_output_str_len = MultiByteToWideChar(code_page, 0, in.c_str(), in.length(),
+ int real_output_str_len = MultiByteToWideChar(code_page, 0, in, len,
&w_out[0], w_out.size() - 1);
//looks like MultiByteToWideChar didn't add null terminator to converted string, see EXT-4858.
@@ -726,30 +688,32 @@ std::wstring ll_convert_string_to_wide(const std::string& in, unsigned int code_
return {&w_out[0]};
}
-LLWString ll_convert_wide_to_wstring(const std::wstring& in)
+LLWString ll_convert_wide_to_wstring(const wchar_t* in, size_t len)
{
- // This function, like its converse, is a placeholder, encapsulating a
- // guilty little hack: the only "official" way nat has found to convert
- // between std::wstring (16 bits on Windows) and LLWString (UTF-32) is
- // by using iconv, which we've avoided so far. It kinda sorta works to
- // just copy individual characters...
- // The point is that if/when we DO introduce some more official way to
- // perform such conversions, we should only have to call it here.
- return { in.begin(), in.end() };
+ // Whether or not std::wstring and llutf16string are distinct types, they
+ // both hold UTF-16LE characters. (See header file comments.) Pretend this
+ // wchar_t* sequence is really a U16* sequence and use the conversion we
+ // define above.
+ return utf16str_to_wstring(reinterpret_cast<const U16*>(in), len);
}
-std::wstring ll_convert_wstring_to_wide(const LLWString& in)
+std::wstring ll_convert_wstring_to_wide(const llwchar* in, size_t len)
{
- // See comments in ll_convert_wide_to_wstring()
- return { in.begin(), in.end() };
+ // first, convert to llutf16string, for which we have a real implementation
+ auto utf16str{ wstring_to_utf16str(in, len) };
+ // then, because each U16 char must be UTF-16LE encoded, pretend the U16*
+ // string pointer is a wchar_t* and instantiate a std::wstring of the same
+ // length.
+ return { reinterpret_cast<const wchar_t*>(utf16str.c_str()), utf16str.length() };
}
std::string ll_convert_string_to_utf8_string(const std::string& in)
{
- auto w_mesg = ll_convert_string_to_wide(in, CP_ACP);
- std::string out_utf8(ll_convert_wide_to_string(w_mesg.c_str(), CP_UTF8));
-
- return out_utf8;
+ // If you pass code_page, you must also pass length, otherwise the code
+ // page parameter will be mistaken for length.
+ auto w_mesg = ll_convert_string_to_wide(in, in.length(), CP_ACP);
+ // CP_UTF8 is default -- see ll_wstring_default_code_page() above.
+ return ll_convert_wide_to_string(w_mesg);
}
namespace
diff --git a/indra/llcommon/llstring.h b/indra/llcommon/llstring.h
index 4263122f36..d94f549480 100644
--- a/indra/llcommon/llstring.h
+++ b/indra/llcommon/llstring.h
@@ -27,9 +27,11 @@
#ifndef LL_LLSTRING_H
#define LL_LLSTRING_H
+#include <boost/call_traits.hpp>
#include <boost/optional/optional.hpp>
#include <string>
#include <cstdio>
+#include <cwchar> // std::wcslen()
//#include <locale>
#include <iomanip>
#include <algorithm>
@@ -527,14 +529,71 @@ struct ll_convert_impl<T, T>
T operator()(const T& in) const { return in; }
};
+// simple construction from char*
+template<typename T>
+struct ll_convert_impl<T, const typename T::value_type*>
+{
+ T operator()(const typename T::value_type* in) const { return { in }; }
+};
+
// specialize ll_convert_impl<TO, FROM> to return EXPR
#define ll_convert_alias(TO, FROM, EXPR) \
template<> \
struct ll_convert_impl<TO, FROM> \
{ \
- TO operator()(const FROM& in) const { return EXPR; } \
+ /* param_type optimally passes both char* and string */ \
+ TO operator()(typename boost::call_traits<FROM>::param_type in) const { return EXPR; } \
+}
+
+// If all we're doing is copying characters, pass this to ll_convert_alias as
+// EXPR. Since it expands into the 'return EXPR' slot in the ll_convert_impl
+// specialization above, it implies TO{ in.begin(), in.end() }.
+#define LL_CONVERT_COPY_CHARS { in.begin(), in.end() }
+
+// Generic name for strlen() / wcslen() - the default implementation should
+// (!) work with U16 and llwchar, but we don't intend to engage it.
+template <typename CHARTYPE>
+size_t ll_convert_length(const CHARTYPE* zstr)
+{
+ const CHARTYPE* zp;
+ // classic C string scan
+ for (zp = zstr; *zp; ++zp)
+ ;
+ return (zp - zstr);
}
+// specialize where we have a library function; may use intrinsic operations
+template <>
+inline size_t ll_convert_length<wchar_t>(const wchar_t* zstr) { return std::wcslen(zstr); }
+template <>
+inline size_t ll_convert_length<char> (const char* zstr) { return std::strlen(zstr); }
+
+// ll_convert_forms() is short for a bunch of boilerplate. It defines
+// longname(const char*, len), longname(const char*), longname(const string&)
+// and longname(const string&, len) so calls written pre-ll_convert() will
+// work. Most of these overloads will be unified once we turn on C++17 and can
+// use std::string_view.
+// It also uses aliasmacro to ensure that both ll_convert<OUTSTR>(const char*)
+// and ll_convert<OUTSTR>(const string&) will work.
+#define ll_convert_forms(aliasmacro, OUTSTR, INSTR, longname) \
+LL_COMMON_API OUTSTR longname(const INSTR::value_type* in, size_t len); \
+inline auto longname(const INSTR& in, size_t len) \
+{ \
+ return longname(in.c_str(), len); \
+} \
+inline auto longname(const INSTR::value_type* in) \
+{ \
+ return longname(in, ll_convert_length(in)); \
+} \
+inline auto longname(const INSTR& in) \
+{ \
+ return longname(in.c_str(), in.length()); \
+} \
+/* string param */ \
+aliasmacro(OUTSTR, INSTR, longname(in)); \
+/* char* param */ \
+aliasmacro(OUTSTR, const INSTR::value_type*, longname(in))
+
// Make the incoming string a utf8 string. Replaces any unknown glyph
// with the UNKNOWN_CHARACTER. Once any unknown glyph is found, the rest
// of the data may not be recovered.
@@ -571,63 +630,47 @@ LL_COMMON_API std::string rawstr_to_utf8(const std::string& raw);
// LL_WCHAR_T_NATIVE.
typedef std::basic_string<U16> llutf16string;
-#if ! defined(LL_WCHAR_T_NATIVE)
-// wchar_t is identical to U16, and std::wstring is identical to llutf16string.
-// Defining an ll_convert alias involving llutf16string would collide with the
-// comparable preferred alias involving std::wstring. (In this scenario, if
-// you pass llutf16string, it will engage the std::wstring specialization.)
-#define ll_convert_u16_alias(TO, FROM, EXPR) // nothing
-#else // defined(LL_WCHAR_T_NATIVE)
-// wchar_t is a distinct native type, so llutf16string is also a distinct
-// type, and there IS a point to converting separately to/from llutf16string.
-// (But why? Windows APIs are still defined in terms of wchar_t, and
-// in this scenario llutf16string won't work for them!)
-#define ll_convert_u16_alias(TO, FROM, EXPR) ll_convert_alias(TO, FROM, EXPR)
+// Considering wchar_t, llwchar and U16, there are three relevant cases:
+#if LLWCHAR_IS_WCHAR_T // every which way but Windows
+// llwchar is identical to wchar_t, LLWString is identical to std::wstring.
+// U16 is distinct, llutf16string is distinct (though pretty useless).
+// Given conversions to/from LLWString and to/from llutf16string, conversions
+// involving std::wstring would collide.
+#define ll_convert_wstr_alias(TO, FROM, EXPR) // nothing
+// but we can define conversions involving llutf16string without collisions
+#define ll_convert_u16_alias(TO, FROM, EXPR) ll_convert_alias(TO, FROM, EXPR)
+
+#elif defined(LL_WCHAR_T_NATIVE) // Windows, either clang or MS /Zc:wchar_t
+// llwchar (32-bit), wchar_t (16-bit) and U16 are all different types.
+// Conversions to/from LLWString, to/from std::wstring and to/from llutf16string
+// can all be defined.
+#define ll_convert_wstr_alias(TO, FROM, EXPR) ll_convert_alias(TO, FROM, EXPR)
+#define ll_convert_u16_alias(TO, FROM, EXPR) ll_convert_alias(TO, FROM, EXPR)
+
+#else // ! LL_WCHAR_T_NATIVE: Windows with MS /Zc:wchar_t-
+// wchar_t is identical to U16, std::wstring is identical to llutf16string.
+// Given conversions to/from LLWString and to/from std::wstring, conversions
+// involving llutf16string would collide.
+#define ll_convert_u16_alias(TO, FROM, EXPR) // nothing
+// but we can define conversions involving std::wstring without collisions
+#define ll_convert_wstr_alias(TO, FROM, EXPR) ll_convert_alias(TO, FROM, EXPR)
+#endif
+
+ll_convert_forms(ll_convert_u16_alias, LLWString, llutf16string, utf16str_to_wstring);
+ll_convert_forms(ll_convert_u16_alias, llutf16string, LLWString, wstring_to_utf16str);
+ll_convert_forms(ll_convert_u16_alias, llutf16string, std::string, utf8str_to_utf16str);
+ll_convert_forms(ll_convert_alias, LLWString, std::string, utf8str_to_wstring);
-#if LL_WINDOWS
-// LL_WCHAR_T_NATIVE is defined on non-Windows systems because, in fact,
-// wchar_t is native. Everywhere but Windows, we use it for llwchar (see
-// stdtypes.h). That makes LLWString identical to std::wstring, so these
-// aliases for std::wstring would collide with those for LLWString. Only
-// define on Windows, where converting between std::wstring and llutf16string
-// means copying chars.
-ll_convert_alias(llutf16string, std::wstring, llutf16string(in.begin(), in.end()));
-ll_convert_alias(std::wstring, llutf16string, std::wstring(in.begin(), in.end()));
-#endif // LL_WINDOWS
-#endif // defined(LL_WCHAR_T_NATIVE)
-
-LL_COMMON_API LLWString utf16str_to_wstring(const llutf16string &utf16str, S32 len);
-LL_COMMON_API LLWString utf16str_to_wstring(const llutf16string &utf16str);
-ll_convert_u16_alias(LLWString, llutf16string, utf16str_to_wstring(in));
-
-LL_COMMON_API llutf16string wstring_to_utf16str(const LLWString &utf32str, S32 len);
-LL_COMMON_API llutf16string wstring_to_utf16str(const LLWString &utf32str);
-ll_convert_u16_alias(llutf16string, LLWString, wstring_to_utf16str(in));
-
-LL_COMMON_API llutf16string utf8str_to_utf16str ( const std::string& utf8str, S32 len);
-LL_COMMON_API llutf16string utf8str_to_utf16str ( const std::string& utf8str );
-ll_convert_u16_alias(llutf16string, std::string, utf8str_to_utf16str(in));
-
-LL_COMMON_API LLWString utf8str_to_wstring(const std::string &utf8str, S32 len);
-LL_COMMON_API LLWString utf8str_to_wstring(const std::string &utf8str);
// Same function, better name. JC
inline LLWString utf8string_to_wstring(const std::string& utf8_string) { return utf8str_to_wstring(utf8_string); }
-// best name of all
-ll_convert_alias(LLWString, std::string, utf8string_to_wstring(in));
-//
LL_COMMON_API S32 wchar_to_utf8chars(llwchar inchar, char* outchars);
-LL_COMMON_API std::string wstring_to_utf8str(const LLWString &utf32str, S32 len);
-LL_COMMON_API std::string wstring_to_utf8str(const LLWString &utf32str);
-ll_convert_alias(std::string, LLWString, wstring_to_utf8str(in));
-LL_COMMON_API std::string utf16str_to_utf8str(const llutf16string &utf16str, S32 len);
-LL_COMMON_API std::string utf16str_to_utf8str(const llutf16string &utf16str);
-ll_convert_u16_alias(std::string, llutf16string, utf16str_to_utf8str(in));
+ll_convert_forms(ll_convert_alias, std::string, LLWString, wstring_to_utf8str);
+ll_convert_forms(ll_convert_u16_alias, std::string, llutf16string, utf16str_to_utf8str);
-#if LL_WINDOWS
+// an older alias for utf16str_to_utf8str(llutf16string)
inline std::string wstring_to_utf8str(const llutf16string &utf16str) { return utf16str_to_utf8str(utf16str);}
-#endif
// Length of this UTF32 string in bytes when transformed to UTF8
LL_COMMON_API S32 wstring_utf8_length(const LLWString& wstr);
@@ -701,42 +744,48 @@ LL_COMMON_API std::string utf8str_removeCRLF(const std::string& utf8str);
//@{
/**
- * @brief Convert a wide string to std::string
+ * @brief Convert a wide string to/from std::string
+ * Convert a Windows wide string to/from our LLWString
*
* This replaces the unsafe W2A macro from ATL.
*/
-LL_COMMON_API std::string ll_convert_wide_to_string(const wchar_t* in, unsigned int code_page);
-LL_COMMON_API std::string ll_convert_wide_to_string(const wchar_t* in); // default CP_UTF8
-inline std::string ll_convert_wide_to_string(const std::wstring& in, unsigned int code_page)
-{
- return ll_convert_wide_to_string(in.c_str(), code_page);
-}
-inline std::string ll_convert_wide_to_string(const std::wstring& in)
-{
- return ll_convert_wide_to_string(in.c_str());
-}
-ll_convert_alias(std::string, std::wstring, ll_convert_wide_to_string(in));
-
-/**
- * Converts a string to wide string.
- */
-LL_COMMON_API std::wstring ll_convert_string_to_wide(const std::string& in,
- unsigned int code_page);
-LL_COMMON_API std::wstring ll_convert_string_to_wide(const std::string& in);
- // default CP_UTF8
-ll_convert_alias(std::wstring, std::string, ll_convert_string_to_wide(in));
-
-/**
- * Convert a Windows wide string to our LLWString
- */
-LL_COMMON_API LLWString ll_convert_wide_to_wstring(const std::wstring& in);
-ll_convert_alias(LLWString, std::wstring, ll_convert_wide_to_wstring(in));
-
-/**
- * Convert LLWString to Windows wide string
- */
-LL_COMMON_API std::wstring ll_convert_wstring_to_wide(const LLWString& in);
-ll_convert_alias(std::wstring, LLWString, ll_convert_wstring_to_wide(in));
+// Avoid requiring this header to #include the Windows header file declaring
+// our actual default code_page by delegating this function to our .cpp file.
+LL_COMMON_API unsigned int ll_wstring_default_code_page();
+
+// This is like ll_convert_forms(), with the added complexity of a code page
+// parameter that may or may not be passed.
+#define ll_convert_cp_forms(aliasmacro, OUTSTR, INSTR, longname) \
+/* declare the only nontrivial implementation (in .cpp file) */ \
+LL_COMMON_API OUTSTR longname( \
+ const INSTR::value_type* in, \
+ size_t len, \
+ unsigned int code_page=ll_wstring_default_code_page()); \
+/* if passed only a char pointer, scan for nul terminator */ \
+inline auto longname(const INSTR::value_type* in) \
+{ \
+ return longname(in, ll_convert_length(in)); \
+} \
+/* if passed string and length, extract its char pointer */ \
+inline auto longname( \
+ const INSTR& in, \
+ size_t len, \
+ unsigned int code_page=ll_wstring_default_code_page()) \
+{ \
+ return longname(in.c_str(), len, code_page); \
+} \
+/* if passed only a string object, no scan, pass known length */ \
+inline auto longname(const INSTR& in) \
+{ \
+ return longname(in.c_str(), in.length()); \
+} \
+aliasmacro(OUTSTR, INSTR, longname(in)); \
+aliasmacro(OUTSTR, const INSTR::value_type*, longname(in))
+
+ll_convert_cp_forms(ll_convert_wstr_alias, std::string, std::wstring, ll_convert_wide_to_string);
+ll_convert_cp_forms(ll_convert_wstr_alias, std::wstring, std::string, ll_convert_string_to_wide);
+ ll_convert_forms(ll_convert_wstr_alias, LLWString, std::wstring, ll_convert_wide_to_wstring);
+ ll_convert_forms(ll_convert_wstr_alias, std::wstring, LLWString, ll_convert_wstring_to_wide);
/**
* Converts incoming string into utf8 string
@@ -1937,4 +1986,14 @@ void LLStringUtilBase<T>::truncate(string_type& string, size_type count)
string.resize(count < cur_size ? count : cur_size);
}
+// The good thing about *declaration* macros, vs. usage macros, is that now
+// we're done with them: we don't need them to bleed into the consuming source
+// file.
+#undef ll_convert_alias
+#undef ll_convert_u16_alias
+#undef ll_convert_wstr_alias
+#undef LL_CONVERT_COPY_CHARS
+#undef ll_convert_forms
+#undef ll_convert_cp_forms
+
#endif // LL_STRING_H
diff --git a/indra/llcommon/llthreadsafequeue.h b/indra/llcommon/llthreadsafequeue.h
index 06e8d8f609..2806506550 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,6 +95,11 @@ 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 <typename T>
+ bool pushIfOpen(T&& element);
+
// Try to add an element to the queue without blocking. Returns
// true only if the element was actually added.
template <typename T>
@@ -270,6 +275,7 @@ template <typename ElementT, typename QueueT>
template <typename CALLABLE>
bool LLThreadSafeQueue<ElementT, QueueT>::tryLock(CALLABLE&& callable)
{
+ LL_PROFILE_ZONE_SCOPED;
lock_t lock1(mLock, std::defer_lock);
if (!lock1.try_lock())
return false;
@@ -286,6 +292,7 @@ bool LLThreadSafeQueue<ElementT, QueueT>::tryLockUntil(
const std::chrono::time_point<Clock, Duration>& until,
CALLABLE&& callable)
{
+ LL_PROFILE_ZONE_SCOPED;
lock_t lock1(mLock, std::defer_lock);
if (!lock1.try_lock_until(until))
return false;
@@ -299,6 +306,7 @@ template <typename ElementT, typename QueueT>
template <typename T>
bool LLThreadSafeQueue<ElementT, QueueT>::push_(lock_t& lock, T&& element)
{
+ LL_PROFILE_ZONE_SCOPED;
if (mStorage.size() >= mCapacity)
return false;
@@ -311,9 +319,10 @@ bool LLThreadSafeQueue<ElementT, QueueT>::push_(lock_t& lock, T&& element)
template <typename ElementT, typename QueueT>
-template<typename T>
-void LLThreadSafeQueue<ElementT, QueueT>::push(T&& element)
+template <typename T>
+bool LLThreadSafeQueue<ElementT, QueueT>::pushIfOpen(T&& element)
{
+ LL_PROFILE_ZONE_SCOPED;
lock_t lock1(mLock);
while (true)
{
@@ -321,12 +330,10 @@ void LLThreadSafeQueue<ElementT, QueueT>::push(T&& element)
// drained or not: the moment either end calls close(), further push()
// operations will fail.
if (mClosed)
- {
- LLTHROW(LLThreadSafeQueueInterrupt());
- }
+ return false;
if (push_(lock1, std::forward<T>(element)))
- return;
+ return true;
// Storage Full. Wait for signal.
mCapacityCond.wait(lock1);
@@ -334,10 +341,23 @@ void LLThreadSafeQueue<ElementT, QueueT>::push(T&& element)
}
+template <typename ElementT, typename QueueT>
+template<typename T>
+void LLThreadSafeQueue<ElementT, QueueT>::push(T&& element)
+{
+ LL_PROFILE_ZONE_SCOPED;
+ if (! pushIfOpen(std::forward<T>(element)))
+ {
+ LLTHROW(LLThreadSafeQueueInterrupt());
+ }
+}
+
+
template<typename ElementT, typename QueueT>
template<typename T>
bool LLThreadSafeQueue<ElementT, QueueT>::tryPush(T&& element)
{
+ LL_PROFILE_ZONE_SCOPED;
return tryLock(
[this, element=std::move(element)](lock_t& lock)
{
@@ -354,6 +374,7 @@ bool LLThreadSafeQueue<ElementT, QueueT>::tryPushFor(
const std::chrono::duration<Rep, Period>& timeout,
T&& element)
{
+ LL_PROFILE_ZONE_SCOPED;
// Convert duration to time_point: passing the same timeout duration to
// each of multiple calls is wrong.
return tryPushUntil(std::chrono::steady_clock::now() + timeout,
@@ -367,6 +388,7 @@ bool LLThreadSafeQueue<ElementT, QueueT>::tryPushUntil(
const std::chrono::time_point<Clock, Duration>& until,
T&& element)
{
+ LL_PROFILE_ZONE_SCOPED;
return tryLockUntil(
until,
[this, until, element=std::move(element)](lock_t& lock)
@@ -399,6 +421,7 @@ template <typename ElementT, typename QueueT>
typename LLThreadSafeQueue<ElementT, QueueT>::pop_result
LLThreadSafeQueue<ElementT, QueueT>::pop_(lock_t& lock, ElementT& element)
{
+ LL_PROFILE_ZONE_SCOPED;
// If mStorage is empty, there's no head element.
if (mStorage.empty())
return mClosed? DONE : EMPTY;
@@ -420,6 +443,7 @@ LLThreadSafeQueue<ElementT, QueueT>::pop_(lock_t& lock, ElementT& element)
template<typename ElementT, typename QueueT>
ElementT LLThreadSafeQueue<ElementT, QueueT>::pop(void)
{
+ LL_PROFILE_ZONE_SCOPED;
lock_t lock1(mLock);
ElementT value;
while (true)
@@ -448,6 +472,7 @@ ElementT LLThreadSafeQueue<ElementT, QueueT>::pop(void)
template<typename ElementT, typename QueueT>
bool LLThreadSafeQueue<ElementT, QueueT>::tryPop(ElementT & element)
{
+ LL_PROFILE_ZONE_SCOPED;
return tryLock(
[this, &element](lock_t& lock)
{
@@ -465,6 +490,7 @@ bool LLThreadSafeQueue<ElementT, QueueT>::tryPopFor(
const std::chrono::duration<Rep, Period>& timeout,
ElementT& element)
{
+ LL_PROFILE_ZONE_SCOPED;
// Convert duration to time_point: passing the same timeout duration to
// each of multiple calls is wrong.
return tryPopUntil(std::chrono::steady_clock::now() + timeout, element);
@@ -477,6 +503,7 @@ bool LLThreadSafeQueue<ElementT, QueueT>::tryPopUntil(
const std::chrono::time_point<Clock, Duration>& until,
ElementT& element)
{
+ LL_PROFILE_ZONE_SCOPED;
return tryLockUntil(
until,
[this, until, &element](lock_t& lock)
@@ -496,6 +523,7 @@ LLThreadSafeQueue<ElementT, QueueT>::tryPopUntil_(
const std::chrono::time_point<Clock, Duration>& until,
ElementT& element)
{
+ LL_PROFILE_ZONE_SCOPED;
while (true)
{
pop_result popped = pop_(lock, element);
@@ -522,6 +550,7 @@ LLThreadSafeQueue<ElementT, QueueT>::tryPopUntil_(
template<typename ElementT, typename QueueT>
size_t LLThreadSafeQueue<ElementT, QueueT>::size(void)
{
+ LL_PROFILE_ZONE_SCOPED;
lock_t lock(mLock);
return mStorage.size();
}
@@ -530,6 +559,7 @@ size_t LLThreadSafeQueue<ElementT, QueueT>::size(void)
template<typename ElementT, typename QueueT>
void LLThreadSafeQueue<ElementT, QueueT>::close()
{
+ LL_PROFILE_ZONE_SCOPED;
lock_t lock(mLock);
mClosed = true;
lock.unlock();
@@ -543,6 +573,7 @@ void LLThreadSafeQueue<ElementT, QueueT>::close()
template<typename ElementT, typename QueueT>
bool LLThreadSafeQueue<ElementT, QueueT>::isClosed()
{
+ LL_PROFILE_ZONE_SCOPED;
lock_t lock(mLock);
return mClosed;
}
@@ -551,6 +582,7 @@ bool LLThreadSafeQueue<ElementT, QueueT>::isClosed()
template<typename ElementT, typename QueueT>
bool LLThreadSafeQueue<ElementT, QueueT>::done()
{
+ LL_PROFILE_ZONE_SCOPED;
lock_t lock(mLock);
return mClosed && mStorage.empty();
}
diff --git a/indra/llcommon/stdtypes.h b/indra/llcommon/stdtypes.h
index 887f6ab733..b07805b628 100644
--- a/indra/llcommon/stdtypes.h
+++ b/indra/llcommon/stdtypes.h
@@ -42,10 +42,17 @@ typedef unsigned int U32;
// Windows wchar_t is 16-bit, whichever way /Zc:wchar_t is set. In effect,
// Windows wchar_t is always a typedef, either for unsigned short or __wchar_t.
// (__wchar_t, available either way, is Microsoft's native 2-byte wchar_t type.)
+// The version of clang available with VS 2019 also defines wchar_t as __wchar_t
+// which is also 16 bits.
// In any case, llwchar should be a UTF-32 type.
typedef U32 llwchar;
#else
typedef wchar_t llwchar;
+// What we'd actually want is a simple module-scope 'if constexpr' to test
+// std::is_same<wchar_t, llwchar>::value and use that to define, or not
+// define, string conversion specializations. Since we don't have that, we'll
+// have to rely on #if instead. Sorry, Dr. Stroustrup.
+#define LLWCHAR_IS_WCHAR_T 1
#endif
#if LL_WINDOWS
diff --git a/indra/llcommon/stringize.h b/indra/llcommon/stringize.h
index 38dd198ad3..12df693910 100644
--- a/indra/llcommon/stringize.h
+++ b/indra/llcommon/stringize.h
@@ -31,58 +31,109 @@
#include <sstream>
#include <llstring.h>
+#include <boost/call_traits.hpp>
/**
- * gstringize(item) encapsulates an idiom we use constantly, using
- * operator<<(std::ostringstream&, TYPE) followed by std::ostringstream::str()
- * or their wstring equivalents
- * to render a string expressing some item.
+ * stream_to(std::ostream&, items, ...) streams each item in the parameter list
+ * to the passed std::ostream using the insertion operator <<. This can be
+ * used, for instance, to make a simple print() function, e.g.:
+ *
+ * @code
+ * template <typename... Items>
+ * void print(Items&&... items)
+ * {
+ * stream_to(std::cout, std::forward<Items>(items)...);
+ * }
+ * @endcode
*/
-template <typename CHARTYPE, typename T>
-std::basic_string<CHARTYPE> gstringize(const T& item)
+// recursion tail
+template <typename CHARTYPE>
+void stream_to(std::basic_ostream<CHARTYPE>& out) {}
+// stream one or more items
+template <typename CHARTYPE, typename T, typename... Items>
+void stream_to(std::basic_ostream<CHARTYPE>& out, T&& item, Items&&... items)
{
- std::basic_ostringstream<CHARTYPE> out;
- out << item;
- return out.str();
+ out << std::forward<T>(item);
+ stream_to(out, std::forward<Items>(items)...);
}
+// why we use function overloads, not function template specializations:
+// http://www.gotw.ca/publications/mill17.htm
+
/**
- *partial specialization of stringize for handling wstring
- *TODO: we should have similar specializations for wchar_t[] but not until it is needed.
+ * gstringize(item, ...) encapsulates an idiom we use constantly, using
+ * operator<<(std::ostringstream&, TYPE) followed by std::ostringstream::str()
+ * or their wstring equivalents to render a string expressing one or more items.
*/
-inline std::string stringize(const std::wstring& item)
+// two or more args - the case of a single argument is handled separately
+template <typename CHARTYPE, typename T0, typename T1, typename... Items>
+auto gstringize(T0&& item0, T1&& item1, Items&&... items)
{
- return wstring_to_utf8str(item);
+ std::basic_ostringstream<CHARTYPE> out;
+ stream_to(out, std::forward<T0>(item0), std::forward<T1>(item1),
+ std::forward<Items>(items)...);
+ return out.str();
}
-/**
- * Specialization of gstringize for std::string return types
- */
-template <typename T>
-std::string stringize(const T& item)
+// generic single argument: stream to out, as above
+template <typename CHARTYPE, typename T>
+struct gstringize_impl
{
- return gstringize<char>(item);
+ auto operator()(typename boost::call_traits<T>::param_type arg)
+ {
+ std::basic_ostringstream<CHARTYPE> out;
+ out << arg;
+ return out.str();
+ }
+};
+
+// partially specialize for a single STRING argument -
+// note that ll_convert<T>(T) already handles the trivial case
+template <typename OUTCHAR, typename INCHAR>
+struct gstringize_impl<OUTCHAR, std::basic_string<INCHAR>>
+{
+ auto operator()(const std::basic_string<INCHAR>& arg)
+ {
+ return ll_convert<std::basic_string<OUTCHAR>>(arg);
+ }
+};
+
+// partially specialize for a single CHARTYPE* argument -
+// since it's not a basic_string and we do want to optimize this common case
+template <typename OUTCHAR, typename INCHAR>
+struct gstringize_impl<OUTCHAR, INCHAR*>
+{
+ auto operator()(const INCHAR* arg)
+ {
+ return ll_convert<std::basic_string<OUTCHAR>>(arg);
+ }
+};
+
+// gstringize(single argument)
+template <typename CHARTYPE, typename T>
+auto gstringize(T&& item)
+{
+ // use decay<T> so we don't require separate specializations
+ // for T, const T, T&, const T& ...
+ return gstringize_impl<CHARTYPE, std::decay_t<T>>()(std::forward<T>(item));
}
/**
- * Specialization for generating wstring from string.
- * Both a convenience function and saves a miniscule amount of overhead.
+ * Specialization of gstringize for std::string return types
*/
-inline std::wstring wstringize(const std::string& item)
+template <typename... Items>
+auto stringize(Items&&... items)
{
- // utf8str_to_wstring() returns LLWString, which isn't necessarily the
- // same as std::wstring
- LLWString s(utf8str_to_wstring(item));
- return std::wstring(s.begin(), s.end());
+ return gstringize<char>(std::forward<Items>(items)...);
}
/**
* Specialization of gstringize for std::wstring return types
*/
-template <typename T>
-std::wstring wstringize(const T& item)
+template <typename... Items>
+auto wstringize(Items&&... items)
{
- return gstringize<wchar_t>(item);
+ return gstringize<wchar_t>(std::forward<Items>(items)...);
}
/**
@@ -146,11 +197,9 @@ void destringize_f(std::basic_string<CHARTYPE> const & str, Functor const & f)
* std::istringstream in(str);
* in >> item1 >> item2 >> item3 ... ;
* @endcode
- * @NOTE - once we get generic lambdas, we shouldn't need DEWSTRINGIZE() any
- * more since DESTRINGIZE() should do the right thing with a std::wstring. But
- * until then, the lambda we pass must accept the right std::basic_istream.
*/
-#define DESTRINGIZE(STR, EXPRESSION) (destringize_f((STR), [&](std::istream& in){in >> EXPRESSION;}))
-#define DEWSTRINGIZE(STR, EXPRESSION) (destringize_f((STR), [&](std::wistream& in){in >> EXPRESSION;}))
+#define DESTRINGIZE(STR, EXPRESSION) (destringize_f((STR), [&](auto& in){in >> EXPRESSION;}))
+// legacy name, just use DESTRINGIZE() going forward
+#define DEWSTRINGIZE(STR, EXPRESSION) DESTRINGIZE(STR, EXPRESSION)
#endif /* ! defined(LL_STRINGIZE_H) */
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..1d73f7aa0d 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"
@@ -96,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);
@@ -138,7 +147,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 +166,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<WorkQueue::Error>(
+ [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
new file mode 100644
index 0000000000..ba914035e2
--- /dev/null
+++ b/indra/llcommon/threadpool.cpp
@@ -0,0 +1,88 @@
+/**
+ * @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),
+ mThreadCount(threads)
+{}
+
+void LL::ThreadPool::start()
+{
+ for (size_t i = 0; i < mThreadCount; ++i)
+ {
+ std::string tname{ stringize(mName, ':', (i+1), '/', mThreadCount) };
+ mThreads.emplace_back(tname, [this, tname]()
+ {
+ LL_PROFILER_SET_THREAD_NAME(tname.c_str());
+ 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
new file mode 100644
index 0000000000..b79c9b9090
--- /dev/null
+++ b/indra/llcommon/threadpool.h
@@ -0,0 +1,71 @@
+/**
+ * @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 <string>
+#include <thread>
+#include <utility> // std::pair
+#include <vector>
+
+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();
+
+ /**
+ * Launch the ThreadPool. Until this call, a constructed ThreadPool
+ * launches no threads. That permits coders to derive from ThreadPool,
+ * or store it as a member of some other class, but refrain from
+ * launching it until all other construction is complete.
+ */
+ void start();
+
+ /**
+ * 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;
+ size_t mThreadCount;
+ std::vector<std::pair<std::string, std::thread>> mThreads;
+ };
+
+} // namespace LL
+
+#endif /* ! defined(LL_THREADPOOL_H) */
diff --git a/indra/llcommon/threadsafeschedule.h b/indra/llcommon/threadsafeschedule.h
index c8ad23532b..601681d550 100644
--- a/indra/llcommon/threadsafeschedule.h
+++ b/indra/llcommon/threadsafeschedule.h
@@ -98,12 +98,14 @@ namespace LL
// we could minimize redundancy by breaking out a common base class...
void push(const DataTuple& tuple)
{
+ LL_PROFILE_ZONE_SCOPED;
push(tuple_cons(Clock::now(), tuple));
}
/// individually pass each component of the TimeTuple
void push(const TimePoint& time, Args&&... args)
{
+ LL_PROFILE_ZONE_SCOPED;
push(TimeTuple(time, std::forward<Args>(args)...));
}
@@ -114,6 +116,7 @@ namespace LL
// and call that overload.
void push(Args&&... args)
{
+ LL_PROFILE_ZONE_SCOPED;
push(Clock::now(), std::forward<Args>(args)...);
}
@@ -124,18 +127,21 @@ namespace LL
/// DataTuple with implicit now
bool tryPush(const DataTuple& tuple)
{
+ LL_PROFILE_ZONE_SCOPED;
return tryPush(tuple_cons(Clock::now(), tuple));
}
/// individually pass components
bool tryPush(const TimePoint& time, Args&&... args)
{
+ LL_PROFILE_ZONE_SCOPED;
return tryPush(TimeTuple(time, std::forward<Args>(args)...));
}
/// individually pass components with implicit now
bool tryPush(Args&&... args)
{
+ LL_PROFILE_ZONE_SCOPED;
return tryPush(Clock::now(), std::forward<Args>(args)...);
}
@@ -148,6 +154,7 @@ namespace LL
bool tryPushFor(const std::chrono::duration<Rep, Period>& timeout,
const DataTuple& tuple)
{
+ LL_PROFILE_ZONE_SCOPED;
return tryPushFor(timeout, tuple_cons(Clock::now(), tuple));
}
@@ -156,6 +163,7 @@ namespace LL
bool tryPushFor(const std::chrono::duration<Rep, Period>& timeout,
const TimePoint& time, Args&&... args)
{
+ LL_PROFILE_ZONE_SCOPED;
return tryPushFor(TimeTuple(time, std::forward<Args>(args)...));
}
@@ -164,6 +172,7 @@ namespace LL
bool tryPushFor(const std::chrono::duration<Rep, Period>& timeout,
Args&&... args)
{
+ LL_PROFILE_ZONE_SCOPED;
return tryPushFor(Clock::now(), std::forward<Args>(args)...);
}
@@ -176,6 +185,7 @@ namespace LL
bool tryPushUntil(const std::chrono::time_point<Clock, Duration>& until,
const DataTuple& tuple)
{
+ LL_PROFILE_ZONE_SCOPED;
return tryPushUntil(until, tuple_cons(Clock::now(), tuple));
}
@@ -184,6 +194,7 @@ namespace LL
bool tryPushUntil(const std::chrono::time_point<Clock, Duration>& until,
const TimePoint& time, Args&&... args)
{
+ LL_PROFILE_ZONE_SCOPED;
return tryPushUntil(until, TimeTuple(time, std::forward<Args>(args)...));
}
@@ -192,6 +203,7 @@ namespace LL
bool tryPushUntil(const std::chrono::time_point<Clock, Duration>& until,
Args&&... args)
{
+ LL_PROFILE_ZONE_SCOPED;
return tryPushUntil(until, Clock::now(), std::forward<Args>(args)...);
}
@@ -209,12 +221,14 @@ namespace LL
// haven't yet jumped through those hoops.
DataTuple pop()
{
+ LL_PROFILE_ZONE_SCOPED;
return tuple_cdr(popWithTime());
}
/// pop TimeTuple by value
TimeTuple popWithTime()
{
+ LL_PROFILE_ZONE_SCOPED;
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*
@@ -254,6 +268,7 @@ namespace LL
/// tryPop(DataTuple&)
bool tryPop(DataTuple& tuple)
{
+ LL_PROFILE_ZONE_SCOPED;
TimeTuple tt;
if (! super::tryPop(tt))
return false;
@@ -264,6 +279,7 @@ namespace LL
/// for when Args has exactly one type
bool tryPop(typename std::tuple_element<1, TimeTuple>::type& value)
{
+ LL_PROFILE_ZONE_SCOPED;
TimeTuple tt;
if (! super::tryPop(tt))
return false;
@@ -275,6 +291,7 @@ namespace LL
template <typename Rep, typename Period, typename Tuple>
bool tryPopFor(const std::chrono::duration<Rep, Period>& timeout, Tuple& tuple)
{
+ LL_PROFILE_ZONE_SCOPED;
// It's important to use OUR tryPopUntil() implementation, rather
// than delegating immediately to our base class.
return tryPopUntil(Clock::now() + timeout, tuple);
@@ -285,6 +302,7 @@ namespace LL
bool tryPopUntil(const std::chrono::time_point<Clock, Duration>& until,
TimeTuple& tuple)
{
+ LL_PROFILE_ZONE_SCOPED;
// 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
@@ -304,20 +322,25 @@ namespace LL
pop_result tryPopUntil_(lock_t& lock, const TimePoint& until, TimeTuple& tuple)
{
+ LL_PROFILE_ZONE_SCOPED;
TimePoint adjusted = until;
if (! super::mStorage.empty())
{
+ LL_PROFILE_ZONE_NAMED("tpu - adjust");
// 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_()
pop_result popped;
- while ((popped = pop_result(super::tryPopUntil_(lock, adjusted, tuple))) == 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.
+ LL_PROFILE_ZONE_NAMED("tpu - super");
+ while ((popped = pop_result(super::tryPopUntil_(lock, adjusted, tuple))) == 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;
}
@@ -327,6 +350,7 @@ namespace LL
bool tryPopUntil(const std::chrono::time_point<Clock, Duration>& until,
DataTuple& tuple)
{
+ LL_PROFILE_ZONE_SCOPED;
TimeTuple tt;
if (! tryPopUntil(until, tt))
return false;
@@ -339,6 +363,7 @@ namespace LL
bool tryPopUntil(const std::chrono::time_point<Clock, Duration>& until,
typename std::tuple_element<1, TimeTuple>::type& value)
{
+ LL_PROFILE_ZONE_SCOPED;
TimeTuple tt;
if (! tryPopUntil(until, tt))
return false;
@@ -362,6 +387,7 @@ namespace LL
// considering whether to deliver the current head element
bool canPop(const TimeTuple& head) const override
{
+ LL_PROFILE_ZONE_SCOPED;
// 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();
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 b32357e832..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,12 +39,28 @@ 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
{
for (;;)
{
+ LL_PROFILE_ZONE_SCOPED;
callWork(mQueue.pop());
}
}
@@ -74,10 +91,11 @@ bool LL::WorkQueue::runOne()
bool LL::WorkQueue::runUntil(const TimePoint& until)
{
+ LL_PROFILE_ZONE_SCOPED;
// 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))
+ // runUntil() is simply a time-bounded runPending().
+ for (Work work; TimePoint::clock::now() < until && mQueue.tryPop(work); )
{
callWork(work);
}
@@ -128,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 5ec790da79..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 <chrono>
+#include <exception> // std::current_exception
#include <functional> // std::function
-#include <queue>
#include <string>
-#include <utility> // std::pair
-#include <vector>
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 <typename CALLABLE>
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)));
}
@@ -83,6 +109,47 @@ namespace LL
}
/**
+ * post work for a particular time, unless the queue is closed before
+ * we can post
+ */
+ template <typename CALLABLE>
+ 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 <typename CALLABLE>
+ 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 <typename CALLABLE>
+ 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 <typename CALLABLE>
+ static bool postMaybe(weak_t target, CALLABLE&& callable)
+ {
+ return postMaybe(target, TimePoint::clock::now(),
+ std::forward<CALLABLE>(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 <typename CALLABLE, typename FOLLOWUP>
- 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 <typename CALLABLE, typename FOLLOWUP>
- 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));
+ 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 <typename CALLABLE>
+ 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 <typename CALLABLE>
+ auto waitForResult(CALLABLE&& callable)
+ {
+ return waitForResult(TimePoint::clock::now(), std::move(callable));
}
/*--------------------------- worker API ---------------------------*/
@@ -222,6 +260,7 @@ namespace LL
template <typename Rep, typename Period>
bool runFor(const std::chrono::duration<Rep, Period>& timeslice)
{
+ LL_PROFILE_ZONE_SCOPED;
return runUntil(TimePoint::clock::now() + timeslice);
}
@@ -232,6 +271,23 @@ namespace LL
bool runUntil(const TimePoint& until);
private:
+ template <typename CALLABLE, typename FOLLOWUP>
+ static auto makeReplyLambda(CALLABLE&& callable, FOLLOWUP&& callback);
+ /// general case: arbitrary C++ return type
+ template <typename CALLABLE, typename FOLLOWUP, typename RETURNTYPE>
+ struct MakeReplyLambda;
+ /// specialize for CALLABLE returning void
+ template <typename CALLABLE, typename FOLLOWUP>
+ struct MakeReplyLambda<CALLABLE, FOLLOWUP, void>;
+
+ /// general case: arbitrary C++ return type
+ template <typename CALLABLE, typename RETURNTYPE>
+ struct WaitForResult;
+ /// specialize for CALLABLE returning void
+ template <typename CALLABLE>
+ struct WaitForResult<CALLABLE, void>;
+
+ 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);
@@ -253,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<Rep, Period>& interval,
CALLABLE&& callable):
mTarget(target),
@@ -301,8 +357,8 @@ namespace LL
}
private:
- WorkQueue::weak_t mTarget;
- WorkQueue::TimePoint mStart;
+ weak_t mTarget;
+ TimePoint mStart;
std::chrono::duration<Rep, Period> mInterval;
CALLABLE mCallable;
};
@@ -330,6 +386,189 @@ namespace LL
getWeak(), TimePoint::clock::now(), interval, std::move(callable)));
}
+ /// general case: arbitrary C++ return type
+ template <typename CALLABLE, typename FOLLOWUP, typename RETURNTYPE>
+ 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>(callable)(),
+ callback = std::move(callback)]
+ ()
+ { callback(std::move(result)); };
+ }
+ };
+
+ /// specialize for CALLABLE returning void
+ template <typename CALLABLE, typename FOLLOWUP>
+ struct WorkQueue::MakeReplyLambda<CALLABLE, FOLLOWUP, void>
+ {
+ auto operator()(CALLABLE&& callable, FOLLOWUP&& callback)
+ {
+ // Call the callable, which produces no result.
+ std::forward<CALLABLE>(callable)();
+ // Our completion callback is simply the caller's callback.
+ return std::move(callback);
+ }
+ };
+
+ template <typename CALLABLE, typename FOLLOWUP>
+ auto WorkQueue::makeReplyLambda(CALLABLE&& callable, FOLLOWUP&& callback)
+ {
+ return MakeReplyLambda<CALLABLE, FOLLOWUP,
+ decltype(std::forward<CALLABLE>(callable)())>()
+ (std::move(callable), std::move(callback));
+ }
+
+ template <typename CALLABLE, typename FOLLOWUP>
+ bool WorkQueue::postTo(weak_t target,
+ const TimePoint& time, CALLABLE&& callable, FOLLOWUP&& callback)
+ {
+ LL_PROFILE_ZONE_SCOPED;
+ // 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 <typename CALLABLE>
+ bool WorkQueue::postMaybe(weak_t target, const TimePoint& time, CALLABLE&& callable)
+ {
+ LL_PROFILE_ZONE_SCOPED;
+ // 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>(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 <typename CALLABLE, typename RETURNTYPE>
+ struct WorkQueue::WaitForResult
+ {
+ auto operator()(WorkQueue* self, const TimePoint& time, CALLABLE&& callable)
+ {
+ LLCoros::Promise<RETURNTYPE> 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 <typename CALLABLE>
+ struct WorkQueue::WaitForResult<CALLABLE, void>
+ {
+ void operator()(WorkQueue* self, const TimePoint& time, CALLABLE&& callable)
+ {
+ LLCoros::Promise<void> 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 <typename CALLABLE>
+ auto WorkQueue::waitForResult(const TimePoint& time, CALLABLE&& callable)
+ {
+ checkCoroutine("waitForResult()");
+ // derive callable's return type so we can specialize for void
+ return WaitForResult<CALLABLE, decltype(std::forward<CALLABLE>(callable)())>()
+ (this, time, std::forward<CALLABLE>(callable));
+ }
+
} // namespace LL
#endif /* ! defined(LL_WORKQUEUE_H) */