first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user