VS2012 Profiling

Es wird mal wieder Zeit für einen Grundlagen-Artikel zu einem VS-Feature, das vielleicht nicht jedermann kennt oder schon oft benutzt hat. Wir widmen uns heute den Profilern und hier vornehmlich dem Aspekt der Performance-Messung und -Auswertung unter VS 2012.

Vorbemerkungen

Was wir uns hier ansehen werden, kann leider nicht jeder VS-Benutzer nachspielen.  Mindestens eine Premium muss es sein, also entweder die oder die Ultimate :-).

Zunächst möchte ich aber ein paar Worte über das Warum verlieren. Als Entwickler schreibt man heutzutage so einiges Zeug zusammen, ohne weiter darüber nachzudenken. Datenbankzugriffe z.B. habe ich selbst schon länger nicht per Hand codiert. EF sei dank, kann ich mich ganz auf meine Aufgabe als Anwendungsenwickler konzentrieren und überlasse das Join un Group und Where einfach dem Entity Framework.

Einerseits ist das ein Segen. Auf der anderen Seite muss ich mich aber auch fragen lassen, was mit der Performance-Seite passiert, wenn ich mich auf diese Tools verlasse. Klar könnte man sagen, dass man die Tools einfach richtig verwenden soll, aber das ist eher kein guter Tipp. „Richtig“ heißt hier zunächst einmal komplett laut Dokumentation. Und selbst wenn man es schafft, diese und die verfügbare Literatur durchzuackern (was nur arbeitslose Programmierer können), ist man nicht davor gefeit, dass das Tool einfach einen Bug oder ein unerwartetes Verhalten aufweist. Was weiß man denn schon über Skalierung und andere Themen bevor man es testet? Genau: Nicht viel!

Hinzu kommen übliche Fehlerteufel. Im eigenen Wahn gefangen kommt es schon mal zu unsinnigen Zugriffen, dem Vergessen von Aufräumarbeiten oder sonstigem Schindluder.

Profiling kann nun dabei helfen, solche Fehler und daraus resultierende Flaschenhälse zu identifizieren. Das Profiling konzentriert sich dabei nicht auf den Quellcode, sondern das tatsächliche Ergebnis (Assembly). Wie verhält sich der kompilierte Code zur Laufzeit und welche Ausführungspfade entstehen bei den im Profiling enthaltenen Aufrufen? Das sind typische Fragen, die durch das Profiling beantwortet werden können.

Webcast

Profiling-Grundlagen

Wie kann da nun VS helfen? In den entsprechenden Editionen sieht man zunächst einmal ein Menu „Analyze“:

Abb. 1: Analyser-Menu
Abb. 1: Analyser-Menu

Neben dem reinen Profiling versammeln sich hier noch so einige andere Tools, wie z.B. der Concurrency Visualizer Analysieren von multi-threaded Anwendungen und den damit einhergehenden Block- und Wait-Problemen, die Code-Analyse (der alte bekannte FxCop-Spross) oder auch der Code-Clone-Analyser zum Auffinden von doppelten Codeblöcken. Uns interessieren in diesem Beitrag vor allem die Perfomance-Reports und ganz besonders das Menu „Profiler“. Damit einher geht seit dem VS 2012 (Performance-Analyse gibts schon seit VS 2010) das Fenster „Performance Explorer“ (Analyze -> Windows -> Performance Explorer):

Abb. 2: Performance Explorer
Abb. 2: Performance Explorer

Das also sind die Ankerpunkte im Visual Studio. Nun noch zu einigen Grundbegriffen zum besseren Verständnis des Kommenden.

Die Performance-Analyse einer Anwendung oder eines Teilprojekts einer Solution erfolgt entweder anhand des Profilings einer EXE oder durch das Ausführen von Unit Tests. Beides basiert darauf, dass irgend etwas Teile der zu analysierenden Logik aufruft. So gesehen verhält sich das Profiling wie ein Lasttest.

Der große Unterschied liegt nun darin, dass das Profiling von Code weitaus mehr tut, als nur den laufenden Code zu beobachten. Dabei gibt es 3 Arten von Profiling-Strategien:

  1. Sampling
  2. Instrumentation
  3. Concurrency

Sampling

Ein Sample ist ein vom Profiler genommener Snapshot einer Applikation. Das ganze macht der Profiler in regelmäßigen Abständen. Dabei „sieht“ der Profiler, welcher Code gerade ausgeführt wird usw. Das Sampling könnte man als das passive Profiling bezeichnen. Der Profiler sieht sich den Code und das System von außen an und „misst“ die benötigten Werte. Er arbeitet sozusagen indirekt. Das Sampling deckt durch diese Vorgehensweise auch Code außerhalb des eigenen Quelltextes mit ab. Wenn er gerade zufällig ein Sample nimmt und die Ausführung sich in dem Moment in sagen wir mal „System.Data.irgendwas“ befindet, dann wird halt der entsprechende Typ mit erfasst.

Instrumentation

Bei der Instrumentation wird durch den Profiler sog. Probes in den zu beobachtenden Code injziert. Visual Studio geht hierzu direkt in die zu messende Anwendung und passt den Code an (Refection). Das hat den Vorteil, dass die Ergebnisse nun direkt ähnlich wie beim Unit Testing erzeugt werden. Bei extrem schnellen Operationen werden die Ergebnisse auf jeden Fall geliefert. Auf der anderen Seite läuft die Anwendung bei der Instrumentation etwas verändert ab. Im Gegensazu zum Sampling wird hier wirklich nur der Quellcode der Ziel-Assembly (Target) erfasst. Ausführungs-Zweige in anderen Assemblies können ja nicht mit instrumentalisiert werden.

Concurrency

Hier geht es vor allem um die Analyse von  multi-threaded Anwendungen. Man kann bei entsprechend eingestellten Optionen und Einstiegspunkten erkennen, wo Engpässe auftreten, aber auch, wo eine Anwendung einfach nicht gut skaliert und u.U. nicht die vollen Performance-Möglichkeiten ausschöpft.

Tiefer gehende Informationen findet man z.B. in der MSDN.

Was noch?

Für das Profiling interessant können auch die Symbole des .NET Framework werden. Wer sich noch nie mit diesem Thema befasst hat, findet hier ein paar Infos (oberflächlich). Wer das Thema schon kennt, kann gern zu „Los gehts!“ springen.

Während des Profilings „folgt“ das Visual Studio dem Code während er ausgeführt wird. Wir erhalten also z.B. die Information, dass Methode Foo() soundso oft aufgerufen wurde. Wenn nun aber Foo() irgendwas aus den .NET-Klassen aufruft, dann ist der Profiler aufgeschmissen. Es sei denn, die sog. Symbole (Debug-Informationen zu Assemblies, die PDB-Dateien im bin-Ordner) sind verfügbar. Seit geraumer Zeit bietet Microsoft diese Symbole auch für die .NET-Klassen an.

Um sie verfügbar zu machen, setzt man nur einen Haken in den VS-Optionen:

Abb. 3: Symbole einschalten
Abb. 3: Symbole einschalten

Die erscheinende Warnung sollte man bei langsamen Internetverbindungen auf keinen Fall ignorieren. Als nächstes kann man noch einstellen, dass man im .NET-Framework debuggen möchte:

Abb. 4: Debugging im .NET Framework
Abb. 4: Debugging im .NET Framework

Bestätigt man diese Einstellung, kann man z.B. auf einem „Console.WriteLine…“ einen Haltepunkt setzen. Drückt man bei Erreichen des Haltepunktes dann F11, findet man sich plötzlich in der Klasse Console wieder und kann sie durchsteppen, als hätte man sie selbst geschrieben.

Das ist natürlich auch für den Profiler von Vorteil und daher spielen Symbole hier ebenfalls eine Rolle (siehe unten).

Los gehts!

Damit das Profiling ein wenig mehr Spaß macht, habe ich eine simple kleine Solution erstellt:

Abb. 5: VS-Solution
Abb. 5: VS-Solution

PerfData beinhaltet einfach nur ein EntityModel, das gegen alle Tabellen des HumanResources-Schema einer AdventureWorks2012-SQLS-Datenbank gebaut wurde.

PerfLogic besteht nur aus einer statischen Klasse EmployeeUtil. Diese hat 2 Methoden, mit denen man sich Employees anhand eines Filter-Kriteriums besorgen kann. Die eine (GetEmployeesByJobTitleSlow) wurde bewusst langsam entwickelt, weil sie jedes mal wirklich eine Abfrage gegen die Tabelle ausführt.

PerfConsole ist eine Konsolenanwendung, die die Klasse EmployeeUtil aus PerfLogic nutzt. Ich habe sie bewusst erweiterbar programmiert:

class Program
{
    #region fields

    private static AutoResetEvent[] _events;

    private static int _currentEvent = -1;

    #endregion

    #region methods

    static void Main(string[] args)
    {
        var tasks = new List<Tuple<Action, int>>
        {
            new Tuple<Action, int>(() => EmployeeUtil.GetEmployeesByJobTitleSlow(""), 100)
        };
        _events = new AutoResetEvent[tasks.Count];
        for (var i = 0; i < _events.Length; i++)
        {
            _events[i] = new AutoResetEvent(false);
        }
        tasks.ForEach(t => ThreadPool.QueueUserWorkItem(DoTask, t));
        Console.WriteLine("Threads started...");
        WaitHandle.WaitAll(_events);
    }

    /// <summary>
    /// Tests the performance of a given Action.
    /// </summary>
    /// <remarks>
    /// A boxed <see cref="Tuple{T,V}"/> where T is Action and V is int.
    /// </remarks>
    private static void DoTask(object state)
    {
        var info = state as Tuple<Action, int>;
        if (info == null)
        {
            throw new ArgumentException("State of wrong type!");
        }
        Console.WriteLine("Starting {0}...", info.Item1.Method.Name);
        // build the tasks
        var tasks = new List();
        for (var i = 0; i < info.Item2; i++)
        {
            tasks.Add(new Task(info.Item1));
        }
        // start the tasks
        tasks.ForEach(t => t.Start());
        // wait for completion
        Task.WaitAll(tasks.ToArray());
        Console.WriteLine("{0} finished.", info.Item1.Method.Name);
        // signal completion
        Interlocked.Increment(ref _currentEvent);
        _events[_currentEvent].Set();
    }

    #endregion
}

Zeile 17 in Listing 1 ist die interessanteste von allen. In ihr wird aktuell genau eine Aufgabe (EmployeeUtil.GetEmployeesByJobTitleSlow) zur 100maligen Ausführung erstellt. Die Liste tasks kann an dieser Stelle sehr einfach erweitert werden. Alle anderen Elemente, wie z.B. das Array der AutoResetEvents werden an der Ausprägung der tasks angelehnt.

Setzt man nun PerfConsole als Start-Projekt und führt es aus, wird GetEmployeesByJobTitleSlow 100 mal aufgerufen und gut.

So eine Konsolenanwendung ist ein guter Start für eine Profiling-Session. Sie führt etwas ausreichend oft aus. Das ist wichtig, um signifikante Aufruf-Statistiken zu erhalten und dies vor allem auch im Kontext häufiger Aufrufe. Mit solchen Konstrukten kann man später gut erkennen, ob sich z.B. irgendwelche Cachings positiv auswirken.

Das einfache Ausführen der Testschleife ist natürlich nicht das, was man Performance-Analyse nennt. Jetzt kommt das Tooling ins Spiel.

Ein guter Startpunkt hier ist der „Performance Explorer“. Ihn kann über „Analyze -> Windows -> Performance Explorer“ anzeigen. Ist in einer Solution noch nichts in diese Richtung eingestellt worden, ist das Fenster noch relativ übersichtlich. Ein Klick auf das „Actions“-Menu bietet aber erste Anhaltspunkte:

Abb. 6: Performance Explorer
Abb. 6: Performance Explorer

Ein Klick auf „Launch Performance Wizard…“ bietet einen sehr guten Einstieg über einen Assistenten an. Der erste Bildschirm des dreiteiligen Assistenten fordert uns auf, den Typ der neu zu erstellenden Performance-Messung zu definieren:

Abb. 7: Assistent Schritt 1
Abb. 7: Assistent Schritt 1

Ich habe mal farblich die Typen (s.o.) hervorgehoben. Empfohlen ist, mit dem CPU-Sampling zu beginnen. Für den ersten Test folge ich der Empfehlung und gehe weiter zu Schritt 2:

Abb. 8: Schritt 2
Abb. 8: Schritt 2

Der Assistent bietet uns hier die Möglichkeit, entweder ein geladenes Projekt oder eine beliebige .NET-Assembly oder Webanwendung als Ausgangspunkt zu nehmen. Hier kann man gut erkennen, dass die Performance-Analyse durchaus auch durchgeführt werden kann, wenn man nicht über eine geladene Solution verfügt. Man könnte auch eine heruntergeladene .NET-EXE verwenden und mal nachsehen, wie sich das Programm aus Performance-Gesichtspunkten verhält. Ich benutze natürlich meine PerfConsole und gehe weiter zum letzten Schritt:

Abb. 9: Schritt 3
Abb. 9: Schritt 3

Hier entferne ich den Haken bei der Launch-Option, weil ich nicht sofort mit der Messung beginnen möchte. Nach einem Klick auf „Finish“ ist der Performance Explorer mit einer neuen sog. Performance-Session bestückt:

Abb. 10: Performance Explorer nach Abschluss des Assistenten
Abb. 10: Performance Explorer nach Abschluss des Assistenten

Es können beliebig viele Sessions angelegt werden. Jede Session ist dabei auf einen bestimmten Messtyp (bei uns Sampling) festgelegt. Innerhalb einer Session gibt es ein oder mehrere Targets. Hätte ich beispielsweise mehrere ausführbare Projekte in meiner Solution, könnte ich der Session entsprechend mehr Targets zuweisen. Man könnte die Targets mit den Startup-Projekten einer Solution vergleichen. Den Targets werden wir später im Dialog für die Session-Optionen noch begegnen.

Der Reports-Bereich ist noch leer, denn in ihm werden die Ergebnis-Berichte zu den Performance-Messungen abgelegt und wir haben ja noch keinen Durchlauf gestartet. Das holen wir jetzt nach.

Ein wichtiger Hinweis an dieser Stelle ist der, dass man Performance-Messungen immer in einer Release-Configuration durchführen sollte, um die Messwerte durch Debugger-Einsprungpunkte nicht zu verwässern.

Ein Rechtsklick auf die Session bringt uns das folgende Kontextmenu:

Abb. 11: Session starten
Abb. 11: Session starten

Ein Klick auf „Start Profiling“ setzt nun die Messung in Gang. Ist man momentan nicht als Administrator am VS angemeldet, kommt noch eine Nachfrage, die man mit „Ja“ beantworten muss, damit es los geht:

Abb. 12: Admin-Rechte nötig
Abb. 12: Admin-Rechte nötig

Je nach Systemkonfiguration könnte hier noch mehr passieren. Ich nutze beispielsweise eine Extra-Firewall-Lösung. Dies führt dazu, dass noch eine Nachfrage erscheint, ob ich dem Perf-Programm den Zugriff auf das Netzwerk erlauben möchte. Während des Durchlaufs wird in meinem Fall mein Konsolenprogramm angezeigt. Im Visual Studio selbst ist ein Wartehinweis zu sehen:

Abb. 13: Die Analyse läuft
Abb. 13: Die Analyse läuft

Nach einigen Sekunden ist mein CPU-Sampling fertig und der Analyse-Report wird automatisch zum Performance Explorer hinzugefügt und mir angezeigt:

Abb. 14: Analyse-Report
Abb. 14: Analyse-Report

Analyse

Im Moment sehen wir die sog. Summary. Sie zeigt uns an, dass 1221 Samples gesammelt wurden. Die Werte werden als Inclusive und Exclusive angezeigt und keiner unserer „heißen Pfade“ hat exklusive Samples. Das bedeutet, dass unsere Methoden nicht wirklich selbst für das Aufkommen der ganzen Samples verantwortlich waren. Die Samples wurden vielmehr in den durch diese Methoden aufgerufenen Code-Einheiten (Children) gesammmelt.

Darunter werden uns die sog. Hot Paths aufgelistet. Es sind die Aufrufe im getesteten Code, zu denen die meisten Samples gesammelt werden konnten. In meinem Fall nicht weiter überraschend die Methode Main und eine DoTask-Methode.

Ein Rechtsklick auf eine dieser Methoden bringt ein Kontextmenu. Klickt man hier auf „View Source“, wird der Quellcode an der betreffenden Stelle geöffnet und markiert. Hier das Bild für „Program.DoTask“:

Abb. 15: DoTask-Quelle
Abb. 15: DoTask-Quelle

Wir befinden uns also an der Stelle, an der unser Code auf die Abarbeitung der gesammelten Aufträge wartet.

Sicherlich noch viel interessanter, weil detaillierter, ist die Liste der Funktionen mit der meisten geleisteten Arbeit unten im Bericht. Sie zeigt uns nun die Funktionen mit den inklusiven Samples an (s.o.). Hier ist also die echte Leistung erbracht worden. Über 72% unseres Codes beschäftigt sich mit der ToList-Methode von System.Linq.Enumerable. Nur ca. 27% der CPU-Last wurde durch Code aus dem Entity Framework erzeugt. Gut auch zu wissen, dass das Tasks.WaitAll den Prozessor nur mit 0,8% auslastete.

Der Summary-View eignet sich nun hervorragend zum Drilldown in die Tiefen der Aufruf-Stacks. Klicken wir z.B. auf den Hot Path „Program Main“, wechselt die Ansicht in den View „Function Details“:

Abb. 16: Function Details
Abb. 16: Function Details

Am oberen Rand erscheinen 3 Bereiche. Die Calling Function ist die WaitAll-Methode. Sie selbst ruft die Main-Methode auf. Das erscheint zunächst falsch herum, denn eigentlich ist doch Main unsere Start-Methode. Bei genauerer Überlegung stimmt es aber dann doch, weil ja WaitAll wartet, bis alle in der Main-Methode definierten Tasks fertig sind. Das erklärt auch, warum 99.9% der Ausführungs-CPU-Leistung in Zeile 27 (siehe Abb. 16 rot markiert) stattgefunden haben. Das ist schließlich die eigentliche Arbeitsmethode und daher wird sie auch als einzige Methode unter „Called functions“ (blauer rechter Block in Abb. 16) aufgeführt.

Ein Drilldown wäre kein solcher, wenn man nicht immer weiter vordringen könnte. Und dementsprechend klicke ich einfach auf den blauen Bereich oben rechts („GetEmployeesByJobTitleSlow“):

Abb. 17: Drilldown
Abb. 17: Drilldown

Jetzt wechselt die Ansicht „Function Details“ einfach den Scope. „Main“ ist jetzt die Calling function und „GetEmployeesByJobTitleSlow“ die Current. Wir sehen nun rechts, dass sie wiederum insgesamt 3 Methoden ruft. Die meisten Samplings erzeugt dabei „ToList“ (siehe auch unten die Darstellung der CPU-Auslastung dazu). Wir sehen außerdem, dass „AsNoTracking“ auch nicht ganz billig war. Und noch etwas sollte nun auffallen: Wir rufen das nicht ganz billige AsNoTracking doppelt auf. Das sollten wir uns näher ansehen.

Neuer Lauf

Ich glaube also, dass ich Verbesserungs-Potential in meinem Code entdeckt habe. Ich schließe den Report, und entferne das zweite AsNoTracking in der Methode. Dann lasse ich die Analyse-Session noch einmal laufen. Der neue Bericht liefert erste Aha-Erlebnisse:

Abb. 18: Neuer Lauf
Abb. 18: Neuer Lauf

Es gibt eine Verschiebung der Hot-Path-Anteile von Main zu DoTask. Die Funktionen werden nun mit 96% noch deutlicher von ToList dominiert. Um sich hier einen besseren Überblick verschaffen zu können, hat Microsoft rechts im Menu den Punkt „Compare Reports“ integriert.

Nachdem ich die Funktion ausgewählt habe, bietet mit das VS ein Fenster an, in dem ich 2 Berichtsdateien (*.vsp) auswählen muss. Standardmäßig ist der aktuelle Bericht als Baseline File ausgewählt, was ich aber gerade nicht will. Ich wähle hier den ersten Bericht aus und selektiere unter Comparison File den neuen:

Abb. 19: Bereichtsvergleich einrichten
Abb. 19: Bereichtsvergleich einrichten

Das Ergebnis ist ein sog. Comparison Report:

Abb. 20: Comparison Report
Abb. 20: Comparison Report

Er zeigt mir die relativen und absoluten Unterschiede in den Samplings an. Auch dieser Bericht ist interaktiv. In den Optionen oben rechts kann man z.B. unter „Column“ zwische Inclusive und Exclusive jeweils absolut und relativ wählen. Bei den absoluten Auswertungen sollte man aufpassen, um keine Fehlinterpretationen zu begehen. Ich empfehle grundsätzlich bei den relativen Auswertungen zu bleiben.

In unserem Beispiel würde man bei flüchtigem Blick vielleicht meinen, wir sollten AsNoTracking wieder rein nehmen, weil sich mehr verschlechtert als verbessert hat. Das stimmt aber so nicht. Wir sehen hier nichts weiter, als eine Verschiebung der Anteile der CPU-Zeit zwischen den unterschiedlichen Modulen. AsNoTracking hat 26% weniger Zeit in Anspruch genommen. Das führt natürlich zu einer Verlagerung der Verteilung hin zu den anderen Modulen.

Der Hintergrund der oftmals durch diese Vergleiche entstehenden Verwirrung ist, dass manche nicht zwischen Performance- und Last-Analyse unterscheiden. Wir bewerten hier nicht die Gesamtperformance unseres Codes sondern wollen entdecken, wo innerhalb des Programms Optimierungen ansetzen könnten. Es ist bei dieser Analyse egal, ob der Testlauf 10 Sekunden oder 10 Tage lief. Wichtig ist uns nur, wieviel Aufwand in den einzelnen Methoden steckte.

Optionen einstellen

Wer nun dachte, er würde nun alles über Performance-Analyse wissen, sieht sich nach einem Rechtsklick auf die Session im Performance Explorer und Auswahl von Properties leider schwer getäuscht:

Abb. 21: Session-Optionen
Abb. 21: Session-Optionen

Insgesamt 10 Options-Sektionen auf der linken Seite lassen sehr viel Spielraum für Anpassungen und Labor-Arbeit. Ich werde hier nicht durch alle Optionen führen, sondern mir einfach ein bis zwei Leckerbissen heraus greifen. Wer tiefer in die Materie einsteigen möchte, dem seien die Links unten ans Herz gelegt.

Eine meiner Lieblings-Optionen verbirgt sich im Bereich „Tier Interactions“. Hier gibt es derzeit einfach nur eine Option anzuhaken, die bewirkt, dass ADO.NET-Samplings und -Informationen mitgemessen werden. Setze ich den Haken in meinem Sample und führe dann eine erneute Messung durch, steht mir im Report die Sicht „Tier interactions“ zur Verfügung:

Abb. 22: Berichts-Sicht "Tier Interactions"
Abb. 22: Berichts-Sicht „Tier Interactions“

Er gibt mir nicht nur die ausgeführten ADO.NET-Abfragen und die damit zugebrachte Zeit aus, sondern zeigt mir die SQL-statements sogar an. Dies hilfreich zu nennen ist wohl eine ungemeine Untertreibung.

Ein weiteres Feature stellt die Möglichkeit dar, Windows-Performance-Counter mitzumessen. Im Optionsbereich „Windows Counters“ kann man hierzu die üblichen Verdächtigen der Liste hinzufügen. Im Ergebnisbericht ist dann die Sicht „Marks“ entscheidend:

Abb. 23: Die Report-Sicht Marks
Abb. 23: Die Report-Sicht Marks

Sie zeigt uns die in Intervallen gemessenen Werte der selektierten Performance-Counter in tabellarischer Form an und wir könnten nun ablesen, wie sich das System über die Zeit verhalten hat.

Fazit

Die Performance-Analyse ergänzt die Analyse-Funktionen von VS um ein wichtiges Instrument. Die Umsetzung ist durchdacht und VS-Like gemacht. Man kommt trotzdem nicht umhin (und das ist auch gut so), wissen zu müssen, was man da tut und warum. Es kann sehr schnell zu falschen Interpretationen kommen und gerade an dieser Stelle zeigt sich, dass .NET-Programmierer zu sein eben doch noch bedeutet, dass man eine tiefere Kenntnis der Basis-Technologien haben sollte. Wer mit Begriffen, wie ctor, CLR oder Pages per Seconds nichts anfangen kann, sollte erstmal an anderer Stelle nachholen.

Links

MSDN-Bereich für Performance-Analyse

Buchtipp: „Professional ALM wird Visual Studio 2012

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.