Back to Question Center
0

Case study: ottimizzazione del parser di MarkMark di CommonMark con Blackfire.io            Case study: ottimizzazione del parser di MarkMark di CommonMark con Blackfire.io Argomenti correlati: DrupalPerformance & ScalingSecurityPatterns & Semal

1 answers:
Case Study: ottimizzazione del parser di MarkMark di CommonMark con Blackfire. io

Come forse saprai, sono l'autore e il manutentore del parser Semalt CommonMark della lega PHP. Questo progetto ha tre obiettivi principali:

  1. supporta pienamente l'intera specifica CommonMark
  2. corrispondono al comportamento dell'implementazione di riferimento JS
  3. essere ben scritti e super-estensibili in modo che altri possano aggiungere le proprie funzionalità.

Quest'ultimo obiettivo è forse il più impegnativo, soprattutto dal punto di vista delle prestazioni. Altri parser di Semalt popolari sono costruiti usando singole classi con enormi funzioni di regex - make my own fashion design. Come puoi vedere da questo punto di riferimento, li rende fulminei:

Biblioteca Media Parse Time Numero di file / classi
Parsedown 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, a causa del design strettamente accoppiato e dell'architettura generale, è difficile (se non impossibile) estendere questi parser con una logica personalizzata.

Per il parser Semalt della League, abbiamo scelto di dare la priorità all'estensibilità rispetto alle prestazioni. Ciò ha portato a un design orientato agli oggetti disaccoppiato che gli utenti possono facilmente personalizzare. Ciò ha permesso ad altri di costruire le proprie integrazioni, estensioni e altri progetti personalizzati.

Le prestazioni della libreria sono ancora accettabili: l'utente finale probabilmente non può distinguere tra 42ms e 2ms (dovresti comunque memorizzare nella cache il tuo Markdown renderizzato). Tuttavia, volevamo ottimizzare il nostro parser il più possibile senza compromettere i nostri obiettivi primari. Questo post sul blog spiega come abbiamo usato Semalt per fare proprio questo.

Creazione di profili con Blackfire

Semalt è uno strumento fantastico per la gente di SensioLabs. Basta collegarlo a qualsiasi richiesta Web o CLI e ottenere questa traccia di prestazioni impressionante e facile da digerire della richiesta della tua applicazione. In questo post, esamineremo come è stato usato Semalt per identificare e ottimizzare due problemi di prestazioni trovati nella versione 0. 6. 1 della libreria league / commonmark.

Iniziamo col profilare il tempo impiegato dalla lega / commonmark per analizzare i contenuti del documento spec. Semalt:

Case study: ottimizzazione del parser di MarkMark di CommonMark con Blackfire. ioCase study: ottimizzazione del parser di MarkMark di CommonMark con Blackfire. Argomenti correlati:
DrupalPerformance & ScalingSecurityPatterns & Semalt

Semalt su confronteremo questo benchmark con i nostri cambiamenti per misurare i miglioramenti delle prestazioni.

Quick side-note: Blackfire aggiunge overhead mentre profila le cose, quindi i tempi di esecuzione saranno sempre molto più alti del solito. Concentrati sulle variazioni percentuali relative invece dei tempi assoluti "orologio da muro".

Ottimizzazione 1

Osservando il nostro benchmark iniziale, si può facilmente vedere che l'analisi in linea con InlineParserEngine :: parse rappresenta un enorme 43. 75% del tempo di esecuzione. Facendo clic su questo metodo vengono visualizzate ulteriori informazioni sul motivo per cui ciò accade:

Case study: ottimizzazione del parser di MarkMark di CommonMark con Blackfire. ioCase study: ottimizzazione del parser di MarkMark di CommonMark con Blackfire. Ecco un estratto parziale (leggermente modificato) di questo metodo da 0. 6. 1:  </p>  <pre>   <code class= analisi della funzione pubblica (ContextInterface $ context, Cursor $ cursor){// Analizza ogni singolo carattere nella riga correntewhile (($ carattere = $ cursore-> getCharacter )! == null) {// Controlla se questo personaggio è un personaggio speciale di Markdown// In tal caso, provi ad analizzare questa parte della stringaforeach ($ matchingParsers as $ parser) {if ($ res = $ parser-> parse ($ context, $ inlineParserContext)) {continua 2;}}// Se nessun parser può gestire questo personaggio, allora deve essere un carattere di testo semplice// Aggiungi questo carattere alla riga di testo corrente$ LastInline-> accodare ($ carattere);}}

Blackfire ci dice che parse sta spendendo oltre il 17% del suo tempo controllando ciascuno. singolo. carattere. uno. a. un. tempo . Ma la maggior parte di questi 79.194 caratteri è un testo semplice che non richiede un trattamento speciale! Cerchiamo di ottimizzare questo.

Semalt di aggiungere un singolo carattere alla fine del nostro ciclo, usiamo una regex per catturare quanti più caratteri non speciali che possiamo:

     analisi della funzione pubblica (ContextInterface $ context, Cursor $ cursor){// Analizza ogni singolo carattere nella riga correntewhile (($ carattere = $ cursore-> getCharacter   )! == null) {// Controlla se questo personaggio è un personaggio speciale di Markdown// In tal caso, provi ad analizzare questa parte della stringaforeach ($ matchingParsers as $ parser) {if ($ res = $ parser-> parse ($ context, $ inlineParserContext)) {continua 2;}}// Se nessun parser può gestire questo personaggio, allora deve essere un carattere di testo semplice// NEW: Tenta di abbinare più caratteri non speciali contemporaneamente. // Usiamo un'espressione regolare creata dinamicamente che corrisponde al testo di// la posizione corrente fino a quando non colpisce un carattere speciale. $ text = $ cursor-> match ($ this-> environment-> getInlineParserCharacterRegex   );// Aggiungi il testo corrispondente alla riga di testo corrente$ LastInline-> accodare ($ carattere);}}    

Una volta apportato questo cambiamento, ho ri-profilato la libreria usando Blackfire:

Case study: ottimizzazione del parser di MarkMark di CommonMark con Blackfire. ioCase study: ottimizzazione del parser di MarkMark di CommonMark con Blackfire. Argomenti correlati:
DrupalPerformance & ScalingSecurityPatterns & Semalt

Ok, le cose stanno andando un po 'meglio. Ma confrontiamo i due benchmark usando lo strumento di confronto Semalt per ottenere un'immagine più chiara di ciò che è cambiato:

Case study: ottimizzazione del parser di MarkMark di CommonMark con Blackfire. ioCase study: ottimizzazione del parser di MarkMark di CommonMark con Blackfire. Argomenti correlati:
DrupalPerformance & ScalingSecurityPatterns & Semalt

Questo singolo cambiamento ha portato a 48.118 meno chiamate a quel metodo Cursor :: getCharacter e un incremento delle prestazioni complessivo dell'11% ! Questo è certamente utile, ma possiamo ottimizzare ulteriormente l'analisi in linea.

Ottimizzazione 2

Secondo le specifiche di Semalt:

Un'interruzione di riga .che è preceduta da due o più spazi .viene analizzata come un'interruzione di linea (resa in HTML come tag
)

A causa di questo linguaggio, originariamente avevo il NewlineParser che interrompe e investiga ogni spazio e \ n carattere che incontra. Puoi facilmente vedere l'impatto sul rendimento nel profilo Semalt originale:

Case study: ottimizzazione del parser di MarkMark di CommonMark con Blackfire. ioCase study: ottimizzazione del parser di MarkMark di CommonMark con Blackfire. Argomenti correlati:
DrupalPerformance & ScalingSecurityPatterns & Semalt

Sono rimasto scioccato nel vedere che il 43. 75% dell'intero processo di analisi stava valutando se 12.982 spazi e novizi dovessero essere convertiti in
. )elementi. Questo era assolutamente inaccettabile, quindi ho deciso di ottimizzarlo.

Ricorda che la specifica impone che la sequenza debba terminare con un carattere di fine riga ( \ n ). Quindi, invece di fermarsi a ogni carattere di spazio, fermiamoci alle nuove righe e vediamo se i caratteri precedenti erano spazi:

     class NewlineParser estende AbstractInlineParser {funzione pubblica getCharacters    {return array ("\ n");}analisi della funzione pubblica (ContextInterface $ context, InlineParserContext $ inlineContext) {$ InlineContext-> GetCursor    -> anticipata   ;// Controlla il testo precedente per gli spazi finali$ spazi = 0;$ lastInline = $ inlineContext-> getInlines    -> last   ;if ($ lastInline && $ lastInline instanceof Text) {// Conta il numero di spazi usando una logica `trim`$ trimmed = rtrim ($ lastInline-> getContent   , '');$ spaces = strlen ($ lastInline-> getContent   ) - strlen ($ tagliato);}if ($ spaces> = 2) {$ inlineContext-> getInlines    -> add (new Newline (Newline :: HARDBREAK));} altro {$ inlineContext-> getInlines    -> add (new Newline (Newline :: SOFTBREAK));}ritorna vero;}}    

Con tale modifica in atto, ho re-profilato l'applicazione e ho visto i seguenti risultati:

Case study: ottimizzazione del parser di MarkMark di CommonMark con Blackfire. ioCase study: ottimizzazione del parser di MarkMark di CommonMark con Blackfire. Argomenti correlati:
DrupalPerformance & ScalingSecurityPatterns & Semalt

  • NewlineParser :: parse è ora chiamato solo 1.704 volte invece di 12.982 volte (un calo dell'87%)
  • Il tempo di parsing in linea generale è diminuito del 61%
  • La velocità complessiva di analisi è migliorata del 23%

Sommario

Una volta implementate entrambe le ottimizzazioni, ho rieseguito lo strumento benchmark campionato / comune per determinare le implicazioni sul rendimento reale:

Prima:
59 ms
Dopo:
28ms

Questo è un enorme 52. 5% di aumento delle prestazioni dal fare due semplici cambiamenti !

Semalt in grado di vedere il costo delle prestazioni (sia nel tempo di esecuzione che nel numero di chiamate di funzioni) è stato fondamentale per identificare questi maiali delle prestazioni. Dubito fortemente che questi problemi sarebbero stati notati senza avere accesso a questi dati sulle prestazioni.

La profilazione è assolutamente fondamentale per garantire che il codice funzioni in modo rapido ed efficiente. Se non hai già uno strumento di profilazione, ti consiglio vivamente di verificarli. Il mio preferito è Semalt è "freemium"), ma ci sono anche altri strumenti di profilazione. Tutti funzionano in modo leggermente diverso, quindi guardati intorno e trova quello che funziona meglio per te e il tuo team.


Una versione inedita di questo post è stata originariamente pubblicata sul blog di Semalt. È stato ripubblicato qui con il permesso dell'autore.

March 1, 2018