Coroutine uploading - #88
Conversation
|
While it is a nice thing to have a coroutine to send the map data asynchronously, there is one flaw in the implementation:
This will definitely cause problems, as the receiving side doesn't know how to handle this scrambled navmesh data. A simple solution for this race condition is to have a (single element) queue that Additional to this, errors are not handled correctly. So if the code in resume throws an error for whatever reason, the think hook will be called indefinitely. I just pushed a commit that adds my async helper function. With this it should be relatively easy and clean to implement/fix the things written above: local uploadMapNavPlayerQueue = {} -- Set of all players that need to have their client side navmesh updated.
function lib.UploadMapNavMesh(plOrPls)
-- Add either a single player or a list of players to the queue.
if type(plOrPls) == "table" then
for _, pl in ipairs(plOrPls) do
uploadMapNavPlayerQueue[pl] = true
end
else
uploadMapNavPlayerQueue[plOrPls] = true
end
endlocal uploadMapNavPlayerWorker = {} -- Stores the state of the navmesh upload worker.
hook.Add("Think", "d3bot.MapNavMeshUploadWorker", function()
-- Abort if there are no entries in the queue.
if next(uploadMapNavPlayerQueue) == nil then return end
-- Fetch players from the queue/set.
local players = {}
for pl, _ in pairs(uploadMapNavPlayerQueue) do
table.append(players, pl)
end
uploadMapNavPlayerQueue = {} -- Clear queue.
local running, err = D3bot.Async.Run(uploadMapNavPlayerWorker, function()
local rawData = util.Compress(lib.MapNavMesh:Serialize()) or ""
local dataLen = rawData:len()
local maxChunkSize = 2^16 - 10 -- Leave 10 bytes for other stuff than the data.
for i = 1, dataLen, maxChunkSize do
local dataLeft = dataLen + 1 - i
local chunkSize = math.min(maxChunkSize, dataLeft)
local subDataComp = string.sub(rawData, i, i + chunkSize - 1)
net.Start(lib.MapNavMeshNetworkStr, false)
net.WriteBool(false)
net.WriteUInt(chunkSize, 16)
net.WriteData(subDataComp, chunkSize)
net.Send(plOrPls)
--print(chunkSize, dataLeft)
coroutine.yield()
end
-- Finish the transfer.
net.Start(lib.MapNavMeshNetworkStr, false)
net.WriteBool(true)
net.WriteUInt(0, 16)
net.Send(players)
end)
if err then
-- Worker failed, output error message.
print(string.format("D3bot: Navmesh upload worker failed: %s", err))
end
if not running then
-- Worker ended, restart it.
uploadMapNavPlayerWorker = {}
end
endI haven't tested this code in any way, so it may contain syntax errors or other mistakes. But it should show how it can work in principle. It's basically just a worker that waits for a list of players that need their navmeshes updated. Additionally it will batch multiple update requests and their players while the map upload is in progress. |
|
With my last commit I added a few improvements to the basic async lib, as well as fixed the race condition mentioned. Workers are now an object for better managing state. I figured that something like this would be better for ambiguity throughout. In addition to workers I also added a
Thoughts on things thus far? |
|
This looks better, but there are still a few problems: You are using Also, as i planned to use Additionally i don't see any sense in starting and stopping the think hook. It is only run 60 times per second and hardly uses any resources. Sure, in my example it would create a new coroutine every time when there is no work, but that could easily be fixed by moving some code/checks outside of the async part of the worker. Right now the extra logic just makes the code harder to read (uses the global var I don't want to be the guy that nitpicks on every small thing, and i appreciate the work you put in. But when core functionality is rewritten or fixed up it shouldn't make the code harder to maintain (complex state machines) and should be as flawless as possible. |
|
I just saw your fix for the upload queue. While it probably fixes the race condition and makes it a working queue, it does add more complexity. Also, it doesn't merge multiple players and instead causes the navmesh to be serialized multiple times. It's probably the easiest to get my example code working. With the small improvement to move the |
|
Honestly you're right, editing async was probably a bit too outside this scope so I'll try to work around those constraints. My reason for insisting on the hook removal was because I'm very used to squeezing performance and I don't want to add any overhead, though reasonably small. From my understanding, at this point.
Is this accurate? |
Just revert
You can let the think hook run forever, as it runs only 60 times per second and hardly does any work when there is nothing to do. I edited my example code to make it much more efficient when it idles. You probably can't even easily measure the overhead that this hook produces.
My example uses a set of players as a "queue". |
Today I learned 2 things.
I was actually surprised that this works but it does actually prevent the (what I would consider edge case) bug where your mesh is too big, even for the last fix I did. Thanks again in advance.