diff --git a/GrasshopperAsyncComponent/Base/GH_AsyncComponent.cs b/GrasshopperAsyncComponent/Base/GH_AsyncComponent.cs index 92c9e6b..4bf3d06 100644 --- a/GrasshopperAsyncComponent/Base/GH_AsyncComponent.cs +++ b/GrasshopperAsyncComponent/Base/GH_AsyncComponent.cs @@ -8,6 +8,9 @@ using Timer = System.Timers.Timer; namespace GrasshopperAsyncComponent { + /// + /// Inherit your component from this class to make all the async goodness available. + /// public abstract class GH_AsyncComponent : GH_Component { public override Guid ComponentGuid { get => new Guid("5DBBD498-0326-4E25-83A5-424D8DC493D4"); } @@ -16,14 +19,6 @@ namespace GrasshopperAsyncComponent public override GH_Exposure Exposure => GH_Exposure.hidden; - public IAsyncComponentWorker Worker; - - IAsyncComponentWorker CurrentWorker; - - Task CurrentRun; - - ConcurrentBag TokenSources = new ConcurrentBag(); - Action ReportProgress; Action ReportError; @@ -32,9 +27,27 @@ namespace GrasshopperAsyncComponent Action Done; + Timer DisplayProgressTimer; + int State = 0; - Timer DisplayProgressTimer; + bool SetData = false; + + List Workers; + + List Tasks; + + List CancelationSources; + + /// + /// Set this property inside the constructor of your derived component. + /// + public WorkerInstance BaseWorker { get; set; } + + /// + /// Optional: if you have opinions on how the default system task scheduler should treat your workers, set it here. + /// + public TaskCreationOptions? TaskCreationOptions { get; set; } = null; protected GH_AsyncComponent(string name, string nickname, string description, string category, string subCategory) : base(name, nickname, description, category, subCategory) { @@ -50,78 +63,127 @@ namespace GrasshopperAsyncComponent ReportProgress = (progress) => { - Rhino.RhinoApp.InvokeOnUiThread((Action)delegate - { - Message = progress; - if (!DisplayProgressTimer.Enabled) DisplayProgressTimer.Start(); - }); + Message = progress; + if (!DisplayProgressTimer.Enabled) DisplayProgressTimer.Start(); }; ReportError = (error, type) => Errors?.Add((error, type)); Done = () => { - Rhino.RhinoApp.InvokeOnUiThread((Action)delegate + State++; + + if (State == Workers.Count) { - State = 1; - ExpireSolution(true); - }); + SetData = true; + // We need to reverse the workers list to set the outputs in the same order as the inputs. + Workers.Reverse(); + + Rhino.RhinoApp.InvokeOnUiThread((Action)delegate + { + ExpireSolution(true); + }); + } }; Errors = new List<(string, GH_RuntimeMessageLevel)>(); + + Workers = new List(); + CancelationSources = new List(); + Tasks = new List(); + } + + protected override void BeforeSolveInstance() + { + if (State != 0 && SetData) return; + + foreach (var source in CancelationSources) source.Cancel(); + + CancelationSources.Clear(); + Workers.Clear(); + Errors.Clear(); + Tasks.Clear(); + + State = 0; + } + + protected override void AfterSolveInstance() + { + // We need to start all the tasks as close as possible to each other. + if (State == 0 && Tasks.Count > 0) + { + foreach (var task in Tasks) task.Start(); + } } protected override void SolveInstance(IGH_DataAccess DA) { if (State == 0) { - if (Worker == null) + if (BaseWorker == null) { AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Worker class not provided."); return; } - CurrentWorker = Worker.GetNewInstance(); + var CurrentWorker = BaseWorker.Duplicate(); if (CurrentWorker == null) { AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Could not get a worker instance."); return; } - Errors = new List<(string, GH_RuntimeMessageLevel)>(); - - // Request the cancellation of any old tasks. - CancellationTokenSource oldTokenSource; - while (TokenSources.TryTake(out oldTokenSource)) - { - oldTokenSource?.Cancel(); - } - // Let the worker collect data. - CurrentWorker.CollectData(DA, Params); + CurrentWorker.GetData(DA, Params); // Create the task var tokenSource = new CancellationTokenSource(); - CurrentRun = new Task(() => CurrentWorker.DoWork(tokenSource.Token, ReportProgress, ReportError, Done), tokenSource.Token); + CurrentWorker.CancellationToken = tokenSource.Token; + CurrentWorker.Id = DA.Iteration.ToString(); + Task CurrentRun; + if (TaskCreationOptions != null) + { + CurrentRun = new Task(() => CurrentWorker.DoWork(ReportProgress, ReportError, Done), tokenSource.Token, (TaskCreationOptions)TaskCreationOptions); + } + else + { + CurrentRun = new Task(() => CurrentWorker.DoWork(ReportProgress, ReportError, Done), tokenSource.Token); + } // Add cancelation source to our bag - TokenSources.Add(tokenSource); - CurrentRun.Start(); + CancelationSources.Add(tokenSource); + + // Add the worker to our list + Workers.Add(CurrentWorker); + + Tasks.Add(CurrentRun); + return; } - foreach (var (message, type) in Errors) + if (SetData) { - AddRuntimeMessage(type, message); + if (Workers.Count > 0) + Workers[--State].SetData(DA); + + if (State == 0) + { + foreach (var (message, type) in Errors) + { + AddRuntimeMessage(type, message); + } + + CancelationSources.Clear(); + Workers.Clear(); + Errors.Clear(); + Tasks.Clear(); + + SetData = false; + + Message = "Done"; + OnDisplayExpired(true); + } } - - OnDisplayExpired(true); - CurrentWorker.SetData(DA); - - Message = "Done"; - - Errors.Clear(); - State = 0; } } } diff --git a/GrasshopperAsyncComponent/Base/IAsyncComponentWorker.cs b/GrasshopperAsyncComponent/Base/IAsyncComponentWorker.cs index 8c5ac07..b0943c0 100644 --- a/GrasshopperAsyncComponent/Base/IAsyncComponentWorker.cs +++ b/GrasshopperAsyncComponent/Base/IAsyncComponentWorker.cs @@ -8,37 +8,49 @@ using System.Threading.Tasks; namespace GrasshopperAsyncComponent { - // TODO: Would an an abstract class be better here? - public interface IAsyncComponentWorker + + /// + /// A class that holds the actual compute logic and encapsulates the state it needs. Every needs to have one. + /// + public abstract class WorkerInstance { + /// + /// This token is set by the parent . + /// + public CancellationToken CancellationToken { get; set; } /// - /// This function should return a duplicate instance of your class. Make sure any state is duplicated (or not) properly. + /// This is set by the parent . You can set it yourself, but it's not really worth it. + /// + public string Id { get; set; } + + /// + /// This is a "factory" method. It should return a fresh instance of this class, but with all the necessary state that you might have passed on directly from your component. /// /// - IAsyncComponentWorker GetNewInstance(); + public abstract WorkerInstance Duplicate(); /// - /// Here you can safely set the data of your component, just like you would normally. Important: do not call this method directly! When you are ready, call the provided "Done" action from the DoWork function. + /// This method is where the actual calculation/computation/heavy lifting should be done. + /// Make sure you always check as frequently as you can if is cancelled. For an example, see the . + /// + /// Call this to report progress up to the parent component. + /// Call this to report errors up to the parent component. + /// Call this when everything is done. It will tell the parent component that you're ready to . + public abstract void DoWork(Action ReportProgress, Action ReportError, Action Done); + + /// + /// Write your data setting logic here. Do not call this function directly from this class. It will be invoked by the parent after you've called `Done` in the function. /// /// - void SetData(IGH_DataAccess DA); + public abstract void SetData(IGH_DataAccess DA); /// - /// Here you can safely collect the data from your component, just like you would normally. Important: do not call this method directly. It will be invoked by the parent component. + /// Write your data collection logic here. Do not call this method directly. It will be invoked by the parent . /// - /// The magic Data Access class. - /// The parameters list, in case you need it. - void CollectData(IGH_DataAccess DA, GH_ComponentParamServer Params); - - /// - /// This where the computation happens. Make sure to check and return if the token is cancelled! - /// - /// The cancellation token. - /// Call this action to report progress. It will be displayed in the component's message bar. - /// Call this to report a warning or an error. - /// When you are done computing, call this function to have the parent component invoke the SetData function. - void DoWork(CancellationToken token, Action ReportProgress, Action ReportError, Action Done); - + /// + /// + public abstract void GetData(IGH_DataAccess DA, GH_ComponentParamServer Params); } + } diff --git a/GrasshopperAsyncComponent/GrasshopperAsyncComponent.csproj b/GrasshopperAsyncComponent/GrasshopperAsyncComponent.csproj index a981acc..1e44e20 100644 --- a/GrasshopperAsyncComponent/GrasshopperAsyncComponent.csproj +++ b/GrasshopperAsyncComponent/GrasshopperAsyncComponent.csproj @@ -63,7 +63,8 @@ - + + diff --git a/GrasshopperAsyncComponent/SampleImplementations/SampleAsyncComponent.cs b/GrasshopperAsyncComponent/SampleImplementations/SampleAsyncComponent.cs deleted file mode 100644 index ec4ab0a..0000000 --- a/GrasshopperAsyncComponent/SampleImplementations/SampleAsyncComponent.cs +++ /dev/null @@ -1,77 +0,0 @@ -using Grasshopper.Kernel; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading; -using System.Threading.Tasks; - -namespace GrasshopperAsyncComponent.SampleImplementations -{ - public class SampleAsyncComponent : GH_AsyncComponent - { - public override Guid ComponentGuid { get => new Guid("DF2B93E2-052D-4BE4-BC62-90DC1F169BF6"); } - - protected override System.Drawing.Bitmap Icon { get => null; } - - public override GH_Exposure Exposure => GH_Exposure.primary; - - public SampleAsyncComponent() : base("Sample Async Component", "ASYNC", "Meaningless labour.", "Samples", "Async") - { - Worker = new SampleAsyncComponentWorker(); - } - - protected override void RegisterInputParams(GH_InputParamManager pManager) - { - pManager.AddIntegerParameter("Max iterations", "M", "How many useless cycles should we spin. Minimum 10, maximum 1000.", GH_ParamAccess.item); - } - - protected override void RegisterOutputParams(GH_OutputParamManager pManager) - { - pManager.AddTextParameter("Output", "O", "Will just say hello world after spinning.", GH_ParamAccess.item); - } - } - - public class SampleAsyncComponentWorker : IAsyncComponentWorker - { - int MaxIterations { get; set; } = 100; - - public void CollectData(IGH_DataAccess DA, GH_ComponentParamServer Params) - { - int _maxIterations = 100; - DA.GetData(0, ref _maxIterations); - if (_maxIterations > 1000) MaxIterations = 1000; - if (_maxIterations < 10) MaxIterations = 10; - - MaxIterations = _maxIterations; - } - - public void DoWork(CancellationToken token, Action ReportProgress, Action ReportError, Action Done) - { - if (token.IsCancellationRequested) return; - - for (int i = 0; i <= MaxIterations; i++) - { - var sw = new SpinWait(); - for (int j = 0; j <= 100; j++) - sw.SpinOnce(); - - ReportProgress(((double)(i + 1) / (double)MaxIterations).ToString("0.00%")); - - if (token.IsCancellationRequested) return; - } - - Done(); - } - - public IAsyncComponentWorker GetNewInstance() - { - return new SampleAsyncComponentWorker(); - } - - public void SetData(IGH_DataAccess DA) - { - DA.SetData(0, "Hello world. I'm done spinning."); - } - } -} diff --git a/GrasshopperAsyncComponent/SampleImplementations/Sample_PrimeCalculatorAsyncComponent.cs b/GrasshopperAsyncComponent/SampleImplementations/Sample_PrimeCalculatorAsyncComponent.cs new file mode 100644 index 0000000..acadebc --- /dev/null +++ b/GrasshopperAsyncComponent/SampleImplementations/Sample_PrimeCalculatorAsyncComponent.cs @@ -0,0 +1,103 @@ +using Grasshopper.Kernel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace GrasshopperAsyncComponent.SampleImplementations +{ + public class Sample_PrimeCalculatorAsyncComponent : GH_AsyncComponent + { + public override Guid ComponentGuid { get => new Guid("22C612B0-2C57-47CE-B9FE-E10621F18933"); } + + protected override System.Drawing.Bitmap Icon { get => null; } + + public override GH_Exposure Exposure => GH_Exposure.primary; + + public Sample_PrimeCalculatorAsyncComponent() : base("Sample Async Component", "PRIME", "Calculates the nth prime number.", "Samples", "Async") + { + BaseWorker = new PrimeCalculatorWorker(); + } + + protected override void RegisterInputParams(GH_InputParamManager pManager) + { + pManager.AddIntegerParameter("N", "N", "Which n-th prime number. Minimum 1, maximum one million. Take care, it can burn your CPU.", GH_ParamAccess.item); + } + + protected override void RegisterOutputParams(GH_OutputParamManager pManager) + { + pManager.AddTextParameter("Output", "O", "The n-th prime number.", GH_ParamAccess.item); + } + } + + public class PrimeCalculatorWorker : WorkerInstance + { + int TheNthPrime { get; set; } = 100; + long ThePrime { get; set; } = -1; + + public override void DoWork(Action ReportProgress, Action ReportError, Action Done) + { + // 👉 Checking for cancellation! + if (CancellationToken.IsCancellationRequested) return; + + int count = 0; + long a = 2; + + // Thanks Steak Overflow (TM) https://stackoverflow.com/a/13001749/ + while (count < TheNthPrime) + { + // 👉 Checking for cancellation! + if (CancellationToken.IsCancellationRequested) return; + + long b = 2; + int prime = 1;// to check if found a prime + while (b * b <= a) + { + // 👉 Checking for cancellation! + if (CancellationToken.IsCancellationRequested) return; + + if (a % b == 0) + { + prime = 0; + break; + } + b++; + } + + ReportProgress(((double)(count) / TheNthPrime).ToString("0.00%")); + + if (prime > 0) + { + count++; + } + a++; + } + + ThePrime = --a; + Done(); + } + + public override WorkerInstance Duplicate() => new PrimeCalculatorWorker(); + + public override void GetData(IGH_DataAccess DA, GH_ComponentParamServer Params) + { + int _nthPrime = 100; + DA.GetData(0, ref _nthPrime); + if (_nthPrime > 1000000) _nthPrime = 1000000; + if (_nthPrime < 1) _nthPrime = 1; + + TheNthPrime = _nthPrime; + } + + public override void SetData(IGH_DataAccess DA) + { + // 👉 Checking for cancellation! + if (CancellationToken.IsCancellationRequested) return; + + DA.SetData(0, $"Worker {Id}: {TheNthPrime}th prime is: {ThePrime}"); + } + } + +} diff --git a/GrasshopperAsyncComponent/SampleImplementations/Sample_UslessCyclesComponent.cs b/GrasshopperAsyncComponent/SampleImplementations/Sample_UslessCyclesComponent.cs new file mode 100644 index 0000000..dcb6041 --- /dev/null +++ b/GrasshopperAsyncComponent/SampleImplementations/Sample_UslessCyclesComponent.cs @@ -0,0 +1,80 @@ +using Grasshopper.Kernel; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace GrasshopperAsyncComponent.SampleImplementations +{ + public class Sample_UselessCyclesAsyncComponent : GH_AsyncComponent + { + public override Guid ComponentGuid { get => new Guid("DF2B93E2-052D-4BE4-BC62-90DC1F169BF6"); } + + protected override System.Drawing.Bitmap Icon { get => null; } + + public override GH_Exposure Exposure => GH_Exposure.primary; + + public Sample_UselessCyclesAsyncComponent() : base("Sample Async Component", "CYCLOMATRON-X", "Spins uselessly.", "Samples", "Async") + { + BaseWorker = new UselessCyclesWorker(); + } + + protected override void RegisterInputParams(GH_InputParamManager pManager) + { + pManager.AddIntegerParameter("N", "N", "Number of spins.", GH_ParamAccess.item); + } + + protected override void RegisterOutputParams(GH_OutputParamManager pManager) + { + pManager.AddTextParameter("Output", "O", "Nothing really interesting.", GH_ParamAccess.item); + } + } + + public class UselessCyclesWorker : WorkerInstance + { + int MaxIterations { get; set; } = 100; + + public override void DoWork(Action ReportProgress, Action ReportError, Action Done) + { + // Checking for cancellation + if (CancellationToken.IsCancellationRequested) return; + + for (int i = 0; i <= MaxIterations; i++) + { + var sw = new SpinWait(); + for (int j = 0; j <= 100; j++) + sw.SpinOnce(); + + ReportProgress(((double)(i + 1) / (double)MaxIterations).ToString("0.00%")); + + // Checking for cancellation + if (CancellationToken.IsCancellationRequested) return; + } + + Done(); + } + + public override WorkerInstance Duplicate() => new UselessCyclesWorker(); + + public override void GetData(IGH_DataAccess DA, GH_ComponentParamServer Params) + { + if (CancellationToken.IsCancellationRequested) return; + + int _maxIterations = 100; + DA.GetData(0, ref _maxIterations); + if (_maxIterations > 1000) _maxIterations = 1000; + if (_maxIterations < 10) _maxIterations = 10; + + MaxIterations = _maxIterations; + } + + public override void SetData(IGH_DataAccess DA) + { + if (CancellationToken.IsCancellationRequested) return; + DA.SetData(0, $"Hello world. Worker {Id} has spun for {MaxIterations} iterations."); + } + } + +}