Entendendo a memória no Unity WebGL

Alguns usuários já estão familiarizados com plataformas em que a memória é limitada. Para outras pessoas, que usam o desktop ou o WebPlayer, isso nunca foi um problema até agora.
A segmentação de plataformas de console é relativamente fácil nesse aspecto, pois você sabe exatamente quanta memória está disponível. Isso permite que você faça um orçamento de memória e garante a execução do seu conteúdo. Nas plataformas móveis, as coisas são um pouco mais complicadas devido à grande variedade de dispositivos existentes, mas pelo menos você pode escolher as especificações mais baixas e decidir colocar na lista negra os dispositivos de baixo custo no nível do marketplace.
Na Web, você simplesmente não pode. O ideal seria que todos os usuários finais tivessem navegadores de 64 bits e toneladas de memória, mas isso está longe de ser realidade. Além disso, não há como saber as especificações do hardware em que seu conteúdo está sendo executado. Você conhece o sistema operacional, o navegador e não muito mais. Por fim, o usuário final pode estar executando o seu conteúdo WebGL, bem como outras páginas da Web. É por isso que esse é um problema difícil.
Aqui está uma visão geral da memória ao executar o conteúdo do Unity WebGL no navegador:

Essa imagem mostra que, além do Heap do Unity, o conteúdo do Unity WebGL exigirá alocações adicionais na memória do navegador. É muito importante entender isso, para que você possa otimizar seu projeto e, portanto, minimizar a taxa de desistência dos usuários.
Como você pode ver na imagem, há vários grupos de alocações: DOM, Unity Heap, Asset Data e Code, que serão persistentes na memória quando a página da Web for carregada. Outros, como Asset Bundles, WebAudio e Memory FS, variam de acordo com o que está acontecendo no seu conteúdo (por exemplo, download de asset bundles, reprodução de áudio etc.).
No momento do carregamento, há também várias alocações temporárias do navegador durante a análise e a compilação do asm.js que, às vezes, causam problemas de falta de memória para alguns usuários em navegadores de 32 bits.
Em geral, o Unity Heap é a memória que contém todos os objetos de jogo, componentes, texturas, shaders etc. específicos do Unity.
No WebGL, o tamanho do heap do Unity precisa ser conhecido com antecedência para que o navegador possa alocar espaço para ele e, uma vez alocado, o buffer não pode diminuir ou aumentar.
O código responsável pela alocação do Heap do Unity é o seguinte:
buffer = new ArrayBuffer(TOTAL_MEMORY);
Esse código pode ser encontrado no build.js gerado e será executado pela VM JS do navegador.
TOTAL_MEMORY é definido pelo tamanho da memória WebGL nas configurações do jogador. O valor padrão é 256 MB, mas esse é apenas um valor arbitrário que escolhemos. De fato, um projeto vazio funciona com apenas 16 MB.
No entanto, o conteúdo do mundo real provavelmente precisará de mais, algo como 256 ou 386 MB na maioria dos casos. Lembre-se de que quanto mais memória for necessária, menos usuários finais poderão executá-lo.
Antes que o código possa ser executado, ele precisa ser executado:
baixado.
copiado em uma bolha de texto.
compilado.
Leve em consideração que cada uma dessas etapas exigirá um pedaço de memória:
- O buffer de download é temporário, mas o código-fonte e os códigos compilados são persistentes na memória.
- O tamanho do buffer baixado e o código-fonte são ambos do tamanho dos js não compactados gerados pelo Unity. Para estimar a quantidade de memória necessária para eles:
- fazer uma compilação de lançamento
- renomeie jsgz e datagz para *.gz e descompacte-os com uma ferramenta de compactação
- seu tamanho descompactado também será o tamanho na memória do navegador.
- O tamanho do código compilado depende do navegador.
Uma otimização fácil é ativar o Strip Engine Code para que sua compilação não inclua o código nativo do mecanismo que você não precisa (por exemplo: O módulo de física 2d será removido se você não precisar dele). Observação: Observação: O código gerenciado é sempre removido.
Lembre-se de que o suporte a exceções e os plug-ins de terceiros contribuirão para o tamanho do seu código. Dito isso, vimos usuários que precisam enviar seus títulos com verificações de nulidade e verificações de limites de matriz, mas não querem incorrer na sobrecarga de memória (e desempenho) do suporte total a exceções. Para fazer isso, você pode passar --emit-null-checks e --enable-array-bounds-check para o il2cpp, por exemplo, por meio de um script de editor:
PlayerSettings.SetPropertyString("additionalIl2CppArgs", "--emit-null-checks --enable-array-bounds-check");
Por fim, lembre-se de que as compilações de desenvolvimento produzirão um código maior porque ele não é reduzido, embora isso não seja uma preocupação, já que você só enviará compilações de lançamento para o usuário final... certo? ;-)
Em outras plataformas, um aplicativo pode simplesmente acessar arquivos no armazenamento permanente (disco rígido, memória flash, etc.). Na Web, isso não é possível, pois não há acesso a um sistema de arquivos real. Portanto, quando os dados do Unity WebGL (arquivo .data) são baixados, eles são armazenados na memória. A desvantagem é que isso exigirá mais memória em comparação com outras plataformas (a partir da versão 5.3, o arquivo .data é armazenado na memória com compactação lz4). Por exemplo, aqui está o que o criador de perfil me diz sobre um projeto que gera um arquivo de dados de aproximadamente 40 MB (com 256 MB de Unity Heap):

O que há no arquivo .data? É uma coleção de arquivos que a unidade gera: data.unity3d (todas as cenas, seus recursos dependentes e tudo na pasta Resources), unity_default_resources e alguns arquivos menores necessários para o mecanismo.
Para saber o tamanho total exato dos assets, dê uma olhada em data.unity3d em Temp\StagingArea\Data depois de criar para WebGL (lembre-se de que a pasta Temp será excluída quando o Unity Editor for fechado). Como alternativa, você pode examinar os offsets passados para o DataRequest no UnityLoader.js:
new DataRequest(0, 39065934, 0, 0).open('GET', '/data.unity3d');
(esse código pode mudar dependendo da versão do Unity - este é o da versão 5.4)
Embora não haja um sistema de arquivos real, como mencionamos anteriormente, o conteúdo do Unity WebGL ainda pode ler/gravar arquivos. A principal diferença em relação a outras plataformas é que qualquer operação de E/S de arquivo será, na verdade, de leitura/gravação na memória. O que é importante saber é que esse sistema de arquivos de memória não reside no Heap do Unity, portanto, exigirá memória adicional. Por exemplo, digamos que eu escreva uma matriz em um arquivo:
var buffer = novo byte [10*1014*1024];
File.WriteAllBytes(Application.temporaryCachePath + "/buffer.bytes", buffer);
O arquivo será gravado na memória, o que também pode ser visto no profiler do navegador:

Observe que o tamanho do Heap do Unity é de 256 MB.
Da mesma forma, como o sistema de cache do Unity depende do sistema de arquivos, todo o armazenamento do cache é armazenado na memória. O que isso significa? Isso significa que coisas como PlayerPrefs e Asset Bundles em cache também serão persistentes na memória, fora do Heap do Unity.
Uma das práticas recomendadas mais importantes para reduzir o consumo de memória no Webgl é usar Asset Bundles (se você não estiver familiarizado com eles, consulte o manual ou este tutorial para começar). No entanto, dependendo de como eles são usados, pode haver um impacto significativo no consumo de memória (dentro e fora do Heap do Unity), o que pode fazer com que seu conteúdo não funcione em navegadores de 32 bits.
Agora que você sabe que realmente precisa usar pacotes de ativos, o que fazer? Despejar todos os seus ativos em um único pacote de ativos?
NÃO! Mesmo que isso reduza a pressão no momento do carregamento da página da Web, você ainda precisará fazer o download de um pacote de ativos (potencialmente muito grande), causando um pico de memória. Vamos dar uma olhada na memória antes de o AB ser baixado:

Como você pode ver, 256 MB estão alocados para o Heap do Unity. E isso ocorre após o download de um pacote de ativos sem armazenamento em cache:

O que você vê agora é um buffer adicional, aproximadamente do mesmo tamanho do pacote no disco (~65 MB), que foi alocado pelo XHR. Esse é apenas um buffer temporário, mas causará um pico de memória por vários quadros até que seja coletado pelo lixo.
O que fazer então para minimizar os picos de memória? Criar um pacote de ativos para cada ativo? Embora seja uma ideia interessante, ela não é muito prática.
O ponto principal é que não existe uma regra geral e você realmente precisa fazer o que faz mais sentido para o seu projeto.
Por fim, lembre-se de descarregar o pacote de ativos por meio de AssetBundle.Unload quando terminar de usá-lo.
O armazenamento em cache do Asset Bundle funciona como em outras plataformas, você só precisa usar WWW.LoadFromCacheOrDownload. No entanto, há uma diferença bastante significativa, que é o consumo de memória. No Unity WebGL, o cache AB depende do IndexedDB para armazenar dados persistentemente, o problema é que as entradas no banco de dados também existem no sistema de arquivos da memória.
Vejamos uma captura de memória antes de fazer o download de um pacote de ativos usando LoadFromCacheOrDownload:

Como você pode ver, 512 MB são usados para o Heap do Unity e cerca de 4 MB para outras alocações. Isso ocorre depois de carregar o pacote:

A memória adicional necessária saltou para ~167 MB. Essa é a memória adicional necessária para esse pacote de ativos (~64 MB de pacote compactado). E isso ocorre após a coleta de lixo do js vm:

É um pouco melhor, mas ainda são necessários cerca de 85 MB: a maior parte é usada para armazenar em cache o pacote de ativos no sistema de arquivos da memória. Essa é uma memória que você não terá de volta, nem mesmo depois de descarregar o pacote. Também é importante lembrar que, quando o usuário abre seu conteúdo no navegador pela segunda vez, essa parte da memória é alocada imediatamente, mesmo antes de carregar o pacote.
Para referência, este é um instantâneo de memória do Chrome:

Da mesma forma, há outra alocação temporária relacionada ao cache fora do Heap do Unity, que é necessária para o nosso sistema de pacotes de ativos. A má notícia é que descobrimos recentemente que ele é muito maior do que o pretendido. A boa notícia, porém, é que isso foi corrigido no próximo Unity 5.5 Beta 4, 5.3.6 Patch 6 e 5.4.1 Patch 2.
Para versões mais antigas do Unity, caso seu conteúdo Unity WebGL já esteja ativo ou próximo do lançamento e você não queira atualizar seu projeto, uma solução rápida é definir a seguinte propriedade por meio do script do editor:
PlayerSettings.SetPropertyString("emscriptenArgs", " -s MEMFS_APPEND_TO_TYPED_ARRAYS=1", BuildTargetGroup.WebGL);
Uma solução de longo prazo para minimizar a sobrecarga de memória de cache do pacote de ativos é usar o WWW Constructor em vez de LoadFromCacheOrDownload() ou usar UnityWebRequest.GetAssetBundle() sem parâmetro de hash/versão se estiver usando a nova API UnityWebRequest.
Em seguida, use um mecanismo de cache alternativo no nível XMLHttpRequest, que armazena o arquivo baixado diretamente no indexedDB, ignorando o sistema de arquivos da memória. Foi exatamente isso que desenvolvemos recentemente e está disponível na loja de ativos. Sinta-se à vontade para usá-lo em seus projetos e personalizá-lo, se necessário.
Nas versões 5.3 e 5.4, há suporte para as compressões LZMA e LZ4. No entanto, embora o uso do LZMA (padrão) resulte em um tamanho de download menor em comparação com o LZ4/Uncompressed, ele tem algumas desvantagens no WebGL: causa paradas de execução perceptíveis e requer mais memória. Portanto, é altamente recomendável usar o LZ4 ou nenhuma compactação (na verdade, a compactação de pacotes de recursos LZMA não estará disponível para WebGL a partir do Unity 5.5) e, para compensar o tamanho maior do download em comparação com o lzma, você pode usar o gzip/brotli nos pacotes de recursos e configurar o servidor de acordo.
Consulte o manual para obter mais informações sobre a compactação de pacotes de ativos.
O áudio no Unity WebGL é implementado de forma diferente. O que isso significa para a memória?
O Unity criará objetos AudioBufferespecíficos em JavaScript, para que possam ser reproduzidos via WebAudio.
Como os buffers do WebAudio vivem fora do Heap do Unity e, portanto, não podem ser rastreados pelo profiler do Unity, você precisa inspecionar a memória com ferramentas específicas do navegador para ver quanta memória é usada para áudio. Aqui está um exemplo (usando a página about:memory do Firefox):

Leve em consideração que esses buffers de áudio contêm dados não compactados, o que pode não ser ideal para ativos de clipes de áudio grandes (por exemplo, música de fundo). Para esses casos, talvez você queira considerar a possibilidade de escrever seu próprio plug-in js para poder usar as tags <audio>. Dessa forma, os arquivos de áudio permanecem compactados e, portanto, usam menos memória.
Aqui está um resumo:
Reduzir o tamanho do Heap do Unity:
Mantenha o "Tamanho da memória WebGL" o menor possível
Reduzir o tamanho do código:
Ativar código de mecanismo de remoção Desativar exceções Tente evitar o uso de plug-ins de terceiros
Reduzir o tamanho de seus dados:
Usar pacotes de ativos Use a compressão de textura Crunch
Sim, a melhor estratégia seria usar o criador de perfil de memória e analisar a quantidade de memória que seu conteúdo realmente requer e, em seguida, alterar o tamanho da memória do WebGL de acordo.
Vamos usar um projeto vazio como exemplo. O Memory Profiler me diz que o "Total Used" é de pouco mais de 16 MB (esse valor pode variar entre as versões do Unity): isso significa que devo definir o WebGL Memory Size para algo maior do que isso. Obviamente, o "Total Used" (Total usado) será diferente de acordo com seu conteúdo.
No entanto, se por algum motivo não for possível usar o Profiler, você pode simplesmente continuar reduzindo o valor do tamanho da memória WebGL até encontrar a quantidade mínima de memória necessária para executar seu conteúdo.
Também é importante observar que qualquer valor que não seja um múltiplo de 16 será automaticamente arredondado (em tempo de execução) para o próximo múltiplo, pois esse é um requisito do Emscripten.
A configuração WebGL Memory Size (mb) determinará o valor de TOTAL_MEMORY (bytes) no html gerado:

Portanto, para iterar o tamanho do heap sem recompilar o projeto, é recomendável modificar o html. Depois de encontrar um valor que lhe agrade, você poderá alterar o tamanho da memória WebGL no projeto Unity.
Felizmente, essa não é a única maneira e a próxima postagem do blog sobre a pilha do Unity tentará fornecer uma resposta melhor a essa pergunta.
Por fim, lembre-se de que o criador de perfil do Unity usará alguma memória do Heap alocado, portanto, talvez você precise aumentar o tamanho da memória do WebGL ao criar o perfil.
Depende do fato de o Unity estar ficando sem memória ou do navegador. A mensagem de erro indicará qual é o problema e como resolvê-lo: "Se você for o desenvolvedor desse conteúdo, tente alocar mais/menos memória para sua compilação WebGL nas configurações do player WebGL." Em seguida, você pode ajustar a configuração do tamanho da memória WebGL de acordo. No entanto, você pode fazer mais para resolver a OOM. Se você receber essa mensagem de erro:

Além do que a mensagem diz, você também pode tentar reduzir o tamanho do código e/ou dos dados. Isso se deve ao fato de que, quando o navegador carrega a página da Web, ele tenta encontrar memória livre para várias coisas, principalmente: código, dados, heap da unidade e asm.js compilado. Elas podem ser bem grandes, especialmente a memória heap do Data e do Unity, o que pode ser um problema para navegadores de 32 bits.
Em alguns casos, mesmo que haja memória livre suficiente, o navegador ainda falhará porque a memória está fragmentada. É por isso que, às vezes, seu conteúdo pode não ser carregado depois que você reiniciar o navegador.
O outro cenário, quando o Unity fica sem memória, exibirá uma mensagem como:

Nesse caso, você precisa otimizar seu projeto Unity.
Para analisar a memória do navegador usada pelo seu conteúdo, você pode usar o Firefox Memory Tool ou o Chrome Heap snapshot. No entanto, saiba que eles não mostrarão a memória do WebAudio. Para isso, você pode usar a página about:memory no Firefox: tire um instantâneo e, em seguida, pesquise por "webaudio". Se você precisar criar um perfil de memória via JavaScript, tente window.performance.memory (somente no Chrome).
Para medir o uso da memória dentro do Heap do Unity, use o Unity Profiler. No entanto, esteja ciente de que talvez seja necessário aumentar o tamanho da memória do WebGL para poder usar o criador de perfil.
Além disso, há uma nova ferramenta na qual estamos trabalhando que permite que você analise o que está em sua compilação: Para usá-lo, crie uma compilação WebGL e acesse http://files.unity3d.com/build-report/. Embora isso esteja disponível a partir do Unity 5.4, observe que essa funcionalidade é um trabalho em andamento e está sujeita a alterações ou remoção a qualquer momento. Mas, por enquanto, estamos disponibilizando-o para fins de teste.
16 é o mínimo. O máximo é 2032, mas geralmente recomendamos ficar abaixo de 512.
Essa é uma limitação técnica: 2048 MB (ou mais) ultrapassará o tamanho do inteiro assinado de 32 bits do TypeArray usado para implementar o heap do Unity em JavaScript.
Pensamos em usar o sinalizador ALLOW_MEMORY_GROWTH do emscripten para permitir que o Heap seja redimensionado, mas até agora decidimos não fazê-lo porque isso desativaria algumas otimizações no Chrome. Ainda temos que fazer um benchmarking real sobre esse impacto. Esperamos que o uso desse recurso possa, de fato, piorar os problemas de memória. Se você chegou a um ponto em que o Heap do Unity é muito pequeno para caber toda a memória necessária e precisa crescer, o navegador terá que alocar um heap maior, copiar tudo do heap antigo e, em seguida, desalocar o heap antigo. Ao fazer isso, ele precisa de memória para o novo e o antigo heap ao mesmo tempo (até terminar a cópia), exigindo, portanto, mais memória total. Portanto, o uso da memória seria maior do que quando se usa um tamanho de memória fixo predeterminado.
Os navegadores de 32 bits terão as mesmas limitações de memória, independentemente de o sistema operacional ser de 64 ou 32 bits.
A recomendação final é traçar o perfil do seu conteúdo Unity WebGL usando ferramentas específicas do navegador também, pois, como descrevemos, há alocações fora do Unity Heap que o profiler do Unity não pode rastrear.
Esperamos que algumas dessas informações sejam úteis para você. Se tiver mais perguntas, não hesite em fazê-las aqui ou no fórum WebGL.
Atualizar:
Falamos sobre a memória usada para o código e mencionamos que o código JS de origem é copiado em um blob de texto temporário. O que descobrimos é que o blob não foi desalocado adequadamente, de modo que era uma alocação permanente na memória do navegador. Em about:memory, ele é rotulado como memory-file-data:

Seu tamanho depende do tamanho do código e, em projetos complexos, pode facilmente chegar a 32 ou 64 MB. Felizmente, isso foi corrigido nas versões 5.3.6 Patch 8, 5.4.2 Patch 1 e 5.5.
Em termos de áudio, sabemos que o consumo de memória ainda é um problema: No momento, não há suporte para streaming de áudio e os ativos de áudio são mantidos na memória do navegador como não compactados. Por isso, sugerimos o uso da tag <audio> para reproduzir arquivos de áudio grandes. Com esse objetivo, publicamos recentemente um novo pacote do Asset Store para ajudá-lo a minimizar o consumo de memória por fontes de áudio de streaming. Dê uma olhada!