Monitorar o uso total da memória da sua página da Web com medidaUserAgentSpecificMemory()

Saiba como medir o uso de memória da sua página da Web em produção para detectar regressões.

Ulan Degenbaev
Ulan Degenbaev

Os navegadores gerenciam a memória das páginas da Web automaticamente. Sempre que uma página da Web cria um objeto, o navegador aloca um pedaço de memória "por trás dos bastidores" para armazenar o objeto. Como a memória é um recurso finito, o navegador executa a coleta de lixo para detectar quando um objeto não é mais necessário e liberar o bloco de memória subjacente.

No entanto, a detecção não é perfeita, e foi comprovado que a detecção perfeita é uma tarefa impossível. Portanto, os navegadores aproximam a noção de "um objeto é necessário" com a noção de "um objeto é acessível". Se a página da Web não conseguir acessar um objeto pelas variáveis e os campos de outros objetos acessíveis, o navegador poderá recuperar o objeto com segurança. A diferença entre essas duas noções leva a vazamentos de memória, como ilustrado no exemplo a seguir.

const object = {a: new Array(1000), b: new Array(2000)};
setInterval(() => console.log(object.a), 1000);

Aqui, a matriz maior b não é mais necessária, mas o navegador não a recupera porque ela ainda pode ser acessada por object.b no callback. Assim, a memória da matriz maior é vazada.

Vazamentos de memória são prevalentes na Web. É fácil introduzir um deles esquecendo de cancelar o registro de um listener de eventos, capturando objetos acidentalmente de um iframe, não fechando um worker, acumulando objetos em matrizes e assim por diante. Se uma página da Web tiver vazamentos de memória, o uso de memória dela vai aumentar com o tempo, e a página da Web vai parecer lenta e inflada para os usuários.

A primeira etapa para resolver esse problema é medi-lo. A nova API performance.measureUserAgentSpecificMemory() permite que os desenvolvedores meçam o uso de memória das páginas da Web em produção e, assim, detectem vazamentos de memória que passam despercebidos no teste local.

Qual é a diferença entre performance.measureUserAgentSpecificMemory() e a API performance.memory legada?

Se você já conhece a API performance.memory não padrão, talvez esteja se perguntando como a nova API é diferente dela. A principal diferença é que a API antiga retorna o tamanho do heap do JavaScript, enquanto a nova API estima a memória usada pela página da Web. Essa diferença se torna importante quando o Chrome compartilha o mesmo heap com várias páginas da Web (ou várias instâncias da mesma página da Web). Nesses casos, o resultado da API antiga pode ser arbitrariamente incorreto. Como a API antiga é definida em termos específicos da implementação, como "heap", não é possível padronizá-la.

Outra diferença é que a nova API realiza a medição de memória durante a coleta de lixo. Isso reduz o ruído nos resultados, mas pode levar um tempo até que eles sejam produzidos. Outros navegadores podem decidir implementar a nova API sem depender da coleta de lixo.

Casos de uso sugeridos

O uso de memória de uma página da Web depende do tempo de eventos, ações do usuário e coletas de lixo. É por isso que a API de medição de memória é destinada a agregar dados de uso de memória da produção. Os resultados de chamadas individuais são menos úteis. Exemplos de casos de uso:

  • Detecção de regressão durante o lançamento de uma nova versão da página da Web para detectar novos vazamentos de memória.
  • Teste A/B de um novo recurso para avaliar o impacto na memória e detectar vazamentos de memória.
  • Correlacionar o uso de memória com a duração da sessão para verificar a presença ou ausência de vazamentos de memória.
  • Correlacionar o uso da memória com as métricas do usuário para entender o impacto geral do uso da memória.

Compatibilidade com navegadores

Compatibilidade com navegadores

  • Chrome: 89.
  • Edge: 89.
  • Firefox: não é compatível.
  • Safari: não é compatível.

Origem

No momento, a API só é compatível com navegadores baseados no Chromium, a partir do Chrome 89. O resultado da API é altamente dependente da implementação porque os navegadores têm maneiras diferentes de representar objetos na memória e maneiras diferentes de estimar o uso da memória. Os navegadores podem excluir algumas regiões de memória da conta se a contabilidade adequada for muito cara ou inviável. Portanto, os resultados não podem ser comparados entre navegadores. Só é significativo comparar os resultados do mesmo navegador.

Como usar o performance.measureUserAgentSpecificMemory()

Detecção de recursos

A função performance.measureUserAgentSpecificMemory vai ficar indisponível ou pode falhar com um SecurityError se o ambiente de execução não atender aos requisitos de segurança para evitar vazamentos de informações entre origens. Ele depende do isolamento entre origens, que uma página da Web pode ativar definindo os cabeçalhos COOP+COEP.

O suporte pode ser detectado no momento da execução:

if (!window.crossOriginIsolated) {
  console.log('performance.measureUserAgentSpecificMemory() is only available in cross-origin-isolated pages');
} else if (!performance.measureUserAgentSpecificMemory) {
  console.log('performance.measureUserAgentSpecificMemory() is not available in this browser');
} else {
  let result;
  try {
    result = await performance.measureUserAgentSpecificMemory();
  } catch (error) {
    if (error instanceof DOMException && error.name === 'SecurityError') {
      console.log('The context is not secure.');
    } else {
      throw error;
    }
  }
  console.log(result);
}

Teste local

O Chrome realiza a medição de memória durante a coleta de lixo, o que significa que a API não resolve a promessa de resultado imediatamente e, em vez disso, aguarda a próxima coleta de lixo.

Chamar a API força uma coleta de lixo após um tempo limite, que está definido como 20 segundos, mas pode acontecer antes. Iniciar o Chrome com a flag de linha de comando --enable-blink-features='ForceEagerMeasureMemory' reduz o tempo limite para zero e é útil para depuração e teste locais.

Exemplo

O uso recomendado da API é definir um monitor de memória global que amostra o uso de memória de toda a página da Web e envia os resultados a um servidor para agregação e análise. A maneira mais simples é fazer a amostragem periodicamente, por exemplo, a cada M minutos. No entanto, isso introduz viés nos dados porque picos de memória podem ocorrer entre as amostras.

O exemplo a seguir mostra como fazer medições de memória imparciais usando um processo de Poisson, que garante que as amostras tenham a mesma probabilidade de ocorrer em qualquer momento (demonstração, fonte).

Primeiro, defina uma função que programe a próxima medição de memória usando setTimeout() com um intervalo aleatório.

function scheduleMeasurement() {
  // Check measurement API is available.
  if (!window.crossOriginIsolated) {
    console.log('performance.measureUserAgentSpecificMemory() is only available in cross-origin-isolated pages');
    console.log('See https://web.dev/coop-coep/ to learn more')
    return;
  }
  if (!performance.measureUserAgentSpecificMemory) {
    console.log('performance.measureUserAgentSpecificMemory() is not available in this browser');
    return;
  }
  const interval = measurementInterval();
  console.log(`Running next memory measurement in ${Math.round(interval / 1000)} seconds`);
  setTimeout(performMeasurement, interval);
}

A função measurementInterval() calcula um intervalo aleatório em milissegundos de modo que, em média, há uma medição a cada cinco minutos. Consulte Distribuição exponencial se você tiver interesse na matemática por trás da função.

function measurementInterval() {
  const MEAN_INTERVAL_IN_MS = 5 * 60 * 1000;
  return -Math.log(Math.random()) * MEAN_INTERVAL_IN_MS;
}

Por fim, a função performMeasurement() assíncrona invoca a API, registra o resultado e programa a próxima medição.

async function performMeasurement() {
  // 1. Invoke performance.measureUserAgentSpecificMemory().
  let result;
  try {
    result = await performance.measureUserAgentSpecificMemory();
  } catch (error) {
    if (error instanceof DOMException && error.name === 'SecurityError') {
      console.log('The context is not secure.');
      return;
    }
    // Rethrow other errors.
    throw error;
  }
  // 2. Record the result.
  console.log('Memory usage:', result);
  // 3. Schedule the next measurement.
  scheduleMeasurement();
}

Por fim, comece a medir.

// Start measurements.
scheduleMeasurement();

O resultado pode ser semelhante a este:

// Console output:
{
  bytes: 60_100_000,
  breakdown: [
    {
      bytes: 40_000_000,
      attribution: [{
        url: 'https://example.com/',
        scope: 'Window',
      }],
      types: ['JavaScript']
    },

    {
      bytes: 20_000_000,
      attribution: [{
          url: 'https://example.com/iframe',
          container: {
            id: 'iframe-id-attribute',
            src: '/iframe',
          },
          scope: 'Window',
      }],
      types: ['JavaScript']
    },

    {
      bytes: 100_000,
      attribution: [],
      types: ['DOM']
    },
  ],
}

A estimativa de uso total de memória é retornada no campo bytes. Esse valor é altamente dependente da implementação e não pode ser comparado entre navegadores. Ele pode até mudar entre diferentes versões do mesmo navegador. O valor inclui a memória do JavaScript e do DOM de todos os iframes, janelas relacionadas e workers da Web no processo atual.

A lista breakdown fornece mais informações sobre a memória usada. Cada entrada descreve uma parte da memória e a atribui a um conjunto de janelas, iframes e workers identificados por URL. O campo types lista os tipos de memória específicos da implementação associados à memória.

É importante tratar todas as listas de maneira genérica e não codificar suposições com base em um navegador específico. Por exemplo, alguns navegadores podem retornar um breakdown vazio ou um attribution vazio. Outros navegadores podem retornar várias entradas em attribution, indicando que não conseguiram distinguir qual delas é proprietária da memória.

Feedback

O Web Performance Community Group e a equipe do Chrome gostariam de saber sua opinião e experiência com o performance.measureUserAgentSpecificMemory().

Conte sobre o design da API

Há algo na API que não funciona como esperado? Ou há propriedades ausentes que você precisa implementar para implementar sua ideia? Registre um problema de especificação no repositório do GitHub performance.measureUserAgentSpecificMemory() ou adicione sua opinião a um problema existente.

Informar um problema com a implementação

Você encontrou um bug na implementação do Chrome? Ou a implementação é diferente da especificação? Informe um bug em new.crbug.com. Inclua o máximo de detalhes possível, forneça instruções simples para reproduzir o bug e defina Components como Blink>PerformanceAPIs. O Glitch é ótimo para compartilhar reprosagens rápidas e fáceis.

Mostrar apoio

Você planeja usar performance.measureUserAgentSpecificMemory()? Seu apoio público ajuda a equipe do Chrome a priorizar recursos e mostra a outros fornecedores de navegadores como é importante oferecer suporte a eles. Envie um tweet para @ChromiumDev e nos informe onde e como você está usando.

Links úteis

Agradecimentos

Agradecemos a Domenic Denicola, Yoav Weiss e Mathias Bynens pelas revisões de design de API e a Dominik Inführ, Hannes Payer, Kentaro Hara e Michael Lippautz pelas revisões de código no Chrome. Também agradeço a Per Parker, Philipp Weis, Olga Belomestnykh, Matthew Bolohan e Neil Mckay por fornecer feedback valioso de usuários que melhorou bastante a API.

Imagem principal de Harrison Broadbent no Unsplash