From 0566af988790e95414ed18cd82206710094d8fae Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Thu, 21 Mar 2024 23:56:46 +0900 Subject: WIP: Add fiber.lua module and use in leap.lua and WaitQueue.lua. fiber.lua goes beyond coro.lua in that it distinguishes ready suspended coroutines from waiting suspended coroutines, and presents a rudimentary scheduler in fiber.yield(). yield() can determine that when all coroutines are waiting, it's time to retrieve the next incoming event from the viewer. Moreover, it can detect when all coroutines have completed and exit without being explicitly told. fiber.launch() associates a name with each fiber for debugging purposes. fiber.get_name() retrieves the name of the specified fiber, or the running fiber. fiber.status() is like coroutine.status(), but can return 'ready' or 'waiting' instead of 'suspended'. fiber.yield() leaves the calling fiber ready, but lets other ready fibers run. fiber.wait() suspends the calling fiber and lets other ready fibers run. fiber.wake(), called from some other coroutine, returns the passed fiber to ready status for a future call to fiber.yield(). fiber.run() drives the scheduler to run all fibers to completion. If, on completion of the subject Lua script, LuaState::expr() detects that the script loaded fiber.lua, it calls fiber.run() to finish running any dangling fibers. This lets a script make calls to fiber.launch() and then just fall off the end, leaving the implicit fiber.run() call to run them all. fiber.lua is designed to allow the main thread, as well as explicitly launched coroutines, to make leap.request() calls. This part still needs debugging. The leap.lua module now configures a fiber.set_idle() function that honors leap.done(), but calls get_event_next() and dispatches the next incoming event. leap.request() and generate() now leave the reqid stamp in the response. This lets a caller handle subsequent events with the same reqid, e.g. for LLLuaFloater. Remove leap.process(): it has been superseded by fiber.run(). Remove leap.WaitFor:iterate(): unfortunately that would run afoul of the Luau bug that prevents suspending the calling coroutine within a generic 'for' iterator function. Make leap.lua use weak tables to track WaitFor objects. Make WaitQueue:Dequeue() call fiber.wait() to suspend its caller when the queue is empty, and Enqueue() call fiber.wake() to set it ready again when a new item is pushed. Make llluamanager_test.cpp's leap test script use the fiber module to launch coroutines, instead of the coro module. Fix a bug in which its drain() function was inadvertently setting and testing the global 'item' variable instead of one local to the function. Since some other modules had the same bug, it was getting confused. Also add printf.lua, providing a printf() function. printf() is short for print(string.format()), but it can also print tables: anything not a number or string is formatted using the inspect() function. Clean up some LL_DEBUGS() output left over from debugging lua_tollsd(). --- indra/newview/tests/llluamanager_test.cpp | 34 ++++++++++++++++++------------- 1 file changed, 20 insertions(+), 14 deletions(-) (limited to 'indra/newview/tests') diff --git a/indra/newview/tests/llluamanager_test.cpp b/indra/newview/tests/llluamanager_test.cpp index 069e10e9cf..1dd081fb98 100644 --- a/indra/newview/tests/llluamanager_test.cpp +++ b/indra/newview/tests/llluamanager_test.cpp @@ -311,23 +311,27 @@ namespace tut const std::string lua( "-- test leap.lua\n" "\n" + "fiber = require('fiber')\n" "leap = require('leap')\n" - "coro = require('coro')\n" + "-- debug = require('printf')\n" + "local function debug(...) end\n" "\n" "-- negative priority ensures catchall is always last\n" "catchall = leap.WaitFor:new(-1, 'catchall')\n" "function catchall:filter(pump, data)\n" + " debug('catchall:filter(%s, %s)', pump, data)\n" " return data\n" "end\n" "\n" "-- but first, catch events with 'special' key\n" "catch_special = leap.WaitFor:new(2, 'catch_special')\n" "function catch_special:filter(pump, data)\n" + " debug('catch_special:filter(%s, %s)', pump, data)\n" " return if data['special'] ~= nil then data else nil\n" "end\n" "\n" "function drain(waitfor)\n" - " print(waitfor.name .. ' start')\n" + " debug('%s start', waitfor.name)\n" " -- It seems as though we ought to be able to code this loop\n" " -- over waitfor:wait() as:\n" " -- for item in waitfor.wait, waitfor do\n" @@ -335,28 +339,30 @@ namespace tut " -- the coroutine call stack, which prohibits coroutine.yield():\n" " -- 'attempt to yield across metamethod/C-call boundary'\n" " -- So we resort to two different calls to waitfor:wait().\n" - " item = waitfor:wait()\n" + " local item = waitfor:wait()\n" " while item do\n" - " print(waitfor.name .. ' caught', item)\n" + " debug('%s caught %s', waitfor.name, item)\n" " item = waitfor:wait()\n" " end\n" - " print(waitfor.name .. ' done')\n" + " debug('%s done', waitfor.name)\n" "end\n" "\n" "function requester(name)\n" - " print('requester('..name..') start')\n" - " response = leap.request('testpump', {name=name})\n" - " print('requester('..name..') got '..tostring(response))\n" + " debug('requester(%s) start', name)\n" + " local response = leap.request('testpump', {name=name})\n" + " debug('requester(%s) got %s', name, response)\n" " -- verify that the correct response was dispatched to this coroutine\n" " assert(response.name == name)\n" "end\n" "\n" - "coro.launch(drain, catchall)\n" - "coro.launch(drain, catch_special)\n" - "coro.launch(requester, 'a')\n" - "coro.launch(requester, 'b')\n" - "\n" - "leap.process()\n" + "-- fiber.print_all()\n" + "fiber.launch('catchall', drain, catchall)\n" + "fiber.launch('catch_special', drain, catch_special)\n" + "fiber.launch('requester(a)', requester, 'a')\n" + "-- requester(a)\n" + "fiber.launch('requester(b)', requester, 'b')\n" + "-- fiber.print_all()\n" + "-- fiber.run()\n" ); LLSD requests; -- cgit v1.2.3 From bb39a8b223f78205a10ffcb61e3b3bfe05b3fd1a Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Fri, 22 Mar 2024 21:04:48 +0900 Subject: Fix a couple bugs in fiber.lua machinery. This fixes a hang if the Lua script explicitly calls fiber.run() before LuaState::expr()'s implicit fiber.run() call. Make fiber.run() remove the calling fiber from the ready list to avoid an infinite loop when all other fibers have terminated: "You're ready!" "Okay, yield()." "You're ready again!" ... But don't claim it's waiting, either, because then when all other fibers have terminated, we'd call idle() in the vain hope that something would make that one last fiber ready. WaitQueue:_wake_waiters() needs to wake waiting fibers if the queue's not empty OR it's been closed. Introduce leap.WaitFor:close() to close the queue gracefully so that a looping waiter can terminate, instead of using WaitFor:exception(), which stops the whole script once it propagates. Make leap's cleanup() function call close(). Streamline fiber.get_name() by using 'or' instead of if ... then. Streamline fiber.status() and fiber.set_waiting() by using table.find() instead of a loop. --- indra/newview/tests/llluamanager_test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'indra/newview/tests') diff --git a/indra/newview/tests/llluamanager_test.cpp b/indra/newview/tests/llluamanager_test.cpp index 1dd081fb98..d1beed84ef 100644 --- a/indra/newview/tests/llluamanager_test.cpp +++ b/indra/newview/tests/llluamanager_test.cpp @@ -361,8 +361,8 @@ namespace tut "fiber.launch('requester(a)', requester, 'a')\n" "-- requester(a)\n" "fiber.launch('requester(b)', requester, 'b')\n" + "fiber.run()\n" "-- fiber.print_all()\n" - "-- fiber.run()\n" ); LLSD requests; -- cgit v1.2.3 From 2dc003779443db99f46b3db6d17a1954f7b141dd Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Sat, 23 Mar 2024 17:43:07 +0900 Subject: Make leap.request() work even from Lua's main thread. Recast fiber.yield() as internal function scheduler(). Move fiber.run() after it so it can call scheduler() as a local function. Add new fiber.yield() that also calls scheduler(); the added value of this new fiber.yield() over plain scheduler() is that if scheduler() returns before the caller is ready (because the configured set_idle() function returned non-nil), it produces an explicit error rather than returning to its caller. So the caller can assume that when fiber.yield() returns normally, the calling fiber is ready. This allows any fiber, including the main thread, to call fiber.yield() or fiber.wait(). This supports using leap.request(), which posts a request and then waits on a WaitForReqid, which calls ErrorQueue:Dequeue(), which calls fiber.wait(). WaitQueue:_wake_waiters() must call fiber.status() instead of coroutine.status() so it understands the special token 'main'. Add a new llluamanager_test.cpp test to exercise calling leap.request() from Lua's main thread. --- indra/newview/tests/llluamanager_test.cpp | 43 ++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 6 deletions(-) (limited to 'indra/newview/tests') diff --git a/indra/newview/tests/llluamanager_test.cpp b/indra/newview/tests/llluamanager_test.cpp index d1beed84ef..0fd91c1354 100644 --- a/indra/newview/tests/llluamanager_test.cpp +++ b/indra/newview/tests/llluamanager_test.cpp @@ -307,9 +307,43 @@ namespace tut template<> template<> void object::test<5>() { - set_test_name("test leap.lua"); + set_test_name("leap.request() from main thread"); const std::string lua( - "-- test leap.lua\n" + "-- leap.request() from main thread\n" + "\n" + "leap = require 'leap'\n" + "\n" + "return {\n" + " a=leap.request('echo', {data='a'}).data,\n" + " b=leap.request('echo', {data='b'}).data\n" + "}\n" + ); + + LLEventStream pump("echo", false); + LLTempBoundListener conn{ + pump.listen( + "test<5>()", + listener([](const LLSD& data) + { + LL_DEBUGS("Lua") << "echo pump got: " << data << LL_ENDL; + LLEventPumps::instance().post( + data["reply"], + llsd::map("reqid", data["reqid"], "data", data["data"])); + })) + }; + + LuaState L; + auto [count, result] = LLLUAmanager::waitScriptLine(L, lua); + ensure_equals("Lua script didn't return item", count, 1); + ensure_equals("echo failed", result, llsd::map("a", "a", "b", "b")); + } + + template<> template<> + void object::test<6>() + { + set_test_name("interleave leap.request() responses"); + const std::string lua( + "-- interleave leap.request() responses\n" "\n" "fiber = require('fiber')\n" "leap = require('leap')\n" @@ -359,16 +393,13 @@ namespace tut "fiber.launch('catchall', drain, catchall)\n" "fiber.launch('catch_special', drain, catch_special)\n" "fiber.launch('requester(a)', requester, 'a')\n" - "-- requester(a)\n" "fiber.launch('requester(b)', requester, 'b')\n" - "fiber.run()\n" - "-- fiber.print_all()\n" ); LLSD requests; LLEventStream pump("testpump", false); LLTempBoundListener conn{ - pump.listen("test<5>()", + pump.listen("test<6>()", listener([&requests](const LLSD& data) { LL_DEBUGS("Lua") << "testpump got: " << data << LL_ENDL; -- cgit v1.2.3 From 93b30f960ea327fe3c36deed72a8075ce0d13f19 Mon Sep 17 00:00:00 2001 From: Nat Goodspeed Date: Sun, 24 Mar 2024 02:16:55 +0900 Subject: Introduce LLStreamListener: bundle LLEventStream+LLTempBoundListener. This is a very common pattern, especially in test code, but elsewhere in the viewer too. Use it in llluamanager_test.cpp. --- indra/newview/tests/llluamanager_test.cpp | 53 ++++++++++++------------------- 1 file changed, 21 insertions(+), 32 deletions(-) (limited to 'indra/newview/tests') diff --git a/indra/newview/tests/llluamanager_test.cpp b/indra/newview/tests/llluamanager_test.cpp index 0fd91c1354..872d7827fe 100644 --- a/indra/newview/tests/llluamanager_test.cpp +++ b/indra/newview/tests/llluamanager_test.cpp @@ -105,10 +105,8 @@ namespace tut void from_lua(const std::string& desc, const std::string_view& construct, const LLSD& expect) { LLSD fromlua; - LLEventStream replypump("testpump"); - LLTempBoundListener conn( - replypump.listen("llluamanager_test", - listener([&fromlua](const LLSD& data){ fromlua = data; }))); + LLStreamListener pump("testpump", + listener([&fromlua](const LLSD& data){ fromlua = data; })); const std::string lua(stringize( "data = ", construct, "\n" "post_on('testpump', data)\n" @@ -137,11 +135,9 @@ namespace tut { set_test_name("test post_on(), get_event_pumps(), get_event_next()"); StringVec posts; - LLEventStream replypump("testpump"); - LLTempBoundListener conn( - replypump.listen("test<3>", - listener([&posts](const LLSD& data) - { posts.push_back(data.asString()); }))); + LLStreamListener pump("testpump", + listener([&posts](const LLSD& data) + { posts.push_back(data.asString()); })); const std::string lua( "-- test post_on,get_event_pumps,get_event_next\n" "post_on('testpump', 'entry')\n" @@ -180,7 +176,7 @@ namespace tut void round_trip(const std::string& desc, const LLSD& send, const LLSD& expect) { - LLEventMailDrop replypump("testpump"); + LLEventMailDrop testpump("testpump"); const std::string lua( "-- test LLSD round trip\n" "replypump, cmdpump = get_event_pumps()\n" @@ -194,7 +190,7 @@ namespace tut // reached the get_event_next() call, which suspends the calling C++ // coroutine (including the Lua code running on it) until we post // something to that reply pump. - auto luapump{ llcoro::suspendUntilEventOn(replypump).asString() }; + auto luapump{ llcoro::suspendUntilEventOn(testpump).asString() }; LLEventPumps::instance().post(luapump, send); // The C++ coroutine running the Lua script is now ready to run. Run // it so it will echo the LLSD back to us. @@ -319,18 +315,13 @@ namespace tut "}\n" ); - LLEventStream pump("echo", false); - LLTempBoundListener conn{ - pump.listen( - "test<5>()", - listener([](const LLSD& data) - { - LL_DEBUGS("Lua") << "echo pump got: " << data << LL_ENDL; - LLEventPumps::instance().post( - data["reply"], - llsd::map("reqid", data["reqid"], "data", data["data"])); - })) - }; + LLStreamListener pump( + "echo", + listener([](const LLSD& data) + { + LL_DEBUGS("Lua") << "echo pump got: " << data << LL_ENDL; + sendReply(data, data); + })); LuaState L; auto [count, result] = LLLUAmanager::waitScriptLine(L, lua); @@ -397,15 +388,13 @@ namespace tut ); LLSD requests; - LLEventStream pump("testpump", false); - LLTempBoundListener conn{ - pump.listen("test<6>()", - listener([&requests](const LLSD& data) - { - LL_DEBUGS("Lua") << "testpump got: " << data << LL_ENDL; - requests.append(data); - })) - }; + LLStreamListener pump( + "testpump", + listener([&requests](const LLSD& data) + { + LL_DEBUGS("Lua") << "testpump got: " << data << LL_ENDL; + requests.append(data); + })); LuaState L; auto future = LLLUAmanager::startScriptLine(L, lua); -- cgit v1.2.3