Wait until end of current event scheduling cycle (equivalent to coroutine.yield's old behavior)

As a Roblox developer, it is currently impossible to activate a condition dependent on future events that might occur within the same event scheduling cycle.


I have two properties that pair together. Most of the time they are both updated together, but sometimes only one of them is updated. Whenever either of them is updated, I need to insert a new model into the game.

I want to wait until the end of the current scheduling cycle in order to check if both properties change, or if only one property changed. This allows me to make only one update to the model instead of two or more!

This used to be possible with the old coroutine.yield behavior:

local object = OBJECT
local updating = false
local function update()
	if updating then
		return
	end
	updating = true
	coroutine.yield()  -- wait until end of current event scheduling cycle
	updating = false
	-- do stuff with object.Property1
	-- do stuff with object.Property2
end

object:GetPropertyChangedSignal("Property1"):connect(update)
object:GetPropertyChangedSignal("Property2"):connect(update)

---

object.Property1 = "example"  --update is queued to end of scheduling cycle
object.Property2 = "example-prop-2"

-- update runs before the cycle ends
coroutine.yield()  -- wait until the end of the cycle, after update has ran
-- the model is now updated, do anything with model.

This is no longer possible. Now we either have to update on both Property1 and Property2 (bad performance), or update after a frame or step has passed (not instantaneous).

Scheduling explanation

Whenever Roblox runs scripts, it goes through a cycle of resuming scripts that need to be resumed. This is an event scheduling cycle. Generally, when waiting, you wait until some other cycle, so you wait a significant amount of time where the game may have updated, network may have sent, rendering might have changed, etc.

By running programming in the same cycle, you can assure that rendering, networking, physics, etc. is all in the same state, and that there is no delay between event and response from the player’s perspective.

A wait to the end of a cycle would be a very small wait and not be useful for waiting. It’s only useful for scheduling operations between multiple scripts that should “all occur at once” from the player’s perspective. As in the example, sometimes some programming should always run last, because it’s dealing with multiple changes. As it is now, we have no way to do that without adding a delay between event and response.

Also worth noting that I don’t know what this is actually called internally. I think Roblox Staff probably have an official name for it other than “event scheduling cycle”.



If Roblox is able to address my issue, it would allow me to write more efficient programming when dealing with multiple related events.

1 Like

Could always use bindables as a middle-man event, no?

How so?

local MiddleMan = Instance.new'BindableEvent';

local function This()
    DoSomething();
    MiddleMan.Event:wait(); -- Wait until it is fired
    DoSomethingElse();
end;

This(); -- Fire MiddleMan from somewhere where you're handling the scheduling of things

Done something similar to this before, and seems to work in all cases I’ve needed it for.

This doesn’t accomplish the goals of the post.

Using that, I’m not able to wait for an unknown number of events to complete and only have a response once all events that would occur within the same scheduling cycle are done, and have the response run in that scheduling cycle. Using your system, I have to have something constantly running separately, which means that there might be scheduling cycles between the events and responses.

I use BindableEvents pretty extensively since I like the scheduling opportunities they provide, but that cannot accomplish what this feature request is looking for. They can get close to it, though, and can make an alright substitute.

You’re likely looking for RunService.Heartbeat or RunService.RenderStepped then. They fire off every frame (which is when all threads have yielded or control jumped I assume).

coroutine.yield was anomalous; the thread was yielded, but a wait time was not specified. The scheduler resolved this by appending the thread to the end of the queue with a reasonable wait time of 0, causing it to be resumed almost immediately. This behavior was never guaranteed, so any code that leveraged it was a hack at best.

As I understand, this was changed so that coroutine.yield (and spawn, it appears) respects settings().Lua.DefaultWaitTime (30Hz), which makes it more consistent with other yielding methods, and more respecting of the concept of “frames”.

So far, I don’t see a problem. Your window for collecting property changes just increases to ~30Hz. Unless this model is being updated frequently (I’m talking >30Hz frequent), this delay is going to be imperceptible.

This sounds like an extremely unsafe assumption to make. Why is it necessary?

1 Like

It’s preferred that updates to models occur almost as soon as the property changes.

For example, I would expect

customObject.model = "fancy new model"
customObject.variant = "a neat variant of the model"

to update the model before the game resumes running non-Lua stuff. I would expect it to be very similar to

customObject.model = "fancy new model"
customObject.variant = "a neat variant of the model"
updateModel()

but I want it to update without explicitly calling updateModel.


Currently, it has to be either the equivalent of

customObject.model = "fancy new model"
updateModel()
customObject.variant = "a neat variant of the model"
updateModel()

or

customObject.model = "fancy new model"
customObject.variant = "a neat variant of the model"
wait()
updateModel()

Edit: It seemed pretty predictable. Not for waiting for an amount of time, but for scheduling tasks: it would always be executed during the same cycle unless it hit the maximum amount of resumes, which was not expected to be hit unless you abused the API. I could be mistaken about this though!


Those might actually work. I doubted they would occur soon enough for my liking, but it looks like they occur ~0.003 seconds after an event fires – in another words, right away.


I can see how writing stuff in this way may be considered bad design, and I’m all for hearing why! I don’t remember any significant discussion about this prior to its removal, so I wanted to mention how it might be useful since I came across a use case.

1 Like

Preferred, but not required. The design is risky more than anything, mainly because it’s working on behaviors that are not guaranteed. If something changes in the scheduler implementation, your carefully structured code could end up becoming less efficient than a more robust solution.

Unfortunately, there isn’t actually very much information on schedulers. So it’s difficult to discern what behaviors are “official”. There’s this page on the wiki, but there isn’t much detail, and it was written by users besides. Unless an engineer can come in here and clarify, we can only speculate.

I would avoid making the assumption that the Lua scheduler traverses its entire queue in one go. Consider: the scheduler could have a time budget, per frame, in which it is allowed to resume threads. Once that budget runs out, it must yield control back to non-Lua routines. In the next frame, the scheduler gets a new budget, and picks up where it left off. If this is how the scheduler is implemented, then your statement,

you can assure that rendering, networking, physics, etc. is all in the same state

will not always be true, because the budget can run out before your thread gets to resume.

I would also avoid the assumption of threads being added to the end of a queue. Or the assumption of a queue, for that matter. The minimum wait times that are now being enforced hide this detail. It would be reasonable to say that it’s being hidden on purpose so that developers don’t try to rely on it, and so that engineers have the option of changing how it works, or swapping it out for some other data structure entirely.

3 Likes

coroutine.yield() unyielding in the same frame was more of a bug than a feature and has been fixed

It’d be nice if there was more information about the threadscheduler, and possibly some new ways of controlling it (e.g. a function to yield even in roblox threads)