Nasal optimisation

From FlightGear wiki
Jump to navigation Jump to search


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;

  1. Measure and locate (profile) code.
  2. Do less
  3. Cache more
  4. Spread out workload
  5. 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)

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;

Emesary real time executive

The following code can be used to provide a basic and yet sophisticated way to run Nasal code in a scheduled (per frame or at a suitable rate) and in a managed way (i.e. something knows which modules are registered)

Each module that wish to be executed needs to implement itself as a receiver or have a receiver wrapper (as follows). Generally I prefer to use a receiver wrapper as it helps to isolate the code that does the work from the code that interfaces to the scheduler.

var ModuleRecipient =
{
    new: func(_ident)
    {
        var new_class = emesary.Recipient.new(_ident);
        new_class.Object = nil;
        new_class.Receive = func(notification)
        {
            if (notification.NotificationType == "FrameNotification")
            {
                if (new_class.Object == nil)
                  new_class.Object = ModuleObject.new();

                if (!math.mod(notifications.frameNotification.FrameCount,2)){
                    new_class.Object.update(notification);
                }
                return emesary.Transmitter.ReceiptStatus_OK;
            }
            return emesary.Transmitter.ReceiptStatus_NotProcessed;
        };
        return new_class;
    },
};

emesary.GlobalTransmitter.Register(ModuleRecipient.new("MODULE-NAME"));

This is the code that is used in the F-15 (Nasal/M_exec.nas) that provides the simple real time frame based executive.

 #---------------------------------------------------------------------------
 #
 #	Title                : Emesary based real time executive
 #
 #	File Type            : Implementation File
 #
 #	Description          : Uses emesary notifications to permit nasal subsystems to
 #                       : be invoked in a controlled manner.
 #
 #	Author               : Richard Harrison (richard@zaretto.com)
 #
 #	Creation Date        : 4 June 2018
 #
 #	Version              : 1.0
 #
 #  Copyright (C) 2018 Richard Harrison           Released under GPL V2
 #
 #---------------------------------------------------------------------------*/

# to add properties to the FrameNotification simply send a FrameNotificationAddProperty
# to the global transmitter. This will be received by the frameNotifcation object and
# included in the update.
#emesary.GlobalTransmitter.NotifyAll(notifications.FrameNotificationAddProperty.new("MODULE", "wow","gear/gear[0]/wow"));
#emesary.GlobalTransmitter.NotifyAll(notifications.FrameNotificationAddProperty.new("MODULE", "engine_n2", "engines/engine[0]/n2"));
#    


#
# real time exec loop.
var frame_inc = 0;
var cur_frame_inc = 0.03;

var rtExec_loop = func
{
    #    
    notifications.frameNotification.fetchvars();

    if (!notifications.frameNotification.running){
        return;
    }
    notifications.frameNotification.dT = notifications.frameNotification.elapsed_seconds - notifications.frameNotification.curT;

    if (notifications.frameNotification.dT > 1.0) 
      notifications.frameNotification.curT = notifications.frameNotification.elapsed_seconds;

    if (notifications.frameNotification.FrameCount >= 16) {
        notifications.frameNotification.FrameCount = 0;
    }
    emesary.GlobalTransmitter.NotifyAll(notifications.frameNotification);
    #    

    notifications.frameNotification.FrameCount = notifications.frameNotification.FrameCount + 1;

    # adjust exec rate based on frame rate.
    if (notifications.frameNotification.frame_rate_worst < 5) {
        frame_inc = 0.25;#4 Hz
    } elsif (notifications.frameNotification.frame_rate_worst < 10) {
        frame_inc = 0.125;#8 Hz
    } elsif (notifications.frameNotification.frame_rate_worst < 15) {
        frame_inc = 0.10;#10 Hz
    } elsif (notifications.frameNotification.frame_rate_worst < 20) {
        frame_inc = 0.075;#13.3 Hz
    } elsif (notifications.frameNotification.frame_rate_worst < 25) {
        frame_inc = 0.05;#20 Hz
    } elsif (notifications.frameNotification.frame_rate_worst < 40) {
        frame_inc = 0.0333;#30 Hz
    } else {
        frame_inc = 0.02;#50 Hz
    }
    if (frame_inc != cur_frame_inc) {
        cur_frame_inc = frame_inc;
    }
    execTimer.restart(cur_frame_inc);
}

# setup the properties to monitor for this system
  input = {
           frame_rate                : "/sim/frame-rate",
           frame_rate_worst          : "/sim/frame-rate-worst",
           elapsed_seconds           : "/sim/time/elapsed-sec",
          };

foreach (var name; keys(input)) {
    emesary.GlobalTransmitter.NotifyAll(notifications.FrameNotificationAddProperty.new("EXEC", name, input[name]));
}

setlistener("sim/signals/fdm-initialized", func {
    notifications.frameNotification.running = 1;
});

notifications.frameNotification.running = 0;
notifications.frameNotification.dT = 0; # seconds
notifications.frameNotification.curT = 0;

var execTimer = maketimer(cur_frame_inc, rtExec_loop);
execTimer.simulatedTime = 1;
setlistener("/sim/signals/fdm-initialized", func {
    print("M_exec: starting");
    execTimer.start()
});

#
# Turn on the automatic overrun detection - this will notify on the console
# if any recipient takes more than the allocated (9ms) amount of time
emesary.GlobalTransmitter.OverrunDetection(9); # warn at 9ms

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)
    {
...
       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.

     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){
                                    proces 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.
                                });