2020-01-31 00:55:50 +00:00
|
|
|
--[[
|
|
|
|
simple kernel for async tasks running in the background
|
2022-03-02 17:55:57 +00:00
|
|
|
|
2020-03-16 09:17:48 +00:00
|
|
|
can "stall" a task by yielding the string "stall"
|
|
|
|
this will suspend the coroutine until the rest of
|
|
|
|
the queue has been processed or stalled
|
|
|
|
and can early-out update_for_time
|
|
|
|
|
2022-03-02 17:55:57 +00:00
|
|
|
todo:
|
2020-02-01 08:39:51 +00:00
|
|
|
multiple types of callbacks
|
|
|
|
finish, error, step
|
|
|
|
getting a reference to the task for manipulation
|
|
|
|
attaching multiple callbacks
|
|
|
|
cancelling
|
2023-12-21 02:59:38 +00:00
|
|
|
proper error traces for coroutines with async:add, additional wrapper?
|
2020-01-31 00:55:50 +00:00
|
|
|
]]
|
|
|
|
|
2020-04-07 03:49:10 +00:00
|
|
|
local path = (...):gsub("async", "")
|
2022-03-06 04:19:07 +00:00
|
|
|
local assert = require(path .. "assert")
|
2020-04-07 03:49:10 +00:00
|
|
|
local class = require(path .. "class")
|
2023-08-08 02:57:35 +00:00
|
|
|
local tablex = require(path .. "tablex")
|
2020-04-07 03:49:10 +00:00
|
|
|
|
2021-07-15 06:09:08 +00:00
|
|
|
local async = class({
|
|
|
|
name = "async",
|
|
|
|
})
|
2020-01-31 00:55:50 +00:00
|
|
|
|
|
|
|
function async:new()
|
2021-07-15 06:09:08 +00:00
|
|
|
self.tasks = {}
|
|
|
|
self.tasks_stalled = {}
|
2020-01-31 00:55:50 +00:00
|
|
|
end
|
|
|
|
|
async: Skip wrapping with xpcall under lovejs
See Davidobot/love.js#54.
lovejs crashes love with "attempt to yield across metamethod/C-call
boundary" when you try to yield in a coroutine, so we skip wrapping.
We lose the coroutine-local callstack, but at least it works.
Here's my repro:
local color = {
purple = {0.25, 0.09, 0.28, 1},
white = {0.89, 0.91, 0.90, 1},
}
local ball = {
x = 100,
y = 100,
r = 20,
}
local S = {}
local input = {
vanilla = 'v',
async = 'c',
quit = 'escape',
}
function love.load()
S.coro = async()
end
local function coro_fn()
S.async_running = true
for i=1,100 do
color.purple[4] = i / 100
coroutine.yield()
end
color.purple[4] = 1
S.async_running = false
return true
end
function love.update(dt)
S.coro:update(dt)
if S.vanilla then
local success, result = coroutine.resume(S.vanilla)
if result then
print("coroutine cleared")
S.vanilla = nil
end
end
if not S.async_running then
if love.keyboard.isDown(input.vanilla) then
S.vanilla = coroutine.create(coro_fn)
print("raw coroutine started")
elseif love.keyboard.isDown(input.async) then
print("Starting async call coroutine from update")
S.coro:call(coro_fn)
elseif love.keyboard.isDown(input.quit) then
love.event.quit()
end
end
end
function love.draw()
love.graphics.setColor(color.purple)
love.graphics.circle("fill", ball.x, ball.y, ball.r)
love.graphics.setColor(color.white)
local str = "To start a coroutine:"
for key,val in pairs(input) do
str = ("%s\n%s: %s"):format(str, key, val)
end
love.graphics.printf(str, 5,5, 200, "left")
end
2022-03-06 03:55:32 +00:00
|
|
|
local capture_callstacks
|
2023-11-09 05:55:08 +00:00
|
|
|
if love and love.system and love.system.getOS() == 'Web' then
|
2023-08-07 07:13:50 +00:00
|
|
|
--do no extra wrapping under lovejs because using xpcall
|
|
|
|
-- causes a yield across a c call boundary
|
async: Skip wrapping with xpcall under lovejs
See Davidobot/love.js#54.
lovejs crashes love with "attempt to yield across metamethod/C-call
boundary" when you try to yield in a coroutine, so we skip wrapping.
We lose the coroutine-local callstack, but at least it works.
Here's my repro:
local color = {
purple = {0.25, 0.09, 0.28, 1},
white = {0.89, 0.91, 0.90, 1},
}
local ball = {
x = 100,
y = 100,
r = 20,
}
local S = {}
local input = {
vanilla = 'v',
async = 'c',
quit = 'escape',
}
function love.load()
S.coro = async()
end
local function coro_fn()
S.async_running = true
for i=1,100 do
color.purple[4] = i / 100
coroutine.yield()
end
color.purple[4] = 1
S.async_running = false
return true
end
function love.update(dt)
S.coro:update(dt)
if S.vanilla then
local success, result = coroutine.resume(S.vanilla)
if result then
print("coroutine cleared")
S.vanilla = nil
end
end
if not S.async_running then
if love.keyboard.isDown(input.vanilla) then
S.vanilla = coroutine.create(coro_fn)
print("raw coroutine started")
elseif love.keyboard.isDown(input.async) then
print("Starting async call coroutine from update")
S.coro:call(coro_fn)
elseif love.keyboard.isDown(input.quit) then
love.event.quit()
end
end
end
function love.draw()
love.graphics.setColor(color.purple)
love.graphics.circle("fill", ball.x, ball.y, ball.r)
love.graphics.setColor(color.white)
local str = "To start a coroutine:"
for key,val in pairs(input) do
str = ("%s\n%s: %s"):format(str, key, val)
end
love.graphics.printf(str, 5,5, 200, "left")
end
2022-03-06 03:55:32 +00:00
|
|
|
capture_callstacks = function(f)
|
|
|
|
return f
|
|
|
|
end
|
|
|
|
else
|
|
|
|
capture_callstacks = function(f)
|
2023-08-07 07:13:50 +00:00
|
|
|
--report errors with the coroutine's callstack instead of one coming
|
|
|
|
-- from async:update
|
async: Skip wrapping with xpcall under lovejs
See Davidobot/love.js#54.
lovejs crashes love with "attempt to yield across metamethod/C-call
boundary" when you try to yield in a coroutine, so we skip wrapping.
We lose the coroutine-local callstack, but at least it works.
Here's my repro:
local color = {
purple = {0.25, 0.09, 0.28, 1},
white = {0.89, 0.91, 0.90, 1},
}
local ball = {
x = 100,
y = 100,
r = 20,
}
local S = {}
local input = {
vanilla = 'v',
async = 'c',
quit = 'escape',
}
function love.load()
S.coro = async()
end
local function coro_fn()
S.async_running = true
for i=1,100 do
color.purple[4] = i / 100
coroutine.yield()
end
color.purple[4] = 1
S.async_running = false
return true
end
function love.update(dt)
S.coro:update(dt)
if S.vanilla then
local success, result = coroutine.resume(S.vanilla)
if result then
print("coroutine cleared")
S.vanilla = nil
end
end
if not S.async_running then
if love.keyboard.isDown(input.vanilla) then
S.vanilla = coroutine.create(coro_fn)
print("raw coroutine started")
elseif love.keyboard.isDown(input.async) then
print("Starting async call coroutine from update")
S.coro:call(coro_fn)
elseif love.keyboard.isDown(input.quit) then
love.event.quit()
end
end
end
function love.draw()
love.graphics.setColor(color.purple)
love.graphics.circle("fill", ball.x, ball.y, ball.r)
love.graphics.setColor(color.white)
local str = "To start a coroutine:"
for key,val in pairs(input) do
str = ("%s\n%s: %s"):format(str, key, val)
end
love.graphics.printf(str, 5,5, 200, "left")
end
2022-03-06 03:55:32 +00:00
|
|
|
return function(...)
|
|
|
|
local results = {xpcall(f, debug.traceback, ...)}
|
|
|
|
local success = table.remove(results, 1)
|
|
|
|
if not success then
|
|
|
|
error(table.remove(results, 1))
|
|
|
|
end
|
|
|
|
return unpack(results)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-01-31 00:55:50 +00:00
|
|
|
--add a task to the kernel
|
2020-04-28 02:08:53 +00:00
|
|
|
function async:call(f, args, callback, error_callback)
|
2022-03-06 04:19:07 +00:00
|
|
|
assert:type_or_nil(args, "table", "async:call - args", 1)
|
async: Skip wrapping with xpcall under lovejs
See Davidobot/love.js#54.
lovejs crashes love with "attempt to yield across metamethod/C-call
boundary" when you try to yield in a coroutine, so we skip wrapping.
We lose the coroutine-local callstack, but at least it works.
Here's my repro:
local color = {
purple = {0.25, 0.09, 0.28, 1},
white = {0.89, 0.91, 0.90, 1},
}
local ball = {
x = 100,
y = 100,
r = 20,
}
local S = {}
local input = {
vanilla = 'v',
async = 'c',
quit = 'escape',
}
function love.load()
S.coro = async()
end
local function coro_fn()
S.async_running = true
for i=1,100 do
color.purple[4] = i / 100
coroutine.yield()
end
color.purple[4] = 1
S.async_running = false
return true
end
function love.update(dt)
S.coro:update(dt)
if S.vanilla then
local success, result = coroutine.resume(S.vanilla)
if result then
print("coroutine cleared")
S.vanilla = nil
end
end
if not S.async_running then
if love.keyboard.isDown(input.vanilla) then
S.vanilla = coroutine.create(coro_fn)
print("raw coroutine started")
elseif love.keyboard.isDown(input.async) then
print("Starting async call coroutine from update")
S.coro:call(coro_fn)
elseif love.keyboard.isDown(input.quit) then
love.event.quit()
end
end
end
function love.draw()
love.graphics.setColor(color.purple)
love.graphics.circle("fill", ball.x, ball.y, ball.r)
love.graphics.setColor(color.white)
local str = "To start a coroutine:"
for key,val in pairs(input) do
str = ("%s\n%s: %s"):format(str, key, val)
end
love.graphics.printf(str, 5,5, 200, "left")
end
2022-03-06 03:55:32 +00:00
|
|
|
f = capture_callstacks(f)
|
2023-08-07 07:13:50 +00:00
|
|
|
return self:add(coroutine.create(f), args, callback, error_callback)
|
2020-04-28 02:08:53 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
--add an already-existing coroutine to the kernel
|
|
|
|
function async:add(co, args, callback, error_callback)
|
2023-08-07 07:13:50 +00:00
|
|
|
local task = {
|
2020-04-28 02:08:53 +00:00
|
|
|
co,
|
2020-05-19 12:11:38 +00:00
|
|
|
args or {},
|
2020-04-28 02:08:53 +00:00
|
|
|
callback or false,
|
|
|
|
error_callback or false,
|
2023-08-07 07:13:50 +00:00
|
|
|
}
|
|
|
|
table.insert(self.tasks, task)
|
|
|
|
return task
|
2020-01-31 00:55:50 +00:00
|
|
|
end
|
|
|
|
|
2023-08-07 07:13:50 +00:00
|
|
|
--remove a running task based on the reference we got earlier
|
|
|
|
function async:remove(task)
|
|
|
|
task.remove = true
|
2023-08-08 02:57:35 +00:00
|
|
|
if coroutine.status(task[1]) == "running" then
|
|
|
|
--removed the current running task
|
|
|
|
return true
|
|
|
|
else
|
|
|
|
--remove from the queues
|
|
|
|
return tablex.remove_value(self.tasks, task)
|
|
|
|
or tablex.remove_value(self.tasks_stalled, task)
|
2023-08-07 07:13:50 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
--separate local for processing a resume;
|
|
|
|
-- because the results come as varargs this way
|
|
|
|
local function process_resume(self, task, success, msg, ...)
|
|
|
|
local co, args, cb, error_cb = unpack(task)
|
2020-01-31 00:55:50 +00:00
|
|
|
--error?
|
|
|
|
if not success then
|
|
|
|
if error_cb then
|
2022-03-02 16:10:56 +00:00
|
|
|
error_cb(msg)
|
2020-01-31 00:55:50 +00:00
|
|
|
else
|
2020-07-15 04:16:55 +00:00
|
|
|
local err = ("failure in async task:\n\n\t%s\n")
|
2022-03-02 16:10:56 +00:00
|
|
|
:format(tostring(msg))
|
2020-07-15 04:16:55 +00:00
|
|
|
error(err)
|
2020-01-31 00:55:50 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
--check done
|
2023-08-07 07:13:50 +00:00
|
|
|
if coroutine.status(co) == "dead" or task.remove then
|
2020-01-31 00:55:50 +00:00
|
|
|
--done? run callback with result
|
2020-03-16 09:17:48 +00:00
|
|
|
if cb then
|
2022-03-02 16:10:56 +00:00
|
|
|
cb(msg, ...)
|
2020-03-16 09:17:48 +00:00
|
|
|
end
|
2020-01-31 00:55:50 +00:00
|
|
|
else
|
2020-03-16 09:17:48 +00:00
|
|
|
--if not completed, re-add to the appropriate queue
|
2022-03-02 16:10:56 +00:00
|
|
|
if msg == "stall" then
|
2020-03-16 09:17:48 +00:00
|
|
|
--add to stalled queue as signalled stall
|
2023-08-07 07:13:50 +00:00
|
|
|
table.insert(self.tasks_stalled, task)
|
2020-03-16 09:17:48 +00:00
|
|
|
else
|
2023-08-07 07:13:50 +00:00
|
|
|
table.insert(self.tasks, task)
|
2020-03-16 09:17:48 +00:00
|
|
|
end
|
2020-01-31 00:55:50 +00:00
|
|
|
end
|
2022-03-02 16:10:56 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
--update some task in the kernel
|
|
|
|
function async:update()
|
|
|
|
--grab task definition
|
2023-08-07 07:13:50 +00:00
|
|
|
local task = table.remove(self.tasks, 1)
|
|
|
|
if not task then
|
2022-03-02 16:10:56 +00:00
|
|
|
--have we got stalled tasks to re-try?
|
|
|
|
if #self.tasks_stalled > 0 then
|
|
|
|
--swap queues rather than churning elements
|
|
|
|
self.tasks_stalled, self.tasks = self.tasks, self.tasks_stalled
|
|
|
|
return self:update()
|
|
|
|
else
|
|
|
|
return false
|
|
|
|
end
|
|
|
|
end
|
2023-08-07 07:13:50 +00:00
|
|
|
|
2022-03-02 16:10:56 +00:00
|
|
|
--run a step
|
|
|
|
--(using unpack because coroutine is also nyi and it's core to this async model)
|
2023-08-07 07:13:50 +00:00
|
|
|
local co, args = unpack(task)
|
|
|
|
process_resume(self, task, coroutine.resume(co, unpack(args)))
|
2020-01-31 00:55:50 +00:00
|
|
|
|
|
|
|
return true
|
|
|
|
end
|
|
|
|
|
|
|
|
--update tasks for some amount of time
|
2020-03-16 09:17:48 +00:00
|
|
|
function async:update_for_time(t, early_out_stalls)
|
2020-01-31 00:55:50 +00:00
|
|
|
local now = love.timer.getTime()
|
|
|
|
while love.timer.getTime() - now < t do
|
|
|
|
if not self:update() then
|
|
|
|
break
|
|
|
|
end
|
2020-03-16 09:17:48 +00:00
|
|
|
--all stalled?
|
|
|
|
if early_out_stalls and #self.tasks == 0 then
|
|
|
|
break
|
|
|
|
end
|
2020-01-31 00:55:50 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-03-16 09:17:48 +00:00
|
|
|
--add a function to run after a certain delay (in seconds)
|
|
|
|
function async:add_timeout(f, delay)
|
|
|
|
self:call(function()
|
2021-11-18 04:15:48 +00:00
|
|
|
async.wait(delay)
|
2020-03-16 09:17:48 +00:00
|
|
|
f()
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
|
|
|
--add a function to run repeatedly every delay (in seconds)
|
2021-11-18 04:15:48 +00:00
|
|
|
--note: not super useful currently unless you plan to destroy the whole async kernel
|
2020-03-16 09:17:48 +00:00
|
|
|
-- as there's no way to remove tasks :)
|
|
|
|
function async:add_interval(f, delay)
|
|
|
|
self:call(function()
|
|
|
|
while true do
|
2021-11-18 04:15:48 +00:00
|
|
|
async.wait(delay)
|
2020-03-16 09:17:48 +00:00
|
|
|
f()
|
|
|
|
end
|
|
|
|
end)
|
|
|
|
end
|
|
|
|
|
2023-12-18 05:18:28 +00:00
|
|
|
--await the result of a function or set of functions
|
|
|
|
--return the results
|
|
|
|
function async:await(to_call, args)
|
|
|
|
local single_call = false
|
|
|
|
if type(to_call) == "function" then
|
|
|
|
to_call = {to_call}
|
|
|
|
single_call = true
|
|
|
|
end
|
|
|
|
|
|
|
|
local awaiting = #to_call
|
|
|
|
local results = {}
|
|
|
|
for i, v in ipairs(to_call) do
|
|
|
|
self:call(function(...)
|
|
|
|
table.insert(results, {v(...)})
|
|
|
|
awaiting = awaiting - 1
|
|
|
|
end, args)
|
|
|
|
end
|
|
|
|
|
|
|
|
while awaiting > 0 do
|
|
|
|
async.stall()
|
|
|
|
end
|
|
|
|
|
|
|
|
--unwrap
|
|
|
|
if single_call then
|
|
|
|
results = results[1]
|
|
|
|
end
|
|
|
|
|
|
|
|
return results
|
|
|
|
end
|
|
|
|
|
2021-11-18 04:15:48 +00:00
|
|
|
--static async operation helpers
|
|
|
|
-- these are not methods on the async object, but are
|
|
|
|
-- intended to be called with dot syntax on the class itself
|
|
|
|
|
|
|
|
--stall the current coroutine
|
2021-10-08 05:17:18 +00:00
|
|
|
function async.stall()
|
|
|
|
return coroutine.yield("stall")
|
|
|
|
end
|
|
|
|
|
2021-11-18 04:15:48 +00:00
|
|
|
--make the current coroutine wait
|
|
|
|
function async.wait(time)
|
|
|
|
if not coroutine.running() then
|
|
|
|
error("attempt to wait in main thread, this will block forever")
|
|
|
|
end
|
|
|
|
local now = love.timer.getTime()
|
2021-11-18 04:18:58 +00:00
|
|
|
while love.timer.getTime() - now < time do
|
2021-11-18 04:15:48 +00:00
|
|
|
async.stall()
|
|
|
|
end
|
|
|
|
end
|
2021-10-08 05:17:18 +00:00
|
|
|
|
2023-10-12 05:15:15 +00:00
|
|
|
--eventually get a result, inline
|
|
|
|
-- repeatedly calls the provided function until it returns something,
|
|
|
|
-- stalling each time it doesn't, returning the result in the end
|
|
|
|
function async.value(f)
|
|
|
|
local r = f()
|
|
|
|
while not r do
|
|
|
|
async.stall()
|
|
|
|
r = f()
|
|
|
|
end
|
|
|
|
return r
|
|
|
|
end
|
|
|
|
|
2023-08-07 07:13:50 +00:00
|
|
|
--make an iterator or search function asynchronous, stalling every n (or 1) iterations
|
|
|
|
--can be useful with functional queries as well, if they are done in a coroutine.
|
|
|
|
function async.wrap_iterator(f, stall, n)
|
|
|
|
stall = stall or false
|
|
|
|
n = n or 1
|
|
|
|
local count = 0
|
|
|
|
return function(...)
|
|
|
|
count = count + 1
|
|
|
|
if count >= n then
|
|
|
|
count = 0
|
|
|
|
if stall then
|
|
|
|
async.stall()
|
|
|
|
else
|
|
|
|
coroutine.yield()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return f(...)
|
|
|
|
end
|
|
|
|
end
|
2021-07-15 06:09:08 +00:00
|
|
|
return async
|