DRW Posted December 28, 2021 Share Posted December 28, 2021 Hi everyone, I'm quite interested in keeping the best performance in my server, and I know exports, custom events and timers are some of the most performance impacting features. So I created a global timer system, where a single, 50ms timer would send events like "onHalfSecondPass", "onSecondPass", "onTenSecondPass", etc. when necessary and other resources would catch that event without the need to create a timer each time. As I'm planning to basically publish a suite of performance improvement tools, I'm showing the code anyway: function hasIntervalPassed (secondInterval) local moduleInterval = timePassed % secondInterval if moduleInterval == 0 then return true else return false end end local eventIntervals = { {50, "on50MillisecondPass"}, {100, "onOneTenthSecondPass"}, {250, "onQuarterSecondPass"}, {500, "onHalfSecondPass"}, {1000, "onSecondPass"}, {2000, "onTwoSecondPass"}, {5000, "onFiveSecondPass"}, {10000, "onTenSecondPass"}, {30000, "onThirtySecondPass"}, {60000, "onMinutePass"}, {300000, "onFiveMinutePass"}, {600000, "onTenMinutePass"}, } Timer(function() if timePassed >= eventIntervals[#eventIntervals][1] then timePassed = 0 end timePassed = timePassed + 50 for i, intData in ipairs(eventIntervals) do local sec, event = unpack(intData) if hasIntervalPassed(sec) then triggerEvent(event, root) end end end,50,0) Is this really a better option than just creating timers? I'm seeing this timer using 4-5, even 8% of the client's CPU (based on the performance browser)... which I believe is a lot, so maybe sending so many events to a lot of resources is still very (maybe more?) demanding and I might as well just create a timer. But then maybe the CPU load of each resource's timers would be more distributed but could affect the performance even more. What do you think? Thank you in advance Link to comment
The_GTA Posted December 28, 2021 Share Posted December 28, 2021 (edited) 43 minutes ago, DRW said: What do you think? Hello DRW, the reference Lua implementation is not made for time-critical applications. The reasons are many: the virtual machine that acts as interpreter does pad the runtime through debug logic and garbage collection interleaving. Such runtime padding does negatively affect the responsiveness of Lua applications if taken to the extreme (for example 50ms). the MTA event system is implemented (or even at this point "designed") to be synchronous. That is why a call to triggerEvent can be very expensive. Imagine that the event call has to wait for all the resources to complete that registered an event handler. At some point you might not even fit your own deadline guarantees anymore. apart from offering very tight deadlines, your implementation is flawed. You are expecting that between each call of your Timer callback exactly 50ms have passed but in all reality at least 50ms have passed thus an offset that is invisible to your implementation is building up in the time, making your event system more and more unreliable. I am reading this as your understanding of the matter because you are adding an exact value of 50 to the timePassed variable instead of calculating the real time difference between invocations. You have asked about the difference between the MTA timer system and a custom Lua based variant. By reading your post I got the impression that you might think that keeping as much things Lua internal as possible would improve performance, as sometimes advertised on the internet (C barrier of the implementation). This is not a good idea in this case, as pushing as much of the time-critical mathematics and dispatching to the underlying C++ timer system is better than just writing a Lua based thing (for reasons already touched on above). How do you know that the user would be interested in steps of fixed time intervals anyway? The beauty of the MTA timer system is that it allows to check for elapsation of a custom time difference approximation > 50ms and that is better than what your idea of "performance improvements" provides. Think about this before you do too much of this "work". Hope this helps! Edited December 28, 2021 by The_GTA 1 Link to comment
Moderators IIYAMA Posted December 28, 2021 Moderators Share Posted December 28, 2021 1 hour ago, DRW said: Is this really a better option than just creating timers? I think it more or less depends on the quantity of timers. As The_GTA mentioned triggerEvent can be very expensive, but if the amount of timers exceed a specific amount, it might become beneficial at some point. But the amount ???. I came with the following concept, which can handle lots of timers and is just as accurate(or better) as a normal timer. Note: Only when your resource(s) use a lot of timers, else it is not beneficial. Feel free to use/modify/inspiration. Spoiler local globalTimer = nil local luaTimers = {} local lastID = 0 local startUpTime = getTickCount() local luaTimerExecuted local putLuaTimerAtHisPlace = function (luaTimer) local index = 1 if #luaTimers > 0 then local endTime = luaTimer.endTime repeat if endTime > luaTimers[index].endTime then break else index = index+1 end until index > #luaTimers end table.insert(luaTimers, index, luaTimer) end function checkGlobalTimer (timeNow) local remaining if globalTimer and isTimer(globalTimer) then remaining, executesRemaining, totalExecutes = getTimerDetails(globalTimer) end if not remaining or remaining <= 0 or remaining > 50 then if globalTimer and isTimer(globalTimer) then killTimer(globalTimer) globalTimer = nil end if (not globalTimer or not isTimer(globalTimer)) and #luaTimers > 0 then local globalTimerInterval = luaTimers[#luaTimers].endTime - timeNow globalTimer = setTimer(luaTimerExecuted, math.max(50, globalTimerInterval),1) end end end function luaTimerExecuted () local index = #luaTimers local timeNow = getTickCount() if index > 0 then repeat local endLoop = false local luaTimerData = luaTimers[index] if timeNow > luaTimerData.endTime - 50 then table.remove(luaTimers, index) local stopTheTimer = false if not luaTimerData.exportFunction then luaTimerData.theFunction(unpack(luaTimerData.arguments)) else local exportData = luaTimerData.exportFunction local thisResource = getResourceFromName(exportData[1]) if thisResource and getResourceState (thisResource) == "running" then if not call (thisResource, exportData[2], unpack(luaTimerData.arguments)) then stopTheTimer = true end else stopTheTimer = true end end if not stopTheTimer then local timesToExecute = luaTimerData.timesToExecute if timesToExecute ~= 0 then local timesAlreadyExecuted = luaTimerData.timesAlreadyExecuted - 1 if timesAlreadyExecuted > 0 then luaTimerData.timesAlreadyExecuted = timesAlreadyExecuted luaTimerData.endTime = timeNow + luaTimerData.timeInterval putLuaTimerAtHisPlace (luaTimerData) end else luaTimerData.endTime = timeNow + luaTimerData.timeInterval putLuaTimerAtHisPlace (luaTimerData) end end else endLoop = true end index = index - 1 until index == 0 or endLoop end if #luaTimers ~= 0 then checkGlobalTimer(timeNow) else globalTimer = nil end end function getResourceNameAndFunctionNameFromExport (exportString) local resourceName, exportFunctionName = string.match(exportString,"exports.(.-):(.*)") if resourceName and exportFunctionName then return resourceName, exportFunctionName end return false end function createLuaTimer (theFunction, timeInterval, timesToExecute, ...) local isExportFunction = false local resourceName, exportFunctionName if type(theFunction) == "string" then resourceName, exportFunctionName = getResourceNameAndFunctionNameFromExport(theFunction) if resourceName and exportFunctionName then isExportFunction = true end end if (type(theFunction) == "function" or isExportFunction) and type(timeInterval) == "number" and timeInterval > 0 then if type(timesToExecute) ~= "number" or timesToExecute < 0 then timesToExecute = 1 end local timeNow = getTickCount() local ID = "luaTimerID:" .. startUpTime .. "|" .. lastID + 1 local luaTimerData = { ID = ID, theFunction = theFunction, timeInterval = timeInterval, timesAlreadyExecuted = timesToExecute, timesToExecute = timesToExecute, endTime = timeNow + timeInterval, arguments = {...}, exportFunction = isExportFunction and {resourceName, exportFunctionName} or false } putLuaTimerAtHisPlace (luaTimerData) lastID = lastID + 1 checkGlobalTimer(timeNow) return ID end return false end function killLuaTimer (ID) for i=1, #luaTimers do if luaTimers[i].ID == ID then table.remove(luaTimers, i) checkGlobalTimer(getTickCount()) return true end end return false end function isLuaTimer (ID) for i=1, #luaTimers do if luaTimers[i].ID == ID then return true end end return false end function getLuaTimers () return luaTimers end function getLuaTimerCount() return #luaTimers end function killGlobalTimer () if globalTimer then killTimer(globalTimer) globalTimer = nil end end https://gitlab.com/IIYAMA12/draw-distance/-/blob/master/scripts/timer_c.lua 1 Link to comment
DRW Posted December 28, 2021 Author Share Posted December 28, 2021 Hi, 1 minute ago, IIYAMA said: I think it more or less depends on the quantity of timers. As The_GTA mentioned triggerEvent can be very expensive, but if the amount of timers exceed a specific amount, it might become beneficial at some point. But the amount ???. Just done a little grep on my code and it would be around 50 to 60 timers total, roughly 60% of those timers would be client-side, I would suppose that might be enough to impact performance. I've checked the implementation you sent and I like the idea, might do this soon and see how it goes. Would this work from external resources? For example an AI resource, which accesses local variables from the resource itself, then creating this custom timer and letting it execute from there, or would that fail? 1 hour ago, The_GTA said: You have asked about the difference between the MTA timer system and a custom Lua based variant. By reading your post I got the impression that you might think that keeping as much things Lua internal as possible would improve performance, as sometimes advertised on the internet (C barrier of the implementation). This is not a good idea in this case, as pushing as much of the time-critical mathematics and dispatching to the underlying C++ timer system is better than just writing a Lua based thing (for reasons already touched on above). How do you know that the user would be interested in steps of fixed time intervals anyway? The beauty of the MTA timer system is that it allows to check for elapsation of a custom time difference approximation > 50ms and that is better than what your idea of "performance improvements" provides. Think about this before you do too much of this "work". Hope this helps! Agree with your points, I use other methods for operations that need precision, in this case it's usually things like loops that I don't really care if they happen a couple seconds later (maybe it delays too much but haven't found issues with this personally, but now that you mentioned the synchronous nature of events it might scale really badly in the future). A couple of questions about this: Do/can timers execute in another thread? Is there any alternative to events/exports that could be executed in another thread? SQL comes to mind. Thank you 1 Link to comment
Moderators IIYAMA Posted December 28, 2021 Moderators Share Posted December 28, 2021 5 minutes ago, DRW said: Would this work from external resources? It does, there is code included for: exports.resourceName1:createLuaTimer("exports.resourceName2:exportedFunction", 3000, 1) <export function="createLuaTimer" type="client"/> But it can be a bit unreliable, since it doesn't check if an resource has been restarted. It will not destroy the timer, you will have to add that yourself. 1 Link to comment
Recommended Posts
Create an account or sign in to comment
You need to be a member in order to leave a comment
Create an account
Sign up for a new account in our community. It's easy!
Register a new accountSign in
Already have an account? Sign in here.
Sign In Now