Towards A Less Janky Grasshopper Canvas

Towards A Less Janky Grasshopper Canvas

Dev log updates: an async component approach for Grasshopper. Less janky and more responsive!

Written by Dimitrie Stefanescu on

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:

Perceptible pause in the smooth rendering of a software application's user interface due to slow operations or poor interface design.

(source)

Problem: Latency, Speckle & Grasshopper

As you may know, Grasshopper can be quite janky when things get... big. While developing the 2.0 version of the Speckle connector, we wanted to avoid - as best as we can! - adding jank to your definitions.

Disclaimer: what follows is not an authorative piece. We thought sharing our intial investigations in how we can solve this problem might be helpful for others, as well as for us, as there's probably better ways to do it. Check out the repository and let us know!

Speckle, within Grasshopper, is at the end of a workflow - converting objects, serialising them and sending them over the internet somewhere. Speckle, by its nature, has to deal with high latency operations - things that take time. If curious, do check this list of Latency Numbers Every Programmer Should Know (hint: Speckle, in user-land, mostly deals with the last four).

So what are we doing to avoid jankyness in Grasshopper, and why is it a problem? Grasshopper 1.0 (there's a 2.0 coming) is, unfortunately, bound to the UI thread. That means all computation happens in the same place as drawing all the nice buttons and responding to user inputs - clicks and so on. When writing custom components, the logic you drop inside the famous protected override void SolveInstance(IGH_DataAccess DA) function will block the UI thread while it's rolling.

There's been quite a few attempts at solving this, and ours is, principally, probably not that different. Or better 😂 The really nice solution of task capable components, while great, still blocks the damn UI thread. We had a few requirements along the way, which can be summarised as follows:

A Solution: GH_AsyncComponent

We've solved all these and generalised the approach so that any component can become one. Here's a link to the repo! Feel free to use it as a base for your future asnyc-ness goodies. First off, here's how it looks and behaves in a test component for Speckle 2.0 (that's converting 15k circles):

async2

Notice that the solution runs "eagerly" - every time the input changes, the the computation restarts and cancels any previous tasks that are still running. Once everything is done calculating, the results are set. And the best parts:

How does it work? Well, simply put you need to create a new component and inherit as a base class the GH_AsyncComponent class. Here you can do the standard things you would normally do (register input/output ports, icons, etc.), with two extra concerns:

// TestAsyncComponent.cs
public class TestAsyncComponent : GH_AsyncComponent 
{
  public override Guid ComponentGuid { get => new Guid("F1E5F78F-242D-44E3-AAD6-AB0257D69256"); }
  protected override System.Drawing.Bitmap Icon { get => null; }
  
  protected override void RegisterInputParams(GH_InputParamManager pManager) { ... }
  protected override void RegisterOutputParams(GH_InputParamManager pManager) { ... }
}

Next, you will need to define a Worker class that implements the IAsyncComponentWorker interface. The implementation should be quite straigtforward!

public interface IAsyncComponentWorker
{
  /// <summary>
  /// This function should return a duplicate instance of your class. Make sure any state is duplicated (or not) properly. 
  /// </summary>
  /// <returns></returns>
  IAsyncComponentWorker GetNewInstance();

  /// <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>
  /// </summary>
  /// <param name="DA"></param>
  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>
  /// </summary>
  /// <param name="DA"></param>
  /// <param name="Params"></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);

}

The magic happens in the DoWork function. You can see this function as a replacement for the SolveInstance one provided by the GH_Component. Here you can write up your logic, knowing that it will safely run outside the UI thread, and use whatever state you have stored internally during the GetData call.

The important part is to make sure you are actually checking for task cancellation within the DoWork function. This is quite easy to do, and make sure you pepper it around wherever you can:

Here's a sample implementation that iterates through a GH_Structure (Objects) and performs a heavy operation on each object. Notice how we check for task cancellation pretty much everywhere we can!

public void DoWork(CancellationToken token, Action<string> ReportProgress, Action<string, GH_RuntimeMessageLevel> ReportError, Action Done)
{
  // 👉 Check for task cancellation!
  if (token.IsCancellationRequested) return;

  var completed = 0;
  foreach (var list in Objects.Branches)
  {
    foreach (var item in list)
    {
      // 👉 Check for task cancellation!
      if (token.IsCancellationRequested) return;
   
      // DO HEAVY LIFTING STUFF

      ReportProgress(((double)(completed++ + 1) / (double)Objects.Count()).ToString("0.00%"));      
    }
  }

  // 👉 Check for task cancellation!
  if (token.IsCancellationRequested) return;
      
  Done();
}

Given the fact that the responsibility to check for 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. There's probably a few other limitations that we're not yet aware of.

That mostly covers the important parts. So far, this approach proved to be quite solid. There's some implementation quirks that we didn't cover here - like how we're displaying updates without calling the OnDisplayExpired function too often, and, most importantly, there's room for improvement - so we're happy to hear your thoughts, suggestions, and PRs!

Wait - there's more! Read on:

logo
Speckle

Empowering your design and construction data

© Aec Systems Ltd. Speckle and Speckle Systems are trademarks of AEC Systems Ltd., registered in the UK and in other countries. Company No. 12403784.

Terms of service | Privacy policy