Blog

WPF Performance – Was geht mich das auf dem User Interface überhaupt an?

Alexander Keller

Ein Thema, welches bei der Entwicklung von Software immer eine große Rolle spielt, ist unumstritten das Thema Performance. Auch wenn uns bei der Softwareentwicklung mit .NET im WPF Umfeld schon sehr viel Arbeit bezüglich gutem Laufzeitverhalten durch Windows abgenommen wird, gibt es eine Vielzahl von Performance Aspekten, welche bei der Implementierung beachtet werden sollten.

Doch was wird eigentlich in der Informatik durch das Wort “Performance” oder „Performanz“ beschrieben? Allgemein wird das Wort Performance (deutsch: Leistung) verwendet, um das Vermögen einer Software zu beschreiben, Aufgaben auszuführen (siehe Wikipedia). Umgangssprachlich geht es beim Begriff Performance meist darum, wie “schnell” eine Anwendung ist. Dabei wird normalerweise jedoch kein Unterschied zwischen der Performance des User Interface und der Performance der Anwendungslogik gemacht.

Trotzdem sollte innerhalb des Entwicklungsteams eine klare Trennung gemacht werden, welche Performance Aspekte für wen von Relevanz sind. Ein häufiger Fehler besteht darin, die Optimierung der Performance in die Verantwortung eines einzelnen Entwicklers zu geben. Obwohl das Thema Performance an sich die gesamte Applikation betrifft, ist es aber vorteilhaft, dass Optimierungen nicht nur durch eine einzelne Person aus dem Entwicklerteam abgedeckt, sondern entsprechend Fachkompetenzen und Spezialisierungen an die richtigen Personen verteilt werden. Genau aus diesem Grund, bemühe ich mich als UI Design Engineer bei Centigrade häufig darum, in Performance Analysen unserer Kunden bei .NET basierten Softwareprojekten einbezogen zu werden, um gerade an den Stellen, die den auf XAML-basierenden Teil des User Interface betreffen eine Optimierung zu realisieren, während unsere Kunden Performance-Optimierungen oftmals eher an der C#-basierten User Interface Logik oder noch tiefer liegenden Schichten vornehmen.“

Performanceprobleme erkennen

Damit eine Performanceoptimierung erfolgen kann, müssen zu Beginn erst einmal die „Problemstellen“ der Software erkannt und lokalisiert werden. Ohne entsprechende Daten lassen sich Performanceanalysen nur schwer durchführen, da das Entwicklungssystem in den seltensten Fällen dem späteren Zielsystem entspricht und durch reines „Ausprobieren“ daher keine genaue Beurteilung bezüglich des Laufzeitverhaltens getroffen werden kann. Um relevante und aussagekräftige Daten für .NET basierte Softwaresysteme zu erhalten, gibt es eine Vielzahl nützlicher Tools. Am bekanntesten dürfte hier die WPF Performance Suite sein. Diese ist im Windows SDK enthalten.

Um ein genaues Performance Profil einer Anwendung erstellen zu können, sind nach der Installation der Performance Suite folgende Tools verfügbar.

Perforator

Das Perforator Tool hilft vor allem bei der Analyse des Renderingverhaltens, also des Zeichenverhaltens einer WPF-Anwendung. Durch verschiedene Diagramme kann so eine genauere Analyse bezüglich des Renderingvorgangs durchgeführt werden, da maximale Auslastungen der CPU so schnell zu erkennen sind. Mit Hilfe einer Vielzahl von Einstellungen können verschiedenste Konfigurationen einer Anwendung, wie beispielsweise das Laufzeitverhalten mit oder ohne Opacity Effekten durchgespielt werden. Dank der als „dirty rectangle“ bezeichneten Renderingtechnik von WPF, werden in einer .NET Anwendung nur Bereiche welche sich ändern neu gezeichnet. Mit Hilfe der Funktionen „Show dirty-region update Overlay“ werden genau diese Bereiche in der Anwendung eingefärbt. So erhält man als UI Design Engineer schon einmal einen kleinen Überblick, welche Teile der Anwendung bei welchen Aktionen neu gezeichnet werden müssen und somit einen negativen Einfluss auf die Performance haben können. Schon durch kleine Änderungen, wie z.B. das Tauschen eines Layout Containers, können so erste Erfolge verbucht werden.

Hier ein Ausschnitt aus Perforator, um den Unterschied zwischen einer Anwendung mit vielen Renderingoperationen und wenigen in Zahlen verdeutlichen zu können.

In diesem Beispiel wurde durch die Funktion „Disable dirty region support“ (rot eingekreist) einmal der Extremfall, nämlich dass sich bei jeder Aktion auf dem UI die komplette Anwendung neu rendern muss, simuliert. Ob dieser Fall in einer Anwendung auftritt, kann durch die oben beschriebene Funktion (blau eingekreist) herausgefunden werden. Beispielsweise können Animationen welche mit der FluidLayout Funktionalität von Expression Blend realisiert wurden zu solchen Effekten führen. Denn mit Hilfe des FluidLayout können Eigenschaften, wie z.B. die Visibility, welche eigentlich keine Übergänge haben, animiert werden. Dies kann zu einem ständigen Neuzeichnen führen. Ein Zeichenvorgang in dieser Beispielanwendung hatte bis zu 79rects/s welche sich während einer Animation zum Ausklappen einer Menüleiste geändert haben. Wird diese Animation durch FluidLayout realisiert, wodurch Expression Blend intern einen Custom Visual State Manager installiert, hat dies zur Folge, dass sich das komplette Fenster neu zeichnet. Für die gleiche Animation wie zuvor wurden jetzt bis zu 231rects/s neu gerendert. Wenn man einmal berücksichtigt, dass die Beispielanwendung wirklich minimale Anforderungen hatte, kann man sich vorstellen, wie negativ sich eine ausufernde Animation bei komplexen Businessanwendungen auf die Performance auswirken kann. Gerade bei Animationen ist ist daher darauf zu achten, den Spagat zwischen „sexy UI“ und guter Performance zu schaffen. Grundsätzlich gilt ohnehin die Regel, möglichst nur dann Animationen einzusetzen, wenn diese für den Nutzer auch einen Mehrwert haben anstatt sie zum reinen Selbstzweck zu machen.

VisualProfiler

Das zweite Tool aus der WPF Performance Suite bietet die Möglichkeit ein einzelnes UI Element im Visual Tree zu ermitteln, welches Leistungsengpässe verursacht – der sprichwörtliche „Flaschenhals“. Durch eine Baumansicht in der Benutzeroberfläche des VisualProfiler kann so der komplette Visual Tree der Applikation analysiert werden. Ermittelt VisualProfiler ein Element, welches sich negativ auf die Performance auswirkt, so wird dieses in verschiedenen Rotschattierungen hervorgehoben. Je dunkler das Rot dargestellt wird, desto größer ist der relative Ressourcenverbrauch eines Objektes.

Das Beispiel zeigt die Analysedaten aus einem StackPanel welches im ItemsPanelTemplate einer ListBox verwendet wird. Interessant ist hier auch der rechte Bereich, welcher Informationen bezüglich der CPU Auslastung der Anwendung bereitstellt. Visuell werden die Daten als Diagramm veranschaulicht. So können maximale Auslastungen in den drei wichtigsten Bereichen Layout, Rendering und Animation schnell erkannt werden.

Detaillierte Informationen zur WPF Performance Suite gibt es direkt bei Microsoft:

DotTrace

Ein weiteres nützliches Tool, um Performanceprobleme aufzuspüren, ist das von der Firma „JetBrains“ entwickelte Tool DotTrace. DotTrace bietet neben Performance Analysen auch die Möglichkeit den Speicherverbrauch einer Anwendung zu untersuchen. Dieser spielt bezüglich der Geschwindigkeit einer Anwendung eine Rolle, da ein hoher Speicherverbrauch oftmals darauf schließen lässt, dass viele und schwergewichtige Objekte erzeugt werden. Da jede Erzeugung eines Objektes Zeit kostet, entstehen dadurch auch Performanceverluste. Im Gegensatz zu der WPF Performance Suite, welche gut zum Analysieren von Performance Aspekten auf View Ebene geeignet ist, ist dieser Profiler eher für die Analyse von C# Code geeignet. Das Tool zeigt in der Analyse unter anderem den kompletten „Call Stack“ der Anwendung mit Angaben der Anzahl der Aufrufe und der dafür benötigten Zeit. So kann beispielsweise bei einer großen Anzahl von Aufrufen eines Konstruktors davon ausgegangen werden, dass evtl. eine Virtualisierung in einer Liste nicht korrekt arbeitet. Um dieses Tool effizient nutzen zu können, sollte aber ein Grundwissen über die verwendeten C# Klassen der Anwendung vorhanden sein.

Weitere Informationen finden Sie bei JetBrains.

Welche Faustregeln sollte ich nun als UI Design Engineer beachten um meine Anwendung performant zu gestalten?

Wie bereits gesehen gibt es eine Vielzahl von potentiellen Performanceproblemen. Aber die meisten Probleme stellen gar keine so große Hürde da, wie man auf den ersten Blick meinen sollte. Warum viele Probleme recht einfach zu lösen sind, werden Sie in diesem Abschnitt erfahren. Ausserdem hat Microsoft seit dem .NET Framework 4.0 schon einige Verbesserungen bezüglich der Performance vorgenommen. Gerade im Hinblick auf die Verwendung von Pixel Shadern und das Rendering von komplex gestylten Elementen wurden Optimierungen eingeführt.

Effekte

So können seither Effekte geschrieben werden, welche Pixel Shader (PS) in der Version 3.0 nutzen. Das PS 3.0-Shadermodell ist im Gegensatz zur alten Version dramatisch verbessert worden. Es bietet noch mehr Effekte, welche auf unterstützte Hardware zurückgreifen, was im Hinblick auf die Performance einen großen Vorteil gegenüber Effekten, welche Softwaregerendert sind, bietet, da diese in der Grafikkarte abgewickelt werden und der Prozessor sich um seine eigentlichen Aufgaben kümmern kann. Trotzdem sollte man als UI Design Engineer immer abwägen, ob ein visueller Effekt nicht auch einfacher realisiert werden kann. Ein ressourcenlastiger Dropshadow-Effekt um einen Schatten unter einem Label zu visualisieren, kann auch vermieden werden, indem ein zweites Label in anderer Farbe, welches um einen Pixel verschoben ist verwendet wird. Gerade solche Vereinfachungen haben sich in der Praxis bewährt und sollten daher beim Styling einer Anwendung in Erwägung gezogen werden.

Caching

Eine weitere Unterstützung zur Performancesteigerung kann die Klasse BitmapCache sein. Durch diese können visuell komplexe UI Elemente in WPF als Bitmap zwischengespeichert werden. Der BitmapCache kann durch das CacheMode Property für ein beliebiges UI Element gesetzt werden.

Dies führt gerade bei Animationen zu einer signifikanten Reduzierung des Rendering Aufwandes. Trotzdem ist das Element weiterhin komplett interaktionsfähig und erhält alle WPF Events, wie z.B. Mausklicks oder Tastatureingaben. Dadurch wird aber der Cache auch revalidiert, was zu einem Neuzeichnen führt. Auch eine Skalierung des Objektes ist möglich. Durch das RenderAtScale Property kann angegeben werden, mit welchem Skalierungsfaktor das Bild zu Beginn zwischengespeichert werden soll. Im Standard wird das Element in seiner ursprünglichen Größe (also bei 96 ppi) als Bitmap gespeichert. Hierbei ist aber darauf zu achten, dass keine visuellen Nebeneffekte entstehen. Denn gerade bei Elementen mit Texten ist davon abzuraten den BitmapCache auf das gesamte Element inklusive der Text Repräsentation anzuwenden, da die Qualität des Textes sehr schnell darunter leidet. Zudem sollte man nur eine vertretbare Anzahl von UI Elementen wirklich zwischenspeichern, da man sonst wiederum Gefahr läuft in Arbeitsspeicherprobleme zu rennen. Ein wesentlicher Punkt bei der Verwendung des BitmapCache ist das Layout des verwendeten Elementes. Denn durch eine Veränderung des Layouts wird das Bitmap neu gespeichert, was eine „teure“ Operation darstellt. Es sollte dementsprechend darauf geachtet werden, dass sich das Layout des Elementes über die Laufzeit des Programms hinweg so wenig wie möglich, im besten Falle gar nicht ändert. Eine RenderTransform Operation kann hier im Gegensatz zur LayoutTransform bedenkenlos verwendet werden, da diese erst zum Zeitpunkt des Renderings ausgeführt wird.
Microsoft bietet eine weitere Möglichkeit Elemente zwischenzuspeichern. Die Klasse BitmapCacheBrush kann ein zuvor zwischengespeichertes BitmapCache Element als BitmapCacheBrush definieren, welcher beispielsweise als Background auf mehreren Elementen angewandt werden kann.

Durch die Wiederverwendung des zwischengespeicherten Elementes und der damit verbundenen Minimierung des Zeichenaufwandes kann so eine gute Performance erzielt werden.
Durch die Verwendung dieser Klassen kann die CPU Auslastung bezüglich des Rendering in bestimmten Fällen um das bis zu Dreifache verringert werden.

Listen

In vielen Fällen kommt es in einer Anwendung genau dort zu Performanceengpässen, wo viele Daten visualisiert werden. Die drei am häufigsten verwendeten Controls zur Darstellung von Daten auf dem User Interface sind in der WPF ListBox, ItemsControl und DataGrid. Hierbei spielt vor allem das Thema Scrolling Performance eine große Rolle. Die WPF bietet hier durch die Klasse VirtualizingPanel eine Möglichkeit, Elemente einer Liste erst dann zu instanziieren, wenn diese auch wirklich sichtbar werden. Dies führt zu einer beachtlichen Verbesserung des Laufzeitverhaltens, gerade im Hinblick auf die Ladezeit einer Liste. Überschreibt man also das ItemsPanel einer Liste, sollte darauf geachtet werden, dass man hier ein VirtualizingPanel verwendet. Standardmäßig wird hier immer ein VirtualizingStackPanel verwendet.
Zusätzlich kann durch das VirtualizationMode Property des VirtualizingPanel eine gute Verbesserung bezüglich der Scrolling Performance erzielt werden. Im WPF Standard werden die Elemente in der Liste nachdem diese aus der View verschwinden verworfen und bei jedem Erscheinen wieder neu erstellt. Nicht immer ist der Standard die beste Lösung. In der Praxis, haben wir festgestellt, dass oft immer wieder die gleichen Elementcontainer in einer Liste verwendet werden. Ist dies gewährleistet, kann ein ständiges Wegwerfen und Neuerstellen evtl. ausgeschaltet werden. Setzt man also das VirtualizationMode Property auf „Recycling“, werden die Elementcontainer wiederverwendet (recycelt)

Dadurch kann ich mit nur einem Property schon eine Verbesserung von bis zu 40% bezüglich der Scrolling Performance erzielen.

Zu Schwierigkeiten kann es kommen, wenn man das Scrollen in Listen pixelweise, statt elementweise realisieren will. Eine einfache Möglichkeit besteht darin, das Property CanContentScroll auf „False“ zu setzen. Dadurch fühlt sich das Scrollen zwar sehr flüssig an, führt aber auch zu einem unerwünschten Seiteneffekt. Der große Nachteil bei diesem Property ist, dass die Virtualisierung einfach deaktiviert wird, auch wenn ein VirtualizingPanel im ItemsPanel der Liste verwendet wird. Dementsprechend ist gerade im Hinblick auf das Styling von Listen Vorsicht geboten.

Resource Dictionaries

Ein sehr wichtiges Thema in großen Softwareprojekten ist immer wieder das Verwalten der Resourcen in den ResourceDictionaries. Eine Besonderheit in WPF, ob nun immer wünschenswert oder nicht, ist es, das Merged Dictionaries immer wieder neu instanziiert werden, wenn diese beispielsweise in einer View via MergedDictionary inkludiert wurden. Berücksichtigt man nun, dass Frameworks wie Prism, bei denen Views dynamisch aus mehreren Views zusammengesetzt und geladen werden, immer stärker genutzt werden, wird klar dass die Organisation von ResourceDictionaries gut durchdacht sein sollte. Um ein besseres Verständnis für entstehenden Probelematiken zu bekommen, dient hier diese Grafik einer View, wie sie in einem realen Softwareprojekt durchaus auftreten könnte.

In diesem einfachen Beispiel ist durch die schlechte ResourceDictionary Struktur, BasicResources.xaml dreimal und BrushResources.xaml zweimal inkludiert. Wäre in der App.xaml nur ButtonResources.xaml inkludiert, würden weiterhin alle Ressourcen zur Verfügung stehen. Der Speicher würde aber im Gegensatz zu der in der Grafik gewählten Variante nicht durch die fünf gedoppelten ResourceDictionaries gefüllt werden.
Durch eine häufige Dopplung von ResourceDictionaries in einer Anwendung läuft man somit als UI Design Engineer also Gefahr, dass das User Interface einen enormen Speicherverbrauch in der Anwendung verursacht.

Da dies ein bekanntes „Problem“ in WPF ist, wird man natürlich schon nach kurzem Googlen mit Verbesserungsvorschlägen überhäuft. Hierbei sollte allerdings beachtet werden, dass nicht jeder als Antwort markierte Beitrag in einem Forum auch wirklich ohne Weiteres verwendet werden kann.

Ein auf den ersten Blick vielversprechender Lösungsansatz bietet hier ein sogenanntes SharedResourceDictionary. Dabei wird jedes ResourceDictionary jeweils nur ein einziges Mal instanziiert und ab dann gemeinsam verwendet. Ein großer Nachteil hierbei ist, dass durch jedes ResourceDictionary eine Referenz auf den Owner gehalten wird. Bei dem von ResourceDictionary abgeleiteten SharedResourceDictionary führt dies dazu, dass der Speicher, den eine View belegt nicht mehr durch den Garbagecollector freigegeben werden kann. Dadurch können enorme Speicherlöcher entstehen, welche nicht ohne weiteres geschlossen werden können.

Allgemein sollte darauf geachtet werden, dass Ressourcen, welche an mehreren Stellen verwendet werden, einmal global über die App.xaml eingebunden werden.
Auch die Art, wie Ressourcen im XAML Code referenziert werden, spielt bezüglich des Laufzeitverhaltens eine Rolle. Die WPF bietet uns hierfür zwei Vorgehen an.

1) Verwendet man, wie es uns Expression Blend vorschlägt, DynamicResources, so hat dies folgende Auswirkungen: Zum einen können Ressourcen während der Laufzeit ausgetauscht werden. Dies führt dazu, dass beispielsweise bei jeder Initilialisierung einer View die Ressourcen neu geladen werden. Folglich kann das Öffnen einer View eine gewisse Ladezeit zur Folge haben, wenn DynamicResources verwendet werden.

2) Nutzt man hingegen StaticResources, werden die Ressourcen gleich zu Beginn, beim Start der Anwendung alle geladen. Dementsprechend hat man nach dem Start der Anwendung keine weiteren Ladezeiten für Ressourcen zu verbuchen. Doch StaticResources können umgekehrt die Startzeit einer Applikation erheblich erhöhen. An dieser Stelle sollte man sich idealerweise schon zu Beginn der Entwicklung entscheiden, ob eher eine längere Startzeit der Gesamtapplikation in Kauf genommen wird, oder mit einer größeren Ladezeit während die Applikation läuft gelebt werden soll.

Trigger vs. Visual States

Oft habe ich mir, als die Entwicklung im WPF Umfeld noch relativ neu für mich war, die Frage gestellt, warum in den Standard WPF Templates immer wieder Zustände, wie z.B. Pressed bei einem Button, mit Hilfe von Triggern visualisiert werden, wo es doch in jedem Template eigene VisualStates mit VisualStateGroups gibt. Doch wenn man einmal die Performance von Controls mit Berücksichtigt, macht dies durchaus Sinn. Denn jeder Aufruf, um in einen State zu wechseln, welcher wiederum ein Storyboard startet und ausführt, kostet Zeit. Dass dies natürlich eine größere Dauer zur Folge hat als einfach ein Property in einem Trigger zu setzen, liegt auf der Hand. Es ist also je nachdem, wie ein Zustand visualisiert werden soll, zu vergleichen, mit welchen Mitteln man die Visualisierung erreichen will oder kann.

Zuletzt möchte ich Ihnen noch einen recht einfach zu berücksichtigenden Ratschlag zur Optimierung der User Interface Performance mit auf den Weg geben. Denn schon beim Definieren eines Layouts können unglückliche Strukturen, welche zu einer großen Anzahl von Elementen im Visual Tree führen, zu Performanceverlusten führen. So sollte darauf geachtet werden, dass möglichst flache Container Hierarchien angelegt werden. Auch der Containertyp sollte nicht nach dem Zufallsprinzip oder aus Gewohnheit gewählt werden. Schwergewichtige ContentControls sollten, sofern die Möglichkeit besteht, nicht als Layout Container verwendet werden. Gerade diese einfache Regel sollte beim Anlegen von Control Templates berücksichtigt werden. Denn Templates werden oft wiederverwendet. Weist man beispielsweise 10 Buttons ein Template zu, welches wiederum 10 Elemente beinhaltet, so kommt man schon auf 100 Elemente!

Fazit

Wie in diesem Blogartikel dargestellt, kann eine Optimierung bezüglich der Performance einer .NET Applikation nicht nur von einem Bereich eines Entwicklungsteams optimal gewährleistet werden. Vielmehr sollte eine Verbesserung des Laufzeitverhaltens im jeweiligen Fachbereich vorgenommen werden, um ein optimales Ergebnis zu erzielen. Und genau darum gehören Performance Analysen und Optimierungen auch zum Aufgabengebiet eines UI Design Engineers. Zumindest bei Centigrade.

Microsoft, Windows, Expression Blend und Expression sind Marken oder eingetragene Marken der Microsoft Corporation in den USA und/oder anderen Ländern.

Möchten Sie mehr zu unseren Leistungen, Produkten oder zu unserem UX-Prozess erfahren?
Wir sind gespannt auf Ihre Anfrage.

Senior UX Manager
+49 681 959 3110

Bitte bestätigen Sie vor dem Versand Ihrer Anfrage über die obige Checkbox, dass wir Sie kontaktieren dürfen.