Skip to content

Instantly share code, notes, and snippets.

@jkoppel
Created March 30, 2026 20:47
Show Gist options
  • Select an option

  • Save jkoppel/39e541099314db9c0e922a37e5f3df7e to your computer and use it in GitHub Desktop.

Select an option

Save jkoppel/39e541099314db9c0e922a37e5f3df7e to your computer and use it in GitHub Desktop.
Survev.io Object Pool Performance Benchmark
/**
* Realistic Combat Benchmark: 4-Player Squad Fight
*
* Scenario:
* - 4 Players with Vectors (26 shots/sec).
* - Vector stats: 33 round magazine, 1.6s reload time.
* - Simulates 30 seconds of a squad fight with reloads.
* - Initial pool contains 100,000 "historical" objects.
*/
// --- TYPES & MOCKS ---
interface AbstractObject {
active: boolean;
__poolIdx: number;
m_init(): void;
m_free(): void;
}
class Projectile implements AbstractObject {
active = false;
__poolIdx = -1;
x = 0;
y = 0;
m_init() {
this.x = Math.random();
this.y = Math.random();
}
m_free() {}
update() {
this.x += 0.01;
this.y += 0.01;
return Math.sqrt(this.x * this.y);
}
}
// --- POOL IMPLEMENTATIONS ---
class OldPool<T extends AbstractObject> {
m_pool: T[] = [];
constructor(private classFn: new () => T) {}
m_alloc() {
let obj: T | null = null;
for (let i = 0; i < this.m_pool.length; i++) {
if (!this.m_pool[i].active) {
obj = this.m_pool[i];
break;
}
}
if (!obj) {
obj = new this.classFn();
this.m_pool.push(obj);
}
obj.active = true;
obj.m_init();
return obj;
}
m_free(obj: T) {
obj.active = false;
}
m_getPool() {
return this.m_pool;
}
}
class NewPool<T extends AbstractObject> {
m_pool: T[] = [];
m_freeList: T[] = [];
constructor(private classFn: new () => T) {}
m_alloc() {
let obj = this.m_freeList.pop();
if (!obj) obj = new this.classFn();
obj.active = true;
obj.__poolIdx = this.m_pool.length;
this.m_pool.push(obj);
obj.m_init();
return obj;
}
m_free(obj: T) {
obj.active = false;
const idx = obj.__poolIdx;
if (idx !== undefined && idx !== -1) {
const last = this.m_pool.pop()!;
if (last !== obj) {
this.m_pool[idx] = last;
last.__poolIdx = idx;
}
obj.__poolIdx = -1;
}
this.m_freeList.push(obj);
}
m_getPool() {
return this.m_pool;
}
}
// --- PLAYER STATE ---
class Player {
ammo = 33;
reloading = 0;
update(dt: number, frame: number, pool: any, activeRefs: any[]) {
if (this.reloading > 0) {
this.reloading -= dt;
if (this.reloading <= 0) this.ammo = 33;
return;
}
if (frame % 2 === 0) {
const p = pool.m_alloc();
activeRefs.push({ obj: p, end: frame + 45 });
this.ammo--;
if (this.ammo <= 0) this.reloading = 1.6;
}
}
}
function runBenchmark(label: string, pool: any) {
const FPS = 60;
const DURATION = 30;
const players = [new Player(), new Player(), new Player(), new Player()];
const activeRefs: { obj: Projectile; end: number }[] = [];
// Simulate high session churn (100,000 objects already processed)
for (let i = 0; i < 100000; i++) {
const p = pool.m_alloc();
pool.m_free(p);
}
const start = performance.now();
let sum = 0;
for (let frame = 0; frame < FPS * DURATION; frame++) {
for (const p of players) {
p.update(1 / FPS, frame, pool, activeRefs);
}
const arr = pool.m_getPool();
for (let i = 0; i < arr.length; i++) {
if (arr[i].active) {
sum += arr[i].update();
}
}
while (activeRefs.length > 0 && activeRefs[0].end <= frame) {
pool.m_free(activeRefs.shift()!.obj);
}
}
const end = performance.now();
console.log(`${label}:`);
console.log(`- Time: ${(end - start).toFixed(2)}ms (chk: ${sum.toFixed(0)})`);
}
console.log("--- 30S SQUAD FIGHT (4 Players, Vectors, Reloads, 100k processed) ---");
runBenchmark("Old Implementation", new OldPool(Projectile));
runBenchmark("New Implementation", new NewPool(Projectile));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment