Inspecionar a memória com o novo pacote Memory Profiler

Neste blog, abordaremos cinco fluxos de trabalho principais no novo pacote Memory Profiler que você pode usar para diagnosticar e examinar problemas relacionados à memória em seu jogo. São elas:
- Monitoramento da pressão de memória do seu aplicativo
- Ver a distribuição de Unity Objects
- Detecção de ativos mal configurados
- Localização de objetos duplicados não intencionais
- Comparação de capturas de memória para validar otimizações
Para obter uma introdução ao Memory Profiler, consulte o blog recente, Tudo o que você precisa saber sobre o Memory Profiler 1.0.0.
Esse primeiro fluxo de trabalho monitora o grau de exigência do seu aplicativo em relação aos recursos de memória do dispositivo. Esse processo é essencial para determinar se o seu aplicativo corre o risco de ter problemas de desempenho ou até mesmo de ser expulso e encerrado pelo sistema operacional, devido ao consumo excessivo de memória.
Para começar, temos uma compilação de um jogo de exemplo em execução no dispositivo de destino. Naturalmente, é essencial que façamos uma captura de memória do jogo, executada no hardware real, para ver como ele usa os recursos de memória disponíveis nos dispositivos. Além disso, a memória não se comporta da mesma forma no Unity Editor e no tempo de execução do Unity, portanto, fazer uma captura de memória do Editor no modo Play não é uma boa representação de como a memória de um jogo será exibida em um dispositivo. (Fazer uma captura de memória do Editor é apropriado ao desenvolver ferramentas para o Editor, como janelas personalizadas do Editor).
Depois de navegar até o estágio do jogo em que queremos analisar o uso da memória, anexamos o Memory Profiler ao nosso dispositivo usando o menu suspenso no Memory Profiler. Em seguida, podemos fazer uma captura de memória, como mostrado abaixo.

Depois de abrir essa captura, o Memory Profiler exibe o espaço de memória do nosso aplicativo na parte superior da página Summary (Resumo) como "Memory Usage On Device" (Uso de memória no dispositivo).

Aqui podemos ver que o espaço de memória do nosso aplicativo é de 492,5 MB, de um total de 3,50 GB disponíveis. Em seguida, precisamos usar nosso melhor julgamento para saber se acreditamos que essa é uma proporção sensata da memória física (RAM) do dispositivo a ser usada no momento da captura. Lembre-se de que a memória física de um dispositivo é compartilhada por todos os processos em execução.
Você perceberá que esse indicador visual está mostrando a memória residente total. A memória residente total refere-se à quantidade de memória do seu aplicativo que reside no hardware de memória física (RAM) do dispositivo. Esse é o indicador mais claro de quão exigente é o uso atual da memória do seu aplicativo no dispositivo de destino por dois motivos. Em primeiro lugar, isso ocorre porque, à medida que o uso total da memória residente do seu aplicativo aumenta, também aumenta a probabilidade de ocorrer falhas de página frequentes, em que o sistema operacional precisa paginar a memória virtual para dentro e para fora da memória física do dispositivo. Falhas frequentes de página causarão uma degradação significativa do desempenho do seu aplicativo. Em segundo lugar, isso ocorre porque muitos sistemas operacionais usam o uso da memória residente do aplicativo para determinar o espaço de memória atual. Se o espaço de memória do seu aplicativo ficar muito grande, o sistema operacional o expulsará e o encerrará, causando uma falha para os jogadores.
Portanto, você pode usar o indicador visual Memory Usage On Device (Uso de memória no dispositivo ) no Memory Profiler para inferir se um aplicativo corre o risco de ter problemas de desempenho ou de ser encerrado pelo sistema operacional, devido ao uso excessivo de memória no momento da captura.
Isso contrasta com a memória alocada, às vezes chamada de memória comprometida, que você pode notar que é exibida em vários gráficos abaixo desse indicador e é atualmente a opção padrão mostrada por todas as outras exibições, como Unity Objects. A memória alocada se refere a toda a memória que o aplicativo tem alocada no momento, independentemente de ter sido tornada residente na memória física ou não, e, portanto, corresponde mais à visão de memória do aplicativo. Assim, isso pode ser útil para explorar toda a memória atualmente alocada do aplicativo, enquanto o uso da memória residente é essencial para entender a pressão da memória que o aplicativo está exercendo sobre o hardware a qualquer momento.
A guia Unity Objects do Memory Profiler oferece uma visão geral da memória do seu aplicativo a partir da perspectiva dos Unity Objects, ou seja, as texturas, os shaders, as malhas, os materiais e assim por diante do seu aplicativo. Esse é um ótimo lugar para começar a explorar o Memory Profiler porque o Unity Objects será inerentemente familiar para muitos usuários do Unity, pois é com ele que a maioria de nós trabalha diretamente no Unity Editor. Isso não apenas fornece um ponto de entrada familiar para entender a memória do nosso aplicativo, mas também pode ajudar a diagnosticar e corrigir uma série de possíveis problemas, fornecendo esse contexto específico da Unidade.

Para ver a exibição Unity Objects, basta selecionar a guia Unity Objects na parte superior do Memory Profiler depois de abrir uma captura de memória, conforme mostrado acima.
Você pode ver como a visualização Unity Objects nos dá rapidamente uma compreensão da distribuição dos tipos de Unity Object em nosso aplicativo. Isso nos permite obter uma compreensão de alto nível de quais tipos estavam consumindo mais memória no momento da captura, bem como raciocinar sobre isso, como, por exemplo, se é esperado que uma determinada cena seja pesada em objetos AudioClip. A expansão de cada tipo também nos permite visualizar cada Unity Object que está alocado no momento, individualmente, conforme mostrado abaixo.

É importante lembrar que os Unity Objects compõem uma proporção da memória total alocada do nosso aplicativo. Você pode ver exatamente quanto no indicador acima da tabela, destacado abaixo.

Aqui, você pode ver que o tamanho total da memória alocada, "Total Memory In Snapshot" (Memória total no instantâneo), é de 4,64 GB e que nossos Unity Objects representam 2,37 GB desse total. Além disso, se filtrarmos a tabela, por exemplo, usando o recurso de pesquisa, você notará que essa barra será atualizada para refletir os resultados da pesquisa. Em outras palavras, ele exibe o tamanho de toda a memória mostrada atualmente na tabela. Isso o ajuda a manter a perspectiva da quantidade exata de memória que está inspecionando como proporção de toda a captura e pode ajudar a informar onde investir esforços de otimização.

Na versão 1.0 do Memory Profiler, a tabela Unity Objects mostra a memória alocada ou, em outras palavras, mostra todos os Unity Objects que estão ativos em seu aplicativo. Estamos explorando a possibilidade de adicionar visibilidade de memória residente a essas exibições em uma próxima versão, o que permitiria ver exatamente quais dos seus Unity Objects estão atualmente residentes na memória física e, portanto, ver exatamente quais estão contribuindo diretamente para o espaço de memória atual do seu aplicativo.
Você pode usar a guia All Of Memory (Toda a memória) para inspecionar o restante da memória do seu aplicativo no momento da captura, que incluirá memória fora dos Unity Objects, como vários subsistemas do Unity, memória somente gerenciada (C#) e DLLs e executáveis.
A visualização do Unity Objects pode nos ajudar a diagnosticar uma série de possíveis problemas. Um desses problemas é a detecção de ativos que foram mal configurados, fazendo com que consumam mais memória do que o necessário.
Na captura abaixo, você pode ver que uma parte substancial de nossos Unity Objects são texturas. A captura é de um projeto com alta fidelidade gráfica que usa o Pipeline de Renderização de Alta Definição e faz uso intenso de efeitos visuais. Portanto, com esse contexto em mente, esperamos ver um uso intenso de texturas, o que é o caso.

No entanto, ao expandir nossa segunda grande categoria, Texture2D, podemos notar que duas texturas parecem muito maiores do que as outras. Com base em nosso entendimento do projeto, ficamos surpresos com o fato de essas texturas serem maiores do que texturas comparáveis, como HoloTable_Normal ou HoloTable_Mask, pois esperávamos que tivessem tamanho semelhante.

Portanto, selecionamos uma dessas texturas na tabela para saber mais detalhes sobre ela e começar a investigar o que pode ser a causa disso. Aqui, na exibição Details (Detalhes), encontramos nossa explicação: nossa textura é gravável, ou "Read/Write Enabled" (Habilitada para leitura/gravação).

Esse é um problema comum que vemos em muitos projetos de usuários: acidentalmente tornar uma textura gravável quando ela não é necessária, marcando a configuração "Leitura/Gravação" nas configurações de importação da textura. Quando uma textura tiver esse sinalizador ativado, ela dobrará seu tamanho na memória. Isso ocorre porque é necessária uma segunda cópia dos dados de textura para que eles possam ser acessados na CPU. Um sinal revelador disso é que o tamanho total de uma textura é o dobro do tamanho esperado ou o dobro do tamanho de texturas semelhantes.
Depois de desativar o sinalizador "Leitura/Gravação" em ambas as texturas e fazer uma segunda captura, podemos ver que o tamanho de ambas as texturas caiu pela metade.

Estamos explorando a adição de uma coluna para memória gráfica (GPU) à tabela Unity Objects em uma versão futura para facilitar a localização de casos em que um objeto Unity tenha alocado memória gráfica, como neste exemplo.
Um erro comum que vemos em projetos Unity é a criação não intencional de Unity Objects duplicados. Por exemplo, é muito fácil criar acidentalmente um Material duplicado acessando a propriedade de material de um MeshRenderer. Isso não só aumenta rapidamente nesse caso - se, por exemplo, for feito em cada instância de um MeshRenderer específico - como também, além disso, esses materiais criados dinamicamente precisam ser explicitamente destruídos.
Para ajudar a localizar esse tipo de problema, a tabela Unity Objects fornece um filtro rápido para mostrar apenas possíveis Unity Objects duplicados. Essa exibição filtrará a tabela para mostrar apenas os Unity Objects que têm várias instâncias com nome e tamanho idênticos. É importante observar que muitas duplicatas em potencial são esperadas e não são motivo de preocupação. Por exemplo, várias instâncias de um pré-fabricado podem ter componentes Transform com nomes e tamanhos idênticos, e essas seriam as duplicatas esperadas. Estamos interessados apenas em descobrir duplicatas não intencionais, o que ilustraremos neste exemplo de fluxo de trabalho a seguir.
A captura abaixo foi feita em uma cena simples com duas instâncias de um pré-fabricado de porta, e ativamos o filtro Show Potential Duplicates Only (Mostrar somente possíveis duplicatas ) localizado abaixo da tabela Unity Objects. Isso filtrou a tabela para nos mostrar apenas os Unity Objects que têm várias instâncias com o mesmo nome e tamanho.

Como temos duas instâncias de um pré-fabricado Door em nossa cena, também temos, como esperado, duas instâncias de todos os objetos relevantes: MeshRenderer, Transform, GameObject e assim por diante. No entanto, também temos duas instâncias do Material "Door" em nossa captura acima. Essas instâncias de porta têm a mesma aparência em nossa cena, portanto, espera-se que elas compartilhem um Material. Trata-se, portanto, de uma duplicata não intencional e, nesse exemplo específico, foi causada pelo acesso à propriedade de material do MeshRenderer no pré-fabricado. Remover esse acesso à propriedade e fazer uma segunda captura mostra que o material duplicado não está mais presente na tabela Unity Objects.

É importante lembrar que esse filtro está simplesmente mostrando todos os Unity Objects que têm várias instâncias com o mesmo nome e tamanho. É necessário que você tenha conhecimento do seu projeto para interpretar se as possíveis duplicatas que você vê são esperadas ou se, na verdade, não são intencionais e são motivo de investigação. Recomendamos prestar atenção à barra Total Memory In Table (Memória total na tabela ) na parte superior, que fornece uma indicação visual da proporção da memória alocada do aplicativo que está sendo exibida na tabela. Isso pode ajudá-lo a manter a perspectiva de onde investir seus esforços de otimização.
O Memory Profiler também oferece a funcionalidade de comparar duas capturas de memória. Isso nos permite fazer alterações em nosso projeto, por exemplo, para resolver um problema que possamos ter encontrado e, posteriormente, testar se nossas alterações realmente tiveram o resultado desejado. É importante sempre testar se sua hipótese está correta e se suas alterações tiveram o resultado desejado no hardware real. Vamos explorar um exemplo desse fluxo de trabalho de comparação.
Veja abaixo uma captura do nosso jogo para celular feita durante o primeiro nível. Podemos ver que a maior categoria de Unity Objects é Texture2D. Depois de abrir essa categoria para verificar quais são nossas maiores texturas, podemos ver que há algumas texturas de interface do usuário que são bem grandes em relação ao restante do jogo - megabytes cada. Isso levanta uma suspeita para nós: Por que essas texturas são muito maiores do que as outras e elas precisam ser assim? Para descobrir o motivo, podemos primeiro localizar o ativo de textura de origem em nosso projeto selecionando a textura no Memory Profiler e usando o botão "Select In Editor", que destacará o ativo de textura de origem em nossa janela Project.

Usando a janela Inspector, podemos ver que todas as nossas texturas grandes da interface do usuário não estão sendo compactadas porque suas dimensões não são uma potência de dois, como mostra o texto "NPOT" (non-power-of-two).

Isso explica esses tamanhos grandes de textura. Agora, podemos usar o conhecimento que temos do nosso projeto para reduzir o uso da memória. Sabemos que três dessas texturas (os controles de ajuda) são sempre exibidas juntas na interface do usuário, assim como as outras três texturas (as criaturas). Portanto, podemos supor com alta confiança que a criação de dois Sprite Atlases para cada conjunto de três texturas reduzirá o uso da memória alocada, pois permitirá que eles sejam compactados sem aumentar o número de texturas na memória.

Para comparar dois instantâneos, comece abrindo o primeiro instantâneo. Essa é a "base" com a qual queremos nos comparar. Agora, acima do instantâneo aberto, selecione a guia "Compare Snapshots" (Comparar instantâneos) e escolha o segundo instantâneo. O Memory Profiler agora apresentará um resumo comparando os dois instantâneos, conforme mostrado abaixo.

Para ver o efeito da nossa alteração e verificar se ela, de fato, reduziu o tamanho da memória alocada do nosso aplicativo para a categoria Texture2D, podemos selecionar a guia Unity Objects. Aqui, é apresentada uma tabela de comparação que mostra os tipos de Unity Object que foram alterados e como eles foram alterados entre as capturas (mostradas abaixo).

Podemos ver que nosso tipo Texture2D como um todo reduziu o tamanho em 3,6 MB e tem quatro texturas a menos do que antes. Expandindo essa categoria, podemos ver a remoção de nossas texturas de sprite individuais e não compactadas e a adição de nossas duas texturas de sprite Atlas, resultando em uma redução líquida de 3,6 MB e 4 objetos Texture2D.

Portanto, isso foi um sucesso - confirmamos que nossa hipótese estava correta usando a funcionalidade de comparação e reduzimos o tamanho dessas texturas na memória alocada.
Ao ler este blog, você deve ter uma melhor compreensão dos cinco principais fluxos de trabalho do novo pacote Memory Profiler. Esses fluxos de trabalho foram projetados para diagnosticar e examinar problemas relacionados à memória em seu jogo. Esperamos que o pacote Memory Profiler lançado no Unity 2022.2 ajude você a monitorar, examinar e entender melhor o consumo de memória do seu jogo. Fique à vontade para entrar em contato com a equipe e compartilhar seus comentários sobre como podemos melhorar as ferramentas de criação de perfil de desempenho por meio da nossa página do fórum - ou compartilhar suas sugestões por meio da nossa página de roteiro, onde também é possível ver alguns dos recursos que estão sendo desenvolvidos.
Se estiver interessado em obter mais detalhes sobre esse tópico, publicaremos outro blog nas próximas semanas que se aprofundará em como o espaço de memória de um aplicativo é calculado, abordando tópicos como memória residente e alocada em mais detalhes.
