Prevent Ammo.js Vector and Quaternion Memory Leaks

Ammo.js, the WebAssembly port of the Bullet physics engine, requires manual memory management because JavaScript’s garbage collector cannot automatically free memory allocated on the WebAssembly heap. This article provides a direct guide on how to strictly prevent memory leaks when working with temporary vectors (btVector3) and quaternions (btQuaternion) using explicit deletion, scratchpad recycling, and object pooling.

The Cause of Ammo.js Memory Leaks

When you instantiate an object in Ammo.js using the new keyword (e.g., new Ammo.btVector3()), memory is allocated on the C++ heap inside the WebAssembly module. JavaScript’s garbage collector only manages the lightweight wrapper object in JS, leaving the actual WebAssembly memory allocated forever if not explicitly freed. Over time, creating temporaries in a render loop will rapidly deplete system memory and crash the application.

To strictly prevent this, you must adopt two primary patterns: explicit destruction and object reuse (scratchpads).


Method 1: Explicit Destruction with Ammo.destroy

If you must instantiate a temporary vector or quaternion within a local scope, you must manually deallocate it using Ammo.destroy() once it is no longer needed.

To ensure the memory is freed even if an error occurs during execution, wrap the logic in a try...finally block:

function calculateForce() {
    const tempVector = new Ammo.btVector3(0, -9.81, 0);
    
    try {
        // Perform physics calculations
        rigidBody.applyCentralForce(tempVector);
    } finally {
        // This block always runs, preventing memory leaks
        Ammo.destroy(tempVector);
    }
}

For animations, physics updates, or render loops where calculations occur 60+ times per second, instantiating and destroying objects constantly causes high CPU overhead. The best practice is to declare static “scratchpad” variables outside of the loop and reuse them.

Instead of calling new, update the values of the existing instances using .setValue() or .setX() / .setY() / .setZ() / .setW().

// Allocate once globally or at the module level
const scratchVector = new Ammo.btVector3();
const scratchQuaternion = new Ammo.btQuaternion();

function onUpdate(deltaTime) {
    // Reuse the existing memory instead of allocating new memory
    scratchVector.setValue(1.0, 0.0, 0.0);
    scratchQuaternion.setValue(0.0, 0.0, 0.0, 1.0);

    // Apply to your physics objects
    rigidBody.setLinearVelocity(scratchVector);
    rigidBody.getWorldTransform().setRotation(scratchQuaternion);
}

// Clean up when the application or level unloads
function cleanup() {
    Ammo.destroy(scratchVector);
    Ammo.destroy(scratchQuaternion);
}

Method 3: Implementing a Simple Object Pool

If you need a dynamic, unpredictable number of temporary vectors or quaternions concurrently within a function, use a simple pre-allocated object pool. This avoids both allocation overhead and manual destruction boilerplate.

class VectorPool {
    constructor(size = 10) {
        this.pool = Array.from({ length: size }, () => new Ammo.btVector3());
        this.index = 0;
    }

    // Get a temporary vector from the pool
    get(x = 0, y = 0, z = 0) {
        const vec = this.pool[this.index];
        vec.setValue(x, y, z);
        
        // Rotate through the pool
        this.index = (this.index + 1) % this.pool.length;
        return vec;
    }

    destroy() {
        this.pool.forEach(vec => Ammo.destroy(vec));
    }
}

// Usage
const vecPool = new VectorPool(5);

function updatePositions() {
    // Safe to use without manual destruction inside the loop
    const posA = vecPool.get(1, 2, 3);
    const posB = vecPool.get(4, 5, 6);
    
    // Perform operations...
}

By enforcing these practices—using Ammo.destroy() for one-off local instantiations and relying on static scratchpads or pools for repetitive calculations—you will ensure your WebAssembly heap remains stable and entirely free of memory leaks.