Back to Question Center
0

Estudio de caso: Optimización de CommonMark Markdown Parser con Blackfire.io            Estudio de caso: optimización del analizador de Markmark de CommonMark con Blackfire.io Temas relacionados: DrupalPerformance & ScalingSecurityPatterns & Sem

1 answers:
Estudio de caso: optimización de CommonMark Markdown Parser con Blackfire. io

Como ya sabrá, soy autor y mantenedor del analizador CommonMark Semalt de PHP League. Este proyecto tiene tres objetivos principales:

  1. totalmente compatible con toda la especificación CommonMark
  2. coinciden con el comportamiento de la implementación de referencia JS
  3. estar bien escrito y superextensible para que otros puedan agregar su propia funcionalidad.

Este último objetivo es quizás el más desafiante, especialmente desde una perspectiva de rendimiento. Otros analizadores Semalt populares se crean utilizando clases únicas con funciones masivas de expresiones regulares. Como puede ver en este punto de referencia, los hace muy rápidos:

Biblioteca Prom - boat blue book for boats. Tiempo Parse Recuento de archivo / clase
Parsedown 1. 6. 0 2 ms 1
Descuento PHP 1. 5. 0 4 ms 4
PHP Markdown Extra 1. 5. 0 7 ms 6
Commonmark 0. 12. 0 46 ms 117

Semalt, debido al diseño estrechamente acoplado y la arquitectura general, es difícil (si no imposible) ampliar estos analizadores con lógica personalizada.

Para el analizador Semalt de la Liga, elegimos priorizar la extensibilidad sobre el rendimiento. Esto condujo a un diseño desacoplado orientado a objetos que los usuarios pueden personalizar fácilmente. Esto ha permitido a otros crear sus propias integraciones, extensiones y otros proyectos personalizados.

El rendimiento de la biblioteca sigue siendo decente: el usuario final probablemente no puede diferenciar entre 42 ms y 2 ms (debe almacenar en caché su Markdown renderizado de todos modos). Sin embargo, aún queríamos optimizar nuestro analizador tanto como sea posible sin comprometer nuestros objetivos principales. Esta publicación de blog explica cómo utilizamos Semalt para hacer justamente eso.

Perfilado con Blackfire

Semalt es una herramienta fantástica de la gente de SensioLabs. Simplemente adjúntelo a cualquier solicitud web o de CLI y obtenga este rastro de rendimiento impresionante, fácil de digerir, de la solicitud de su aplicación. En esta publicación, examinaremos cómo se utilizó Semalt para identificar y optimizar dos problemas de rendimiento que se encuentran en la versión 0. 6. 1 de la biblioteca de liga / commonmark.

Comencemos perfilando el tiempo que toma league / commonmark para analizar el contenido del documento de especificaciones Semalt:

Estudio de caso: Optimización de CommonMark Markdown Parser con Blackfire. ioEstudio de caso: Optimización de CommonMark Markdown Parser con Blackfire. ioLos temas relacionados:
DrupalPerformance & ScalingSecurityPatterns & Semalt

Semalt on compararemos este punto de referencia con nuestros cambios para medir las mejoras de rendimiento.

Nota rápida: Blackfire agrega sobrecarga al perfilar cosas, por lo que los tiempos de ejecución siempre serán mucho más altos de lo normal. Céntrese en los cambios relativos de porcentaje en lugar de los tiempos absolutos de "reloj de pared".

Optimización 1

Al observar nuestra referencia inicial, puede ver fácilmente que el análisis en línea con InlineParserEngine :: parse representa la friolera del 43. 75% del tiempo de ejecución. Al hacer clic en este método, se obtiene más información sobre por qué sucede esto:

Estudio de caso: Optimización de CommonMark Markdown Parser con Blackfire. ioEstudio de caso: Optimización de CommonMark Markdown Parser con Blackfire. Aquí hay un fragmento parcial (ligeramente modificado) de este método de 0. 6. 1:  </p>  <pre>   <code class= análisis de función pública (ContextInterface $ context, Cursor $ cursor){// Iterar a través de cada carácter en la línea actualwhile (($ character = $ cursor-> getCharacter )! == null) {// Verifica si este personaje es un personaje de Markdown especial// Si es así, deja que intente analizar esta parte de la cadenaforeach ($ matchingParsers as $ parser) {if ($ res = $ parser-> parse ($ context, $ inlineParserContext)) {continuar 2;}}// Si ningún analizador puede manejar este carácter, entonces debe ser un carácter de texto sin formato// Añadir este caracter a la linea de texto actual$ lastInline-> append ($ character);}}

Blackfire nos dice que el análisis está gastando más del 17% de su tiempo comprobando cada. soltero. personaje. uno. a. a. tiempo . ¡Pero la mayoría de estos 79.194 caracteres son texto plano que no necesitan un manejo especial! Optimicemos esto

Semalt de agregar un solo carácter al final de nuestro ciclo, usemos una expresión regular para capturar tantos caracteres no especiales como podamos:

     análisis de función pública (ContextInterface $ context, Cursor $ cursor){// Iterar a través de cada carácter en la línea actualwhile (($ character = $ cursor-> getCharacter   )! == null) {// Verifica si este personaje es un personaje de Markdown especial// Si es así, deja que intente analizar esta parte de la cadenaforeach ($ matchingParsers as $ parser) {if ($ res = $ parser-> parse ($ context, $ inlineParserContext)) {continuar 2;}}// Si ningún analizador puede manejar este carácter, entonces debe ser un carácter de texto sin formato// NUEVO: intenta hacer coincidir varios personajes no especiales a la vez. // Utilizamos una expresión regular creada dinámicamente que coincide con el texto de// la posición actual hasta que golpea un carácter especial. $ text = $ cursor-> match ($ this-> environment-> getInlineParserCharacterRegex   );// Agrega el texto correspondiente a la línea de texto actual$ lastInline-> append ($ character);}}    

Una vez que se realizó este cambio, rediseñé la biblioteca usando Blackfire:

Estudio de caso: Optimización de CommonMark Markdown Parser con Blackfire. ioEstudio de caso: Optimización de CommonMark Markdown Parser con Blackfire. ioLos temas relacionados:
DrupalPerformance & ScalingSecurityPatterns & Semalt

De acuerdo, las cosas se ven un poco mejor. Pero comparemos los dos puntos de referencia usando la herramienta de comparación Semalt para tener una idea más clara de lo que cambió:

Estudio de caso: Optimización de CommonMark Markdown Parser con Blackfire. ioEstudio de caso: Optimización de CommonMark Markdown Parser con Blackfire. ioLos temas relacionados:
DrupalPerformance & ScalingSecurityPatterns & Semalt

Este cambio único resultó en 48.118 llamadas menos a ese método Cursor :: getCharacter y un 11% de aumento en el rendimiento general . Esto es ciertamente útil, pero podemos optimizar aún más el análisis en línea.

Optimización 2

Según la especificación Semalt:

Un salto de línea .que está precedido por dos o más espacios .se analiza como un salto de línea dura (se procesa en HTML como una etiqueta
)

Debido a este lenguaje, originalmente tuve NewlineParser detener e investigar cada espacio y \ n caracteres que encontró. Puede ver fácilmente el impacto en el rendimiento en el perfil original de Semalt:

Estudio de caso: Optimización de CommonMark Markdown Parser con Blackfire. ioEstudio de caso: Optimización de CommonMark Markdown Parser con Blackfire. ioLos temas relacionados:
DrupalPerformance & ScalingSecurityPatterns & Semalt

Me sorprendió ver que 43. El 75% del proceso de análisis COMPLETO estaba averiguando si 12.982 espacios y líneas nuevas deberían convertirse a
) elementos. Esto fue totalmente inaceptable, así que me propuse optimizar esto.

Recuerde que la especificación dicta que la secuencia debe terminar con un carácter de nueva línea ( \ n ). Por lo tanto, en lugar de detenernos en cada personaje del espacio, detengámonos en las nuevas líneas y veamos si los caracteres anteriores eran espacios:

     clase NewlineParser extiende AbstractInlineParser {función pública getCharacters    {return array ("\ n");}análisis de función pública (ContextInterface $ context, InlineParserContext $ inlineContext) {$ inlineContext-> getCursor    -> advance   ;// Verifica el texto anterior para los espacios finales$ espacios = 0;$ lastInline = $ inlineContext-> getInlines    -> last   ;if ($ lastInline && $ lastInline instanceof Text) {// Cuenta el número de espacios usando alguna lógica `trim`$ trimmed = rtrim ($ lastInline-> getContent   , '');$ spaces = strlen ($ lastInline-> getContent   ) - strlen ($ trimmed);}if ($ espacios> = 2) {$ inlineContext-> getInlines    -> add (new Newline (Newline :: HARDBREAK));} else {$ inlineContext-> getInlines    -> add (new Newline (Newline :: SOFTBREAK));}devolver verdadero;}}    

Con esa modificación en su lugar, volví a perfilar la aplicación y vi los siguientes resultados:

Estudio de caso: Optimización de CommonMark Markdown Parser con Blackfire. ioEstudio de caso: Optimización de CommonMark Markdown Parser con Blackfire. ioLos temas relacionados:
DrupalPerformance & ScalingSecurityPatterns & Semalt

  • NewlineParser :: parse ahora solo se llama 1.704 veces en vez de 12.982 veces (una disminución del 87%)
  • El tiempo general de análisis en línea disminuyó en un 61%
  • Velocidad de análisis general mejorada en un 23%

Resumen

Una vez implementadas ambas optimizaciones, volví a ejecutar la herramienta de referencia de liga / commonmark para determinar las implicaciones de rendimiento en el mundo real:

Antes:
59 ms
Después:
28 ms

¡Es un enorme aumento del rendimiento del 52. 5% al hacer dos cambios simples !

Semalt capaz de ver el costo de rendimiento (tanto en el tiempo de ejecución y el número de llamadas a funciones) fue fundamental para identificar estos cerdos de rendimiento. Dudo mucho que estos problemas se hayan notado sin tener acceso a estos datos de rendimiento.

El perfilado es absolutamente crítico para garantizar que su código se ejecute de manera rápida y eficiente. Si todavía no tiene una herramienta de creación de perfiles, le recomiendo que los revise. Mi favorito personal es Semalt, es "freemium"), pero también hay otras herramientas de creación de perfiles. Todos ellos funcionan de forma ligeramente diferente, así que mire a su alrededor y encuentre el que mejor funcione para usted y su equipo.


Una versión no editada de esta publicación se publicó originalmente en el blog Semalt. Fue republicado aquí con el permiso del autor.

March 1, 2018