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);
}
}Method 2: Global Scratchpad Variables (Recommended for Render Loops)
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.