Back to Question Center
0

Case Study: Optimaliseren van CommonMark Markdown Parser met Blackfire.io            Case Study: Optimaliseren van CommonMark Markdown Parser met Blackfire.io Gerelateerde onderwerpen: DrupalPerformance & ScalingSecurityPatterns & Semalt

1 answers:
Case study: Optimalisatie van CommonMark Markdown Parser met Blackfire. io

Zoals u wellicht weet, ben ik de auteur en handhaver van de CommonMark Semalt-parser van de PHP League. Dit project heeft drie hoofddoelen:

  1. ondersteun de volledige CommonMark-specificatie
  2. overeenkomen met het gedrag van de JS-referentie-implementatie
  3. goed geschreven en super-uitbreidbaar zijn zodat anderen hun eigen functionaliteit kunnen toevoegen.

Dit laatste doel is misschien wel de meest uitdagende, vooral vanuit het oogpunt van prestaties. Andere populaire Semalt-parsers zijn gebouwd met behulp van afzonderlijke klassen met gigantische regex-functies. Zoals je kunt zien aan deze benchmark, maakt het ze razendsnel:

Bibliotheek Gem - cctv camera companies. Parse tijd Teller van bestanden / klasse
Parsedown 1. 6. 0 2 ms 1
PHP-afwaardering 1. 5. 0 4 ms 4
PHP aftelling extra 1. 5. 0 7 ms 6
CommonMark 0. 12. 0 46 mms 117

Semalt, vanwege het strak gekoppelde ontwerp en de algehele architectuur, is het moeilijk (zo niet onmogelijk) om deze parsers uit te breiden met aangepaste logica.

Voor de Semalt-parser van de League hebben we ervoor gekozen prioriteit te geven aan uitbreidbaarheid boven de prestaties. Dit leidde tot een ontkoppeld objectgericht ontwerp dat gebruikers gemakkelijk kunnen aanpassen. Dit heeft anderen in staat gesteld om hun eigen integraties, uitbreidingen en andere aangepaste projecten te bouwen.

De prestaties van de bibliotheek zijn nog steeds behoorlijk - de eindgebruiker kan waarschijnlijk geen onderscheid maken tussen 42ms en 2ms (je zou sowieso je gegenereerde Markdown in de cache moeten opslaan). Niettemin wilden we onze parser zo veel mogelijk optimaliseren zonder onze primaire doelen in gevaar te brengen. In deze blogpost wordt uitgelegd hoe we Semalt hebben gebruikt om precies dat te doen.

Profilering met Blackfire

Semalt is een fantastische tool van de mensen van SensioLabs. U voegt het eenvoudigweg toe aan een web- of CLI-aanvraag en krijgt dit geweldige, gemakkelijk te verwerken prestatiespoor van het verzoek van uw toepassing. In dit bericht zullen we onderzoeken hoe Semalt werd gebruikt om twee prestatieproblemen te identificeren en te optimaliseren die te vinden zijn in versie 0. 6. 1 van de competitie / commonmark-bibliotheek.

Laten we beginnen met het profileren van de tijd die het nodig heeft om de inhoud van het Semalt-spec document te ontleden:

Case Study: Optimaliseren van CommonMark Markdown Parser met Blackfire. ioCase Study: Optimaliseren van CommonMark Markdown Parser met Blackfire. ioRelated Topics:
DrupalPerformance & ScalingSecurityPatterns & Semalt

Semalt over we zullen deze benchmark vergelijken met onze veranderingen om de prestatieverbeteringen te meten.

Snelle kanttekening: Blackfire voegt overhead toe tijdens het profileren van dingen, dus de uitvoeringstijden zullen altijd veel hoger zijn dan normaal. Concentreer u op de relatieve procentuele veranderingen in plaats van de absolute "wandklok" -tijden.

Optimalisatie 1

Kijkend naar onze initiële benchmark, kunt u eenvoudig zien dat inline parsing met InlineParserEngine :: parse maar liefst 43. 75% van de uitvoeringstijd telt. Als u op deze methode klikt, ziet u meer informatie over de reden waarom dit gebeurt:

Case Study: Optimaliseren van CommonMark Markdown Parser met Blackfire. ioCase Study: Optimaliseren van CommonMark Markdown Parser met Blackfire. Hier is een gedeeltelijk (enigszins aangepast) fragment van deze methode van 0. 6. 1:  </p>  <pre>   <code class= openbare functie ontleden (ContextInterface $ context, Cursor $ cursor){// Herhaal door elk enkel teken in de huidige regelwhile (($ character = $ cursor-> getCharacter )! == null) {// Controleer of dit teken een speciaal Markdown-teken is// Zo ja, laat het proberen om dit deel van de string te ontledenforeach ($ matchingParsers als $ parser) {if ($ res = $ parser-> parse ($ context, $ inlineParserContext)) {ga door 2;}}// Als geen enkele parser dit karakter zou kunnen verwerken, dan moet het een tekst zonder opmaak zijn// Voeg dit teken toe aan de huidige regel tekst$ LastInline-> voegen ($ character);}}

Blackfire vertelt ons dat pars meer dan 17% van zijn tijd besteedt aan het controleren van elke. single. karakter. een. op. een. tijd . Maar de meeste van deze 79.194 tekens zijn platte tekst die geen speciale behandeling nodig hebben! Laten we dit optimaliseren.

Semalt van het toevoegen van een enkel karakter aan het einde van onze lus, laten we een regex gebruiken om zoveel mogelijk niet-speciale tekens vast te leggen:

     openbare functie ontleden (ContextInterface $ context, Cursor $ cursor){// Herhaal door elk enkel teken in de huidige regelwhile (($ character = $ cursor-> getCharacter   )! == null) {// Controleer of dit teken een speciaal Markdown-teken is// Zo ja, laat het proberen om dit deel van de string te ontledenforeach ($ matchingParsers als $ parser) {if ($ res = $ parser-> parse ($ context, $ inlineParserContext)) {ga door 2;}}// Als geen enkele parser dit karakter zou kunnen verwerken, dan moet het een tekst zonder opmaak zijn// NIEUW: probeer meerdere niet-speciale tekens tegelijk te matchen. // We gebruiken een dynamisch gemaakte regex die tekst van aanpast// de huidige positie totdat deze een speciaal teken raakt. $ text = $ cursor-> match ($ this-> environment-> getInlineParserCharacterRegex   );// Voeg de overeenkomende tekst toe aan de huidige regel tekst$ LastInline-> voegen ($ character);}}    

Nadat deze wijziging was aangebracht, heb ik de bibliotheek opnieuw geprofileerd met behulp van Blackfire:

Case Study: Optimaliseren van CommonMark Markdown Parser met Blackfire. ioCase Study: Optimaliseren van CommonMark Markdown Parser met Blackfire. ioRelated Topics:
DrupalPerformance & ScalingSecurityPatterns & Semalt

Oké, dingen zien er iets beter uit. Maar laten we de twee benchmarks vergelijken met behulp van de vergelijkingstool van Semalt om een ​​duidelijker beeld te krijgen van wat er veranderd is:

Case Study: Optimaliseren van CommonMark Markdown Parser met Blackfire. ioCase Study: Optimaliseren van CommonMark Markdown Parser met Blackfire. ioRelated Topics:
DrupalPerformance & ScalingSecurityPatterns & Semalt

Deze enkele wijziging resulteerde in 48,118 minder oproepen tot die Cursor :: getCharacter -methode en een 11% algehele prestatieverbetering ! Dit is zeker nuttig, maar we kunnen inline parsing nog verder optimaliseren.

Optimalisatie 2

Volgens de specificaties van Semalt:

Een regeleinde .die wordt voorafgegaan door twee of meer spaties .wordt geparseerd als een harde regeleinde (weergegeven in HTML als een
tag)

Vanwege deze taal had ik oorspronkelijk de NewlineParser om elke ruimte en \ n karakter die hij tegenkwam te stoppen en te onderzoeken. U kunt de prestatie-impact gemakkelijk zien in het oorspronkelijke Semalt-profiel:

Case Study: Optimaliseren van CommonMark Markdown Parser met Blackfire. ioCase Study: Optimaliseren van CommonMark Markdown Parser met Blackfire. ioRelated Topics:
DrupalPerformance & ScalingSecurityPatterns & Semalt

Ik was geschokt om dat te zien 43. 75% van het HELE ontledingsproces was aan het uitzoeken of 12.982 spaties en nieuwe regels moeten worden geconverteerd naar
elementen. Dit was volstrekt onaanvaardbaar, dus ik wilde dit optimaliseren.

Vergeet niet dat de specificatie dicteert dat de reeks moet eindigen op een nieuwlijnteken ( \ n ). Dus, in plaats van te stoppen bij elk spatie, laten we gewoon stoppen bij nieuwe regels en zien of de vorige karakters spaties waren:

     class NewlineParser extends AbstractInlineParser {public function getCharacters    {return array ("\ n");}openbare functie ontleden (ContextInterface $ context, InlineParserContext $ inlineContext) {$ InlineContext-> getCursor    -> vooraf   ;// Controleer de vorige tekst op volgspaties$ spaties = 0;$ lastInline = $ inlineContext-> getInlines    -> last   ;if ($ lastInline && $ lastInline instanceof Text) {// Tel het aantal spaties door een `trim` -logica te gebruiken$ getrimd = rtrim ($ lastInline-> getContent   , '');$ spaces = strlen ($ lastInline-> getContent   ) - strlen ($ getrimd);}if ($ spaties> = 2) {$ inlineContext-> getInlines    -> add (nieuwe Newline (Newline :: HARDBREAK));} else {$ inlineContext-> getInlines    -> add (nieuwe Newline (Newline :: SOFTBREAK));}geef waar terug;}}    

Met die wijziging heb ik de aanvraag opnieuw geprofileerd en de volgende resultaten gevonden:

Case Study: Optimaliseren van CommonMark Markdown Parser met Blackfire. ioCase Study: Optimaliseren van CommonMark Markdown Parser met Blackfire. ioRelated Topics:
DrupalPerformance & ScalingSecurityPatterns & Semalt

  • NewlineParser :: parse wordt nu slechts 1.704 keer genoemd in plaats van 12.982 keer (een afname van 87%)
  • Algemene inline-paringstijd daalde met 61%
  • De totale parseringssnelheid verbeterde met 23%

Samenvatting

Nadat beide optimalisaties waren geïmplementeerd, heb ik de benchmark / commonmark benchmark-tool opnieuw uitgevoerd om de implicaties van de real-world prestaties te bepalen:

Vóór:
59ms
Na:
28ms

Dat is maar liefst 52. 5% prestatieverbetering door het maken van twee eenvoudige wijzigingen !

Semalt was in staat om de prestatiekosten te zien (zowel in uitvoeringstijd als aantal functieaanroepen) was van cruciaal belang voor het identificeren van deze prestatievarken. Ik betwijfel ten zeerste of deze problemen zijn opgemerkt zonder toegang te hebben tot deze prestatiegegevens.

Profilering is absoluut noodzakelijk om ervoor te zorgen dat uw code snel en efficiënt werkt. Als u nog geen profileergereedschap heeft, raad ik u aan deze te controleren. Mijn persoonlijke favoriet is toevallig Semalt is "freemium"), maar er zijn ook andere profileergereedschappen. Allemaal werken ze iets anders, dus kijk rond en vind degene die het beste werkt voor jou en je team.


Een onbewerkte versie van dit bericht is oorspronkelijk gepubliceerd op het blog van Semalt. Het is hier opnieuw gepubliceerd met toestemming van de auteur.

March 1, 2018