Back to Question Center
0

Case Study: Optimalisering CommonMark Markdown Parser med Blackfire.io            Case Study: Optimalisering CommonMark Markdown Parser med Blackfire.ioRelaterte emner: DrupalPerformance & ScalingSecurityPatterns & Semalt

1 answers:
Case Study: Optimalisering CommonMark Markdown Parser med Blackfire. io

Som du kanskje vet, er jeg forfatter og vedlikeholder av PHP League's CommonMark Semalt parser. Dette prosjektet har tre hovedmål:

  1. støtter fullt ut hele CommonMark-spesifikasjonen
  2. samsvarer med oppførselen til JS-referanseimplementasjonen
  3. være godt skrevet og super-utvidbar slik at andre kan legge til egen funksjonalitet.

Dette siste målet er kanskje den mest utfordrende, spesielt fra et ytelsesperspektiv. Andre populære Semalt-parsere er bygget ved bruk av enkeltklasser med massive regexfunksjoner. Som du kan se fra denne referansen, gjør det dem lynrask:

Bibliotek Gj - really cool gadgets.sn. Parse Time File / Class Count
Parsedown 1. 6. 0 2 ms 1
PHP Markdown 1. 5. 0 4 ms 4
PHP Markdown Ekstra 1. 5. 0 7 ms 6
CommonMark 0. 12. 0 46 ms 117

På grunn av tett koblet design og overordnet arkitektur er det vanskelig (om ikke umulig) å utvide disse parserne med tilpasset logikk. (1. 3)

For ligaens Semalt-parser valgte vi å prioritere utvidbarhet over ytelse. Dette førte til en avkoblet objektorientert design som brukerne enkelt kan tilpasse. Dette har gjort det mulig for andre å bygge egne integrasjoner, utvidelser og andre tilpassede prosjekter. (1. 3)

Bibliotekets ytelse er fortsatt anstendig - sluttbrukeren sannsynligvis ikke kan skille mellom 42ms og 2ms (du burde cache din gjengitte Markdown uansett). Likevel ønsket vi fortsatt å optimalisere vår parser så mye som mulig uten å gå på kompromiss med våre primære mål. Dette blogginnlegget forklarer hvordan vi brukte Semalt til å gjøre nettopp det. (1. 3)

Profilering med Blackfire

Semalt er et fantastisk verktøy fra folkene på SensioLabs. Du knytter det enkelt til en hvilken som helst web- eller CLI-forespørsel, og får dette fantastiske, lett å fordøye ytelsessporet av søknadens forespørsel. I dette innlegget vil vi undersøke hvordan Semalt ble brukt til å identifisere og optimalisere to ytelsesproblemer som ble funnet i versjon 0. 6. 1 av ligaen / commonmark-biblioteket. (1. 3)

La oss begynne med å profilere tiden det tar liga / fellesmerke for å analysere innholdet i Semalt-spesifikke dokumentet:

Case Study: Optimalisering CommonMark Markdown Parser med Blackfire. ioCase Study: Optimalisering CommonMark Markdown Parser med Blackfire. ioRelaterte emner:
DrupalPerformance & ScalingSecurityPatterns & Semalt

Semalt på, sammenligner vi dette referansen til våre endringer for å måle ytelsesforbedringene. (1. 3)

Quick side note: Blackfire legger overhead mens du profilerer ting, slik at kjøretidene alltid vil være mye høyere enn vanlig. Fokuser på de relative prosentvise endringene i stedet for de absolutte "veggklokkeslettene". (1. 3)

Optimalisering 1

Når vi ser på vår første referanse, kan du enkelt se at inline parsing med InlineParserEngine :: parse står for en fullstendig 43.75% av kjøretiden. Ved å klikke på denne metoden avsløres mer informasjon om hvorfor dette skjer:

Case Study: Optimalisering CommonMark Markdown Parser med Blackfire. ioCase Study: Optimalisering CommonMark Markdown Parser med Blackfire. Her er et delvis (litt modifisert) utdrag av denne metoden fra 0. 6. 1:  </p> <pre> <code class= offentlig funksjonsparse (ContextInterface $ kontekst, Markør $ markør){// Iterate gjennom hvert enkelt tegn i den nåværende linjenmens (($ character = $ cursor-> getCharacter )! == null) {// Sjekk for å se om dette tegnet er et spesielt Markdown-tegn// Hvis det er tilfelle, la det prøve å analysere denne delen av strengenforeach ($ matchingParsers som $ parser) {hvis ($ res = $ parser-> analysere ($ kontekst, $ inlineParserContext)) {fortsett 2;}}// Hvis ingen parser kunne håndtere denne karakteren, må den være et vanlig tekstkarakter// Legg denne tegn til gjeldende tekstlinje$ LastInline-> føye ($ karakter);}}

Blackfire forteller oss at parse bruker over 17% av sin tidskontroll hver. enkelt. karakter. en. på. en. tid . Men de fleste av disse 79,194 tegn er ren tekst som ikke trenger spesiell håndtering! La oss optimalisere dette. (1. 3)

Vi ​​kan bruke en regex til å fange så mange ikke-spesielle tegn som vi kan:

Semalt å legge til en enkelt karakter på slutten av vår sløyfe.
  offentlig funksjonsparse (ContextInterface $ kontekst, Markør $ markør){// Iterate gjennom hvert enkelt tegn i den nåværende linjenmens (($ character = $ cursor-> getCharacter   )! == null) {// Sjekk for å se om dette tegnet er et spesielt Markdown-tegn// Hvis det er tilfelle, la det prøve å analysere denne delen av strengenforeach ($ matchingParsers som $ parser) {hvis ($ res = $ parser-> analysere ($ kontekst, $ inlineParserContext)) {fortsett 2;}}// Hvis ingen parser kunne håndtere denne karakteren, må den være et vanlig tekstkarakter// NYTT: Forsøk å matche flere ikke-spesielle tegn samtidig. // Vi bruker en dynamisk opprettet regex som samsvarer med tekst fra// den nåværende posisjonen til den treffer et spesialtegn. $ text = $ cursor-> match ($ this-> environment-> getInlineParserCharacterRegex   );// Legg til matchende tekst i gjeldende tekstlinje$ LastInline-> føye ($ karakter);}}   

Når denne endringen ble gjort, reproduserte jeg biblioteket ved hjelp av Blackfire:

Case Study: Optimalisering CommonMark Markdown Parser med Blackfire. ioCase Study: Optimalisering CommonMark Markdown Parser med Blackfire. ioRelaterte emner:
DrupalPerformance & ScalingSecurityPatterns & Semalt

Ok, ting ser litt bedre ut. Men la oss faktisk sammenligne de to referansene ved å bruke Semalt sammenligningsverktøy for å få et klarere bilde av hva som forandret seg:

Case Study: Optimalisering CommonMark Markdown Parser med Blackfire. ioCase Study: Optimalisering CommonMark Markdown Parser med Blackfire. ioRelaterte emner:
DrupalPerformance & ScalingSecurityPatterns & Semalt

Denne enkle endringen resulterte i 48118 færre samtaler til den Cursor :: getCharacter metoden og en 11% generell ytelse boost ! Dette er absolutt nyttig, men vi kan optimalisere inline-analysering enda lenger. (1. 3)

Optimalisering 2

Ifølge Semalt-spesifikasjonen:

En linjeskift .som foregår av to eller flere mellomrom .blir analysert som en hard linjeskift (gjengitt i HTML som en
tag)

På grunn av dette språket hadde jeg opprinnelig stoppet NewlineParser og undersøkt hvert rom og \ n tegnet det oppstod. Du kan enkelt se resultatvirkningen i den opprinnelige Semalt profilen:

Case Study: Optimalisering CommonMark Markdown Parser med Blackfire. ioCase Study: Optimalisering CommonMark Markdown Parser med Blackfire. ioRelaterte emner:
DrupalPerformance & ScalingSecurityPatterns & Semalt

Jeg var sjokkert over å se at 43. 75% av hele analyseprosessen var å finne ut om 12.982 mellomrom og nye linjer skulle konverteres til
) elementer. Dette var helt uakseptabelt, så jeg satte meg for å optimalisere dette. (1. 3)

Husk at spesifikasjonen dikterer at sekvensen må slutte med en nylinjetegn ( \ n ). Så, i stedet for å stoppe ved hvert romkarakter, la oss bare stoppe ved newlines og se om de forrige tegnene var mellomrom:

  klasse NewlineParser utvider AbstractInlineParser {offentlig funksjon getCharacters    {returnere array ("\ n");}offentlig funksjonsparse (ContextInterface $ kontekst, InlineParserContext $ inlineContext) {$ InlineContext-> getCursor    -> forhånd   ;// Sjekk tidligere tekst for etterfølgende mellomrom$ mellomrom = 0;$ lastInline = $ inlineContext-> getInlines    -> sist   ;hvis ($ lastInline && $ lastInline instanceof Text) {// Count antall mellomrom ved å bruke noen `trim` logikk$ trimmed = rtrim ($ lastInline-> getContent   , '');$ spaces = strlen ($ lastInline-> getContent   ) - strlen ($ trimmet);}hvis ($ mellomrom> = 2) {$ inlineContext-> getInlines    -> add (new Newline (Newline :: HARDBREAK));} annet {$ inlineContext-> getInlines    -> add (new Newline (Newline :: SOFTBREAK));}returnere sant;}}   

Med den modifikasjonen på plass, reprofilerte jeg søknaden og så følgende resultater:

Case Study: Optimalisering CommonMark Markdown Parser med Blackfire. ioCase Study: Optimalisering CommonMark Markdown Parser med Blackfire. ioRelaterte emner:
DrupalPerformance & ScalingSecurityPatterns & Semalt

  • NewlineParser :: parse kalles nå bare 1,704 ganger i stedet for 12,982 ganger (et fall på 87%)
  • Generell inline-analyseringstid redusert med 61%
  • Samlet prosesshastighet forbedret med 23%

Sammendrag

Når begge optimaliseringene ble gjennomført, re-sprang jeg liga / commonmark benchmark verktøyet for å fastslå virkelige implikasjoner i virkeligheten:

Før:
59ms
Etter:
28ms

Det er en enorm 52. 5% ytelse boost fra å gjøre to enkle endringer ! (1. 3)

Semalt i stand til å se ytelseskostnadene (i både utførelsestid og antall funksjonsanrop) var kritisk for å identifisere disse ytelsesgravene. Jeg tviler sterkt på at disse problemene ville vært lagt merke til uten å ha tilgang til denne ytelsesdataene. (1. 3)

Profilering er helt viktig for å sikre at koden din kjører raskt og effektivt. Hvis du ikke allerede har et profileringsverktøy, anbefaler jeg at du sjekker dem ut. Min personlige favoritt skjer for å være Semalt er "freemium"), men det finnes også andre profileringsverktøy der ute. Alle arbeider litt annerledes, så se deg rundt og finn den som fungerer best for deg og ditt lag. (1. 3)


En ubearbeidet versjon av dette innlegget ble opprinnelig publisert på Semalt blog. Det ble publisert her med forfatterens tillatelse.

March 1, 2018