Best practices for Rhino plugin development wrt async operations

Hi,

I’ve been working on a Rhino plugin and have a question about best-practices, specifically within the context of developing plugins for Rhino.

I’m well-aware that RhinoCommon is NOT threadsafe. I always try to avoid calls to RhinoCommon from multiple threads for this reason because unstable behavior can occur.

My solution has always been to basically execute all operations within the plugin in a single-threaded manner, which of course, in the larger context of software development isn’t great because the user experiences a non-responsive program while the code executes. Especially in long-running operations, this can create unease for the user. My interim solution in those cases has been to periodically update the Rhino progress bar from that single thread at certain predefined points in the execution of an operation. So, essentially what you experience is a frozen window that has a progress bar that gradually fills up.

I notice that most Rhino commands, when you are simply modeling in the viewport, do not freeze the program the same way. You may get a progress bar, or a spinning cursor, but generally-speaking you don’t see the program hang.

Is there something I’m missing about the best way to organize my plugin so that it both does not execute unsafe code with respect to RhinoCommon but also gives the best user experience possible in terms of program hangs? I’m not willing to compromise any thread-safety with respect to RhinoCommon since stability is the first priority above all else, but if there is some best-practice that I’m missing, I would love to know.

Thanks!

I was looking at a model for engineering calcs such as ship stability in which I take a set of data out of Rhino in the main Rhino application thread, then sync up (either to show progress or show results) in the Rhino idle loop.

It’s harder if you are doing all of your operations with RhinoCommon calls and want the user to be able to edit the model while you’re doing it.

What’s the workflow you’re trying to code?

We have a plugin that allows a proprietary custom modeling workflow built on top of the Rhino platform. The users interact with the viewport and have some additional modeling tools available to them through a panel we have added to the Rhino UI. We actually never want them doing anything while calculations are happening, but at the end of the day my concerns come down to user comfort.

That’s about as specific as I can get right now without going too far into domain specifics, however it should be enough to describe the issue.

Basically I’d prefer if the users weren’t presented with a spinning cursor and “not responding” window when they try to run some operations that can take minutes. We’ve tried methods of calling RhinoCommon from asynchronous commands which has the effect of working 99.999% of the time and giving random crashes the remaining 0.001% of the time, which is unacceptable by our standards. So, we have resorted to cutting this out of our project. But, of course, there is the cost in terms of UX that users have to know that it is “OK” that the software is frozen. I mean, we could go to the effort of building a popup window with a progress bar that won’t go away until the operation is complete but that seems like a kludge.

To avoid kludges, I’m trying to see if there is a way the folks at McNeel recommend handling these situations. Somehow, the Rhino program itself never really freezes when you run long operations, such as heavy mesh operations, but rather gently shows that you can’t do anything.

You can try using the GetCancel class in RhinoCommon to allow the user to press escape for cancellation of a background task.

1 Like

It’s great you’re thinking about this and trying to make a better product. Sadly however, I’d love to know if there’s a different approach (e.g. in C#) or if I’ve got this wrong, but I thought making changes to the Rhino Document and Grasshopper Canvas is always blocking because of the Undo queue, and because the order of operations is important (with a command pattern behind the scenes)?

If your modelling workflow doesn’t change any Rhino state for the user (until they elect to import the results after they’re ready?) perhaps it’s possible to off load the work to an external process and free up Rhino.

I use subprocess.check_output from GhPython. It’s still blocking (I only run naively though), but a command line window is opened up, so the User can see any stdout feedback from the called subprocess, e.g. a progress counter.

That’s intriguing. I didn’t know that GetCancel existed. Is this just for interruption of custom Rhino “commands” or is there also some way I could wrap some functionality I have that happens to call RhinoCommon?

Our tool actually doesn’t depend on custom commands in Rhino and rather there are buttons on our side panel that execute some more complex operations in the background and then magically display the results to the user in the viewport/bake some geometry to Rhino. There’s a whole model view view model architecture that knows about things in the Rhino model, so users generally aren’t touching much in the viewport besides clicking on objects and maybe moving a gumball. This is deliberate as our users are not expected to be expert Rhino users. Our product is trying, as much as possible, to be a skin on top of Rhino and we don’t use too many typed commands.

Here’s a pattern I’ve used in the past, particularly for any web-based operations.

It does the following:

  • Show the user a counter for how long the task has been running (make it look like things are working hard! Also useful for development/testing)
  • (Optionally) automatically cancels on timeout (after X seconds)
  • Cancels when escape key is pressed

The ‘responsiveness’ comes from yielding with RhinoApp.Wait() while the operation is being performed. This could also happen between operations being performed on the main thread.

Though perhaps GetCancel could simplify some of this too?

    protected override Result RunCommand(RhinoDoc doc, RunMode mode)
    {
        Result result = Result.Nothing;
        using (CancellationTokenSource cts = new CancellationTokenSource())
        {
            cts.CancelAfter(TimeSpan.FromSeconds(30));

            MyLongRunningTask(cts.Token).ContinueWith(task =>
            {
                if (task.IsCanceled)
                {
                    result = Result.Cancel;
                    return;
                }
                else if (task.IsFaulted)
                {
                    // Do something with task exception
                    result = Result.Failure;
                    return;
                }
                else
                {
                    // Do something with task result
                    result = Result.Success;
                }
            });

            Stopwatch sw = new Stopwatch();
            sw.Start();

            void Cancel(object sender, EventArgs e)
            {
                cts.Cancel();
            }

            RhinoApp.EscapeKeyPressed += Cancel;

            while (result == Result.Nothing && !cts.IsCancellationRequested)
            {
                RhinoApp.SetCommandPrompt($"Running My Task... {sw.Elapsed.TotalSeconds:F2}s");
                RhinoApp.Wait();
            }
            RhinoApp.EscapeKeyPressed -= Cancel;
        }
        return result;
    }
3 Likes

Very handy @camnewnham, thankyou!

GetCancel should simplify that pattern.

Yes, GetCancel was designed to be run from inside of a Rhino Command.

2 Likes

I think the simplest form would then be:

    protected override Result RunCommand(RhinoDoc doc, RunMode mode)
    {
        Task task = CreateLongRunningTask();
        GetCancel gc = new GetCancel();
        gc.SetCommandPrompt("Running task... Press esc to cancel...");
        return gc.Wait(task, doc);
    }

With the extension that we could update GetCancel.Progress to update the user/UI.

I’m not entirely sure what GetCancel.SetWaitDuration is supposed to do or if it applies in this context. I expected it to cancel the task (and return GetResult.Timeout) upon completion. However, the task (and Wait) would always run to completion and return GetResult.Success:

    protected override Result RunCommand(RhinoDoc doc, RunMode mode)
    {
        GetCancel gc = new GetCancel();
        gc.SetWaitDuration(100);
        Task task = CreateLongRunningTask(gc.Token);
        gc.SetCommandPrompt("Running task... Press esc to cancel...");
        return gc.Wait(task, doc);
    }
3 Likes

Thanks @camnewnham for your suggestions!

@stevebaer To your note about the GetCancel class being for Rhino commands, perhaps we can make an effort to refactor some of our operations to be wrapped by Rhino commands so that cancelation is easier. Is it possible to issue Rhino commands without running “send keystroke” commands to the command line / is there a way to bypass the command line and still send Rhino “commands”? We’ve run into some unusual order of operations issues in the past (see another post of mine) when sending keystrokes. I worry that RhinoApp.RunScript might also have similar issues.

RhinoApp.RunScript is the way to go. We use this internally for doing what you are describing.

3 Likes