Como tornar o desempenho visível com o GitLab CI e o Hoodoo de artefatos do GitLab

Publicados: 2022-03-10
Resumo rápido ↬ Não basta otimizar um aplicativo. Você precisa evitar a degradação do desempenho, e o primeiro passo para fazer isso é tornar as alterações de desempenho visíveis. Neste artigo, Anton Nemtsev mostra algumas maneiras de mostrá-los nas solicitações de mesclagem do GitLab.

A degradação do desempenho é um problema que enfrentamos diariamente. Poderíamos nos esforçar para tornar o aplicativo muito rápido, mas logo acabamos onde começamos. Isso está acontecendo por causa de novos recursos sendo adicionados e pelo fato de que às vezes não pensamos duas vezes em pacotes que adicionamos e atualizamos constantemente, ou pensamos na complexidade do nosso código. Geralmente é uma coisa pequena, mas ainda é tudo sobre as pequenas coisas.

Não podemos nos dar ao luxo de ter um aplicativo lento. O desempenho é uma vantagem competitiva que pode trazer e reter clientes. Não podemos nos dar ao luxo de gastar tempo otimizando aplicativos regularmente novamente. É caro e complexo. E isso significa que, apesar de todos os benefícios do desempenho de uma perspectiva de negócios, dificilmente é lucrativo. Como primeiro passo para encontrar uma solução para qualquer problema, precisamos tornar o problema visível. Este artigo irá ajudá-lo exatamente com isso.

Observação : se você tem um conhecimento básico de Node.js, uma vaga ideia sobre como seu CI/CD funciona e se preocupa com o desempenho do aplicativo ou com as vantagens comerciais que ele pode trazer, então estamos prontos.

Como criar um orçamento de desempenho para um projeto

As primeiras perguntas que devemos nos fazer são:

“Qual é o projeto performático?”

“Quais métricas devo usar?”

“Quais valores dessas métricas são aceitáveis?”

A seleção de métricas está fora do escopo deste artigo e depende muito do contexto do projeto, mas recomendo que você comece lendo Métricas de desempenho centradas no usuário de Philip Walton.

Da minha perspectiva, é uma boa ideia usar o tamanho da biblioteca em kilobytes como uma métrica para o pacote npm. Por quê? Bem, é porque se outras pessoas estão incluindo seu código em seus projetos, elas talvez queiram minimizar o impacto do seu código no tamanho final do aplicativo.

Para o site, eu consideraria o Time To First Byte (TTFB) como uma métrica. Essa métrica mostra quanto tempo leva para o servidor responder com algo. Essa métrica é importante, mas bastante vaga porque pode incluir qualquer coisa – começando pelo tempo de renderização do servidor e terminando com problemas de latência. Portanto, é bom usá-lo em conjunto com o Server Timing ou OpenTracing para descobrir exatamente em que consiste.

Você também deve considerar métricas como Time to Interactive (TTI) e First Meaningful Paint (o último será substituído em breve pelo Largest Contentful Paint (LCP)). Eu acho que ambos são mais importantes – do ponto de vista do desempenho percebido.

Mas lembre-se: as métricas são sempre relacionadas ao contexto , portanto, não tome isso como garantido. Pense no que é importante no seu caso específico.

A maneira mais fácil de definir os valores desejados para as métricas é usar seus concorrentes – ou até você mesmo. Além disso, de tempos em tempos, ferramentas como a Calculadora de Orçamento de Desempenho podem ser úteis - apenas brinque um pouco com ela.

A degradação do desempenho é um problema que enfrentamos diariamente. Poderíamos nos esforçar para tornar o aplicativo muito rápido, mas logo acabamos onde começamos.

Use concorrentes para seu benefício

Se você já fugiu de um urso em êxtase superexcitado, então você já sabe que não precisa ser um campeão olímpico de corrida para sair desse problema. Você só precisa ser um pouco mais rápido que o outro cara.

Então faça uma lista de concorrentes. Se forem projetos do mesmo tipo, geralmente consistem em tipos de página semelhantes entre si. Por exemplo, para uma loja na Internet, pode ser uma página com uma lista de produtos, página de detalhes do produto, carrinho de compras, checkout e assim por diante.

  1. Meça os valores das suas métricas selecionadas em cada tipo de página para os projetos do seu concorrente;
  2. Meça as mesmas métricas em seu projeto;
  3. Encontre o mais próximo melhor do que seu valor para cada métrica nos projetos do concorrente. Adicionando 20% a eles e defina como seus próximos objetivos.

Por que 20%? Este é um número mágico que supostamente significa que a diferença será perceptível a olho nu. Você pode ler mais sobre esse número no artigo de Denys Mishunov “Por que o desempenho percebido é importante, parte 1: a percepção do tempo”.

Uma luta com uma sombra

Você tem um projeto único? Não tem concorrentes? Ou você já é melhor do que qualquer um deles em todos os sentidos possíveis? Não é um problema. Você sempre pode competir com o único oponente digno, ou seja, você mesmo. Meça cada métrica de desempenho do seu projeto em cada tipo de página e melhore-as nos mesmos 20%.

Mais depois do salto! Continue lendo abaixo ↓

Testes sintéticos

Existem duas maneiras de medir o desempenho:

  • Sintético (em ambiente controlado)
  • RUM (Medições do Usuário Real)
    Os dados estão sendo coletados de usuários reais em produção.

Neste artigo, usaremos testes sintéticos e presumiremos que nosso projeto usa o GitLab com seu CI integrado para implantação do projeto.

Biblioteca e seu tamanho como métrica

Vamos supor que você decidiu desenvolver uma biblioteca e publicá-la no NPM. Você quer mantê-lo leve – muito mais leve que os concorrentes – para que tenha menos impacto no tamanho final do projeto resultante. Isso economiza tráfego de clientes – às vezes, tráfego pelo qual o cliente está pagando. Também permite que o projeto seja carregado mais rapidamente, o que é muito importante no que diz respeito à crescente participação móvel e novos mercados com velocidades de conexão lentas e cobertura de internet fragmentada.

Pacote para medir o tamanho da biblioteca

Para manter o tamanho da biblioteca o menor possível, precisamos observar cuidadosamente como ela muda ao longo do tempo de desenvolvimento. Mas como você pode fazer isso? Bem, poderíamos usar o pacote Size Limit criado por Andrey Sitnik de Evil Martians.

Vamos instalá-lo.

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

Em seguida, adicione-o ao package.json .

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

O bloco "size-limit":[{},{},…] contém uma lista do tamanho dos arquivos que queremos verificar. No nosso caso, é apenas um único arquivo: index.js .

O size do script do NPM apenas executa o pacote size-limit , que lê o size-limit do bloco de configuração mencionado anteriormente e verifica o tamanho dos arquivos listados lá. Vamos executá-lo e ver o que acontece:

 npm run size 
O resultado da execução do comando mostra o tamanho de index.js
O resultado da execução do comando mostra o tamanho de index.js. (Visualização grande)

Podemos ver o tamanho do arquivo, mas esse tamanho não está realmente sob controle. Vamos corrigir isso adicionando limit ao package.json :

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

Agora, se executarmos o script, ele será validado em relação ao limite que definimos.

Uma captura de tela do terminal; o tamanho do arquivo é menor que o limite e é mostrado em verde
Uma captura de tela do terminal; o tamanho do arquivo é menor que o limite e é mostrado em verde. (Visualização grande)

Caso o novo desenvolvimento altere o tamanho do arquivo a ponto de exceder o limite definido, o script será concluído com código diferente de zero. Isso, além de outras coisas, significa que interromperá o pipeline no GitLab CI.

Uma captura de tela do terminal onde o tamanho do arquivo excede o limite e está sendo mostrado em vermelho. O script foi finalizado com um código diferente de zero.
Uma captura de tela do terminal onde o tamanho do arquivo excede o limite e está sendo mostrado em vermelho. O script foi finalizado com um código diferente de zero. (Visualização grande)

Agora podemos usar o git hook para verificar o tamanho do arquivo em relação ao limite antes de cada commit. Podemos até usar o pacote husky para fazê-lo de uma maneira agradável e simples.

Vamos instalá-lo.

 npm i -D husky

Em seguida, modifique nosso package.json .

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

E agora, antes de cada commit, o comando npm run size seria executado automaticamente e, se terminar com um código diferente de zero, o commit nunca aconteceria.

Uma captura de tela do terminal em que a confirmação foi abortada porque o tamanho do arquivo excede o limite
Uma captura de tela do terminal em que a confirmação foi abortada porque o tamanho do arquivo excede o limite. (Visualização grande)

Mas há muitas maneiras de pular ganchos (intencionalmente ou mesmo por acidente), então não devemos confiar muito neles.

Além disso, é importante observar que não deveríamos precisar fazer esse bloqueio de verificação. Por quê? Porque não há problema em que o tamanho da biblioteca cresça enquanto você adiciona novos recursos. Precisamos tornar as mudanças visíveis, só isso. Isso ajudará a evitar um aumento acidental de tamanho devido à introdução de uma biblioteca auxiliar que não precisamos. E, talvez, dar aos desenvolvedores e proprietários de produtos uma razão para considerar se o recurso que está sendo adicionado vale o aumento de tamanho. Ou, talvez, se existem pacotes alternativos menores. Bundlephobia nos permite encontrar uma alternativa para quase qualquer pacote NPM.

Então o que deveríamos fazer? Vamos mostrar a mudança no tamanho do arquivo diretamente na solicitação de mesclagem! Mas você não força para dominar diretamente; você age como um desenvolvedor adulto, certo?

Executando nossa verificação no GitLab CI

Vamos adicionar um artefato GitLab do tipo métrica. Um artefato é um arquivo, que ficará “vivo” após a conclusão da operação do pipeline. Esse tipo específico de artefato nos permite mostrar um widget adicional na solicitação de mesclagem, mostrando qualquer alteração no valor da métrica entre o artefato no mestre e a ramificação do recurso. O formato do artefato de metrics é um formato de texto do Prometheus. Para valores do GitLab dentro do artefato, é apenas texto. O GitLab não entende o que exatamente mudou no valor - ele apenas sabe que o valor é diferente. Então, o que exatamente devemos fazer?

  1. Defina artefatos no pipeline.
  2. Altere o script para que ele crie um artefato no pipeline.

Para criar um artefato, precisamos alterar .gitlab-ci.yml desta forma:

 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 — o artefato existirá por 7 dias.
  2.  paths: metric.txt

    Ele será salvo no catálogo raiz. Se você pular esta opção, não será possível baixá-la.
  3.  reports: metrics: metric.txt

    O artefato terá o tipo reports:metrics

Agora vamos fazer com que o Size Limit gere um relatório. Para fazer isso, precisamos alterar package.json :

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

size-limit com chave --json produzirá dados no formato json:

O comando size-limit --json produz JSON no console. JSON contém uma matriz de objetos que contém um nome e tamanho de arquivo, além de nos informar se excede o limite de tamanho
O comando size-limit --json JSON no console. O JSON contém uma matriz de objetos que contém um nome e tamanho de arquivo, além de nos informar se excede o limite de tamanho. (Visualização grande)

E o redirecionamento > size-limit.json salvará o JSON no arquivo size-limit.json .

Agora precisamos criar um artefato a partir disso. O formato se resume a [metrics name][space][metrics value] . Vamos criar o script generate-metric.js :

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

E adicione-o ao package.json :

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

Como usamos o prefixo post , o comando npm run size executará primeiro o script size e, em seguida, automaticamente, executará o script postsize , o que resultará na criação do arquivo metric.txt , nosso artefato.

Como resultado, quando mesclarmos esse branch para master, alterarmos algo e criarmos uma nova solicitação de mesclagem, veremos o seguinte:

Captura de tela com uma solicitação de mesclagem, que nos mostra um widget com valor de métrica novo e antigo entre colchetes
Captura de tela com uma solicitação de mesclagem, que nos mostra um widget com valor de métrica novo e antigo entre colchetes. (Visualização grande)

No widget que aparece na página, primeiro vemos o nome da métrica ( size ) seguido pelo valor da métrica na ramificação do recurso, bem como o valor no mestre dentro dos colchetes.

Agora podemos realmente ver como alterar o tamanho do pacote e tomar uma decisão razoável se devemos mesclá-lo ou não.

  • Você pode ver todo esse código neste repositório.

Retomar

OK! Então, descobrimos como lidar com o caso trivial. Se você tiver vários arquivos, basta separar as métricas com quebras de linha. Como alternativa ao Limite de tamanho, você pode considerar o tamanho do pacote. Se você estiver usando o WebPack, poderá obter todos os tamanhos necessários construindo com os sinalizadores --profile e --json :

 webpack --profile --json > stats.json

Se estiver usando next.js, você pode usar o plugin @next/bundle-analyzer. Você decide!

Usando o Farol

Lighthouse é o padrão de fato na análise de projetos. Vamos escrever um script que nos permita medir o desempenho, sempre, as melhores práticas e nos fornecer uma pontuação de SEO.

Script para medir todas as coisas

Para começar, precisamos instalar o pacote lighthouse que fará as medições. Também precisamos instalar o marionetista que usaremos como um navegador sem cabeça.

 npm i -D lighthouse puppeteer

Em seguida, vamos criar um script lighthouse.js e iniciar nosso navegador:

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

Agora vamos escrever uma função que nos ajudará a analisar uma determinada URL:

 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); // … }

Excelente! Agora temos uma função que aceitará o objeto do navegador como argumento e retornará uma função que aceitará URL como argumento e gerará um relatório após passar essa URL para o lighthouse .

Estamos passando os seguintes argumentos para o lighthouse :

  1. O endereço que queremos analisar;
  2. opções lighthouse , port do navegador em particular e output (formato de saída do relatório);
  3. configuração do report e lighthouse:full (tudo o que podemos medir). Para uma configuração mais precisa, consulte a documentação.

Maravilhoso! Agora temos nosso relatório. Mas o que podemos fazer com isso? Bem, podemos verificar as métricas em relação aos limites e sair do script com código diferente de zero que interromperá o pipeline:

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

Mas queremos apenas tornar o desempenho visível e sem bloqueios? Então vamos adotar outro tipo de artefato: artefato de desempenho do GitLab.

Artefato de desempenho do GitLab

Para entender este formato de artefatos, temos que ler o código do plugin sitespeed.io. (Por que o GitLab não pode descrever o formato de seus artefatos dentro de sua própria documentação? Mistério. )

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

Um artefato é um arquivo JSON que contém uma matriz dos objetos. Cada um deles representa um relatório sobre um URL .

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

Cada página é representada por um objeto com os seguintes atributos:

  1. subject
    Identificador de página (é bastante útil usar esse nome de caminho);
  2. metrics
    Uma matriz dos objetos (cada um deles representa uma medida que foi feita na página).
 { "subject":"/login/", "metrics":[{measurement 1}, {measurement 2}, {measurement 3}, …] }

Uma measurement é um objeto que contém os seguintes atributos:

  1. name
    Nome da medição, por exemplo, pode ser Time to first byte ou Time to interactive .
  2. value
    Resultado numérico da medição.
  3. desiredSize
    Se o valor de destino for o menor possível, por exemplo, para a métrica Time to interactive , o valor deverá ser smaller . Se for o maior possível, por exemplo, para a Performance score do farol , use larger .
 { "name":"Time to first byte (ms)", "value":240, "desiredSize":"smaller" }

Vamos modificar nossa função buildReport de forma que ela retorne um relatório para uma página com métricas padrão do farol.

Captura de tela com relatório do farol. Há pontuação de desempenho, pontuação a11y, pontuação de melhores práticas, pontuação de SEO
Captura de tela com relatório do farol. Existem pontuação de desempenho, pontuação a11y, pontuação de melhores práticas, pontuação de SEO. (Visualização 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, }; }

Agora, quando temos uma função que gera um relatório. Vamos aplicá-lo a cada tipo de página do projeto. Primeiro, preciso declarar que process.env.DOMAIN deve conter um domínio de teste (para o qual você precisa implantar seu projeto de uma ramificação de recurso antecipadamente).

 + 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(); })();
  • Você pode encontrar a fonte completa neste gist e um exemplo de trabalho neste repositório.

Nota : Neste ponto, você pode querer me interromper e gritar em vão: "Por que você está tomando meu tempo - você não pode nem usar Promise.all corretamente!" Em minha defesa, atrevo-me a dizer que não é recomendado executar mais de uma instância do farol ao mesmo tempo porque isso afeta negativamente a precisão dos resultados da medição. Além disso, se você não mostrar a devida engenhosidade, isso levará a uma exceção.

Uso de vários processos

Você ainda está em medições paralelas? Tudo bem, você pode querer usar cluster de nós (ou mesmo Worker Threads se você gosta de jogar em negrito), mas faz sentido discutir isso apenas no caso de seu pipeline ser executado no ambiente com vários cors disponíveis. E mesmo assim, você deve ter em mente que, devido à natureza do Node.js, você terá uma instância Node.js de peso total gerada em cada bifurcação do processo (em vez de reutilizar a mesma, o que levará ao aumento do consumo de RAM). Tudo isso significa que será mais caro por causa da crescente necessidade de hardware e um pouco mais rápido. Pode parecer que o jogo não vale a pena.

Se você quiser correr esse risco, precisará:

  1. Divida a matriz de URL em pedaços por número de núcleos;
  2. Crie uma bifurcação de um processo de acordo com o número de núcleos;
  3. Transfira partes da matriz para as bifurcações e, em seguida, recupere os relatórios gerados.

Para dividir uma matriz, você pode usar abordagens de várias pilhas. O código a seguir - escrito em apenas alguns minutos - não seria pior que os outros:

 /** * 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; }

Faça garfos de acordo com a contagem 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 } })();

Vamos transferir uma matriz de pedaços para processos filhos e recuperar relatórios:

 (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(); + }); } })();

E, finalmente, remonte os relatórios em um array e gere um artefato.

  • Confira o código completo e o repositório com um exemplo que mostra como usar o lighthouse com vários processos.

Precisão das Medições

Bem, paralelizamos as medidas, o que aumentou o já lamentável grande erro de medição do lighthouse . Mas como podemos reduzi-lo? Bem, faça algumas medições e calcule a média.

Para isso, escreveremos uma função que calculará a média entre os resultados da medição atual e os 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, } }), } }

Em seguida, altere nosso código para usá-los:

 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(); }); }
  • Confira a essência com o código completo e o repositório com um exemplo.

E agora podemos adicionar o lighthouse no pipeline.

Adicionando-o ao pipeline

Primeiro, crie um arquivo de configuração chamado .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

Os vários pacotes instalados são necessários para o puppeteer . Como alternativa, você pode considerar usar o docker . Além disso, faz sentido o fato de definirmos o tipo de artefato como desempenho. E, assim que o master e o feature branch o tiverem, você verá um widget como este na solicitação de mesclagem:

Uma captura de tela da página de solicitação de mesclagem. Há um widget que mostra quais métricas do farol foram alteradas e como exatamente
Uma captura de tela da página de solicitação de mesclagem. Há um widget que mostra quais métricas do farol foram alteradas e como exatamente. (Visualização grande)

Agradável?

Retomar

Finalmente terminamos com um caso mais complexo. Obviamente, existem várias ferramentas semelhantes além do farol. Por exemplo, sitespeed.io. A documentação do GitLab contém até um artigo que explica como usar sitespeed no pipeline do GitLab. Há também um plugin para GitLab que nos permite gerar um artefato. Mas quem preferiria produtos de código aberto voltados para a comunidade ao invés de um monstro corporativo?

Não há descanso para os ímpios

Pode parecer que finalmente chegamos lá, mas não, ainda não. Se você estiver usando uma versão paga do GitLab, os artefatos com metrics de tipos de relatório e performance estarão presentes nos planos a partir do premium e do silver , que custam US$ 19 por mês para cada usuário. Além disso, você não pode simplesmente comprar um recurso específico de que precisa – você só pode alterar o plano. Desculpe. Então o que podemos fazer? Ao contrário do GitHub com sua API de verificação e API de status, o GitLab não permitiria que você mesmo criasse um widget real na solicitação de mesclagem. E não há esperança de obtê-los tão cedo.

Uma captura de tela do tweet postado por Ilya Klimov (funcionário do GitLab) escreveu sobre a probabilidade de análogos de aparência para Github Checks and Status API: “Extremamente improvável. As verificações já estão disponíveis por meio da API de status de confirmação e, quanto aos status, estamos nos esforçando para ser um ecossistema fechado.”
Uma captura de tela do tweet postado por Ilya Klimov (funcionário do GitLab) que escreveu sobre a probabilidade de análogos de aparência para Github Checks e API de status. (Visualização grande)

Uma maneira de verificar se você realmente tem suporte para esses recursos: você pode pesquisar a variável de ambiente GITLAB_FEATURES no pipeline. Se não merge_request_performance_metrics metrics_reports na lista, esses recursos não serão suportados.

 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

Se não houver apoio, precisamos pensar em algo. Por exemplo, podemos adicionar um comentário ao pedido de mesclagem, comentar com a tabela, contendo todos os dados que precisamos. Podemos deixar nosso código intocado — artefatos serão criados, mas widgets sempre mostrarão uma mensagem «metrics are unchanged» .

Comportamento muito estranho e não óbvio; Eu tive que pensar com cuidado para entender o que estava acontecendo.

Então qual é o plano?

  1. Precisamos ler o artefato do branch master ;
  2. Crie um comentário no formato markdown ;
  3. Obtenha o identificador da solicitação de mesclagem da ramificação de recurso atual para o mestre;
  4. Adicione o comentário.

Como Ler Artefato do Ramo Mestre

Se quisermos mostrar como as métricas de desempenho são alteradas entre as ramificações master e feature, precisamos ler o artefato do master . E para isso, precisaremos 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; };

Criando um texto de comentário

Precisamos construir o texto do comentário no formato markdown . Vamos criar algumas funções de serviço que nos ajudarão:

 /** * 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} `; };

Script que vai construir um comentário

Você precisará ter um token para trabalhar com a API do GitLab. Para gerar um, você precisa abrir o GitLab, fazer login, abrir a opção 'Configurações' do menu e, em seguida, abrir 'Tokens de acesso' encontrados no lado esquerdo do menu de navegação. Você deve poder ver o formulário, que permite gerar o token.

Captura de tela, que mostra o formulário de geração de token e as opções de menu que mencionei acima.
Captura de tela, que mostra o formulário de geração de token e as opções de menu que mencionei acima. (Visualização grande)

Além disso, você precisará de um ID do projeto. Você pode encontrá-lo no repositório 'Configurações' (no submenu 'Geral'):

A captura de tela mostra a página de configurações, onde você pode encontrar o ID do projeto
A captura de tela mostra a página de configurações, onde você pode encontrar o ID do projeto. (Visualização grande)

Para adicionar um comentário à solicitação de mesclagem, precisamos saber seu ID. A função que permite adquirir o ID da solicitação de mesclagem se parece com isso:

 // 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. (Visualização grande)

Retomar

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.

Autenticação

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. Feito.

Resumo

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. Boa sorte com isso!