Lua Signal Class Comparison & Optimal `GoodSignal` Class

Hey, thanks for your response. It appears that the errors are completely anonymous. Have no clue what to do at this point; the error doesn’t even appear after key game events or triggers so I’m completely lost. Why is Roblox not supplying context with the error message?

Note: I can only reproduce this in-game. Haven’t seen it once in studio; perhaps more common when large amount of players in-game?

function Signal.new()
	return setmetatable({
		_handlerListHead = false,
		_firing = false,
	}, Signal)
end
function Signal:Fire(...)
	local item = self._handlerListHead
	
	if self._firing then
		error("re-entrancy")
	end
	
	self._firing = true
	
	while item do
		if item._connected then
			--local trace = debug.traceback()

			--local split = trace:split("\n")
			--local new = {}

			--for _, v in split do
			--	local newSplit = v:split(".")

			--	table.insert(new, newSplit[#newSplit])
			--end

			--print(table.concat(new, ", "))

			if not freeRunnerThread then
				freeRunnerThread = coroutine.create(runEventHandlerInFreeThread)
				-- Get the freeRunnerThread to the first yield
				coroutine.resume(freeRunnerThread)
			end
			task.spawn(freeRunnerThread, item._fn, ...)
		end
		item = item._next
	end
	
	self._firing = false
end

Do you have deferred events enabled?

Hey, I use Good Signal.

I’m curious about the relevance of enabling deferred events when Good Signal is written in pure Lua. Typically, the “deferred events” setting only affects Instances.

Not at this moment, I do not.

I am going off the same assumption as the above

Edit: I’ve found the source of the bug, thank you for your help.

Lately, I’ve thought about how modules that people release on these forums have their own events that feel just like the built-in ones, and have discovered why they use “Signal” and how useful it could be for my own scripts. (Custom events could be so helpful!)

One question, though. I’ve noticed that multiple modules that I’ve added to my game come with their own copies of signal within them. Does having multiple redundant copies of this signal module accessed by different scripts affect performance or is suboptimal in some way?

I’m considering placing the signal module in a universal location then pointing all of the modules to use it instead of their embedded versions, but I don’t know if it’s worthwhile or necessary.

1 Like

Having multiple modules for the same class would only hurt a miniscule amount of performance. I would say plugging in a universal implementation would be useless.


My main problem with any signal implementation, though, is the lack of being able to type-annotate them. I semi-succeeded in creating one that got the datatypes, but there is no way to type the parameter names, making it a bit useless.

For self-serving modules, I usually just use BindableEvents and override their RBXScriptSignal with a pseudo-type. For example:

local Bindable = Instance.new("BindableEvent")

type Callback = (Name: string, Value: number) -> ()
type Signal = {
	Connect: (self: Signal, Callback: Callback) -> RBXScriptConnection,
	Wait: (self: Signal) -> (string, number),
	Once: (self: Signal, Callback: Callback) -> RBXScriptConnection,
	ConnectParallel: (self: Signal, Callback: Callback) -> RBXScriptConnection
}

local Event = (Bindable.Event :: any) :: Signal
Event:Connect(print) -- Callback: (Name: string, Value: number) -> ()

Obviously, this is far from being compact and trivial, but the alternative is no autocomplete.

2 Likes

Not worth the trouble.

Roblox has a parser cache, so if two modules have exactly the same source code in them, the game engine will only parse the module once, and both modules will share most of the same function definitions etc. The only thing that won’t be shared is the closure / table memory which is not very much.

I measured it and extra copy of GoodSignal uses just under 3kb of memory (and no additional network bandwidth because script sources are deduplicated for network replication). 1 full size texture uses 4000kb of memory, so pick your battles. There’s probably better things for you to be spending your time on than deduplicating GoodSignal.

1 Like

Do you ever plan on supporting ConnectParallel? Or is that not possible.

I think it’s too early, with details of parallel Luau still being likely to change a bit. The next major update to parallel Luau is probably the first time I’ll consider it.

1 Like

ConnectParallel can be implemented by adding task.synchronize and task.desynchronize, after that the user need to use Actor Instance for the usage.

Does GoodSignal behave the same as deferred lua event handling as described here?

No, however, if you just replace the line:
task.spawn(freeRunnerThread, item._fn, ...)

With:
task.defer(freeRunnerThread, item._fn, ...)

It will have that behavior. I don’t think it makes sense for me to offer this variant until after the transition to deferred has happened.

1 Like

Hello, I’m still a little confused on the GC behavior of goodsignal, it might be general lua GC behavior, but for some reason cyclic references are causing the GC to not collect things.

Here is an example to show this:

local Weak = require(script.Parent.ModuleScript)
local Table = {
Event = require(game.ServerScriptService.Signal).new()
}

Table.Event:Connect(function()
print(Table)
end)

Weak[2] = Table

Here’s my test for checking if the table has been GCed

local Weak = setmetatable({}, {__mode = ‘v’})

task.spawn(function()
while true do
table.create(10^6)
print(Weak)
task.wait()
end
end)

return Weak

And in this first example, as you see the table never get’s gced.

I know this is specfically an issue with cyclic references, because if I reference an instance it still gets collected

local Weak = require(script.Parent.ModuleScript)
local Table = {
Event = require(game.ServerScriptService.Signal).new()
}

Table.Event:Connect(function()
print(workspace.Baseplate)
end)

Weak[2] = Table

This does get collected.

And the bizzare thing is, this only happens in the script corourtine, if I spawn a new coroutine it does getcollected

local Weak = require(script.Parent.ModuleScript)

task.spawn(function()
local Table = {
Event = require(game.ServerScriptService.Signal).new()
}

Table.Event:Connect(function()
print(Table)
end)

Weak[2] = Table
end)

Which doesn’t make sense to me, because in both cases “Table” it isn’t accessible anymore, once the script is done running or the coroutine finishes. But even more this specfically seems to be an issue with cyclic references

This is a PSA, if you coroutine.yield() inside of a connection callback and then later resume it, DONT RESUME IT MORE THAN ONCE. Seems like common sense but you won’t get any direct errors making it unclear where the problem is and the signal event handler will get messed up. Spent a good hour figuring out why GoodSignal was erroring.

you could probably change the GoodSignal code to check if a RunnerThread was resumed from within GoodSignal and ignore or error otherwise

1 Like

Is this how I would implement a RemoteEvent with a signal?

	local Event = Signal.new(RemoteEvent.OnClientEvent)
	Event:Connect(function()
		print("remote fired to client")
	end)

i think you are looking for this

1 Like

What do I do if I want to delete a signal?

You call :DisconnectAll() to remove all the connections and then remove any references to that signal.

I fire the signal on the server, but nothing is received on the client, how do I fix that?

This is not for use across the server-client boundary, its like a BindableEvent but in luau.

1 Like