Управление памятью: Emscripten heap и утечки

Ammo.js — это JavaScript-порт известной физической библиотеки Bullet Physics, скомпилированный с помощью Emscripten. Понимание внутреннего управления памятью в этом контексте критически важно для стабильной работы сложных приложений и предотвращения утечек.

Структура памяти в Ammo.js

Основной механизм работы с памятью в Ammo.js — Emscripten heap, представляющий собой линейный блок памяти, управляемый вручную. Heap состоит из следующих сегментов:

  • HEAP8, HEAP16, HEAP32 — типизированные массивы для работы с байтами, 16-битными и 32-битными значениями.
  • HEAPF32, HEAPF64 — массивы для работы с числами с плавающей точкой (float и double).

Все объекты Bullet, такие как btRigidBody, btCollisionShape, btDynamicsWorld, создаются внутри этой памяти. Управление осуществляется через вызовы конструктора и деструктора, например:

let shape = new Ammo.btBoxShape(new Ammo.btVector3(1, 1, 1));

Каждый вызов new Ammo.* выделяет память в Emscripten heap, которая не будет освобождена автоматически сборщиком мусора JavaScript. Без явного удаления объекты будут сохраняться в памяти до конца жизни страницы.

Методы освобождения памяти

Для предотвращения утечек необходимо использовать методы .delete() у каждого объекта, созданного в heap:

shape.delete();

Ключевые принципы:

  1. Всегда удалять временные объекты. В вычислениях часто создаются временные векторы или матрицы:

    let vec = new Ammo.btVector3(0, 10, 0);
    // использование vec
    vec.delete();

    Игнорирование таких объектов ведёт к накоплению неиспользуемой памяти.

  2. Удаление комплексных объектов. Объекты с внутренними зависимостями, например btRigidBody, часто требуют последовательного удаления:

    rigidBody.getMotionState().delete();
    rigidBody.delete();
  3. Управление формами коллизий. Любая форма (btCollisionShape) должна быть удалена после использования:

    let shape = new Ammo.btSphereShape(1);
    dynamicsWorld.addRigidBody(rigidBody);
    // после удаления тела
    dynamicsWorld.removeRigidBody(rigidBody);
    shape.delete();

Утечки памяти: как их обнаружить

Утечки в Ammo.js возникают из-за неприменения .delete() к объектам в heap. Основные признаки:

  • Постепенный рост использования памяти в браузере при длительном запуске симуляции.
  • Снижение производительности со временем, вызванное фрагментацией heap.

Отслеживать можно с помощью инструментов разработчика браузера (Memory profiler, Performance tab) и простого подсчёта объектов:

console.log(Ammo._malloc_stats());

Стратегии безопасного использования heap

  1. Создание пулов объектов Для часто используемых временных объектов имеет смысл создать пул:

    const tempVecPool = [];
    for (let i = 0; i < 10; i++) {
        tempVecPool.push(new Ammo.btVector3(0, 0, 0));
    }
    // использовать vec = tempVecPool[i]
  2. Минимизация выделений внутри цикла симуляции Любые new Ammo.btVector3 или new Ammo.btTransform внутри игрового цикла должны быть либо переиспользуемыми, либо сразу удаляемыми после применения.

  3. Последовательное удаление объектов при окончании сцены При полной очистке мира рекомендуется:

    • Удалять все тела (btRigidBody) и их MotionState.
    • Удалять все формы (btCollisionShape).
    • Удалять весь btDynamicsWorld и его конфигурации (btDispatcher, btBroadphaseInterface).

Пример безопасного полного удаления:

for (let i = dynamicsWorld.getNumCollisionObjects() - 1; i >= 0; i--) {
    let obj = dynamicsWorld.getCollisionObjectArray().at(i);
    let body = Ammo.castObject(obj, Ammo.btRigidBody);
    dynamicsWorld.removeRigidBody(body);
    if (body.getMotionState()) {
        body.getMotionState().delete();
    }
    body.getCollisionShape().delete();
    body.delete();
}

dynamicsWorld.delete();
dispatcher.delete();
broadphase.delete();
solver.delete();
collisionConfiguration.delete();

Интеграция с JavaScript

Поскольку JavaScript использует автоматический сборщик мусора, а Ammo.js — ручное управление, важно четко разделять области:

  • JS объекты (Array, Object) очищаются автоматически.
  • Ammo.js объекты требуют ручного удаления через .delete().

Невыполнение этого правила приводит к классической «утечке памяти в heap», которая не видна в обычном JS профайлере и может занимать сотни мегабайт при долгих симуляциях.

Практические рекомендации

  • Использовать вспомогательные функции для создания и удаления объектов:

    function createVector(x, y, z) {
        return new Ammo.btVector3(x, y, z);
    }
    function deleteVector(vec) {
        vec.delete();
    }
  • Не смешивать временные объекты и глобальные: временные всегда удалять сразу после использования, глобальные — при уничтожении сцены.

  • Регулярно профилировать память при разработке больших симуляций.

Эффективное управление памятью в Ammo.js требует дисциплины: каждое выделение в Emscripten heap должно иметь соответствующее удаление. Только такой подход обеспечивает стабильную работу физических симуляций и предотвращает накопление неиспользуемых объектов.