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

Fixes yield proc scheduling. Turns sleeps into an opcode #1539

Draft
wants to merge 15 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions Content.Tests/DMProject/Tests/Sleeping/YieldOrder.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
var/counter
#define ExpectOrder(n) ASSERT(++counter == ##n)

/proc/BackgroundSleep(delay, expect)
set waitfor = FALSE
sleep(delay)
world.log << "Expect: [expect]"
ExpectOrder(expect)

#define MODE_INLINE 0 // spawn
#define MODE_BACKGROUND 1 // set waitfor = FALSE + sleep
#define MODE_RAND 2 // random seeded

#define TestSleep(delay, expect) if(mode == MODE_INLINE || (mode == MODE_RAND && prob(50))){ spawn(##delay) { ExpectOrder(##expect); } } else { BackgroundSleep(##delay, ##expect); }

/proc/TestSequence(mode)
counter = 0
var/start_tick = world.time

TestSleep(0, 2)
ExpectOrder(1)
sleep(0)
ExpectOrder(3)

TestSleep(-1, 4)
ExpectOrder(5)

TestSleep(0, 6)
sleep(-1)
ExpectOrder(7)

TestSleep(-1, 8)
ExpectOrder(9)
sleep(-1)
ExpectOrder(10)

TestSleep(1, 13)
sleep(-1)
ExpectOrder(11)
sleep(0)
ExpectOrder(12)

ASSERT(world.time == start_tick)

sleep(1)
ExpectOrder(14)

/proc/RunTest()
world.log << "Inline:"
TestSequence(MODE_INLINE)

world.log << "Background:"
TestSequence(MODE_BACKGROUND)

rand_seed(22475)
for(var/i in 1 to 10000)
world.log << "Rand-[i]:"
TestSequence(MODE_RAND)
Comment on lines +55 to +58
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to up the test timeout because this overran 500ms

4 changes: 3 additions & 1 deletion Content.Tests/DMTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using NUnit.Framework;
using OpenDreamRuntime;
using OpenDreamRuntime.Objects;
using OpenDreamRuntime.Procs;
using OpenDreamRuntime.Rendering;
using OpenDreamShared.Rendering;
using Robust.Shared.Asynchronous;
Expand All @@ -28,6 +29,7 @@ public sealed class DMTests : ContentUnitTest {

[Dependency] private readonly DreamManager _dreamMan = default!;
[Dependency] private readonly DreamObjectTree _objectTree = default!;
[Dependency] private readonly ProcScheduler _procScheduler = default!;
[Dependency] private readonly ITaskManager _taskManager = default!;

[Flags]
Expand Down Expand Up @@ -138,7 +140,7 @@ public void TestFiles(string sourceFile, DMTestFlags testFlags) {
watch.Start();

// Tick until our inner call has finished
while (!callTask.IsCompleted) {
while (!callTask.IsCompleted || _procScheduler.HasProcsQueued || _procScheduler.HasProcsSleeping) {
_dreamMan.Update();
_taskManager.ProcessPendingTasks();

Expand Down
14 changes: 3 additions & 11 deletions OpenDreamRuntime/Procs/DMOpcodeHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1703,20 +1703,12 @@ public static ProcStatus Spawn(DMProcState state) {
// and have state.Spawn return a ProcState instead
DreamThread newContext = state.Spawn();

//Negative delays mean the spawned code runs immediately
if (delayMilliseconds < 0) {
async void Wait() {
await state.ProcScheduler.CreateDelay(delay);
newContext.Resume();
// TODO: Does the rest of the proc get scheduled?
// Does the value of the delay mean anything?
} else {
async void Wait() {
await state.ProcScheduler.CreateDelay(delay);
newContext.Resume();
}

Wait();
}

Wait();
state.Jump(jumpTo);
return ProcStatus.Continue;
}
Expand Down
16 changes: 11 additions & 5 deletions OpenDreamRuntime/Procs/ProcScheduler.Delays.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ public sealed partial class ProcScheduler {
// This is for deferred tasks that need to fire in the current tick.
private readonly Queue<TaskCompletionSource> _deferredTasks = new();

public bool HasProcsSleeping => _tickers.Count > 0;

/// <summary>
/// Create a task that will delay by an amount of time, following the rules for <c>sleep</c> and <c>spawn</c>.
/// </summary>
Expand All @@ -40,12 +42,16 @@ public Task CreateDelay(float deciseconds) {
/// The amount of ticks to sleep.
/// </param>
public Task CreateDelayTicks(int ticks) {
// When the delay is <= zero, we should run again in the current tick.
// Now, BYOND apparently does have a difference between 0 and -1. See https://github.com/OpenDreamProject/OpenDream/issues/1262#issuecomment-1563663041
// They both delay execution and allow other sleeping procs in the current tick to run immediately.
// We achieve this by putting the proc on the _deferredTasks lists, so it can be immediately executed again.
if (ticks < 0 && !HasProcsQueued) {
// special case, only yields when there is more work to do
return Task.CompletedTask;
}

if (ticks <= 0) {
// When the delay is <= zero, we should run again in the current tick.
// Now, BYOND apparently does have a difference between 0 and -1, but we're not quite sure what it is yet.
// This is "good enough" for now.
// They both delay execution and allow other sleeping procs in the current tick to run immediately.
// We achieve this by putting the proc on the _deferredTasks lists, so it can be immediately executed again.

var defTcs = new TaskCompletionSource();
_deferredTasks.Enqueue(defTcs);
Expand Down
4 changes: 3 additions & 1 deletion OpenDreamRuntime/Procs/ProcScheduler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ public sealed partial class ProcScheduler {
private readonly Queue<AsyncNativeProc.State> _scheduled = new();
private AsyncNativeProc.State? _current;

public bool HasProcsQueued => _scheduled.Count > 0 || _deferredTasks.Count > 0;

public Task Schedule(AsyncNativeProc.State state, Func<AsyncNativeProc.State, Task<DreamValue>> taskFunc) {
async Task Foo() {
state.Result = await taskFunc(state);
Expand All @@ -51,7 +53,7 @@ public void Process() {
// If a proc calls sleep(1) or such, it gets put into _deferredTasks.
// When we drain the _deferredTasks lists, it'll indirectly schedule things into _scheduled again.
// This should all happen synchronously (see above).
while (_scheduled.Count > 0 || _deferredTasks.Count > 0) {
while (HasProcsQueued) {
while (_scheduled.TryDequeue(out _current)) {
_current.SafeResume();
}
Expand Down
Loading