diff --git a/Nerfed.Editor/Program.cs b/Nerfed.Editor/Program.cs index dcf929b..17570a6 100644 --- a/Nerfed.Editor/Program.cs +++ b/Nerfed.Editor/Program.cs @@ -2,7 +2,9 @@ using Nerfed.Editor.Systems; using Nerfed.Runtime; using Nerfed.Runtime.Components; +using Nerfed.Runtime.Scheduling; using Nerfed.Runtime.Systems; +using Nerfed.Runtime.Systems.Synthetic; using Nerfed.Runtime.Util; using System.Numerics; @@ -12,10 +14,22 @@ internal class Program { private static readonly World world = new World(); private static List systems = new List(); + private static SystemScheduler scheduler; public static List editorSystems = new List(); + private static bool enableDummySchedulerHarness; + private static bool enableParallelSystemExecution; private static void Main(string[] args) { + enableDummySchedulerHarness = args.Contains("--dummy-scheduler-test", StringComparer.OrdinalIgnoreCase); + enableParallelSystemExecution = args.Contains("--parallel-systems", StringComparer.OrdinalIgnoreCase); + + scheduler = new SystemScheduler(new SystemSchedulerOptions + { + EnableParallelSystemExecution = enableParallelSystemExecution, + StrictDependencyValidation = true, + }); + Engine.OnInitialize += HandleOnInitialize; Engine.OnUpdate += HandleOnUpdate; Engine.OnRender += HandleOnRender; @@ -27,7 +41,42 @@ internal class Program private static void HandleOnInitialize() { //systems.Add(new ParentSystem(world)); - systems.Add(new LocalToWorldSystem(world)); + LocalToWorldSystem localToWorldSystem = new LocalToWorldSystem(world); + systems.Add(localToWorldSystem); + scheduler.Register(localToWorldSystem, SystemSchedulePhase.Simulation); + + if (enableDummySchedulerHarness) + { + DummyLongJobSharedState sharedState = new DummyLongJobSharedState(); + + // Producer starts long work, middle system burns time on main thread, consumer acts as a barrier. + DummyLongJobProducerSystem producer = new DummyLongJobProducerSystem( + world, + sharedState, + JobSystem.Default, + TimeSpan.FromMilliseconds(6), + DummyWorkloadMode.Compute, + JobDeadlineMode.FrameCritical); + systems.Add(producer); + scheduler.Register(producer, SystemSchedulePhase.Simulation); + + DummyMainThreadWorkSystem mainThreadWork = new DummyMainThreadWorkSystem( + world, + TimeSpan.FromMilliseconds(2), + DummyWorkloadMode.Compute, + nameof(DummyMainThreadWorkSystem)); + systems.Add(mainThreadWork); + scheduler.Register(mainThreadWork, SystemSchedulePhase.Simulation); + + DummyLongJobConsumerSystem consumer = new DummyLongJobConsumerSystem( + world, + sharedState, + JobSystem.Default, + requireCompletion: true); + systems.Add(consumer); + scheduler.Register(consumer, SystemSchedulePhase.LateSimulation, nameof(DummyLongJobProducerSystem)); + } + editorSystems.Add(new EditorProfilerWindow(world)); editorSystems.Add(new EditorHierarchyWindow(world)); #if DEBUG @@ -72,10 +121,9 @@ internal class Program private static void HandleOnUpdate() { - foreach (MoonTools.ECS.System system in systems) + using (new ProfilerScope("SystemScheduler.Execute")) { - using ProfilerScope scope = new(system.GetType().Name); - system.Update(Engine.Timestep); + scheduler.Execute(Engine.Timestep); } using (new ProfilerScope("EditorGui.Update")) diff --git a/Nerfed.Runtime/JobSystem.cs b/Nerfed.Runtime/JobSystem.cs index 5ca2ac4..22bbfd7 100644 --- a/Nerfed.Runtime/JobSystem.cs +++ b/Nerfed.Runtime/JobSystem.cs @@ -1,7 +1,67 @@ +using System.Collections.Concurrent; using System.Diagnostics; +using System.Runtime.ExceptionServices; namespace Nerfed.Runtime; +public enum JobDeadlineMode +{ + FrameCritical, + Deferred, +} + +public enum JobExecutionStatus +{ + Invalid, + Queued, + Running, + Completed, + Faulted, + Canceled, +} + +public readonly struct JobHandle : IEquatable +{ + internal readonly int id; + + internal JobHandle(int id) + { + this.id = id; + } + + public bool IsValid => id > 0; + + public bool Equals(JobHandle other) + { + return id == other.id; + } + + public override bool Equals(object obj) + { + return obj is JobHandle other && Equals(other); + } + + public override int GetHashCode() + { + return id; + } + + public static bool operator ==(JobHandle left, JobHandle right) + { + return left.Equals(right); + } + + public static bool operator !=(JobHandle left, JobHandle right) + { + return !left.Equals(right); + } + + public override string ToString() + { + return IsValid ? $"JobHandle({id})" : "JobHandle(Invalid)"; + } +} + public sealed class JobSystem : IDisposable { public static readonly JobSystem Default = new JobSystem(); @@ -9,6 +69,13 @@ public sealed class JobSystem : IDisposable private Thread[] workers; private SemaphoreSlim startSignal; private CountdownEvent completionEvent; + + private Thread[] asyncWorkers; + private SemaphoreSlim asyncSignal; + private ConcurrentQueue asyncQueue; + private ConcurrentDictionary asyncJobStates; + private int nextAsyncJobId; + private volatile bool running; // Shared per-dispatch state written by main thread before workers wake. @@ -16,10 +83,41 @@ public sealed class JobSystem : IDisposable private int workCount; private int nextIndex; // grabbed with Interlocked.Increment for work-stealing + private sealed class AsyncJobState : IDisposable + { + public readonly Action Action; + public readonly JobDeadlineMode Deadline; + public readonly string Name; + public readonly ManualResetEventSlim Completion = new(false); + + public ExceptionDispatchInfo CapturedException; + public volatile JobExecutionStatus Status; + public volatile bool CancellationRequested; + + public AsyncJobState(Action action, JobDeadlineMode deadline, string name) + { + Action = action; + Deadline = deadline; + Name = name; + Status = JobExecutionStatus.Queued; + } + + public bool IsTerminal => + Status == JobExecutionStatus.Completed || + Status == JobExecutionStatus.Faulted || + Status == JobExecutionStatus.Canceled; + + public void Dispose() + { + Completion.Dispose(); + } + } + public int WorkerCount => workers?.Length ?? 0; + public int AsyncWorkerCount => asyncWorkers?.Length ?? 0; public bool IsInitialized => workers != null; - public void Initialize(int threadCount = -1) + public void Initialize(int threadCount = -1, int asyncThreadCount = -1) { if (IsInitialized) { @@ -33,7 +131,16 @@ public sealed class JobSystem : IDisposable running = true; startSignal = new SemaphoreSlim(0); completionEvent = new CountdownEvent(1); + asyncSignal = new SemaphoreSlim(0); workers = new Thread[threadCount]; + asyncQueue = new ConcurrentQueue(); + asyncJobStates = new ConcurrentDictionary(); + + asyncThreadCount = asyncThreadCount < 0 + ? Math.Max(1, Environment.ProcessorCount >= 8 ? 2 : 1) + : Math.Max(0, asyncThreadCount); + + asyncWorkers = new Thread[asyncThreadCount]; for (int i = 0; i < threadCount; i++) { @@ -44,6 +151,98 @@ public sealed class JobSystem : IDisposable }; workers[i].Start(); } + + for (int i = 0; i < asyncThreadCount; i++) + { + asyncWorkers[i] = new Thread(AsyncWorkerLoop) + { + IsBackground = true, + Name = $"AsyncJob-{i}", + }; + asyncWorkers[i].Start(); + } + } + + public JobHandle Submit(Action action, JobDeadlineMode deadline = JobDeadlineMode.Deferred, string name = "") + { + ArgumentNullException.ThrowIfNull(action); + + if (!running) + { + throw new InvalidOperationException("JobSystem is not running. Call Initialize before submitting jobs."); + } + + int id = Interlocked.Increment(ref nextAsyncJobId); + var state = new AsyncJobState(action, deadline, name ?? string.Empty); + + if (!asyncJobStates.TryAdd(id, state)) + { + state.Dispose(); + throw new InvalidOperationException($"Failed to register async job {id}."); + } + + asyncQueue.Enqueue(id); + asyncSignal.Release(); + return new JobHandle(id); + } + + public JobExecutionStatus GetStatus(JobHandle handle) + { + if (!handle.IsValid) + { + return JobExecutionStatus.Invalid; + } + + return asyncJobStates.TryGetValue(handle.id, out AsyncJobState state) + ? state.Status + : JobExecutionStatus.Invalid; + } + + public bool IsCompleted(JobHandle handle) + { + return GetStatus(handle) is JobExecutionStatus.Completed or JobExecutionStatus.Faulted or JobExecutionStatus.Canceled; + } + + public bool Cancel(JobHandle handle) + { + if (!handle.IsValid || !asyncJobStates.TryGetValue(handle.id, out AsyncJobState state)) + { + return false; + } + + state.CancellationRequested = true; + return true; + } + + public void Wait(JobHandle handle) + { + if (!handle.IsValid || !asyncJobStates.TryGetValue(handle.id, out AsyncJobState state)) + { + throw new InvalidOperationException($"Unknown job handle {handle}."); + } + + state.Completion.Wait(); + + if (state.Status == JobExecutionStatus.Faulted) + { + state.CapturedException?.Throw(); + } + } + + public bool TryForget(JobHandle handle) + { + if (!handle.IsValid || !asyncJobStates.TryGetValue(handle.id, out AsyncJobState state) || !state.IsTerminal) + { + return false; + } + + if (asyncJobStates.TryRemove(handle.id, out AsyncJobState removedState)) + { + removedState.Dispose(); + return true; + } + + return false; } public void Dispatch(int count, Action action) @@ -79,8 +278,30 @@ public sealed class JobSystem : IDisposable foreach (Thread t in workers) t.Join(); + for (int i = 0; i < asyncWorkers.Length; i++) + { + asyncSignal.Release(); + } + + foreach (Thread t in asyncWorkers) + { + t.Join(); + } + completionEvent.Dispose(); startSignal.Dispose(); + asyncSignal.Dispose(); + + foreach (KeyValuePair pair in asyncJobStates) + { + pair.Value.Dispose(); + } + + asyncJobStates.Clear(); + + asyncWorkers = null; + asyncQueue = null; + asyncJobStates = null; workers = null; } @@ -106,4 +327,52 @@ public sealed class JobSystem : IDisposable completionEvent.Signal(); } } + + private void AsyncWorkerLoop() + { + while (true) + { + asyncSignal.Wait(); + + if (!running) + { + return; + } + + if (!asyncQueue.TryDequeue(out int jobId)) + { + continue; + } + + if (!asyncJobStates.TryGetValue(jobId, out AsyncJobState state)) + { + continue; + } + + if (state.CancellationRequested) + { + state.Status = JobExecutionStatus.Canceled; + state.Completion.Set(); + continue; + } + + state.Status = JobExecutionStatus.Running; + try + { + state.Action(); + state.Status = state.CancellationRequested + ? JobExecutionStatus.Canceled + : JobExecutionStatus.Completed; + } + catch (Exception ex) + { + state.CapturedException = ExceptionDispatchInfo.Capture(ex); + state.Status = JobExecutionStatus.Faulted; + } + finally + { + state.Completion.Set(); + } + } + } } diff --git a/Nerfed.Runtime/Scheduling/IParallelSystemMetadata.cs b/Nerfed.Runtime/Scheduling/IParallelSystemMetadata.cs new file mode 100644 index 0000000..d11fc64 --- /dev/null +++ b/Nerfed.Runtime/Scheduling/IParallelSystemMetadata.cs @@ -0,0 +1,7 @@ +namespace Nerfed.Runtime.Scheduling; + +public interface IParallelSystemMetadata +{ + string ScheduleName { get; } + SystemAccessDeclaration AccessDeclaration { get; } +} diff --git a/Nerfed.Runtime/Scheduling/SystemAccessDeclaration.cs b/Nerfed.Runtime/Scheduling/SystemAccessDeclaration.cs new file mode 100644 index 0000000..480a859 --- /dev/null +++ b/Nerfed.Runtime/Scheduling/SystemAccessDeclaration.cs @@ -0,0 +1,69 @@ +namespace Nerfed.Runtime.Scheduling; + +public enum SystemAccessMode +{ + Read, + WriteExisting, + StructuralWrite, +} + +public readonly struct ComponentAccess +{ + public ComponentAccess(Type componentType, SystemAccessMode mode) + { + ComponentType = componentType; + Mode = mode; + } + + public Type ComponentType { get; } + public SystemAccessMode Mode { get; } +} + +public readonly struct SystemAccessDeclaration +{ + private readonly ComponentAccess[] accesses; + + public static readonly SystemAccessDeclaration Empty = new(Array.Empty()); + + public SystemAccessDeclaration(params ComponentAccess[] accesses) + { + this.accesses = accesses ?? Array.Empty(); + } + + public ReadOnlySpan Accesses => accesses ?? Array.Empty(); + + public bool ConflictsWith(in SystemAccessDeclaration other) + { + ReadOnlySpan left = Accesses; + ReadOnlySpan right = other.Accesses; + + for (int i = 0; i < left.Length; i++) + { + for (int j = 0; j < right.Length; j++) + { + if (left[i].Mode == SystemAccessMode.StructuralWrite || right[j].Mode == SystemAccessMode.StructuralWrite) + { + return true; + } + + if (left[i].ComponentType != right[j].ComponentType) + { + continue; + } + + if (left[i].Mode == SystemAccessMode.Read && right[j].Mode == SystemAccessMode.Read) + { + continue; + } + + return true; + } + } + + return false; + } + + public static ComponentAccess Read() where T : unmanaged => new(typeof(T), SystemAccessMode.Read); + public static ComponentAccess WriteExisting() where T : unmanaged => new(typeof(T), SystemAccessMode.WriteExisting); + public static ComponentAccess StructuralWrite() where T : unmanaged => new(typeof(T), SystemAccessMode.StructuralWrite); +} diff --git a/Nerfed.Runtime/Scheduling/SystemScheduleEntry.cs b/Nerfed.Runtime/Scheduling/SystemScheduleEntry.cs new file mode 100644 index 0000000..2ff840b --- /dev/null +++ b/Nerfed.Runtime/Scheduling/SystemScheduleEntry.cs @@ -0,0 +1,37 @@ +using MoonTools.ECS; + +namespace Nerfed.Runtime.Scheduling; + +public sealed class SystemScheduleEntry +{ + public SystemScheduleEntry( + MoonTools.ECS.System system, + SystemSchedulePhase phase, + string[] dependsOn, + int registrationIndex) + { + System = system; + Phase = phase; + DependsOn = dependsOn ?? Array.Empty(); + RegistrationIndex = registrationIndex; + + if (system is IParallelSystemMetadata metadata) + { + Name = metadata.ScheduleName; + Access = metadata.AccessDeclaration; + } + else + { + Name = system.GetType().Name; + // Unknown systems are treated as structural to preserve safety until metadata is declared. + Access = new SystemAccessDeclaration(new ComponentAccess(typeof(object), SystemAccessMode.StructuralWrite)); + } + } + + public MoonTools.ECS.System System { get; } + public string Name { get; } + public SystemSchedulePhase Phase { get; } + public string[] DependsOn { get; } + public int RegistrationIndex { get; } + public SystemAccessDeclaration Access { get; } +} diff --git a/Nerfed.Runtime/Scheduling/SystemSchedulePhase.cs b/Nerfed.Runtime/Scheduling/SystemSchedulePhase.cs new file mode 100644 index 0000000..54193c5 --- /dev/null +++ b/Nerfed.Runtime/Scheduling/SystemSchedulePhase.cs @@ -0,0 +1,9 @@ +namespace Nerfed.Runtime.Scheduling; + +public enum SystemSchedulePhase +{ + PreUpdate = 0, + Simulation = 1, + LateSimulation = 2, + PreRender = 3, +} diff --git a/Nerfed.Runtime/Scheduling/SystemScheduler.cs b/Nerfed.Runtime/Scheduling/SystemScheduler.cs new file mode 100644 index 0000000..590aae3 --- /dev/null +++ b/Nerfed.Runtime/Scheduling/SystemScheduler.cs @@ -0,0 +1,188 @@ +using MoonTools.ECS; + +namespace Nerfed.Runtime.Scheduling; + +public sealed class SystemScheduler +{ + private readonly List entries = new List(); + private readonly SystemSchedulerOptions options; + + public SystemScheduler(SystemSchedulerOptions options) + { + this.options = options ?? new SystemSchedulerOptions(); + } + + public IReadOnlyList Entries => entries; + + public void Register(MoonTools.ECS.System system, SystemSchedulePhase phase, params string[] dependsOn) + { + ArgumentNullException.ThrowIfNull(system); + string[] dependencies = dependsOn?.Where(static value => !string.IsNullOrWhiteSpace(value)).ToArray() ?? Array.Empty(); + entries.Add(new SystemScheduleEntry(system, phase, dependencies, entries.Count)); + } + + public void Execute(TimeSpan delta) + { + if (entries.Count == 0) + { + return; + } + + Dictionary byName = BuildNameIndex(entries); + ValidateDependencies(byName, entries); + + foreach (SystemSchedulePhase phase in Enum.GetValues()) + { + ExecutePhase(phase, delta, byName); + } + } + + private static Dictionary BuildNameIndex(List allEntries) + { + var byName = new Dictionary(StringComparer.Ordinal); + foreach (SystemScheduleEntry entry in allEntries) + { + if (byName.ContainsKey(entry.Name)) + { + throw new InvalidOperationException($"Duplicate scheduled system name '{entry.Name}'."); + } + + byName.Add(entry.Name, entry); + } + + return byName; + } + + private void ValidateDependencies(Dictionary byName, List allEntries) + { + foreach (SystemScheduleEntry entry in allEntries) + { + foreach (string dependency in entry.DependsOn) + { + if (!byName.TryGetValue(dependency, out SystemScheduleEntry dependencyEntry)) + { + if (options.StrictDependencyValidation) + { + throw new InvalidOperationException($"Scheduled system '{entry.Name}' depends on missing system '{dependency}'."); + } + + continue; + } + + if (dependencyEntry.Phase > entry.Phase) + { + throw new InvalidOperationException( + $"Scheduled system '{entry.Name}' in phase {entry.Phase} cannot depend on later phase system '{dependency}' in phase {dependencyEntry.Phase}."); + } + } + } + } + + private void ExecutePhase(SystemSchedulePhase phase, TimeSpan delta, Dictionary byName) + { + List phaseEntries = entries + .Where(entry => entry.Phase == phase) + .OrderBy(entry => entry.RegistrationIndex) + .ToList(); + + if (phaseEntries.Count == 0) + { + return; + } + + var completed = new HashSet(StringComparer.Ordinal); + + while (completed.Count < phaseEntries.Count) + { + List ready = phaseEntries + .Where(entry => !completed.Contains(entry.Name) && DependenciesSatisfied(entry, completed, byName)) + .OrderBy(entry => entry.RegistrationIndex) + .ToList(); + + if (ready.Count == 0) + { + throw new InvalidOperationException($"Cyclic or unsatisfied dependencies detected in phase {phase}."); + } + + List batch = BuildNonConflictingBatch(ready); + ExecuteBatch(batch, delta, phase); + + foreach (SystemScheduleEntry entry in batch) + { + completed.Add(entry.Name); + } + } + } + + private static bool DependenciesSatisfied(SystemScheduleEntry entry, HashSet completed, Dictionary byName) + { + foreach (string dependency in entry.DependsOn) + { + if (!byName.TryGetValue(dependency, out SystemScheduleEntry dependencyEntry)) + { + continue; + } + + if (dependencyEntry.Phase == entry.Phase && !completed.Contains(dependency)) + { + return false; + } + } + + return true; + } + + private static List BuildNonConflictingBatch(List ready) + { + var batch = new List(ready.Count); + + for (int i = 0; i < ready.Count; i++) + { + SystemScheduleEntry candidate = ready[i]; + bool conflicts = false; + + for (int j = 0; j < batch.Count; j++) + { + if (candidate.Access.ConflictsWith(batch[j].Access)) + { + conflicts = true; + break; + } + } + + if (!conflicts) + { + batch.Add(candidate); + } + } + + if (batch.Count == 0) + { + batch.Add(ready[0]); + } + + return batch; + } + + private void ExecuteBatch(List batch, TimeSpan delta, SystemSchedulePhase phase) + { + using ProfilerScope phaseScope = new($"Schedule.{phase}.Batch[{batch.Count}]"); + + if (options.EnableParallelSystemExecution && batch.Count > 1) + { + Parallel.ForEach(batch, entry => ExecuteSystem(entry, delta)); + return; + } + + for (int i = 0; i < batch.Count; i++) + { + ExecuteSystem(batch[i], delta); + } + } + + private static void ExecuteSystem(SystemScheduleEntry entry, TimeSpan delta) + { + using ProfilerScope scope = new(entry.Name); + entry.System.Update(delta); + } +} diff --git a/Nerfed.Runtime/Scheduling/SystemSchedulerOptions.cs b/Nerfed.Runtime/Scheduling/SystemSchedulerOptions.cs new file mode 100644 index 0000000..ff78ca4 --- /dev/null +++ b/Nerfed.Runtime/Scheduling/SystemSchedulerOptions.cs @@ -0,0 +1,7 @@ +namespace Nerfed.Runtime.Scheduling; + +public sealed class SystemSchedulerOptions +{ + public bool EnableParallelSystemExecution { get; set; } + public bool StrictDependencyValidation { get; set; } = true; +} diff --git a/Nerfed.Runtime/Systems/Synthetic/DummyLongJobConsumerSystem.cs b/Nerfed.Runtime/Systems/Synthetic/DummyLongJobConsumerSystem.cs new file mode 100644 index 0000000..165ddc1 --- /dev/null +++ b/Nerfed.Runtime/Systems/Synthetic/DummyLongJobConsumerSystem.cs @@ -0,0 +1,47 @@ +using MoonTools.ECS; +using Nerfed.Runtime.Scheduling; + +namespace Nerfed.Runtime.Systems.Synthetic; + +public sealed class DummyLongJobConsumerSystem : MoonTools.ECS.System, IParallelSystemMetadata +{ + private readonly JobSystem jobs; + private readonly DummyLongJobSharedState state; + private readonly bool requireCompletion; + + public DummyLongJobConsumerSystem( + World world, + DummyLongJobSharedState state, + JobSystem jobs, + bool requireCompletion) : base(world) + { + this.state = state; + this.jobs = jobs; + this.requireCompletion = requireCompletion; + } + + public string ScheduleName => nameof(DummyLongJobConsumerSystem); + + // Consumer/apply stage is where structural world work should happen in real systems. + public SystemAccessDeclaration AccessDeclaration => SystemAccessDeclaration.Empty; + + public override void Update(TimeSpan delta) + { + JobHandle handle = state.InFlight; + if (!handle.IsValid) + { + return; + } + + if (!requireCompletion && !jobs.IsCompleted(handle)) + { + return; + } + + jobs.Wait(handle); + jobs.TryForget(handle); + + state.InFlight = default; + state.ConsumeCount++; + } +} diff --git a/Nerfed.Runtime/Systems/Synthetic/DummyLongJobProducerSystem.cs b/Nerfed.Runtime/Systems/Synthetic/DummyLongJobProducerSystem.cs new file mode 100644 index 0000000..3145f50 --- /dev/null +++ b/Nerfed.Runtime/Systems/Synthetic/DummyLongJobProducerSystem.cs @@ -0,0 +1,53 @@ +using MoonTools.ECS; +using Nerfed.Runtime.Scheduling; + +namespace Nerfed.Runtime.Systems.Synthetic; + +public sealed class DummyLongJobProducerSystem : MoonTools.ECS.System, IParallelSystemMetadata +{ + private readonly JobSystem jobs; + private readonly DummyLongJobSharedState state; + private readonly TimeSpan duration; + private readonly DummyWorkloadMode mode; + private readonly JobDeadlineMode deadline; + private int seed; + + public DummyLongJobProducerSystem( + World world, + DummyLongJobSharedState state, + JobSystem jobs, + TimeSpan duration, + DummyWorkloadMode mode, + JobDeadlineMode deadline, + int initialSeed = 1) : base(world) + { + this.state = state; + this.jobs = jobs; + this.duration = duration; + this.mode = mode; + this.deadline = deadline; + seed = initialSeed; + } + + public string ScheduleName => nameof(DummyLongJobProducerSystem); + + // Producer pattern: reads current world state and emits immutable job payload. + public SystemAccessDeclaration AccessDeclaration => SystemAccessDeclaration.Empty; + + public override void Update(TimeSpan delta) + { + if (state.InFlight.IsValid) + { + return; + } + + int capturedSeed = seed; + state.InFlight = jobs.Submit( + () => state.LastResult = DummyWorkloadRunner.Run(duration, mode, capturedSeed), + deadline, + nameof(DummyLongJobProducerSystem)); + + seed = unchecked(capturedSeed + 1); + state.SubmitCount++; + } +} diff --git a/Nerfed.Runtime/Systems/Synthetic/DummyLongJobSharedState.cs b/Nerfed.Runtime/Systems/Synthetic/DummyLongJobSharedState.cs new file mode 100644 index 0000000..a40ca5b --- /dev/null +++ b/Nerfed.Runtime/Systems/Synthetic/DummyLongJobSharedState.cs @@ -0,0 +1,9 @@ +namespace Nerfed.Runtime.Systems.Synthetic; + +public sealed class DummyLongJobSharedState +{ + public JobHandle InFlight; + public int LastResult; + public int SubmitCount; + public int ConsumeCount; +} diff --git a/Nerfed.Runtime/Systems/Synthetic/DummyMainThreadWorkSystem.cs b/Nerfed.Runtime/Systems/Synthetic/DummyMainThreadWorkSystem.cs new file mode 100644 index 0000000..c9d3349 --- /dev/null +++ b/Nerfed.Runtime/Systems/Synthetic/DummyMainThreadWorkSystem.cs @@ -0,0 +1,34 @@ +using MoonTools.ECS; +using Nerfed.Runtime.Scheduling; + +namespace Nerfed.Runtime.Systems.Synthetic; + +public sealed class DummyMainThreadWorkSystem : MoonTools.ECS.System, IParallelSystemMetadata +{ + private readonly TimeSpan duration; + private readonly DummyWorkloadMode mode; + private readonly string scheduleName; + private int seed; + + public DummyMainThreadWorkSystem( + World world, + TimeSpan duration, + DummyWorkloadMode mode, + string scheduleName, + int initialSeed = 1) : base(world) + { + this.duration = duration; + this.mode = mode; + this.scheduleName = scheduleName; + seed = initialSeed; + } + + public string ScheduleName => scheduleName; + + public SystemAccessDeclaration AccessDeclaration => SystemAccessDeclaration.Empty; + + public override void Update(TimeSpan delta) + { + seed = DummyWorkloadRunner.Run(duration, mode, seed); + } +} diff --git a/Nerfed.Runtime/Systems/Synthetic/DummyWorkloadMode.cs b/Nerfed.Runtime/Systems/Synthetic/DummyWorkloadMode.cs new file mode 100644 index 0000000..7cd6974 --- /dev/null +++ b/Nerfed.Runtime/Systems/Synthetic/DummyWorkloadMode.cs @@ -0,0 +1,8 @@ +namespace Nerfed.Runtime.Systems.Synthetic; + +public enum DummyWorkloadMode +{ + Spin, + Sleep, + Compute, +} diff --git a/Nerfed.Runtime/Systems/Synthetic/DummyWorkloadRunner.cs b/Nerfed.Runtime/Systems/Synthetic/DummyWorkloadRunner.cs new file mode 100644 index 0000000..01ef67e --- /dev/null +++ b/Nerfed.Runtime/Systems/Synthetic/DummyWorkloadRunner.cs @@ -0,0 +1,54 @@ +using System.Diagnostics; + +namespace Nerfed.Runtime.Systems.Synthetic; + +internal static class DummyWorkloadRunner +{ + public static int Run(TimeSpan duration, DummyWorkloadMode mode, int seed) + { + return mode switch + { + DummyWorkloadMode.Sleep => Sleep(duration, seed), + DummyWorkloadMode.Compute => Compute(duration, seed), + _ => Spin(duration, seed), + }; + } + + private static int Sleep(TimeSpan duration, int seed) + { + if (duration > TimeSpan.Zero) + { + Thread.Sleep(duration); + } + + return seed; + } + + private static int Spin(TimeSpan duration, int seed) + { + Stopwatch stopwatch = Stopwatch.StartNew(); + int value = seed; + while (stopwatch.Elapsed < duration) + { + value = unchecked((value * 1664525) + 1013904223); + Thread.SpinWait(128); + } + + return value; + } + + private static int Compute(TimeSpan duration, int seed) + { + Stopwatch stopwatch = Stopwatch.StartNew(); + int value = seed; + while (stopwatch.Elapsed < duration) + { + for (int i = 0; i < 2048; i++) + { + value = unchecked((value << 5) - value + i); + } + } + + return value; + } +}