Cómo hacer que el rendimiento sea visible con GitLab CI y Hoodoo Of GitLab Artifacts

Publicado: 2022-03-10
Resumen rápido ↬ No basta con optimizar una aplicación. Debe evitar que el rendimiento se degrade, y el primer paso para hacerlo es hacer que los cambios de rendimiento sean visibles. En este artículo, Anton Nemtsev muestra un par de formas de mostrarlas en las solicitudes de combinación de GitLab.

La degradación del rendimiento es un problema al que nos enfrentamos a diario. Podríamos esforzarnos para que la aplicación fuera ultrarrápida, pero pronto terminamos donde empezamos. Sucede debido a que se agregan nuevas funciones y al hecho de que a veces no pensamos dos veces en los paquetes que agregamos y actualizamos constantemente, o pensamos en la complejidad de nuestro código. Generalmente es una cosa pequeña, pero todavía se trata de las cosas pequeñas.

No podemos darnos el lujo de tener una aplicación lenta. El rendimiento es una ventaja competitiva que puede atraer y retener clientes. No podemos darnos el lujo de perder tiempo optimizando aplicaciones de nuevo. Es costoso y complejo. Y eso significa que, a pesar de todos los beneficios del rendimiento desde una perspectiva comercial, apenas es rentable. Como primer paso para encontrar una solución a cualquier problema, debemos hacer que el problema sea visible. Este artículo te ayudará exactamente con eso.

Nota : si tiene un conocimiento básico de Node.js, una idea vaga sobre cómo funciona su CI/CD y le preocupa el rendimiento de la aplicación o las ventajas comerciales que puede brindar, entonces estamos listos para comenzar.

Cómo crear un presupuesto de desempeño para un proyecto

Las primeras preguntas que debemos hacernos son:

“¿Qué es el proyecto performante?”

“¿Qué métricas debo usar?”

“¿Qué valores de estas métricas son aceptables?”

La selección de métricas está fuera del alcance de este artículo y depende en gran medida del contexto del proyecto, pero le recomiendo que comience leyendo Métricas de rendimiento centradas en el usuario de Philip Walton.

Desde mi perspectiva, es una buena idea usar el tamaño de la biblioteca en kilobytes como métrica para el paquete npm. ¿Por qué? Bueno, es porque si otras personas incluyen su código en sus proyectos, tal vez quieran minimizar el impacto de su código en el tamaño final de su aplicación.

Para el sitio, consideraría el tiempo hasta el primer byte (TTFB) como una métrica. Esta métrica muestra cuánto tiempo le toma al servidor responder con algo. Esta métrica es importante, pero bastante vaga porque puede incluir cualquier cosa, desde el tiempo de procesamiento del servidor hasta los problemas de latencia. Por lo tanto, es bueno usarlo junto con Server Timing u OpenTracing para descubrir en qué consiste exactamente.

También debe considerar métricas como el tiempo de interacción (TTI) y la primera pintura significativa (esta última pronto será reemplazada por la pintura más grande con contenido (LCP)). Creo que ambos son los más importantes, desde la perspectiva del rendimiento percibido.

Pero tenga en cuenta: las métricas siempre están relacionadas con el contexto , así que no lo dé por sentado. Piensa en lo que es importante en tu caso específico.

La forma más fácil de definir los valores deseados para las métricas es usar a sus competidores, o incluso a usted mismo. Además, de vez en cuando, herramientas como la Calculadora de presupuesto de rendimiento pueden ser útiles, solo juegue un poco con ella.

La degradación del rendimiento es un problema al que nos enfrentamos a diario. Podríamos esforzarnos para que la aplicación sea increíblemente rápida, pero pronto terminamos donde comenzamos.

Utilice competidores para su beneficio

Si alguna vez te escapaste de un oso extasiado y sobreexcitado, entonces ya sabes que no necesitas ser un campeón olímpico en carrera para salir de este problema. Solo necesitas ser un poco más rápido que el otro tipo.

Así que haz una lista de competidores. Si se trata de proyectos del mismo tipo, normalmente consisten en tipos de página similares entre sí. Por ejemplo, para una tienda de Internet, puede ser una página con una lista de productos, una página de detalles del producto, un carrito de compras, un pago, etc.

  1. Mida los valores de sus métricas seleccionadas en cada tipo de página para los proyectos de su competencia;
  2. Mida las mismas métricas en su proyecto;
  3. Encuentre el mejor que su valor más cercano para cada métrica en los proyectos de la competencia. Agregarles un 20% y establecer como sus próximas metas.

¿Por qué 20%? Este es un número mágico que supuestamente significa que la diferencia se notará a simple vista. Puede leer más sobre este número en el artículo de Denys Mishunov “Por qué importa el rendimiento percibido, Parte 1: La percepción del tiempo”.

Una pelea con una sombra

¿Tienes un proyecto único? ¿No tienes competidores? ¿O ya eres mejor que cualquiera de ellos en todos los sentidos posibles? No es un problema. Siempre puedes competir con el único oponente digno, es decir, tú mismo. Mida cada métrica de rendimiento de su proyecto en cada tipo de página y luego mejórelas en el mismo 20%.

¡Más después del salto! Continúe leyendo a continuación ↓

Pruebas Sintéticas

Hay dos formas de medir el rendimiento:

  • Sintético (en un ambiente controlado)
  • RUM (Medidas de Usuario Real)
    Los datos se recopilan de usuarios reales en producción.

En este artículo, usaremos pruebas sintéticas y supondremos que nuestro proyecto usa GitLab con su CI integrado para la implementación del proyecto.

Biblioteca y su tamaño como métrica

Supongamos que ha decidido desarrollar una biblioteca y publicarla en NPM. Desea que sea liviano, mucho más liviano que los competidores, para que tenga menos impacto en el tamaño final del proyecto resultante. Esto ahorra tráfico a los clientes, a veces tráfico por el que el cliente está pagando. También permite que el proyecto se cargue más rápido, lo cual es muy importante en lo que respecta a la creciente participación móvil y los nuevos mercados con velocidades de conexión lentas y cobertura de Internet fragmentada.

Paquete para medir el tamaño de la biblioteca

Para mantener el tamaño de la biblioteca lo más pequeño posible, debemos observar cuidadosamente cómo cambia durante el tiempo de desarrollo. Pero, ¿cómo puedes hacerlo? Bueno, podríamos usar el límite de tamaño del paquete creado por Andrey Sitnik de Evil Martians.

Vamos a instalarlo.

 npm i -D size-limit @size-limit/preset-small-lib

Luego, agréguelo a package.json .

 "scripts": { + "size": "size-limit", "test": "jest && eslint ." }, + "size-limit": [ + { + "path": "index.js" + } + ],

El bloque "size-limit":[{},{},…] contiene una lista del tamaño de los archivos que queremos comprobar. En nuestro caso, es solo un único archivo: index.js .

El size del script NPM simplemente ejecuta el paquete size-limit , que lee el size-limit bloque de configuración mencionado anteriormente y verifica el tamaño de los archivos que se enumeran allí. Ejecutémoslo y veamos qué sucede:

 npm run size 
El resultado de la ejecución del comando muestra el tamaño de index.js
El resultado de la ejecución del comando muestra el tamaño de index.js. (Vista previa grande)

Podemos ver el tamaño del archivo, pero este tamaño no está realmente bajo control. Arreglemos eso agregando limit a package.json :

 "size-limit": [ { + "limit": "2 KB", "path": "index.js" } ],

Ahora, si ejecutamos el script, se validará contra el límite que establezcamos.

Una captura de pantalla de la terminal; el tamaño del archivo es inferior al límite y se muestra en verde
Una captura de pantalla de la terminal; el tamaño del archivo es inferior al límite y se muestra en verde. (Vista previa grande)

En el caso de que un nuevo desarrollo cambie el tamaño del archivo hasta el punto de exceder el límite definido, el script se completará con un código distinto de cero. Esto, aparte de otras cosas, significa que detendrá la canalización en GitLab CI.

Una captura de pantalla de la terminal donde el tamaño del archivo excede el límite y se muestra en rojo. El script se terminó con un código distinto de cero.
Una captura de pantalla de la terminal donde el tamaño del archivo excede el límite y se muestra en rojo. El script se terminó con un código distinto de cero. (Vista previa grande)

Ahora podemos usar git hook para comparar el tamaño del archivo con el límite antes de cada confirmación. Incluso podemos usar el paquete husky para hacerlo de una manera agradable y simple.

Vamos a instalarlo.

 npm i -D husky

Luego, modifica nuestro package.json .

 "size-limit": [ { "limit": "2 KB", "path": "index.js" } ], + "husky": { + "hooks": { + "pre-commit": "npm run size" + } + },

Y ahora, antes de cada confirmación, se ejecutará automáticamente el comando npm run size y si terminará con un código distinto de cero, la confirmación nunca sucederá.

Una captura de pantalla de la terminal donde se anula la confirmación porque el tamaño del archivo supera el límite
Una captura de pantalla de la terminal donde se anula la confirmación porque el tamaño del archivo supera el límite. (Vista previa grande)

Pero hay muchas formas de saltarse los ganchos (intencionalmente o incluso por accidente), por lo que no debemos confiar demasiado en ellos.

Además, es importante tener en cuenta que no deberíamos necesitar hacer este bloqueo de verificación. ¿Por qué? Porque está bien que el tamaño de la biblioteca crezca mientras agrega nuevas funciones. Necesitamos que los cambios sean visibles, eso es todo. Esto ayudará a evitar un aumento de tamaño accidental debido a la introducción de una biblioteca auxiliar que no necesitamos. Y, tal vez, brinde a los desarrolladores y propietarios de productos una razón para considerar si vale la pena aumentar el tamaño de la función que se agrega. O, tal vez, si existen paquetes alternativos más pequeños. Bundlephobia nos permite encontrar una alternativa para casi cualquier paquete de NPM.

¿Entonces, qué debemos hacer? ¡Mostremos el cambio en el tamaño del archivo directamente en la solicitud de fusión! Pero no presionas para dominar directamente; actúas como un desarrollador adulto, ¿verdad?

Ejecutando nuestro cheque en GitLab CI

Agreguemos un artefacto de GitLab del tipo de métricas. Un artefacto es un archivo, que "vivirá" después de que finalice la operación de canalización. Este tipo específico de artefacto nos permite mostrar un widget adicional en la solicitud de combinación, mostrando cualquier cambio en el valor de la métrica entre el artefacto en el maestro y la rama de funciones. El formato del artefacto de metrics es un formato Prometheus de texto. Para los valores de GitLab dentro del artefacto, es solo texto. GitLab no entiende qué es lo que ha cambiado exactamente en el valor, solo sabe que el valor es diferente. Entonces, ¿qué debemos hacer exactamente?

  1. Definir artefactos en la canalización.
  2. Cambie el script para que cree un artefacto en la canalización.

Para crear un artefacto necesitamos cambiar .gitlab-ci.yml esta manera:

 image: node:latest stages: - performance sizecheck: stage: performance before_script: - npm ci script: - npm run size + artifacts: + expire_in: 7 days + paths: + - metric.txt + reports: + metrics: metric.txt
  1. expire_in: 7 days : el artefacto existirá durante 7 días.
  2.  paths: metric.txt

    Se guardará en el catálogo raíz. Si omite esta opción, no será posible descargarlo.
  3.  reports: metrics: metric.txt

    El artefacto tendrá el tipo reports:metrics

Ahora hagamos que Límite de tamaño genere un informe. Para hacerlo, debemos cambiar package.json :

 "scripts": { - "size": "size-limit", + "size": "size-limit --json > size-limit.json", "test": "jest && eslint ." },

size-limit con clave --json generará datos en formato json:

El comando size-limit --json envía JSON a la consola. JSON contiene una matriz de objetos que contienen un nombre y tamaño de archivo, y también nos permite saber si excede el límite de tamaño.
El comando size-limit --json JSON a la consola. JSON contiene una matriz de objetos que contienen un nombre y tamaño de archivo, y también nos permite saber si excede el límite de tamaño. (Vista previa grande)

Y la redirección > size-limit.json guardará JSON en el archivo size-limit.json .

Ahora necesitamos crear un artefacto a partir de esto. El formato se reduce a [metrics name][space][metrics value] . Vamos a crear el script generate-metric.js :

 const report = require('./size-limit.json'); process.stdout.write(`size ${(report[0].size/1024).toFixed(1)}Kb`); process.exit(0);

Y agréguelo a package.json :

 "scripts": { "size": "size-limit --json > size-limit.json", + "postsize": "node generate-metric.js > metric.txt", "test": "jest && eslint ." },

Debido a que hemos usado el prefijo post , el npm run size ejecutará primero el script size y luego, automáticamente, ejecutará el script postsize , lo que dará como resultado la creación del archivo metric.txt , nuestro artefacto.

Como resultado, cuando fusionemos esta rama con master, cambiemos algo y creemos una nueva solicitud de fusión, veremos lo siguiente:

Captura de pantalla con una solicitud de fusión, que nos muestra un widget con valores de métrica nuevos y antiguos entre corchetes
Captura de pantalla con una solicitud de combinación, que nos muestra un widget con valores de métrica nuevos y antiguos entre corchetes. (Vista previa grande)

En el widget que aparece en la página, primero vemos el nombre de la métrica ( size ) seguido del valor de la métrica en la rama de características, así como el valor en el maestro entre corchetes.

Ahora podemos ver cómo cambiar el tamaño del paquete y tomar una decisión razonable sobre si debemos fusionarlo o no.

  • Puede ver todo este código en este repositorio.

Reanudar

¡OK! Entonces, hemos descubierto cómo manejar el caso trivial. Si tiene varios archivos, simplemente separe las métricas con saltos de línea. Como alternativa para el límite de tamaño, puede considerar el tamaño del paquete. Si está utilizando WebPack, puede obtener todos los tamaños que necesita compilando con las banderas --profile y --json :

 webpack --profile --json > stats.json

Si usa next.js, puede usar el complemento @next/bundle-analyzer. ¡Tu decides!

usando el faro

Lighthouse es el estándar de facto en el análisis de proyectos. Escribamos un script que nos permita medir el rendimiento, a11y, las mejores prácticas y nos proporcione una puntuación de SEO.

Guión para medir todas las cosas

Para comenzar, necesitamos instalar el paquete lighthouse que realizará las mediciones. También necesitamos instalar el titiritero que usaremos como navegador sin interfaz.

 npm i -D lighthouse puppeteer

A continuación, creemos un script lighthouse.js e iniciemos nuestro navegador:

 const puppeteer = require('puppeteer'); (async () => { const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox', '--headless'], }); })();

Ahora escribamos una función que nos ayude a analizar una URL dada:

 const lighthouse = require('lighthouse'); const DOMAIN = process.env.DOMAIN; const buildReport = browser => async url => { const data = await lighthouse( `${DOMAIN}${url}`, { port: new URL(browser.wsEndpoint()).port, output: 'json', }, { extends: 'lighthouse:full', } ); const { report: reportJSON } = data; const report = JSON.parse(reportJSON); // … }

¡Genial! Ahora tenemos una función que aceptará el objeto del navegador como argumento y devolverá una función que aceptará la URL como argumento y generará un informe después de pasar esa URL al lighthouse .

Estamos pasando los siguientes argumentos al lighthouse :

  1. La dirección que queremos analizar;
  2. opciones de lighthouse , port del navegador en particular y output (formato de salida del informe);
  3. report de configuración y lighthouse:full (todo lo que podemos medir). Para una configuración más precisa, consulte la documentación.

¡Maravilloso! Ya tenemos nuestro informe. Pero, ¿qué podemos hacer con él? Bueno, podemos comparar las métricas con los límites y salir del script con un código distinto de cero que detendrá la canalización:

 if (report.categories.performance.score < 0.8) process.exit(1);

¿Pero solo queremos que el rendimiento sea visible y no bloquee? Luego, adoptemos otro tipo de artefacto: el artefacto de rendimiento de GitLab.

Artefacto de rendimiento de GitLab

Para comprender este formato de artefactos, debemos leer el código del complemento sitespeed.io. (¿Por qué GitLab no puede describir el formato de sus artefactos dentro de su propia documentación? Misterio).

 [ { "subject":"/", "metrics":[ { "name":"Transfer Size (KB)", "value":"19.5", "desiredSize":"smaller" }, { "name":"Total Score", "value":92, "desiredSize":"larger" }, {…} ] }, {…} ]

Un artefacto es un archivo JSON que contiene una matriz de objetos. Cada uno de ellos representa un informe sobre una URL .

 [{page 1}, {page 2}, …]

Cada página está representada por un objeto con los siguientes atributos:

  1. subject
    Identificador de página (es bastante útil usar ese nombre de ruta);
  2. metrics
    Una matriz de los objetos (cada uno de ellos representa una medida que se realizó en la página).
 { "subject":"/login/", "metrics":[{measurement 1}, {measurement 2}, {measurement 3}, …] }

Una measurement es un objeto que contiene los siguientes atributos:

  1. name
    Nombre de la medida, por ejemplo, puede ser Time to first byte o Time to interactive .
  2. value
    Resultado de la medición numérica.
  3. desiredSize
    Si el valor objetivo debe ser lo más pequeño posible, por ejemplo, para la métrica Time to interactive , entonces el valor debe ser smaller . Si debe ser lo más grande posible, por ejemplo, para la Performance score del faro, utilice larger .
 { "name":"Time to first byte (ms)", "value":240, "desiredSize":"smaller" }

Modifiquemos nuestra función buildReport de manera que devuelva un informe para una página con métricas estándar de Lighthouse.

Captura de pantalla con informe de faro. Hay puntuación de rendimiento, puntuación a11y, puntuación de mejores prácticas, puntuación de SEO
Captura de pantalla con informe de faro. Hay puntuación de rendimiento, puntuación a11y, puntuación de mejores prácticas, puntuación de SEO. (Vista previa grande)
 const buildReport = browser => async url => { // … const metrics = [ { name: report.categories.performance.title, value: report.categories.performance.score, desiredSize: 'larger', }, { name: report.categories.accessibility.title, value: report.categories.accessibility.score, desiredSize: 'larger', }, { name: report.categories['best-practices'].title, value: report.categories['best-practices'].score, desiredSize: 'larger', }, { name: report.categories.seo.title, value: report.categories.seo.score, desiredSize: 'larger', }, { name: report.categories.pwa.title, value: report.categories.pwa.score, desiredSize: 'larger', }, ]; return { subject: url, metrics: metrics, }; }

Ahora, cuando tenemos una función que genera un informe. Vamos a aplicarlo a cada tipo de las páginas del proyecto. Primero, debo indicar que process.env.DOMAIN debe contener un dominio de ensayo (en el que debe implementar su proyecto desde una rama de función de antemano).

 + const fs = require('fs'); const lighthouse = require('lighthouse'); const puppeteer = require('puppeteer'); const DOMAIN = process.env.DOMAIN; const buildReport = browser => async url => {/* … */}; + const urls = [ + '/inloggen', + '/wachtwoord-herstellen-otp', + '/lp/service', + '/send-request-to/ww-tammer', + '/post-service-request/binnenschilderwerk', + ]; (async () => { const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox', '--headless'], }); + const builder = buildReport(browser); + const report = []; + for (let url of urls) { + const metrics = await builder(url); + report.push(metrics); + } + fs.writeFileSync(`./performance.json`, JSON.stringify(report)); + await browser.close(); })();
  • Puede encontrar la fuente completa en esta esencia y un ejemplo de trabajo en este repositorio.

Nota : en este punto, es posible que desee interrumpirme y gritar en vano: "¿Por qué me quita el tiempo? ¡Ni siquiera puede usar Promise.all correctamente!" En mi defensa, me atrevo a decir que no se recomienda ejecutar más de una instancia de faro al mismo tiempo porque esto afecta negativamente la precisión de los resultados de la medición. Además, si no demuestras el debido ingenio, dará lugar a una excepción.

Uso de múltiples procesos

¿Todavía te gustan las medidas paralelas? Bien, es posible que desee usar un clúster de nodos (o incluso subprocesos de trabajo si le gusta jugar en negrita), pero tiene sentido discutirlo solo en el caso de que su canalización se ejecute en el entorno con múltiples cors disponibles. E incluso entonces, debe tener en cuenta que, debido a la naturaleza de Node.js, se generará una instancia de Node.js de peso completo en cada bifurcación del proceso (en lugar de reutilizar la misma, lo que conducirá a un mayor consumo de RAM). Todo esto significa que será más costoso debido al creciente requerimiento de hardware y un poco más rápido. Puede parecer que el juego no vale la pena.

Si quieres correr ese riesgo, tendrás que:

  1. Divida la matriz de URL en fragmentos por número de núcleos;
  2. Cree una bifurcación de un proceso de acuerdo con el número de núcleos;
  3. Transfiera partes de la matriz a las bifurcaciones y luego recupere los informes generados.

Para dividir una matriz, puede usar enfoques de pila múltiple. El siguiente código, escrito en solo un par de minutos, no sería peor que los demás:

 /** * Returns urls array splited to chunks accordin to cors number * * @param urls {String[]} — URLs array * @param cors {Number} — count of available cors * @return {Array } — URLs array splited to chunks */ function chunkArray(urls, cors) { const chunks = [...Array(cors)].map(() => []); let index = 0; urls.forEach((url) => { if (index > (chunks.length - 1)) { index = 0; } chunks[index].push(url); index += 1; }); return chunks; } /** * Returns urls array splited to chunks accordin to cors number * * @param urls {String[]} — URLs array * @param cors {Number} — count of available cors * @return {Array } — URLs array splited to chunks */ function chunkArray(urls, cors) { const chunks = [...Array(cors)].map(() => []); let index = 0; urls.forEach((url) => { if (index > (chunks.length - 1)) { index = 0; } chunks[index].push(url); index += 1; }); return chunks; }

Haz bifurcaciones según el número de núcleos:

 // Adding packages that allow us to use cluster const cluster = require('cluster'); // And find out how many cors are available. Both packages are build-in for node.js. const numCPUs = require('os').cpus().length; (async () => { if (cluster.isMaster) { // Parent process const chunks = chunkArray(urls, urls.length/numCPUs); chunks.map(chunk => { // Creating child processes const worker = cluster.fork(); }); } else { // Child process } })();

Transfiramos una serie de fragmentos a procesos secundarios y recuperemos los informes:

 (async () => { if (cluster.isMaster) { // Parent process const chunks = chunkArray(urls, urls.length/numCPUs); chunks.map(chunk => { const worker = cluster.fork(); + // Send message with URL's array to child process + worker.send(chunk); }); } else { // Child process + // Recieveing message from parent proccess + process.on('message', async (urls) => { + const browser = await puppeteer.launch({ + args: ['--no-sandbox', '--disable-setuid-sandbox', '--headless'], + }); + const builder = buildReport(browser); + const report = []; + for (let url of urls) { + // Generating report for each URL + const metrics = await builder(url); + report.push(metrics); + } + // Send array of reports back to the parent proccess + cluster.worker.send(report); + await browser.close(); + }); } })();

Y, finalmente, vuelva a ensamblar los informes en una matriz y genere un artefacto.

  • Consulte el código completo y el repositorio con un ejemplo que muestra cómo usar Lighthouse con múltiples procesos.

Precisión de las medidas

Bueno, paralelizamos las medidas, lo que aumentó el ya desafortunado gran error de medición del lighthouse . Pero, ¿cómo lo reducimos? Bueno, haz algunas medidas y calcula el promedio.

Para ello, escribiremos una función que calculará la media entre los resultados de las mediciones actuales y las anteriores.

 // Count of measurements we want to make const MEASURES_COUNT = 3; /* * Reducer which will calculate an avarage value of all page measurements * @param pages {Object} — accumulator * @param page {Object} — page * @return {Object} — page with avarage metrics values */ const mergeMetrics = (pages, page) => { if (!pages) return page; return { subject: pages.subject, metrics: pages.metrics.map((measure, index) => { let value = (measure.value + page.metrics[index].value)/2; value = +value.toFixed(2); return { ...measure, value, } }), } }

Luego, cambia nuestro código para usarlos:

 process.on('message', async (urls) => { const browser = await puppeteer.launch({ args: ['--no-sandbox', '--disable-setuid-sandbox', '--headless'], }); const builder = buildReport(browser); const report = []; for (let url of urls) { + // Let's measure MEASURES_COUNT times and calculate the avarage + let measures = []; + let index = MEASURES_COUNT; + while(index--){ const metric = await builder(url); + measures.push(metric); + } + const measure = measures.reduce(mergeMetrics); report.push(measure); } cluster.worker.send(report); await browser.close(); }); }
  • Consulte la esencia con el código completo y el repositorio con un ejemplo.

Y ahora podemos agregar el lighthouse a la canalización.

Agregarlo a la tubería

Primero, cree un archivo de configuración llamado .gitlab-ci.yml .

 image: node:latest stages: # You need to deploy a project to staging and put the staging domain name # into the environment variable DOMAIN. But this is beyond the scope of this article, # primarily because it is very dependent on your specific project. # - deploy # - performance lighthouse: stage: performance before_script: - apt-get update - apt-get -y install gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget - npm ci script: - node lighthouse.js artifacts: expire_in: 7 days paths: - performance.json reports: performance: performance.json

Los múltiples paquetes instalados son necesarios para el puppeteer . Como alternativa, puede considerar usar docker . Aparte de eso, tiene sentido el hecho de que establezcamos el tipo de artefacto como rendimiento. Y, tan pronto como la rama principal y la de funciones la tengan, verá un widget como este en la solicitud de fusión:

Una captura de pantalla de la página de solicitud de fusión. Hay un widget que muestra qué métricas de Lighthouse han cambiado y cómo exactamente
Una captura de pantalla de la página de solicitud de fusión. Hay un widget que muestra qué métricas de Lighthouse han cambiado y cómo exactamente. (Vista previa grande)

¿Agradable?

Reanudar

Finalmente hemos terminado con un caso más complejo. Obviamente, hay varias herramientas similares además del faro. Por ejemplo, sitespeed.io. La documentación de GitLab incluso contiene un artículo que explica cómo usar sitespeed en la canalización de GitLab. También existe un complemento para GitLab que nos permite generar un artefacto. Pero, ¿quién preferiría productos de código abierto impulsados ​​por la comunidad a los que son propiedad de un monstruo corporativo?

No hay descanso para los malvados

Puede parecer que finalmente estamos ahí, pero no, todavía no. Si está utilizando una versión paga de GitLab, los artefactos con metrics de tipos de informes y performance están presentes en los planes a partir de premium y silver , que cuestan $ 19 por mes para cada usuario. Además, no puede simplemente comprar una función específica que necesita, solo puede cambiar el plan. Lo siento. ¿Así que lo que podemos hacer? A diferencia de GitHub con su API de verificación y su API de estado, GitLab no le permitiría crear un widget real en la solicitud de combinación usted mismo. Y no hay esperanza de conseguirlos pronto.

Una captura de pantalla del tweet publicado por Ilya Klimov (empleado de GitLab) escribió sobre la probabilidad de que aparezcan análogos para Github Checks and Status API: “Extremadamente improbable. Los controles ya están disponibles a través de la API de estado de compromiso y, en cuanto a los estados, nos esforzamos por ser un ecosistema cerrado”.
Una captura de pantalla del tweet publicado por Ilya Klimov (empleado de GitLab) que escribió sobre la probabilidad de que aparezcan análogos para Github Checks and Status API. (Vista previa grande)

Una forma de verificar si realmente tiene soporte para estas funciones: puede buscar la variable de entorno GITLAB_FEATURES en la canalización. Si carece merge_request_performance_metrics y metrics_reports en la lista, entonces estas características no son compatibles.

 GITLAB_FEATURES=audit_events,burndown_charts,code_owners,contribution_analytics, elastic_search, export_issues,group_bulk_edit,group_burndown_charts,group_webhooks, issuable_default_templates,issue_board_focus_mode,issue_weights,jenkins_integration, ldap_group_sync,member_lock,merge_request_approvers,multiple_issue_assignees, multiple_ldap_servers,multiple_merge_request_assignees,protected_refs_for_users, push_rules,related_issues,repository_mirrors,repository_size_limit,scoped_issue_board, usage_quotas,visual_review_app,wip_limits

Si no hay apoyo, tenemos que pensar en algo. Por ejemplo, podemos agregar un comentario a la solicitud de fusión, comentar con la tabla, que contiene todos los datos que necesitamos. Podemos dejar nuestro código intacto: se crearán artefactos, pero los widgets siempre mostrarán el mensaje «metrics are unchanged» .

Comportamiento muy extraño y no obvio; Tuve que pensar cuidadosamente para entender lo que estaba pasando.

¿Así que, cuál es el plan?

  1. Necesitamos leer el artefacto de la rama master ;
  2. Cree un comentario en el formato de markdown ;
  3. Obtener el identificador de la solicitud de fusión de la rama de función actual al maestro;
  4. Agrega el comentario.

Cómo leer artefactos de la rama maestra

Si queremos mostrar cómo se cambian las métricas de rendimiento entre las ramas master y característica, debemos leer el artefacto del master . Y para hacerlo, necesitaremos usar fetch .

 npm i -S isomorphic-fetch
 // You can use predefined CI environment variables // @see https://gitlab.com/help/ci/variables/predefined_variables.md // We need fetch polyfill for node.js const fetch = require('isomorphic-fetch'); // GitLab domain const GITLAB_DOMAIN = process.env.CI_SERVER_HOST || process.env.GITLAB_DOMAIN || 'gitlab.com'; // User or organization name const NAME_SPACE = process.env.CI_PROJECT_NAMESPACE || process.env.PROJECT_NAMESPACE || 'silentimp'; // Repo name const PROJECT = process.env.CI_PROJECT_NAME || process.env.PROJECT_NAME || 'lighthouse-comments'; // Name of the job, which create an artifact const JOB_NAME = process.env.CI_JOB_NAME || process.env.JOB_NAME || 'lighthouse'; /* * Returns an artifact * * @param name {String} - artifact file name * @return {Object} - object with performance artifact * @throw {Error} - thhrow an error, if artifact contain string, that can't be parsed as a JSON. Or in case of fetch errors. */ const getArtifact = async name => { const response = await fetch(`https://${GITLAB_DOMAIN}/${NAME_SPACE}/${PROJECT}/-/jobs/artifacts/master/raw/${name}?job=${JOB_NAME}`); if (!response.ok) throw new Error('Artifact not found'); const data = await response.json(); return data; };

Creación de un texto de comentario

Necesitamos construir el texto del comentario en el formato de markdown . Vamos a crear algunas funciones de servicio que nos ayudarán:

 /** * Return part of report for specific page * * @param report {Object} — report * @param subject {String} — subject, that allow find specific page * @return {Object} — page report */ const getPage = (report, subject) => report.find(item => (item.subject === subject)); /** * Return specific metric for the page * * @param page {Object} — page * @param name {String} — metrics name * @return {Object} — metric */ const getMetric = (page, name) => page.metrics.find(item => item.name === name); /** * Return table cell for desired metric * * @param branch {Object} - report from feature branch * @param master {Object} - report from master branch * @param name {String} - metrics name */ const buildCell = (branch, master, name) => { const branchMetric = getMetric(branch, name); const masterMetric = getMetric(master, name); const branchValue = branchMetric.value; const masterValue = masterMetric.value; const desiredLarger = branchMetric.desiredSize === 'larger'; const isChanged = branchValue !== masterValue; const larger = branchValue > masterValue; if (!isChanged) return `${branchValue}`; if (larger) return `${branchValue} ${desiredLarger ? '' : '' } **+${Math.abs(branchValue - masterValue).toFixed(2)}**`; return `${branchValue} ${!desiredLarger ? '' : '' } **-${Math.abs(branchValue - masterValue).toFixed(2)}**`; }; /** * Returns text of the comment with table inside * This table contain changes in all metrics * * @param branch {Object} report from feature branch * @param master {Object} report from master branch * @return {String} comment markdown */ const buildCommentText = (branch, master) =>{ const md = branch.map( page => { const pageAtMaster = getPage(master, page.subject); if (!pageAtMaster) return ''; const md = `|${page.subject}|${buildCell(page, pageAtMaster, 'Performance')}|${buildCell(page, pageAtMaster, 'Accessibility')}|${buildCell(page, pageAtMaster, 'Best Practices')}|${buildCell(page, pageAtMaster, 'SEO')}| `; return md; }).join(''); return ` |Path|Performance|Accessibility|Best Practices|SEO| |--- |--- |--- |--- |--- | ${md} `; };

Guión que construirá un comentario

Deberá tener un token para trabajar con la API de GitLab. Para generar uno, debe abrir GitLab, iniciar sesión, abrir la opción 'Configuración' del menú y luego abrir 'Tokens de acceso' que se encuentra en el lado izquierdo del menú de navegación. Entonces debería poder ver el formulario, que le permite generar el token.

Captura de pantalla que muestra el formulario de generación de tokens y las opciones de menú que mencioné anteriormente.
Captura de pantalla que muestra el formulario de generación de tokens y las opciones de menú que mencioné anteriormente. (Vista previa grande)

Además, necesitará una identificación del proyecto. Puede encontrarlo en el repositorio 'Configuración' (en el submenú 'General'):

La captura de pantalla muestra la página de configuración, donde puede encontrar el ID del proyecto
La captura de pantalla muestra la página de configuración, donde puede encontrar el ID del proyecto. (Vista previa grande)

Para agregar un comentario a la solicitud de combinación, necesitamos saber su ID. La función que le permite adquirir la ID de solicitud de fusión se ve así:

 // You can set environment variables via CI/CD UI. // @see https://gitlab.com/help/ci/variables/README#variables // I have set GITLAB_TOKEN this way // ID of the project const GITLAB_PROJECT_ID = process.env.CI_PROJECT_ID || '18090019'; // Token const TOKEN = process.env.GITLAB_TOKEN; /** * Returns iid of the merge request from feature branch to master * @param from {String} — name of the feature branch * @param to {String} — name of the master branch * @return {Number} — iid of the merge request */ const getMRID = async (from, to) => { const response = await fetch(`https://${GITLAB_DOMAIN}/api/v4/projects/${GITLAB_PROJECT_ID}/merge_requests?target_branch=${to}&source_branch=${from}`, { method: 'GET', headers: { 'PRIVATE-TOKEN': TOKEN, } }); if (!response.ok) throw new Error('Merge request not found'); const [{iid}] = await response.json(); return iid; };

We need to get a feature branch name. You may use the environment variable CI_COMMIT_REF_SLUG inside the pipeline. Outside of the pipeline, you can use the current-git-branch package. Also, you will need to form a message body.

Let's install the packages we need for this matter:

 npm i -S current-git-branch form-data

And now, finally, function to add a comment:

 const FormData = require('form-data'); const branchName = require('current-git-branch'); // Branch from which we are making merge request // In the pipeline we have environment variable `CI_COMMIT_REF_NAME`, // which contains name of this banch. Function `branchName` // will return something like «HEAD detached» message in the pipeline. // And name of the branch outside of pipeline const CURRENT_BRANCH = process.env.CI_COMMIT_REF_NAME || branchName(); // Merge request target branch, usually it's master const DEFAULT_BRANCH = process.env.CI_DEFAULT_BRANCH || 'master'; /** * Adding comment to merege request * @param md {String} — markdown text of the comment */ const addComment = async md => { const iid = await getMRID(CURRENT_BRANCH, DEFAULT_BRANCH); const commentPath = `https://${GITLAB_DOMAIN}/api/v4/projects/${GITLAB_PROJECT_ID}/merge_requests/${iid}/notes`; const body = new FormData(); body.append('body', md); await fetch(commentPath, { method: 'POST', headers: { 'PRIVATE-TOKEN': TOKEN, }, body, }); };

And now we can generate and add a comment:

 cluster.on('message', (worker, msg) => { report = [...report, ...msg]; worker.disconnect(); reportsCount++; if (reportsCount === chunks.length) { fs.writeFileSync(`./performance.json`, JSON.stringify(report)); + if (CURRENT_BRANCH === DEFAULT_BRANCH) process.exit(0); + try { + const masterReport = await getArtifact('performance.json'); + const md = buildCommentText(report, masterReport) + await addComment(md); + } catch (error) { + console.log(error); + } process.exit(0); } });
  • Check the gist and demo repository.

Now create a merge request and you will get:

A screenshot of the merge request which shows comment with a table that contains a table with lighthouse metrics change
A screenshot of the merge request which shows comment with a table that contains a table with lighthouse metrics change. (Vista previa grande)

Reanudar

Comments are much less visible than widgets but it's still much better than nothing. This way we can visualize the performance even without artifacts.

Autenticación

OK, but what about authentication? The performance of the pages that require authentication is also important. It's easy: we will simply log in. puppeteer is essentially a fully-fledged browser and we can write scripts that mimic user actions:

 const LOGIN_URL = '/login'; const USER_EMAIL = process.env.USER_EMAIL; const USER_PASSWORD = process.env.USER_PASSWORD; /** * Authentication sctipt * @param browser {Object} — browser instance */ const login = async browser => { const page = await browser.newPage(); page.setCacheEnabled(false); await page.goto(`${DOMAIN}${LOGIN_URL}`, { waitUntil: 'networkidle2' }); await page.click('input[name=email]'); await page.keyboard.type(USER_EMAIL); await page.click('input[name=password]'); await page.keyboard.type(USER_PASSWORD); await page.click('button[data-test]', { waitUntil: 'domcontentloaded' }); };

Before checking a page that requires authentication, we may just run this script. Hecho.

Resumen

In this way, I built the performance monitoring system at Werkspot — a company I currently work for. It's great when you have the opportunity to experiment with the bleeding edge technology.

Now you also know how to visualize performance change, and it's sure to help you better track performance degradation. But what comes next? You can save the data and visualize it for a time period in order to better understand the big picture, and you can collect performance data directly from the users.

You may also check out a great talk on this subject: “Measuring Real User Performance In The Browser.” When you build the system that will collect performance data and visualize them, it will help to find your performance bottlenecks and resolve them. Good luck with that!