Speckle Blog / Towards A Less Janky Grasshopper Canvas

While working on Speckle 2.0, we really felt like we needed to come out about this one. Yes, we really don't like jank. Even more, we really don't like when Speckle makes things janky. What's jank? Here's a definition:


This is a companion discussion topic for the original entry at https://speckle.systems/blog/async-gh
1 Like

This doesn’t need another post, so I’ll just paste updates in here for those that miss out on github. The big news is that this approach is now working for lists & trees! Basically for each run of the component, we keep track of all the child tasks it creates, and then we set the outputs at the other end.

Look at those prime numbers going! Also got this question whether this approach uses up all your cores:

The answer is yes! The default task scheduler takes care of all that. We’ve also allowed you to give it some hints re TaskCreationOptions (see docs) if you know in advance how your computation will behave. You can set them in the components that inherit from the GH_AsyncComponent class.

If you like this, don’t forget to star the repo :star2: :heart_eyes:

@dimitrie Awesome stuff! Can a task capable component also be a gh_asyncomponent? Currently I have a bunch of components set up to be task capable and would love to implement the method you’ve illustrated as well. Any input is appreciated!

M

Hey @mjuliani! They’re very similar overall, and I think porting over could be quite easy. The GH_AsyncComponent is actually using tasks to delegate its work - it spins them up for you. In the example above, each component iteration has its own independent task in which the n-th prime is calculated.

I think the simplest analogy is that the DoWork function is the equivalent of the SolveResults method from McNeel’s TaskCapableComponent tutorial.

That said, there’s been some changes in the last week that solved the flash of null data that was plaguing this approach, and we’ve switched from an interface implementation to an abstract class. I’m breaking down below an example implementation (taken from here).

First off, the actual component implementation:

  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("The N-th Prime Calculator", "PRIME", "Calculates the nth prime number.", "Samples", "Async")
    {
      // 👉 Important! Set the BaseWorker prop of this component to whatever your "worker" class implementation is called.  
      BaseWorker = new PrimeCalculatorWorker();
    }

    // Input output params: proceed as usual! 
    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.AddNumberParameter("Output", "O", "The n-th prime number.", GH_ParamAccess.item);
    }
  }

Things to note:

  • Your component class needs to inherit from the GH_AsyncComponent class.
  • Define your input and output parameters as always.
  • In the constructor you need to instantiate your actual worker class that basically does the heavy lifting. More on this below!
  • Do not override the solve instance method. This is taken care of by the parent!GH_AsyncComponent class. Your actual computation logic will go in the DoWork method of the WorkerInstance class. More below!

Here’s a sample implementation of a worker class. This class is the key!

  
  // Note: we've moved away from an interface to an abstract class. Anyway: 
  public class PrimeCalculatorWorker : WorkerInstance
  {
    // You can hold whatever copies of local state you need to in here. These can be set from 
    // the component above, or from the GetData function below. 
    int TheNthPrime { get; set; } = 100;
    long ThePrime { get; set; } = -1;
    
    // This function will be called at the beginning, and in it you can just collect your input data 
    // just like you would in a normal component. Store whatever you need inside this class so
    // you can use them later in the `DoWork` function. 
    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;
    }
    
    // In here simply set data like you normally would in a grasshopper component. 
    public override void SetData(IGH_DataAccess DA)
    {
      // 👉 Checking for cancellation!
      if (CancellationToken.IsCancellationRequested) return;

      DA.SetData(0, ThePrime);
    }
    
    // This is where the magic happens! Whatever logic you have should happen in here.
    // Make sure to check for task cancellation often and rigorously! 
    public override void DoWork(Action<string, double> ReportProgress, Action<string, GH_RuntimeMessageLevel> ReportError, Action Done)
    {
      // 👉 Checking for cancellation!
      if (CancellationToken.IsCancellationRequested) return;

      int count = 0;
      long a = 2;
      while (count < TheNthPrime)
      {
        // 👉 Checking for cancellation!
        if (CancellationToken.IsCancellationRequested) return;

        // more calc code... (see repo for full implementation) 
        
        // Call this action to report your calculation progress. It expects values from 0 to 1 (percentages). 
        ReportProgress(Id, ((double)count) / TheNthPrime);

        if (prime > 0) count++;
        
        a++;
      }

      ThePrime = --a;

      // When you're done, simply call the `Done` action provided. This will tell the parent component that it's time to start setting data once all other workers finish. 
      Done();
    }
    
     // Last but not least, you need to proved a way to duplicate this class. This is important
    // if you pass in state from the parent component. In this case, we don't, so we just 
    // return a new instance of it. 
    public override WorkerInstance Duplicate() => new PrimeCalculatorWorker();

  }

Okay, a little summary of the WorkerInstance implementation:

  • DoWork - this is where your calculation logic goes. It will run on in its own Task. Inside DoWork you can
    • call ReportProgress to report upwards this specific instance’s progress.
    • call Done at the end of your calculations.
    • check for task cancellation! If you don’t, nothing will happen, but you’ll keep the computer busy for no reason at all.
  • SetData & GetData - just like a normal Grasshopper component.
  • Duplicate - returns a fresh instance of the class, with all its needed state passed on from the parent component. Just return a new instance if that’s not the case.

Anyways, hope this helps! Let us know if you have any other questions, or if you find bugs.

Hey @dimitrie, that’s all very helpful. I understand the setup now :). I guess this answers the question of whether I would be unable to obtain this functionality if I am calling a function belonging to an instance of an class instance elsewhere (no for now, right?). Also for getting/setting data (list/tree) would a set/get data overload be needed? Lastly and most importantly, I cloned and built the solution you posted, dropped the contents of the bin folder in my Grasshopper libraries folder, and I cant seem to find the sample components under ‘Samples’ (as defined in the component constructor). The GH_Exposure seems to be overwritten correctly and set to visible, but still cant find the GH_Async components… What am I missing??

Thanks in advance.
M

I guess so, but you can defs do your standard stuff inside the GetData and SetData, just like you would inside a normal SolveInstance call!

Depends: if you have access to that code, what you need to do is pass along the CancellationToken and make sure to check whenever you can if it’s cancelled and return early. If the class belongs to a 3rd party dll, then I’m afraid that this approach won’t help.

That’s really weird. Not sure what’s going on! I usually set the build folder, rather than copy pasting the components (using GrasshopperDeveloperSettings) - maybe give that route a shot?