From b347ad5deb1c9abb210ac5da0534766bf5b6f2f0 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 27 Jun 2024 18:06:13 -0400 Subject: Make test.cpp test driver recognize LOGTEST_testname. Setting LOGTEST=DEBUG, when many unit/integration tests must be rebuilt and run, can result in lots of unnecessary output. When we only want DEBUG log output from a specific test program, make test.cpp recognize an environment variable LOGTEST_testname, where 'testname' might be the full basename of the executable, or part of INTEGRATION_TEST_testname or PROJECT_foo_TEST_testname. When test.cpp notices a non-empty variable by that name, it behaves as if LOGTEST were set to that value. --- indra/test/test.cpp | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/indra/test/test.cpp b/indra/test/test.cpp index 61a4eb07c5..3cbac41f4a 100644 --- a/indra/test/test.cpp +++ b/indra/test/test.cpp @@ -35,13 +35,14 @@ */ #include "linden_common.h" -#include "llerrorcontrol.h" -#include "lltut.h" #include "chained_callback.h" -#include "stringize.h" -#include "namedtempfile.h" +#include "fsyspath.h" +#include "llerrorcontrol.h" #include "lltrace.h" #include "lltracethreadrecorder.h" +#include "lltut.h" +#include "namedtempfile.h" +#include "stringize.h" #include "apr_pools.h" #include "apr_getopt.h" @@ -545,6 +546,27 @@ int main(int argc, char **argv) // LOGTEST overrides default, but can be overridden by --debug. const char* LOGTEST = getenv("LOGTEST"); + // Sometimes we must rebuild much of the viewer before we get to the + // specific test we want to monitor, and some viewer integration tests are + // quite verbose. In addition to noticing plain LOGTEST= (for all tests), + // also notice LOGTEST_progname= (for a specific test). + std::string basename{ fsyspath(argv[0]).stem() }; + // don't make user set LOGTEST_INTEGRATION_TEST_progname or (worse) + // LOGTEST_PROJECT_foo_TEST_bar -- only LOGTEST_progname or LOGTEST_bar + auto _TEST_ = basename.find("_TEST_"); + if (_TEST_ != std::string::npos) + { + basename.erase(0, _TEST_+6); + } + std::string LOGTEST_prog_key{ "LOGTEST_" + basename }; + const char* LOGTEST_prog = getenv(LOGTEST_prog_key.c_str()); +// std::cout << LOGTEST_prog_key << "='" << (LOGTEST_prog? LOGTEST_prog : "") << "'" << std::endl; + if (LOGTEST_prog && *LOGTEST_prog) + { + LOGTEST = LOGTEST_prog; + std::cout << "LOGTEST='" << LOGTEST << "' from " << LOGTEST_prog_key << std::endl; + } + // values used for options parsing apr_status_t apr_err; const char* opt_arg = NULL; -- cgit v1.2.3 From 6ee98f4e9b5ea9ade06056a9d1f4c1e0b1c00b44 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 27 Jun 2024 19:01:28 -0400 Subject: Make lua_emplace() use Luau userdata tags with destructors. It turns out that Luau does not honor PUC-Rio Lua's __gc metafunction, so despite elaborate measures, the previous lua_emplace() implementation would not have destroyed the contained C++ T object when the resulting userdata object was garbage-collected. Moreover, using LL.atexit() as the mechanism to destroy lua_emplace() userdata objects (e.g. LuaListener) would have been slightly fragile because we also want to use LL.atexit() to make the final fiber.run() call, when appropriate. Introducing an order dependency between fiber.run() and the LuaListener destructor would not be robust. Both of those problems are addressed by leveraging one of Luau's extensions over PUC-Rio Lua. A Luau userdata object can have an int tag; and a tag can have an associated C++ destructor function. When any userdata object bearing that tag is garbage-collected, Luau will call that destructor; and Luau's lua_close() function destroys all userdata objects. The resulting lua_emplace() and lua_toclass() code is far simpler. It only remains to generate a distinct int tag value for each different C++ type passed to the lua_emplace() template. unordered_map addresses that need. --- indra/llcommon/lua_function.h | 223 +++++++++--------------------------------- 1 file changed, 48 insertions(+), 175 deletions(-) diff --git a/indra/llcommon/lua_function.h b/indra/llcommon/lua_function.h index 9cdd5665dc..284f911fa8 100644 --- a/indra/llcommon/lua_function.h +++ b/indra/llcommon/lua_function.h @@ -21,8 +21,9 @@ #include "stringize.h" #include // std::uncaught_exceptions() #include // std::shared_ptr -#include +#include #include +#include #include // std::pair class LuaListener; @@ -196,28 +197,33 @@ int name##_luasub::call(lua_State* L) *****************************************************************************/ namespace { -// this closure function retrieves its bound argument to pass to -// lua_emplace_gc() -template -int lua_emplace_call_gc(lua_State* L); -// this will be the function called by the new userdata's metatable's __gc() -template -int lua_emplace_gc(lua_State* L); -// name by which we'll store the new userdata's metatable in the Registry -template -std::string lua_emplace_metaname(const std::string& Tname = LLError::Log::classname()); +// If we start engaging lua_emplace() from more than one thread, type_tags +// will need locking. +std::unordered_map type_tags; + +// find or create a new Luau userdata "tag" for type T +template +int type_tag() +{ + // The first time we encounter a given type T, assign a new distinct tag + // value based on the number of previously-created tags. But avoid tag 0, + // which is evidently the default for userdata objects created without + // explicit tags. Don't try to destroy a nonexistent T object in a random + // userdata object! + auto [entry, created] = type_tags.emplace(std::type_index(typeid(T)), int(type_tags.size()+1)); + // Luau only permits up to LUA_UTAG_LIMIT distinct userdata tags (ca. 128) + llassert(entry->second < LUA_UTAG_LIMIT); + return entry->second; +} } // anonymous namespace /** * On the stack belonging to the passed lua_State, push a Lua userdata object - * with a newly-constructed C++ object std::optional(args...). The new - * userdata has a metadata table with a __gc() function to ensure that when - * the userdata instance is garbage-collected, ~T() is called. Also call - * LL.atexit(lua_emplace_call_gc(object)) to make ~LuaState() call ~T(). - * - * We wrap the userdata object as std::optional so we can explicitly - * destroy the contained T, and detect that we've done so. + * containing a newly-constructed C++ object T(args...). The userdata has a + * Luau destructor guaranteeing that the new T instance is destroyed when the + * userdata is garbage-collected, no later than when the LuaState is + * destroyed. * * Usage: * lua_emplace(L, T constructor args...); @@ -226,178 +232,45 @@ std::string lua_emplace_metaname(const std::string& Tname = LLError::Log::classn template void lua_emplace(lua_State* L, ARGS&&... args) { - using optT = std::optional; - luaL_checkstack(L, 5, nullptr); - auto ptr = lua_newuserdata(L, sizeof(optT)); + luaL_checkstack(L, 1, nullptr); + int tag{ type_tag() }; + if (! lua_getuserdatadtor(L, tag)) + { + // We haven't yet told THIS lua_State the destructor to use for this tag. + lua_setuserdatadtor( + L, tag, + [](lua_State*, void* ptr) + { + // destroy the contained T instance + static_cast(ptr)->~T(); + }); + } + auto ptr = lua_newuserdatatagged(L, sizeof(T), tag); // stack is uninitialized userdata // For now, assume (but verify) that lua_newuserdata() returns a // conservatively-aligned ptr. If that turns out not to be the case, we // might have to discard the new userdata, overallocate its successor and // perform manual alignment -- but only if we must. - llassert((uintptr_t(ptr) % alignof(optT)) == 0); + llassert((uintptr_t(ptr) % alignof(T)) == 0); // Construct our T there using placement new - new (ptr) optT(std::in_place, std::forward(args)...); - // stack is now initialized userdata containing our T instance - - // Find or create the metatable shared by all userdata instances holding - // C++ type T. We want it to be shared across instances, but it must be - // type-specific because its __gc field is lua_emplace_gc. - auto Tname{ LLError::Log::classname() }; - auto metaname{ lua_emplace_metaname(Tname) }; - if (luaL_newmetatable(L, metaname.c_str())) - { - // just created it: populate it - auto gcname{ stringize("lua_emplace_gc<", Tname, ">") }; - lua_pushcfunction(L, lua_emplace_gc, gcname.c_str()); - // stack is userdata, metatable, lua_emplace_gc - lua_setfield(L, -2, "__gc"); - } - // stack is userdata, metatable - lua_setmetatable(L, -2); - // Stack is now userdata, initialized with T(args), - // with metatable.__gc pointing to lua_emplace_gc. - - // But wait, there's more! Use our atexit() function to ensure that this - // C++ object is eventually destroyed even if the garbage collector never - // gets around to it. - lua_getglobal(L, "LL"); - // stack contains userdata, LL - lua_getfield(L, -1, "atexit"); - // stack contains userdata, LL, LL.atexit - // ditch LL - lua_replace(L, -2); - // stack contains userdata, LL.atexit - - // We have a bit of a problem here. We want to allow the garbage collector - // to collect the userdata if it must; but we also want to register a - // cleanup function to destroy the value if (usual case) it has NOT been - // garbage-collected. The problem is that if we bind into atexit()'s queue - // a strong reference to the userdata, we ensure that the garbage - // collector cannot collect it, making our metatable with __gc function - // completely moot. And we must assume that lua_pushcclosure() binds a - // strong reference to each value passed as a closure. - - // The solution is to use one more indirection: create a weak table whose - // sole entry is the userdata. If all other references to the new userdata - // are forgotten, so the only remaining reference is the weak table, the - // userdata can be collected. Then we can bind that weak table as the - // closure value for our cleanup function. - // The new weak table will have at most 1 array value, 0 other keys. - lua_createtable(L, 1, 0); - // stack contains userdata, LL.atexit, weak_table - if (luaL_newmetatable(L, "weak_values")) - { - // stack contains userdata, LL.atexit, weak_table, weak_values - // just created "weak_values" metatable: populate it - // Registry.weak_values = {__mode="v"} - lua_pushliteral(L, "v"); - // stack contains userdata, LL.atexit, weak_table, weak_values, "v" - lua_setfield(L, -2, "__mode"); - } - // stack contains userdata, LL.atexit, weak_table, weak_values - // setmetatable(weak_table, weak_values) - lua_setmetatable(L, -2); - // stack contains userdata, LL.atexit, weak_table - lua_pushinteger(L, 1); - // stack contains userdata, LL.atexit, weak_table, 1 - // duplicate userdata - lua_pushvalue(L, -4); - // stack contains userdata, LL.atexit, weak_table, 1, userdata - // weak_table[1] = userdata - lua_settable(L, -3); - // stack contains userdata, LL.atexit, weak_table - - // push a closure binding (lua_emplace_call_gc, weak_table) - auto callgcname{ stringize("lua_emplace_call_gc<", Tname, ">") }; - lua_pushcclosure(L, lua_emplace_call_gc, callgcname.c_str(), 1); - // stack contains userdata, LL.atexit, closure - // Call LL.atexit(closure) - lua_call(L, 1, 0); - // stack contains userdata -- return that -} - -namespace { - -// passed to LL.atexit(closure(lua_emplace_call_gc, weak_table{userdata})); -// retrieves bound userdata to pass to lua_emplace_gc() -template -int lua_emplace_call_gc(lua_State* L) -{ - luaL_checkstack(L, 2, nullptr); - // retrieve the first (only) bound upvalue and push to stack top - lua_pushvalue(L, lua_upvalueindex(1)); - // This is the weak_table bound by lua_emplace(). Its one and only - // entry should be the lua_emplace() userdata -- unless userdata has - // been garbage collected. Retrieve weak_table[1]. - lua_pushinteger(L, 1); - // stack contains weak_table, 1 - lua_gettable(L, -2); - // stack contains weak_table, weak_table[1] - // If our userdata was garbage-collected, there is no weak_table[1], - // and we just retrieved nil. - if (lua_isnil(L, -1)) - { - lua_pop(L, 2); - return 0; - } - // stack contains weak_table, userdata - // ditch weak_table - lua_replace(L, -2); - // pass userdata to lua_emplace_gc() - return lua_emplace_gc(L); -} - -// set as metatable(userdata).__gc to be called by the garbage collector -template -int lua_emplace_gc(lua_State* L) -{ - using optT = std::optional; - // We're called with userdata on the stack holding an instance of type T. - auto ptr = lua_touserdata(L, -1); - llassert(ptr); - // Destroy the T object contained in optT at the void* address ptr. If - // in future lua_emplace() must manually align our optT* within the - // Lua-provided void*, derive optT* from ptr. - static_cast(ptr)->reset(); - // pop the userdata - lua_pop(L, 1); - return 0; -} - -template -std::string lua_emplace_metaname(const std::string& Tname) -{ - return stringize("lua_emplace_", Tname, "_meta"); + new (ptr) T(std::forward(args)...); + // stack is now initialized userdata containing our T instance -- return + // that } -} // anonymous namespace - /** * If the value at the passed acceptable index is a full userdata created by - * lua_emplace() -- that is, the userdata contains a non-empty - * std::optional -- return a pointer to the contained T instance. Otherwise - * (index is not a full userdata; userdata is not of type std::optional; - * std::optional is empty) return nullptr. + * lua_emplace(), return a pointer to the contained T instance. Otherwise + * (index is not a full userdata; userdata is not of type T) return nullptr. */ template T* lua_toclass(lua_State* L, int index) { - using optT = std::optional; - // recreate the name lua_emplace() uses for its metatable - auto metaname{ lua_emplace_metaname() }; // get void* pointer to userdata (if that's what it is) - void* ptr{ luaL_checkudata(L, index, metaname.c_str()) }; - if (! ptr) - return nullptr; - // Derive the optT* from ptr. If in future lua_emplace() must manually - // align our optT* within the Lua-provided void*, adjust accordingly. - optT* tptr(static_cast(ptr)); - // make sure our optT isn't empty - if (! *tptr) - return nullptr; - // looks like we still have a non-empty optT: return the *address* of the - // value() reference - return &tptr->value(); + void* ptr{ lua_touserdatatagged(L, index, type_tag()) }; + // Derive the T* from ptr. If in future lua_emplace() must manually + // align our T* within the Lua-provided void*, adjust accordingly. + return static_cast(ptr); } /***************************************************************************** -- cgit v1.2.3 From 0cc7436be1f57299384c5acad5d32e13f2f4d1cf Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 27 Jun 2024 19:47:00 -0400 Subject: Introduce TypeTag template whose int value differs for each T. This replaces type_tag(), which searched and possibly extended the type_tags unordered_map at runtime. If we called lua_emplace() from different threads, that would require locking type_tags. In contrast, the compiler must instantiate a distinct TypeTag for every distinct T passed to lua_emplace(), so each gets a distinct value at static initialization time. No locking is required; no lookup; no allocations. Add a test to llluamanager_test.cpp to verify that each distinct T passed to lua_emplace() gets its own TypeTag::value, and that each gets its own destructor -- but that different lua_emplace() calls with the same T share the same TypeTag::value and the same destructor. --- indra/llcommon/lua_function.cpp | 2 ++ indra/llcommon/lua_function.h | 44 ++++++++++++++++++------------- indra/newview/tests/llluamanager_test.cpp | 35 ++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 18 deletions(-) diff --git a/indra/llcommon/lua_function.cpp b/indra/llcommon/lua_function.cpp index 255385b8c4..2d08de68c5 100644 --- a/indra/llcommon/lua_function.cpp +++ b/indra/llcommon/lua_function.cpp @@ -38,6 +38,8 @@ const S32 INTERRUPTS_SUSPEND_LIMIT = 100; #define lua_register(L, n, f) (lua_pushcfunction(L, (f), n), lua_setglobal(L, (n))) #define lua_rawlen lua_objlen +int DistinctInt::mValues{0}; + /***************************************************************************** * luau namespace *****************************************************************************/ diff --git a/indra/llcommon/lua_function.h b/indra/llcommon/lua_function.h index 284f911fa8..c32a586d79 100644 --- a/indra/llcommon/lua_function.h +++ b/indra/llcommon/lua_function.h @@ -195,26 +195,34 @@ int name##_luasub::call(lua_State* L) /***************************************************************************** * lua_emplace(), lua_toclass() *****************************************************************************/ -namespace { +// Every instance of DistinctInt has a different int value, barring int +// wraparound. +class DistinctInt +{ +public: + DistinctInt(): mValue(++mValues) {} + int get() const { return mValue; } + operator int() const { return mValue; } +private: + static int mValues; + int mValue; +}; -// If we start engaging lua_emplace() from more than one thread, type_tags -// will need locking. -std::unordered_map type_tags; +namespace { -// find or create a new Luau userdata "tag" for type T template -int type_tag() +struct TypeTag { - // The first time we encounter a given type T, assign a new distinct tag - // value based on the number of previously-created tags. But avoid tag 0, - // which is evidently the default for userdata objects created without - // explicit tags. Don't try to destroy a nonexistent T object in a random - // userdata object! - auto [entry, created] = type_tags.emplace(std::type_index(typeid(T)), int(type_tags.size()+1)); - // Luau only permits up to LUA_UTAG_LIMIT distinct userdata tags (ca. 128) - llassert(entry->second < LUA_UTAG_LIMIT); - return entry->second; -} + // For (std::is_same), &TypeTag::value == &TypeTag::value. + // For (! std::is_same), &TypeTag::value != &TypeTag::value. + // And every distinct instance of DistinctInt has a distinct value. + // Therefore, TypeTag::value is an int uniquely associated with each + // distinct T. + static DistinctInt value; +}; + +template +DistinctInt TypeTag::value; } // anonymous namespace @@ -233,7 +241,7 @@ template void lua_emplace(lua_State* L, ARGS&&... args) { luaL_checkstack(L, 1, nullptr); - int tag{ type_tag() }; + int tag{ TypeTag::value }; if (! lua_getuserdatadtor(L, tag)) { // We haven't yet told THIS lua_State the destructor to use for this tag. @@ -267,7 +275,7 @@ template T* lua_toclass(lua_State* L, int index) { // get void* pointer to userdata (if that's what it is) - void* ptr{ lua_touserdatatagged(L, index, type_tag()) }; + void* ptr{ lua_touserdatatagged(L, index, TypeTag::value) }; // Derive the T* from ptr. If in future lua_emplace() must manually // align our T* within the Lua-provided void*, adjust accordingly. return static_cast(ptr); diff --git a/indra/newview/tests/llluamanager_test.cpp b/indra/newview/tests/llluamanager_test.cpp index 2d525f7913..d3fc70dfd5 100644 --- a/indra/newview/tests/llluamanager_test.cpp +++ b/indra/newview/tests/llluamanager_test.cpp @@ -465,4 +465,39 @@ namespace tut ensure_equals(desc + " count: " + result.asString(), count, -1); ensure_contains(desc + " result", result.asString(), "terminated"); } + + template + struct Visible + { + Visible(T name): name(name) + { + LL_INFOS() << "Visible<" << LLError::Log::classname() << ">('" << name << "')" << LL_ENDL; + } + Visible(const Visible&) = delete; + Visible& operator=(const Visible&) = delete; + ~Visible() + { + LL_INFOS() << "~Visible<" << LLError::Log::classname() << ">('" << name << "')" << LL_ENDL; + } + T name; + }; + + template<> template<> + void object::test<9>() + { + set_test_name("track distinct lua_emplace() types"); + LuaState L; + lua_emplace>(L, "std::string 0"); + int st0tag = lua_userdatatag(L, -1); + lua_emplace>(L, "const char* 0"); + int cp0tag = lua_userdatatag(L, -1); + lua_emplace>(L, "std::string 1"); + int st1tag = lua_userdatatag(L, -1); + lua_emplace>(L, "const char* 1"); + int cp1tag = lua_userdatatag(L, -1); + lua_settop(L, 0); + ensure_equals("lua_emplace() tags diverge", st0tag, st1tag); + ensure_equals("lua_emplace() tags diverge", cp0tag, cp1tag); + ensure_not_equals("lua_emplace<>() tags collide", st0tag, cp0tag); + } } // namespace tut -- cgit v1.2.3 From 982ea7fb796924877e95bee2f9ba6b2296219139 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 27 Jun 2024 23:11:18 -0400 Subject: Work around VS refusal to initialize a string --- indra/test/test.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/indra/test/test.cpp b/indra/test/test.cpp index 3cbac41f4a..22f9ccf334 100644 --- a/indra/test/test.cpp +++ b/indra/test/test.cpp @@ -550,7 +550,7 @@ int main(int argc, char **argv) // specific test we want to monitor, and some viewer integration tests are // quite verbose. In addition to noticing plain LOGTEST= (for all tests), // also notice LOGTEST_progname= (for a specific test). - std::string basename{ fsyspath(argv[0]).stem() }; + std::string basename(fsyspath(argv[0]).stem()); // don't make user set LOGTEST_INTEGRATION_TEST_progname or (worse) // LOGTEST_PROJECT_foo_TEST_bar -- only LOGTEST_progname or LOGTEST_bar auto _TEST_ = basename.find("_TEST_"); @@ -558,7 +558,7 @@ int main(int argc, char **argv) { basename.erase(0, _TEST_+6); } - std::string LOGTEST_prog_key{ "LOGTEST_" + basename }; + std::string LOGTEST_prog_key("LOGTEST_" + basename); const char* LOGTEST_prog = getenv(LOGTEST_prog_key.c_str()); // std::cout << LOGTEST_prog_key << "='" << (LOGTEST_prog? LOGTEST_prog : "") << "'" << std::endl; if (LOGTEST_prog && *LOGTEST_prog) -- cgit v1.2.3 From fbeff6d8052d4b614a0a2c8ebaf35b45379ab578 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Fri, 28 Jun 2024 07:54:46 -0400 Subject: Give our fsyspath an operator std::string() conversion method. This is redundant (but harmless) on a Posix system, but it fills a missing puzzle piece on Windows. The point of fsyspath is to be able to interchange freely between fsyspath and std::string. Existing fsyspath could be constructed and assigned from std::string, and we could explicitly call its string() method to get a std::string, but an implicit fsyspath-to-string conversion that worked on Posix would trip us up on Windows. Fix that. --- indra/llcommon/fsyspath.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/indra/llcommon/fsyspath.h b/indra/llcommon/fsyspath.h index aa4e0132bc..3c749d84de 100644 --- a/indra/llcommon/fsyspath.h +++ b/indra/llcommon/fsyspath.h @@ -69,6 +69,11 @@ public: // shadow base-class string() method with UTF-8 aware method std::string string() const { return super::u8string(); } + // On Posix systems, where value_type is already char, this operator + // std::string() method shadows the base class operator string_type() + // method. But on Windows, where value_type is wchar_t, the base class + // doesn't have operator std::string(). Provide it. + operator std::string() const { return string(); } }; #endif /* ! defined(LL_FSYSPATH_H) */ -- cgit v1.2.3 From cfd7d8905d686411a774c47bbfc13f49882b65e6 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Fri, 28 Jun 2024 08:30:41 -0400 Subject: Work around MSVC limitation: explicitly call fsyspath::string(). --- indra/test/test.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/indra/test/test.cpp b/indra/test/test.cpp index 22f9ccf334..0e863d8084 100644 --- a/indra/test/test.cpp +++ b/indra/test/test.cpp @@ -550,7 +550,9 @@ int main(int argc, char **argv) // specific test we want to monitor, and some viewer integration tests are // quite verbose. In addition to noticing plain LOGTEST= (for all tests), // also notice LOGTEST_progname= (for a specific test). - std::string basename(fsyspath(argv[0]).stem()); + // (Why doesn't MSVC notice fsyspath::operator std::string()? + // Why must we explicitly call fsyspath::string()?) + std::string basename(fsyspath(argv[0]).stem().string()); // don't make user set LOGTEST_INTEGRATION_TEST_progname or (worse) // LOGTEST_PROJECT_foo_TEST_bar -- only LOGTEST_progname or LOGTEST_bar auto _TEST_ = basename.find("_TEST_"); -- cgit v1.2.3