AMX When does DEFINE_PROGRAM run (or, why loops in mainline are bad)
Table of Contents
Understanding this process can explain programs with abnormally high CPU usage and how to fix them.
There are four conditions that cause the NetLinx master to run DEFINE_PROGRAM:
- An unhandled event occurs
- A variable is written to (this is the CPU usage culprit)
- The 'run occasionally anyway' timer fires (~1/second)
- The event queue has become empty Unhandled Events.
Unhandled Events
DEFINE_PROGRAM runs when an unhandled event occurs, which ensures that channel- or level-based feedback is up to date. It also aides backwards compatibility by allowing SYSTEM_CALLs and mainline PUSH and RELEASE statements to run. To understand unhandled events, consider the following code:
BUTTON_EVENT[dvTP,123]
{
PUSH:
{
PULSE[dvRelay,1]
}
}
When someone presses button 123, there are 3 unhandled events and 1 handled event. The button press has been handled by PUSH, but the RELEASE, channel ON, and eventual channel OFF are not handled. The result is that DEFINE_PROGRAM runs far more often than the button push in DEFINE_EVENT.
Normally, this is not a large concern. (A user can only poke the system so fast). However, if want to, you can prevent this by adding empty BUTTON_EVENTs and CHANNEL_EVENTS, like this:
BUTTON_EVENT[dvTP,0]
{
PUSH:{}
RELEASE:{}
}
CHANNEL_EVENT[dvRelay,0]
{
ON:{}
OFF:{}
}
Now, all channel-related events will be handled and DEFINE_PROGRAM will not run for these events.
Variable is Written To
The second condition (a variable being written to) is the culprit for abnormally high CPU usage. The intent behind this trigger for DEFINE_PROGRAM is to more accurately display feedback in a timely fashion. Since many people use DEFINE_PROGRAM to set button feedback with statements like:
[dvTP,201] = (nCurrentInput == 1)
It makes sense to run DEFINE_PROGRAM if any change is detected in the states of any variable in the program. Normally, this is a very beneficial process. The problem comes with using loops to set feedback. This code will cause high CPU usage:
DEFINE_VARIABLE
VOLATILE INTEGER INC
DEFINE_PROGRAM
FOR(INC=1;INC<=8;INC++)
{
[dvTP,200+INC] = (nCurrentInput == INC)
}
It is not the loop itself that is the problem. It is the global variable INC being incremented that causes the issue. Since we've written to a variable, DEFINE_PROGRAM will want to run again. If there are no other events waiting in the queue, it will do so immediately. Of course, when it runs again, it will set itself up to run yet again.
The net result is that any time the processor would normally spend in an idle state is now consumed by repeatedly running DEFINE_PROGRAM. This does not interfere with processing events that come in, as they are given priority. The only speed penalty that is incurred is that the next incoming event can only be processed when the current pass of DEFINE_PROGRAM is finished. If you have an exceedingly long DEFINE_PROGRAM, it will slow event processing down.
The usual message conveyed with loops in mainline is “DON'T", but the code above can be fixed quite easily. All we need is a variable that won't be around when DEFINE_PROGRAM ends.
DEFINE_PROGRAM
{
INTEGER INC
FOR(INC=1;INC<=8;INC++)
{
[dvTP,200+INC] = (nCurrentInput == INC)
}
}
There are three things to note here:
- We can arbitrarily 'compound' statements by placing them in braces.
- Compounding allows us to define a local scope variable (they must be the first thing in a compound statement, before any executable code).
- The default local scope variable behavior is STACK_VAR, which is released once you leave that block of code.
Since the variable is destroyed upon exiting the code, no variables are left in the dirty state. No dirty variables means no reason to run DEFINE_PROGRAM.
If we were to use a LOCAL_VAR instead, we would be back in a high CPU usage state as a LOCAL_VAR is non-volatile. It keeps its value between uses and is still around and 'written to' once DEFINE_PROGRAM is done.
If you simply must use a LOCAL_VAR or a global-scope variable, there is still a way to salvage most of the CPU usage. If you employ a WAIT, you can control how often the feedback runs.
DEFINE_PROGRAM
WAIT 1
{
LOCAL_VAR INTEGER INC
FOR(INC=1;INC<=8;INC++)
{
[dvTP,200+INC] = (nCurrentInput == INC)
}
}
Now, no matter how often DEFINE_PROGRAM is compelled to run, the feedback will only run 10 times per second. A particular WAIT in your NetLinx code can only be put in the WAIT list once at any given time. This makes it a great choice for periodic functionality.
The 'Run Occasionally Anyway' Timer
To make sure that any feedback statements in DEFINE_PROGRAM are enforced eventually, there is a timer that fires every second that compels DEFINE_PROGRAM to run. This is given priority over event processing. The once per second mode can be proven easily. Just write this as the only line in the program:
DEFINE_PROGRAM
SEND_STRING 0,'DEFINE_PROGRAM JUST RAN'
Turn on Diagnostics in NetLinx Studio and you will find that it occurs roughly once per second. The once per second fallback can cause one very large problem in one very specific situation. If you manage to write a DEFINE_PROGRAM section that takes more than one whole second to run (~400,000,000 machine instructions on a current master) then you can actually stop processing any events.
The event queue servicing becomes starved. When the 'run anyway' timer expires, it has the highest priority of any of the triggers. If it fires before finishing the last 'run anyway' DEFINE_PROGRAM run, it will simply run again. If this happens every run of DEFINE_PROGRAM, no events will be processed and the master will appear to be locked up.
In practice, DEFINE_PROGRAM should never run this long. If you are in a situation where you must process this much information, you should consider making one iteration of a loop with each pass of DEFINE PROGRAM.
Instead of this:
DEFINE_PROGRAM
FOR(INC=1;INC<=4000000000;INC++)
{
// DO SOMETHING AWFUL WITH THE UNSIGNED LONG INC
}
Do this:
DEFINE_PROGRAM
INC = INC MOD 4000000000 // FORCE THE RANGE OF 0-3999999999 INC++ // THEN ADD ONE
/// DO THE SAME HORRIBLE THING, BUT ONLY 1/PASS OF DEFINE_PROGRAM
Of course, if you have reached the point where DEFINE_PROGRAM takes longer than a second to run, you are past the point of needing another master on the job or re-evaluating your approach of the problem.
The Empty Event Queue
The final reason that DEFINE_PROGRAM will be run is when all the events that have come in have been processed. There are two reasons we should not care about this.
- When it occurs, our system is, by definition, not busy.
- In busier systems, this occurs with decreasing frequency.