You can disable the timeout in Studio with the following command (or just set the setting manually):
settings().Studio.ScriptTimeoutLength = -1
Long-running scripts should basically never happen on live servers (blocking important server stuff) and clients (player experiences lag), so you should find a way to batch if that’s where you’re going.
A dumb but effective way to “batch” is to yield conditionally based on execution time. For example:
local Budget = 1/60 -- seconds
local expireTime = 0
-- Call at start of process.
function ResetTimer()
expireTime = tick() + Budget
end
-- Call where appropriate, such as at the top of loops.
function MaybeYield()
if tick() >= expireTime then
wait() -- insert preferred yielding method
ResetTimer()
end
end
Threads eventually need to yield back to the engine, or they will time out. Top-level code in a script, as well as event listeners, are resumed by the engine, so these are what eventually need to yield. The entry point is when the engine resumes a thread, and the exit point is when the thread yields back to the engine.
Here is an example, along with steps describing how it executes.
local i = 0
while true do
i = i + 1
print(i)
coroutine.yield()
end
- When the script runs, the thread of the script (let’s call it “Root”) is resumed by the engine.
- Root calls
coroutine.yield()
, which yields back to the engine.
coroutine.yield()
doesn’t do anything special to get the running thread to be resumed later (this used to be the case, but not anymore), so Root is effectively killed.
Consider the same example as a separate thread:
local Work = coroutine.create(function()
local i = 0
while true do
i = i + 1
print(i)
coroutine.yield()
end
end)
coroutine.resume(Work)
- Engine resumes Root.
- Root creates the “Work” thread.
- Root resumes Work.
- Works calls
coroutine.yield()
, yielding back to Root.
- At this point, Root continues running. There’s no more code, so Root dies, yielding back to Engine.
Now consider this slightly altered example:
local Work = coroutine.create(function()
local i = 0
while true do
i = i + 1
print(i)
coroutine.yield()
end
end)
while true do
coroutine.resume(Work)
end
- Engine resumes Root.
- Root creates the “Work” thread.
- Root resumes Work.
- Work calls
coroutine.yield()
, yielding back to Root.
- Root resumes Work.
- Work calls
coroutine.yield()
, yielding back to Root.
- Root resumes Work.
- Work calls
coroutine.yield()
, yielding back to Root.
- …
Here, execution moves back and forth between Root and Work, but never actually yields back to the engine. This will eventually cause a timeout, and demonstrates that it is not enough just to yield a thread. You have to consider what you’re yielding back to.
To drive the point home, let’s see what happens when wait()
is used instead of coroutine.yield()
:
local Work = coroutine.create(function()
local i = 0
while true do
i = i + 1
print(i)
wait()
end
end)
while true do
coroutine.resume(Work)
end
- Engine resumes Root.
- Root creates the “Work” thread.
- Root resumes Work.
- Work calls
wait()
, adding Work to the engine’s scheduler queue, then yielding back to Root.
- Root resumes Work.
- Work calls
wait()
, adding Work to the engine’s scheduler queue, then yielding back to Root.
- Root resumes Work.
- Work calls
wait()
, adding Work to the engine’s scheduler queue, then yielding back to Root.
- …
Eventually, a timeout occurs, then the scheduler gets to work emptying its queue by resuming the Work thread over and over again. Because threads scheduled by wait()
run on a budget, this is rolled out slowly over a lengthy amount of time.
The problem here is that, by calling wait()
, the Work thread is being managed by both the scheduler and Root’s resume loop. The simple resolution to this is to let the engine do all the work managing threads:
local i = 0
while true do
i = i + 1
print(i)
wait()
end
- Engine resumes Root.
- Root calls
wait()
, adding Root to the engine’s scheduler queue, then yielding back to Engine.