Parrallel lua is just as slow as serial lua

Here are some images of the server microprofiler:

Here is an image of these 7 things closer (They are pretty much the same)

Here is some big thing that takes up to 1600 ms frame times, I think this is the problem, since it uses a lot of cpu and it isn’t stacked like your image:

I can also send you the HTML if you would need it

This is the terrain script:

local Emitter = script.Parent.Emitter
local Receiver = script.Parent.Receiver

function PerlinNoise(coords,amplitude,octaves,persistence)	
	coords = coords or {}
	octaves = octaves or 1
	persistence = persistence or 0.5
	if #coords > 4 then
		error("The Perlin Noise API doesn't support more than 4 dimensions!")
	else
		if octaves < 1 then
			error("Octaves have to be 1 or higher!")
		else
			local X = coords[1] or 0
			local Y = coords[2] or 0
			local Z = coords[3] or 0
			local W = coords[4] or 0

			amplitude = amplitude or 10
			octaves = octaves-1
			if W == 0 then
				local perlinvalue = (math.noise(X/amplitude,Y/amplitude,Z/amplitude))
				if octaves ~= 0 then
					for i = 1,octaves do
						perlinvalue = perlinvalue+(math.noise(X/(amplitude*(persistence^i)),Y/(amplitude*(persistence^i)),Z/(amplitude*(persistence^i)))/(2^i))
					end
				end
				return perlinvalue
			else
				local AB = math.noise(X/amplitude,Y/amplitude)
				local AC = math.noise(X/amplitude,Z/amplitude)
				local AD = math.noise(X/amplitude,W/amplitude)
				local BC = math.noise(Y/amplitude,Z/amplitude)
				local BD = math.noise(Y/amplitude,W/amplitude)
				local CD = math.noise(Z/amplitude,W/amplitude)

				local BA = math.noise(Y/amplitude,X/amplitude)
				local CA = math.noise(Z/amplitude,X/amplitude)
				local DA = math.noise(W/amplitude,X/amplitude)
				local CB = math.noise(Z/amplitude,Y/amplitude)
				local DB = math.noise(W/amplitude,Y/amplitude)
				local DC = math.noise(W/amplitude,Z/amplitude)

				local ABCD = AB+AC+AD+BC+BD+CD+BA+CA+DA+CB+DB+DC

				local perlinvalue = ABCD/12

				if octaves ~= 0 then
					for i = 1,octaves do
						local AB = math.noise(X/(amplitude*(persistence^i)),Y/(amplitude*(persistence^i)))
						local AC = math.noise(X/(amplitude*(persistence^i)),Z/(amplitude*(persistence^i)))
						local AD = math.noise(X/(amplitude*(persistence^i)),W/(amplitude*(persistence^i)))
						local BC = math.noise(Y/(amplitude*(persistence^i)),Z/(amplitude*(persistence^i)))
						local BD = math.noise(Y/(amplitude*(persistence^i)),W/(amplitude*(persistence^i)))
						local CD = math.noise(Z/(amplitude*(persistence^i)),W/(amplitude*(persistence^i)))

						local BA = math.noise(Y/(amplitude*(persistence^i)),X/(amplitude*(persistence^i)))
						local CA = math.noise(Z/(amplitude*(persistence^i)),X/(amplitude*(persistence^i)))
						local DA = math.noise(W/(amplitude*(persistence^i)),X/(amplitude*(persistence^i)))
						local CB = math.noise(Z/(amplitude*(persistence^i)),Y/(amplitude*(persistence^i)))
						local DB = math.noise(W/(amplitude*(persistence^i)),Y/(amplitude*(persistence^i)))
						local DC = math.noise(W/(amplitude*(persistence^i)),Z/(amplitude*(persistence^i)))

						local ABCD = AB+AC+AD+BC+BD+CD+BA+CA+DA+CB+DB+DC

						perlinvalue = perlinvalue+((ABCD/12)/(2^i))
					end
				end

				return perlinvalue
			end
		end
	end
end

local availablePositions = {
	Vector3.new(0, 1, 0),
	Vector3.new(1, 0, 0),
	Vector3.new(0, 0, 1),
	Vector3.new(-1, 0, 0),
	Vector3.new(0, 0, -1),
	Vector3.new(0, -1, 0),
}

--task.desynchronize()

local function toRun(chunk, position, extras)		
	local chunkSize, seed, scale, amplitude, cave_scale, cave_amplitude = unpack(extras)
	local blocks = table.create(chunkSize.X)
	
	task.desynchronize()
		
	for x = 1, chunkSize.X + 1 do
		if not blocks[x] then blocks[x] = table.create(chunkSize.Z) end	
		local real_x = position.X * chunkSize.X + x

		for z = 1, chunkSize.Z + 1 do
			if not blocks[x][z] then blocks[x][z] = table.create(chunkSize.Y) end
			local real_z = position.Z * chunkSize.Z + z

			for y = 1, chunkSize.Y  + 1 do
				local real_y = position.Y * chunkSize.Y + y - 1
				local cave_density = PerlinNoise({real_x, real_y, real_z, seed}, cave_scale) * cave_amplitude
				
				local block = {
					--position = Vector3.new(real_x, real_y, real_z),
					material = "unknown",
				}

				if real_y > 3 then
					if cave_density > 25 then
						block.material = "air"
					else
						local density = y + PerlinNoise({real_x, real_y, real_z, seed}, scale, 4) * amplitude -- (amplitude + chunk.splineValue)
						
						--if 150 > (density - chunk.splineValue) then
						--if density < 130 then
						if density < 135 then
							block.material = "stone"
						else
							--block.light = 0
							block.material = "air"
						end
					end
				else
					if real_y == 1 then
						block.material = "barrier"
					else
						local barrier_density = (PerlinNoise({real_x, real_y, real_z, seed}, 5) * 100) + real_y

						if math.abs(barrier_density) > 2 then
							block.material = "barrier"
						end
					end
				end
				
				
				blocks[x][z][y] = block
			end
		end

		--[[if x % 5 == 0 then
			task.wait()
			--task.desynchronize()
		end]]
	end

	for x = 1, chunkSize.X + 1 do
		for z = 1, chunkSize.Z + 1 do
			for y = 1, chunkSize.Y + 1 do
				local block = blocks[x][z][y]

				if block.material ~= "air" then
					--[[local topBlock = blocks[x][z][y + 1]
					if not topBlock or topBlock.material == "air" then
						block.visible = true
					end]]
				else
					for _, position in ipairs(availablePositions) do
						local real_x = x + position.X
						local real_y = y + position.Y
						local real_z = z + position.Z
						
						if blocks[real_x] and blocks[real_x][real_z] then
							local neighbor = blocks[real_x][real_z][real_y]
							
							if neighbor and neighbor.material ~= "air" then
								neighbor.visible = true
							end
						end
					end
				end
			end
		end

		--[[if x % 5 == 0 then
			task.wait()
			--task.desynchronize()
		end]]
	end
			
	return blocks
end


Receiver.Event:Connect(function(threadId, name, arguments)	
	local result = toRun(unpack(arguments))
	Emitter:Fire(threadId, result)
end)

and this is how I’m creating the actors:

local ThreadManager = {}
local Thread = {}
Thread.__index = Thread

local HttpService = game:GetService("HttpService")
local ThreadsFolder = game:GetService("ServerScriptService"):FindFirstChild("Threads")

if not ThreadsFolder then
	ThreadsFolder = Instance.new("Folder", game:GetService("ServerScriptService"))
	ThreadsFolder.Name = "Threads"
end

function ThreadManager.new(threads)
	threads = threads or 12
	assert(typeof(threads) == "number", "The 'threads' argument must be a number")
	assert(threads > 1, "You must chose at least 1 thread")
	
	local threadObject = setmetatable({}, Thread)
	threadObject.lastRun = 0
	threadObject.threadNum = threads
	threadObject.threads = {}
	
	for i = 1, threads do
		local newThread = script.Thread:Clone()
		newThread.Parent = ThreadsFolder
		
		table.insert(threadObject.threads, newThread)
	end
	
	return threadObject
end

function Thread:RunFunction(name, arguments, onFinished)
	assert(typeof(name) == "string", "The 'name' argument must be a string")
	assert(typeof(arguments) == "table", "The 'arguments' argument must be a table")
	assert(typeof(onFinished) == "function" or typeof(onFinished) == "nil", "The 'onFinished' argument must be a function or nil")
	--assert(script.Functions:FindFirstChild(name), "function not found")
	
	self.lastRun += 1
	if self.lastRun > self.threadNum then
		self.lastRun = 1
	end
	
	local threadId = HttpService:GenerateGUID(false)
	local threadUsing = self.threads[self.lastRun]
	
	local emitterEvent ; emitterEvent = threadUsing.Emitter.Event:Connect(function(receivingId, ...)		
		if threadId == receivingId then
			emitterEvent:Disconnect()
			
			if onFinished then
				onFinished(...)
			end
		end
	end)
	
	threadUsing.Receiver:Fire(threadId, name, arguments)
end

return ThreadManager

This is the structure of it:

image

I know it’s a lot to take in, but I don’t really understand how I’m supposed to fix this

Yes it is. It clearly shows the threads all running in serial, one after the other, instead of in parallel where they are all done simultaneously. Well now you know what the exact problem is.

Firstly, have you checked to make sure the tasks are being evenly distributed among the threads? There’s the possibility that a bug made all the tasks only go to one actor and thus it’s still technically running in serial.
You can check for that in the microprofiler too by numerically naming the threads and seeing which ones are working and which ones aren’t. Name them like how I did it here; the actor scripts are called “ChunkThread (number)”:

Secondly, why is there a discrepancy with the names? The actor script’s name is “Manager” in the explorer, but in the microprofiler it’s reported as “Script_Manager”. In reality, the names should perfectly match.
image

image

1 Like

I think this is because the server and the client microprofieler are different? There is no script in the game starting with Script_ and I even renamed the script from Manager to Manager (id), and now the microprofiler calls it Script_Manager (id) like in this photo:

I belive the problem lays in one of these scripts:

Terrain builder:

	for _, chunk in ipairs(newChunks) do
		local start = tick()
		
		Threads:RunFunction("build_chunks", {chunk, chunk.position, extras}, function(blocks)
			self.chunks[chunk.position.X][chunk.position.Y][chunk.position.Z].blocks = blocks
			finished += 1
			
			warn(tick() - start)
			if finished == #newChunks then
				onFinished()
			end
		end)
	end

Actor manager:

local ThreadManager = {}
local Thread = {}
Thread.__index = Thread

local HttpService = game:GetService("HttpService")
local ThreadsFolder = game:GetService("ServerScriptService"):FindFirstChild("Threads")

if not ThreadsFolder then
	ThreadsFolder = Instance.new("Folder", game:GetService("ServerScriptService"))
	ThreadsFolder.Name = "Threads"
end

function ThreadManager.new(threads)
	threads = threads or 12
	assert(typeof(threads) == "number", "The 'threads' argument must be a number")
	assert(threads >= 1, "You must chose at least 1 thread")
	
	local threadObject = setmetatable({}, Thread)
	threadObject.lastRun = 0
	threadObject.threadNum = threads
	threadObject.threads = {}
	
	for i = 1, threads do
		local newThread = script.Thread:Clone()
		newThread:FindFirstChild("Manager (id)").Name = `Manager ({i})`
		newThread.Parent = ThreadsFolder
		
		table.insert(threadObject.threads, newThread)
	end
	
	return threadObject
end

function Thread:RunFunction(name, arguments, onFinished)
	assert(typeof(name) == "string", "The 'name' argument must be a string")
	assert(typeof(arguments) == "table", "The 'arguments' argument must be a table")
	assert(typeof(onFinished) == "function" or typeof(onFinished) == "nil", "The 'onFinished' argument must be a function or nil")
	--assert(script.Functions:FindFirstChild(name), "function not found")
	
	self.lastRun += 1
	if self.lastRun > self.threadNum then
		self.lastRun = 1
	end
	
	local threadId = HttpService:GenerateGUID(false)
	local threadUsing = self.threads[self.lastRun]
	
	local emitterEvent ; emitterEvent = threadUsing.Emitter.Event:Connect(function(receivingId, ...)		
		if threadId == receivingId then
			emitterEvent:Disconnect()
			
			if onFinished then
				onFinished(...)
			end
		end
	end)
	
	threadUsing.Receiver:Fire(threadId, name, arguments)
end

return ThreadManager

It uses all 4 actors (I am testing it with 4 so it won’t crash the server), each of them using 1.2 seconds to generate a chunk.

EDIT:

Do you mind if I enable team create or uncopylock the game so you can look in for yourself?

May I ask what are the specs of your computer? How many cores does your CPU have?

1.2 seconds is the total, and each of them uses 0.3 seconds.

That is fine.

1 Like

I have a I5 10400 (a 6 core CPU), 16GB of RAM, and a gtx 970

Thanks! Here is a link to the game (I uncopylocked it so you can have a look for yourself): Voxel terrain game - Roblox

It appears to be multithreading for me when I test it in studio.

1 Like

It doesn’t seem to work in live games, since server response times rise up to 20 000 ms. Are just roblox servers this bad? Also is there any way to prevent it from running a actor on the main thread?

I’m afraid that’s something outside of your control. All I know is that it works perfectly fine on the client. Could be a bug or just that Roblox hasn’t enabled it for servers yet. :neutral_face:

1 Like

Yeah, I think roblox didn’t enable workers on the server, since it seems to work fine when running on local studio, however the microprofiler on real servers show it serially.

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