A call to ll.SetText triggers errors in immediately following metatable __index calls
in progress
SungAli Resident
Second Life Project Lua Editor 7.1.12.14888088240 (64bit)
I created a short script demonstrating multiple inheritance in SLua. The program worked exactly as intended until I added a call to ll.SetText at the beginning of the script, whereupon the script produced an incomprehensible "Failed to perform mandatory yield" error when the __index function was invoked.
Attached is a stripped down version of the script demonstrating the problem, together with various commented out alternatives trying to narrow the problem down. Details of trying the alternatives, and the full error message are included in the comments at the end of the script.
Log In
H
Harold Linden
in progress
Just an update, we've got a work-in-progress change that adds a conditional interrupt point at the end
LOP_CALL
instructions when calling functions we wrote in C++.Basically, it lets C++ functions set a flag that says they might run a long time, and then checks if the script ran over its time allotment, _before_ it ends up inside the
__index
where it won't be able to yield. Traditionally the Luau VM only checks if an interrupt needs to happen at the start of a function call.This should allow scripts to gracefully pause after a long-running
ll.*()
function rather than get killed by the simulator because it thinks it was intentionally hogging script time.H
Harold Linden
Thanks for sending this in!
This is definitely a bug and we'd like to improve behavior here, though the underlying issue is a bit complex.
For context, SLua works by allowing Lua scripts to run small slices of code before injecting a yield (well, break, but similar to a yield) after a certain amount of time so that other scripts' code can run. These yield time checks use Luau's
VM_INTERRUPT()
construct and happen whenever a function call entered, when a return
opcode is hit, and at the back edge of the various looping constructs.The complicating factors are that:
- These yield checks only happen when a functions is _entered_, not when it is _exited_ if the function is written in C++.
- Many ll.functions that work with prims are slow enough that they actually take longer than the script's time slice (this is also the case in Mono, as the functions share C++ implementations.)
- The simulator cannot inject a yield when a script is inside an __indexmetamethod, or any other metamethod other than__calldue to https://luau.org/compatibility#lua-52 (see "yieldable metamethods" and the notes below the table)
- When we run into a situation where we _need_ to yield to keep the sim responsive, but we cannot safely inject a yield, we instead throw an "unable to perform mandatory yield" error to get us back to a place in the code where we can yield (or let the script explode)
In the situations below, the script likely already run out of time due to the runtime of an
ll.*()
call just before the Test[1]
, but hasn't hit a place where it can do the yield check yet. The first place it hits a yield check is the function call inside __index
which makes it explode because the timeslice has already been overrun so much that we've decided to make the yield mandatory.It seems to me that we can improve behavior by doing yield checks at the _end_ of every C++ function call as well, so we're more likely to know we've run out of time in a place where we can safely inject a yield, and not potentially explode the script by doing the check later.
As a side note though this isn't happening here, putting too much complex logic in metamethods like
__index
is a potential pitfall as they aren't yieldable. They should be kept as trivial as possible unless we can figure out a way to make the Luau VM allow yielding inside them.H
Harold Linden
Also agreed that "unable to perform mandatory yield" is a bit vague. We'll write up a better error message there, and include some pointers about what the actual issue is and how to avoid it once we've got the docs figured out.
Nexii Malthus
Harold Linden Would be great to figure out what
ll*
functions are slow and have this documented, it's really been hard to measure this as a resident, but maybe you can measure this more accurately with debugging on the sim side?As a region owner, content creator etc, I've been wanting to figure out how to better advise scripters on scripting best practices and efficiency but I feel pretty clueless or not being able to provide a confident answer even after having been a scripter for decades.
H
Harold Linden
Nexii Malthus I wish I had that data myself, honestly. I'm in the middle of writing profiling instrumentation for the C++ functions so that I can get a sense of which ones are inherently expensive and which ones are unexpectedly expensive.
There's currently not a good system for gathering detailed LSL runtime performance data like, for example, 99th percentiles for native function invocation durations. Currently only running averages are kept, and that doesn't help much for functions that do wildly different things depending on inputs like
llSetLinkPrimititiveParamsFast()
.On our end, we have to treat most functions as if they _might_ be slow because unless a function is truly trivial there's always the possibility for performance regressions to creep in like happened with LLSPP before the recent speedups.
This is the best way for a resident to figure out how long a function invocation takes is to do something like
// Voluntarily yield so the next bit of code runs at the start of a new timeslice and we're less likely to get forcibly yielded before we call `llGetTime()`
llSleep(0.01);
// Naturally, reset the high-resolution timer after the sleep.
llResetTime();
llSomeFunction(foo, bar);
// If a function is _particularly_ long-running and caused a punitive sleep due to how long it spent in the native function (which may be the case for particularly tortuous LLSPP calls) `llGetTime()`s return time should reflect that punitive sleep as well.
llOwnerSay((string)llGetTime());
or something like that where you keep all your data points in a list and then calculate percentiles and averages later.
Nexii Malthus
Harold Linden have you taken a look at the instrumentation tools in the SL viewer sourcecode? I'm surprised if that's not been used in the simulator side yet
H
Harold Linden
Nexii Malthus
The issue is less the instrumentation for gathering the data and more figuring out a principled way to push it to whichever flavor-of-the-month metrics-collection service so the data can be analyzed in a meaningful way.
There are existing systems in place for that, but they seem to be more focused on system metrics and data extracted from log files than live app data, and wouldn't be well-suited to high volumes of data from function metrics. Totally possible to do, but figuring out how to wedge a proper statsd publisher into the server code is lower priority for me than getting SLua working properly.
I can at least use callgrind and gperftools in dev for now, it's just production stats collection that's in a bit of a weird state until I get around to it.
H
Harold Linden
tracked
SuzannaLinn Resident
also a "Failed to perform mandatory yield" with:
Test=setmetatable({},{ __index = function() return 0 end })
ll.SetPrimitiveParams({PRIM_NAME,"testing"})
print(Test[1])
or with:
Test=setmetatable({},{ __index = function() return 0 end })
_ = ll.GetPrimitiveParams({PRIM_NAME})
print(Test[1])
or with:
Test=setmetatable({},{ __newindex = function() end })
ll.SetRot(ZERO_ROTATION)
Test[1]=1
no error with SetObjectName or GetObjectName
no error with __index set to a table
Janet Rossini
Good catch, Ali and Suzanne! And thanks Harold!
Do we think that the issue comes down to
__index
being a function, and that having it be a table will not cause the problem, and that the connection to ll.SetPrimitiveParams (and its cousins) is NOT a sign that there is issue with those related functions?llSLPPF, as we fondly call it, is critical to most every non-physical moving object in SL, so it really needs to work.
Harold, we Valkyries have a lot of trains and trams and such that use llSLPPF. Do you think we need to prioritize testing that capability in SLua? We've been assuming that our redesign of our movers could rely on that function working well, but we can probably reprioritize our work to bear down on it a bit if you think it's particularly needed.
SuzannaLinn Resident
Until Harold solves it, the current workaround is to insert something that includes a yield time check, for instance:
Test=setmetatable({},{ __index = function() return 0 end })
ll.SetPrimitiveParams({PRIM_NAME,"testing"})
;(function() end)()
print(Test[1])
or the same, in a more beautiful style:
YIELD = function() end
Test=setmetatable({},{ __index = function() return 0 end })
ll.SetPrimitiveParams({PRIM_NAME,"testing"})
YIELD()
print(Test[1])