Trying to debounce layerstate events in a listener

Hi there –

I have written a layer listener to get granular information out of the RhinoDoc.LayerTableEvent. I added some code to attempt to debounce the results, since certain operations in Rhino cause a large number of events to fire in quick succession (deleting 10 layers, for instance) – and I only want one event to cause a downstream solution in Grasshopper.

Here’s the thing – it works pretty well in a small test definition. But occasionally the debounce stops working entirely, and occasionally I get this very weird GH canvas meltdown, where everything starts lagging (pan, zoom, wire connections). The additional problems occur in my massive 26,000 component definition: certain event types are not arriving properly, specifically events that report that the current active layer has changed. When I do something different (like change visibility) the event shows up and then the visibility event comes on its heels. If I perform an active layer change and then rewire an output from the listener, it updates. There is very little difference in the code between one event and another (Deleted vs. Active).

Here are the most relevant portions of the code:

public override void ExpireSolution(bool recompute)
    {
        this.m_expiring = true; // At the end of solveInstance, this is set to false;
        base.ExpireSolution(recompute);
    }

void RhinoDoc_LayerTableEvent(object sender, Rhino.DocObjects.Tables.LayerTableEventArgs e)
    {

        int del = 250;

        bool isExpiring = this.m_expiring;

        if (!isExpiring)  // prevent scheduling a solution during an existing solution
        {
            GH_Document gh_doc = OnPingDocument();

            if (gh_doc == null) return;



            ev = e;
            if (e.OldState != null) OldState.CopyAttributesFrom(e.OldState); // avoid exception when Deleting Layers
            if (e.NewState != null) NewState.CopyAttributesFrom(e.NewState); // avoid random BORKS (never happened yet, just to be safe)

            if (debounce)
            {
                ready = true; // event has arrived, time to activate the value getter in SolveInstance
                gh_doc.ScheduleSolution(del, ScheduleCallback);

            } else
            {
                ready = true; // event has arrived, time to activate the value getter in SolveInstance
                this.ExpireSolution(true);
            }

        }


    }

    private void ScheduleCallback(GH_Document doc)

    {

        this.ExpireSolution(false);

    }

Thanks,
Marc

Tried wrapping with my debounce boolean instead of isExpiring (since ScheduleSolution should handle canceling scheduled solutions when a new schedule arrives) – but still no luck. I still have some events (specifically when the active layer changes) that get stuck in the hopper and the outputs never get expired… the new code:

void RhinoDoc_LayerTableEvent(object sender, Rhino.DocObjects.Tables.LayerTableEventArgs e)
    {

        int del = 250;
        GH_Document gh_doc = OnPingDocument();

        if (debounce)
        {
            ev = e;
            if (e.OldState != null) OldState.CopyAttributesFrom(e.OldState); // avoid exception when Deleting Layers
            if (e.NewState != null) NewState.CopyAttributesFrom(e.NewState); // avoid random BORKS (never happened yet, just to be safe)

            ready = true; // event has arrived, time to activate the value getter in SolveInstance
            gh_doc.ScheduleSolution(del, ScheduleCallback);

        }
        else
        {
        
        bool isExpiring = this.m_expiring;

        if (!isExpiring)  // prevent scheduling a solution during an existing solution
        {


            if (gh_doc == null) return;



            ev = e;
            if (e.OldState != null) OldState.CopyAttributesFrom(e.OldState); // avoid exception when Deleting Layers
            if (e.NewState != null) NewState.CopyAttributesFrom(e.NewState); // avoid random BORKS (never happened yet, just to be safe)

            ready = true; // event has arrived, time to activate the value getter in SolveInstance
            this.ExpireSolution(true);


        }
        }

    }

Any ideas on what could be happening here?

Thanks,
Marc

Is this question too complex? Not enough information to follow up?

I am still having this problem in a very large definition. Outputs are not being expired properly. One extra note is that if I change the active layer in Rhino, the Layer listener does not report the event – but if I do anything in Rhino (like select a Rhino object), the outputs then update with the correct information.

Any ideas on where to start looking?

Thanks,
Marc

Some updates –

It appears that the solution is being scheduled properly, but the callback never fires. I’ve tried adjusting the schedule delay all the way down to 5ms or even 0ms, no difference. This only happens with the “Current” e.EventType – renaming or other modifications work fine.

With a “Current” event type, the schedule is simply added to some kind of queue (or otherwise blocked), and then once a single action is taken in Rhino/GH (like changing the tool selection or selecting an object, or placing a component) all the scheduled solution callbacks that were blocked up fire in succession.

Really struggling to get to the bottom of this, as my code is not showing anything obvious in terms of the differences between the “Current” event type and any others… going to go with the nuclear option and tag you @DavidRutten on this one (sorry, I try to use that only as a last resort).

Thanks,
Marc

EDIT: Some other interesting (unwanted behavior) of the RhinoDoc.LayerTableEvent… it is firing when a new object is created and also when objects are moved, with an EventType of “Sorted”. Weird.

Sorry to be a pest, but this problem is really plaguing me – any thoughts @stevebaer / @dale?

P.S. I have since reproduced the problem in a much smaller (2900 components) but still complex definition. I am starting to think that it may have something to do with Telepathy, which we use extensively and has shown a similar behavior in large definitions (updates to the solution hang until some further UI interaction).

Up until now, the issue with Telepathy hasn’t been a problem because it is largely updated while building a definition, not while using it – but I think there might possibly be something in the Telepathy code which is preventing the ScheduleSolution in my layer listener from completing.

Not sure how to debug this or help you get to a solution, but I’m willing to do some live debugging if you are.

Thanks,
Marc

Tried invoking the ScheduleSolution method on the UI thread with the following code:

System.Windows.Forms.Control control = Grasshopper.Instances.ActiveCanvas;
            if (control == null)
                return;
            Action delayedSolution = () => { gh_doc.ScheduleSolution(del, ScheduleCallback); };
            control?.Invoke(delayedSolution);

No luck there either. Callback doesn’t fire until another GH action is taken.

Hi @marcsyp,

I am not familiar enough with the GH API to help.

@DavidRutten, is this something you can help with?

– Dale

Thanks Dale –

I managed to get around this finally by ditching ScheduleSolution entirely and using a custom-built debouncer class I found on the webs:

                // https://weblog.west-wind.com/posts/2017/Jul/02/Debouncing-and-Throttling-Dispatcher-Events#PostTitle
            debounceTimer.Debounce(150, (p) =>
            {
                this.ExpireSolution(true);
            });

So far so good, but I have no idea if this is a safe solution – it’s being invoked inside an event handler (potentially off the UI thread?). Would love to know more about what’s going on here, for instance if I’m making an obvious mistake somewhere.

Thanks,
Marc

Sorry, I totally missed this discussion. I’ll have a look once at the office. The scheduler does some unexpected things which may the cause of the problem.

1 Like