Leistungsmetriken für die Suche nach unglaublich schnellen Webanwendungen

Es gibt ein Sprichwort: "Was Sie nicht messen können, können Sie nicht verbessern." Der Autor des Artikels, dessen Übersetzung wir heute veröffentlichen, arbeitet für Superhuman . Er sagt, dass dieses Unternehmen den schnellsten E-Mail-Client der Welt entwickelt. Hier werden wir darüber sprechen, was „schnell“ ist und wie Tools zur Messung der Leistung unglaublich schneller Webanwendungen erstellt werden.



Anwendungsgeschwindigkeitsmessung


Um unsere Entwicklung zu verbessern, haben wir viel Zeit damit verbracht, die Geschwindigkeit zu messen. Und wie sich herausstellte, sind Leistungsmetriken Indikatoren, die überraschend schwer zu verstehen und anzuwenden sind.

Einerseits ist es schwierig, Metriken zu entwerfen, die die Empfindungen, die der Benutzer während der Arbeit mit dem System erfährt, genau beschreiben. Andererseits ist es nicht einfach, Metriken zu erstellen, die so genau sind, dass Sie mit ihrer Analyse fundierte Entscheidungen treffen können. Infolgedessen können viele Entwicklungsteams den Daten, die sie über die Leistung ihrer Projekte sammeln, nicht vertrauen.

Selbst wenn Entwickler über zuverlässige und genaue Metriken verfügen, ist deren Verwendung nicht einfach. Wie definiere ich den Begriff „schnell“? Wie finde ich ein Gleichgewicht zwischen Geschwindigkeit und Beständigkeit? Wie kann man lernen, Leistungseinbußen schnell zu erkennen oder die Auswirkungen von Optimierungen auf das System zu bewerten?

Hier möchten wir einige Gedanken zur Entwicklung von Tools zur Leistungsanalyse von Webanwendungen teilen.

1. Mit der richtigen "Uhr"


JavaScript verfügt über zwei Mechanismen zum Abrufen von Zeitstempeln: performance.now() und new Date() .

Wie unterscheiden sie sich? Die folgenden zwei Unterschiede sind für uns von grundlegender Bedeutung:

  • Die Methode performance.now() ist viel genauer. Die Genauigkeit des new Date() -Konstrukts beträgt ± 1 ms, während die Genauigkeit von performance.now() bereits ± 100 µs beträgt (ja, es geht um Mikrosekunden !).
  • Die von der Methode performance.now() Werte steigen immer mit konstanter Geschwindigkeit und sind unabhängig von der Systemzeit. Diese Methode misst einfach Zeitintervalle, ohne sich auf die Systemzeit zu konzentrieren. Und am new Date() wirkt sich new Date() Systemzeit aus. Wenn Sie die Systemuhr neu anordnen, ändert sich auch die Rückgabe von new Date () , wodurch die Leistungsüberwachungsdaten zerstört werden.

Obwohl die durch die Methode performance.now() „Uhren“ offensichtlich viel besser zum Messen von Zeitintervallen geeignet sind, sind sie auch nicht ideal. Sowohl performance.now() als auch new Date() leiden unter demselben Problem, das sich in dem Fall äußert, dass sich das System im Ruhezustand befindet: Die Messungen umfassen die Zeit, zu der die Maschine noch nicht einmal aktiv war.

2. Überprüfen der Anwendungsaktivität


Wenn Sie die Leistung einer Webanwendung messen und von der Registerkarte zu einer anderen wechseln, wird der Datenerfassungsprozess unterbrochen. Warum? Tatsache ist, dass der Browser die Anwendungen auf den Hintergrundregistern einschränkt.

Es gibt zwei Situationen, in denen Metriken verzerrt sein können. Infolgedessen erscheint die Anwendung viel langsamer als sie tatsächlich ist.

  1. Der Computer wechselt in den Ruhemodus.
  2. Die Anwendung wird auf der Registerkarte "Hintergrund" des Browsers ausgeführt.

Das Auftreten dieser beiden Situationen ist nicht ungewöhnlich. Glücklicherweise haben wir zwei Möglichkeiten, sie zu lösen.

Erstens können wir verzerrte Metriken einfach ignorieren und Messergebnisse verwerfen, die zu stark von einigen vernünftigen Werten abweichen. Zum Beispiel kann der Code, der beim Drücken einer Taste aufgerufen wird, 15 Minuten lang nicht ausgeführt werden! Vielleicht ist dies das einzige, was Sie brauchen, um die beiden oben beschriebenen Probleme zu lösen.

Zweitens können Sie die Eigenschaft document.hidden und das Ereignis Sichtbarkeitsänderung verwenden . Das visibilitychange wird ausgelöst, wenn der Benutzer von der gewünschten Browser-Registerkarte zu einer anderen wechselt oder zur für uns interessanten Registerkarte zurückkehrt. Es wird aufgerufen, wenn das Browserfenster minimiert oder maximiert, wenn der Computer zu arbeiten beginnt und den Ruhemodus verlässt. Mit anderen Worten, genau das brauchen wir. Außerdem befindet sich die Eigenschaft document.hidden true , solange sich die Registerkarte im Hintergrund befindet.

Hier ist ein einfaches Beispiel, das die Verwendung der Eigenschaft document.hidden und des Ereignisses " visibilitychange Change" demonstriert.

 let lastVisibilityChange = 0 window.addEventListener('visibilitychange', () => {  lastVisibilityChange = performance.now() }) //    ,      , //  ,   ,     if (metric.start < lastVisibilityChange || document.hidden) return 

Wie Sie sehen können, verwerfen wir einige Daten, aber das ist gut. Tatsache ist, dass dies Daten sind, die sich auf jene Zeiträume des Programms beziehen, in denen die Ressourcen des Systems nicht vollständig genutzt werden können.

Jetzt haben wir über Indikatoren gesprochen, die uns nicht interessieren. Es gibt jedoch viele Situationen, in denen die gesammelten Daten für uns sehr interessant sind. Schauen wir uns an, wie diese Daten gesammelt werden.

3. Suchen Sie nach dem Indikator, mit dem Sie den Zeitpunkt des Ereignisses am besten erfassen können


Eine der umstrittensten Funktionen von JavaScript ist, dass die Ereignisschleife für diese Sprache Single-Threaded ist. Zu einem bestimmten Zeitpunkt kann nur ein Code ausgeführt werden, dessen Ausführung nicht unterbrochen werden kann.

Wenn der Benutzer während der Ausführung eines bestimmten Codes die Taste drückt, weiß das Programm nichts davon, bis die Ausführung dieses Codes abgeschlossen ist. Wenn die Anwendung beispielsweise 1000 ms in einem kontinuierlichen Zyklus verbracht hat und der Benutzer 100 ms nach Beginn des Zyklus die Escape Taste gedrückt hat, wird das Ereignis für weitere 900 ms nicht aufgezeichnet.

Dies kann Metriken stark verzerren. Wenn wir genau messen müssen, wie der Benutzer die Arbeit mit dem Programm wahrnimmt, ist dies ein großes Problem!

Glücklicherweise ist die Lösung dieses Problems nicht so schwierig. Wenn es sich um das aktuelle Ereignis handelt, können wir anstelle von performance.now() (dem Zeitpunkt, zu dem wir das Ereignis gesehen haben) window.event.timeStamp (dem Zeitpunkt, zu dem das Ereignis erstellt wurde) verwenden.

Der Zeitstempel des Ereignisses wird vom Hauptbrowserprozess festgelegt. Da dieser Prozess nicht blockiert, wenn die JS-Ereignisschleife gesperrt ist, liefert event.timeStamp viel wertvollere Informationen darüber, wann das Ereignis tatsächlich ausgelöst wurde.

Es ist zu beachten, dass dieser Mechanismus nicht ideal ist. Zwischen dem Drücken der physischen Taste und dem Eintreffen des entsprechenden Ereignisses in Chrome vergehen 9 bis 15 ms nicht berücksichtigter Zeit ( hier ist ein ausgezeichneter Artikel, in dem Sie erfahren können, warum dies geschieht).

Selbst wenn wir die Zeit messen können, die das Ereignis benötigt, um Chrome zu erreichen, sollten wir diese Zeit nicht in unsere Metriken einbeziehen. Warum? Tatsache ist, dass wir solche Optimierungen nicht in den Code einführen können, die solche Verzögerungen erheblich beeinflussen können. Wir können sie in keiner Weise verbessern.

Wenn wir also über das Finden des Zeitstempels für den Beginn des Ereignisses event.timeStamp , sieht die event.timeStamp Anzeige hier am besten aus.

Was ist die beste Schätzung, wann die Veranstaltung endet?

4. Schalten Sie den Timer in requestAnimationFrame () aus.


Eine weitere Konsequenz ergibt sich aus den Funktionen des Ereignisschleifengeräts in JavaScript: Einige Codes, die nicht mit Ihrem Code zusammenhängen, können danach ausgeführt werden, bevor der Browser eine aktualisierte Version der Seite auf dem Bildschirm anzeigt.

Betrachten Sie zum Beispiel Reagieren. Nach der Ausführung Ihres Codes aktualisiert React das DOM. Wenn Sie nur die Zeit in Ihrem Code messen, bedeutet dies, dass Sie nicht die Zeit messen, die zum Ausführen des React-Codes benötigt wurde.

Um diese zusätzliche Zeit zu messen, verwenden wir requestAnimationFrame() , um den Timer auszuschalten. Dies erfolgt nur, wenn der Browser bereit ist, das nächste Bild auszugeben.

 requestAnimationFrame(() => { metric.finish(performance.now()) }) 

Hier ist der Lebenszyklus des Rahmens (das Diagramm stammt aus diesem wunderbaren Material auf requestAnimationFrame ).


Rahmenlebenszyklus

Wie Sie in dieser Abbildung sehen können, wird requestAnimationFrame() aufgerufen, nachdem der Prozessor abgeschlossen wurde, unmittelbar bevor der Frame angezeigt wird. Wenn wir hier den Timer ausschalten, können wir absolut sicher sein, dass alles, was die Zeit zum Aktualisieren des Bildschirms in Anspruch genommen hat, in den gesammelten Daten des Zeitintervalls enthalten ist.

So weit so gut, aber jetzt wird die Situation ziemlich kompliziert ...

5. Ignorieren Sie die Zeit, die zum Erstellen eines Seitenlayouts und seiner Visualisierung erforderlich ist.


Das vorherige Diagramm, das den Lebenszyklus eines Frames zeigt, zeigt ein weiteres Problem, auf das wir gestoßen sind. Am Ende des Lebenszyklus des Frames befinden sich Layoutblöcke (bilden ein Seitenlayout) und Malen (Anzeigen einer Seite). Wenn Sie die für diese Vorgänge erforderliche Zeit nicht berücksichtigen, ist die von uns gemessene Zeit kürzer als die Zeit, die einige aktualisierte Daten benötigen, um auf dem Bildschirm angezeigt zu werden.

Zum Glück hat requestAnimationFrame ein weiteres Ass im Ärmel. Wenn die von requestAnimationFrame Funktion requestAnimationFrame , wird dieser Funktion ein Zeitstempel übergeben, der die Startzeit für die Bildung des aktuellen Frames requestAnimationFrame die im linken Teil unseres Diagramms befindliche). Dieser Zeitstempel liegt normalerweise sehr nahe an der Endzeit des vorherigen Frames.

Infolgedessen kann der obige Nachteil behoben werden, indem die Gesamtzeit gemessen wird, die vom Moment des event.timeStamp bis zum Zeitpunkt des event.timeStamp der Bildung des nächsten Rahmens verstrichen ist. Beachten Sie den verschachtelten requestAnimationFrame :

 requestAnimationFrame(() => {  requestAnimationFrame((timestamp) => { metric.finish(timestamp) }) }) 

Obwohl das oben gezeigte eine hervorragende Lösung für das Problem darstellt, haben wir uns letztendlich entschieden, dieses Design nicht zu verwenden. Tatsache ist, dass, obwohl diese Technik es ermöglicht, zuverlässigere Daten zu erhalten, die Genauigkeit solcher Daten verringert ist. Frames in Chrome werden mit einer Frequenz von 16 ms gebildet. Dies bedeutet, dass die höchste uns zur Verfügung stehende Genauigkeit ± 16 ms beträgt. Und wenn der Browser überlastet ist und Frames überspringt, ist die Genauigkeit noch geringer und diese Verschlechterung ist unvorhersehbar.

Wenn Sie diese Lösung implementieren, wirkt sich eine ernsthafte Verbesserung der Leistung Ihres Codes, z. B. die Beschleunigung einer Aufgabe, die zuvor um 32 ms bis zu 15 ms ausgeführt wurde, möglicherweise nicht auf die Ergebnisse der Leistungsmessung aus.

Ohne Berücksichtigung der Zeit, die zum Erstellen eines Seitenlayouts und seiner Ausgabe erforderlich ist, erhalten wir viel genauere Metriken (± 100 μs) für den Code, den wir steuern. Infolgedessen können wir einen numerischen Ausdruck jeder Verbesserung erhalten, die an diesem Code vorgenommen wurde.

Wir haben auch eine ähnliche Idee untersucht:

 requestAnimationFrame(() => {  setTimeout(() => { metric.finish(performance.now()) } }) 

Dies schließt die Renderzeit ein, aber die Genauigkeit des Indikators ist nicht auf ± 16 ms beschränkt. Wir haben uns jedoch auch entschieden, diesen Ansatz nicht zu verwenden. Wenn das System auf ein langes Eingabeereignis stößt, kann der Aufruf von setTimeout erheblich verzögert und ausgeführt werden, nachdem die Benutzeroberfläche aktualisiert wurde.

6. Klärung des „Prozentsatzes der Ereignisse, die unter dem Ziel liegen“


Wir entwickeln ein Projekt, konzentrieren uns auf hohe Leistung und versuchen, es auf zwei Arten zu optimieren:

  1. Geschwindigkeit. Die Ausführungszeit der schnellsten Aufgabe sollte so nahe wie möglich bei 0 ms liegen.
  2. Einheitlichkeit. Die Ausführungszeit der langsamsten Aufgabe sollte so nahe wie möglich an der Ausführungszeit der schnellsten Aufgabe liegen.

Aufgrund der Tatsache, dass sich diese Indikatoren im Laufe der Zeit ändern, sind sie schwer zu visualisieren und nicht leicht zu diskutieren. Ist es möglich, ein System zur Visualisierung solcher Indikatoren zu erstellen, das uns dazu inspirieren würde, sowohl Geschwindigkeit als auch Gleichmäßigkeit zu optimieren?

Ein typischer Ansatz besteht darin, das 90. Perzentil der Verzögerung zu messen. Mit diesem Ansatz können Sie ein Liniendiagramm entlang der Y-Achse zeichnen, dessen Zeit in Millisekunden gespeichert wird. In diesem Diagramm können Sie sehen, dass 90% der Ereignisse unterhalb des Liniendiagramms liegen, dh, sie werden schneller ausgeführt als die im Liniendiagramm angegebene Zeit.

Es ist bekannt, dass 100 ms die Grenze zwischen dem ist, was als "schnell" und "langsam" wahrgenommen wird.

Aber was werden wir darüber herausfinden, wie sich Benutzer von der Arbeit fühlen, wenn wir wissen, dass das 90. Perzentil der Verzögerung 103 ms beträgt? Nicht besonders viel. Welche Indikatoren bieten Benutzern Benutzerfreundlichkeit? Es gibt keine Möglichkeit, dies sicher zu wissen.

Aber was ist, wenn wir wissen, dass das 90. Perzentil der Verzögerung 93 ms beträgt? Es besteht das Gefühl, dass 93 besser als 103 ist, aber wir können nichts mehr über diese Indikatoren sagen und darüber, was sie für die Wahrnehmung des Projekts durch die Benutzer bedeuten. Auch hier gibt es keine genaue Antwort auf diese Frage.

Wir haben eine Lösung für dieses Problem gefunden. Es besteht darin, den Prozentsatz der Ereignisse zu messen, deren Ausführungszeit 100 ms nicht überschreitet. Dieser Ansatz bietet drei große Vorteile:

  • Die Metrik ist benutzerorientiert. Sie kann uns sagen, wie viel Prozent der Zeit unsere Anwendung schnell ist und wie viel Prozent der Benutzer sie als schnell wahrnehmen.
  • Mit dieser Metrik können wir die Messungen auf die Genauigkeit zurücksetzen, die aufgrund der Tatsache verloren gegangen ist, dass wir die Zeit, die für die Ausführung der Aufgaben am Ende des Frames benötigt wurde, nicht gemessen haben (darüber haben wir in Abschnitt Nr. 5 gesprochen). Aufgrund der Tatsache, dass wir einen Zielindikator festlegen, der in mehrere Frames passt, sind die Messergebnisse, die diesem Indikator nahe kommen, entweder geringer oder höher.
  • Diese Metrik ist einfacher zu berechnen. Es reicht aus, einfach die Anzahl der Ereignisse zu berechnen, deren Ausführungszeit unter dem Zielindikator liegt, und sie anschließend durch die Gesamtzahl der Ereignisse zu dividieren. Perzentile sind viel schwieriger zu zählen. Es gibt effektive Annäherungen, aber um alles richtig zu machen, müssen Sie jede Dimension berücksichtigen.

Dieser Ansatz hat nur ein Minus: Wenn die Indikatoren schlechter als das Ziel sind, ist es nicht leicht, ihre Verbesserung zu bemerken.

7. Verwendung mehrerer Schwellenwerte bei der Analyse von Indikatoren


Um das Ergebnis der Leistungsoptimierung zu visualisieren, haben wir mehrere zusätzliche Schwellenwerte in unser System eingeführt - über 100 ms und darunter.

Wir haben die Verzögerungen folgendermaßen gruppiert:

  • Weniger als 50 ms (schnell).
  • 50 bis 100 ms (gut).
  • 100 bis 1000 ms (langsam).
  • Mehr als 1000 ms (furchtbar langsam).

"Schrecklich langsame" Ergebnisse lassen uns erkennen, dass wir irgendwo sehr viel verpasst haben. Deshalb markieren wir sie hellrot.

Was in 50 ms passt, reagiert sehr empfindlich auf Änderungen. Hier sind Leistungsverbesserungen oft sichtbar, lange bevor sie in einer Gruppe sichtbar werden, die 100 ms entspricht.

Das folgende Diagramm zeigt beispielsweise die Leistung der Thread-Anzeige in Superhuman.


Thread anzeigen

Es zeigt den Zeitraum des Leistungsabfalls und dann - die Ergebnisse von Verbesserungen. Es ist schwierig, den Leistungsabfall zu bewerten, wenn Sie nur Indikatoren betrachten, die 100 ms entsprechen (die oberen Teile der blauen Spalten). Bei Betrachtung der Ergebnisse, die in 50 ms passen (die oberen Teile der grünen Spalten), sind Leistungsprobleme bereits viel deutlicher sichtbar.

Wenn wir den traditionellen Ansatz zur Untersuchung von Leistungsmetriken verwendet hätten, hätten wir wahrscheinlich kein Problem bemerkt, dessen Auswirkungen auf das System in der vorherigen Abbildung dargestellt sind. Dank der Art und Weise, wie wir Messungen durchführen und unsere Metriken visualisieren, konnten wir ein Problem sehr schnell finden und lösen.

Zusammenfassung


Es stellte sich heraus, dass es überraschend schwierig war, den richtigen Ansatz für die Arbeit mit Leistungsmetriken zu finden. Es ist uns gelungen, eine Methodik zu entwickeln, mit der wir hochwertige Tools zur Messung der Leistung von Webanwendungen erstellen können. Wir sprechen nämlich über Folgendes:

  1. Die Startzeit eines Ereignisses wird mit event.timeStamp gemessen.
  2. Die Ereignisendzeit wird mithilfe von performance.now() in dem an requestAnimationFrame() Rückruf requestAnimationFrame() .
  3. Alles, was mit der Anwendung passiert, während sie sich auf der inaktiven Browserregisterkarte befindet, wird ignoriert.
  4. Die Daten werden mithilfe eines Indikators aggregiert, der als „Prozentsatz der Ereignisse, die unter dem Ziel liegen“ beschrieben werden kann.
  5. Die Daten werden mit mehreren Schwellenwerten dargestellt.

Diese Technik bietet Ihnen die Werkzeuge, um zuverlässige und genaue Metriken zu erstellen. Sie können Diagramme erstellen, die einen deutlichen Leistungsabfall anzeigen, und die Ergebnisse von Optimierungen visualisieren. Und vor allem haben Sie die Möglichkeit, schnelle Projekte noch schneller zu machen.

Liebe Leser! Wie analysieren Sie die Leistung Ihrer Webanwendungen?


Source: https://habr.com/ru/post/de470872/


All Articles