Nasal optimisation
The FlightGear forum has a subforum related to: Nasal Scripting |
Nasal scripting |
---|
Nasal internals |
---|
Memory Management (GC) |
Introduction to writing Nasal for optimum performance
Writing optimal performing code in a real time system (in any language) is usually related to minmising the per frame impact of any given set of code; which may end up with slightly slower code but the point is to ensure that the real time frame rate does not degrade.
The basic techniques are as follows;
- Measure and locate (profile) code.
- Do less
- Cache more
- Spread out workload
- Avoid garbage collection
Inside each Nasal frame the programmer needs to ensure that the minimum necessary work is performed; ideally a predictable workload to ensure that Nasal processing per frame is constant - because otherwise there can be frame pauses - which is where an occasional frame will take much longer than usual.
Splitting any given task across frames is the normal way of ensuring that performance remains good; unfortunately a lot of Nasal doesn't do this and instead will process everything each frame. This is what the PartitionProcessor is designed to help with
Do not use getprop or setprop - these are very slow; instead use the props.nas module to refer to the property tree.
All property tree access is much slower than using native Nasal variables; and it is therefore wise to only read each property from the tree once per frame. This is what the FrameNotification is designed to help with.
Writing data to Canvas displays can be considered to be a slow operation; so only write values that are changed. props.UpdateManager can help with this.
Generally using props.UpdateManager (with the FrameNotification hash) can be generally used to perform OnChange processing that is more efficient than the property listeners - however property listeners should still be used to perform occasional actions (rather than per frame actions)
Using the built in performance monitor
You can do this from the debug menu Monitor System Performance and currently the GUI only shows the top level.
When the performance monitor is active either by having the GUI on display or because /sim/performance-monitor/enabled
is true overruns will be logged at ALRT level. An overrun is defined as exceeding/sim/performance-monitor/max-time-per-frame-ms
).
Setting /sim/performance-monitor/dump-stats
to non-zero will cause detailed performance info will be output to the log.
Note that it is generally a good idea not to use the integrated UI to display these stats (aka no PUI/Canvas). Instead, consider using Phi or the telnet/props interface, so that performance isn't degraded even more by displaying all these stats via the main loop.
fdm-initialized listener
It is important to realise that sim/fdm-initialized will be set to true every time the model is repositioned; i.e. it does not fire just once. When sim/fdm-initialized is set to false the model should clean up all previously initialised listeners, threads, timers etc.
Generally it is much better to set timers, listeners (etc) at module level scope inside any Nasal file; as these will be executed only once. Using the Emesary FrameNotification can further simplify per frame processing because there is no need to use a timer per module and instead any modules can register themselves for frame processing simply by using GlobalNotifier.Register and all of the details are abstracted away and all that needs to happen is to handle the received notifications.
This is an example of acquiring and releasing listeners and timers.
var l1 = nil;
var timer_loopTimer = nil;
var fdm_init = func(v) {
# always clean up values that aren't nil - this is safe
if (l1 != nil)
removelistener(l1);
l1 = nil;
if (timer_loopTimer != nil)
timer_loopTimer.stop();
timer_loopTimer = nil;
# then acquire new values
if (v.getValue()){
print("Initializing Systems");
l1 = setlistener("/some-prop", listener_func);
timer_loopTimer = maketimer(0.25, timer_loop);
timer_loopTimer.start();
}
else {
# now perform any other cleanup.
print("Cleaning up");
}
}
setlistener("sim/signals/fdm-initialized", fdm_init);
However the above can be better written as follows;
var l1 = setlistener("/some-prop", listener_func);
var timer_loopTimer = maketimer(0.25, timer_loop);
var fdm_init = func(v) {
if (v.getValue()){
timer_loopTimer.start();
}
else {
timer_loopTimer.stop();
}
}
setlistener("sim/signals/fdm-initialized", fdm_init);
Nasal threads
Nasal threads are of limited use because most of the time it is property tree access and other API calls that are slow and a lot of the API library (including properties) are not thread safe and should not be called from outside the main loop.
Optimisation techniques
The F-15 I think is the reference implementation of my three main Nasal optimisation techniques;
Nasal Profiling
As yet there isn't a profiler available so instead there is an OperationTimer that is ships as part of emexec (2020.3 or later released after Aprial 2022).
This makes it easy to output (via logprint) information about how much time a module is taking.
To create an instance of a timer there are two parameters, the first is the ident and the second the log level (3=info, 2=debug)
var ot = emexec.OperationTimer.new("VSD",2);
The output is a cumulative number of milliseconds since the OperationTimer was reset.
ot.reset();
ot.log("Start");
<some code to time>
ot.log("1");
<more code>
ot.log("ed");
Emesary real time executive
Starting from 2020.3 released after April 2022 FGdata includes the emexec module which provides an easy to use object scheduler that uses Emesary to do most of the work.
By default it is recommended to use the built in scheduler emexec.ExecModule
The scheduler will adjust the frequency from 50hz right down to 4hz.
Any object that wishes to be invoked simply needs to provide an update(notification)
method and register itself via ExecModule.register method.
The register method takes the following arguments
- ident - text to identify this object registration
- properties_to_monitor : a key value pair hash of properties to include in the notification. This helps to optimise property tree access (see FrameNotification below for the format)
- object : an instance of an objec that has an update(notification) method
- rate : frame skip rate (1/rate is the effective rate)
- frame_offset : frame skip offset. Must be less than rate and when used with rate it can allow interleaving of modules (usually for performance)
During development it would be usual to set the overrun detection active emexec.ExecModule.transmitter.OverrunDetection(9) for a warning (log leve info) of any frame that exceeds 9 ms
FrameNotification
The FrameNotification is a notification sent out at defined intervals that contains a hash with property values.
Use the FrameNotificationAddProperty to request that certain properties are contained within the FrameNotification.
notifications.FrameNotificationAddProperty.new("F15-HUD", **[HASH-Name]**, **[property-string]**)
e.g.
emesary.GlobalTransmitter.NotifyAll(notifications.FrameNotificationAddProperty.new("F15-HUD", "AirspeedIndicatorIndicatedMach", "instrumentation/airspeed-indicator/indicated-mach"));`
although more typically used by having a hash that contains the keys and properties that you want to monitor and then using a loop to send out the FrameNotificationAddProperty; e.g.:
input = {
AirspeedIndicatorIndicatedMach : "instrumentation/airspeed-indicator/indicated-mach",
Alpha : "orientation/alpha-indicated-deg",
AltimeterIndicatedAltitudeFt : "instrumentation/altimeter/indicated-altitude-ft",
ArmamentAgmCount : "sim/model/f15/systems/armament/agm/count",
ArmamentAim120Count : "sim/model/f15/systems/armament/aim120/count",
ArmamentAim7Count : "sim/model/f15/systems/armament/aim7/count",
ArmamentAim9Count : "sim/model/f15/systems/armament/aim9/count",
ArmamentRounds : "sim/model/f15/systems/gun/rounds",
AutopilotRouteManagerActive : "autopilot/route-manager/active",
AutopilotRouteManagerWpDist : "autopilot/route-manager/wp/dist",
AutopilotRouteManagerWpEtaSeconds : "autopilot/route-manager/wp/eta-seconds",
CadcOwsMaximumG : "fdm/jsbsim/systems/cadc/ows-maximum-g",
ControlsArmamentMasterArmSwitch : "sim/model/f15/controls/armament/master-arm-switch",
ControlsArmamentWeaponSelector : "sim/model/f15/controls/armament/weapon-selector",
ControlsGearBrakeParking : "controls/gear/brake-parking",
ControlsGearGearDown : "controls/gear/gear-down",
ControlsHudBrightness : "sim/model/f15/controls/HUD/brightness",
ControlsHudSymRej : "sim/model/f15/controls/HUD/sym-rej",
ElectricsAcLeftMainBus : "fdm/jsbsim/systems/electrics/ac-left-main-bus",
HudNavRangeDisplay : "sim/model/f15/instrumentation/hud/nav-range-display",
HudNavRangeETA : "sim/model/f15/instrumentation/hud/nav-range-eta",
OrientationHeadingDeg : "orientation/heading-deg",
OrientationPitchDeg : "orientation/pitch-deg",
OrientationRollDeg : "orientation/roll-deg",
OrientationSideSlipDeg : "orientation/side-slip-deg",
RadarActiveTargetAvailable : "sim/model/f15/instrumentation/radar-awg-9/active-target-available",
RadarActiveTargetCallsign : "sim/model/f15/instrumentation/radar-awg-9/active-target-callsign",
RadarActiveTargetClosure : "sim/model/f15/instrumentation/radar-awg-9/active-target-closure",
RadarActiveTargetDisplay : "sim/model/f15/instrumentation/radar-awg-9/active-target-display",
RadarActiveTargetRange : "sim/model/f15/instrumentation/radar-awg-9/active-target-range",
RadarActiveTargetType : "sim/model/f15/instrumentation/radar-awg-9/active-target-type",
InstrumentedG : "instrumentation/g-meter/instrumented-g",
VelocitiesAirspeedKt : "velocities/airspeed-kt",
};
foreach (var name; keys(input)) {
emesary.GlobalTransmitter.NotifyAll(notifications.FrameNotificationAddProperty.new("F15-HUD", name, input[name]));
}
The idea of the FrameNotification is that it provides an easy way to run your update logic; simply by registering with the GlobalTransmitter and then processing the FrameNotification you can call your update logic in an efficient manner;
e.g.
var HUDRecipient =
{
new: func(_ident)
{
var new_class = emesary.Recipient.new(_ident);
new_class.MainHUD = nil;
new_class.Receive = func(notification)
{
if (notification.NotificationType == "FrameNotification")
{
if (new_class.MainHUD == nil)
new_class.MainHUD = F15HUD.new("Nasal/HUD/HUD.svg", "HUDImage1");
if (!math.mod(notifications.frameNotification.FrameCount,2)){
new_class.MainHUD.update(notification);
}
return emesary.Transmitter.ReceiptStatus_OK;
}
return emesary.Transmitter.ReceiptStatus_NotProcessed;
};
return new_class;
},
};
and then register that function with the Emesary Global transmitter.
emesary.GlobalTransmitter.Register(HUDRecipient.new("F15-HUD"));
props.UpdateManager
This will monitor one or more property, or values from a hash and call a function (or inline function) when the value or a value changes more than a defined amount.
e.g.
obj.update_items = [
props.UpdateManager.FromHashList(["ElectricsAcLeftMainBus","ControlsHudBrightness"] , 0.01, func(val)
{
if (val.ElectricsAcLeftMainBus <= 0
or val.ControlsHudBrightness <= 0) {
obj.svg.setVisible(0);
} else {
obj.svg.setVisible(1);
}
}),
props.UpdateManager.FromHashValue("AltimeterIndicatedAltitudeFt", 1, func(val)
{
obj.alt_range.setTranslation(0, val * alt_range_factor);
}),
props.UpdateManager.FromHashValue("VelocitiesAirspeedKt", 0.1, func(val)
{
obj.ias_range.setTranslation(0, val * ias_range_factor);
}),
props.UpdateManager.FromHashValue("ControlsHudSymRej", 0.1, func(val)
{
obj.symbol_reject = val;
}),
...
and then in the update method
foreach(var update_item; me.update_items)
{
update_item.update(notification);
}
As you can see above the update method is called from the recipient of the frame notification.
PartitionProcessor
When working with lists (arrays) of data that can get quite long (e.g. targets from radar) the partition processor is an easy way to only process a part of that list each frame.
What it does is to manage the processing of data in a manner suitable for real time operations. Given a data array [0..size] this will process a number of array elements each time it is called This allows for a simple way to split up intensive processing across multiple frames.
The limit is the number of elements to process per frame (invocation of .process method) or to limit the processing to a specified amount of time using a Nasal timestamp to measure the amount (in usec)
example usage (object);
var VSD_Device =
{
new : func(designation, model_element, target_module_id, root_node)
{
...
# new : func(_name, _size, _timestamp=nil)
obj.process_targets = PartitionProcessor.new("VSD-targets", 20, nil);
obj.process_targets.set_max_time_usec(500);
...
me.process_targets.set_timestamp(notification.Timestamp);
then invoke.
# process : func (object, data, init_method, process_method, complete_method)
me.process_targets.process(me, awg_9.tgts_list,
func(pp, obj, data){
initialisation; called before processing element[0]
params
pp is the partition processor that called this
obj is the reference object (first argument in the .process)
data is the entire data array.
}
,
func(pp, obj, element){
process individual element;
params
pp is the partition processor that called this
obj is the reference object (first argument in the .process)
element is the element data[pp.data_index]
return 0 to stop processing any more elements and call the completed method
return 1 to continue processing.
},
func(pp, obj, data)
{
completed; called after the last element processed
params
pp is the partition processor that called this
obj is the reference object (first argument in the .process)
data is the entire data array.
});