MSDN Magazin > Home > Ausgaben > 2007 > November >  Bugslayer: Messen der Auswirkungen des Ansichts...
Bugslayer
Messen der Auswirkungen des Ansichtstatus
John Robbins

Codedownload verfügbar unter: Bugslayer2007_11.exe (209 KB)
Browse the Code Online
Ist es nicht ein Witz, dass Microsoft® .NET Framework als Umgebung gilt, in der man sich über Speicher keine Sorgen mehr zu machen braucht? Was ist denn eines der größten Sorgenkinder, das die verwalteten Anwendungen von heute immer wieder plagt? Speicher! Warum? Wird zur Speicherfreigabe den Garbage Collector ausgeführt, hält die CLR (Common Language Runtime) alle Threads in der Anwendung an und bringt damit jede Arbeit zum Stillstand. Wenn keine Arbeit erledigt wird, liegt ein Leistungsproblem vor.
Wie sehr Sie sich auch um Ihre .NET Framework-Speicherauslastung kümmern mögen, werden Sie doch ständig durch einige größere Probleme daran gehindert, einen nützlichen Einblick in diesen Speicher zu gewinnen. Obwohl Ihnen .NET Framework ungeheuer viel Arbeit abnimmt, ist die Dokumentation nicht unbedingt sehr hilfreich, wenn Sie herausfinden möchten, welche Arbeit geleistet und wie viel Speicher beansprucht wird. Zum Glück leistet Reflector for .NET (aisto.com/roeder/dotnet) von Lutz Roeder hier Abhilfe.
Das zweite Problem bei der Überprüfung der Speicherauslastung besteht darin, dass Visual Studio®, das wichtigste Entwicklungstool für .NET, keine Informationen über den Speicher liefert. Das ist nicht der Fehler des Visual Studio-Teams, sondern ein Fehler der ursprünglichen CLR-Architektur, die über die CLR-Debug-API kein Hilfsmittel bereitstellt, um nachzusehen, welcher Generation ein bestimmtes Objekt angehört. Es ist zwar möglich, Objektgenerationen mithilfe der CLR-Profilerstellungs-API zu verfolgen, aber dies erfordert einen beträchtlichen Aufwand und funktioniert beim Debuggen nicht. Wie bereits in meinem Bugslayer-Artikel vom Juni 2003 (msdn.microsoft.com/msdnmag/issues/03/06/Bugslayer) und ein weiteres Mal im Bugslayer-Artikel vom März 2005 (msdn.microsoft.com/msdnmag/issues/05/03/Bugslayer) erwähnt, können Sie zwar mithilfe der WinDBG-Erweiterung SOS (Son of Strike) alle gewünschten Speicherinformationen sichtbar machen, aber dies ist wahrscheinlich das ärgerlichste Tool seit dem ursprünglichen Kerneldebugger von Windows® 95.
In dieser Ausgabe von Bugslayer möchte ich ein Tool vorstellen, mit dem Sie einen Blick auf den heimtückischsten Leistungskiller werfen können, den es in einer ASP.NET-Anwendung geben kann: den Ansichtstatus. Sie werden sehen, dass die Möglichkeit der Überprüfung der Ansichtstatusnutzung bei einem Produktionsserver ohne Weiteres den Unterschied zwischen dreißig Minuten Debugging und sechs Wochen Ratespielchen ausmachen kann.

Ansichtstatus
Auf einer ASP.NET-Seite ist der Ansichtstatus ein sehr raffinierter Mechanismus, bei dem der Status der auf der Seite enthaltenen Steuerelemente als versteckter Wert in dem an den Client gesendeten HTML-Code gespeichert wird. Dadurch erübrigt sich die Verwaltung des Seitenstatus im ASP.NET-Arbeitsprozess, was den Server einer noch viel höheren Speicherbelastung aussetzen würde. Wenn Sie jemals einen ISAPI-Filter bearbeitet haben, der in der Kreidezeit des Internets (so ungefähr um 1996) in systemeigenem C++ geschrieben wurde, dann wissen Sie, wie viel Aufwand betrieben wurde, um den Seiten-, Sitzungs- und Anwendungsstatus zu verfolgen. Wenn Sie mit ASP.NET nicht viel Erfahrung haben, sollten Sie den Artikel lesen, den Dino Esposito für die Cutting Edge-Ausgabe vom Februar 2003 als Einstieg in den Ansichtstatus geschrieben hat (msdn.microsoft.com/msdnmag/issues/03/02/cuttingedge).
Das Problem mit dem Ansichtstatus besteht darin, dass man nie die geringste Ahnung hat, wie er gerade ist. Dies trifft vor allem dann zu, wenn ein GridView-Steuerelement auf der Seite enthalten ist. Die Leichtigkeit, mit der man diese Datenbankinformationen erlangen kann, hat ihren Preis. Glücklicherweise haben sich die Entwickler von Microsoft im Vergleich zu ASP.NET 1.1 bei ASP.NET 2.0 viel Mühe gegeben, die Größe des Ansichtstatus möglichst gering zu halten. Trotzdem musste ich in den letzten Monaten helfen, mehrere umfangreiche ASP.NET 2.0-Anwendungen zu debuggen, bei denen die Leistungsprobleme durch die riesigen Ansichtsstatus bestimmter Seiten verursacht wurden.

Tipps zum Debuggen
Bevor wir uns dem Tool und dem Code zuwenden, will ich auf einige meiner Tricks zum Debuggen eingehen. Wenn sich Entwickler mit einem Leistungsproblem bei einer ASP.NET-Anwendung an mich wenden, sagen sie ausnahmslos: „Die Anwendung hat tadellos funktioniert, aber jetzt arbeitet sie unglaublich langsam.“
Meine erste Frage lautet: „Verwenden Sie in Ihrer Anwendung einige GridViews?“ Da es sich bei den meisten ASP.NET-Anwendungen um Front-Ends für Datenbanken handelt, ist das meistens, aber nicht immer, der Fall. Wenn sie keine GridViews verwenden, muss ich einen anderen Weg einschlagen.
Meine zweite Frage lautet: „Kann ich mir mal den Verlauf der gespeicherten Datenbankprozeduren in der Versionskontrolle ansehen?“ Dann schlucken leider viele Mitarbeiter der Entwicklungsteams schwer und erzählen mir, dass sich ihre gespeicherten Prozeduren nicht in der Versionskontrolle befinden. Eine gespeicherte SQL-Prozedur ist aber nach wie vor ein Code, und wenn Sie die Regeln für eine sinnvolle Programmentwicklung befolgen, landen alle Codes in der Versionskontrolle. Mit Visual Studio 2005 Team Edition for Database Professionals macht Microsoft dies sogar unglaublich einfach. Wenn sich Ihre gespeicherten Prozeduren nicht in der Versionskontrolle befinden, müssen Sie jetzt sofort aufhören zu lesen und sie der Versionskontrolle hinzufügen.
Ich bitte aus folgendem Grund darum, mir die gespeicherten Prozeduren in der Versionskontrolle ansehen zu können: Wenn die Anwendung tadellos läuft und irgendwann plötzlich Probleme auftreten, muss ich herausfinden, welche Änderungen zwischen den vorherigen und den aktuellen Ausgaben vorliegen. Schon allzu oft bin ich in Änderungsprotokollen zu den gespeicherten Prozeduren auf Stellen gestoßen, an denen jemand eine Abfrage etwa so umgeschrieben hat:
   select * from table
Wenn Sie diese Ausgabe in ein GridView-Steuerelement lenken, das sich in hohem Maß auf den Ansichtstatus verlässt, und die Datenbank sechs Millionen Datensätze enthält, bekommen Sie ernsthafte Probleme mit dem Ansichtstatus!
Obwohl der Ansichtstatus leicht zu verwenden ist, ist es fast unmöglich, zu erkennen, an welcher Stelle ein Problem aufgetreten ist. Die meisten Entwickler wissen, dass man den Ansichtstatus einer Seite sichtbar machen kann, indem man die ASP.NET-Ablaufverfolgung aktiviert. Leider speichert die ASP.NET-Ablaufverfolgung nur eine begrenzte Anzahl von Ablaufverfolgungen und arbeitet auf einem Produktionsserver unter Umständen extrem langsam. Ihre Alternative bestand darin, jeden Benutzer zu bitten, im Browser mit der rechten Maustaste zu klicken, im Kontextmenü „Quelltext anzeigen“ auszuwählen und die einzelnen Zeichen zu zählen. Es ist ganz klar, dass etwas Besseres benötigt wurde: eine einfache Möglichkeit, für alle Seiten auf einem Produktionsserver den größten Ansichtstatus zu verfolgen, damit man seine Daten an einem einzigen Ort sammeln und sinnvolle Debugentscheidungen treffen kann.

Ziele
Die Hauptzielsetzung bei einem entsprechenden Tool, so meine Überlegung, bestand in der Eignung für die Produktionsumgebung. Der Code muss also schnell arbeiten und gleichzeitig sparsam mit dem Speicher umgehen. Darum beschloss ich, für jede Seite nur die höchste Ansichtstatusgröße zu speichern. Wenn ich für jede Seite alle Ansichtstatusgrößen speichern wollte, sähe ich mich schnell gezwungen, Millionen von Datensätzen zu speichern. Durch das Speichern des jeweils größten Ansichtstatus lässt sich der Speicherbedarf enorm reduzieren und dafür sorgen, dass die Anzahl der Dateneinträge die der ASPX-Dateien in der ASP.NET-Anwendung nie übersteigt.
Darüber hinaus wollte ich die Ansichtstatusdaten auch selbst für Personen, die keine Entwickler sind, einfach und verständlich präsentieren. Es ist zwar schön, die gesamte Ansichtstatusgröße einer Seite zu sehen, aber ich wollte auch eine Option hinzufügen, mit der die Ansichtstatusgrößen aller auf einer Seite enthaltenen Steuerelemente gespeichert und angezeigt werden können. Dadurch kann das Steuerelement, das zu einem Problem am meisten beigetragen hat, leichter gefunden werden.
Zudem wollte ich die Ansichtstatusgröße von HTTP GET-Anforderungen sichtbar machen. Obwohl fast alle Ansichtstatusprobleme mit HTTP POST-Anforderungen in Verbindung stehen, können Benutzer die vollständige URL einer Seite speichern und diese direkt aufrufen. Dies kommt zwar selten vor, doch ich wollte diese Option der Vollständigkeit halber einbeziehen.
Zum Schluss wollte ich noch sicherstellen, dass das Tool leicht bereitgestellt werden kann. Sekundär wäre es ideal, wenn meine Lösung keine Änderungen am Quellcode erfordern würde.
Im Quellcodedownload für den Artikel dieses Monats ist die Assembly mit dem gesamten Zauberwerk enthalten: Bugslayer.Web. Ebenfalls enthalten sind die kompletten Komponententests, damit Sie mit der Assembly experimentieren und sehen können, wie sie funktioniert. Für Produktionsserver sollten Sie Bugslayer.Web.DLL im globalen Assemblycache (GAC) installieren, weil Sie dadurch den Code von jedem beliebigen Ort auf dem Computer aus laden können.
Bugslayer.Web besteht aus zwei Teilen: HttpModule und HttpHandler. Wenn Sie viel über die ASP.NET-Verarbeitungspipeline gelesen haben, können Sie wahrscheinlich erraten, dass HttpModule die Komponente ist, in der die Datensammlung erfolgt, und HttpHandler für das Anzeigen der Daten zuständig ist. Für den Einsatz von Bugslayer.Web sind daher keinerlei Codeänderungen Ihrerseits erforderlich.
Um Ihre ASP.NET-Anwendung für HttpModule zu konfigurieren, müssen Sie das Element <httpModules> folgendermaßen in Ihre Datei „web.config“ aufnehmen:
<?xml version="1.0"?>
<configuration>
  <system.web>
    <httpModules>
      <add name="ViewStateStats" 
          type="Bugslayer.Web.ViewStateStatisticsModule"/>
    </httpModules>
  </system.web>
</configuration>
HttpHandler lässt sich fast genauso leicht einrichten. Fügen Sie in Ihrer Datei „web.config“ das Element <httpHandlers> folgendermaßen hinzu:
<?xml version="1.0"?>
<configuration>
  <system.web>
    <httpHandlers>
      <add verb="*" 
           path="viewstatestats.axd"
           type="Bugslayer.Web.ViewStateStatisticsHandler" 
           validate="false" />
    </httpHandlers>
  </system.web>
</configuration>
Bugslayer.Web.ViewStateStatisticsHandler wird viewstatestats.axd zugeordnet. Ich habe die Erweiterung „.axd“ gewählt, weil diese von IIS bereits als ASP.NET-Erweiterungstyp erkannt wird. Durch Einstellen des Attributs „path“ auf einen bestimmten Dateinamen ruft ASP.NET HttpHandler auf, Anforderungen für andere Dateinamen werden jedoch an den internen .axd-Handler von ASP.NET weitergeleitet.

Ablaufverfolgung beim Ansichtstatus
Nach Installation von Bugslayer.Web und Hinzufügen der Einträge in der Datei „web.config“ wird für jede Seite der jeweils größte Ansichtstatus in Byte aufgezeichnet. Diese Daten müssen natürlich auch angezeigt werden können. Dafür ist, wie bereits erwähnt, ViewStateStats.axd zuständig. Abbildung 1 zeigt eine Beispielausgabe. Das Format dürfte Ihnen vertraut vorkommen, denn ich habe den Stil der Ausgabe der ASP.NET-Ablaufverfolgung nachgeahmt. Wie Sie sehen können, ist die Ausgabe standardmäßig so angeordnet, dass sie vom größten zum kleinsten Ansichtstatus verläuft. Wenn Sie aufsteigend nach URL oder Zeit sortieren möchten, dann klicken Sie auf die entsprechende Spaltenüberschrift.
Abbildung 1 Beispiel einer Ausgabe von ViewStateStats.axd (Klicken Sie zum Vergrößern auf das Bild)
Sie können die Sammlung der Ansichtstatusdaten im oberen Abschnitt der Seite „ViewStateStats.axd“ steuern. Die interessanteste Option ist der zweite Link „Collect Control View State Sizes“ (Ansichtstatusgrößen der Steuerelemente sammeln). Ist diese Option deaktiviert (Standardeinstellung), werden für die betreffende Seite nur Gesamtgrößen gesammelt. Wenn Sie auf den Link klicken, wird die Option aktiviert, sodass für den größten Gesamtansichtstatus jeder Seite auch eine Liste der Ansichtstatusgrößen der untergeordneten Steuerelemente erfasst wird. Obwohl dadurch die Leistung beeinträchtigt und mehr Speicher beansprucht wird, kommt es sehr häufig vor, dass Sie herausfinden müssen, welches Steuerelement den Ansichtstatus so stark aufbläht. Abbildung 2 zeigt das Beispiel der Seite „ViewStateStats.axd“ mit Angaben zu den Steuerelementen.
Abbildung 2 Baumansicht der Steuerelemente und Ansichtstatusgrößen (Klicken Sie zum Vergrößern auf das Bild)
Genau wie bei der ASP.NET-Ablaufverfolgung enthält die Baumansicht die Namen der Steuerelemente und die zugehörige Ansichtstatusgröße. Sie fragen sich vielleicht, wo die Bytes der Wiedergabe- und ControlState-Größe sind. Es gibt jedoch keine Möglichkeit, die Wiedergabedaten abzurufen, und ich hatte den Eindruck, die Berechnungen zur Ermittlung der ControlState-Größe würden das Tool zu stark bremsen. Wie ich zu dieser Auffassung gekommen bin, werde ich im Abschnitt über die Implementierung erläutern. Da der Ansichtstatus der größte Leistungskiller ist, ist er auch der wichtigste Parameter bei der Analyse des Beitrags der Steuerelemente. Beachten Sie, dass sich die einzelnen Steuerelementgrößen nicht zur gesamten Ansichtstatusgröße, die neben URL und Uhrzeit angezeigt wird, aufaddieren lassen. Da der Ansichtstatus Base64-codiert ist, fügt die Codierung dem Gesamtansichtstatus zusätzliche Bytes hinzu.
Der erste Link unterhalb des Titels „ViewStateStats.axd“ „Clear all view state statistics“ (Gesamte Ansichtstatusstatistik löschen) bewirkt, dass alle gespeicherten Werte gelöscht werden und von vorne begonnen wird. Mit dem letzten Link „Collect View State Sizes for HTTP GET Requests“ (Ansichtstatusgrößen für HTTP GET-Anforderungen sammeln) werden Berichte über HTTP GET-Anforderungen aktiviert. Da fast jedes durch einen Ansichtstatus hervorgerufene Problem auf einer HTTP POST-Anforderung beruht, werden Sie diese Option nur selten aktivieren müssen. Falls es trotzdem einmal notwendig sein sollte, die Ansichtstatusgrößen sowohl für HTTP GET- als auch für HTTP POST-Anforderungen sichtbar zu machen, dann haben Sie jedenfalls diese Möglichkeit. Wenn Sie diese Option aktivieren, wird auf der Seite „ViewStateStats.axd“ nach jeder URL entweder „(GET)“ oder „(POST)“ angezeigt. Die Erörterung meiner Methode der Berechnung der Ansichtstatusgröße von HTTP GET-Anforderungen wird zeigen, dass die Erfassung dieser Daten Auswirkungen auf die Leistung hat.

Das Wichtigste bei der Implementierung
Als ich mit dem Gedanken zu spielen begann, die Ansichtstatusgröße für eine ganze Anwendung zu erfassen, habe ich als Prototyp eine abgeleitete System.Web.UI.Page-Klasse erstellt. Es einfach, den Ansichtstatus mit dem folgenden C#-Code abzurufen:
String viewState = Request.Params [ "__VIEWSTATE" ];
Das Problem beim Verwenden einer abgeleiteten Seite besteht jedoch darin, dass zur anwendungsübergreifenden Anzeige des Ansichtstatus bei vielen vorhandenen ASP.NET-Anwendungen der Code geändert werden müsste. Da HttpModules dem Code ermöglichen, sich an der normalen ASP.NET-Pipeline zu beteiligen, war dies die ideale Lösung. Weil ich die Seite erst dann überprüfen wollte, wenn sie bereit war, zum Benutzer gesendet zu werden, habe ich in meiner Implementierungsmethode „IHttpModule.Init“ einen EndRequest-Ereignishandler festgelegt.
Beim Nachlesen, wie der ASP.NET-Ansichtstatus funktioniert, habe ich die folgende interessante web.config-Einstellung entdeckt:
<pages maxPageStateFieldLength="XX"/>
Hierbei steht „XX“ für die maximale Anzahl Bytes zum Aufteilen des Werts „__VIEWSTATE“ in mehrere Teile. Sie können dies auch mithilfe der Eigenschaft „System.Web.Ui.Page.MaxPageStateFieldLength“ einstellen.
In ASP.NET 2.0 wurde diese Einstellung eingeführt, um dabei zu helfen, riesige __VIEWSTATE-Werte aufzuteilen, weil manche Firewalls und Proxys riesige Zeichenfolgen auf der Seite nur schwer bewältigen können. Da riesige Zeichenfolgen zu vermeiden sind (was ja auch Thema dieses Artikels ist), könnte man vielleicht annehmen, dass das Einstellen der Eigenschaft „maxPageStateFieldLength“ zur Größenreduzierung beitragen könnte. Leider ist genau das Gegenteil der Fall, denn diese Einstellung bläht Ihre Seiten, die zum Browser geschickt werden, sogar noch zusätzlich auf.
Nehmen wir einmal an, die Seite besitzt folgenden Ansichtstatus:
<div>
<input type="hidden" name="__VIEWSTATE" id="__VIEWSTATE"
value="/wEPDwULLTE1ODEyMjM1MTgPZBYCAgMPZBYCAgcPEGQPFgNmAg­ECAhYDEAUBYQUBYWcQBQFiBQFiZxAFAW
MFAWNnZGRkQIU7yEIZAT5uuif4cZcFHBJgZ48=" />
</div>
Das Einstellen der Eigenschaft „maxPageStateFieldLength“ auf den Wert 50 liefert folgendes Ergebnis, bei dem sich wesentlich mehr Text als vorher auf der Seite befindet:
<div>
<input type="hidden" name="__VIEWSTATEFIELDCOUNT" 
       id="__VIEWSTATEFIELDCOUNT" value="3" />
<input type="hidden" name="__VIEWSTATE" 
       id="__VIEWSTATE" value="/wEPDwULLTE1ODEyMjM1MTgPZBYCAgMPZBYCAgcPEGQPFgNmAg" />
<input type="hidden" name="__VIEWSTATE1" 
       id="__VIEWSTATE1" value="ECAhYDEAUBYQUBYWcQBQFiBQFiZxAFAWMFAWNnZGRkQIU7yEIZ" />
<input type="hidden" name="__VIEWSTATE2" 
       id="__VIEWSTATE2" value="AT5uuif4cZcFHBJgZ48=" />
</div>
Es gibt wahrscheinlich einige Leser, die die Eigenschaft „maxPageStateFieldLength“ eingestellt haben, aber ich bezweifle, dass viele unter Ihnen diese Eigenschaft verwenden. Wenn Sie den Ansichtstatus aufteilen, um Firewall- und Proxyprobleme zu vermeiden, rate ich Ihnen dringend, eine Umgestaltung der Seite in Betracht zu ziehen oder Ihren Ansichtstatus auf dem Server zu speichern, um zu vermeiden, riesige Datenmengen zu übertragen.
Mein HttpModule, das in ViewStateStatisticsModule.cs implementiert ist, kann Fälle, in denen maxPageStateFieldLength eingestellt wurde, einwandfrei verarbeiten. Da ich jeden der __VIEWSTATE*-Parameter aufzählen muss, arbeitet der Code, den ich dazu verwende, ein wenig langsamer als beim Zugriff auf einen einzelnen __VIEWSTATE-Parameter.
Weil das Sammeln und das Anzeigen der Daten logisch gekoppelt sind, habe ich beschlossen, sie hinter der Schnittstelle „IViewStateProcessor“ zu gruppieren und anschließend diese gesamte Funktionalität von meinem HttpModule und meinem HttpHandler abzukoppeln. Die Schnittstelle „IViewStateProcessor“ bietet Methoden zum Verarbeiten von HTTP GET- und HTTP POST-Anforderungen sowie zum Anzeigen der Daten. Mein Ziel war, die Sammlung und Anzeige zusätzlicher Daten einfach zu gestalten, ohne dass dafür in Bugslayer.Web.dll viel Code bearbeitet werden muss.
Da die von IViewStateProcessor abgeleitete Klasse jederzeit im Speicher sein muss, speichere ich sie innerhalb meiner Klasse „ViewStateStatisticsModule“ in einem statischen Feld. Die Klasse „ViewStateStatisticsHandler“ greift einfach auf eine statische Eigenschaft in ViewStateStatisticsModule zu. Momentan unterstützt der Code nur eine einzelne von IViewStateProcessor abgeleitete Klasse. Die Möglichkeit, weitere Klassen gleichzeitig zu verwenden, wäre eine schöne Ergänzung.
Wenn Sie stattdessen Ihre eigene Klasse festlegen möchten, die von IViewStateProcessor abgeleitet wird, müssen Sie die Methode „HttpApplication.Init“ überschreiben. Dort greifen Sie über HttpApplication.Modules["ViewStateStats"] auf ViewStateStatisticsModule zu. Erstellen Sie eine Instanz Ihrer von IViewStateProcessor abgeleiteten Klasse, und stellen Sie ViewStateStatisticsModule.ViewStateProcessor auf diese Instanz ein.
Die von IViewStateProcessor abgeleitete Standardklasse „BiggestPageProcessor“ ist der Ort, an dem die gesamte eigentliche Arbeit zum Sammeln und Anzeigen der Daten stattfindet, die Sie in den Abbildungen 1 und 2 gesehen haben. Die Sammlungsdaten werden in einer Dictionaryklasse gespeichert, in der die URL selbst als Schlüssel für die schnelle Suche dient. Der interessante Teil beim Schreiben des Sammlungscodes bestand darin, herausfinden, wie die Ansichtsstatus einzelner Steuerelemente abgerufen werden können.
Da jeder daran gewöhnt ist, die Ansichtstatusgrößen für jedes einzelne Steuerelement zu sehen, wollte ich sicherstellen, dass meine Zahlen auch übereinstimmen. Dafür musste ich herausfinden, an welcher Stelle im Code der ASP.NET-Ablaufverfolgung diese Aktion stattfindet. Seinen ersten Halt machte Reflector bei System.Web.Handlers.TraceHandler. Das ist der für trace.axd registrierte HttpHandler. Nachdem ich den dekompilierten Code 15 Minuten lang untersucht und nach den Datentypen gesucht hatte, auf die verwiesen wurde, brachte mich dies zu System.Web.Ui.Page.ProcessRequestMain, wo die eigentliche Ablaufverfolgung stattfindet, falls Page.Context.TracingEnabled als „true“ ausgewertet wird. Nachdem ich Page.BuildPageProfileTree durchgegangen war, kam ich schließlich zu Control.BuildProfileTree, wo die Steuerelemente und ihre Daten abgerufen und zur Ablaufverfolgungsausgabe geleitet werden. Abbildung 3 zeigt den dekompilierten Code.
protected void BuildProfileTree(string parentId, bool calcViewState)
{
    int viewStateSize;
    calcViewState = calcViewState && !this.flags[4];
    if (calcViewState)
    {
        viewStateSize = this.EstimateStateSize(this.SaveViewState());
    }
    else
    {
        viewStateSize = 0;
    }
    int controlStateSize = 0;
    if (((this.Page != null) && 
         (this.Page._registeredControlsRequiringControlState != null)) && 
         this.Page._registeredControlsRequiringControlState.Contains(
         this))
    {
        controlStateSize = 
            this.EstimateStateSize(this.SaveControlStateInternal());
    }
    this.Page.Trace.AddNewControl(this.UniqueID, parentId, 
        base.GetType().FullName,  viewStateSize, controlStateSize);
    if ((this._occasionalFields != null) && 
        (this._occasionalFields.Controls != null))
    {
        int count = this._occasionalFields.Controls.Count;
        for (int i = 0; i < count; i++)
        {
            this._occasionalFields.Controls[i].BuildProfileTree(
                this.UniqueID, calcViewState);
        }
    }
}

Der für den Ansichtstatus interessante Teil des Codes sind die ersten zehn Zeilen. Was ich erstaunlich fand, ist der Verweis „!this.flags[4]“, weil Reflector das Kennzeichenfeld als privat ausweist. Mithilfe des rettenden Analyzer von Reflector bin ich zum Kennzeichenfeld gesprungen und habe auf das Strukturelement „Used By“ (Verwendet von) geklickt. Eine Suche nach allem, was mit dem Ansichtstatus zu tun hat, ergab, dass die Control.EnableViewState-Eigenschaft „getter“ in der Liste enthalten war. Ein Sprung zur dekompilierten EnableViewState-Methode „getter“ zeigte, dass die Methode tatsächlich „!this.flags[4]“ zurückgibt, und damit war das Rätsel gelöst.
Da EstimateStateSize lediglich einen ObjectStateFormatter erstellt, die Methode „Serialize“ aufruft und die Größe der Serialisierungsbytes zurückgibt, war es nicht schwer, dies nachzuvollziehen. Der Aufruf der geschützten Methode „this.SaveViewState“ war da schon interessanter. Ich wollte diese Methode über mein HttpModule aufrufen. Da .NET Reflection das Wundermittel ist, mit dem man einfach alles aufrufen kann, habe ich ein wenig davon verwendet, um sicherzustellen, dass ich die gleichen Zahlen wie die ASP.NET-Ablaufverfolgung erhalte. Abbildung 4 zeigt den Hauptcode, den ich geschrieben habe, um die Ansichtstatusgrößen einzelner Steuerelemente zu berechnen.
private PageViewState CreatePageViewState ( 
    String url ,
    DateTime timeStamp ,
    Int32 viewStateLength ,
    String httpRequestType )
{
    PageViewState returnValue = new PageViewState ( 
        url ,
        timeStamp ,
        viewStateLength ,
        httpRequestType );
    if ( true == collectControlData )
    {
        Page currentPage = HttpContext.Current.Handler as Page;
        if ( null != currentPage )
        {
            CalculateControlSizes ( currentPage ,
                                    0 ,
                                    returnValue.ControlList );
        }
    }
    return ( returnValue );
}

private static void CalculateControlSizes ( Control ctl ,
                                            Int32 indentLevel ,
                                            List<ControlViewState> ctlList )
{
    Int32 viewStateSize = EstimateSize ( ctl );
    ControlViewState state = new ControlViewState ( indentLevel ,
                                                ctl.UniqueID ,
                                                ctl.GetType ( ).FullName ,
                                                viewStateSize );
    // Add it to the list.
    ctlList.Add ( state );
    for ( int i = 0 ; i < ctl.Controls.Count ; i++ )
    {
        CalculateControlSizes ( ctl.Controls [ i ] , indentLevel + 1 , 
                                ctlList );
    }
}

private static Int32 EstimateSize ( Control ctl )
{
    // In Control.BuildProfileTree, the check is done against flag[4], 
    // which is the same as the implementation of EnableViewState. 
    if ( false == ctl.EnableViewState )
    {
        return ( 0 );
    }
    Object saved = CallSaveViewStateMethod ( ctl );
    if ( null == saved )
    {
        return ( 0 );
    }
    ObjectStateFormatter osf = new ObjectStateFormatter ( );
    Int32 ret = osf.Serialize ( saved ).Length;
    return ( ret );
}

private static Object CallSaveViewStateMethod ( Control ctl )
{
    // This is way ugly, but the SaveViewState method is protected on 
    // the Control class so in order to get the total size, I need to 
    // call it through Reflection.
    Object ret = null;
    BindingFlags flags = BindingFlags.Instance | BindingFlags.Public | 
                            BindingFlags.NonPublic;
    Type type = ctl.GetType ( );
    MethodInfo m = type.GetMethod ( "SaveViewState" , flags );
    if ( null != m )
    {
        ret = m.Invoke ( ctl , null );
    }
    return ( ret );
}

private Int32 SumControlSizes ( Control ctl )
{
    Int32 viewStateSize = EstimateSize ( ctl );
    for ( int i = 0 ; i < ctl.Controls.Count ; i++ )
    {
        viewStateSize += SumControlSizes ( ctl.Controls [ i ] );
    }
    return ( viewStateSize );
}

Nachdem ich die Lösung gefunden hatte, wie ich den Ansichtstatus genau so berechnen lassen kann, wie dies der Code der ASP.NET-Ablaufverfolgung tut, musste ich mit Tests beginnen, um zu prüfen, ob ich dieselben Ansichtstatuswerte erhalte, wenn ich sie innerhalb des Ereignishandlers „HttpModule.EndRequest“ anfordere. Ich hatte Glück, denn als ich alle möglichen Arten von Seiten durch Bugslayer.Web laufen ließ, bin ich nie auf eine Diskrepanz zwischen dem, was die Ablaufverfolgung meldete, und meinen eigenen Berechnungen gestoßen.
Wenn Sie wissen möchten, wieso ich keine Bytes für die Wiedergabegröße in die Anzeige aufgenommen habe, kann ich Ihnen nur sagen, dass es für mich einfach keine Möglichkeit gibt, diese Werte zu berechnen. Diese Berechnung findet während der Seitenwiedergabe statt, aber mein HttpModule befindet sich in der Pipeline viel zu weit hinten. Aus Gründen der Leistungserhaltung habe ich auch auf die Berechnung der Byte der ControlState-Größe verzichtet. Abbildung 3 zeigt, die dafür erforderliche Arbeit. Für diese Aufrufe hätte ich allerdings sehr viel mit langsamen Reflection-Vorgängen arbeiten müssen, und das Ergebnis hätte sich kaum gelohnt.
Sehr aufmerksame Leser haben vielleicht bemerkt, dass ich geschrieben habe, beim Abrufen des Ansichtstatus durch „Request.Params["__VIEWSTATE"]“ werde auf eine Formularvariable zugegriffen, die bei einer HTTP GET-Anforderung nicht existiere. Wenn Sie sich jedoch den Quellcode einer Seite im Browser ansehen, werden Sie feststellen, dass die Variable „__VIEWSTATE“ tatsächlich vorhanden ist. Daher wollte ich für den Fall, dass ein Benutzer direkt über die Adressleiste des Browsers zu einer Seite springt, eine Möglichkeit bieten, diese Daten abzurufen.
Nachdem ich alles Mögliche ausprobiert habe, bestand die beste Lösung darin, die Berechnungen selbst durchzuführen, indem ich in einer Schleife die Steuerelemente durcharbeite und jede einzelne Ansichtstatusgröße abrufe. Diese Lösung ist zwar nicht perfekt, aber damit kommen wir dicht an die tatsächliche Ansichtstatusgröße heran, ohne mit kompletter BASE64-Codierung und dem damit verbundenen zusätzlichen Aufwand arbeiten zu müssen. Aufgrund dieser Beschränkung der HTTP GET-Verarbeitung ließ ich die Option zu deren Überwachen standardmäßig deaktiviert.

Weitere Ideen
Alles in allem bin ich recht zufrieden damit, wie sich das Tool zur Ansichtstatusüberwachung entwickelt hat. Da es sich sehr leicht in eine bestehende Produktionsanwendung integrieren lässt, müsste ich mein Ziel, aufgeblähte Ansichtsstatus zu finden, wesentlich einfacher erreichen können. Die Möglichkeit, die Ansichtstatusgröße zu prüfen und so heimliche Leistungskiller schneller zu entlarven und zu beseitigen bedeutet, dass Ihnen mehr Zeit für die Arbeit an neuen und interessanten Features bleibt, deretwegen wir alle diesen Beruf ja schließlich gewählt haben.
Wie bei allen Bugslayer-Artikeln gibt es auch hier zusätzliche Funktionen, um die Sie Bugslayer.Web erweitern können, damit Sie noch mehr Informationen zu Ihren Ansichtstatusproblemfällen verfolgen können.
Die zusammen mit diesem Artikel bereitgestellte Version des Tools verfolgt lediglich den Ansichtstatus, der in Form eines ausgeblendeten Felds „__VIEWSTATE“ auf der Seite enthalten ist, weil dies der schlimmste aller Leistungskiller ist. Sie können Bugslayer.Web bei Bedarf erweitern, um Ansichtstatusinformationen zu verfolgen, die auf dem Server gespeichert sind. Das erfordert zusätzliche Arbeit, denn es muss eine von der Klasse „Page“ abgeleitete benutzerdefinierte Klasse erstellt werden, die dann Vorrang vor der Methode „Page.SaveViewState“ hat.
In dem aufgeführten Beispiel arbeitet der Ansichtstatusspeicher lediglich auf einer HttpApplication-orientierten Basis. Es wäre eine gute Idee für eine Erweiterung, den größten Ansichtstatus in einer ganzen Webfarm zu verfolgen.
Da ich den Steuerelementbaum ähnlich gestalten wollte wie die Ausgabe der ASP.NET-Ablaufverfolgung, beziehe ich Steuerelemente ein, die keine Ansichtstatusdaten besitzen. Eine leicht vorzunehmende Optimierung würde also darin bestehen, lediglich diejenigen Steuerelemente zu berücksichtigen, die zum Ansichtstatus beitragen. Noch besser wäre es, dies als umschaltbare Option im Handler „ViewStateStats.axd“ einzubauen.
Wenn Sie ein echter HTML- und JavaScript-Künstler sind, können Sie die Anzeige der Seite „ViewStateStats.axd“ bestimmt verbessern. Ich habe eine ganz einfache Anzeigeform gewählt, doch eine Steuerelementanzeige mit Möglichkeiten zum Ein- und Ausblenden würde die Anzeige sehr umfangreicher Websitedaten wesentlich übersichtlicher gestalten.
Zum Schluss möchte ich mich noch bei allen Bugslayer-Lesern herzlich für ihre Treue in den letzten zehn Jahren bedanken. Dank aller Fragen und Kommentare, die Sie im Lauf der Jahre eingesendet haben, macht das Verfassen dieser Artikel viel mehr Spaß gemacht, als Sie denken mögen.
Tipp 80 Vor kurzem habe ich an einem Problem gearbeitet, bei dem ein Anwendungspool in IIS völlig überraschend neu gestartet wurde. Bei meiner verzweifelten Internetsuche bin ich unter blogs.msdn.com/johan/archive/2007/05/16/common-reasons-why-your-application-pool-may-unexpectedly-recycle.aspx auf den sehr guten Blogeintrag „Common reasons why your application pool may unexpectedly recycle“ von Johan Straarup gestoßen. Dieser Eintrag bietet eine sehr nützliche Zusammenfassung aller denkbaren Ursachen für eine Fehlfunktion bei W3WP.EXE. Den Blog von Straarup müssen Sie unbedingt abonnieren.
Tipp 81 Möchten Sie ein besserer Debugger werden? Roberto Farah, der wesentlich mehr über WinDBG vergessen hat, als einer von uns jemals darüber wissen wird, hat unter blogs.msdn.com/debuggingtoolbox/archive/2007/06/08/recommended-books-how-to-acquire-or-improve-debugging-skills.aspx eine der besten Bücherlisten zum Thema Debugging zusammengestellt, die ich je gesehen habe. Welche Art von Entwicklung Sie auch immer unter Windows betreiben, Farah hat genau die richtigen Bücher für Sie parat. In seinem Blog stellt er mit WinDBG-Skriptprogrammierung Dinge an, bei denen Sie nicht mehr aus dem Staunen herauskommen werden! Mein persönlicher Favorit ist sein WinDBG-Skript, mit dem man bei Mine Sweeper schummeln kann (blogs.msdn.com/debuggingtoolbox/archive/2007/03/28/windbg-script-playing-with-minesweeper.aspx). Jetzt gewinne ich jedes Mal!
Tipp 82 Wenn Sie die bisherigen Bugslayer-Artikel gelesen haben, wissen Sie, dass ich ganz verrückt nach MSBuild-Aufgaben bin (msdn.microsoft.com/msdnmag/issues/06/03/Bugslayer). Sie sollten die Welt mithilfe von MSBuild automatisieren. Die SDC-Aufgaben wurden aktualisiert und umfassen jetzt über 300 Aufgaben, wie z. B. zum Erstellen von Websites, Anwendungspools und Active Directory-Benutzerkonten, zum Ausführen von FxCop, zum Konfigurieren virtueller Server, zum Erstellen von ZIP-Dateien, zum Konfigurieren von COM+, zum Erstellen von Ordnerfreigaben, zum Installieren im GAC, zum Konfigurieren von SQL Server und zum Konfigurieren von BizTalk® Server 2004 und BizTalk Server 2006. Wenn Sie Ihr Leben per automatisierten Build steuern möchten, laden Sie die neuen Aufgaben von codeplex.com/sdctasks herunter.

Senden Sie Fragen und Kommentare für John Robbins in englischer Sprache an slayer@microsoft.com.


John Robbins ist Mitbegründer von Wintellect, einer Firma für Softwareberatung, -schulung und -entwicklung mit Spezialisierung auf .NET- und Windows-Plattformen. Sein neuestes Buch heißt „Debugging Microsoft .NET 2.0 Applications“ (Microsoft Press, 2006). Sie können über wintellect.com

Page view tracker