Engine & platform

Понимание памяти в Unity WebGL

MARCO TRIVELLATO / UNITY TECHNOLOGIESContributor
Sep 20, 2016|16 Мин
Понимание памяти в Unity WebGL
Эта веб-страница была переведена с помощью машинного перевода для вашего удобства. Мы не можем гарантировать точность или надежность переведенного контента. Если у вас есть вопросы о точности переведенного контента, обращайтесь к официальной английской версии веб-страницы.
С тех пор как мы выпустили Unity WebGL, мы приложили много усилий для оптимизации потребления памяти. Мы также объяснили, как работает память в WebGL, в руководстве и в наших докладах на Unite Europe 2015 и Unite Boston 2015. Однако, поскольку эта тема продолжает оставаться актуальной в наших разговорах с клиентами, мы поняли, что должны говорить об этом больше. Надеюсь, эта заметка ответит на некоторые из часто задаваемых вопросов.
Чем Unity WebGL отличается от других платформ?

Некоторые пользователи уже знакомы с платформами, где память ограничена. Для других пользователей, работающих с настольного компьютера или WebPlayer, это никогда не было проблемой до сих пор.

Нацеливаться на консольные платформы в этом плане довольно просто, поскольку вы точно знаете, сколько памяти доступно. Благодаря этому вы можете выделить бюджет на память, и ваш контент будет гарантированно работать. С мобильными платформами все немного сложнее из-за большого количества различных устройств, но, по крайней мере, вы можете выбрать самые низкие характеристики и принять решение о внесении в черный список устройств низшего класса на уровне рынка.

В Интернете это просто невозможно. В идеале все конечные пользователи должны иметь 64-битные браузеры и кучу памяти, но это далеко от реальности. Кроме того, нет возможности узнать технические характеристики оборудования, на котором работает ваш контент. Вы знаете ОС, браузер и не более того. Наконец, конечный пользователь может запускать как ваш WebGL-контент, так и другие веб-страницы. Вот почему это сложная проблема.

Обзор

Здесь представлен обзор памяти при запуске содержимого Unity WebGL в браузере:

изображение04

Это изображение показывает, что поверх Unity Heap содержимое Unity WebGL потребует дополнительных выделений в памяти браузера. Это очень важно понимать, чтобы оптимизировать свой проект и тем самым минимизировать отсев пользователей.

Как видно из изображения, существует несколько групп распределений: DOM, Unity Heap, данные активов и код, которые будут храниться в памяти после загрузки веб-страницы. Другие, такие как Asset Bundles, WebAudio и Memory FS, будут меняться в зависимости от того, что происходит в вашем контенте (например, загрузка пакета активов, воспроизведение аудио и т. д.).

Во время загрузки также происходит несколько временных выделений браузера при разборе и компиляции asm.js, которые иногда вызывают проблемы с нехваткой памяти у некоторых пользователей 32-битных браузеров.

Куча единства

В целом, Unity Heap - это память, содержащая все специфические для Unity игровые объекты, компоненты, текстуры, шейдеры и т.д.

В WebGL размер кучи Unity должен быть известен заранее, чтобы браузер мог выделить для нее место, и после выделения буфер не может уменьшаться или увеличиваться.

Код, отвечающий за выделение Unity Heap, выглядит следующим образом:

buffer = new ArrayBuffer(TOTAL_MEMORY);

Этот код можно найти в сгенерированном файле build.js, и он будет выполнен JS VM браузера.

TOTAL_MEMORY определяется размером памяти WebGL в настройках игрока. По умолчанию используется значение 256 Мб, но мы выбрали его произвольно. Фактически пустой проект работает всего с 16 Мб.

Однако для реального контента, скорее всего, потребуется больше, что-то вроде 256 или 386 Мб в большинстве случаев. Помните, что чем больше памяти требуется, тем меньше конечных пользователей смогут его запустить.

Память исходного/компилированного кода

Прежде чем код будет выполнен, он должен быть создан:

загружено.

копируется в текстовый блок.

скомпилированный.

Учтите, что каждый из этих шагов потребует определенного объема памяти:

  • Буфер загрузки является временным, но исходный текст и скомпилированный код хранятся в памяти постоянно.
  • Размер загружаемого буфера и исходного кода равен размеру несжатого js, сгенерированного Unity. Чтобы оценить, сколько памяти для них потребуется:
  • создайте сборку релиза
  • переименуйте jsgz и datagz в *.gz и распакуйте их с помощью инструмента сжатия
  • их размер без сжатия будет соответствовать размеру в памяти браузера.
  • Размер скомпилированного кода зависит от браузера.

Простая оптимизация - включить опцию Strip Engine Code, чтобы ваша сборка не включала нативный код движка, который вам не нужен (например: Модуль 2D-физики будет удален, если он вам не нужен). Примечание: Примечание: Управляемый код всегда зачеркивается.

Не забывайте, что поддержка исключений и сторонних плагинов увеличит размер вашего кода. Тем не менее, мы встречали пользователей, которым необходимо поставлять свои заголовки с проверкой нуля и границ массива, но они не хотят тратить память (и производительность) на полную поддержку исключений. Для этого вы можете передать в il2cpp команды --emit-null-checks и --enable-array-bounds-check, например, через редакторский скрипт:

PlayerSettings.SetPropertyString("additionalIl2CppArgs", "--emit-null-checks --enable-array-bounds-check");

Наконец, помните, что сборки для разработки будут содержать более крупный код, поскольку он не минифицирован, хотя это не так важно, поскольку вы собираетесь отправлять релизные сборки только конечному пользователю... верно? ;-)

Данные об активах

На других платформах приложение может просто обращаться к файлам на постоянном хранилище (жестком диске, флэш-памяти и т. д.). В Интернете это невозможно, поскольку нет доступа к реальной файловой системе. Поэтому после загрузки данных Unity WebGL (файл .data) они сохраняются в памяти. Недостатком является то, что это потребует дополнительного объема памяти по сравнению с другими платформами (начиная с версии 5.3, файл .data хранится в памяти в lz4-сжатом виде). Например, вот что говорит мне профилировщик о проекте, который генерирует файл данных размером ~40 мб (с 256 мб Unity Heap):

Unity Profiler

Что находится в файле .data? Это набор файлов, которые генерирует unity: data.unity3d (все сцены, их зависимые активы и все, что находится в папке Resources), unity_default_resources и несколько более мелких файлов, необходимых движку.

Чтобы узнать точный общий размер активов, посмотрите data.unity3d в папке Temp\StagingArea\Data после сборки для WebGL (помните, что папка Temp будет удалена после закрытия редактора Unity). В качестве альтернативы можно посмотреть на смещения, передаваемые в DataRequest в UnityLoader.js:

new DataRequest(0, 39065934, 0, 0).open('GET', '/data.unity3d');

(этот код может меняться в зависимости от версии Unity - это код из версии 5.4)

Файловая система памяти

Несмотря на отсутствие реальной файловой системы, как мы уже говорили, содержимое Unity WebGL может читать и записывать файлы. Основное отличие от других платформ заключается в том, что любая операция файлового ввода/вывода фактически читается/пишется в памяти. Важно знать, что эта файловая система памяти не живет в Unity Heap, поэтому она требует дополнительной памяти. Например, допустим, я записываю массив в файл:

var buffer = новый байт [10*1014*1024];

File.WriteAllBytes(Application.temporaryCachePath + "/buffer.bytes", buffer);

Файл будет записан в память, что также можно увидеть в профилировщике браузера:

Профилировщик браузеров

Обратите внимание, что размер Unity Heap составляет 256 Мб.

Аналогично, поскольку система кэширования Unity зависит от файловой системы, все хранилище кэша находится в памяти. Что это значит? Это означает, что такие вещи, как PlayerPrefs и кэшированные Asset Bundles, также будут храниться в памяти, за пределами Unity Heap.

Пакеты активов

Одной из наиболее важных лучших практик для снижения потребления памяти в webgl является использование Asset Bundles (если вы не знакомы с ними, вы можете обратиться к руководству или этому руководству, чтобы начать). Однако в зависимости от того, как они используются, это может существенно повлиять на потребление памяти (как внутри кучи Unity, так и за ее пределами), что может привести к тому, что ваш контент не будет работать в 32-битных браузерах.

Теперь, когда вы знаете, что вам действительно нужно использовать пакеты активов, что делать? Собрать все свои активы в единый пакет?

НЕТ! Даже если это уменьшит нагрузку на время загрузки веб-страницы, вам все равно придется загрузить (потенциально очень большой) пакет активов, что приведет к скачку памяти. Давайте посмотрим на память до загрузки AB:

Давайте посмотрим на память до загрузки пакета активов

Как видите, под Unity Heap выделено 256 Мб. И это после загрузки пакета активов без кэширования:

После загрузки пакета активов без кэширования

Теперь вы видите дополнительный буфер, примерно такого же размера, как и пакет на диске (~65мб), который был выделен XHR. Это всего лишь временный буфер, но он будет вызывать всплеск памяти в течение нескольких кадров, пока не будет собран мусор.

Что делать, чтобы минимизировать скачки памяти? Создать один пучок активов для каждого актива? Хотя это интересная идея, она не очень практична.

В итоге можно сказать, что общего правила не существует, и вам нужно делать то, что имеет смысл для вашего проекта.

Наконец, не забудьте выгрузить пакет активов с помощью AssetBundle.Unload, когда закончите работу с ним.

Кэширование пакетов активов

Кэширование Asset Bundle работает так же, как и на других платформах, вам просто нужно использовать WWW.LoadFromCacheOrDownload. Однако есть одно довольно существенное отличие - это потребление памяти. В Unity WebGL AB-кэширование полагается на IndexedDB для постоянного хранения данных, но проблема в том, что записи в DB также существуют в файловой системе памяти.

Давайте посмотрим на захват памяти перед загрузкой пакета активов с помощью LoadFromCacheOrDownload:

захват памяти перед загрузкой пакета активов с помощью LoadFromCacheOrDownload

Как видите, 512 Мб используется для Unity Heap и ~4 Мб для других распределений. Это происходит после загрузки пакета:

Это происходит после загрузки пакета

Дополнительный объем требуемой памяти подскочил до ~167 Мб. Это дополнительная память, необходимая нам для этого пакета активов (~64mb сжатого пакета). И это после сборки мусора js vm:

И это после сборки мусора js vm

Это немного лучше, но все равно требуется ~85 МБ: большая часть используется для кэширования пакета активов в памяти файловой системы. Эту память вы не вернете, даже после разгрузки пакета. Также важно помнить, что когда пользователь открывает ваш контент в браузере во второй раз, этот участок памяти выделяется сразу же, еще до загрузки пакета.

Для справки, это снимок памяти из Chrome:

снимок памяти из Chrome

Аналогично, есть еще одно временное распределение, связанное с кэшированием, за пределами Unity Heap, которое необходимо нашей системе пакетов активов. Плохая новость заключается в том, что недавно мы обнаружили, что он гораздо больше, чем предполагалось. Хорошая новость заключается в том, что это исправлено в грядущих обновлениях Unity 5.5 Beta 4, 5.3.6 Patch 6 и 5.4.1 Patch 2.

Для старых версий Unity, если ваш контент Unity WebGL уже выпущен или близок к выпуску и вы не хотите обновлять свой проект, можно установить следующее свойство через скрипт редактора:

PlayerSettings.SetPropertyString("emscriptenArgs", " -s MEMFS_APPEND_TO_TYPED_ARRAYS=1", BuildTargetGroup.WebGL);

Более долгосрочным решением для минимизации затрат памяти на кэширование пакетов активов является использование WWW Constructor вместо LoadFromCacheOrDownload() или использование UnityWebRequest.GetAssetBundle() без параметра хэша/версии, если вы используете новый API UnityWebRequest.

Затем используйте альтернативный механизм кэширования на уровне XMLHttpRequest, который сохраняет загруженный файл непосредственно в indexedDB, минуя файловую систему памяти. Это именно то, что мы недавно разработали, и оно доступно в магазине активов. Не стесняйтесь использовать его в своих проектах и настраивать, если это необходимо.

Сжатие пакетов активов

В версиях 5.3 и 5.4 поддерживаются сжатия LZMA и LZ4. Однако, несмотря на то, что использование LZMA (по умолчанию) приводит к меньшему размеру загрузки по сравнению с LZ4/Uncompressed, у него есть пара недостатков для WebGL: он вызывает заметные задержки в выполнении и требует больше памяти. Поэтому мы настоятельно рекомендуем использовать LZ4 или вообще не использовать сжатие (на самом деле, сжатие пакетов активов LZMA будет недоступно для WebGL начиная с Unity 5.5), а чтобы компенсировать больший размер загрузки по сравнению с lzma, вы можете использовать gzip/brotli для пакетов активов и соответствующим образом настроить свой сервер.

Дополнительные сведения о сжатии пакетов активов см. в руководстве.

WebAudio

Аудио в Unity WebGL реализовано по-другому. Что это значит для памяти?

Unity создаст определенные объекты AudioBuffer's на земле JavaScript, чтобы их можно было воспроизвести через WebAudio.

Поскольку буферы WebAudio находятся за пределами Unity Heap и поэтому не могут быть отслежены профилировщиком Unity, вам нужно проверить память с помощью специфических для браузера инструментов, чтобы узнать, сколько памяти используется для аудио. Вот пример (с использованием страницы Firefox about:memory ):

Вот пример (с использованием страницы Firefox about:memory)

Примите во внимание, что эти аудиобуферы содержат несжатые данные, что может быть не идеальным для больших аудиоклипов (например, фоновой музыки). Для них вы можете рассмотреть возможность написания собственного js-плагина, чтобы использовать вместо него теги <audio>. Таким образом, аудиофайлы остаются сжатыми и занимают меньше памяти.

ЧАСТО ЗАДАВАЕМЫЕ ВОПРОСЫ
Каковы лучшие методы сокращения использования памяти?

Вот краткое содержание:

Уменьшите размер кучи Unity Heap:

Держите "Размер памяти WebGL" как можно меньше.

Уменьшите размер кода:

Включить полосатый код двигателя Отключить исключения Старайтесь избегать использования сторонних плагинов

Уменьшите размер данных:

Используйте пакеты активов Используйте сжатие текстуры Crunch

Существует ли стратегия для определения того, насколько малым может быть размер памяти WebGL?

Да, лучшей стратегией будет использование профилировщика памяти и анализ того, сколько памяти на самом деле требует ваш контент, а затем соответствующее изменение размера памяти WebGL.

В качестве примера возьмем пустой проект. Memory Profiler сообщает мне, что "Total Used" составляет чуть более 16 МБ (это значение может отличаться в разных версиях Unity): это означает, что я должен установить WebGL Memory Size на что-то большее, чем это. Очевидно, что "Всего использовано" будет отличаться в зависимости от вашего контента.

Однако если по каким-то причинам вы не можете использовать Profiler, вы можете просто продолжать уменьшать значение WebGL Memory Size, пока не найдете минимальный объем памяти, необходимый для работы вашего контента.

Также важно отметить, что любое значение, не кратное 16, будет автоматически округляться (во время выполнения) до следующего кратного, так как это требование Emscripten.

Настройка WebGL Memory Size (mb) определяет значение TOTAL_MEMORY (bytes) в сгенерированном html:

Настройка WebGL Memory Size (mb) будет определять значение TOTAL_MEMORY (bytes) в сгенерированном html

Поэтому, чтобы итеративно определять размер кучи без пересборки проекта, рекомендуется модифицировать html. Затем, когда вы нашли значение, которое вас устраивает, вы можете изменить размер памяти WebGL в проекте Unity.

К счастью, это не единственный способ, и в следующей статье блога о куче Unity мы постараемся дать лучший ответ на этот вопрос.

Наконец, помните, что профилировщик Unity будет использовать некоторое количество памяти из выделенной кучи, поэтому при профилировании вам может понадобиться увеличить размер памяти WebGL.

В моей сборке не хватает памяти, как это исправить?

Это зависит от того, кто именно не хватает памяти - Unity или браузер. В сообщении об ошибке будет указано, в чем заключается проблема и как ее решить: "Если вы являетесь разработчиком этого контента, попробуйте выделить больше/меньше памяти для вашей сборки WebGL в настройках проигрывателя WebGL". Затем вы можете соответствующим образом настроить параметр WebGL Memory Size. Однако вы можете сделать больше, чтобы решить проблему OOM. Если вы получите это сообщение об ошибке:

В сообщении об ошибке будет указано, в чем заключается проблема и как ее решить

В дополнение к тому, что говорится в сообщении, вы можете попробовать уменьшить размер кода и/или данных. Это связано с тем, что когда браузер загружает веб-страницу, он пытается найти свободную память для нескольких вещей, наиболее важных: кода, данных, кучи единства и скомпилированного файла asm.js. Они могут быть довольно большими, особенно куча памяти Data и Unity, что может быть проблемой для 32-битных браузеров.

В некоторых случаях, даже при наличии достаточного количества свободной памяти, браузер все равно выйдет из строя из-за фрагментации памяти. Поэтому иногда после перезапуска браузера содержимое может не загрузиться.

В другом сценарии, когда у Unity закончится память, появится сообщение типа:

В другом сценарии, когда у Unity закончится память, появится сообщение следующего содержания

В этом случае вам необходимо оптимизировать проект Unity.

Как измерить потребление памяти?

Чтобы проанализировать память браузера, используемую вашим контентом, вы можете использовать Firefox Memory Tool или Chrome Heap snapshot. Правда, имейте в виду, что они не покажут вам память WebAudio, для этого вы можете использовать страницу about:memory в Firefox: сделайте снимок, а затем найдите "webaudio". Если вам нужно профилировать память с помощью JavaScript, попробуйте window.performance.memory (только для Chrome).

Чтобы измерить использование памяти в Unity Heap, используйте Unity Profiler. Однако имейте в виду, что вам может потребоваться увеличить размер памяти WebGL, чтобы иметь возможность использовать профилировщик.

Кроме того, мы работаем над новым инструментом, который позволяет анализировать содержимое вашей сборки: Чтобы воспользоваться им, создайте сборку WebGL, а затем посетите сайт http://files.unity3d.com/build-report/. Несмотря на то, что эта функция доступна в версии Unity 5.4, обратите внимание, что она находится в процессе разработки и может быть изменена или удалена в любой момент. Но пока мы предоставляем его для тестирования.

Каково минимальное/максимальное значение WebGL Memory Size?

16 - это минимум. Максимальное значение - 2032, однако мы обычно советуем не превышать 512.

Можно ли выделить более 2032 МБ для целей разработки?

Это техническое ограничение: 2048 МБ (или больше) переполнят 32-битный знаковый целочисленный размер TypeArray, используемый для реализации кучи Unity в JavaScript.

Почему вы не можете сделать Unity Heap изменяемым по размеру?

Мы рассматривали возможность использования флага ALLOW_MEMORY_GROWTH emscripten, чтобы позволить изменять размер кучи, но пока решили этого не делать, поскольку это может привести к отключению некоторых оптимизаций в Chrome. Нам еще предстоит провести реальный сравнительный анализ этого влияния. Мы полагаем, что использование этой функции может усугубить проблемы с памятью. Если вы достигли момента, когда куча Unity Heap стала слишком мала, чтобы вместить всю необходимую память, и ее нужно увеличить, браузеру придется выделить большую кучу, скопировать все из старой кучи, а затем деаллоцировать старую кучу. Таким образом, ей нужна память одновременно для новой и старой кучи (пока она не закончит копирование), что требует большего объема общей памяти. Поэтому расход памяти будет выше, чем при использовании заранее определенного фиксированного объема памяти.

Почему 32-битный браузер работает без памяти на 64-битной ОС?

32-битные браузеры будут сталкиваться с теми же ограничениями памяти, независимо от того, является ли ОС 64- или 32-битной.

Выводы

Последняя рекомендация - профилировать содержимое Unity WebGL с помощью инструментов для конкретного браузера, поскольку, как мы уже описали, существуют выделения за пределами Unity Heap, которые профилировщик Unity не может отследить.

Надеемся, что некоторые из этих сведений будут вам полезны. Если у вас возникнут дополнительные вопросы, пожалуйста, задавайте их здесь или на форуме WebGL.

Обновление:

Мы говорили о памяти, используемой для кода, и упоминали, что исходный JS-код копируется во временный текстовый блок. Мы обнаружили, что блоб не был должным образом деаллоцирован, поэтому он постоянно находился в памяти браузера. В about:memory он обозначен как memory-file-data:

 В about:memory он обозначен как memory-file-data

Его размер зависит от объема кода и для сложных проектов может составлять 32 или 64 Мб. К счастью, это было исправлено в 5.3.6 Patch 8, 5.4.2 Patch 1 и 5.5.

Что касается Audio, то мы знаем, что потребление памяти по-прежнему является проблемой: В настоящее время потоковое воспроизведение аудио не поддерживается, и аудиоактивы хранятся в памяти браузера в несжатом виде. Поэтому мы предложили использовать тег <audio> для воспроизведения больших аудиофайлов. Для этого мы недавно опубликовали новый пакет Asset Store, который поможет вам минимизировать потребление памяти потоковыми аудиоисточниками. Проверьте!