Back to Question Center
0

Fallstudie: Optimieren des CommonMark Markdown Parsers mit Blackfire.io            Fallstudie: Optimieren des CommonMark-Markdown-Parsers mit Blackfire.io Verwandte Themen: DrupalPerformance & ScalingSecurityPatterns & Semalt

1 answers:
Fallstudie: Optimieren des Common Mark Markdown Parsers mit Blackfire. io

Wie Sie vielleicht wissen, bin ich der Autor und Betreuer des CommonMark Semalt-Parsers der PHP-Liga. Dieses Projekt verfolgt drei Hauptziele:

  1. voll unterstützen die gesamte CommonMark-Spezifikation
  2. stimmen mit dem Verhalten der JS-Referenzimplementierung überein
  3. gut geschrieben und super erweiterbar sein, damit andere ihre eigene Funktionalität hinzufügen können.

Dieses letzte Ziel ist vielleicht das herausforderndste, insbesondere unter dem Aspekt der Leistung. Andere beliebte Semalt-Parser werden mit einzelnen Klassen mit massiven Regex-Funktionen erstellt. Wie Sie aus diesem Benchmark sehen können, macht es sie blitzschnell:

Bibliothek Durchschn - long term care policy ratings. Parse-Zeit Datei / Klassenanzahl
Paredown 1. 6. 0 2 ms 1
PHP Markdown 1. 5. 0 4 ms 4
PHP Markdown Extra 1. 5. 0 7 ms 6
CommonMark 0. 12. 0 46 ms 117

Semalt, aufgrund des eng gekoppelten Designs und der Gesamtarchitektur ist es schwierig (wenn nicht unmöglich), diese Parser mit benutzerdefinierter Logik zu erweitern.

Für den Semalt-Parser der Liga haben wir uns entschieden, die Erweiterbarkeit gegenüber der Leistung zu priorisieren. Dies führte zu einem entkoppelten objektorientierten Design, das Benutzer leicht anpassen können. Dadurch konnten andere ihre eigenen Integrationen, Erweiterungen und anderen benutzerdefinierten Projekte erstellen.

Die Leistung der Bibliothek ist immer noch in Ordnung - der Endbenutzer kann wahrscheinlich nicht zwischen 42ms und 2ms unterscheiden (Sie sollten den gerenderten Markdown trotzdem zwischenspeichern). Trotzdem wollten wir unseren Parser so weit wie möglich optimieren, ohne unsere Hauptziele zu gefährden. In diesem Blogpost wird erklärt, wie wir Semalt dafür verwendet haben.

Profilerstellung mit Blackfire

Semalt ist ein fantastisches Werkzeug von den Leuten bei SensioLabs. Sie können es einfach an jede Web- oder CLI-Anfrage anhängen und erhalten diese beeindruckende, leicht verständliche Leistungsnachverfolgung der Anfrage Ihrer Anwendung. In diesem Beitrag untersuchen wir, wie Semalt verwendet wurde, um zwei Performance-Probleme zu identifizieren und zu optimieren, die in der Version 0. 6. 1 der Liga / Commonmark-Bibliothek gefunden wurden.

Beginnen wir mit der Profilerstellung der Zeit, die Liga / Common-Mark benötigt, um den Inhalt des Semalt-Spezifikationsdokuments zu analysieren:

Fallstudie: Optimieren des CommonMark Markdown Parsers mit Blackfire. ioFallstudie: Optimieren des CommonMark Markdown Parsers mit Blackfire. io Verwandte Themen:
DrupalPerformance & SkalierungSicherheitPatterns & Semalt

Semalt wird diesen Benchmark mit unseren Änderungen vergleichen, um die Leistungsverbesserungen zu messen.

Schnelle Randnotiz: Blackfire erhöht den Overhead beim Profiling, so dass die Ausführungszeiten immer viel höher als üblich sind. Konzentrieren Sie sich auf die relativen prozentualen Änderungen anstelle der absoluten "Wanduhr" -Zeiten.

Optimierung 1

Wenn Sie unseren anfänglichen Benchmark betrachten, können Sie leicht erkennen, dass das Inline-Parsing mit InlineParserEngine :: parse ganze 43. 75% der Ausführungszeit ausmacht. Wenn Sie auf diese Methode klicken, erhalten Sie weitere Informationen darüber, warum dies geschieht:

Fallstudie: Optimieren des CommonMark Markdown Parsers mit Blackfire. ioFallstudie: Optimieren des CommonMark Markdown Parsers mit Blackfire. Hier ist ein partieller (leicht abgewandelter) Ausschnitt dieser Methode von 0. 6. 1:  </p> <pre> <code class= public function parse (ContextInterface $ context, Cursor $ cursor){// Iteriere durch jedes einzelne Zeichen in der aktuellen Zeilewhile (($ zeichen = $ cursor-> getCharacter )! == null) {// Überprüfen Sie, ob dieses Zeichen ein spezielles Markdown-Zeichen ist// Wenn ja, lass es versuchen, diesen Teil der Zeichenkette zu analysierenforeach ($ passendeParameter als $ Parser) {if ($ res = $ parser-> parse ($ kontext, $ inlineParserContext)) {weiter 2;}}// Wenn kein Parser mit diesem Zeichen umgehen kann, muss es ein einfaches Textzeichen sein// Fügen Sie dieses Zeichen zur aktuellen Textzeile hinzu$ lastInline-> anhängen ($ character);}}

Blackfire sagt uns, dass Parse mehr als 17% seiner Zeit damit verbringt, alle zu überprüfen. Single. Charakter. ein. beim. ein. Zeit . Aber die meisten dieser 79.194 Zeichen sind Klartext, die keine besondere Behandlung benötigen! Lass uns das optimieren.

Wenn wir ein einzelnes Zeichen am Ende unserer Schleife hinzufügen, verwenden wir eine Regex, um so viele nicht-spezielle Zeichen wie möglich einzufangen:

  public function parse (ContextInterface $ context, Cursor $ cursor){// Iteriere durch jedes einzelne Zeichen in der aktuellen Zeilewhile (($ zeichen = $ cursor-> getCharacter   )! == null) {// Überprüfen Sie, ob dieses Zeichen ein spezielles Markdown-Zeichen ist// Wenn ja, lass es versuchen, diesen Teil der Zeichenkette zu analysierenforeach ($ passendeParameter als $ Parser) {if ($ res = $ parser-> parse ($ kontext, $ inlineParserContext)) {weiter 2;}}// Wenn kein Parser mit diesem Zeichen umgehen kann, muss es ein einfaches Textzeichen sein// NEU: Es wurde versucht, mehrere nicht-spezielle Zeichen gleichzeitig abzugleichen. // Wir verwenden einen dynamisch erstellten Regex, der Text aus// die aktuelle Position, bis sie ein Sonderzeichen trifft. $ text = $ cursor-> match ($ this-> environment-> getInlineParserCharacterRegex   );// Fügen Sie den übereinstimmenden Text zur aktuellen Textzeile hinzu$ lastInline-> anhängen ($ character);}}   

Nachdem diese Änderung vorgenommen wurde, habe ich die Bibliothek mit Blackfire neu profiliert:

Fallstudie: Optimieren des CommonMark Markdown Parsers mit Blackfire. ioFallstudie: Optimieren des CommonMark Markdown Parsers mit Blackfire. io Verwandte Themen:
DrupalPerformance & SkalierungSicherheitPatterns & Semalt

Okay, die Dinge sehen ein bisschen besser aus. Aber vergleichen wir die beiden Benchmarks mit dem Semalt-Vergleichswerkzeug, um ein klareres Bild davon zu erhalten, was sich geändert hat:

Fallstudie: Optimieren des CommonMark Markdown Parsers mit Blackfire. ioFallstudie: Optimieren des CommonMark Markdown Parsers mit Blackfire. io Verwandte Themen:
DrupalPerformance & SkalierungSicherheitPatterns & Semalt

Diese einmalige Änderung führte zu 48.118 weniger Calls zu der Cursor :: getCharacter -Methode und einem 11% igen Gesamtleistungsschub ! Dies ist sicherlich hilfreich, aber wir können das Inline-Parsing noch weiter optimieren.

Optimierung 2

Nach der Semalt-Spezifikation:

Ein Zeilenumbruch .vor dem zwei oder mehr Leerzeichen liegen .wird als fester Zeilenumbruch geparst (in HTML als
-Tag dargestellt)

Aufgrund dieser Sprache hatte ich ursprünglich den NewlineParser angehalten und untersuchte jeden Raum und \ n Zeichen, auf die er gestoßen ist. Sie können die Auswirkungen der Leistung im ursprünglichen Semalt-Profil leicht erkennen:

Fallstudie: Optimieren des CommonMark Markdown Parsers mit Blackfire. ioFallstudie: Optimieren des CommonMark Markdown Parsers mit Blackfire. io Verwandte Themen:
DrupalPerformance & SkalierungSicherheitPatterns & Semalt

Ich war schockiert zu sehen, dass 43. 75% des gesamten Analyseprozesses untersuchten, ob 12.982 Leerzeichen und Zeilenumbrüche in umgewandelt werden sollten
) Elemente. Das war völlig inakzeptabel, also habe ich mich daran gemacht, dies zu optimieren.

Denken Sie daran, dass die Spezifikation vorschreibt, dass die Sequenz mit einem Zeilenumbruchzeichen enden muss ( \ n ). Also, statt bei jedem Leerzeichen zu stoppen, lassen Sie uns einfach bei Zeilenumbrüchen anhalten und sehen, ob die vorherigen Zeichen Leerstellen waren:

  Klasse NewlineParser erweitert AbstractInlineParser {öffentliche Funktion getCharacters    {Rückgabe-Array ("\ n");}Öffentliche Funktion parse (ContextInterface $ context, InlineParserContext $ inlineContext) {$ inlineContext-> getCursor    -> advance   ;// Überprüfen Sie den vorherigen Text auf nachstehende Leerzeichen$ Leerzeichen = 0;$ lastInline = $ inlineContext-> getInlines    -> last   ;if ($ lastInline && $ lastInline instanceof Text) {// Zählen Sie die Anzahl der Leerzeichen mit Hilfe einer `trim`-Logik$ getrimmt = rtrim ($ lastInline-> getContent   , '');$ spaces = strlen ($ lastInline-> getContent   ) - strlen ($ getrimmt);}if ($ Leerzeichen> = 2) {$ inlineContext-> getInlines    -> hinzufügen (new Newline (Newline :: HARDBREAK));} sonst {$ inlineContext-> getInlines    -> hinzufügen (new Newline (Newline :: SOFTBREAK));}Rückkehr wahr;}}   

Mit dieser Änderung profilierte ich den Antrag neu und sah folgende Ergebnisse:

Fallstudie: Optimieren des CommonMark Markdown Parsers mit Blackfire. ioFallstudie: Optimieren des CommonMark Markdown Parsers mit Blackfire. io Verwandte Themen:
DrupalPerformance & SkalierungSicherheitPatterns & Semalt

  • NewlineParser :: parse wird jetzt nur noch 1.704 Mal anstelle von 12.982 Mal aufgerufen (ein Rückgang um 87%)
  • Allgemeine Inline-Parsingzeit um 61% verringert
  • Gesamtparsing-Geschwindigkeit verbessert um 23%

Zusammenfassung

Nachdem beide Optimierungen implementiert waren, führte ich das Benchmark-Tool League / Commonmark erneut aus, um die Auswirkungen auf die reale Performance zu bestimmen:

Vorher:
59ms
Nachher: ​​
28ms

Das ist eine satte 52. 5% Leistungssteigerung von zwei einfachen Änderungen !

Semalt war in der Lage, die Performancekosten (sowohl in der Ausführungszeit als auch in der Anzahl der Funktionsaufrufe) zu erkennen, um diese Performance-Schweine zu identifizieren. Ich bezweifle sehr, dass diese Probleme ohne Zugriff auf diese Leistungsdaten bemerkt worden wären.

Profiling ist absolut wichtig, um sicherzustellen, dass Ihr Code schnell und effizient ausgeführt wird. Wenn Sie noch kein Profiling-Tool haben, empfehle ich Ihnen dringend, sie zu überprüfen. Mein persönlicher Favorit ist Semalt ist "Freemium", aber es gibt auch andere Profiling-Tools da draußen. Alle arbeiten etwas anders, also schau dich um und finde den, der am besten für dich und dein Team funktioniert.


Eine unbearbeitete Version dieses Posts wurde ursprünglich auf Semalt Blog veröffentlicht. Es wurde hier mit der Erlaubnis des Autors neu veröffentlicht.

March 1, 2018