GH_Component execution logic¶
Working outside of the standardized component workflow, such as data damming, async..etc, is quite complicated, and IMO not very well documented. Here I attempt to document the process execution flow for future reference.
A component can be considered an object with input, solution, and output processes, each can have their own logics, a breif overview of the process looks like this:
Component recieves expire request
🡫
Component expires, and expires downstream
🡫
Collects upsteam data
🡫
Runs and finishes solution
🡫
Passes data to downstream
🡫
downstream starts their solutions
CheatSheet¶
*Input param excluded
Upstream State | Component Phase | Component Executing | Component Output Param | Downstream Phase |
---|---|---|---|---|
Starting new solution | Computed | - | - | Computed |
In solution | Collecting | ExpireDownStreamObjects() | ExpireSolution() Clears data |
Collecting |
Computed | Collected | BeforeSolveInstance() | - | " |
" | Collected | SolveInstance() Executed multiple times if passing list to GH_ParamAccess.item |
- | " |
" | Collected | - | CollectData() Applying Graft, Flatten...etc |
" |
" | Collected | AfterSolveInstance() | - | " |
" | Computed | - | - | Collected |
Damming Output¶
To dam an ouput, the SolveInstance would basically have two sections of code, the compute section and output section. By default execut the compute section, and when an ouput is required, expire the solution to run the ouput section. All data that needs outputing would have to be a persistant variable.
Pseudocode:
string data = ""
bool output = false;
SolveInstance(DA)
{
if(output)
{
DA.Set(0,data);
output = false;
}
else
{
// some compute logic...
data = computedData
if(outputRequired)
{
output = true;
//OnPingDocument().ScheduleSolution(5);
ExpireSolution(true);
}
}
}
ScheduleSolution
creates expires the solution and queues a new solution after x milliseconds. In theory it is thread-safe, but may be unpredictable, since there can only be one queue at any given time, say you queue for 100ms later, but another component created a queue for 1ms at the same time, the both solutions are calculated after 1ms.
ExpireSolution
expire the solution and recalculates immediately if true; false still expires, but doesn't request a recompute nor does it queue. If this is called while in the middle of a solution, exceptions will be thrown, it should only be called at the end of some compute logic.
Functionally speaking for data damming, ScheduleSolution
and ExpireSolution
is basically the same.
This however will still output data when output is false, it'll be null if you didn't set any data. for most usages this is probably not desirable, to prevent the output of null when output isn't set, there's 2 methods:
Method 1: Disable Gap Logic¶
One options if to use DisableGapLogic()
in the SolveInstance()
, this prevent the component from outputting null's, and treats the output as not valid/empty (as if the component didn't execute).
Pseudocode:
string data = ""
bool output = false;
SolveInstance(DA)
{
DA.DisableGapLogic();
if(output)
{
DA.Set(0,data);
output = false;
}
else
{
// some compute logic...
data = computedData
if(outputRequired)
{
output = true;
//OnPingDocument().ScheduleSolution(5);
ExpireSolution(true);
}
}
}
*Do note that depending on how the downstream object's logic works, it may execute despite your component not outputting anything, since ExpireDownStreamObject()
is still called (This may or may not be desirable).
Method 2: Override ExpireDownStreamObjects¶
Another method is to not expire the downstream components, this avoid the downstream object from executing, instead of relying on that component to recognize that the input is not valid and shouldn't execute.
Pseudocode:
string data = ""
bool output = false;
SolveInstance(DA)
{
if(output)
{
DA.Set(0,data);
output = false;
}
else
{
// some compute logic...
data = computedData
if(outputRequired)
{
output = true;
OnPingDocument().ScheduleSolution(5)
}
}
}
override ExpireDownStreamObjects()
{
if (output)
{
base.ExpireDownStreamObjects();
}
}
Since the clearing of data on the output param (ExpireSolution
) is called by base.ExpireDownStreamObjects
, by not doing anything in ExpireDownStreamObjects
when an output is not required, the out params keeps their previous values despite a new solution being computed.
Async/Background Task¶
Another situation where data control is important is when async computation is required. Instead of locking up the UI with a complex/lengthy computation, the compute can be done on a different thread in the background asynchronously, freeing up the UI. When the compute is finished, the component can then self-expire to output the result. Async in C# is relatively simple to define:
The real challenge here is thatExpireSolution
cannot be be called from a non-UI thread. To execute commands on the UI thread from a non-UI thread, InvokeOnUiThread can be used, such as:
public delegate void ExpireSolutionDelegate(bool recompute);
protected override void SolveInstance(IGH_DataAccess DA)
{
Action callback = () => RhinoApp.InvokeOnUiThread(new ExpireSolutionDelegate(ExpireSolution),true);
DoWork();
}
private void DoWork(Action callback)
{
Task.Run(() =>
{
// do something...
callback();
});
}
Pseudocode:
string data = ""
bool output = false;
SolveInstance(DA)
{
if(output)
{
DA.Set(0,data);
output = false;
}
else
{
Task.Run(() =>
{
// do something...
data = computedData
if(outputRequired)
{
output = true;
RhinoApp.InvokeOnUiThread((Action)delegate { ExpireSolution(true); });
}
});
}
}
override ExpireDownStreamObjects()
{
if (output)
{
base.ExpireDownStreamObjects();
}
}
Side Remark:
If only select output params needs to expire, instead of calling base.ExpireDownStreamObjects
which expires all the output params, a specific param can be expired using Params.Output[i].ExpireSolution(false);
Reference: Melanoplus | Idempotent
Reference: Dynamic Data Tools