Métricas de rendimiento para investigar aplicaciones web increíblemente rápidas

Hay un dicho: "Lo que no puedes medir, no puedes mejorar". El autor del artículo, cuya traducción publicamos hoy, trabaja para Superhuman . Él dice que esta compañía está desarrollando el cliente de correo electrónico más rápido del mundo. Aquí hablaremos sobre lo que es "rápido" y cómo crear herramientas para medir el rendimiento de aplicaciones web increíblemente rápidas.



Aplicación de medición de velocidad


En un esfuerzo por mejorar nuestro desarrollo, pasamos mucho tiempo midiendo su velocidad. Y, como resultó, las métricas de rendimiento son indicadores que son sorprendentemente difíciles de entender y aplicar.

Por un lado, es difícil diseñar métricas que describan con precisión las sensaciones que experimenta el usuario mientras trabaja con el sistema. Por otro lado, no es fácil crear métricas que sean tan precisas que su análisis le permita tomar decisiones informadas. Como resultado, muchos equipos de desarrollo no pueden confiar en los datos que recopilan sobre el desempeño de sus proyectos.

Incluso si los desarrolladores tienen métricas confiables y precisas, usarlas no es fácil. ¿Cómo definir el término "rápido"? ¿Cómo encontrar un equilibrio entre velocidad y consistencia? ¿Cómo aprender a detectar rápidamente la degradación del rendimiento o aprender a evaluar el impacto de las optimizaciones en el sistema?

Aquí queremos compartir algunas ideas sobre el desarrollo de herramientas de análisis de rendimiento de aplicaciones web.

1. Usando el "reloj" correcto


JavaScript tiene dos mecanismos para recuperar marcas de tiempo: performance.now() y new Date() .

¿Cómo se diferencian? Las siguientes dos diferencias son fundamentales para nosotros:

  • El método performance.now() es mucho más preciso. La precisión de la new Date() construcción new Date() es de ± 1 ms, mientras que la precisión de performance.now() ya es de ± 100 µs (sí, ¡se trata de microsegundos !).
  • Los valores devueltos por el método performance.now() siempre aumentan a una velocidad constante y son independientes de la hora del sistema. Este método simplemente mide intervalos de tiempo sin enfocarse en el tiempo del sistema. Y en la new Date() afecta la hora new Date() sistema. Si reorganiza el reloj del sistema, también cambiará lo que devuelve la new Date () , y esto arruinará los datos de monitoreo del rendimiento.

Aunque los "relojes" representados por el método performance.now() obviamente son mucho más adecuados para medir intervalos de tiempo, tampoco son ideales. Tanto performance.now() como new Date() sufren el mismo problema, que se manifiesta en el caso de que el sistema esté en estado de suspensión: las mediciones incluyen el momento en que la máquina ni siquiera estaba activa.

2. Comprobación de la actividad de la aplicación


Si, midiendo el rendimiento de una aplicación web, cambia de su pestaña a otra, esto interrumpirá el proceso de recopilación de datos. Por qué El hecho es que el navegador restringe las aplicaciones ubicadas en las pestañas de fondo.

Hay dos situaciones en las que las métricas pueden estar distorsionadas. Como resultado, la aplicación parecerá mucho más lenta de lo que realmente es.

  1. La computadora entra en modo de suspensión.
  2. La aplicación se ejecuta en la pestaña de fondo del navegador.

La ocurrencia de ambas situaciones no es infrecuente. Afortunadamente, tenemos dos opciones para resolverlos.

Primero, simplemente podemos ignorar las métricas distorsionadas, descartando los resultados de medición que difieren demasiado de algunos valores razonables. Por ejemplo, el código que se llama cuando se presiona un botón simplemente no se puede ejecutar durante 15 minutos. Quizás esto es lo único que necesita para lidiar con los dos problemas descritos anteriormente.

En segundo lugar, puede usar la propiedad document.hidden y el evento de cambio de visibilidad . El evento de cambio de visibilitychange se genera cuando el usuario cambia de la pestaña de interés del navegador a otra pestaña o vuelve a la pestaña de interés para nosotros. Se llama cuando la ventana del navegador minimiza o maximiza cuando la computadora comienza a funcionar, saliendo del modo de suspensión. En otras palabras, esto es exactamente lo que necesitamos. Además, siempre que la pestaña esté en segundo plano, la propiedad document.hidden es true .

Aquí hay un ejemplo simple que demuestra el uso de la propiedad document.hidden y el evento de cambio de visibilitychange .

 let lastVisibilityChange = 0 window.addEventListener('visibilitychange', () => {  lastVisibilityChange = performance.now() }) //    ,      , //  ,   ,     if (metric.start < lastVisibilityChange || document.hidden) return 

Como puede ver, descartamos algunos datos, pero esto es bueno. El hecho es que se trata de datos relacionados con esos períodos del programa en los que no puede utilizar completamente los recursos del sistema.

Ahora hablamos de indicadores que no nos interesan. Pero hay muchas situaciones, los datos recopilados son muy interesantes para nosotros. Veamos cómo recopilar estos datos.

3. Busque el indicador que le permite capturar mejor la hora en que comenzó el evento


Una de las características más controvertidas de JavaScript es que el bucle de eventos para este lenguaje es de un solo subproceso. En cierto momento, solo una pieza de código es capaz de ejecutarse, cuya ejecución no puede ser interrumpida.

Si el usuario presiona el botón mientras ejecuta un código determinado, el programa no lo sabrá hasta que se complete la ejecución de este código. Por ejemplo, si la aplicación pasó 1000 ms en un ciclo continuo y el usuario presionó el botón Escape 100 ms después del inicio del ciclo, el evento no se registrará durante otros 900 ms.

Esto puede distorsionar severamente las métricas. Si necesitamos precisión para medir exactamente cómo el usuario percibe trabajar con el programa, ¡entonces este es un gran problema!

Afortunadamente, resolver este problema no es tan difícil. Si estamos hablando del evento actual, entonces podemos, en lugar de usar performance.now() (el momento en que vimos el evento), usar window.event.timeStamp (el momento en que se creó el evento).

La marca de tiempo del evento se establece mediante el proceso del navegador principal. Dado que este proceso no se bloquea cuando el bucle de eventos JS está bloqueado, event.timeStamp nos brinda información mucho más valiosa sobre cuándo se event.timeStamp el evento.

Cabe señalar que este mecanismo no es ideal. Entonces, entre el momento en que se presiona el botón físico y el momento en que el evento correspondiente llega a Chrome, transcurren entre 9 y 15 ms de tiempo no contabilizado ( aquí hay un excelente artículo del que puede aprender por qué sucede esto).

Sin embargo, incluso si podemos medir el tiempo que tarda el evento en llegar a Chrome, no deberíamos incluir este tiempo en nuestras métricas. Por qué El hecho es que no podemos introducir tales optimizaciones en el código que puedan afectar significativamente dichos retrasos. No podemos mejorarlos de ninguna manera.

Como resultado, si hablamos de encontrar la marca de tiempo para el inicio del evento, entonces el indicador event.timeStamp ve más adecuado aquí.

¿Cuál es la mejor estimación de cuándo termina el evento?

4. Apague el temporizador en requestAnimationFrame ()


Una consecuencia más se deduce de las características del dispositivo de bucle de eventos en JavaScript: algunos códigos que no están relacionados con su código pueden ejecutarse después, pero antes de que el navegador muestre una versión actualizada de la página en la pantalla.

Considere, por ejemplo, React. Después de ejecutar su código, React actualiza el DOM. Si solo mide el tiempo en su código, significa que no medirá el tiempo que llevó ejecutar el código React.

Para medir este tiempo extra, usamos requestAnimationFrame() para apagar el temporizador. Esto se hace solo cuando el navegador está listo para generar el siguiente fotograma.

 requestAnimationFrame(() => { metric.finish(performance.now()) }) 

Aquí está el ciclo de vida del marco (el diagrama está tomado de este maravilloso material bajo requestAnimationFrame ).


Ciclo de vida del marco

Como puede ver en esta figura, se llama a requestAnimationFrame() después de completar el procesador, justo antes de que se muestre el marco. Si apagamos el temporizador aquí, significa que podemos estar absolutamente seguros de que todo lo que se tomó el tiempo para actualizar la pantalla se incluye en los datos recopilados en el intervalo de tiempo.

Hasta ahora todo bien, pero ahora la situación se está volviendo bastante complicada ...

5. Ignorando el tiempo requerido para crear un diseño de página y su visualización.


El diagrama anterior, que muestra el ciclo de vida de un marco, ilustra otro problema que encontramos. Al final del ciclo de vida del marco, hay bloques de Diseño (formando un diseño de página) y Pintura (mostrando una página). Si no tiene en cuenta el tiempo requerido para completar estas operaciones, el tiempo medido por nosotros será inferior al tiempo que tardan en aparecer algunos datos actualizados en la pantalla.

Afortunadamente, requestAnimationFrame tiene otro as bajo la manga. Cuando requestAnimationFrame llama a la función pasada por requestAnimationFrame , se pasa una marca de tiempo a esta función, que indica el tiempo de inicio de la formación del marco actual (es decir, el que se encuentra en la parte izquierda de nuestro diagrama). Esta marca de tiempo suele estar muy cerca de la hora de finalización del fotograma anterior.

Como resultado, el inconveniente anterior se puede corregir midiendo el tiempo total transcurrido desde el momento del event.timeStamp hasta el momento en que comienza la formación del siguiente cuadro. Tenga en cuenta el requestAnimationFrame anidado:

 requestAnimationFrame(() => {  requestAnimationFrame((timestamp) => { metric.finish(timestamp) }) }) 

Aunque lo que se muestra arriba parece una excelente solución al problema, al final, decidimos no usar este diseño. El hecho es que, aunque esta técnica permite obtener datos más confiables, la precisión de dichos datos se reduce. Los marcos en Chrome se forman con una frecuencia de 16 ms. Esto significa que la mayor precisión disponible para nosotros es de ± 16 ms. Y si el navegador está sobrecargado y omite fotogramas, la precisión será aún menor y este deterioro será impredecible.

Si implementa esta solución, una mejora importante en el rendimiento de su código, como acelerar una tarea que se realizó anteriormente 32 ms, hasta 15 ms, puede no afectar los resultados de la medición del rendimiento.

Sin tener en cuenta el tiempo requerido para crear un diseño de página y su salida, obtenemos métricas mucho más precisas (± 100 μs) para el código que está bajo nuestro control. Como resultado, podemos obtener una expresión numérica de cualquier mejora realizada en este código.

También exploramos una idea similar:

 requestAnimationFrame(() => {  setTimeout(() => { metric.finish(performance.now()) } }) 

Esto incluirá el tiempo de renderizado, pero la precisión del indicador no se limitará a ± 16 ms. Sin embargo, decidimos no usar este enfoque tampoco. Si el sistema encuentra un evento de entrada largo, la llamada a qué setTimeout transmite puede retrasarse y ejecutarse significativamente después de actualizar la interfaz de usuario.

6. Aclaración del "porcentaje de eventos que están por debajo del objetivo"


Estamos desarrollando un proyecto y centrándonos en el alto rendimiento, tratando de optimizarlo de dos maneras:

  1. Velocidad. El tiempo de ejecución de la tarea más rápida debe ser lo más cercano posible a 0 ms.
  2. Uniformidad El tiempo de ejecución de la tarea más lenta debe ser lo más cercano posible al tiempo de ejecución de la tarea más rápida.

Debido al hecho de que estos indicadores cambian con el tiempo, son difíciles de visualizar y no fáciles de discutir. ¿Es posible crear un sistema de visualización de tales indicadores que nos inspire a optimizar tanto la velocidad como la uniformidad?

Un enfoque típico es medir el percentil 90 de retraso. Este enfoque le permite dibujar un gráfico lineal a lo largo del eje Y del cual se guarda el tiempo en milisegundos. Este gráfico le permite ver que el 90% de los eventos están debajo del gráfico lineal, es decir, se ejecutan más rápido que el tiempo que indica el gráfico lineal.

Se sabe que 100 ms es el límite entre lo que se percibe como "rápido" y "lento".

Pero, ¿qué descubriremos acerca de cómo se sienten los usuarios del trabajo si sabemos que el percentil 90 de retraso es de 103 ms? No particularmente mucho. ¿Qué indicadores proporcionarán a los usuarios usabilidad? No hay forma de saber esto con seguridad.

Pero, ¿qué pasa si sabemos que el percentil 90 de retraso es de 93 ms? Existe la sensación de que 93 es mejor que 103, pero no podemos decir nada más acerca de estos indicadores, así como de lo que significan en términos de percepción del usuario del proyecto. Nuevamente, no hay una respuesta exacta a esta pregunta.

Hemos encontrado una solución a este problema. Consiste en medir el porcentaje de eventos cuyo tiempo de ejecución no supera los 100 ms. Hay tres grandes ventajas de este enfoque:

  • La métrica está orientada al usuario. Puede decirnos qué porcentaje de tiempo nuestra aplicación es rápida y qué porcentaje de usuarios la perciben como rápida.
  • Esta métrica nos permite devolver las mediciones a la precisión que se perdió debido al hecho de que no medimos el tiempo necesario para completar las tareas al final del marco (hablamos de esto en la sección No. 5). Debido al hecho de que establecemos un indicador de objetivo que se ajusta en varios cuadros, los resultados de medición que están cerca de este indicador resultan ser menores o mayores.
  • Esta métrica es más fácil de calcular. Es suficiente calcular simplemente el número de eventos cuyo tiempo de ejecución está por debajo del indicador de destino, y después de eso, dividirlos por el número total de eventos. Los percentiles son mucho más difíciles de contar. Hay aproximaciones efectivas, pero para hacer todo bien, debe tener en cuenta cada dimensión.

Este enfoque tiene solo una desventaja: si los indicadores son peores que el objetivo, entonces no será fácil notar su mejora.

7. El uso de varios valores umbral en el análisis de indicadores.


Para visualizar el resultado de la optimización del rendimiento, introdujimos varios valores de umbral adicionales en nuestro sistema, por encima de 100 ms y por debajo.

Agrupamos los retrasos así:

  • Menos de 50 ms (rápido).
  • 50 a 100 ms (bien).
  • 100 a 1000 ms (lento).
  • Más de 1000 ms (terriblemente lento).

Los resultados "terriblemente lentos" nos permiten ver que nos hemos perdido mucho en alguna parte. Por lo tanto, los resaltamos en rojo brillante.

Lo que cabe en 50 ms es muy sensible a los cambios. Aquí, las mejoras de rendimiento a menudo son visibles mucho antes de que se puedan ver en un grupo que corresponde a 100 ms.

Por ejemplo, el siguiente gráfico visualiza el rendimiento de la visualización de subprocesos en Superhuman.


Ver hilo

Muestra el período de disminución del rendimiento y, luego, los resultados de las mejoras. Es difícil evaluar la caída del rendimiento si observa solo los indicadores correspondientes a 100 ms (las partes superiores de las columnas azules). Al observar los resultados que se ajustan a 50 ms (las partes superiores de las columnas verdes), los problemas de rendimiento ya son visibles mucho más claramente.

Si utilizamos el enfoque tradicional para el estudio de las métricas de rendimiento, probablemente no habríamos notado un problema cuyo efecto en el sistema se muestra en la figura anterior. Pero gracias a la forma en que tomamos medidas y la forma en que visualizamos nuestras métricas, pudimos encontrar y resolver un problema muy rápidamente.

Resumen


Resultó que era sorprendentemente difícil encontrar el enfoque correcto para trabajar con métricas de rendimiento. Logramos desarrollar una metodología que nos permite crear herramientas de alta calidad para medir el rendimiento de las aplicaciones web. A saber, estamos hablando de lo siguiente:

  1. El tiempo de inicio de un evento se mide usando event.timeStamp .
  2. El tiempo de finalización del evento se mide usando performance.now() en la devolución de llamada pasada a requestAnimationFrame() .
  3. Todo lo que sucede con la aplicación mientras está en la pestaña inactiva del navegador se ignora.
  4. Los datos se agregan utilizando un indicador, que se puede describir como "el porcentaje de eventos que están por debajo del objetivo".
  5. Los datos se visualizan con varios niveles de valores umbral.

Esta técnica le brinda las herramientas para crear métricas confiables y precisas. Puede crear gráficos que muestren claramente una caída en el rendimiento, puede visualizar los resultados de las optimizaciones. Y lo más importante: tiene la oportunidad de hacer proyectos rápidos aún más rápido.

Estimados lectores! ¿Cómo analiza el rendimiento de sus aplicaciones web?


Source: https://habr.com/ru/post/470872/


All Articles