Hey developers,
We’re enabling a new opt-in feature which will change when event handlers are called by our engine. Here I’m going to dive into detail about what the feature does, why we are doing it, and how it may affect you if you choose to opt-in.
Update (May 21)
We know some of you had concerns about this feature, we have updated the post to better reflect our intentions. See the reply from @zeuxcg for more in depth information.
What we’re doing
Right now, all event handlers get called immediately when an event fires. For every instance added to your game, property that changes, or some other trigger that is invoked we need to run Lua code right then and there.
Deferred events change this. Instead of resuming handlers immediately we will defer them until a slightly later time. We then have specific ‘invocation points’ where we call all of the deferred event handlers and any event handlers they in turn defer.
This diagram compares the existing behavior (‘Immediate’) and the behavior when this feature is enabled (‘Deferred’). Overall, the total time taken does not change but the ordering does slightly. This occurs because the handler for ‘Event A’, which invokes ‘Event C’, does not run until after the main thread (blue) yields.
Our set of invocation points may increase over time depending on the feedback we receive, however right now these are:
- RenderStepped
- Waiting script resumption (e.g. wait, spawn, delay)
- Stepped
- Heartbeat
- BindToClose
Why we’re doing it
Across the Roblox engine there are many different events which can all invoke Lua code. For a number of reasons, this isn’t ideal:
- If we want to change 1,000 properties, 1,000 snippets of code potentially need to run after each change
- Performance-critical systems can fire events requiring them to yield back and forth to Lua
- Event handlers can make changes to the place or trigger other events any time an event is fired
- An event can fire multiple times despite being redundant (such as a property changing twice)
By having specific portions in which Lua-can-run we can build better systems based on a number of assumptions:
- Performance-critical systems don’t need to yield to Lua leading to performance gains
- The place will never change outside of an invocation point (unless we change it)
- We can collapse redundant events into a single event (we don’t do that just yet)
How you’ll be affected
Unless you have code in your game that depends on the immediacy of event handlers you are unlikely to notice a difference. Event handlers will continue to be called in order, in the same engine step, albeit slightly later.
We know some developers rely on event handlers being called immediately and can appreciate that transitioning to the new behavior may not be easy therefore you can explicitly set your place’s signal behavior to ‘Immediate’ opting-out long before we make any changes to the default behavior. Doing this will ensure your games continue to use the current behavior for as long as we support it.
To help you determine if you’re affected, here are some common code-patterns that will have a slightly different behavior when deferred events are enabled:
FastSpawn
FastSpawn is an alternative to Spawn which allows you to run a callback instantly without halting the current thread if it yields. If it is implemented with an engine event (as shown below) it will now be resumed later in the current Lua invocation point.
local function fastSpawn(f, ...)
local bindable = Instance.new("BindableEvent")
local args = table.pack(...)
bindable.Event:Connect(function ()
bindable:Destroy()
f(table.unpack(args))
end)
bindable:Fire() -- will now queue the event for resumption
end
In addition, we are planning to introduce a new method which will make deferring a thread easier. More to come on this soon.
Inline Resumption
Code which relies on events being resumed immediately will stop working correctly. In the following example, false will always be returned when deferred events are enabled because the callback has not run.
local success = false
event:Connect(function ()
success = true
end)
doSomethingToTriggerEvent() -- Causes `event` to fire
return success
You must make sure you yield the thread until at least when the event should have fired.
Connect Once
Sometimes you only want to listen for the first occurence of an event
connection = event:Connect(function ()
connection:Disconnect()
-- do something
end)
With deferred events enabled, multiple events can be queued before you disconnect from the event. We are looking at alternative solutions but for now you can manually check if the event is still connected before running any code.
connection = event:Connect(function ()
if not connection.Connected then
return
end
connection:Disconnect()
-- do something
end)
We are also aware of some minor issues with Roblox scripts. These will be fixed as soon as possible.
How you can enable it
You can test the new behavior in Studio by changing the SignalBehavior property of Workspace:
- Default: The default behavior (currently ‘Immediate’)
- Deferred: Event handlers are deferred
- Immediate: Event handlers are called immediately
Feedback
If you have any feedback about this feature, questions, comments, or concerns please post them here.
FAQ
If an event fires is it deferred until the next frame?
No, we process the queue of deferred events until it is empty. If you invoke another event with your code, that event will be added to the back of the queue and run later in the same invocation point. The diagram above shows an example of this. ‘Event C’ is triggered by the handler of ‘Event A’ so we add its handler to the back of the current queue.
There is still a concept of ‘re-entrancy’ which prevents events from continuously firing one another when they reach a certain depth. The current limit for this is 10.
When will Lua run?
Lua will run at specific invocation points. Right now, these are: