Q-System der Ewigkeit: 120 Sekunden für eine Fiori
Ich hätte vorher fragen sollen. Derzeit entwickel ich für einen Kunden eine Fiori im Master-Detail-Design für das SAP PM Modul. Im Kern sollen technische Plätze (TP) angelegt, angezeigt, klassifiziert und verändert werden können. In der Fiori dargestellt werden sollen:
- Kopf- und Detaildaten
- beschreibende Texte
- Standort und Verantwortliche
- Adresse
- Systemstatus
- Klassifizierungen und deren Merkmale
Die notwendigen Daten werden über Standard BAPIs sowie Funktionsbausteine gewonnen und in einer READ-Operation dem Datensatz des TP zugewiesen. Für die QUERY-Operation werden nur im Voraus die relevanten TP-Nummern geladen und über den READ-Funktionsbaustein, der dann alle notwendigen Daten pro Satz zuweisen kann, iteriert. Im Anschluss findet die Weitergabe an das Gateway und abschließend an die Fiori statt.
FUNCTION zfiori_tp_query. [..]*// Diverse Filter-Ranges CALL FUNCTION 'BAPI_FUNCLOC_GETLIST' EXPORTING language = sy-langu TABLES funcloc_ra = funcloc_ra descript_ra = descript_ra status_ra = status_ra funcloc_list = funcloc_list. *// Liste an TP LOOP AT funcloc_list INTO ls_list. CALL FUNCTION 'ZFIORI_TP_READ' *// Erweitert TP-Liste um die EXPORTING *// oben genanntnen Daten iv_tplnr = ls_list-functlocation IMPORTING es_fiori_tp = ls_fiori_tp. APPEND ls_fiori_tp TO et_tp. ENDLOOP. ENDFUNCTION.
Durchschnittlich lädt ein TP im READ-Funtionsbaustein in 0,012 Sekunden. Eigentlich ganz schnell, oder? Im Entwicklungssystem, mit ca. 350 Datensätzen, war es auch so. Leider erfuhr ich erst später, dass auf dem Q-System – welches regelmäßig die Daten der produktiven Umgebung spiegelt – 10.000 relevante Datensätze liegen! 10.000 * 0,012s sind am Ende leider doch 120 Sekunden, und 2 Minuten möchte niemand auf seine Fiori warten müssen :-/ Wie so oft, fand der Transport auf das Q-System erst zu einer relativ späten Phase der Entwicklung statt. Ich hatte also nicht mehr viel Zeit um Fiori, Gateway-Projekt und Funktionsbausteine komplett umzubauen. Daher war eine Lösung nötig, die mit geringen Aufwand realisierbar wäre.
Ursachenforschung
Der zuvor gezeigte Funktionsbaustein ‘ZFIORI_TP_QUERY ‘dient der Datengewinnung für das EntitySet ‘TPSet’ im zugehören Gateway Projekt. Das in diesem Fall gedachte Vorgehen wäre eigentlich wie Verwendung der OData-Parameter $top und $skip. Diese Parameter regeln die maximale Anzahl an Datensätzen ($top) und wie viele Datensätze übersprungen ($skip) werden sollen. Somit wird ermöglicht, dass die Fiori die Datensätze in kleine Blöcke aufteilen lassen kann (Pagination), um durchgängig hochperformant laufen zu können. Doch nun unterstützt der verwendete BAPI ‘BAPI_FUNCLOC_GETLIST’ keine Möglichkeit diese Parameter direkt anzuwenden. Ich kann Filter verwenden, aber die gewonnenen Datensätze lassen sich nicht bereits beim Laden stückeln. Ich hatte schon überlegt die Datensätze über einen eigenen SELECT zu bekommen. Nur bietet die Tabelle für den technischen Platz ‘IFLOT’ keine simplen Primärschlüssel, wie beispielsweise die Tabelle ‘VBAK’, sondern komplexe wie “ICC—ASD21–“. Ich konnte also nicht feststellen, wie ich anhand des Schlüssels Daten überspringen lassen kann. Stand jetzt kann ich $top und $skip nur nachträglich im Gateway anwenden, um die Fiori wenigstens nicht mit Daten zu überfluten. Langsam laden tut sie trotzdem.
Wie schon erwähnt, nutzt die Fiori das Master Detail Layout. Die geladenen Daten werden mit einem simplen Binding an die Liste der Master View gebunden. Ein Klick auf einen Eintrag der Liste löst in der Detail View ein entsprechendes Binding auf den gewählten Datensatz aus. Nichts Neues. In der Master-Liste werden die Kopfdaten und der Systemstatus – in Form von gefärbten Icons – gezeigt, in der Detail View die restlichen, komplexen Daten. Hier ist das ursprüngliche Coding des List Control der Master View:
<List id="list" growing="true" growingScrollToLoad="true" growingThreshold="40" items="{ path: '/TPSet' }" [...]> <items> <ObjectListItem intro="{Descript}" title="{Functlocation}" > <firstStatus> <ObjectStatus icon="{ parts: [ {path: 'StateActive' }, { path: 'StateDelete' } ] , formatter: '.formatter.objectIcon' }" state="{ parts: [ {path: 'StateActive' }, { path: 'StateDelete' } ] , formatter: '.formatter.objectState' }" /> [...]
Hier wurde mir bewusst: das TPSet muss sich um Alles kümmern! Verdeutlichen soll dies das folgende Bild:
Master, Detail, Systemstatus: Alles läuft über das ‘TPSet’, welches mir IMMER komplett im Backend geladen und dann erst nachträglich, über die growing-Parameter des List Control, eingeschränkt wird. Ich habe mich entschieden, die 10.000 als unvermeidlich Datensätze hinzunehmen und habe statt dessen die beiden Views der Fiori genauer betrachtet: eigentlich brauche ich die Detaildaten, Texte, Adresse, Klassenmerkmale, etc. erst in der Detail View. Warum dann nicht jeder View 1-2, für die jeweiligen Bedürfnisse optimiertes, EntitySet geben und über Relationen verknüpfen?
Mit Köpfchen
Die Master View bekam ein eigenes EntitySet ‘TPHeadSet’ im Backend, welches Quasi nur aus der Nummer vom technischen Platz und der Beschreibung besteht. Ich nutze hier ebenfalls den mit Filtern versehenen ‘BAPI_FUNCLOC_GETLIST’, erweitere aber nicht mehr die hier geladenen Daten. Die Detail View behält weiterhin das ‘TPSet’. Dieses ‘langsame’ Set wird aber nun nur noch im Einzelsatz (READ) aufgerufen. Dafür konnte ich direkt den bereits vorhandenen Funktionsbaustein ‘ZFIORI_TP_READ’ weiterhin verwenden. Ebenfalls über einen Einzelsatz wird das neue Set ‘TPStateSet’ geladen, welches den Systemstatus des TP ermittelt. Im Gateway-Projekt werden diese Entitäten über 1:1 Relationen verbunden. Das Konzept zeigt die folgende Abbildung.
Das Coding der Fiori reflektiert nun das erweiterte OData-Modell. Besonders wichtig war dabei der Einsatz der growing- und expand-Parameter.
<List id="list" growing="true" growingScrollToLoad="true" growingThreshold="40" items="{ path: '/TPHeadSet', parameters: { expand: 'TPStateNav,TPNav' } }" [...]> <items> <ObjectListItem intro="{Descript}" title="{Functlocation}" > <firstStatus> <ObjectStatus icon="{ parts: [ {path: 'TPStateNav/StateActive' }, { path: 'TPTankHeadStateNav/StateDelete' } ] , formatter: '.formatter.TPStateNav' }" state="{ parts: [ {path: 'TPStateNav/StateActive' }, { path: 'TPStateNav/StateDelete' } ] , formatter: '.formatter.objectState' }" /> [...]
In der View passiert ziemlich viel auf einmal und es werden die Möglichkeiten des OData-Modell in Zusammenspiel mit dem neuen Design im OData-Modell voll genutzt. Kurz gefasst passiert in dem Coding Folgendes:
- ‘TPHeadSet’ lädt im Backend die nun 10.000 schlanken Datensätze
- Mittels dem growing-Parametern werden aber nur jeweils 40 dieser Datensätze an die Fiori weiter gegeben
- Nur für diese 40 Datensätze werden über expand die rechenintensiven Detail- und Statusdaten mitgeladen
Die neue Zuständigkeiten innerhalb des Codings zeigen das nächste Bild.
Ein Klick auf die Liste benötigt dabei keine weitere Ladezeit, da die relevanten Detaildaten bereits vorliegen. Somit wird die Fiori schnell geladen und bleibt ebenfalls schnell bedienbar. Der Systemstatus wird automatisch über den Verweis auf das neue EntitySet mitgeladen. Was bleiben sind ca. 3,8 weitere Sekunden für jedes nachladen weiterer Blöcke an Daten wenn der Anwender an das Ende der Liste gescrollt hat.
Ergebnis
Der Zugewinn an Performance wird durch diese Grafik deutlich:
Die Aufteilung auf schlanke, schnell geladene Listen in Zusammenarbeit mit nach Bedarf geladenen Detaildaten konnte die Performance der Fiori erheblich verbessern. Von 120 auf 4 Sekunden – da war ich schon stolz 🙂 In einem folgenden Beitrag würde ich meine Ergebnisse präsentieren, ob es doch möglich ist jede Art von SAP Datensatz performant über die OData-Methodik der Pagination laden zu können.
Du interessierst dich für Fiori und hast Lust coole Projekte mit uns zu machen? Wir suchen dich! Schau doch mal in unserer Stellenbeschreibung vorbei.