Custom component: wait for external process without locking GH UI

Dear all!

Developing a set of real time or live-link components for a steel FEA software. I was wondering, how to achieve the following component behaviour:

While the component waits for the external software to update it’s own model, give control back to the Grasshopper GUI (eg user can pan, zoom around, mouse over component parameters to get the string overview of the values etc) but without the component lying about being solved. (so other components down the line wait for it to actually really finish).

I found this behaviour working in another component family. (namely the Grasshopper live-link of Tekla, which is a CAD software also part of our production chain)
But I could not find out how to reach this behaviour.

Any pointers would be welcome, what should I look for with this goal in mind?

Best Regards,
Balint

Hello,

does this help?

solutionExpireTest2.gh (3.5 KB)

1 Like

Hi, well, kind of :frowning:

Sorry if I was not clear enough, but I got to the threading part,the main problem is how to make sure, that the component also waits for the background thread to finish, before reporting itself as solved.

My problem is about how to handle the output parameters, and the components downstream while we give control back from SolveInstance()

I have attached a simplistic test component, where I tried various methods.

Currently, I use a Task to run in the background, which seems good in simple cases, but trips up when chaining these components. (see at bottom for the custom component code)

As long as it is only one component, or parallel components, it seems to work alright.
The component gives back control to the UI, and after the computing task is finished in the background, updates it’s outputs with the results, and updates any other components downstream. So far so good.

But as soon as I try to chain these type of components together, the ones downstream lock up the canvas, and fail to give out a runtime message about background computing. (Eventually they too reach the correct output.)

GHLibraryTest.gha (15 KB)
async_test.gh (9.5 KB)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using System.Threading;

using Grasshopper.Kernel;

namespace GHLibraryTest
{
    public class AdvancedParametersExample : GH_Component
    {
        public AdvancedParametersExample() : base("Trigonometric values", "TrigVal", "Computes sine, socine and tan values", "Extra", "Test")
        {
        }

        public override Guid ComponentGuid
        {
            get
            {
                return new Guid("2757f7fb-68e2-4986-809a-aa251e286570");
            }
        }

        protected override void RegisterInputParams(GH_InputParamManager pManager)
        {
            pManager.AddNumberParameter("Angle", "A", "The angle to measure", GH_ParamAccess.item);
            pManager.AddBooleanParameter("Radians", "R", "Work in radians", GH_ParamAccess.item, true); // default, we assume radians.
        }

        protected override void RegisterOutputParams(GH_OutputParamManager pManager)
        {
            pManager.AddNumberParameter("Sin", "sin", "The sine of the angle", GH_ParamAccess.item);
            pManager.AddNumberParameter("Cos", "cos", "The cosine of the angle", GH_ParamAccess.item);
            pManager.AddNumberParameter("Tan", "tan", "The tangent of the angle", GH_ParamAccess.item);
        }

        protected override void SolveInstance(IGH_DataAccess DA)
        {
            if (skip)
            {
                DA.SetData(0, sin);
                DA.SetData(1, cos);
                DA.SetData(2, tan);
                skip = false;
                return;
            }
            // Define variables to hold inputs, with initial values
            double angle = double.NaN;
            bool radians = true;

            // Collect input parameters, abort if unsuccesfull
            if (!DA.GetData(0, ref angle))
            {
                return;
            }
            if (!DA.GetData(1, ref radians))
            {
                return;
            }
            if (!Rhino.RhinoMath.IsValidDouble(angle))
            {
                return;
            }

            // Convert to degrees if needed
            if (radians == false)
            {
                angle = Rhino.RhinoMath.ToRadians(angle);
            }
            this.angle = angle;
            this.DA = DA;
            DA.DisableGapLogic(); // required to prevent a tree output, with nulls on one branch, and the outputs filled in by bg process on another branch NOTE: propably should be only set in a case where we are sure to run the bg process.
            Task computeTask = new Task(calculate);
            computeTask.ContinueWith(task =>
            {
                if (task.Status == TaskStatus.RanToCompletion)
                {
                    skip = true;
                    ExpireSolution(true);
                }
                else
                {
                    Grasshopper.Instances.RedrawAll();
                }
            },
            TaskScheduler.FromCurrentSynchronizationContext());
            computeTask.Start();
            Grasshopper.Instances.RedrawAll();
        }

        private double angle;
        private double sin;
        private double cos;
        private double tan;
        private bool skip;
        private IGH_DataAccess DA;

        private void calculate()
        {
            this.AddRuntimeMessage(GH_RuntimeMessageLevel.Remark, "background work...");
            sin = Math.Sin(angle);
            Thread.Sleep(5000);
            cos = Math.Cos(angle);
            Thread.Sleep(5000);
            tan = Math.Tan(angle);
            // Produce output values
            DA.SetData(0, sin);
            DA.SetData(1, cos);
            DA.SetData(2, tan);
            skip = true;
            ExpireDownStreamObjects();
            ClearRuntimeMessages();
        }
    }
}

First of all, I know its not quite what you have been expected. One important aspect of my code was that “BeginInvoke” part. If you want to wait for a thread to complete, then you can split into two solution runs.
The first solution run returns a null and starts the worker thread. Solution is completed you can work with GH as usual.
Once the Worker is finished the worker invokes a solution recalculation and passes data to a field. Being in the second run you return whats in these fields. Does this makes sense?

It is basically what I do, first solution calls the async task, then the task saves the result into fields, including setting the bool skip field to true, then calls ExpireSolution. The second time SolveInstance gets called, it check the skip field, and if the task set it true, it just skips solving, and puts out the results to the component outputs.

And this works brilliantly until there is only one such component, and all components downstream are “classical” but as soon as I try to chain 2 together, the second one freezes up the UI until it finishes.

I tried with threads directly like in your example, but ran into the same problem.

I’ve got a lot of love for you right now!! :smiley: This might not have done the trick for you because of the ‘daisy-chaining’ limitation, but for the components I am working on this is perfect (at least for now…).

Thanks a lot!

Also…Does anyone know if a better way to do this already exists? I also checked @TomTom’s example, which is very cool ‘telemetry’ like, but I fail to see how I could assign my components output from there. Maybe attaching the ‘DA’ entity on the Worker instead of the GHDoc?

Anyway… Thanks again!

Did you ever solve this? Running into the same issues.

No, in the end we went with a non async solution due to other problems in conjunction with this.

However if I needed to do something like this now, I would do it differently, with special GH_Goo types, where the value itself is the Task<T>, and then, downstream components of mine that would use this would create yet more Tasks based on the input ones. Of course this only works when your components are not used as inputs for non Task based ones. The moment you have to cast your GH_Goo<Task<T>> to something like a GH_Number, you will have to await the task and lock the UI.

Maybe I will try to write something concrete up later, because I just remembered this problem and feel like it should be possible to get it right.

Thanks for the quick response.

My current solution is as you did, await the Tasks, expire downstream, but then have to let several components fight and wait for the dust to settle!

Out of interest how did you implement with blocking? Tbh I’m struggling with that a little as well.
Blocking implementations I have seen take in a data tree instead of letting Grasshopper work out the permutations and then handle that themselves, which seems like a lot of effort and I had hoped there was a better way around it!

I’m afriad Grasshopper’s solver doesn’t support such thing.

1 Like

Just to throw in some additional information.

The absolute lowest level of thread synchronization can be achieved by using AutoResetHandles or Mutexes in C#. The problem is that in order to synchronize to the Gui thread, it has to be in some sort of idle state.This doesn‘t work if the main solution computation works on the GUI thread. Therefore an approach would be to finish the solution and restart it once the loading has been finished. This is really difficult to achieve, this would mean you‘ll need to recompute the whole solution with all the drawbacks and headaches. I personally would do it as the romans do it. Use the Gui thread in Grasshopper and live with the flaw in the design. Ui stays blocked, so what…

Not sure what you mean by blocking? The components are blocking by default you do not have to do anything special in that case. Taking in the entire datatree is only needed when you want to do something with all the data all at once (eg you want to write a file with all the data, and can’t just append the file)

What I have been finding is that if I have an async method that i just call .Result on it locks up and never comples. I presume this is what @TomTom is referring to? Is there a simple way around this?