Dieses Topic beinhaltet Erläuterung zu performanten und unperformanten Customizing.

Python-API

Effiziente Nutzung von search_record()/get_children()

Informationen

  • Die Python API-Funktionen ppms.search_record sowie DtpRecord.get_children besitzen einen di_list -Parameter.
  • Mit diesem Parameter werden die Dataitems, die von der API-Funktion geladen werden, eingeschränkt.

Unperformant

  • Wird eine leere Liste übergeben, werden alle Dataitems aus der entsprechenden Datentabelle geladen, selbst wenn auf diese Attribute nicht zugegriffen wird.
  • Dies führt zu einer erheblich längeren Laufzeit, um die entsprechenden Datensätze zu laden.
  • Je mehr Dataitems in der Tabelle vorhanden sind, desto länger dauert es, jedes Attribut zu laden.
from ppms import ppms

project_record = ppms.search_record(461, ['000041'], [])  # No restriction on the amount of dataitems to load
project_name = project_record.project_name.get_value()

total_remaining_effort = 0
for task_record in project_record.get_children(463):  # No restriction on the amount of dataitems to load
    remaining_effort = task_record.effort_rem.get_value()
    
    total_remaining_effort += remaining_effort
    
ppms.ui_message_box(project_name, 'The remaining effort is {}'.format(total_remaining_effort))
PY

Performant

  • Durch eine Einschränkung auf die relevanten Dataitems kann die Laufzeit des Makros verringert werden.
from ppms import ppms

project_record = ppms.search_record(461, ['000041'], ['project_name'])  # The dataitems to load are restricted to "project_name"
project_name = project_record.project_name.get_value()

total_remaining_effort = 0
for task_record in project_record.get_children(463, ['effort_rem']):  # The dataitems to load are restricted to "effort_rem"
    remaining_effort = task_record.effort_rem.get_value()
    
    total_remaining_effort += remaining_effort
    
ppms.ui_message_box(project_name, 'The remaining effort is {}'.format(total_remaining_effort))
PY

Wertebereiche

Datenbankzugriffe in computeOutput()

Teilabfragen in Wertebereichen

  • Ein klassisches Beispiel für ein unperformantes Customizing sind Datenbankabfragen innerhalb eines computeOutput() -Wertebereichs.
  • Der PLANTA-Server geht beim Laden eines Moduls wie folgt vor (vereinfacht):
    • Das Modul-Customizing wird analysiert, um festzustellen, welche Daten geladen werden müssen.
    • Die Daten werden, eingeschränkt auf die Filterkriterien, aus der Datenbank geholt.
    • Die Wertebereiche werden berechnet.
  • Befindet sich ein computeOutput() -Wertebereich auf einem der DIs im Modul, so muss dieser berechnet werden.
  • Die Berechnung findet für jeden Datensatz dieser Tabelle im Datenbereich statt.
  • Ein ppms.search_record() setzt eine Datenbankabfrage vom Server ab.
  • Da dies für jeden Datensatz geschehen muss, gibt es für den Datenbereich statt einer Datenbankabfrage, nun n-Abfragen, welche die Laufzeit verringern.

Hinweis

  • Viele computeOutput-Wertebereiche lassen sich einfach in computeSqlValueRanges umwandeln, was sich positiv auf die Laufzeit auswirken kann.

Korrekte Definition von Abhängigkeiten (Dependencies)

Kurzübersicht

  • Die Wertebereichsfunktionen computeOutput(), processInput() und checkInput() brauchen eine definierte Liste von Abhängigkeiten.
  • Die Abhängigkeiten werden bei der Ausführung des Wertebereichs mit in den DtpRecord des Dataitems gepackt.
  • Im Fall von computeOutput() findet die Berechnung jedesmal statt, wenn sich eine der Abhängigkeiten ändert.

Abhängigkeit "Stern" (*)

  • Ein computeOutput() -Wertebereich mit Abhängigkeit "Stern" versucht, sich so oft wie möglich neu zu berechnen.
  • Solange ein Dataitem mit Abhängigkeit "Stern" geladen ist, führt jedes Speichern , Rücksetzen und Filtern zu einer Neuberechnung, unabhängig davon, ob das Dataitem in dem Modul vorhanden ist.
  • Wenn kein Modul, indem das Dataitem vorkommt, mehr geöffnet ist, wird es nicht mehr neu berechnet.
  • Die ständige Neuberechnung kann bei komplexen Wertebereichen zu erheblich schlechterer Laufzeit führen.

Alternative

  • Wenn ein Dataitem sich neu berechnen soll, es aber keine DI-Abhängigkeit gibt, die man eintragen könnte, kann sich mit einer kleinen Umgehungslösung beholfen werden:
    • Man legt ein virtuelles Dataitem in der gleichen Tabelle als Ja/Nein-Feld an.
    • Dieses Dataitem wird als Abhängigkeit in das eigentliche Dataitem mit Wertebereich eingetragen.
    • Soll sich das Dataitem neu berechnen, so invertiert man einfach den Wert des virtuelles DIs (di.set_value(not di.get_value())).

Customizing

Datenbereiche customizen

Hinweis

  • Um Laufzeitproblemen vorzubeugen, ist es zu empfehlen beim Customizen der Datenbereiche pro Datenberich immer die Datenfelder aus derselben Datentabelle zu verwenden.

Datenbankabfragen minimieren

Die Latenz zwischen Datenbank- und Applikations-Server ist ein sehr wichtiger Punkt bei der Betrachtung von Performance.

  • Für jede Datenbankabfrage ist ein Round-Trip zwischen dem Applikations- und Datenbank-Server notwendig:
    • Der PLANTA-Server sendet eine Abfrage an die Datenbank.
    • Die Datenbank antwortet mit dem Ergebnis.
  • Bei sämtlichem Customizing soll man deshalb immer die Anzahl der Datenbankabfragen minimieren.

Das Problem sowie die Lösung können anhand des folgenden Beispiels verdeutlicht werden. Hierfür wurden

  • zwei Systeme miteinander verglichen:
    • System "Lokal": Datenbank und PLANTA-Server sind auf der gleichen Maschine installiert, zwischen den beiden herrscht weniger als 1 ms Latenz.
    • System "Remote": Datenbank und PLANTA-Server sind auf verschiedenen Maschinen installiert, zwischen den beiden herrscht eine Latenz von ungefähr 6 ms.
  • zwei verschiedene Python-Makros ausgeführt, um Daten aus der Datenbank zu holen und die Zeiten zu messen.
    • 1 Abfrage pro Eingabewert: Es wird über eine Liste von Ressourcen iteriert und für jede Ressource eine Abfrage an die Datenbank gesendet.
    • 1 Abfrage mit n-Eingabewerten: Es wird eine Liste von Ressourcen als Parameter in eine Abfrage formatiert und an die Datenbank gesendet.

Die folgenden Charts zeigen den Anstieg der Laufzeit im Vergleich zu einer Abfrage:

 


Folgendes Beispiel könnte "normaler" Code sein, den jemand ohne schlechtes Gewissen geschrieben hat

Folgendes Beispiel könnte "normaler" Code sein, den jemand ohne schlechtes Gewissen geschrieben hat

import time

from ppms import ppms

RESOURCES = ['R1', 'R8', 'R41']


def get_start_and_end_period_of_resource(resource_id):
    """Retrieve the start and end period of a resource"""
    resource_record = ppms.search_record(467, [resource_id], ['start_period', 'end_period'])
    
    start_period = resource_record.start_period.get_value()
    end_period = resource_record.end_period.get_value()
    
    return start_period, end_period

start = time.time()
for resource in RESOURCES:
    start_period, end_period = get_start_and_end_period_of_resource(resource_id=resource)
end = time.time()

execution_time = round(end - start, ndigits=6)
ppms.ui_message_box('Executing the code took {} seconds!'.format(execution_time))
PY
  • Für jede Ressource muss eine Datenbankabfrage (= ppms.search_record ) gesendet werden, welche im System mit höherer Latenz deutlich langsamer ist.
  • Wenn sich die Anzahl der abgefragten Ressourcen erhöht, steigt die Laufzeit mit jeder Ressource spürbar.

Eine effizientere Version des vorherigen Codes:

import time

from ppms import ppms

RESOURCES = ['R1', 'R8', 'R41']
RAW_QUERY = """SELECT 
                 DI001218 "resource",
                 DI001230 "start period",
                 DI001231 "end period"
               FROM
                 DT467
               WHERE
                 DI001218 IN ({resources})
            """


def get_start_and_end_period_of_resources(resources):
    """Retrieve the start and end period of a list of resources"""
    formatted_resources = "'" + "', '".join(resources) + "'"
    query = RAW_QUERY.format(resources=formatted_resources)
    
    result = ppms.db_select(query)
    
    periods = {}
    for resource_id, start_period, end_period in result:
        periods[resource_id] = start_period, end_period
        
    return periods
   
    
start = time.time()
periods = get_start_and_end_period_of_resources(resources=RESOURCES)  
end = time.time()

execution_time = round(end - start, ndigits=6)
ppms.ui_message_box('Executing the code took {} seconds!'.format(execution_time))
PY
  • Nun werden sämtliche Start-/Endperioden mit Hilfe einer einzigen Datenbankabfrage geholt, egal wie viele Ressourcen abgefragt werden.
  • Wenn sich die Anzahl der abgefragten Ressourcen erhöht, steigt die Laufzeit kaum.
          


Doppeltes Filtern

Oft wird in Modulen versehentlich doppelt gefiltert, was die Laufzeit unnötigerweise erhöht.

  • Oft wird dies durch unsaubere Modul-Makros verursacht.
  • Wenn ein Modul initial aufgerufen wird, werden die Funktionen on_load() und on_initial_focus() ausgeführt.
  • Wenn beide dieser Methoden ein Module.menu(12) beinhalten, wird doppelt gefiltert.

Modul-Laden verzögern

Das Laden der Daten in einem Modul kostet stets Laufzeit, weswegen dies erst dann passieren sollte, wenn der Benutzer die Daten wirklich sehen will.

  • Ein Modul wird geladen, sobald es mit Module.menu(12) gefiltert wird.
  • Um das Laden zu verzögern, muss das Filtern so spät wie möglich aufgerufen werden.
  • Hierfür eignet sich die Modul-Methode on_initial_focus(), da diese aufgerufen wird, wenn der Benutzer ein Modul zum ersten Mal fokussiert.
  • Ob das Laden der Modul-Daten noch aussteht, kann mit der Modul-Methode Module.is_stub() überprüft werden.

Ein Untermodul, welches zu Modul-Start schon offen ist, ruft die on_initial_focus() Methode erst auf, wenn der Benutzer in das Modul klickt!

  • Hier muss im on_load() gefiltert werden.

Filtern auf virtuellen Dataitems

Das Filtern auf virtuellen Dataitems ist immer langsamer, als das Filtern auf reelen Dataitems

  • Ein Filter auf einem reelen Dataitem wird von der PLANTA-Software direkt in die Datenbankabfrage eingefügt, um die Anzahl der Ergebnisse einzuschränken.
  • Bei einem virtuellen Dataitem müssen zuerst alle Daten der Datentabelle aus der Datenbank geholt werden und diese dann innerhalb von PLANTA gefiltert werden.
    • Ausnahme: Dataitems mit der Wertebereichsfunktion computeSQLValueRange sowie Hol-Exits werden ebenfalls performant beim Laden der reelen Dataitems abgefragt

Verwendung von Caches

Wenn ein Wert sich kaum ändert, aber häufig gelesen wird, soll man sich überlegen, ob es nicht sinnvoll wäre, den Wert zu puffern.

  • Man muss sich dabei folgende Fragen stellen:
    • Wie oft wird der Wert abgerufen?
      • Ein Wert, der nur 1-2 mal pro Session abgerufen wird, ist kein guter Caching-Kandidat.
    • Wie lange dauert es, den Wert zu ermitteln?
      • Einfache Berechnungen, die bereits im Bruchteil einer Sekunde ermittelt werden, lohnen sich nicht, gecacht werden, hier kann der Aufwand, den Cache einzurichten und zu invalidieren, den Nutzen übersteigen.
    • Wie häufig ändert sich der Wert?
      • Werte, die ständig wechseln, sind keine guten Cache-Kandidaten, außer die Berechnung dauert länger und die Werte werden trotzdem an verschiedenen Stellen öfter benötigt.
    • Wann muss ich den Cache leeren?
      • Oft wird der Cache geleert, wenn sich der Wert ändert. In manchen Fällen reicht es aber auch, an bestimmten Punkten im Code den Cache zurückzusetzen, wenn man weiß, dass nur der nachfolgende Code auf den Cache zugreift.
    • Von wo muss ich den Cache leeren können?
      • Wenn ich in einem Modul eine Funktion mit Cache anbiete, dann muss auch eine Funktion angeboten werden, um diesen Cache zu leeren.

LRU-Cache

Stolperfallen

  • Die Parameter der gecachten Funktion sowie der Rückgabewert sollen primitive Datentypen sein (Zahlen, Texte, ...), keine PLANTA-Datentypen (DtpRecord, DataItem, ...).
  • Wenn I-Texte Session-übergreifend gecacht werden sollen, muss die Sprache des Benutzers Teil der Parameter sein, sonst wird der I-Text in der Sprache des ersten Benutzers, der die Funktion aufruft, gecacht.

Beispiel-Code

import functools

from ppms import ppms


@functools.lru_cache(maxsize=None)
def get_dataitem_attributes(di_id):
    """Return the attributes of a DI or None if the DI is not valid"""
    di_record = ppms.search_record(412, [di_id], ['column_type', 'df_length', 'dt', 'di_name', 'format_id'])

    if di_record is None:
        return None

    column_type = di_record.column_type.get_value()
    df_length = di_record.df_length.get_value()
    dt = di_record.dt.get_value()
    di_name = di_record.di_name.get_value()
    format_id = di_record.format_id.get_value()

    return column_type, df_length, dt, di_name, format_id

def cache_clear():
    """Clear all caches used in this module"""
    get_dataitem_attributes.cache_clear()
PY

Datenbank

Verwendung von Indizes

Information

  • Wenn Abfragen oft über dieselben Suchkriterien gefiltert werden, kann eine Performance-Verbesserung erreicht werden, indem man auf die Suchkriterien Indizes setzt.
    • Die jeweilige Art des Index ist hierbei datenbankabhängig.