Signal Certifications & Classes Guide

Signal Certifications & Classes

[1] What are Signals?

It’s a custom module meant to improve-upon and replace BindableEvents as an event-dispatcher, they usually take up less memory and often include additional features for versatility such as :DisconnectAll().

Terminology Clarification

  • Event-dispatcher = The Signal module itself
  • Dispatch = :Fire()
  • Listeners = connections
  • Mutations = new connections & disconnections, midway through a dispatch.
  • Reentrancy = A function that calls itself in a recursive-like manner. :Fire() inside of :Fire() to the same event.
  • Deferred = NOT task.defer, I just mean the literal concept of inserting an action to the very end of a list/queue/stack.

[2] What are Certifications?

Certifications are badges to serve as proof of capability that they’re guaranteed to be reliable in some form of specified functionality.

List of Certifications

:locked: Scheduler Certification

If the Signal has the minimum working features expected of any event-dispatcher on Roblox.

Details
  • :Connect() → Event Listener that can be dispatched multiple times until disconnected.
  • :Once() → Event Listener that will disconnect itself upon/after its first dispatch.
  • :Wait() → Event Listener that yields the current cooperative thread (coroutine thread).
  • :Disconnect() → Method called by connections to disconnect themselves.
  • :DisconnectAll() → Method called by the signal to disconnect all connections.

:high_voltage: Speed Certification

If all minimum features of the Signal are faster or fast enough to replace BindableEvents without perceptibly harming existing systems implemented by developers. Results are compared to the baseline speeds (BindableEvents) within a margin of acceptable speed.

Details

< +0.1µs means that the Signal is allowed to be up to +0.1µs slower or faster than BindableEvents, measured in microseconds or 0.000001 of a second. Comparison values are retrieved from the 50th percentile of samples, 1_000 → 100_000 samples depending on the metric.

  • Create< +2.0µs, to create the Signal object.
  • Connect< +0.1µs
  • Once< +0.1µs
  • Fire_None< +2.0µs, to dispatch with 0 listeners
  • Fire_One< +3.0µs, to dispatch with 1 listener
  • Fire_Many< +3.0µs, to dispatch with 100 listeners
  • Fire_OneYield< +5.0µs, to dispatch with 1 listener that yield using task.wait()
  • Fire_ManyYield< +5.0µs, to dispatch with 100 listeners that yield using task.wait()
  • Disconnect< +0.1µs, to disconnect 1 listener
  • DisconnectAll< +0.1µs, to disconnect 100 listeners at once

:light_bulb: Connected Certification

If the Signal includes full support for the .Connected value/property for connections. Readability of a connection’s state is important for cleanup systems/modules to effectively handle them.

Details

It must be accounted for by the following:

  • :Connect()true
  • :Once()true
  • :Disconnect()false
  • :DisconnectAll()false
  • .Connectedboolean

:puzzle_piece: Snapshot Certification

If midway-mutations are processed only after listeners are ran and reentrant dispatches are deferred.

Details

Ideal Stack Order:

  1. :Fire() [START]
  2. Run connections → ALL connections are ran even if they’re disconnected midway through.
  3. Process mutations
  4. Run/Continue deferred reentrant dispatch queue:Fire() within a :Fire() is deferred so mutations are respected.
  5. [END]

:clapper_board: Synchronous Certification

If event-dispatches are ran-to-completion before it returns/synchronous. If all connections are ran, mutations processed, and reentrant dispatches ran & completed before the initial :Fire() returns. The code will wait for it to finish before it moves on.

Details

Note: Connections being ran in a dispatch will exit the “stack” (but remain connected) if they become asynchronous/encounter a yield such as task.wait or coroutine.yield(), this is to preserve the dispatcher’s synchronous behavior.

:alarm_clock: Order Certification

If connections are ran and maintained in the order of Oldest → Newest inserted.

Details

Preserved behaviors with Oldest → Newest

  1. Causality
    In order to respect how time works, each new listener should only extend or observe older listeners. It’s easier to work with what has already happened than to predict what might happen. This is non-negotiable in complex systems where you can’t keep track of the timing of every connection.

  2. Composability
    Exploring deeper in causality and how it affects systems that stack on one another, this ordering allows for assumptions to remain stable without the need for side-effect safety due to new systems modifying values afterwards rather than beforehand.

  3. Determinism
    Causality and composability affects how independent handlers determine actions and results based on what has already happened rather than through pure prediction.

  4. Debuggability
    Determinism except for the specialized purpose of debugging/searching for potential issues. Debuggers should only ever observe what has already happened, this is the only point that truly necessitates Oldest → Newest.

^ Although the above behaviors can similarly be done in Newest → Oldest, it will require an unnecessary amount of extra effort depending on application complexity.

[3] What are Classes?

When certain certifications are combined, they qualify for one of three levels of classification representing their utility-capability and favorability over BindableEvents. Although it’s strongly encouraged to use Class 3 signals, it’s ultimately not necessary depending on your needs. Any Signal can be considered “good enough” as long as it isn’t Class 0.

List of Classes

:3rd_place_medal: Class 0

The signal lacks one or all necessary certifications to be safe or even worth using over BindableEvents.

:2nd_place_medal: Class 1

The signal is safe and fast enough for specialized use outside of needing the .Connected property, a potential suitable replacement to BindableEvents.
Requirements

  • :locked: Scheduler Certified
  • :high_voltage: Speed Certified

:1st_place_medal: Class 2

The signal is a complete suitable replacement to BindableEvents for all situations outside of absolute speed.
Requirements

  • :locked: Scheduler Certified
  • :high_voltage: Speed Certified
  • :light_bulb: Connected Certified

Note: Absolute speed is only required in outlier-heavy workloads that deals with 100_000+ connections at once which most developers will never need nor encounter.

:trophy: Class 3

The signal follows the modern standard that best fits Roblox game development, Synchronous FRP (Functional Reactive Programming) with Atomic Propagation. Signals of this classification should always be used over BindableEvents and other classifications due to these superior event-dispatcher behaviors:

  • Snapshot/Queue-based dispatch ordering
    It’ll run all connections uninterrupted first, apply mutations afterwards, then finally resume deferred reentrant event dispatches (:Fire() within a :Fire()). This is awesome because it entirely eliminates mutation-related bugs caused by dependence on connection timing or insertion order, which is crucial for complex projects where you can’t easily keep track of connection-execution timing.
  • Synchronous timing
    It’ll run all connections, mutations, and nested dispatches to completion prior to returning the :Fire(). This guarantees that the event’s effects are applied exactly when you wanted it and never after (not asynchronous).
  • Connections are ran and maintained in the order of “Oldest → Newest”
    This direction is far superior because it preserves:
    • Causality, respects time.
    • Composability, no side-effect safety needed for stacking systems.
    • Determinism, easier to determine based on what has already happened than what might happen.
    • Debuggability, this REQUIRES Oldest → Newest if attempting to debug older connections.

It’s still completely backwards compatible to reeimplement some or all of BindableEvent’s quirks at your discretion at minimal to no human-perceptible speed cost.

The only things that BindableEvents do right are order maintenance (but in Newest → Oldest) and reentrance deferral.

Requirements

  • :locked: Scheduler Certified
  • :high_voltage: Speed Certified
  • :light_bulb: Connected Certified
  • :puzzle_piece: Snapshot Certification
  • :clapper_board: Synchronous Certification
  • :alarm_clock: Order Certification
Rant Yappathon

Thanks to BindableEvents (and all pre-existing Signals) lacking one or multiple of those vital behaviors, it should be easy to understand the canon events that it forced every developer designing a complex system to encounter. Such as, finding out that there’s an order-based bug that required you to implement side-effect safety. Or even worse (happened to me MULTIPLE times), you were forced to design your ENTIRE event handling process around order dependence because you had no choice. That is the PRIMARY reason I was spiteful enough to spend 2 months of my life researching and developing Signal Certifications & Classes for the best-possible event-dispatcher standard in the context of Roblox game development. Class 3 signals finally exist on Roblox not only to prevent those canon events from happening in the first place, but they also finally give you a CHOICE.

[4] How to test a Signal?

By following these 5 easy steps:

  1. Download the Grader (either download works)
  1. Insert the grader to ServerScriptService
Image

  1. Set the SignalReference’s value to the Signal module you want to test
Image

  1. Ensure that all of the configurations are to your liking.
Image

  1. Run/Playtest to view your results in the output
Images




[5] What do existing Signals score as?

This is of all current Signals that I could find on the developer forum and niche development servers.
Downloads and images of certifications are present below, feel free to test them out yourself.
If you do happen to pick a Signal to use, here are my personal recommendations:

🥉 Class 0

CuteSignal

A synchronous-only signal that runs connections one at a time to completion, where you need to manually wrap connections in a thread (task.spawn | task.defer) to be able to have asynchronicity support. The only reason it’s Class 0 is because it doesn’t have built-in asynchronous error-catching, so an entire main-thread that calls Fire will break if there’s a single error in any of the connections. If you do wish to use this signal, I recommend wrapping connections in a pcall/xpcall where-ever applicable.
Download: Devforum Page

DProSignal

Download: Github Page

SignalPlus

Download: Devforum Page

SimpleSignal

Download: Devforum Page

Suphi’s Signal Module

Download: Creator Store Page

ZeroSignal

Another synchronous-only signal (you need to manually wrap connections in a thread for asynchronicity). This signal is archived and should never be used in a live project.
Archive: Devforum Page

🥈 Class 1

FastSignal

Download: Devforum Page

GoodSignal

Download: Devforum Page

🥇 Class 2

LemonSignal

Download: Github Page

MadSignal

Creator: GohanDucis
The Signal of my own design during the development of the Signal Certifications & Class system, abandoned after the release of the :alarm_clock: Order Certification in favor of entrusting the first Class 3 Signal efforts to the awesome creator of SomeSignal.
Download: MadSignal.lua (23.9 KB)

Wire

A signal where you can create “Wires”, which are unique Ids that you use to manually pass through to wire-functions.
NOTE: Use [5] instead of .Connected to check if a connection is still connected (example in Code Sample).

Code Sample
local Wire = require(path.to.Wire)

-- Create a signal
local PlayerDamaged = Wire.signal()

-- Connect a listener
local connection = Wire.connect(PlayerDamaged, function(player, damage)
    print(player.Name .. " took " .. damage .. " damage!")
end)

-- Fire the signal
Wire.fire(PlayerDamaged, player, 25)

-- Disconnect when done
connection:disconnect()

-- Check that it disconnected
print(connection[5]) -- false

Download: Github Page
Grader Adapter: WireAdapter.lua (636 Bytes)

Zignal

Creator: Z3SC
Download: Zignal.lua (2.9 KB)

🏆 Class 3

NamedSignal

A Luau signal implementation that lets you name and define variadic parameters — conveniently.
Download: Devforum Page

NewSignal

New Gen Signal implementation.
NOTE: Use [1] instead of .Connected to check if a connection is still connected (example in Code Sample).

Code Sample
local signalMD = require(path.to.NewSignal)

-- Create signal
local signal = signalMD.new()

-- Connect a listener
local connection = signal:Connect(function()
	print("Signal fired!")
end)

-- Fire the signal
signal:Fire()

-- Disconnect when done
connection:Disconnect()

-- Check that it disconnected
print(connection[1]) -- false

Download: Github Page

SomeSignal

Creator: 6inch9inch
SomeSignal used and even built upon this guide to also offer extra features at no compromise such as:

Download: Devforum Page

❔ Honorable Mentions

Signals that cannot be tested by the SIgnal Certifications Grader without significant modifications due to the non-standard API that they offer to developers. (I haven’t made an adapter to test them yet)

There’s nothing here…

20 Likes

That’s just reinventing the wheel but making it lag :wilted_flower: :v:

local next = 1
local stack = table.create(4)::{[number]:(...any)->()}
stack[next] = function():()
	
end
next+=1

for i,v in stack do v() end

Cool code, now try running it through the grader and please admit you haven’t read a single word of the post.

This is a guide meant to modernize the Roblox event-dispatcher status quo that’s decades behind what’s found in other programming languages and engines.

If you’re talking about “reinventing the wheel”, yeah this obviously already has been done before outside of Roblox, the standard I’m introducing is long-standing and already proven to be better, that’s why it’s one of the many standards. But what about it in the context of it already existing on Roblox? 6inch9inch’s SomeSignal actually is the first of its kind as far as I’m deeply aware of, you can check it out in the Class 3 dropdown at the bottom of the post.

Edit: I realize I might’ve misinterpreted your comment as mockery rather than a genuine suggestion, if I happened to do so, I apologize.
MadSignal does use arrays as you suggest if you want to check them out, but LinkedLists are more versatile and easier to work with at roughly the same practical/applied speed and memory costs. Something to disclose is that I’ve only benchmarked superficial speed costs and the dev console LuauHeap memory, but I have yet to check the Microprofiler and Garbage Collection statistics to see if there truly is a major performance difference to consider between LinkedLists & Arrays in Signals as they are.

6 Likes

Hello Yarik :face_with_tongue:
I heard your suggesting :money_mouth_face: and decided to experiment with that idea :money_bag: :grin:
This is very good idea and I will cut ze fluff and eat ze bugs :fist:, see you next update :grin: :100:

3 Likes

From mine :backhand_index_pointing_left: shallow research :test_tube: :scientist:
Linked list fairs better :file_cabinet: :pensive: than array based :car::dashing_away: signal :loudspeaker::speaking_head:
Sorry to dissapoint you Yarik :headstone::dove:

1 Like

How many emojis… express yourself I guess. :face_exhaling:

This looks yummy, cant wait to test it on my own

1 Like

Happy to hear that this may be of help!

1 Like

I lied about the stable release my bad.
NOW there are no more bugs in SomeSignal.

  • Fixed consecutive reentrant dispatches/Fires
  • Fixed multi-signal mutation handling

Now we wait for its creator to officially release it and have it claim its rightfully-earned #1 spot as the indisputable best Signal/Event-Dispatcher on the Roblox platform. :>

SomeSignal Changelog

Refer to SomeSignal’s current documentation for more information of the changes below.
[5] What do existing Signals score as? -> :trophy: Class 3 dropdown -> Extra Features & Documentation

  • More Disconnection methods to be able to specifically disable current OR nested connections during a dispatch/Fire.
    • [signal]:DisconnectCurrentConnections(mode: "deferred" | "immediate")
    • [signal]:DisconnectNestedConnections(mode: "deferred" | "immediate" | "both")
  • Renamed ⁨:DisconnectAllMutations()⁩ to ⁨:CancelAllMutations()⁩ for a more accurate implication (less confusion).
  • Fixed the last few KNOWN multi-signal usage bugs. The creator & I have done EXTENSIVE testing this time to account even for the most niche of potential issues.
  • There’s now an creator-store release version that’ll receive continued support, it’ll officially be released on Github & the Devforum Community Resources as soon as SomeSignal’s creator finally gets appealed (banned because of his username).

CHANGELOG | Signal Certifications & Class Guide

  • Scheduler Certification Changes
    • New test: AsyncErrorCatch, if signals that don’t use threads for Connect & Once connections use pcall or xpcall instead for safe asynchronous error catching.
    • New test: ConsecutiveReentrancy, if a signal can do reentrant Fire in the same Fire layer, as well as reentrant Fire in multiple reentrant layers.
    • New test: MultiSignalCNs, tests if created connections are bound strictly to their signal in creation & to Fire.
    • New test: MultiSignalNested, an expansion of the previous test but for nested connections. Also tests if one signal can Fire other signals within its connections (nested but not reentrant Fire).
    • New test: MultiSignalDisconnectAll, if DisconnectAll only affects the signal it’s used on.
  • Misc Changes
    • Now supports signals that internally don’t use threads at all for Connect & Once within the primary :Fire() method, which also reflects the speed benefits in the Speed Certification’s Fire_One and Fire_Many benchmarks.
2 Likes

This is basically a Thing to follow when making a Signal system. I was making one and just found this to test mine. It still has some issues, I will upload soon to see if I can get to Class 3.

1 Like

CHANGELOG | Signal Certifications & Class Guide

  • Misc Changes
    • The grader no longer gets stuck on a few indefinite yield cases (regarding Wait_Safety & Wait_Chained tests) and C stack overflow errors (ConsecutiveReentrancy test).
      • Meaning? Extremely bugged signals such as ZeroSignal can now run the Signal Grader to completion.
1 Like

Unfortunately for you, Zero Signal is archived and I’ve already released the continuation in the form of FlowSignal(devforum).
Now with SoA, Better perfomance and safety. You can test with Async callbacks

Btw I made a new signal module and it should be Class 3, if you want to add it to the list. (Connected check needs to use [1].

1 Like

This might actually be the fastest Class 3 Signal thus far, congratulations :fire:
Let me know if you want anything changed in your section of the post.

I’ve also updated the Signal Certifications Grader to easily change the ConnectedIndex (so it can work with [1] instead of [“Connected”]).

all roads lead to signal bloat, it’s inevitable yarik

1 Like

Do you have like any benchmarks if it was possible?

I gotchu:

NewSignal

SomeSignal

NamedSignal

NewSignal’s relative speed ranking:

  • Create: 1st
  • Connect: 2nd
  • Once: 3rd
  • Fire_None: 3rd
  • Fire_One: ~2nd
  • Fire_Many: ~2nd
  • Fire_OneYield: ~2nd
  • Fire_ManyYield: ~2nd
  • Disconnect: 3rd
  • DisconnectAll: 3rd

Nvm I misremembered the benchmarks for SomeSignal & NamedSignal sorry, NewSignal is definitively faster in only one category (creating the signal obj).
You could check out NamedSignal’s speed optimization techniques to apply to your own signal if you’d like.

The problem with Names Signal is that it uses Coroutine.resume. Which is faster than task.spawn but it doesn’t sync well with the scheduler. That makes it around 120% faster

1 Like