Towards A Less Janky Grasshopper Canvas
Dimitrie Stefanescu
October 5th 2020 connectorsdev

async-gh

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:

  • Long running operations inside Speckle components should not block the UI,
  • Components should be able to report progress back to the user in the Grasshopper UI,
  • Components should be able to report back any errors and warnings that happen inside other threads, and
  • Components should respond to user input eagerly and not compute for old states.

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:

  • Grasshopper and Rhino are still responsive!
  • There's progress reporting! (personally I hate waiting for Gh to unfreeze...).

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:

  • Do not override the SolveInstance(IGH_DataAccess DA) function.
  • Define and set a Worker instance of a type that implements IAsyncComponentWorker
// 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:

  • at the beginning and end of the function
  • inside any for loops that do heavy lifting
  • whenever you can check for it.

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!

Links: