diff options
author | Nat Goodspeed <nat@lindenlab.com> | 2024-05-31 17:15:06 -0400 |
---|---|---|
committer | Nat Goodspeed <nat@lindenlab.com> | 2024-05-31 17:15:06 -0400 |
commit | de719553fddc381e274d8bff218ab4e3f6691945 (patch) | |
tree | fa43af2d44d8fdb0ea881d6f36fe39e5d6308088 /indra | |
parent | 894dd1937511df08fa57c5e586d40a7778473dae (diff) |
Add timers.lua API module and test_timers.lua test program.
Since timers presents a timers.Timer Lua class supporting queries and
cancellation, make TimersListener::scheduleAfter() and scheduleEvery() respond
immediately so the newly constructed Timer object has the reqid necessary to
perform those subsequent operations.
This requires that Lua invocations of these operations avoid calling the
caller's callback with that initial response.
Reinvent leap.generate() to return a Lua object supporting next() and done()
methods. A plain Lua coroutine that (indirectly) calls fiber.wait() confuses
the fiber scheduler, so avoid implementing generate() as a Lua coroutine.
Add a bit more leap.lua diagnostic output.
Diffstat (limited to 'indra')
-rw-r--r-- | indra/llcommon/llcallbacklist.cpp | 14 | ||||
-rw-r--r-- | indra/newview/scripts/lua/leap.lua | 59 | ||||
-rw-r--r-- | indra/newview/scripts/lua/test_timers.lua | 39 | ||||
-rw-r--r-- | indra/newview/scripts/lua/timers.lua | 101 |
4 files changed, 187 insertions, 26 deletions
diff --git a/indra/llcommon/llcallbacklist.cpp b/indra/llcommon/llcallbacklist.cpp index 59ff8d3759..9f324b2fe9 100644 --- a/indra/llcommon/llcallbacklist.cpp +++ b/indra/llcommon/llcallbacklist.cpp @@ -410,11 +410,14 @@ private: void TimersListener::scheduleAfter(const LLSD& params) { + // Timer creation functions respond immediately with the reqid of the + // created timer, as well as later when the timer fires. That lets the + // requester invoke cancel, isRunning or timeUntilCall. + Response response(LLSD(), params); LLSD::Real after{ params["after"] }; if (after < MINTIMER) { - sendReply(llsd::map("error", stringize("after must be at least ", MINTIMER)), params); - return; + return response.error(stringize("after must be at least ", MINTIMER)); } mHandles.emplace( @@ -432,11 +435,14 @@ void TimersListener::scheduleAfter(const LLSD& params) void TimersListener::scheduleEvery(const LLSD& params) { + // Timer creation functions respond immediately with the reqid of the + // created timer, as well as later when the timer fires. That lets the + // requester invoke cancel, isRunning or timeUntilCall. + Response response(LLSD(), params); LLSD::Real every{ params["every"] }; if (every < MINTIMER) { - sendReply(llsd::map("error", stringize("every must be at least ", MINTIMER)), params); - return; + return response.error(stringize("every must be at least ", MINTIMER)); } mHandles.emplace( diff --git a/indra/newview/scripts/lua/leap.lua b/indra/newview/scripts/lua/leap.lua index 69cedb4c9e..8caae24e94 100644 --- a/indra/newview/scripts/lua/leap.lua +++ b/indra/newview/scripts/lua/leap.lua @@ -172,36 +172,45 @@ function leap.request(pump, data) end -- Send the specified request LLSD, expecting an arbitrary number of replies. --- Each one is yielded on receipt. If you omit checklast, this is an infinite --- generator; it's up to the caller to recognize when the last reply has been --- received, and stop resuming for more. --- --- If you pass checklast=<callable accepting(event)>, each response event is --- passed to that callable (after the yield). When the callable returns --- true, the generator terminates in the usual way. +-- Each one is returned on request. +-- +-- Usage: +-- sequence = leap.generate(pump, data) +-- repeat +-- response = sequence.next() +-- until last(response) +-- (last() means whatever test the caller wants to perform on response) +-- sequence.done() -- -- See request() remarks about ["reqid"]. +-- +-- Note: this seems like a prime use case for Lua coroutines. But in a script +-- using fibers.lua, a "wild" coroutine confuses the fiber scheduler. If +-- generate() were itself a coroutine, it would call WaitForReqid:wait(), +-- which would yield -- thereby resuming generate() WITHOUT waiting. function leap.generate(pump, data, checklast) -- Invent a new, unique reqid. Arrange to handle incoming events -- bearing that reqid. Stamp the outbound request with that reqid, and -- send it. local reqid, waitfor = requestSetup(pump, data) - local ok, response, resumed_with - repeat - ok, response = pcall(waitfor.wait, waitfor) - if (not ok) or response.error then - break + return { + next = function() + dbg('leap.generate(%s).next() about to wait on %s', reqid, tostring(waitfor)) + local ok, response = pcall(waitfor.wait, waitfor) + dbg('leap.generate(%s).next() got %s: %s', reqid, ok, response) + if not ok then + error(response) + elseif response.error then + error(response.error) + else + return response + end + end, + done = function() + -- cleanup consists of removing our WaitForReqid from pending + pending[reqid] = nil end - -- can resume(false) to terminate generate() and clean up - resumed_with = coroutine.yield(response) - until (checklast and checklast(response)) or (resumed_with == false) - -- If we break the above loop, whether or not due to error, clean up. - pending[reqid] = nil - if not ok then - error(response) - elseif response.error then - error(response.error) - end + } end -- Send the specified request LLSD, expecting an immediate reply followed by @@ -220,7 +229,9 @@ function leap.eventstream(pump, data, callback) end -- No error, so far so good: -- call the callback with the first response just in case + dbg('leap.eventstream(%s): first callback', reqid) local ok, done = pcall(callback, response) + dbg('leap.eventstream(%s) got %s, %s', reqid, ok, done) if not ok then -- clean up our WaitForReqid waitfor:close() @@ -236,13 +247,17 @@ function leap.eventstream(pump, data, callback) pump, function () local ok, done + local nth = 1 repeat event = waitfor:wait() if not event then -- wait() returns nil once the queue is closed (e.g. cancelreq()) ok, done = true, true else + nth += 1 + dbg('leap.eventstream(%s): callback %d', reqid, nth) ok, done = pcall(callback, event) + dbg('leap.eventstream(%s) got %s, %s', reqid, ok, done) end -- not ok means callback threw an error (caught as 'done') -- done means callback succeeded but wants to stop diff --git a/indra/newview/scripts/lua/test_timers.lua b/indra/newview/scripts/lua/test_timers.lua new file mode 100644 index 0000000000..53a2dc83f2 --- /dev/null +++ b/indra/newview/scripts/lua/test_timers.lua @@ -0,0 +1,39 @@ +local timers = require 'timers' + +print('t0:new(10)') +start = os.clock() +t0 = timers.Timer:new(10, function() print('t0 fired at', os.clock() - start) end) +print('t0:isRunning(): ', t0:isRunning()) +print('t0:timeUntilCall(): ', t0:timeUntilCall()) +print('t0:cancel(): ', t0:cancel()) +print('t0:isRunning(): ', t0:isRunning()) +print('t0:timeUntilCall(): ', t0:timeUntilCall()) +print('t0:cancel(): ', t0:cancel()) + +print('t1:new(5)') +start = os.clock() +t1 = timers.Timer:new(5, function() print('t1 fired at', os.clock() - start) end) + +print('t2:new(2)') +start = os.clock() +t2 = timers.Timer:new(2) +function t2:tick() + print('t2 fired at', os.clock() - start) +end + +start = os.clock() +timers.Timer:new(5, 'wait') +print(string.format('Timer(5) waited %f seconds', os.clock() - start)) + +start = os.clock() +timers.Timer:new( + 2, + coroutine.wrap(function() + for i = 1,5 do + print('repeat(2) timer fired at ', os.clock() - start) + coroutine.yield(nil) -- keep running + end + print('repeat(2) timer fired last at ', os.clock() - start) + return true -- stop + end), + true) -- iterate diff --git a/indra/newview/scripts/lua/timers.lua b/indra/newview/scripts/lua/timers.lua new file mode 100644 index 0000000000..e0d27a680d --- /dev/null +++ b/indra/newview/scripts/lua/timers.lua @@ -0,0 +1,101 @@ +-- Access to the viewer's time-delay facilities + +local leap = require 'leap' + +local timers = {} + +local function dbg(...) end +-- local dbg = require 'printf' + +timers.Timer = {} + +-- delay: time in seconds until callback +-- callback: 'wait', or function to call when timer fires (self:tick if nil) +-- iterate: if non-nil, call callback repeatedly until it returns non-nil +-- (ignored if 'wait') +function timers.Timer:new(delay, callback, iterate) + local obj = setmetatable({}, self) + self.__index = self + + if callback == 'wait' then + dbg('scheduleAfter(%d):', delay) + sequence = leap.generate('Timers', {op='scheduleAfter', after=delay}) + -- ignore the immediate return + dbg('scheduleAfter(%d) -> %s', delay, + sequence.next()) + -- this call is where we wait for real + dbg('next():') + dbg('next() -> %s', + sequence.next()) + sequence.done() + return + end + + callback = callback or function() obj:tick() end + + local first = true + if iterate then + obj.id = leap.eventstream( + 'Timers', + {op='scheduleEvery', every=delay}, + function (event) + local reqid = event.reqid + if first then + first = false + dbg('timer(%s) first callback', reqid) + -- discard the first (immediate) response: don't call callback + return nil + else + dbg('timer(%s) nth callback', reqid) + return callback(event) + end + end + ).reqid + else + obj.id = leap.eventstream( + 'Timers', + {op='scheduleAfter', after=delay}, + function (event) + -- Arrange to return nil the first time, true the second. This + -- callback is called immediately with the response to + -- 'scheduleAfter', and if we immediately returned true, we'd + -- be done, and the subsequent timer event would be discarded. + if first then + first = false + -- Caller doesn't expect an immediate callback. + return nil + else + callback(event) + -- Since caller doesn't want to iterate, the value + -- returned by the callback is irrelevant: just stop after + -- this one and only call. + return true + end + end + ).reqid + end + + return obj +end + +function timers.Timer:tick() + error('Pass a callback to Timer:new(), or override Timer:tick()') +end + +function timers.Timer:cancel() + local ok = leap.request('Timers', {op='cancel', id=self.id}).ok + leap.cancelreq(self.id) + return ok +end + +function timers.Timer:isRunning() + return leap.request('Timers', {op='isRunning', id=self.id}).running +end + +-- returns (true, seconds left) for a live timer, else (false, 0) +function timers.Timer:timeUntilCall() + local result = leap.request('Timers', {op='timeUntilCall', id=self.id}) + return result.ok, result.remaining +end + +return timers |