diff --git a/src/CromulentBisgetti.ContainerPacking/Algorithms/EB_AFIT.cs b/src/CromulentBisgetti.ContainerPacking/Algorithms/EB_AFIT.cs index c10292c..97b4df3 100644 --- a/src/CromulentBisgetti.ContainerPacking/Algorithms/EB_AFIT.cs +++ b/src/CromulentBisgetti.ContainerPacking/Algorithms/EB_AFIT.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; namespace CromulentBisgetti.ContainerPacking.Algorithms { @@ -20,9 +21,9 @@ public class EB_AFIT : IPackingAlgorithm /// The container to pack items into. /// The items to pack. /// The bin packing result. - public AlgorithmPackingResult Run(Container container, List items) + public AlgorithmPackingResult Run(Container container, List items, CancellationToken ct) { - Initialize(container, items); + Initialize(container, items, ct); ExecuteIterations(container); Report(container); @@ -59,6 +60,7 @@ public AlgorithmPackingResult Run(Container container, List items) private List itemsToPack; private List itemsPackedInOrder; private List layers; + private CancellationToken _ct; private ContainerPackingResult result; private ScrapPad scrapfirst; @@ -70,7 +72,6 @@ public AlgorithmPackingResult Run(Container container, List items) private bool layerDone; private bool packing; private bool packingBest = false; - private bool quit = false; private int bboxi; private int bestIteration; @@ -288,7 +289,7 @@ private void ExecuteIterations(Container container) int layersIndex; decimal bestVolume = 0.0M; - for (int containerOrientationVariant = 1; (containerOrientationVariant <= 6) && !quit; containerOrientationVariant++) + for (int containerOrientationVariant = 1; (containerOrientationVariant <= 6) && !_ct.IsCancellationRequested; containerOrientationVariant++) { switch (containerOrientationVariant) { @@ -321,7 +322,7 @@ private void ExecuteIterations(Container container) ListCanditLayers(); layers = layers.OrderBy(l => l.LayerEval).ToList(); - for (layersIndex = 1; (layersIndex <= layerListLen) && !quit; layersIndex++) + for (layersIndex = 1; (layersIndex <= layerListLen) && !_ct.IsCancellationRequested; layersIndex++) { packedVolume = 0.0M; packedy = 0; @@ -347,7 +348,7 @@ private void ExecuteIterations(Container container) packedy = packedy + layerThickness; remainpy = py - packedy; - if (layerinlayer != 0 && !quit) + if (layerinlayer != 0 && !_ct.IsCancellationRequested) { prepackedy = packedy; preremainpy = remainpy; @@ -365,9 +366,9 @@ private void ExecuteIterations(Container container) } FindLayer(remainpy); - } while (packing && !quit); + } while (packing && !_ct.IsCancellationRequested); - if ((packedVolume > bestVolume) && !quit) + if ((packedVolume > bestVolume) && !_ct.IsCancellationRequested) { bestVolume = packedVolume; bestVariant = containerOrientationVariant; @@ -525,10 +526,11 @@ private void FindSmallestZ() /// /// Initializes everything. /// - private void Initialize(Container container, List items) + private void Initialize(Container container, List items, CancellationToken ct) { itemsToPack = new List(); itemsPackedInOrder = new List(); + _ct = ct; result = new ContainerPackingResult(); // The original code uses 1-based indexing everywhere. This fake entry is added to the beginning @@ -565,7 +567,6 @@ private void Initialize(Container container, List items) scrapfirst.Post = null; packingBest = false; hundredPercentPacked = false; - quit = false; } /// @@ -752,7 +753,7 @@ private void PackLayer() scrapfirst.CumX = px; scrapfirst.CumZ = 0; - for (; !quit;) + for (; !_ct.IsCancellationRequested;) { FindSmallestZ(); @@ -1037,8 +1038,6 @@ private void PackLayer() /// private void Report(Container container) { - quit = false; - switch (bestVariant) { case 1: @@ -1112,11 +1111,11 @@ private void Report(Container container) remainpz = pz; } - if (!quit) + if (!_ct.IsCancellationRequested) { FindLayer(remainpy); } - } while (packing && !quit); + } while (packing && !_ct.IsCancellationRequested); } /// diff --git a/src/CromulentBisgetti.ContainerPacking/Algorithms/IPackingAlgorithm.cs b/src/CromulentBisgetti.ContainerPacking/Algorithms/IPackingAlgorithm.cs index 4213bee..a2bc388 100644 --- a/src/CromulentBisgetti.ContainerPacking/Algorithms/IPackingAlgorithm.cs +++ b/src/CromulentBisgetti.ContainerPacking/Algorithms/IPackingAlgorithm.cs @@ -1,5 +1,6 @@ using CromulentBisgetti.ContainerPacking.Entities; using System.Collections.Generic; +using System.Threading; namespace CromulentBisgetti.ContainerPacking.Algorithms { @@ -13,7 +14,8 @@ public interface IPackingAlgorithm /// /// The container. /// The items to pack. + /// Sets the System.Threading.CancellationToken associated with this AlgorithmPackingResult instance. /// The algorithm packing result. - AlgorithmPackingResult Run(Container container, List items); + AlgorithmPackingResult Run(Container container, List items, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/CromulentBisgetti.ContainerPacking/PackingService.cs b/src/CromulentBisgetti.ContainerPacking/PackingService.cs index e7fae8b..65b61c4 100644 --- a/src/CromulentBisgetti.ContainerPacking/PackingService.cs +++ b/src/CromulentBisgetti.ContainerPacking/PackingService.cs @@ -4,10 +4,11 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace CromulentBisgetti.ContainerPacking -{ +{ /// /// The container packing service. /// @@ -21,6 +22,13 @@ public static class PackingService /// The list of algorithm type IDs to use for packing. /// A container packing result with lists of the packed and unpacked items. public static List Pack(List containers, List itemsToPack, List algorithmTypeIDs) + { + var source = new CancellationTokenSource(); + + return Pack(containers, itemsToPack, algorithmTypeIDs, source.Token); + } + + public static List Pack(List containers, List itemsToPack, List algorithmTypeIDs, CancellationToken cancellationToken) { Object sync = new Object { }; List result = new List(); @@ -45,16 +53,16 @@ public static List Pack(List containers, List Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); - AlgorithmPackingResult algorithmResult = algorithm.Run(container, items); + AlgorithmPackingResult algorithmResult = algorithm.Run(container, items, cancellationToken); stopwatch.Stop(); algorithmResult.PackTimeInMilliseconds = stopwatch.ElapsedMilliseconds; - decimal containerVolume = container.Length * container.Width * container.Height; - decimal itemVolumePacked = algorithmResult.PackedItems.Sum(i => i.Volume); + decimal containerVolume = container.Length * container.Width * container.Height; + decimal itemVolumePacked = algorithmResult.PackedItems.Sum(i => i.Volume); decimal itemVolumeUnpacked = algorithmResult.UnpackedItems.Sum(i => i.Volume); - algorithmResult.PercentContainerVolumePacked = Math.Round(itemVolumePacked / containerVolume * 100, 2); + algorithmResult.PercentContainerVolumePacked = Math.Round(itemVolumePacked / containerVolume * 100, 2); algorithmResult.PercentItemVolumePacked = Math.Round(itemVolumePacked / (itemVolumePacked + itemVolumeUnpacked) * 100, 2); lock (sync) diff --git a/src/CromulentBisgetti.ContainerPackingTests/ContainerPackingCancelTests.cs b/src/CromulentBisgetti.ContainerPackingTests/ContainerPackingCancelTests.cs new file mode 100644 index 0000000..651c46d --- /dev/null +++ b/src/CromulentBisgetti.ContainerPackingTests/ContainerPackingCancelTests.cs @@ -0,0 +1,54 @@ +using CromulentBisgetti.ContainerPacking; +using CromulentBisgetti.ContainerPacking.Algorithms; +using CromulentBisgetti.ContainerPacking.Entities; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace CromulentBisgetti.ContainerPackingTests +{ + [TestClass] + public class ContainerPackingCancelTests + { + [TestMethod] + public async Task LongRunningTest_CanBeCancelled() + { + // One of the longer-running 700 tests, #479 + var itemsToPack = new List + { + new Item(1, 64, 48, 64, 14), + new Item(2, 79, 25, 79, 23), + new Item(3, 89, 85, 89, 19), + new Item(4, 79, 66, 79, 17), + new Item(5, 79, 54, 79, 16), + new Item(6, 115, 95, 115, 11), + new Item(7, 76, 54, 76, 20), + new Item(8, 80, 44, 80, 10), + new Item(9, 66, 33, 66, 15), + new Item(10, 50, 32, 50, 15), + new Item(11, 116, 93, 116, 19), + new Item(12, 113, 64, 113, 11), + }; + var containers = new List + { + new Container(1, 587, 233, 220), + }; + + var source = new CancellationTokenSource(); + + // start the packing task on another thread and give it a bit of time to start + var packingTask = Task.Run(() => + PackingService.Pack(containers, itemsToPack, new List { (int)AlgorithmType.EB_AFIT }, source.Token)); + await Task.Delay(50); + + // then cancel it. Packing should return quickly + source.Cancel(); + + var result = await packingTask; + + var elapsedMilliSec = result[0].AlgorithmPackingResults[0].PackTimeInMilliseconds; + Assert.IsTrue(elapsedMilliSec < 100, $"Expected elapsed time to be less than 100 but found {elapsedMilliSec} msec"); + } + } +} \ No newline at end of file diff --git a/src/CromulentBisgetti.ContainerPackingTests/ContainerPackingTests.cs b/src/CromulentBisgetti.ContainerPackingTests/ContainerPackingTests.cs index 897479c..3f52caf 100644 --- a/src/CromulentBisgetti.ContainerPackingTests/ContainerPackingTests.cs +++ b/src/CromulentBisgetti.ContainerPackingTests/ContainerPackingTests.cs @@ -6,6 +6,8 @@ using CromulentBisgetti.ContainerPacking; using CromulentBisgetti.ContainerPacking.Entities; using CromulentBisgetti.ContainerPacking.Algorithms; +using System.Globalization; +using System.Diagnostics; namespace CromulentBisgetti.ContainerPackingTests { @@ -19,6 +21,8 @@ public void EB_AFIT_Passes_700_Standard_Reference_Tests() string resourceName = "CromulentBisgetti.ContainerPackingTests.DataFiles.ORLibrary.txt"; Assembly assembly = Assembly.GetExecutingAssembly(); + var decimalPointCulture = CultureInfo.GetCultureInfo("en-us"); + using (Stream stream = assembly.GetManifestResourceStream(resourceName)) { using (StreamReader reader = new StreamReader(stream)) @@ -54,6 +58,8 @@ public void EB_AFIT_Passes_700_Standard_Reference_Tests() List result = PackingService.Pack(containers, itemsToPack, new List { (int)AlgorithmType.EB_AFIT }); + Debug.WriteLine($"Test #{counter} took {result[0].AlgorithmPackingResults[0].PackTimeInMilliseconds}msec"); + // Assert that the number of items we tried to pack equals the number stated in the published reference. Assert.AreEqual(result[0].AlgorithmPackingResults[0].PackedItems.Count + result[0].AlgorithmPackingResults[0].UnpackedItems.Count, Convert.ToDecimal(testResults[1])); @@ -61,14 +67,16 @@ public void EB_AFIT_Passes_700_Standard_Reference_Tests() Assert.AreEqual(result[0].AlgorithmPackingResults[0].PackedItems.Count, Convert.ToDecimal(testResults[2])); // Assert that the packed container volume percentage is equal to the published reference result. - // Make an exception for a couple of tests where this algorithm yields 87.20% and the published result - // was 87.21% (acceptable rounding error). - Assert.IsTrue(result[0].AlgorithmPackingResults[0].PercentContainerVolumePacked == Convert.ToDecimal(testResults[3]) || - (result[0].AlgorithmPackingResults[0].PercentContainerVolumePacked == 87.20M && Convert.ToDecimal(testResults[3]) == 87.21M)); + var actualPercentage = result[0].AlgorithmPackingResults[0].PercentContainerVolumePacked; + var expectedPrecentage = Convert.ToDecimal(testResults[3], decimalPointCulture); - // Assert that the packed item volume percentage is equal to the published reference result. - Assert.AreEqual(result[0].AlgorithmPackingResults[0].PercentItemVolumePacked, Convert.ToDecimal(testResults[4])); + Assert.IsTrue( + Math.Abs(actualPercentage - expectedPrecentage) < 0.02M, + $"Test #{counter} failed: expected%={expectedPrecentage}; actual%={actualPercentage};"); + // Assert that the packed item volume percentage is equal to the published reference result. + Assert.AreEqual(result[0].AlgorithmPackingResults[0].PercentItemVolumePacked, Convert.ToDecimal(testResults[4], decimalPointCulture)); + counter++; } }