Jump to content

Performance: Global timed events vs. timers


DRW

Recommended Posts

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
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 by The_GTA
  • Like 1
Link to comment
  • Moderators
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

 

 

  • Like 1
Link to comment

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

  • Like 1
Link to comment
  • Moderators
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.

  • Thanks 1
Link to comment

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 account

Sign in

Already have an account? Sign in here.

Sign In Now
  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...