Pattern to manage heavy operations - WoWInterface
Thread Tools Display Modes
09-04-21, 04:19 AM   #1
A Wyrmkin Dreamwalker
Join Date: Feb 2012
Posts: 57
Pattern to manage heavy operations


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:

local chunkSize = 100

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

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

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

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

        -- continue next chunk
        if (currentIndex ~= chunkSize) then
             -- process next chunk after sometime, using an async timer or OnUpdate?
             -- this is the part I'm missing
Or perhaps instead of processing the table by chunks use a ticker or something like that and process 1 by 1?


Last edited by maqjav : 09-04-21 at 04:21 AM.
  Reply With Quote
09-04-21, 08:04 AM   #2
A Chromatic Dragonspawn
AddOn Author - Click to view addons
Join Date: Apr 2008
Posts: 158
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?
  Reply With Quote
09-04-21, 08:18 AM   #3
A Wyrmkin Dreamwalker
Join Date: Feb 2012
Posts: 57
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?
  Reply With Quote
09-04-21, 09:11 AM   #4
A Wyrmkin Dreamwalker
Join Date: Feb 2012
Posts: 57
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.
  Reply With Quote
09-04-21, 10:50 AM   #5
A Theradrim Guardian
DahkCeles's Avatar
AddOn Author - Click to view addons
Join Date: Jun 2020
Posts: 64
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
  7. local workload = {}
  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
  18. end
  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"

Last edited by DahkCeles : 09-04-21 at 02:31 PM. Reason: I've now tested this in-game and fleshed it out with an example
  Reply With Quote
09-04-21, 02:04 PM   #6
A Wyrmkin Dreamwalker
Join Date: Feb 2012
Posts: 57
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
  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)
  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
  24.                     callback(self.context, key, value)
  25.                     self.context.currentIndex = self.context.currentIndex + 1
  26.                     chunkIndex = chunkIndex + 1
  27.                 end
  29.                 totalIndex = totalIndex + 1
  30.             end
  32.             self.context.finished = true
  33.             return true
  34.         until debugprofilestop() > self.deadline
  35.     end
  37.     function Routine:IsRunning()
  38.         if self.context and not self.context.finished then
  39.             return true
  40.         end
  41.         return false
  42.     end
  44.     function Routine:Reset()
  45.         self.context = {}
  46.     end
  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)
  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)
  10.     if (finished) then
  11.         routine:Reset()
  12.         self:SetScript("OnUpdate", nil)
  13.     end
  14. end);
  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)
  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!

Last edited by maqjav : 09-12-21 at 06:38 AM.
  Reply With Quote
09-04-21, 02:59 PM   #7
A Theradrim Guardian
DahkCeles's Avatar
AddOn Author - Click to view addons
Join Date: Jun 2020
Posts: 64
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.
  Reply With Quote
09-04-21, 04:35 PM   #8
A Wyrmkin Dreamwalker
Join Date: Feb 2012
Posts: 57
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.

  Reply With Quote

WoWInterface » Developer Discussions » Lua/XML Help » Pattern to manage heavy operations

Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

vB code is On
Smilies are On
[IMG] code is On
HTML code is Off