using System;
using System.Threading;
using Microsoft.Extensions.ObjectPool;
using Robust.Shared.Configuration;
using Robust.Shared.IoC;
using Robust.Shared.Log;
namespace Robust.Shared.Threading;
[NotContentImplementable]
public interface IParallelManager
{
event Action ParallelCountChanged;
int ParallelProcessCount { get; }
///
/// Add the delegate to and immediately invoke it.
///
void AddAndInvokeParallelCountChanged(Action changed);
///
/// Takes in a job that gets flushed.
///
///
WaitHandle Process(IRobustJob job);
public void ProcessNow(IRobustJob job);
///
/// Takes in a parallel job and runs it the specified amount.
///
void ProcessNow(IParallelRobustJob jobs, int amount);
///
/// Processes a robust job sequentially if desired.
///
void ProcessSerialNow(IParallelRobustJob jobs, int amount);
///
/// Takes in a parallel job and runs it without blocking.
///
WaitHandle Process(IParallelRobustJob jobs, int amount);
///
/// Takes in a bulk parallel job and runs it the specified amount.
///
/// The bulk parallel job to process.
/// The total number of elements to process.
void ProcessNow(IParallelBulkRobustJob jobs, int amount);
///
/// Processes a bulk robust job sequentially if desired.
///
/// The bulk parallel job to process.
/// The total number of elements to process.
void ProcessSerialNow(IParallelBulkRobustJob jobs, int amount);
///
/// Takes in a bulk parallel job and runs it without blocking.
///
/// The bulk parallel job to process.
/// The total number of elements to process.
/// A wait handle that signals when the job is complete.
WaitHandle Process(IParallelBulkRobustJob jobs, int amount);
///
/// Takes in a range parallel job and runs it without blocking.
///
/// The bulk parallel job to process.
/// The total number of elements to process.
/// A wait handle that signals when the job is complete.
WaitHandle Process(IParallelRangeRobustJob jobs, int amount);
}
internal interface IParallelManagerInternal : IParallelManager
{
void Initialize();
}
internal sealed partial class ParallelManager : IParallelManagerInternal
{
[Dependency] private IConfigurationManager _cfg = default!;
[Dependency] private ILogManager _logs = default!;
public event Action? ParallelCountChanged;
public int ParallelProcessCount { get; private set; }
public static readonly ManualResetEventSlim DummyResetEvent = new(true);
private ISawmill _sawmill = default!;
// Without pooling it's hard to keep task allocations down for classes
// This lets us avoid re-allocating the ManualResetEventSlims constantly when we just need a way to signal job completion
// and Parallel.For is really not built for running parallel tasks every tick.
private readonly ObjectPool _jobPool =
new DefaultObjectPool(new DefaultPooledObjectPolicy(), 1024);
private readonly ObjectPool _parallelPool =
new DefaultObjectPool(new DefaultPooledObjectPolicy(), 1024);
///
/// Used internally for Parallel jobs, for external callers it gets garbage collected.
///
private readonly ObjectPool _trackerPool =
new DefaultObjectPool(new DefaultPooledObjectPolicy(), 1024);
public void Initialize()
{
_sawmill = _logs.GetSawmill("parallel");
_cfg.OnValueChanged(CVars.ThreadParallelCount, UpdateCVar, true);
}
public void AddAndInvokeParallelCountChanged(Action changed)
{
ParallelCountChanged += changed;
changed();
}
private InternalJob GetJob(IRobustJob job)
{
var robustJob = _jobPool.Get();
robustJob.Event.Reset();
robustJob.Set(_sawmill, job, _jobPool);
return robustJob;
}
private InternalParallelRangeJob GetParallelJob(
IParallelRangeRobustJob job,
int start,
int end,
ParallelTracker tracker)
{
var internalJob = _parallelPool.Get();
internalJob.Set(_sawmill, job, start, end, tracker, _parallelPool);
return internalJob;
}
private void UpdateCVar(int value)
{
var oldCount = ParallelProcessCount;
ThreadPool.GetAvailableThreads(out var oldWorker, out var oldCompletion);
ParallelProcessCount = value == 0 ? oldWorker : value;
if (oldCount != ParallelProcessCount)
{
ParallelCountChanged?.Invoke();
ThreadPool.SetMaxThreads(ParallelProcessCount, oldCompletion);
}
}
///
public WaitHandle Process(IRobustJob job)
{
var subJob = GetJob(job);
// From what I can tell preferLocal is more of a !forceGlobal flag.
// Also UnsafeQueue should be fine as long as we don't use async locals.
ThreadPool.UnsafeQueueUserWorkItem(subJob, true);
return subJob.Event.WaitHandle;
}
public void ProcessNow(IRobustJob job)
{
job.Execute();
}
public void ProcessNow(IParallelRobustJob jobs, int amount) =>
ProcessNow((IParallelRangeRobustJob) jobs, amount);
public void ProcessNow(IParallelBulkRobustJob jobs, int amount) =>
ProcessNow((IParallelRangeRobustJob) jobs, amount);
public void ProcessSerialNow(IParallelRobustJob jobs, int amount) =>
ProcessSerialNow((IParallelRangeRobustJob) jobs, amount);
public void ProcessSerialNow(IParallelBulkRobustJob jobs, int amount) =>
ProcessSerialNow((IParallelRangeRobustJob) jobs, amount);
public WaitHandle Process(IParallelRobustJob jobs, int amount) =>
Process((IParallelRangeRobustJob) jobs, amount);
public WaitHandle Process(IParallelBulkRobustJob jobs, int amount) =>
Process((IParallelRangeRobustJob) jobs, amount);
public void ProcessNow(IParallelRangeRobustJob job, int amount)
{
var batches = amount / (float) job.BatchSize;
// Below the threshold so just do it now.
if (batches <= job.MinimumBatchParallel)
{
ProcessSerialNow(job, amount);
return;
}
var tracker = InternalProcess(job, amount);
tracker.Event.WaitHandle.WaitOne();
_trackerPool.Return(tracker);
}
public void ProcessSerialNow(IParallelRangeRobustJob jobs, int amount)
{
if (amount <= 0)
return;
jobs.ExecuteRange(0, amount);
}
public WaitHandle Process(IParallelRangeRobustJob job, int amount)
{
var tracker = InternalProcess(job, amount);
return tracker.Event.WaitHandle;
}
///
/// Runs a parallel job internally. Used so we can pool the tracker task for ProcessParallelNow
/// and not rely on external callers to return it where they don't want to wait.
///
private ParallelTracker InternalProcess(IParallelRangeRobustJob job, int amount)
{
var batches = (int) MathF.Ceiling(amount / (float) job.BatchSize);
var batchSize = job.BatchSize;
var tracker = _trackerPool.Get();
// Need to set this up front to avoid firing too early.
tracker.Event.Reset();
if (amount <= 0)
{
tracker.Event.Set();
return tracker;
}
tracker.PendingTasks = batches;
for (var i = 0; i < batches; i++)
{
var start = i * batchSize;
var end = Math.Min(start + batchSize, amount);
var subJob = GetParallelJob(job, start, end, tracker);
// From what I can tell preferLocal is more of a !forceGlobal flag.
// Also UnsafeQueue should be fine as long as we don't use async locals.
ThreadPool.UnsafeQueueUserWorkItem(subJob, true);
}
return tracker;
}
#region Jobs
///
/// Runs an and handles cleanup.
///
private sealed class InternalJob : IRobustJob, IThreadPoolWorkItem
{
private ISawmill _sawmill = default!;
private IRobustJob _robust = default!;
public readonly ManualResetEventSlim Event = new();
private ObjectPool _parentPool = default!;
public void Set(ISawmill sawmill, IRobustJob job, ObjectPool parentPool)
{
_sawmill = sawmill;
_robust = job;
_parentPool = parentPool;
}
public void Execute()
{
try
{
_robust.Execute();
}
catch (Exception exc)
{
_sawmill.Error($"Exception in ParallelManager: {exc}");
}
finally
{
Event.Set();
_parentPool.Return(this);
}
}
}
///
/// Runs a for a specified range and handles cleanup.
/// This is so jobs that process per-element ()
/// and jobs that process in bulk () can both use it.
///
private sealed class InternalParallelRangeJob : IRobustJob, IThreadPoolWorkItem
{
private IParallelRangeRobustJob _robust = default!;
private int _start;
private int _end;
private ISawmill _sawmill = default!;
private ParallelTracker _tracker = default!;
private ObjectPool _parentPool = default!;
public void Set(
ISawmill sawmill,
IParallelRangeRobustJob robust,
int start,
int end,
ParallelTracker tracker,
ObjectPool parentPool)
{
_sawmill = sawmill;
_robust = robust;
_start = start;
_end = end;
_tracker = tracker;
_parentPool = parentPool;
}
public void Execute()
{
try
{
_robust.ExecuteRange(_start, _end);
}
catch (Exception exc)
{
_sawmill.Error($"Exception in ParallelManager: {exc}");
}
finally
{
// Task is done, so tell the tracker that it has one less task to process.
// And of course return the job to the pool.
_tracker.Set();
_parentPool.Return(this);
}
}
}
///
/// Tracks jobs internally. This is because WaitHandle has a max limit of 64 tasks.
/// So we'll just decrement PendingTasks in lieu.
///
private sealed class ParallelTracker
{
public readonly ManualResetEventSlim Event = new();
public int PendingTasks;
///
/// Marks the tracker as having 1 less pending task.
///
public void Set()
{
// We should atomically get new value of PendingTasks
// as the result of Decrement call and use it to prevent data race.
if (Interlocked.Decrement(ref PendingTasks) <= 0)
Event.Set();
}
}
#endregion
}