first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,120 @@
# ObjectPool
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`ObjectPool<T>` is a generic typed object pool that dampens GC pressure
in hot allocation paths — game-loop frame allocations, network message
envelopes, session metadata containers — by recycling typed objects
instead of letting them churn through the GC. Each pool is configured
with a `factory` (creates a new instance), a `reset` (clears state
before an instance returns to the pool), and an optional `dispose`
(invoked on cleanup paths so callers can release native handles).
The pool is dependency-free apart from the typed
`GameServerError` registry, Zod (for numeric-config validation), and
the OpenTelemetry `metrics` API. Misuse is loud: releasing an object
that didn't come from this pool raises `RELEASE_FOREIGN_OBJECT`;
acquiring past the configured `maxSize` raises `POOL_EXHAUSTED`; using
a pool after `dispose()` raises `POOL_DISPOSED`.
## Features
- Configurable `initialSize`, `maxSize`, and pre-allocation toggle.
- Typed factory + reset + optional dispose hooks.
- `acquire`, `release`, and `withObject(callback)` (auto-release with
finally semantics).
- Hard cap enforcement at acquisition time, with typed-error rejection.
- Lifecycle: `clear()` (resets stats + drops pool, fires dispose),
`dispose()` (terminal, all subsequent calls throw).
- Two OTel counters — `pool.acquire_total{outcome=reused|created}` and
`pool.release_total`.
- Stats snapshot — size, available, in-use, totals, hit-rate.
## Usage: Basic
```typescript
import { ObjectPool } from "@suaveplan/game-server/performance/object-pool";
interface Vec3 { x: number; y: number; z: number; }
const pool = new ObjectPool<Vec3>({
factory: () => ({ x: 0, y: 0, z: 0 }),
reset: (v) => { v.x = v.y = v.z = 0; },
initialSize: 64,
});
const v = pool.acquire();
v.x = 1; v.y = 2;
pool.release(v);
```
## Usage: Advanced
```typescript
import { ObjectPool } from "@suaveplan/game-server/performance/object-pool";
import { metrics } from "@opentelemetry/api";
interface PacketBuffer { bytes: Uint8Array; offset: number; }
const pool = new ObjectPool<PacketBuffer>(
{
factory: () => ({ bytes: new Uint8Array(2048), offset: 0 }),
reset: (b) => {
b.bytes.fill(0);
b.offset = 0;
},
dispose: (b) => {
// Optionally hand the underlying buffer to an async ring.
ringFree.push(b.bytes);
},
initialSize: 32,
maxSize: 512,
},
{ meter: metrics.getMeter("game-server.network") },
);
// Hot loop: borrow a buffer per packet, auto-release on errors.
await pool.withObject(async (buffer) => {
await readInto(buffer.bytes);
await transport.send(buffer.bytes);
});
const stats = pool.getStats();
console.log(`hit rate ${stats.hitRate}`);
```
## API Reference
- `new ObjectPool<T>(config: ObjectPoolConfig<T>, deps?: ObjectPoolDeps)`
— validates the numeric config via Zod; throws
`INVALID_POOL_CONFIG` on missing `factory`/`reset` or invalid
numeric fields.
- `acquire(): T` — returns a recycled or freshly factory-allocated
object. Throws `POOL_EXHAUSTED` when the pool is empty AND
`pool+inUse >= maxSize`. Throws `POOL_DISPOSED` after dispose.
- `release(obj: T): void` — throws `RELEASE_FOREIGN_OBJECT` if the
object was not handed out by this pool; throws `POOL_DISPOSED` after
dispose.
- `withObject(cb): Promise<R>` — borrows + finally-releases.
- `clear()` — drops every pooled object and resets stats (fires
dispose hook).
- `dispose()` — terminal, idempotent. Subsequent acquire/release/clear
raise `POOL_DISPOSED`. Fires dispose hook on every pooled object.
- `getStats(): PoolStats`.
- `disposed: boolean`.
## Implementation Notes
The cap check at acquire is `pool.length + inUse.size >= maxSize`. When
hit AND `pool.length === 0`, acquisition fails with `POOL_EXHAUSTED`.
When hit AND `pool.length > 0`, the pool reuses an existing instance —
this is the steady-state happy path for a saturated pool. Release
always succeeds back into the pool because the cap is enforced at
acquire, never letting `pool + inUse` exceed `maxSize`, so there is no
unreachable eviction branch to test. The `dispose` hook fires only on
`clear()` and `dispose()` paths, where the pool is being torn down or
emptied. Pre-allocation runs the factory synchronously inside
the constructor, which keeps every `acquire` constant-time in steady
state.