Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Match Scratch's script sequencing and execution semantics #212

Open
adroitwhiz opened this issue Jul 16, 2024 · 6 comments
Open

Match Scratch's script sequencing and execution semantics #212

adroitwhiz opened this issue Jul 16, 2024 · 6 comments

Comments

@adroitwhiz
Copy link
Collaborator

Right now, Leopard's execution semantics seem to be rather simple--we step each script once per frame. This is close enough to what Scratch does in many cases, but in actuality it's a bit more complex. Scratch's logic is roughly:

  • Once per frame, we step all the scripts.
  • Then, we check if any redraws have been requested. Most blocks which cause a visual change on-stage (e.g. motion blocks, looks blocks) will request a redraw, but notably the "hide" block does not.
  • If no redraws have been requested, we keep stepping all the scripts until either a redraw is requested, all the scripts are done, or we've run out of time budget (75% of the frametime).

Scratch also supports "turbo mode" semantics, which fit neatly into the above logic: if turbo mode is enabled, then we don't stop when a redraw is requested, only caring about the other two conditions (scripts are done, or time budget).

The main pitfall to watch out for when implementing this is busy-waiting. Right now, there's a lot of code that yields in a loop while waiting for a promise to resolve. This is fine if we only step threads once per frame, but in the case where no redraws have been requested or we're running in turbo mode, those loops will gladly spin until we've used our time budget, eating up a bunch of CPU time.

To avoid busy-waiting, I believe we need runtime support for yielding to promises--a script waiting for a promise is placed into a "sleeping" state where it will not be stepped until the promise resolves.

@towerofnix
Copy link
Member

Out of curiosity... busy waiting is (unfortunately) a thing in Scratch, right? The easiest tell is that just clicking a "forever" block will cause dragging around blocks in the editor to suddenly feel choppy and slow.

Can you narrow down on which circumstances of busy waiting we wish to avoid, and/or define busy waiting more particularly? Although we want to match Scratch's project behavior i.e. all execution semantics, we can (and do) differ in the implementation that "gets us there". This may be able to include avoiding certain instances of busy-waiting, but we have to be sure that the definition of busy-waiting is separate from the definition of runtime semantics. The presence or absence of any busy-wait we remove shouldn't affect project semantics.

@adroitwhiz
Copy link
Collaborator Author

Yes, there are still situations where Scratch will busy wait--as you mentioned, an empty "forever" block with nothing to request a redraw will do so, and I believe a "wait" block will do so as well in the absence of any redraw requests.

Scratch will not busy-wait on promises, however--threads have their own "promise wait" state that causes them to be skipped over until the promise resolves. Any thread that is itself waiting for one of those threads to finish will need to do the same--see scratchfoundation/scratch-vm#1211, for instance.

@PullJosh
Copy link
Collaborator

While we're discussing promises, this feels like a good time to reference the discussion about triggers: #197 I'm not very happy with the user-facing API for triggers, and it feels like an addEventListener-inspired approach would be better. In that discussion, I created a proof of concept system that includes support for async functions and async generator functions.

@adroitwhiz
Copy link
Collaborator Author

adroitwhiz commented Jul 17, 2024

I actually don't think we need async functions or generators--I'm working on an experimental runtime revamp that allows you to simply yield a promise (more or less) and have the runtime automatically pause the script then resume it with the resolved value of the promise.

EDIT: For instance, this is how askAndWait is implemented:

public *askAndWait(question: string): Yielding<void> {
  yield* Thread.await(this._project.askAndWait(question));
}

@PullJosh
Copy link
Collaborator

Oh, nice! (I believe yielding a promise is equivalent to an async generator, but I'm not 100% sure.)

@adroitwhiz
Copy link
Collaborator Author

It looks like the difference is that an async generator always yields promises, whereas a regular generator only yields promises if the thing being yielded is already a promise.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants