Working around "Instruction limit exceeded" error

call_ignore_timeout (and call_retry_until)

SKILL LEVEL: Beginner

This lets you run code that would otherwise exceed the instruction limit. It can be used in any situation where you want a loop to complete regardless of the instruction limit. Procedural content generation is my use case.

I made this because I'm doing some procedural level generation that runs fairly slowly, and I want to be able to run a loop as fast as possible. That means I can't add a Task.Wait() call every iteration (or every 100 iterations or whatever) for two reasons: it still has a chance of failing because not every iteration takes the same amount of time, and it forces me to run it way slower than the theoretical limit because I need to prevent it from failing.

Here's the code (2 functions)
function call_retry_until(retry_callback, until_callback)
	local success, result
	
	repeat
		success, result = pcall(retry_callback)
		if success then
			return true, result
		else
			local err = result
			if until_callback(err) then
				Task.Wait()
			else
				return false, err
			end
		end
	until success
	
	return true, result
end

function call_ignore_timeout(callback)
	local timeout_err_suffix = "Instruction limit exceeded. Your code may be in an infinite loop."
	local success, result
	
	return call_retry_until(callback, function(err)
			return err and err:find(timeout_err_suffix, -timeout_err_suffix:len()) 
	end)
end

It's not much use to keep calling a function again and again if it just keeps failing in the same way, so you should use it to call a function that makes some progress every time it's called.

Here's a simplified usage example
local progress = 0	
local function try_generating()
	generate_block(progress)
        progress = progress + 1
end
local success, result = call_ignore_timeout(try_generating)	

If you know for sure that the only way that your function can error is by exceeding the instruction limit, you can replace return err and err:find(timeout_err_suffix, -timeout_err_suffix:len()) with return true to omit the check. This might make it a bit faster.

You can also use call_retry_until to repeatedly try calling a function when it gives you any error or any other condition is met, not just the instruction limit exceeded error.

1 Like

Hi, thanks for this.

Is this "instruction limit" documented somewhere? Like how much CPU time an script can take for its own use on a single call. How many calls or iterations?

Similar use cases: Procedural generation and level data pre-processing.

1 Like

I tried your functions but still get the exception error.
This is my code, was not easy for me to understand what you did, I never use pcall :-o . Please let me know what I did wrong.

My code is just a benchmark test to try your functions:

local function write(s)
    UI.PrintToScreen(s)
end

local function sysInfo()
  write(_VERSION)
end

function call_retry_until(retry_callback, until_callback)
	local success, result
	
	repeat
		success, result = pcall(retry_callback)
		if success then
			return true, result
		else
			local err = result
			if until_callback(err) then
				Task.Wait()
			else
				return false, err
			end
		end
	until success
	
	return true, result
end

function call_ignore_timeout(callback)
	local timeout_err_suffix = "Instruction limit exceeded. Your code may be in an infinite loop."
	local success, result
	
	return call_retry_until(callback, function(err)
			return err and err:find(timeout_err_suffix, -timeout_err_suffix:len()) 
	end)
end

local progress = 0
local total = 0
local numIters = 0

local function benchRandomMath()    
  while progress < numIters do
    total = total + 10 * math.sin( math.random() * 3.14 )
    total = total + math.sqrt(progress) * math.cos(progress)  
    progress = progress + 1
  end
end


local function runAll()
  sysInfo()
  progress = 0
  numIters = 100100
  local tstart = time()
  --benchRandomMath()
  local success, result = call_ignore_timeout(benchRandomMath)
  print(sucess, result)
  local duration = getTime() - tstart
  write('Random Math: '..tostring(duration)..' seconds. Result ='..tostring(total))  
end

runAll()
1 Like

I'm afraid an update a while ago made this workaround impossible, I haven't been on the Core platform since because the change basically meant my WIP proc-gen based game had no chance of working fast enough :frowning: So I don't know if any new workarounds have been found since.

I think your best chance is making a feedback/feature request in the community Discord server asking for an official way of "running code as fast as possible" or for the change that broke the workaround to be revoked.

1 Like

That's sad to hear.
That's one heavy point against the balance between the Nice features VS justified limitations.

I barely understand discord, I'm old guy :smiley: I thought a forum was the most "official".

Thanks for your answer.

1 Like

I think the Discord server is just as official, and since these forums aren't used very much I think you'll have more luck if you post on the Discord instead, for example in the core-feedback or lua-help channels. Someone there might know a different workaround, or you might even get the attention of Core staff if you post in the feedback channel.

1 Like