Towards A Less Janky Grasshopper Canvas
Dev log updates: an async component approach for Grasshopper. Less janky and more responsive!
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):
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
Next, you will need to define a Worker class that implements the IAsyncComponentWorker
interface. The implementation should be quite straigtforward!
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!
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:
- GH_AsyncComponent Github Repo (don't forget to give it a ⭐)
- Speckle 2.0 Discussion Forum - come check out the future Speckle!
Subscribe to Speckle News
Stay updated on the amazing tools coming from the talented Speckle community.