Merge pull request #4 from specklesystems/dim/dev

Dim/dev
This commit is contained in:
Dimitrie Stefanescu
2020-10-07 19:40:41 +01:00
committed by GitHub
6 changed files with 322 additions and 141 deletions
@@ -8,6 +8,9 @@ using Timer = System.Timers.Timer;
namespace GrasshopperAsyncComponent
{
/// <summary>
/// Inherit your component from this class to make all the async goodness available.
/// </summary>
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<CancellationTokenSource> TokenSources = new ConcurrentBag<CancellationTokenSource>();
Action<string> ReportProgress;
Action<string, GH_RuntimeMessageLevel> ReportError;
@@ -32,9 +27,27 @@ namespace GrasshopperAsyncComponent
Action Done;
Timer DisplayProgressTimer;
int State = 0;
Timer DisplayProgressTimer;
bool SetData = false;
List<WorkerInstance> Workers;
List<Task> Tasks;
List<CancellationTokenSource> CancelationSources;
/// <summary>
/// Set this property inside the constructor of your derived component.
/// </summary>
public WorkerInstance BaseWorker { get; set; }
/// <summary>
/// Optional: if you have opinions on how the default system task scheduler should treat your workers, set it here.
/// </summary>
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<WorkerInstance>();
CancelationSources = new List<CancellationTokenSource>();
Tasks = new List<Task>();
}
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;
}
}
}
@@ -8,37 +8,49 @@ using System.Threading.Tasks;
namespace GrasshopperAsyncComponent
{
// TODO: Would an an abstract class be better here?
public interface IAsyncComponentWorker
/// <summary>
/// A class that holds the actual compute logic and encapsulates the state it needs. Every <see cref="GH_AsyncComponent"/> needs to have one.
/// </summary>
public abstract class WorkerInstance
{
/// <summary>
/// This token is set by the parent <see cref="GH_AsyncComponent"/>.
/// </summary>
public CancellationToken CancellationToken { get; set; }
/// <summary>
/// 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 <see cref="GH_AsyncComponent"/>. You can set it yourself, but it's not really worth it.
/// </summary>
public string Id { get; set; }
/// <summary>
/// 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.
/// </summary>
/// <returns></returns>
IAsyncComponentWorker GetNewInstance();
public abstract WorkerInstance Duplicate();
/// <summary>
/// Here you can safely set the data of your component, just like you would normally. <b>Important: do not call this method directly! When you are ready, call the provided "Done" action from the DoWork function.</b>
/// This method is where the actual calculation/computation/heavy lifting should be done.
/// <b>Make sure you always check as frequently as you can if <see cref="WorkerInstance.CancellationToken"/> is cancelled. For an example, see the <see cref="GrasshopperAsyncComponent.SampleImplementations.PrimeCalculatorWorker"/>.</b>
/// </summary>
/// <param name="ReportProgress">Call this to report progress up to the parent component.</param>
/// <param name="ReportError">Call this to report errors up to the parent component.</param>
/// <param name="Done">Call this when everything is <b>done</b>. It will tell the parent component that you're ready to <see cref="SetData(IGH_DataAccess)"/>.</param>
public abstract void DoWork(Action<string> ReportProgress, Action<string, GH_RuntimeMessageLevel> ReportError, Action Done);
/// <summary>
/// Write your data setting logic here. <b>Do not call this function directly from this class. It will be invoked by the parent <see cref="GH_AsyncComponent"/> after you've called `Done` in the <see cref="DoWork(Action{string}, Action{string, GH_RuntimeMessageLevel}, Action)"/> function.</b>
/// </summary>
/// <param name="DA"></param>
void SetData(IGH_DataAccess DA);
public abstract void SetData(IGH_DataAccess DA);
/// <summary>
/// Here you can safely collect the data from your component, just like you would normally. <b>Important: do not call this method directly. It will be invoked by the parent component.</b>
/// Write your data collection logic here. <b>Do not call this method directly. It will be invoked by the parent <see cref="GH_AsyncComponent"/>.</b>
/// </summary>
/// <param name="DA">The magic Data Access class.</param>
/// <param name="Params">The parameters list, in case you need it.</param>
void CollectData(IGH_DataAccess DA, GH_ComponentParamServer Params);
/// <summary>
/// This where the computation happens. Make sure to check and return if the token is cancelled!
/// </summary>
/// <param name="token">The cancellation token.</param>
/// <param name="ReportProgress">Call this action to report progress. It will be displayed in the component's message bar.</param>
/// <param name="ReportError">Call this to report a warning or an error.</param>
/// <param name="Done">When you are done computing, call this function to have the parent component invoke the SetData function.</param>
void DoWork(CancellationToken token, Action<string> ReportProgress, Action<string, GH_RuntimeMessageLevel> ReportError, Action Done);
/// <param name="DA"></param>
/// <param name="Params"></param>
public abstract void GetData(IGH_DataAccess DA, GH_ComponentParamServer Params);
}
}
@@ -63,7 +63,8 @@
<Compile Include="Info\GrasshopperAsyncComponentInfo.cs" />
<Compile Include="Base\IAsyncComponentWorker.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="SampleImplementations\SampleAsyncComponent.cs" />
<Compile Include="SampleImplementations\Sample_PrimeCalculatorAsyncComponent.cs" />
<Compile Include="SampleImplementations\Sample_UslessCyclesComponent.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
@@ -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<string> ReportProgress, Action<string, GH_RuntimeMessageLevel> 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.");
}
}
}
@@ -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<string> ReportProgress, Action<string, GH_RuntimeMessageLevel> 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}");
}
}
}
@@ -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<string> ReportProgress, Action<string, GH_RuntimeMessageLevel> 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.");
}
}
}