LLTimers:once() and :on() can create a duplicate internal `timer` event wrapper on `LLEvents`
in progress
Rohacan Hirons
Hello, I have technical questions regarding SLua timer behavior and recursive/self-rescheduling callbacks.
I’m using the following pattern, where a function schedules itself again using LLTimers:once(). The snippet below is a highly simplified version of the real use case, included only to demonstrate the issue (I’m aware of LLTimers:every()).
```lua
local main
main = function()
ll.SetText(tostring(ll.GetTime()) .. "\n" .. tostring(ll.GetFreeMemory()), vector(1, 1, 1), 1)
LLTimers:once(1, main)
end
main()
```
Issue observed
With this code,
ll.GetFreeMemory()
steadily decreases over time, as if memory is being accumulated or not released correctly.Why I’m asking (comparison with LSL habits)
In LSL, it’s very common to avoid recursive loops by using
llSetTimerEvent(x)
—the timer event triggers again later without building up a recursive call chain. So from an LSL perspective, the pattern “schedule again in 1 second” is typically considered safe and non-recursive in practice.With SLua +
LLTimers:once()
, I expected a similar behavior (a clean callback invocation each tick), but the memory decrease makes me wonder if this pattern is internally treated as a form of recursion that keeps references/callbacks alive longer than expected.Questions
- Is this memory decrease expected behavior when using LLTimers:once()in a self-rescheduling callback?
- Internally, are timer callbacks handled in a way that could retain closures/references across calls (causing memory to accumulate)?
- Would it be possible (now or in the future) for timer callbacks to be executed in a way that breaks this “recursive” chaining—e.g., treated like an isolated execution context (separate thread-like handling or a coroutine-style dispatch)—so that each timer tick runs cleanly without retaining the previous call context?
Real use case
The simplified example above is only to demonstrate the problem. In real scripts, I have a controller function that checks multiple conditions and schedules follow-up actions with different delays (sometimes calling other functions that schedule additional timers, and sometimes rescheduling the original function). This makes
LLTimers:every()
unsuitable in many cases, because the next delay depends on the outcome of the current step.Thanks in advance for any clarification on whether this is expected. For now, as a workaround, I use linked messages to schedule new timers that execute the desired functions after a given delay.
Log In
H
Harold Linden
marked this post as
in progress
Thanks for the report! There's a fix in for this that should come out with the next SLua server deploy.
SuzannaLinn Resident
It seems that:
- the timer is removed just before calling the handler
- the new timer is added
- since there are no timers, the timer event is added to LLEvents
- but it hadn't been removed yet
In each call there is one more timer listener in LLEvents:
local function main()
LLTimers:once(1, main)
print(#LLEvents:listeners("timer"))
end
main()
-- >
1
2
3
4
5
The workaround is to add another timer to avoid having 0 timers:
local main
main = function()
ll.SetText(tostring(ll.GetTime()) .. "\n" .. tostring(ll.GetFreeMemory()), vector(1, 1, 1), 1)
LLTimers:once(1, main)
end
LLTimers:every(86400, function() end)
main()
Rohacan Hirons
Hi, for now I'm using this :
local function TIMER_Start(id, interval, loop, callback)
if TIMERS[id] then
LLTimers:off(TIMERS[id])
end
if loop then
TIMERS[id] = LLTimers:every(interval, callback)
else
TIMERS[id] = LLTimers:every(interval, function()
TIMER_Stop(id)
callback()
end)
end
end
This way, I only use "every" and don’t run into this issue. Timers added with loop = false remove themselves automatically. Your workaround is really nice too. I’ll probably use yours.
Thanks for helping me pinpoint the real issue. I hope they fix it.
Rohacan Hirons
I did some additional testing and it looks like LLTimers:once() and LLTimers:every() do not behave the same way regarding memory usage.
With once, even if I explicitly cancel the previous handler before scheduling the next one, free memory still decreases over time (suggesting a leak or accumulation):
local main
local handler = nil
main = function()
ll.SetText(tostring(ll.GetTime()) .. "\n" .. tostring(ll.GetFreeMemory()), vector(1, 1, 1), 1)
if handler then
LLTimers:off(handler)
end
handler = LLTimers:once(0.1, main)
end
main()
However, the exact same logic using every instead remains stable and does not trigger the memory loss:
local main
local handler = nil
main = function()
ll.SetText(tostring(ll.GetTime()) .. "\n" .. tostring(ll.GetFreeMemory()), vector(1, 1, 1), 1)
if handler then
LLTimers:off(handler)
end
handler = LLTimers:every(0.1, main)
end