A história de um problema com o velocímetro ou como o Chromium gerencia a memória

Um navegador moderno é um projeto extremamente complexo no qual mesmo alterações inofensivas podem levar a surpresas inesperadas. Portanto, existem muitos testes internos que devem capturar essas alterações antes do lançamento. Como nunca há muitos testes, é útil usar também benchmarks públicos de terceiros.

Meu nome é Andrey Logvinov, trabalho no grupo de desenvolvimento de mecanismos de renderização Yandex.Browser em Nizhny Novgorod. Hoje vou contar aos leitores da Habr sobre como o gerenciamento de memória no projeto Chromium funciona com o exemplo de um problema misterioso que levou à degradação do desempenho no teste do velocímetro . Esta postagem é baseada no meu relatório do evento Yandex.Inside.




Uma vez em nosso painel de desempenho, vimos uma deterioração na velocidade do teste do velocímetro. Esse teste mede o desempenho geral do navegador em um aplicativo que é próximo da realidade - uma lista de tarefas, em que o teste adiciona itens à lista e depois os cruza. Os resultados do teste são afetados pelo desempenho do mecanismo V8 JS e pela velocidade de renderização de páginas no mecanismo Blink. O teste do velocímetro consiste em vários subtestes, nos quais o aplicativo de teste é gravado usando uma das estruturas JS populares, por exemplo, jQuery ou ReactJS. O resultado geral do teste é definido como a média dos resultados para todas as estruturas, mas o teste permite que você veja o desempenho de cada estrutura individualmente. Vale ressaltar que o teste não tem como objetivo avaliar o desempenho das estruturas, elas são usadas apenas para tornar o teste menos sintético e mais próximo de aplicativos da Web reais. O detalhamento por subteste mostrou que a deterioração é observada apenas para a versão do aplicativo de teste criada usando o jQuery. E isso já é interessante, concordo.

A investigação de tais situações começa bastante padronizada - determinamos qual comprometimento específico com o código levou ao problema. Para fazer isso, armazenamos os assemblies Yandex.Browser para cada Confirmação (!) Nos últimos anos (seria impraticável remontar, pois a montagem leva várias horas). Isso ocupa muito espaço nos servidores, mas geralmente ajuda a encontrar rapidamente a fonte do problema. Mas desta vez rapidamente não funcionou. Aconteceu que a deterioração dos resultados do teste coincidiu com um commit integrando a próxima versão do Chromium. O resultado não é animador, porque a nova versão do Chromium traz um grande número de alterações ao mesmo tempo.

Como não recebemos nenhuma informação indicando uma alteração específica, tive que fazer um estudo substantivo do problema. Para fazer isso, usamos as Ferramentas do desenvolvedor para remover os rastreamentos de teste. Percebemos um recurso estranho - intervalos "rasgados" para a execução de funções de teste de Javascript.

imagem

Removemos um rastreio mais técnico com about: rastreio e vemos que é coleta de lixo (GC) no Blink.

imagem

A faixa de memória abaixo mostra que essas pausas no GC não só levam muito tempo, mas também não ajudam a interromper o crescimento do consumo de memória.

imagem

Mas se você inserir uma chamada explícita do GC no teste, veremos uma imagem completamente diferente - a memória é mantida na região zero e não vaza. Portanto, não temos vazamentos de memória e o problema está relacionado aos recursos do coletor. Continuamos a cavar. Iniciamos o depurador e vemos que o coletor de lixo ultrapassou cerca de 500 mil objetos! Esse número de objetos não pode afetar o desempenho. Mas de onde eles vieram?

E aqui precisamos de um pequeno flashback sobre o dispositivo coletor de lixo no Blink. Ele remove objetos mortos, mas não move objetos ativos, o que permite operar com ponteiros nus em variáveis ​​locais no código C ++. Esse padrão é usado ativamente no Blink. Mas também tem seu preço - ao coletar lixo, você precisa varrer a pilha de fluxo e, se algo semelhante a um ponteiro para um objeto de um heap (heap) for encontrado lá, considere o objeto e tudo que ele refere direta ou indiretamente estar vivo. Isso leva ao fato de que alguns objetos praticamente inacessíveis e, portanto, "mortos" são identificados como vivos. Portanto, essa forma de coleta de lixo também é chamada de conservadora.

Verificamos a conexão com a varredura de pilha e pulamos. O problema desapareceu.

O que pode ser isso em uma pilha que contém 500 mil objetos? Colocamos um ponto de interrupção na função de adicionar objetos - entre outras coisas, vemos que há suspeitas:

blink :: TraceTrait <blink :: HeapHashTableBacking <WTF :: HashTable <blink :: WeakMember ...

Uma referência de tabela de hash é um provável suspeito! Testamos a hipótese ignorando a adição deste link. O problema desapareceu. Bem, estamos um passo mais perto da resposta.

Recordamos outro recurso do coletor de lixo no Blink: se ele vir um ponteiro para o interior da tabela de hash, considerará isso um sinal de iteração contínua sobre a tabela, o que significa que considera todos os links dessa tabela úteis e continua ignorando-os. No nosso caso, ocioso. Mas qual função é a fonte desse link?

Avançamos alguns quadros da pilha mais alto, assumimos a posição atual do scanner, observamos o quadro da pilha em que função ele se encaixa. Essa é uma função chamada ScheduleGCIfNeeded . Parece que aqui ele é o culpado, mas ... olhamos para o código fonte da função e vemos que não há tabelas de hash lá. Além disso, isso já faz parte do próprio coletor de lixo e simplesmente não precisa se referir a objetos do heap do Blink. De onde veio esse link "ruim"?

Estabelecemos um ponto de interrupção na alteração da célula de memória, na qual encontramos um link para a tabela de hash. Vimos que uma das funções internas chamadas V8PerIsolateData :: AddActiveScriptWrappable grava lá. Lá, eles adicionam alguns elementos HTML criados de alguns tipos, incluindo entrada, a uma única tabela de hash active_script_wrappables_. Esta tabela é necessária para impedir a remoção de elementos que não são mais referenciados do Javascript ou da árvore DOM, mas que estão associados a qualquer atividade externa que, por exemplo, possa gerar eventos.

O coletor de lixo durante o percurso normal da tabela leva em consideração o estado dos elementos contidos nele e os marca como ativos ou não, e então eles são excluídos no próximo estágio de montagem. No entanto, no nosso caso, um ponteiro para o armazenamento interno dessa tabela é exibido quando a pilha é varrida e todos os elementos da tabela são marcados como ativos.

Mas como o valor da pilha de uma função atingiu a pilha de outra ?!

Pense em ScheduleGCIfNeeded. Lembre-se de que nada útil foi encontrado no código-fonte dessa função, mas isso significa apenas que é hora de descer para um nível mais baixo e verificar o compilador . O prólogo desmontado da função ScheduleGCIfNeeded tem esta aparência:

0FCDD13A push ebp 0FCDD13B mov ebp,esp 0FCDD13D push edi 0FCDD13E push esi 0FCDD13F and esp,0FFFFFFF8h 0FCDD142 sub esp,0B8h 0FCDD148 mov eax,dword ptr [__security_cookie (13DD3888h)] 0FCDD14D mov esi,ecx 0FCDD14F xor eax,ebp 0FCDD151 mov dword ptr [esp+0B4h],eax 

Pode-se observar que a função move esp para 0B8h , e esse local não é mais usado. Mas, por causa disso, o scanner de pilha vê o que foi gravado anteriormente por outras funções. E, por acaso, um ponteiro para o interior da tabela de hash deixado pela função AddActiveScriptWrappable entra nesse "buraco". Como se viu, o motivo da aparência do “buraco” nesse caso foi a macro de depuração do VLOG dentro da função, que exibe informações adicionais no log.

Mas por que a tabela active_script_wrappable_ tinha centenas de milhares de elementos? Por que a degradação do desempenho é observada apenas no teste jQuery? A resposta para as duas perguntas é a mesma - nesse teste específico, para cada alteração (como uma marca de seleção na caixa de seleção), toda a interface do usuário é completamente recriada. O teste produz elementos que quase imediatamente se transformam em lixo. Os demais testes no Speedometer são mais razoáveis ​​e não criam elementos desnecessários; portanto, para eles, a degradação do desempenho não é observada. Se você estiver desenvolvendo serviços da Web, leve isso em consideração para não criar trabalho desnecessário para o navegador.

Mas por que o problema surgiu apenas agora se a macro VLOG era anterior? Não há resposta exata, mas provavelmente, durante a atualização, a posição relativa dos elementos na pilha foi alterada, pelo que o ponteiro para a tabela de hash se tornou acidentalmente acessível ao scanner. De fato, ganhamos na loteria. Para fechar rapidamente o "buraco" e restaurar o desempenho, removemos a macro de depuração do VLOG. Para os usuários, é inútil e, para nossas próprias necessidades de diagnóstico, sempre podemos ativá-lo novamente. Também compartilhamos nossas experiências com outros desenvolvedores do Chromium. A resposta confirmou nossos medos: este é um problema fundamental da coleta conservadora de lixo em Blink, que não possui uma solução sistêmica.

Links interessantes


1. Se você está interessado em aprender sobre outra vida cotidiana incomum do nosso grupo, lembramos a história do retângulo preto , que levou à aceleração não apenas do Yandex.Browser, mas de todo o projeto Chromium.

2. Convido você também a ouvir outros relatórios no próximo evento Yandex.Inside , em 16 de fevereiro, as inscrições estão abertas e a transmissão também será.

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


All Articles