[Beta] Deferred Lua Event Handling

Using @tnavarts’ implementation of Signal, the sheer speed has proved to be 2-3x times faster than any existing implementations of Signal Classes. But do note, that it doesn’t allow for yielding in any of the connected handlers.



Sample A: Old Signal Classes. Refer to any of the archived implementations seen here: https://devforum.roblox.com/t/when-to-use-bindableevents/828609/4?u=ukendio
Sample B: New Signal Class. Refer to the Oberserver Pattern in aforementioned post

I have thoroughly tested it, which you can inspect here: https://github.com/Ukendio/Game/blob/master/src/shared/tests/test2.spec.ts.

Overall, I believe there is very good rationale behind this change. Previously, we have had to make some nasty workarounds in our code to comply to the weird behaviour of yielding handlers, having to suspend Signals during the reconciliation processes. It is one of those things that if Roblox changed this earlier, we wouldn’t have minded. In the future, I hope when changes like these are implemented that we are thoroughly assured of what would be affected in detail. When the post was first announced, I was panicking at type of problems would arise, and that’s not always a nice feeling. It is not reassuring that a lot of the recent updates has been breaking old games and/or current workflow.

1 Like

In the near future we will be enabling a new method that allows you to defer a thread for resumption in the same invocation point. It’s effectively fastSpawn if you aren’t worried about exactly when the thread is called. Separately, we are looking at adding a version of this method which does resume instantly.

Thanks for taking the time to looking into this a little further.

Input event handlers are deferred to the first invocation point after we process the input itself, RenderStepped. This results in them running after BindToRenderStep when they should run before. We will address this in a future update.

1 Like

Thanks for the report. I’ve figured out the cause and it will be fixed in a future update.

1 Like

It should be possible for us to link these errors to the event handlers that are exceeding the re-entrancy depth. I’ll update you when I’ve taken a more in depth look at this.

2 Likes

If your game is set to ‘Immediate’ or ‘Default’ then it should be working as normal. Deferred event handling is only enabled when SignalBehavior is set to ‘Deferred’.

This will still result in an infinite wait since the event is queued (and then deferred) before you start waiting for it.

1 Like

I really hope that there will be a feature in the future where you can customize each event to be instant or deferred, with deferred being the default option. Something similar to this would also be better then just removing instant events.

When I turn my game to deferred I get lots of errors in scripts which do not error in the default/immediate mode:

When the server starts there is a script which copies, saves and then deletes certain models from the workspace so those models can be put in workspace later during the game. The scripts inside those models seem to run even after deletion of their parent resulting in errors like:

10:00:03.171  Workspace.Building.Building4.Purchases.0 Bathroom Door.DoorSetup:1: attempt to index nil with 'Parent'  -  Studio
10:00:03.171  Stack Begin  -  Studio
10:00:03.171  Script 'Workspace.Building.Building4.Purchases.0 Bathroom Door.DoorSetup', Line 1  -  Studio
10:00:03.172  Stack End  -  Studio

most (if not all) errors are located where the script is trying to index either its parent or childs. There are no events involved in this proces.

the order in which my game is setup (there are no events involved in this proces):

  1. server starts
  2. serverscript copies the models from workspace and stores them in a table and then deletes them from workspace. (errors in deferred mode)
  3. serverscript put those models back in the game when required at a later point

After reading the post quoted above I see that script deferring is also part of this new feature. Does this mean that these errors are intended behaviour and that scripts can this way still run even after deletion of their parent because they’re queued/delayed? if so, how would one fix this issue effectively?

wait(x) before the deletion of those models DOES work but is not preferred as this feels a bit hacky and can still result in errors if queued/delayed scripts are not finished before deletion of their parent.

5 Likes

I found a problem about callıng functions with UserInputService.InputBegan:Connect()

I’m going to include just the necessary parts and do my best to explain the rest.

Let’s say we bind the button “G” to a function through this method:

UIS.InputBegan:Connect(function(input)
	if input.KeyCode == Enum.KeyCode.G then
		a()
    end
end)

And our function “a” is like this:

    frame.Size = UDim2.new(0, 0, 0, 0)
    frame.Position = UDim2.new(0.5, 0, 0.5, 0)
    frame.Visible = true
   
    TW:Create(frame, TweenInfo.new(duration, Enum.EasingStyle[style], Enum.EasingDirection.Out, 0, false, 0), {Size = UDim2.new(sizeX, 0, sizeY, 0)}):Play()
    TW:Create(frame, TweenInfo.new(duration, Enum.EasingStyle[style], Enum.EasingDirection.Out, 0, false, 0), {Position = UDim2.new(positionX, 0, positionY, 0)}):Play()
    wait(duration)

Just ignore the variables in the function, what it simply does is, it makes any type of frame object visible by animating it through TweenService. It sets the size to zero and the position to the middle point of the screen. Then it scales it up while maintaining its position in the middle. The wait(duration) just halts the script until the tween animation is completed.

However, if the initial size of the frame is not zero and it is not positioned in the middle of the screen then the frame appears as if it wasn’t set to zero size nor positioned to the middle for a single frame even though frame.Visible = false in the initial state (before this function was called).

So as soon as this function is called, the frame appears in its original state (size & position) for a one-frame time, then the animation starts to play by first setting its size to zero and position to the middle of the screen.

I solved this problem by calling the function in a spawn(function()). But this wasn’t the case back when I was using “Immidiate” SignalBehaviour.

I’m going to include a video to show you what exactly I’m talking about incase my explanation was not sufficient:

Here you can see, for a fraction of a second the frame appears as nothing has changed even though the frame.Visible = true comes after the initial size and position being changed in the function.

And here how it appears after I use spawn(function()) to call the same function.

In summary, it seems like there is a relatively huge delay between the property change and the line of code that makes it happen. So the order of the lines doesn’t really help the output.

Thanks for your attention.

1 Like

As a developer in multiple engines. we always use deferred functions instead of directly editing our game’s state.
For example instead of executing an immediate function like a destroy function. most of the time we use it’s deferred version to avoid any problems.
Basically we queue the task to the engine instead of executing it immediately.

A basic example from godot engine:
The difference between the function “free” and “queue_free”.

Relying on roblox previous event immediate firing. Might make the game run as fine in 99% of situations but there will be that one game where the order of firing is just not perfect, and the game breaks leaving you with no idea of what caused it.

But if I understand roblox’s new update right. this will make events more stable and confusion free for us and also for roblox’s future updates that are related to parallel lua and other advanced features.
Of course that might not be the case. but that’s my best guess!

5 Likes

Looks like the same issue I reported earlier.

Now we only need a way to defer BindToClose to run while the game is closing without freezing studio.

1 Like

I’m so sorry, I must have not seen it since there are so many posts on this topic. I just wanted to post a report about the problem right away. I forgot to check if there are similar ones beforehand.

This might be a dumb question because I don’t understand parts of what all goes into this, but could we have something where this feature is introduced as a second type of connect, something like :connectSynchronous(), and then not change or replace :connect() but mark it as deprecated or give it a warning? In spite of performance benefits, a few other people have pointed out there are a few cases where continuing to use non-deferred event handling seems more maneuverable, and there’s also selectively maintaining old scripts if given the choice between both methods

3 Likes

Not sure how or why, maybe its just that i use the Knit framework, but this behavior randomly causes my game to not initialize on the server.

Can you give an example of what this might look like? The game I’m working on right now makes heavy use of Binders for its objects and game logic.

Let me note that he whole value of fastSpawn isn’t deferring execution. It’s saying “hey, Roblox, next time you effectively yield, please don’t block this current thread.

In other languages you have to write keywords like “await X” or declare functions as async. Lua doesn’t really care about this, and creating new threads (i.e coroutines) is reasonably cheap.

If coroutines reported their stacktrace accordingly, we wouldn’t even need other mechanisms for this sort of thing.

It’s unfortunate that wait/delay/spawn can’t be changed because effectively these already solved this problem, but are stuck operating at 30hz, on an old system.

5 Likes

This is problematic if the AncestryChanged event fires after :Destroy() or :Destroy() is called during AncestryChanged.

This will potentially break garbage collection in my games in the case of the :Destroy() event being called.

11 Likes

Agreed. A friend and I were just discussing how insane of a change that would be.

Really hope they don’t go through with that.