FLEXSignal v1.1 | OUTDATED

THIS POST IS OUTDATED

FLEXSignal is a top-performance signal handling system. FLEXSignal allows you to create signals-like instances that you can use for event-based programming while reducing the memory it collects as much as possible without affecting the UX.

FLEXSignal is the first ( as far as I know) system that doesn’t rely on bindableEvents. It uses a cache-based system to lower down the memory collection as much as possible.


FLEXSignal is 100% free and open-source and available for anybody to use. You can get it by using this link: FLEXSignal - Roblox

FLEXSignal always try to make it easier for you to use it; which it achieves it amazingly using the following features that you can see in the RblxSignals(Default Events).

Features

Mutli-usage: FLEXSignal offer you the same usage experience when you are working with the normal events from multiple scripts. Just reference the signal with the ID you have created it with and then you are good to go!

Multi-Listening: FELXSignal offers you a multi-listening system where you can connect more functions to a signal(Although it is possible to make one event for everything, I discourage you from doing so).

Performance by design: FLEXSignal, unlike most of the other signal systems, doesn’t rely on BindableEvents which reduces the memory that is taken by signal. The memory differs depending on how many functions you connect and how you clear them, and the ID itself.


FLEXSignal offers you a rich API to create your own signals similar to RblxSignals without the use of BindableEvents which is simple as if like you are using a default event!

Docs

API Doc

FLEXSignal.new(int ID, int Mode)
Creates a signal and creates a cache table for it in the global cache table.

FLEXSignal:Connect( int ConnectionID, function method)
Connects a function while indexing the connection as ID. Returns a table with Disconnect that removes the connection.

FLEXSignal:Disconnect(int ConnectionID)
Removes the connection.

FLEXSignal:DisconnectAll()
Removes all the connections that are attached to the signal.

FLEXSignal:Fire(any …)
Fires the signal with the provided arguments

FLEXSignal:Destroy()
Removes the signal’s cache. Set any variables that reference the signal to nil.

FLEXSignal:GetSignal(int ID)
Returns the signal that is associated with the given ID . ForceFind means that if the function didn’t find the signal, it won’t yield.

Simple Project and Best Practices

In this section, you will create your first signal with FLEXSignal and know the best practices to follow!

Preparing

First get the model and then put it in the Serverstorage. Now create two scripts, 1 called Connector and the other one Handler and both in the serverscriptservice.

In the both scripts, reference the FLEXSignal module like the following:

local FLEXSignal = require(game.ServerStorage.FLEXSignal)

And now, you are good to go to the next step!

Creating a signal and connecting it and then Fire it

After you require the modulescript, you should create a new signal object in both of the scripts like this:

local NewSignal = FLEXSignal.new(1)

Now, why we choose 1? Well, you can put any number if it isn’t used before or 0.

Well, let’s make the crazy stuff! Let’s connect it to a function that will simply kick the provided player! We will start the connection in the Connector script by the following:

local Connection = NewSignal:Connect(2, function(Player) 
         Player:Kick("you are bad boy");
end)

Now let’s break down what’s happening here; When you connect the function, you pass two arguments, the first one is the ID that indexes the connection in the connections table and the second one, is well, the method that will be ran when the signal fires.

Now, in the Handler script, reference the signal object by the following:

local Signal = FLEXSignal:GetSignal(1)

It is pretty basic here, we get the signal by getting it from the cache that is saved in the modulescript using the ID we originally created it with. Please keep in mind that GetSignal() will yield for wait for the signal to be created for 20 seconds, if the said time passes but it still didn’t find the signal it will return nil. You can disable this behavior by adding a second argument that is equal to true; as a result, the function will return nil right away.

To fire the signal, we can do the following:

Signal:Fire(Player)
Best Practices

Use Disconnect() on connection that are no longer used

Use Destroy() on signals that are no longer used; FLEXSignal doesn’t have an automatic destroying signals due to the signals not being attached to a destroyable instance.

Use lower IDs, the more numbers you put, the more memory they take ( You can ignore this since it doesn’t impact that much)

Don’t form many connections on a single signal(it will take more time to loop through them)

Use DisconnectAll() on signals that you want to clear the connections of but still need it.

FAQ

Why use it over other signal-handling systems?
FLEXSignal is the most performant system that doesn’t rely on bindableEvents. It uses a cache-based system to lower down the memory collection as much as possible. In comparison to FastSignal, it is fast. You can verify that by going to the Update V1.1 post.

Is it fast?
FLEXSignals is reasonably fast if you don’t overload a signal with tons of connections that preforms complex jobs; at the most of the time, FLEXSignals doesn’t struggle really.

Do Server-made signal replicate with the client if the modulescript is placed in a replicated storage container?
Yes they do; meaning that clients can perform server-made connections but that’s will be only for them.

That’s it. I am more than happy to receive criticism or any suggestion that will improve the system.

9 Likes

That’s a really awesome Module. But i have a few things to point out that got my attention while i was reading about it.


That’s sadly not true, There’s plenty alot of other Signal Modules that don’t also rely on BindableEvents - Such as GoodSignal, FastSignal and SimpleSignal.


What makes you think this? Did you do any Unit Tests or compared it with other Signals? There’s a Devforum Post talking about Signals and their performance related to all of their functions.


Besides that, I have nothing else to say - I’m just trying to help you and know more your resource! :slightly_smiling_face:

2 Likes

Hello, thanks for your post.

Well, I think I should change that post. But thanks for that!

FLEXSignal massively functions like GoodSignal for example. However, I will try to do more unit tests to verify my claims.

Besides that, I will always try to improve my module. Actually, I am working on a server-sided & client-sided cache right now that will secure server-made signals.

EDIT
I will also add more features such as Wait and much more features to compete with the other sources & systems.

Peace

Good job, it seems pretty good. I recommend adding a couple of graphs comparing the speed of FLEXSignal and competitors.

2 Likes

UPDATE V1.1|| NEW MODES & PERFORMANCE IMPROVEMENTS


Modes


  • Strict Mode: Disables the multi-listening feature; meaning that the latest listener is the one that will be fired. Turn on with 1 in FLEXSignal.new second parameter.

  • Lean Mode: Enables the multi-listening feature: meaning that all the functions are ran, unless if the connection is either disconnected or the signal is destroyed. Turn on with 0 in FLEXSignal.new second parameter.


Usage

local Signal = FLEXSignal.new(ID, mode)

General Performance Improvements

Generally increased performance in functions such as Connect and Fire, and now the system uses pure coroutines without the use of task.spawn.

The avg processing time for connectandfire 50 attempts in Microseconds:

FLEXSignal : 27 
FASTSignal: 71

I tested my module’s performance with FastSignal since this is the fastest module that is in this Devforum post. In the future, I will compare my module’s performance with other competitors, and provide you a detailed doc about FLEX Performance as well.

1 Like

Nice resource, however, I have one critique. Why do you use an ID system instead of just using table.insert, which will automatically assign the next available space to your value? It would get annoying if you’re using a lot of signals, where you need to keep track of the IDs manually. If you have many scripts using this module, you could easily lose track of how many IDs you have.

Hello Medonox, I know this can be hard to track the signals with the ID system however, it is quite needed to get the signals outside of the script you created your signal in. I am not sure if there is another way of doing that, if you know one, tell me.

With the connections, well, it is for the disconnect function to work.

I am currently in the process of finding another way of doing all of that but in the meantime, that’s all what FLEXSignal got.

Peace

Putting criticism here, as this module can genuinely disrupt the workflow of users, raising my concerns.


  1. The overall structure is very messy, and makes it hard to work. There is no reason to implement an Id system, let alone force it on others.

  2. You use coroutine.wrap, which is advised against. coroutine.wrap eats up the stack trace, producing errors whose origins you will not be able to know about. It also creates a new thread for each time you call it.

  3. FLEXSignal.IsConnection code can be condensed down to the following:

return FLEXSignal.IsSignal(SignalD) and Cache[ConnectionID]
  1. There are inconsistent and non-idiomatic coding practices, such as:
  • inconsistent variable casing (ID, id, mode, etc have different casing from one another)
  • no space in local CacheD={
  • single-line blocks of code which bloaten the code, such as if self.__mode == 1 then coroutine.wraP(self.__connections)(...) return end
  • functions should be defined in the format of function Table.Method, rather than Table.Method = function()
  • triple-empty-line between SignalClass:Destroy() and SignalClass:Fire(); only one empty line is necessary
  1. For some reason, IntValue objects are used for connecting and disconnecting, despite the post being ‘proud’ over it not relying on Roblox OOP objects such as BindableEvents.

  2. The module does not work. Running the following code:

local FLEXSignalModule = require(script.FLEXSignal)
FLEXSignalModule.new(1)
FLEXSignal = FLEXSignalModule:GetSignal(1)

produces the following error:
image


Now, onto the benchmarks. Fixing the error above, here are the benchmark results using Boatbomber’s Benchmarking plugin:

1 callback function per signal:

100 callback functions per signal:

Despite the promises of this being the ‘best signal system out there’ and your statement of it being the fastest module, the benchmarks above show otherwise.

Benchmark code used
--[[
Comparison benchmark between the following Signal modules:

https://github.com/RBLXUtils/FastSignal
https://devforum.roblox.com/t/flexsignal-v1-the-best-signal-system-out-there/1762576
https://devforum.roblox.com/t/lua-signal-class-comparison-optimal-goodsignal-class/1387063/

]]

local NUM_CALLBACKS = 100

local FLEXSignalModule = require(script.FLEXSignal:Clone())
FLEXSignalModule.new(0, 2)
FLEXSignal = FLEXSignalModule:GetSignal(0)

local FastSignal = require(script.FastSignal:Clone()).new()
local GoodSignal = require(script.GoodSignal:Clone()).new()

for n = 1, NUM_CALLBACKS do
	local function Callback()

	end
	FLEXSignal:Connect(n, Callback)
end
for _ = 1, NUM_CALLBACKS do
	local function Callback()
		
	end
	FastSignal:Connect(Callback)
end
for _ = 1, NUM_CALLBACKS do
	local function Callback()

	end
	GoodSignal:Connect(Callback)
end

return {

	ParameterGenerator = function()
		return math.random(1000) / 10
	end,

	Functions = {
		FastSignal = function(Profiler, RandomNumber)
			for _ = 1, 1000 do
				FastSignal:Fire(RandomNumber)
			end
		end,

		FLEXSignal = function(Profiler, RandomNumber)
			for _ = 1, 1000 do
				FLEXSignal:Fire(RandomNumber)
			end
		end,

		GoodSignal = function(Profiler, RandomNumber)
			for _ = 1, 1000 do
				GoodSignal:Fire(RandomNumber)
			end
		end,
	}
}
3 Likes

I’ve also done some benchmarking but I benchmarked the shared features between each module and I’ve also found data that proves that FLEXSignal isn’t the fastest.

Code for the benchmark:

--[[

|WARNING| THESE TESTS RUN IN YOUR REAL ENVIRONMENT. |WARNING|

If your tests alter a DataStore, it will actually alter your DataStore.

This is useful in allowing your tests to move Parts around in the workspace or something,
but with great power comes great responsibility. Don't mess up your stuff!

---------------------------------------------------------------------

Documentation and Change Log:
https://devforum.roblox.com/t/benchmarker-plugin-compare-function-speeds-with-graphs-percentiles-and-more/829912/1

--------------------------------------------------------------------]]

local FlexSignal = require(script.FLEXSignal)
local FastSignal = require(script.FastSignal)
local GoodSignal = require(script.good)
return {

	ParameterGenerator = function()
	end;

	Functions = {
		["Fast"] = function(Profiler, RandomNumber)
			
			for i = 1, 500 do
			local sig = FastSignal.new()
			local connection = sig:Connect(function(...)
			end)
			sig:Fire(nil, "test1")
				connection:Disconnect()
			end
		end;

		["Good"] = function(Profiler, RandomNumber)
			
			for i = 1, 500 do
				local sig = GoodSignal.new()
				local connection = sig:Connect(function(...)
				end)
				sig:Fire(nil, "test1")
				connection:Disconnect()
			end
		end;
		
		["Flex"] = function(Profiler, RandomNumber)
			
			for i = 1, 500 do
				local sig = FlexSignal.new(i)
				local connection = sig:Connect(i,function(...)
					
				end)
				sig:Fire(nil, "test1")
				connection:Disconnect()
			end
		end;

		-- You can add as many functions as you like!
	};

}
1 Like

Hello Phseph, in this post I will reply to your concerns.

Can you please specify why it’s “messy”? Also, there is reason to implement an ID system which is referencing to a signal from other scripts. This is generally to give the users a more freedom on whatever signal they want to reference.

I am currently creating a thread manager system that will likely solve this issue.

Thanks for the suggestion!

Issue has been solved. the doc will be rewritten in order to keep up with the change

Fixed

I am just used to this type of coding :slight_smile: . I am sure this doesn’t affect performance, does it?

Same as above

And same as above, but I will fix it

Oh sorry, I kinda mislead it. I got mixed between both int and intValue. Fixed it!

Fixed

I am generally curious, which mode did you use for this benchmark test? All the tests I made were generally for strict-mode. However, the new thread managing system will optimize the lean-mode more.

NOTE: All the fixes will be in the public version soon.

A post on why I am using an ID System:

Most of the people are misunderstanding the real purpose of the ID system in identify signals and GetSignal as well.

GetSignal and when to use
GetSignal function is a powerful function that let’s you to interact with the signals you created from other scripts, not the scripts you originally created the Signal in. Use it when you need to interact with the signal outside of the script you created it in. Signal.new already returns the signal by default, so why the heck you don’t utilize that?

Can’t you just use table.insert?
No, and will never try to use it in the future. This limits your freedom on interacting with signals using GetSignal. It will likely return the latest event, which is indeed useless. If you love that behavior then FLEXSignal isn’t for you at all.

Well, then what’s with the ConnectionID?
It won’t be removed, since this will limit your interaction with manipulating connections from other scripts. Again, if you somehow don’t need to disconnect a connection outside of the parent-script, then this module isn’t for you.

A complete reply to @Madonox , @PysephDEV.

Okay, I have a few criticisms as well:

1- Biggest thing that stuck out to me was that you’re missing the signal:Wait() function, creating a whole connection for it to be fired a single time and then disconnecting it is a poor use of resources.

2- As others have said, you should be using table.insert for inserting connections. Not only for convenience, but it makes no sense that you’re expecting a number as an ID but refuse to use table.insert which only allows numbers as indexes. Furthermore, not using table.insert means you have to use pairs to iterate through the table, which is considerably slower than ipairs. The fact that you mention performance as one of your “selling points” but have refused to use table.insert (which apparently is faster than doing t[index] = v) isn’t a good sign.

3- There is a large discrepancy between this signal module and actual Roblox signals. An actual signal has two methods: Connect() and Wait() (the second one is missing from this module). When you call :Connect on this signal, it returns another object, RBXScriptSignal, and it is through this object you can disconnect, check if it’s connected and whatnot. Your script creates a single object on which you have to call Connect and Disconnect using some index the user decides, therefore your code sample will lead to unexpected results:

The Connection variable will be nil.

4- Your type checking is incorrect, you’re expecting an IntValue when you should be expecting a number. Likewise, there’s no checking that the inputted type is one the function expects,
image

5- In your constructor, you don’t check that an object with that ID exists in your cache, therefore you overwrite the old signal with a new one.

6- I don’t think using a cache in this case is necessary, one who is properly using this module should manage the objects on their own. A cache could lead to unpredicted results, especially given the fact that when calling the deconstructor, the deconstructed object still exists in the table, therefore it exists in the memory and could cause a memory leak. Also, if one of these signal objects were to go unreferenced in another script, the objects from this module will never be garbage collected, which is another source of a memory leak.
image

In most cases, caches are used to store data that is a result of an asynchronous call (eg. images or data returned from a web request). Since constructing your object is synchronous, it is nonsensical to use a cache in this case.

You also talk about how your cache can allow multiple scripts to access the same object, however one who has proper knowledge of modular programming would know how to and would implement a way to replicate this feature in their own style.

That’s actually one of the trickiest things about Signals, and even task.insert is wrong.

The actual correct way to go about storing connections is using a linked list, a linked list is just a bunch of little nodes with info that point to each other, more specifically to the next one in the list (when it doesnt have also a reference to the previous node, which my fastsignal does for performance reasons), the most recently connected nodes, so the most recently connections, will be the first on the list, while the oldest, is the last.

In lua that’s done with tables, you’ll have a member inside the signal object which points to the first node in the list, and on the node, you’ll have a reference to the next node, and also the connection function, etc.

If you don’t use a linked list, then essentially it doesn’t have parity to Roblox’s RBXScriptSignal, it can also cause some issues with race conditions. (If it’s using task.defer to fire, usually those aren’t an issue, just the order is gonna be off).

And then you just adjust and update these references as you connect/disconnect connections.

There’s a lot to making a good signal library, I hyperfocused on it last year and I figured a lot of little details that make the whole difference. Things you would think don’t make a difference but do, especially with Immediate mode because of those race conditions that are hard to explain.

If I was to recommend a Signal library at the moment, it would be FastSignal (duh it’s literally mine) (not to be confused with stravant’s fastsignal which is almost an example of what not to do), because GoodSignal hasn’t fixed a few issues that hardly have any effect on performance.

1 Like

ill do a benchmark but running the functions 100k times. Lets see if flexsystem is truly a “best system out there”

image
whats this annoying error.

script:

--[[
Benchmark between FastSignal and FLEXSignal.
]]

local FLEXSignal = require(script:WaitForChild("FLEXSignal"))
local FastSignal = require(script:WaitForChild("FastSignal"))

local MethodToCount = os.clock
local TimesToRun = 100000

local FirstTest = MethodToCount()
for i = 1,TimesToRun do
	local Signal = FastSignal.new()
	local connect = Signal:Connect(function() end)
	Signal:Fire(nil,'bench')
	connect:Disconnect()
end
print(MethodToCount() - FirstTest," Fast signal.")
local SecondTest = MethodToCount()
for i = 1,TimesToRun do
	local generatedid = math.random(math.ceil(i/10)) -- lol
	local Signal = FLEXSignal.new(generatedid)
	local connect = Signal:Connect(i,function() end)
	Signal:Fire(nil,'bench')
	Signal:Disconnect(generatedid)
end
print(MethodToCount() - SecondTest," Flex signal.")