9 Commits

Author SHA1 Message Date
Jedd Morgan ab9c1786f4 Update README.md (#33)
.NET Build and Publish / build (push) Has been cancelled
2025-07-09 14:57:02 +01:00
Jedd Morgan b022d84364 Merge pull request #32 from specklesystems/jrm/expose-extras
Expose some extra props
2025-07-09 13:11:19 +01:00
Jedd Morgan f632c0948d Expose some extra props 2025-07-09 12:56:29 +01:00
Jedd Morgan dafebf5b03 Merge pull request #31 from specklesystems/jrm/re-add-rhino6-support
.NET Build and Publish / build (push) Has been cancelled
re-add rhino 6 support
2025-07-09 11:07:23 +01:00
Jedd Morgan e3f4fb07dc re-add rhino 6 support 2025-07-09 11:03:33 +01:00
Jedd Morgan 89cd7fff62 Merge pull request #30 from specklesystems/dev
.NET Build and Publish / build (push) Has been cancelled
2.0.0
2025-06-16 14:27:10 +01:00
Jedd Morgan 63649cd9a7 Extra warnings 2025-06-16 14:25:10 +01:00
Jedd Morgan a50809bc91 refactor!: Major code cleanup for clearer state (#27)
.NET Build and Publish / build (push) Has been cancelled
* Quick pass

* no locks

* github actions

* publish via github actions

* pack

* Parent non-optional

* Generics are fun

* second pass

* Group workers, CTS, and Task in a class for simpler nullability

* pollysharp

* reverted cancellation changes

* reset state

* Update samples

* changes

* still working

* the fix

* private workers

* comments

* comments 2

* comments 3

* public worker count
2025-06-16 13:00:44 +01:00
Jedd Morgan 442b06dff7 removed unneeded env 2025-06-13 18:53:34 +01:00
8 changed files with 256 additions and 157 deletions
-1
View File
@@ -9,7 +9,6 @@ jobs:
build:
env:
SOLUTION_NAME: "GrasshopperAsyncComponent.sln"
OUTPUT_PATH: "output"
runs-on: ubuntu-latest
steps:
- name: Checkout
-8
View File
@@ -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
>
+112 -83
View File
@@ -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" />
+15 -9
View File
@@ -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>
@@ -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);
}
}
@@ -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)
{
+64 -12
View File
@@ -1,11 +1,14 @@
# GrasshopperAsyncComponent
[![Twitter Follow](https://img.shields.io/twitter/follow/SpeckleSystems?style=social)](https://twitter.com/SpeckleSystems) [![Discourse users](https://img.shields.io/discourse/users?server=https%3A%2F%2Fdiscourse.speckle.works&style=flat-square)](https://discourse.speckle.works)
[![Slack Invite](https://img.shields.io/badge/-slack-grey?style=flat-square&logo=slack)](https://speckle-works.slack.com/join/shared_invite/enQtNjY5Mzk2NTYxNTA4LTU4MWI5ZjdhMjFmMTIxZDIzOTAzMzRmMTZhY2QxMmM1ZjVmNzJmZGMzMDVlZmJjYWQxYWU0MWJkYmY3N2JjNGI) [![website](https://img.shields.io/badge/www-speckle.systems-royalblue?style=flat-square)](https://speckle.systems)
[![NuGet Version](https://img.shields.io/nuget/v/GrasshopperAsyncComponent)](https://www.nuget.org/packages/GrasshopperAsyncComponent)
[![Twitter Follow](https://img.shields.io/twitter/follow/SpeckleSystems?style=social)](https://twitter.com/SpeckleSystems)
[![Discourse users](https://img.shields.io/discourse/users?server=https%3A%2F%2Fspeckle.community&style=flat-square)](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