Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ab9c1786f4 | |||
| b022d84364 | |||
| f632c0948d | |||
| dafebf5b03 | |||
| e3f4fb07dc | |||
| 89cd7fff62 | |||
| 63649cd9a7 | |||
| a50809bc91 | |||
| 442b06dff7 |
@@ -9,7 +9,6 @@ jobs:
|
||||
build:
|
||||
env:
|
||||
SOLUTION_NAME: "GrasshopperAsyncComponent.sln"
|
||||
OUTPUT_PATH: "output"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
||||
@@ -36,20 +36,12 @@
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<!-- Ingored warnings, some aspirational but too noisy for now, some by design. -->
|
||||
<NoWarn>
|
||||
<!--Disabled by design-->
|
||||
CA5399;CA1812;
|
||||
<!--XML comment-->
|
||||
CS1591;CS1573;
|
||||
<!-- Globalization rules -->
|
||||
CA1303;CA1304;CA1305;CA1307;CA1308;CA1309;CA1310;CA1311;
|
||||
<!-- Logging -->
|
||||
CA1848;CA1727;
|
||||
<!-- Others we don't want -->
|
||||
CA1815;CA1725;
|
||||
<!-- Naming things is hard enough -->
|
||||
CA1710;CA1711;CA1720;CA1724;
|
||||
<!-- Aspirational -->
|
||||
CA1502;CA1716;NETSDK1206;
|
||||
$(NoWarn)
|
||||
</NoWarn
|
||||
>
|
||||
|
||||
@@ -1,51 +1,79 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using Grasshopper.Kernel;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
namespace GrasshopperAsyncComponent;
|
||||
|
||||
internal sealed class Worker<T> : IDisposable
|
||||
where T : GH_Component
|
||||
{
|
||||
public required WorkerInstance<T> Instance { get; init; }
|
||||
|
||||
public required Task Task { get; init; }
|
||||
|
||||
public required CancellationTokenSource CancellationSource { get; init; }
|
||||
|
||||
public void Cancel() => CancellationSource.Cancel();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Task.IsCompleted)
|
||||
{
|
||||
Task.Dispose();
|
||||
}
|
||||
CancellationSource.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Inherit your component from this class to make all the async goodness available.
|
||||
/// </summary>
|
||||
public abstract class GH_AsyncComponent : GH_Component, IDisposable
|
||||
public abstract class GH_AsyncComponent<T> : GH_Component, IDisposable
|
||||
where T : GH_Component
|
||||
{
|
||||
//List<(string, GH_RuntimeMessageLevel)> Errors;
|
||||
|
||||
private readonly Action<string, double> _reportProgress;
|
||||
|
||||
public ConcurrentDictionary<string, double> ProgressReports { get; protected set; }
|
||||
|
||||
private readonly Action _done;
|
||||
public ConcurrentDictionary<string, double> ProgressReports { get; }
|
||||
|
||||
private readonly Timer _displayProgressTimer;
|
||||
|
||||
private int _state;
|
||||
/// <summary>
|
||||
/// a counter, used to count up the number of workers that have completed,
|
||||
/// until _setData is set true, when it starts to count down the workers as their data is set.
|
||||
/// </summary>
|
||||
private volatile int _state;
|
||||
|
||||
private int _setData;
|
||||
/// <summary>
|
||||
/// functionally, a boolean, 1 or 0;
|
||||
/// it will be set to 1 once all workers are ready for SetData to be called on them, then set back to 0.
|
||||
/// </summary>
|
||||
private volatile int _setData;
|
||||
|
||||
public List<WorkerInstance> Workers { get; protected set; }
|
||||
private readonly List<Worker<T>> _workers;
|
||||
|
||||
private readonly List<Task> _tasks;
|
||||
public IEnumerable<CancellationTokenSource> CancellationTokenSources => _workers.Select(x => x.CancellationSource);
|
||||
public IEnumerable<WorkerInstance<T>> Workers => _workers.Select(x => x.Instance);
|
||||
|
||||
public List<CancellationTokenSource> CancellationSources { get; }
|
||||
public int WorkerCount => _workers.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Set this property inside the constructor of your derived component.
|
||||
/// </summary>
|
||||
public WorkerInstance? BaseWorker { get; set; }
|
||||
public WorkerInstance<T>? 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; }
|
||||
public TaskCreationOptions TaskCreationOptions { get; set; } = TaskCreationOptions.None;
|
||||
|
||||
protected GH_AsyncComponent(string name, string nickname, string description, string category, string subCategory)
|
||||
: base(name, nickname, description, category, subCategory)
|
||||
{
|
||||
Workers = new List<WorkerInstance>();
|
||||
CancellationSources = new List<CancellationTokenSource>();
|
||||
_tasks = new List<Task>();
|
||||
_workers = new List<Worker<T>>();
|
||||
|
||||
ProgressReports = new ConcurrentDictionary<string, double>();
|
||||
|
||||
_displayProgressTimer = new Timer(333) { AutoReset = false };
|
||||
@@ -59,36 +87,36 @@ public abstract class GH_AsyncComponent : GH_Component, IDisposable
|
||||
_displayProgressTimer.Start();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
_done = () =>
|
||||
private void Done()
|
||||
{
|
||||
Interlocked.Increment(ref _state);
|
||||
if (_state == _workers.Count && _setData == 0)
|
||||
{
|
||||
Interlocked.Increment(ref _state);
|
||||
if (_state == Workers.Count && _setData == 0)
|
||||
{
|
||||
Interlocked.Exchange(ref _setData, 1);
|
||||
Interlocked.Exchange(ref _setData, 1);
|
||||
|
||||
// We need to reverse the workers list to set the outputs in the same order as the inputs.
|
||||
Workers.Reverse();
|
||||
// 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);
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
Rhino.RhinoApp.InvokeOnUiThread(
|
||||
(Action)
|
||||
delegate
|
||||
{
|
||||
ExpireSolution(true);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public virtual void DisplayProgress(object sender, System.Timers.ElapsedEventArgs e)
|
||||
{
|
||||
if (Workers.Count == 0 || ProgressReports.Values.Count == 0)
|
||||
if (_workers.Count == 0 || ProgressReports.Values.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Workers.Count == 1)
|
||||
if (_workers.Count == 1)
|
||||
{
|
||||
Message = ProgressReports.Values.Last().ToString("0.00%");
|
||||
}
|
||||
@@ -100,7 +128,7 @@ public abstract class GH_AsyncComponent : GH_Component, IDisposable
|
||||
total += kvp.Value;
|
||||
}
|
||||
|
||||
Message = (total / Workers.Count).ToString("0.00%");
|
||||
Message = (total / _workers.Count).ToString("0.00%");
|
||||
}
|
||||
|
||||
Rhino.RhinoApp.InvokeOnUiThread(
|
||||
@@ -121,29 +149,24 @@ public abstract class GH_AsyncComponent : GH_Component, IDisposable
|
||||
|
||||
Debug.WriteLine("Killing");
|
||||
|
||||
foreach (var source in CancellationSources)
|
||||
foreach (var currentWorker in _workers)
|
||||
{
|
||||
source.Cancel();
|
||||
currentWorker.Cancel();
|
||||
}
|
||||
|
||||
CancellationSources.Clear();
|
||||
Workers.Clear();
|
||||
ProgressReports.Clear();
|
||||
_tasks.Clear();
|
||||
|
||||
Interlocked.Exchange(ref _state, 0);
|
||||
ResetState();
|
||||
}
|
||||
|
||||
protected override void AfterSolveInstance()
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine("After solve instance was called " + _state + " ? " + Workers.Count);
|
||||
Debug.WriteLine("After solve instance was called " + _state + " ? " + _workers.Count);
|
||||
// We need to start all the tasks as close as possible to each other.
|
||||
if (_state == 0 && _tasks.Count > 0 && _setData == 0)
|
||||
if (_state == 0 && _workers.Count > 0 && _setData == 0)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine("After solve INVOKATION");
|
||||
foreach (var task in _tasks)
|
||||
Debug.WriteLine("After solve INVOCATION");
|
||||
foreach (var worker in _workers)
|
||||
{
|
||||
task.Start();
|
||||
worker.Task.Start();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -157,7 +180,7 @@ public abstract class GH_AsyncComponent : GH_Component, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
protected override void SolveInstance(IGH_DataAccess da)
|
||||
protected override void SolveInstance(IGH_DataAccess DA)
|
||||
{
|
||||
//return;
|
||||
if (_state == 0)
|
||||
@@ -170,31 +193,30 @@ public abstract class GH_AsyncComponent : GH_Component, IDisposable
|
||||
|
||||
// Add cancellation source to our bag
|
||||
var tokenSource = new CancellationTokenSource();
|
||||
CancellationSources.Add(tokenSource);
|
||||
|
||||
var currentWorker = BaseWorker.Duplicate($"Worker-{da.Iteration}", tokenSource.Token);
|
||||
if (currentWorker == null)
|
||||
{
|
||||
AddRuntimeMessage(GH_RuntimeMessageLevel.Error, "Could not get a worker instance.");
|
||||
return;
|
||||
}
|
||||
var currentWorker = BaseWorker.Duplicate($"Worker-{DA.Iteration}", tokenSource.Token);
|
||||
|
||||
// Let the worker collect data.
|
||||
currentWorker.GetData(da, Params);
|
||||
currentWorker.GetData(DA, Params);
|
||||
|
||||
var currentRun =
|
||||
TaskCreationOptions != null
|
||||
? new Task(
|
||||
() => currentWorker.DoWork(_reportProgress, _done),
|
||||
tokenSource.Token,
|
||||
(TaskCreationOptions)TaskCreationOptions
|
||||
)
|
||||
: new Task(() => currentWorker.DoWork(_reportProgress, _done), tokenSource.Token);
|
||||
var currentRun = new Task<Task>(
|
||||
async () =>
|
||||
{
|
||||
await currentWorker.DoWork(_reportProgress, Done).ConfigureAwait(true);
|
||||
},
|
||||
tokenSource.Token,
|
||||
TaskCreationOptions
|
||||
);
|
||||
|
||||
// Add the worker to our list
|
||||
Workers.Add(currentWorker);
|
||||
|
||||
_tasks.Add(currentRun);
|
||||
_workers.Add(
|
||||
new()
|
||||
{
|
||||
Instance = currentWorker,
|
||||
Task = currentRun,
|
||||
CancellationSource = tokenSource,
|
||||
}
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -204,10 +226,10 @@ public abstract class GH_AsyncComponent : GH_Component, IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
if (Workers.Count > 0)
|
||||
if (_workers.Count > 0)
|
||||
{
|
||||
Interlocked.Decrement(ref _state);
|
||||
Workers[_state].SetData(da);
|
||||
_workers[_state].Instance.SetData(DA);
|
||||
}
|
||||
|
||||
if (_state != 0)
|
||||
@@ -215,31 +237,34 @@ public abstract class GH_AsyncComponent : GH_Component, IDisposable
|
||||
return;
|
||||
}
|
||||
|
||||
CancellationSources.Clear();
|
||||
Workers.Clear();
|
||||
ProgressReports.Clear();
|
||||
_tasks.Clear();
|
||||
foreach (var worker in _workers)
|
||||
{
|
||||
worker?.Dispose();
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref _setData, 0);
|
||||
ResetState();
|
||||
|
||||
Message = "Done";
|
||||
OnDisplayExpired(true);
|
||||
}
|
||||
|
||||
public void RequestCancellation()
|
||||
private void ResetState()
|
||||
{
|
||||
foreach (var source in CancellationSources)
|
||||
{
|
||||
source.Cancel();
|
||||
}
|
||||
|
||||
CancellationSources.Clear();
|
||||
Workers.Clear();
|
||||
_workers.Clear();
|
||||
ProgressReports.Clear();
|
||||
_tasks.Clear();
|
||||
|
||||
Interlocked.Exchange(ref _state, 0);
|
||||
Interlocked.Exchange(ref _setData, 0);
|
||||
}
|
||||
|
||||
public void RequestCancellation()
|
||||
{
|
||||
foreach (var worker in _workers)
|
||||
{
|
||||
worker.Cancel();
|
||||
}
|
||||
|
||||
ResetState();
|
||||
Message = "Cancelled";
|
||||
OnDisplayExpired(true);
|
||||
}
|
||||
@@ -260,6 +285,10 @@ public abstract class GH_AsyncComponent : GH_Component, IDisposable
|
||||
|
||||
if (disposing)
|
||||
{
|
||||
foreach (var worker in _workers)
|
||||
{
|
||||
worker?.Dispose();
|
||||
}
|
||||
_displayProgressTimer?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup Label="Compiler Properties">
|
||||
<TargetFramework>net48</TargetFramework>
|
||||
<TargetFrameworks>net462;net48</TargetFrameworks>
|
||||
<RootNamespace>GrasshopperAsyncComponent</RootNamespace>
|
||||
<AssemblyName>GrasshopperAsyncComponent</AssemblyName>
|
||||
</PropertyGroup>
|
||||
@@ -20,8 +20,17 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Label="Package References">
|
||||
<PackageReference Include="PolySharp" Version="1.14.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net462'">
|
||||
<PackageReference Include="Grasshopper" Version="6.28.20199.17141" IncludeAssets="compile;build" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition=" '$(TargetFramework)' == 'net48'">
|
||||
<PackageReference Include="Grasshopper" Version="7.4.21078.1001" IncludeAssets="compile;build" PrivateAssets="all" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<Reference Include="System.Windows.Forms" />
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
using Grasshopper.Kernel;
|
||||
using Grasshopper.Kernel;
|
||||
|
||||
namespace GrasshopperAsyncComponent;
|
||||
|
||||
/// <summary>
|
||||
/// A class that holds the actual compute logic and encapsulates the state it needs. Every <see cref="GH_AsyncComponent"/> needs to have one.
|
||||
/// A class that holds the actual compute logic and encapsulates the state it needs. Every <see cref="GH_AsyncComponent{T}"/> needs to have one.
|
||||
/// </summary>
|
||||
public abstract class WorkerInstance(GH_Component? parent, string id, CancellationToken cancellationToken)
|
||||
public abstract class WorkerInstance<T>(T parent, string id, CancellationToken cancellationToken)
|
||||
where T : GH_Component
|
||||
{
|
||||
/// <summary>
|
||||
/// The parent component. Useful for passing state back to the host component.
|
||||
/// </summary>
|
||||
public GH_Component? Parent { get; set; } = parent;
|
||||
public T Parent { get; set; } = parent;
|
||||
|
||||
public CancellationToken CancellationToken { get; } = cancellationToken;
|
||||
|
||||
@@ -22,24 +23,29 @@ public abstract class WorkerInstance(GH_Component? parent, string id, Cancellati
|
||||
/// <param name="id">A Unique id for the new duplicate</param>
|
||||
/// <param name="cancellationToken">A cancellationToken to be passed to the new duplicate</param>
|
||||
/// <returns></returns>
|
||||
public abstract WorkerInstance? Duplicate(string id, CancellationToken cancellationToken);
|
||||
public abstract WorkerInstance<T> Duplicate(string id, CancellationToken cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 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 PrimeCalculatorWorker example.</b>
|
||||
/// <b>Make sure you always check as frequently as you can if <see cref="WorkerInstance{T}.CancellationToken"/> is cancelled. For an example, see the PrimeCalculatorWorker example.</b>
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// If you don't need <see langword="async"/> function, then you can simply return <see cref="Task.CompletedTask"/>
|
||||
/// Either way, you should be sure to handle exceptions in this function. Otherwise, they will be Unobserved!
|
||||
/// You can call <paramref name="done"/> on <see cref="Exception"/>s, but avoid calling it when cancellation is has been observed.
|
||||
/// </remarks>
|
||||
/// <param name="reportProgress">Call this to report progress 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, double> reportProgress, Action done);
|
||||
public abstract Task DoWork(Action<string, double> reportProgress, 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"/> function.</b>
|
||||
/// 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{T}"/> after you've called `Done` in the <see cref="DoWork"/> function.</b>
|
||||
/// </summary>
|
||||
/// <param name="da"></param>
|
||||
public abstract void SetData(IGH_DataAccess da);
|
||||
|
||||
/// <summary>
|
||||
/// 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>
|
||||
/// Write your data collection logic here. <b>Do not call this method directly. It will be invoked by the parent <see cref="GH_AsyncComponent{T}"/>.</b>
|
||||
/// </summary>
|
||||
/// <param name="da"></param>
|
||||
/// <param name="parameters"></param>
|
||||
|
||||
+28
-26
@@ -1,10 +1,10 @@
|
||||
using System.Windows.Forms;
|
||||
using System.Windows.Forms;
|
||||
using Grasshopper.Kernel;
|
||||
using GrasshopperAsyncComponent;
|
||||
|
||||
namespace GrasshopperAsyncComponentDemo.SampleImplementations;
|
||||
|
||||
public class Sample_PrimeCalculatorAsyncComponent : GH_AsyncComponent
|
||||
public class Sample_PrimeCalculatorAsyncComponent : GH_AsyncComponent<Sample_PrimeCalculatorAsyncComponent>
|
||||
{
|
||||
public override Guid ComponentGuid => new Guid("22C612B0-2C57-47CE-B9FE-E10621F18933");
|
||||
|
||||
@@ -46,25 +46,38 @@ public class Sample_PrimeCalculatorAsyncComponent : GH_AsyncComponent
|
||||
);
|
||||
}
|
||||
|
||||
private sealed class PrimeCalculatorWorker : WorkerInstance
|
||||
private sealed class PrimeCalculatorWorker : WorkerInstance<Sample_PrimeCalculatorAsyncComponent>
|
||||
{
|
||||
private int TheNthPrime { get; set; } = 100;
|
||||
private long ThePrime { get; set; } = -1;
|
||||
|
||||
public PrimeCalculatorWorker(
|
||||
GH_Component? parent,
|
||||
Sample_PrimeCalculatorAsyncComponent parent,
|
||||
string id = "baseworker",
|
||||
CancellationToken cancellationToken = default
|
||||
)
|
||||
: base(parent, id, cancellationToken) { }
|
||||
|
||||
public override void DoWork(Action<string, double> reportProgress, Action done)
|
||||
public override Task DoWork(Action<string, double> reportProgress, Action done)
|
||||
{
|
||||
try
|
||||
{
|
||||
CalculatePrimes(reportProgress);
|
||||
done();
|
||||
}
|
||||
catch (OperationCanceledException) when (CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
//No need to call `done()` - GrasshopperAsyncComponent assumes immediate cancel,
|
||||
//thus it has already performed clean-up actions that would normally be done on `done()`
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void CalculatePrimes(Action<string, double> reportProgress)
|
||||
{
|
||||
// 👉 Checking for cancellation!
|
||||
if (CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
CancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
int count = 0;
|
||||
long a = 2;
|
||||
@@ -73,20 +86,14 @@ public class Sample_PrimeCalculatorAsyncComponent : GH_AsyncComponent
|
||||
while (count < TheNthPrime)
|
||||
{
|
||||
// 👉 Checking for cancellation!
|
||||
if (CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
CancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
long b = 2;
|
||||
int prime = 1; // to check if found a prime
|
||||
while (b * b <= a)
|
||||
{
|
||||
// 👉 Checking for cancellation!
|
||||
if (CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
CancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
if (a % b == 0)
|
||||
{
|
||||
@@ -106,11 +113,12 @@ public class Sample_PrimeCalculatorAsyncComponent : GH_AsyncComponent
|
||||
}
|
||||
|
||||
ThePrime = --a;
|
||||
done();
|
||||
}
|
||||
|
||||
public override WorkerInstance Duplicate(string id, CancellationToken cancellationToken) =>
|
||||
new PrimeCalculatorWorker(Parent, id, cancellationToken);
|
||||
public override WorkerInstance<Sample_PrimeCalculatorAsyncComponent> Duplicate(
|
||||
string id,
|
||||
CancellationToken cancellationToken
|
||||
) => new PrimeCalculatorWorker(Parent, id, cancellationToken);
|
||||
|
||||
public override void GetData(IGH_DataAccess da, GH_ComponentParamServer parameters)
|
||||
{
|
||||
@@ -131,12 +139,6 @@ public class Sample_PrimeCalculatorAsyncComponent : GH_AsyncComponent
|
||||
|
||||
public override void SetData(IGH_DataAccess da)
|
||||
{
|
||||
// 👉 Checking for cancellation!
|
||||
if (CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
da.SetData(0, ThePrime);
|
||||
}
|
||||
}
|
||||
|
||||
Executable → Regular
+27
-17
@@ -1,10 +1,10 @@
|
||||
using System.Windows.Forms;
|
||||
using System.Windows.Forms;
|
||||
using Grasshopper.Kernel;
|
||||
using GrasshopperAsyncComponent;
|
||||
|
||||
namespace GrasshopperAsyncComponentDemo.SampleImplementations;
|
||||
|
||||
public class Sample_UselessCyclesAsyncComponent : GH_AsyncComponent
|
||||
public class Sample_UselessCyclesAsyncComponent : GH_AsyncComponent<Sample_UselessCyclesAsyncComponent>
|
||||
{
|
||||
public override Guid ComponentGuid => new Guid("DF2B93E2-052D-4BE4-BC62-90DC1F169BF6");
|
||||
|
||||
@@ -19,20 +19,33 @@ public class Sample_UselessCyclesAsyncComponent : GH_AsyncComponent
|
||||
}
|
||||
|
||||
private sealed class UselessCyclesWorker(
|
||||
GH_Component? parent,
|
||||
Sample_UselessCyclesAsyncComponent parent,
|
||||
string id = "baseworker",
|
||||
CancellationToken cancellationToken = default
|
||||
) : WorkerInstance(parent, id, cancellationToken)
|
||||
) : WorkerInstance<Sample_UselessCyclesAsyncComponent>(parent, id, cancellationToken)
|
||||
{
|
||||
private int MaxIterations { get; set; } = 100;
|
||||
|
||||
public override void DoWork(Action<string, double> reportProgress, Action done)
|
||||
public override Task DoWork(Action<string, double> reportProgress, Action done)
|
||||
{
|
||||
try
|
||||
{
|
||||
RunUselessCycles(reportProgress);
|
||||
done();
|
||||
}
|
||||
catch (OperationCanceledException) when (CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
//No need to call `done()` - GrasshopperAsyncComponent assumes immediate cancel,
|
||||
//thus it has already performed clean-up actions that would normally be done on `done()`
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public void RunUselessCycles(Action<string, double> reportProgress)
|
||||
{
|
||||
// Checking for cancellation
|
||||
if (CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
CancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
for (int i = 0; i <= MaxIterations; i++)
|
||||
{
|
||||
@@ -45,17 +58,14 @@ public class Sample_UselessCyclesAsyncComponent : GH_AsyncComponent
|
||||
reportProgress(Id, (i + 1) / (double)MaxIterations);
|
||||
|
||||
// Checking for cancellation
|
||||
if (CancellationToken.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
CancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
public override WorkerInstance Duplicate(string id, CancellationToken cancellationToken) =>
|
||||
new UselessCyclesWorker(Parent, id, cancellationToken);
|
||||
public override WorkerInstance<Sample_UselessCyclesAsyncComponent> Duplicate(
|
||||
string id,
|
||||
CancellationToken cancellationToken
|
||||
) => new UselessCyclesWorker(Parent, id, cancellationToken);
|
||||
|
||||
public override void GetData(IGH_DataAccess da, GH_ComponentParamServer parameters)
|
||||
{
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
# GrasshopperAsyncComponent
|
||||
|
||||
[](https://twitter.com/SpeckleSystems) [](https://discourse.speckle.works)
|
||||
[](https://speckle-works.slack.com/join/shared_invite/enQtNjY5Mzk2NTYxNTA4LTU4MWI5ZjdhMjFmMTIxZDIzOTAzMzRmMTZhY2QxMmM1ZjVmNzJmZGMzMDVlZmJjYWQxYWU0MWJkYmY3N2JjNGI) [](https://speckle.systems)
|
||||
[](https://www.nuget.org/packages/GrasshopperAsyncComponent)
|
||||
[](https://twitter.com/SpeckleSystems)
|
||||
[](https://speckle.community/)
|
||||
|
||||
|
||||
|
||||
## Less Janky Grasshopper Components
|
||||
|
||||
See the [companion blog post](https://speckle.systems/blog/async-gh/) about the rationale behind this approach. This repo demonstrates how to create an eager and responsive async component that does not block the Grasshopper UI thread while doing heavy work in the background, reports on progress and - theoretically - makes your life easier.
|
||||
See the [companion blog post](https://v1.speckle.systems/blog/async-gh/) about the rationale behind this approach. This repo demonstrates how to create an eager and responsive async component that does not block the Grasshopper UI thread while doing heavy work in the background, reports on progress and - theoretically - makes your life easier.
|
||||
|
||||
We're not so sure about the last part! We've put this repo out in the hope that others will find something useful inside - even just inspiration for the approach.
|
||||
|
||||
@@ -17,13 +20,13 @@ Looks nice, doesn't it? Notice that the solution runs "eagerly" - every time the
|
||||
- **There's progress reporting!** (personally I hate waiting for Gh to unfreeze...).
|
||||
- **Thread safe**: 99% of the times this won't explode in your face. It still might though!
|
||||
|
||||
### Approach
|
||||
## Approach
|
||||
|
||||
Provides an abstract `GH_AsyncComponent` which you can inherit from to scaffold your own async component. There's more info in the [blogpost](https://speckle.systems/blog/async-gh/) on how to go about it.
|
||||
Provides an abstract `GH_AsyncComponent` which you can inherit from to scaffold your own async component. There's more info in the [blogpost](https://v1.speckle.systems/blog/async-gh/) on how to go about it.
|
||||
|
||||
> #### Checkout the sample implementation!
|
||||
> - [Prime number calculator](https://github.com/specklesystems/GrasshopperAsyncComponent/blob/a53cef31a8750a18d06fad0f41b2dc452fdc253b/GrasshopperAsyncComponentDemo/SampleImplementations/Sample_PrimeCalculatorAsyncComponent.cs#L11-L53) Calculates the n'th prime. Can actually spin your computer's fans quite a bit for numbers > 100.000!
|
||||
> - [Usless spinner](https://github.com/specklesystems/GrasshopperAsyncComponent/blob/2f2be53bffd2402337ba40d65bb5b619d1161b3e/GrasshopperAsyncComponentDemo/SampleImplementations/Sample_UslessCyclesComponent.cs#L13-L91) does no meaningfull CPU work, just keeps a thread busy with SpinWait().
|
||||
> - [Prime number calculator](https://github.com/specklesystems/GrasshopperAsyncComponent/blob/main/GrasshopperAsyncComponentDemo/SampleImplementations/Sample_PrimeCalculatorAsyncComponent.cs) Calculates the n'th prime. Can actually spin your computer's fans quite a bit for numbers > 100.000!
|
||||
> - [Usless spinner](https://github.com/specklesystems/GrasshopperAsyncComponent/blob/main/GrasshopperAsyncComponentDemo/SampleImplementations/Sample_UslessCyclesComponent.cs) does no meaningfull CPU work, just keeps a thread busy with SpinWait().
|
||||
|
||||
### Current limitations
|
||||
|
||||
@@ -39,9 +42,9 @@ Other limitations:
|
||||
|
||||
- This approach is most efficient if you can batch together as many iterations as possible. Ideally you'd work with trees straight away.
|
||||
|
||||
- Task cancellation is up to the developer: this approach won't be too well suited for components calling code from other libraries that you don't, or can't, manage.
|
||||
- Cancellation and Error handling is up to the developer: this approach won't be too well suited for components calling code from other libraries that you don't, or can't, manage.
|
||||
|
||||
### FAQ
|
||||
## FAQ
|
||||
|
||||
Q: Does this component use all my cores? A: OH YES. It goes WROOOM.
|
||||
|
||||
@@ -64,9 +67,10 @@ A: Yes, now you can! In your component, just add a right click menu action like
|
||||
|
||||
```
|
||||
|
||||
Note: The `GrasshopperAsyncComponent` assumes immediate cancel, so it's important that your DoWork functions regularly observe the CancellationToken
|
||||
You should also avoid calling `done()` after observing a cancellation request.
|
||||
|
||||
|
||||
### Debugging
|
||||
## Debugging
|
||||
|
||||
Quite easy:
|
||||
- Clone this repository and open up the solution in Visual Studio.
|
||||
@@ -74,13 +78,61 @@ Quite easy:
|
||||
- You should see a new component popping up under "Samples > Async" in the ribbon.
|
||||
- A simple
|
||||
|
||||
|
||||
## Migration from 1.2.x -> 2.0.x
|
||||
|
||||
With the 2.0 release of the GrasshopperAsyncComponent there was a couple of breaking changes.
|
||||
With the introduction of generic args to the `WorkerInstance` class, the need to cast the `Parent` property is avoided, since it's typed correctly
|
||||
|
||||
### Before
|
||||
```csharp
|
||||
public class MyGrasshopperComponent : GH_AsyncComponent
|
||||
|
||||
public MyGrasshopperComponent()
|
||||
: base("etc..", "etc..", "etc..", "etc..", "etc..")
|
||||
{
|
||||
BaseWorker = new MyWorker(this);
|
||||
}
|
||||
}
|
||||
|
||||
//MyWorker doesn't know what type of component it's parent is,
|
||||
// if it needs to use it, it would have to cast and assume...
|
||||
public class MyWorker : WorkerInstance
|
||||
|
||||
public MyWorker(GH_Component parent)
|
||||
: base(parent) { }
|
||||
}
|
||||
```
|
||||
### After
|
||||
```csharp
|
||||
public class MyGrasshopperComponent : GH_AsyncComponent<MyGrasshopperComponent>
|
||||
|
||||
public MyGrasshopperComponent()
|
||||
: base("etc..", "etc..", "etc..", "etc..", "etc..")
|
||||
{
|
||||
BaseWorker = new MyWorker(this);
|
||||
}
|
||||
}
|
||||
|
||||
//Thanks to the generic arg, MyWorker now knows what type it's parent is!
|
||||
// we get compile time assurace that we're doing this correct!
|
||||
public class MyWorker : WorkerInstance<MyGrasshopperComponent>
|
||||
{
|
||||
//There's also another change here, we're expclitiy passing in an the id and cancellation token...
|
||||
//Previously these were being set after construction automatically by the GH_AsyncComponent class
|
||||
//Setting the id via constructor gives us better nullability safty, as we can be true to the nullability syntax.
|
||||
public MyWorker(MyGrasshopperComponent parent, string id = "baseWorker", cancellationToken = default)
|
||||
: base(parent, id, cancellationToken) { }
|
||||
}
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
Please make sure you read the [contribution guidelines](.github/CONTRIBUTING.md) and [Code of Conduct](.github/CODE_OF_CONDUCT.md) for an overview of the best practices we try to follow.
|
||||
|
||||
## Community
|
||||
|
||||
The Speckle Community hangs out on [the forum](https://discourse.speckle.works), do join and introduce yourself & feel free to ask us questions!
|
||||
The Speckle Community hangs out on [the forum](https://speckle.community/), do join and introduce yourself & feel free to ask us questions!
|
||||
|
||||
## Security
|
||||
|
||||
|
||||
Reference in New Issue
Block a user