npm package: @nxtedition/slice
Description: A high-performance buffer slice and pool allocator for Node.js.
Latest version: 1.1.3

# @nxtedition/slice

A high-performance buffer slice and pool allocator for Node.js.

## Why

Node.js `Buffer.subarray()` is slow. Every call creates a new `Buffer` object — a typed array wrapper with prototype chain setup, internal slot initialization, and bounds validation. This overhead is negligible for occasional use, but becomes a bottleneck in hot paths — protocol parsers, binary codecs, streaming pipelines — where thousands of sub-views are created per second.

`Buffer.allocUnsafe()` is worse. Allocations above the pool size (`Buffer.poolSize`) threshold go through `allocBuffer` which crosses into C++ to create a new `ArrayBuffer` backing store. The pooled fast path still involves bookkeeping and pool management overhead, and every allocation produces a new `Buffer` object that the GC must eventually collect.

`Slice` avoids this entirely. It is a plain JavaScript object with `buffer`, `byteOffset`, and `byteLength` fields. Creating a slice is just setting three properties — no typed array wrapper creation, no GC pressure from short-lived `Buffer` objects. Operations like `toString`, `copy`, and `compare` delegate directly to the underlying buffer with the correct offsets.

`PoolAllocator` takes this further. Like Node's internal pool, it has management overhead — but it rarely (if ever) allocates new backing stores, and because `Slice` is a plain object rather than a typed array, resizing or freeing a slice doesn't produce garbage for V8 to collect. It pre-allocates a large contiguous buffer and hands out regions using power-of-2 bucketing. When a slice is freed, its slot is recycled. When a slice is resized within the same bucket, no data moves at all — just a field update. This gives you `malloc`/`realloc`/`free` semantics with near-zero overhead per operation. The trade-off is upfront memory allocation and internal fragmentation from power-of-2 rounding — a 10-byte allocation uses a 16-byte slot. Buckets are also independent: a freed 16-byte slot cannot satisfy a 32-byte request, so the pool can become fragmented if allocation sizes are uneven. Use `stats` to monitor pool utilization and tune the pool size for your workload.

## Install

```sh
npm install @nxtedition/slice
```

## Usage

```js
import { Slice, PoolAllocator } from '@nxtedition/slice'

// Create a slice from an existing buffer
const buf = Buffer.from('hello world')
const slice = new Slice(buf, 6, 5)
slice.toString() // 'world'

// Use a pool allocator for high-throughput allocation
const pool = new PoolAllocator()
const s = new Slice()

pool.realloc(s, 64) // allocate 64 bytes from pool
s.write('hello')
pool.realloc(s, 128) // grow — may reuse same slot
pool.realloc(s, 0) // free — slot is recycled
```

## Benchmarks

Measured on Apple M3 Pro, Node.js v25.3.0:

### Allocation

| Operation        | `Buffer.allocUnsafe` | `Buffer.allocUnsafeSlow` | `PoolAllocator` | Speedup |
| ---------------- | -------------------: | -----------------------: | --------------: | ------: |
| alloc 64 bytes   |             38.08 ns |                 41.23 ns |     **5.66 ns** |    6.7x |
| alloc 256 bytes  |             52.09 ns |                231.46 ns |     **5.90 ns** |    8.8x |
| alloc 1024 bytes |             91.24 ns |                340.75 ns |     **5.83 ns** |   15.6x |
| alloc 4096 bytes |            446.53 ns |                437.83 ns |     **6.24 ns** |   71.6x |

### Allocation (GC)

| Operation        | `Buffer.allocUnsafe` | `Buffer.allocUnsafeSlow` | `PoolAllocator` | Speedup |
| ---------------- | -------------------: | -----------------------: | --------------: | ------: |
| alloc 64 bytes   |            400.46 ns |                167.94 ns |     **6.33 ns** |   63.3x |
| alloc 256 bytes  |            309.57 ns |                500.58 ns |     **6.35 ns** |   48.7x |
| alloc 4096 bytes |            653.40 ns |                620.19 ns |     **6.32 ns** |  103.4x |

Under GC pressure, the advantage grows dramatically — up to **103x** faster — because `PoolAllocator` reuses slots from a pre-allocated buffer and never creates objects for V8 to trace.

### Slice creation vs `Buffer.subarray`

| Operation              | `Buffer.subarray` |      `Slice` | Speedup |
| ---------------------- | ----------------: | -----------: | ------: |
| subarray 64 bytes      |          38.11 ns | **12.99 ns** |    2.9x |
| subarray 1024 bytes    |          36.87 ns | **13.26 ns** |    2.8x |
| subarray 64 bytes (GC) |         127.30 ns | **81.60 ns** |    1.6x |

### Combined operations

| Operation                             |  `Buffer` | `PoolAllocator` | Speedup |
| ------------------------------------- | --------: | --------------: | ------: |
| alloc/free 64 bytes                   |  32.72 ns |    **30.80 ns** |    1.1x |
| alloc/free 64 bytes (GC)              | 273.35 ns |    **73.34 ns** |    3.7x |
| alloc/free 256 bytes                  |  58.88 ns |    **29.38 ns** |    2.0x |
| realloc churn (64 → 128 → 64)         |  93.63 ns |    **26.99 ns** |    3.5x |
| realloc in-place (grow within bucket) |  60.16 ns |    **11.06 ns** |    5.4x |
| 10 concurrent allocs then free        | 406.26 ns |   **337.83 ns** |    1.2x |
| 10 concurrent allocs then free (GC)   | 647.95 ns |   **649.73 ns** |    1.0x |

## API

### `Slice`

A lightweight view over a `Buffer` with explicit offset and length tracking.

#### `new Slice(buffer?: Buffer, byteOffset?: number, byteLength?: number, maxByteLength?: number)`

Creates a new slice. All parameters are optional — defaults to an empty slice.

#### Properties

- `buffer: Buffer` — The underlying `Buffer`
- `byteOffset: number` — Start offset into the buffer
- `byteLength: number` — Current length in bytes
- `maxByteLength: number` — Maximum capacity in bytes
- `length: number` — Alias for `byteLength`

#### Methods

- `reset(): void` — Clear the slice back to empty state. **Note:** this does not return the slot to the `PoolAllocator` — you must call `realloc(slice, 0)` to free pool memory.
- `copy(target: Buffer | Slice, targetStart?: number, sourceStart?: number, sourceEnd?: number): number` — Copy data to a `Buffer` or `Slice`. Returns bytes copied.
- `compare(target: Buffer | Slice, targetStart?: number, targetEnd?: number, sourceStart?: number, sourceEnd?: number): -1 | 0 | 1` — Compare with a `Buffer` or `Slice`
- `write(string: string, offset?: number, length?: number, encoding?: BufferEncoding): number` — Write a string into the slice. Returns bytes written.
- `set(source: Buffer | Slice | null | undefined, offset?: number): void` — Copy from a `Buffer` or `Slice` into this slice
- `at(index: number): number` — Read byte at index (supports negative indexing)
- `test(expr: { test(buffer: Buffer, byteOffset: number, byteLength: number): boolean }): boolean` — Test the slice against an expression object
- `toString(encoding?: BufferEncoding, start?: number, end?: number): string` — Convert to string
- `toBuffer(start?: number, end?: number): Buffer` — Return a `Buffer` view

#### Static

- `Slice.EMPTY_BUF: Buffer` — Shared empty buffer singleton

### `PoolAllocator`

Pre-allocates a contiguous memory pool and manages slices using power-of-2 bucketing.

#### `new PoolAllocator(poolTotal?: number)`

Creates a pool allocator. Default pool size is 128 MB.

#### Methods

- `realloc(slice: Slice, byteLength: number): Slice` — Allocate, resize, or free a slice. Pass `0` to free.
- `isFromPool(slice: Slice | null | undefined): boolean` — Check if a slice was allocated from this pool

#### Properties

- `size: number` — Total size of all active allocations
- `stats: { size: number, padding: number, ratio: number, poolTotal: number, poolUsed: number, poolSize: number, poolCount: number }` — Detailed allocation statistics

## License

MIT