How can I optimise this? (Parallel Luau in OOP Anticheat)

TL;DR: I need to optimise my anticheat, but I don’t know how without reducing effectiveness too much.




Hello!

I’m optimising an Object-Oriented anticheat system of mine, and have come across some performance concerns. There’s a maximum of 20 players per server, each one having their own anticheat. This anticheat contains a procedure connected to RunService.Heartbeat to detect speed exploits, and I’m concerned on how this may affect performance of the rest of my game.

I decided to run a lot of the code in the procedures parallel to the main thread to reduce strain. However, from one anticheat when I am looking within the script profiler, the procedure connected to RunService.Heartbeat’s value climbs until it settles around 5. This is for one of the modules.

I need to find a way to reduce this value, but since there is so much of this code relative to the class, it would be simpler and probably nicer on memory to keep it all within the class instead of seperating it into another script.

The heartbeat function is run natively, and along with a lot of other code in this script, is run in parallel.

(see updated code in post 3)

I need help reducing memory strain from this function, if anyone can help I’d greatly appreciate it.

2 Likes

Unless you’re planning on adding other checks like flight, running a speed check in a heartbeat is overkill, 1 second will do as exploiters will still get rubber banded but less frequently just make sure you adjust the distance threshold.

As for parallel lua, it’s better to use it on more complex tasks, in terms of an anti-cheat maybe a custom floor finder that sends multiple ray casts every frame or maybe automatically switch to it if there are more than 15 players in a server.

7 Likes

Hey, thanks for the help. I’ve changed it to every 1 second and now I can’t even find that thread on the script profiler (which I’m assuming means the value is so small there’s no longer any point in showing it). Looking at LuauHeap and main memory, I think I’ll leave the anticheat in parallel because of how many checks it does, for example there’s also antifly, humanoid state checks, and more. These cause the size to settle around 30K in LuauHeap, one of the biggest items in there.

However, the native anti-speed hacks function alone is taking up ~15K size in LuauHeap. Do you know why this might be?

This is still too fast … I’d go with a task.wait(3). A fair balance, give and take from both sides.
Your game is first and foremost a game. Not a constantly bombarded cheating detector.
This will give you back your memory overuse while still maintaining a detector presences.
… Drastically reducing CPU usage and that new program smell. GL

Before you start the debate …
The thing about cheaters is they just keep cheating. You only need to catch that once.
No need for the microscope, you’ll get them anyways, while giving your actual program the majority of the resources the majority of the time. win/win

This is great work btw. I’m an instant fan!

1 Like

That’s not changed the memory performance at all, or not enough to be noticed. I’ll leave it at task.wait(1), because the longer I leave it, the average speed calculation becomes less accurate. It’s now a case of optimising the code I have. It’s native and it’s running on a seperate thread to the other code, this helps with impacting performance on the rest of the game.

Of course this changes the performance of the program. Just not this part of the program. The part of the program that matters the most, everything else. Also for something like this it would be nice to have a server controlled on and off or pause option, generically thinking of usage. This is to be a tool correct? …15k just don’t sound right. I’m suspecting multiple spawn calls.
This version calls it once for sure …

@native function Anticheat:AntiSpeedhacksAsync()
	task.desynchronize()
	local interval = 3

	local function manageStrikes()
		while true do
			task.wait(20)
			if self and self.Strikes then
				self.Strikes["Speed hacking"] = math.max(self.Strikes["Speed hacking"] - 1, 0)
			end
		end
	end
	
	task.spawn(manageStrikes)

	while true do
		task.wait(interval)
		if self.ShouldBreak then break end

		local player = self.Player
		if not player or self.Ziplining or self.Teleporting or not self.LastTick or not self.LastPosition then continue end

		local currentTick = tick()
		local hrp = self.HumanoidRootPart
		if not hrp then continue end

		local currentPosition = hrp.Position
		local yDiff = math.abs(currentPosition.Y - self.LastPosition.Y)
		local xDiff = math.abs(currentPosition.X - self.LastPosition.X)
		local timePassed = currentTick - self.LastTick
		local distanceTraveled = (currentPosition - self.LastPosition).Magnitude
		local speed = distanceTraveled / timePassed

		if yDiff < tolerableYDiff or xDiff > tolerableXDiff then
			if speed > tolerableWalkSpeed and speed < 300 and not self.Debounce and not self.Teleporting and not self.Ziplining and not self.Dash then
				self.Strikes["Speed hacking"] = (self.Strikes["Speed hacking"] or 0) + 1
				if self.Strikes["Speed hacking"] >= 5 then
					self:Sanction()
				end
			end
		end

		self.LastTick = currentTick
		self.LastPosition = currentPosition
	end
end

Well that’s my two cents. Good luck with your project.

There’s nothing I can see that could cause a 15K increase in heap, my guess is that somewhere maybe you’re running the checks multiple times for one player which stacks up over time? See if the heap continues to increase over time, if not check if it increases around 15K every time a player joins.

Also possible self is not being managed properly elsewhere in the code, such as if it’s being duplicated or not cleaned up correctly, this could lead to uncontrolled growth in heap usage, could also be caused by the repeated task creation?

The heap doesn’t increase over time, nor does it increase when multiple players are playing (tried in test server with 2 players). It’s never duplicated, and I made sure to disconnect all connections, stop all iterations, clear the object, and assign nil to self when destroying it. I’m not sure what could be causing this, I don’t have any memory leaks either.

I just took a look at it and under the main heap log for the entire class the anti-speedhacks function is marked as taking 15,283 in the heap (it’s shown as [native] in the console but it’s the only native one), however I see the actual name further down taking up just 4,016 in the heap. I think I misunderstood it, as it’s marked as the only native function within the anticheat script itself. When I removed the native attribute, I looked again and something was still there marked as [native], so I don’t really know what it might be that’s taking up 15,000 in the heap. If you have any idea of what it might be, please do let me know so I can try and fix it, otherwise thanks so much for your help!

Native functions or operations can have higher memory overhead since they interact with lower-level engine features, it might be that the memory usage is attributed to how Roblox manages these operations rather than your code directly.

Try and simplify your function as much as possible and see if the heap usage changes. For example, just check a player’s speed without any special conditions in AntiSpeedhacksAsync and see if this affects the heap. This could possibly help isolate the issue. Could also be a visual bug on Roblox’s end but it’s unlikely.

It seems that simply removing the native attribute reduces the size of the heap massively now, more than reducing the code size itself. But now I’m unsure whether to keep the native code or not, I heard from a staff member (staff videos) that native code compiling is better and more performance-efficient for functions that frequently perform mathematical calculations and comparisons and also don’t interact with Roblox APIs/interact minimally. However, this code is also stored separately and takes up more memory.

I’m a bit unsure whether to keep it native or not, what do you think?

Native is just about optimizing by generating C++ code for it. But if all you’re doing is accessing properties and using functions like math.abs, it won’t make a huge difference. The impact is significant when, for example, you’re doing buffer swapping, because the code generation turns it into C++ buffer swaps.

I’ll do a little more research and see how much it would benefit from being native, I’ll also refer back to the staff video I watched when learning about native code compiling in Luau. I think the main difference was when it was connected to RunService.Heartbeat, because I definitely saw a performance increase there.

Thanks for all your help!

:+1:
‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎ ‎

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.