WoWInterface

WoWInterface (https://www.wowinterface.com/forums/index.php)
-   Lua/XML Help (https://www.wowinterface.com/forums/forumdisplay.php?f=16)
-   -   Pattern to manage heavy operations (https://www.wowinterface.com/forums/showthread.php?t=58916)

maqjav 09-04-21 04:19 AM

Pattern to manage heavy operations
 
Hello.

I'm planning on writing a piece of code that will process a lot of information and I'm afraid that this will freeze the users client. How can I avoid this situation?

Let's put it in an example:

Code:

local chunkSize = 100

function getNextChunk(currentPosition)
    -- return next chunk from huge table.
end

function processItem(data)
    -- Data is also a big table that will require several operations
end

function doIt(currentPosition, callback)
    local nextChunk = getNextChunk(currentposition)
    if (not nextChunk) then
        return
    end

    local currentIndex = -1
    for id, data in pairs (nextChunk) do
        currentIndex = currentIndex + 1

        -- continue next chunk
        if (currentIndex ~= chunkSize) then
            callback(processItem(data))
        else
            -- process next chunk after sometime, using an async timer or OnUpdate?
            -- this is the part I'm missing
            return
        end
    end
end

Or perhaps instead of processing the table by chunks use a ticker or something like that and process 1 by 1?

Thanks!

sezz 09-04-21 08:04 AM

you could use onupdate to process x items every frame or coroutines or a mix of both (that's what i do, throttling based on fps)

are you sure the issue isn't how you process (or even access) your data?

maqjav 09-04-21 08:18 AM

Hey sezz, thanks for your answer.

In the implementation I did the client freezes 1 second until the whole thing is done. Which is not so bad, but I wanted to do it properly so that doesn't happend at all. I know that TSM or other addons move more data than mine and they don't have those problems.

I honestly didn't know anything about coroutines. I'm going to read about it, but first I will try with the OnUpdate event. I haven't played enough with it, let's see what I can do.

Do you know of any site where they explain coroutines with a good example? I imagine they work as threads, right?

maqjav 09-04-21 09:11 AM

Ok, so I've been researching a little bit about OnUpdate + coroutines and I think I understand how to do it, basically I have to set the Script "OnUpdate" to call a method where I start my own coroutine. The coroutine will process an specific amount of data, based on time or number of items, and keep the state internaly. Once the coroutine is done, I set OnUpdate to nil so it stops calling the coroutine, and I clear its internal state to be reused. That should do it.

I'm going to implement a test to see how it goes.

DahkCeles 09-04-21 10:50 AM

This function takes a workload and chips away at it, moving to the next frame after a budgeted amount of time. The budget is set to 50% of the target FPS, or 50% of the current FPS (if there is no target).

Better alternatives might check after X iterations, or sample the frame rate at more than a single point and maybe average it out.

Lua Code:
  1. --  Budgets 50% of target or current FPS to perform a workload.
  2. --  finished = start(workload, onFinish, onDelay)
  3. --  Arguments:
  4. --      workload        table       Stack (last in, first out) of functions to call.
  5. --      onFinish        function?   Optional callback when the table is empty.
  6. --      onDelay         function?   Optional callback each time work delays to the next frame.
  7. --  Returns:
  8. --      finished        boolean     True when finished without any delay; false otherwise.
  9. local function start(workload, onFinish, onDelay)
  10.     local maxDuration = 500/(tonumber(C_CVar.GetCVar("targetFPS")) or GetFrameRate())
  11.     local startTime = debugprofilestop()
  12.     local function continue()
  13.         local startTime = debugprofilestop()
  14.         local task = tremove(workload)
  15.         while (task) do
  16.             task()
  17.             if (debugprofilestop() - startTime > maxDuration) then
  18.                 C_Timer.After(0, continue)
  19.                 if (onDelay) then
  20.                     onDelay()
  21.                 end
  22.                 return false
  23.             end
  24.             task = tremove(workload)
  25.         end
  26.         if (onFinish) then
  27.             onFinish()
  28.         end
  29.         return true
  30.     end
  31.     return continue()
  32. end


And here is the testing code I used to make sure it works...
Lua Code:
  1. -- Simple testing code
  2. -- /sillyloop           Runs a brief atomic workload (same as /sillyloop 1)
  3. -- /sillyloop 0         Runs an empty workload
  4. -- /sillyloop 10000     Runs a long workload with 10000 parts
  5. -- This overwrites existing workloads, so call /sillyloop to stop an ongoing workload
  6.  
  7. local workload = {}
  8.  
  9. local function sillyLoop()
  10.     local x=1
  11.     for i=1, 10000 do
  12.         x = x+1+2+3+4+5+6+7+8+9
  13.         x = x-1-2-3-4-5-6-7-8-9
  14.         x = x/2/3/4/5/6/7/8/9
  15.         x = x*i*2*3*4*5*6*7*8*9
  16.     end
  17.    
  18. end
  19.  
  20. SlashCmdList["DEBUG_SILLYLOOP"] = function(msg)
  21.     wipe(workload)
  22.     for i=1, tonumber(msg) or 1 do
  23.         workload[i] = sillyLoop
  24.     end
  25.     local overallStart = debugprofilestop()
  26.     start(
  27.         workload,
  28.         function() print("done!") end,
  29.         function() print(#workload.." remaining after "..("%.2d"):format(debugprofilestop()-overallStart)/1000) end
  30.     )
  31. end
  32. SLASH_DEBUG_SILLYLOOP1 = "/sillyloop"

maqjav 09-04-21 02:04 PM

This is the code I wrote with your ideas:

Lua Code:
  1. local Routine = {} do  
  2.     function Routine:Init(getterItems, chunkSize)
  3.         self.context, self.deadline = {}
  4.         self.context.currentIndex = 1
  5.         self.context.getterItems = getterItems
  6.         self.context.chunkSize = chunkSize
  7.     end
  8.  
  9.     function Routine:Run(callback)
  10.         if (not self.context) then
  11.             return true
  12.         end
  13.         self.deadline = debugprofilestop() + 500/(tonumber(C_CVar.GetCVar("targetFPS")) or GetFrameRate() or 35)
  14.  
  15.         repeat
  16.             local totalIndex = 1
  17.             local chunkIndex = 1
  18.             for key, value in pairs(self.context.getterItems()) do
  19.                 if (totalIndex >= self.context.currentIndex) then
  20.                     if (chunkIndex == self.context.chunkSize) then
  21.                         return false
  22.                     end
  23.                    
  24.                     callback(self.context, key, value)
  25.                     self.context.currentIndex = self.context.currentIndex + 1
  26.                     chunkIndex = chunkIndex + 1
  27.                 end
  28.                
  29.                 totalIndex = totalIndex + 1
  30.             end
  31.            
  32.             self.context.finished = true
  33.             return true
  34.         until debugprofilestop() > self.deadline
  35.     end
  36.    
  37.     function Routine:IsRunning()
  38.         if self.context and not self.context.finished then
  39.             return true
  40.         end
  41.         return false
  42.     end
  43.    
  44.     function Routine:Reset()
  45.         self.context = {}
  46.     end
  47.    
  48.     function Routine:New(routine)
  49.         routine = routine or {}
  50.         setmetatable(routine, self)
  51.         self.__index = self
  52.         return routine
  53.     end
  54. end

And this is how I use it:
Lua Code:
  1. local routine = Routine:New()
  2. routine:Init(GetStuff, 100)
  3.  
  4. -- Using OnUpdate script
  5. myFrame:SetScript("OnUpdate", function(self)
  6.     local finished = routine:Run(function(state, key, value)
  7.         doHeavyStuff(key, value)
  8.     end)
  9.    
  10.     if (finished) then
  11.         routine:Reset()
  12.         self:SetScript("OnUpdate", nil)
  13.     end
  14. end);
  15.  
  16. -- Using a ticker if the frame is hidden
  17. C_Timer.NewTicker(0.5, function(self)
  18.     local finished = routine:Run(function(context, key, value)
  19.         doHeavyStuff(key, value)
  20.     end)
  21.    
  22.     if (finished) then
  23.         routine:Reset()
  24.         self:Cancel()
  25.     end
  26. end

So far this improves the performance a lot. It takes longer to finish, but I'm sure I can improve it changing the value of "state.chunkSize".

The issue I find with this code is that it loops through the table returned by GetStuff() several times and I don't see how to avoid it.

Thank you both!

DahkCeles 09-04-21 02:59 PM

I edited my post some time after originally writing it, as I now had time to log in and do some testing. See above for the updated code.

maqjav 09-04-21 04:35 PM

Thanks for the code DahkCeles.

I see that in your example in order to process the loop in chunks I will have to split it and add it to the workload array splitted. I like the way you calculate the duration, taking into account the FPS.

With this example I have more than enough to improve the performance.

Cheers.


All times are GMT -6. The time now is 09:08 AM.

vBulletin © 2024, Jelsoft Enterprises Ltd
© 2004 - 2022 MMOUI