#nullable enable
using System;
using System.Threading;
using OpenNest.Engine.Jobs;
namespace OpenNest.Engine.NestingEngines;
///
/// Runs another engine under a wall-clock limit. When the engine finishes in time its result
/// is returned unchanged. When the limit cancels it, the best whole-job plan it reported
/// () is returned instead, provided that plan passes
/// ; plans are ranked like ranks
/// results, and a tie goes to the later plan, as engines report only plans they rank higher.
/// With no valid plan by the limit, the solve fails with a .
/// Cancellation by the caller's own token is never turned into a result. A result the engine
/// returns, even after the limit, is its own and is returned unchanged. The engine sees the
/// job with set, so an engine that supports
/// it keeps improving its plan until the limit.
///
///
/// The engine's plate commits are not forwarded: the returned result's commits are reported
/// once it is known. Plans are forwarded only when they beat every plan shown so far. A
/// limited solve depends on the clock, so it is not reproducible like an unlimited one, and
/// the engine may overrun the limit by however long it takes to observe cancellation. Plans
/// are expected from the solve thread, as the built-in engines report them. Cancellation
/// callbacks run on a dedicated clock thread that the solve joins before returning, so a
/// callback must not wait on the solve thread.
///
public sealed class DeadlineNestingEngine : INestingEngine
{
/// The longest limit accepted: about 49.7 days, kept from the original timer bound.
///
public static readonly TimeSpan MaxLimit = TimeSpan.FromMilliseconds(uint.MaxValue - 1.0);
private readonly INestingEngine inner;
public DeadlineNestingEngine(INestingEngine inner, TimeSpan limit)
{
ArgumentNullException.ThrowIfNull(inner);
if (limit <= TimeSpan.Zero || limit > MaxLimit)
throw new ArgumentOutOfRangeException(
nameof(limit),
$"The time limit must be positive and at most {MaxLimit}."
);
this.inner = inner;
Limit = limit;
}
public TimeSpan Limit { get; }
public NestJobResult Solve(
NestJob job,
IProgress? progress = null,
CancellationToken token = default
)
{
ArgumentNullException.ThrowIfNull(job);
token.ThrowIfCancellationRequested();
// Engines that can keep searching until cancelled may use all the time.
job = new NestJob(job.Parts, job.Plates, job.Options.WithSearchUntilCancelled(true));
using var deadline = CancellationTokenSource.CreateLinkedTokenSource(token);
using var finished = new ManualResetEventSlim();
var clock = StartClock(deadline, finished);
var plans = new BestPlan(job, progress);
NestJobResult result;
try
{
result = inner.Solve(job, plans, deadline.Token);
}
catch (OperationCanceledException)
when (!token.IsCancellationRequested && deadline.IsCancellationRequested)
{
if (plans.Best is not { Valid: true } best)
throw new TimeoutException(
$"No valid layout was found within the {Limit.TotalSeconds:0.###} s time limit."
);
result = best.Result;
}
finally
{
finished.Set();
clock.Join();
}
token.ThrowIfCancellationRequested();
if (result == null)
throw new InvalidOperationException("The engine returned no result.");
CandidateProgress.ReportCommits(result, progress);
return result;
}
///
/// Cancels at the limit from a dedicated thread. A thread-pool
/// timer () can fire many
/// seconds late, or not before the solve ends, when the engine's parallel loops hold every
/// pool thread, as they do on one CPU.
///
private Thread StartClock(CancellationTokenSource deadline, ManualResetEventSlim finished)
{
var thread = new Thread(() =>
{
var due = System.Diagnostics.Stopwatch.StartNew();
while (true)
{
var left = Limit - due.Elapsed;
if (left <= TimeSpan.Zero)
break;
var wait = left.TotalMilliseconds >= int.MaxValue
? TimeSpan.FromMilliseconds(int.MaxValue - 1.0)
: left;
if (finished.Wait(wait))
return;
}
deadline.Cancel();
})
{
IsBackground = true,
Name = "OpenNest time limit",
};
thread.Start();
return thread;
}
/// Keeps the best reported plan and forwards everything but plate commits.
private sealed class BestPlan(NestJob job, IProgress? inner)
: IProgress
{
public CandidateScore? Best { get; private set; }
public void Report(NestJobProgress value)
{
if (value == null || value.Stage == NestJobStage.PlateCommitted)
return;
if (value.Stage != NestJobStage.PlanImproved)
{
inner?.Report(value);
return;
}
if (value.Plan == null)
return;
CandidateScore plan;
try
{
plan = CandidateScore.Of(job, value.Plan, 0);
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
// A plan that cannot be scored is never kept or shown.
return;
}
if (Best != null && Best.IsBetterThan(plan))
return;
Best = plan;
inner?.Report(value);
}
}
}