Skip to content

FreeList

Eugene Lazutkin edited this page Jul 18, 2026 · 1 revision

Free list / object pool — recycles objects by threading dead ones into an intrusive singly linked list through their own link property. The pool itself allocates nothing; recycled nodes come out ready for adoption by the toolkit's lists.

Legend for tables
  • API
    • options - an object with optional properties:
      • create() - factory used by acquire() when the pool is empty. Omit for a pure recycler.
      • reset(node) - hook called on every release() before pooling — scrub payload references here.
      • capacity - maximum number of pooled objects; releases beyond it are dropped for GC. Default: Infinity; 0 disables pooling.
      • nextName - property name (string or symbol) threading the free list. Default: 'next'.
      • prevName - optional second link property scrubbed on acquire() — set it (e.g. 'prev') when pooling DLL nodes.
    • node - a pooled object
  • Complexity
    • O - complexity of an operation
    • O(1) - constant time
    • O(n) - linear time proportional to the pool size
    • O(k) - linear time proportional to the argument

Implementation of FreeList:

import FreeList from 'list-toolkit/free-list.js';
import ValueList from 'list-toolkit/value-list.js';
import {ValueNode} from 'list-toolkit/list/nodes.js';

const pool = new FreeList({
  create: () => new ValueNode(null),
  reset: node => (node.value = null), // don't pin payloads while pooled
  prevName: 'prev' // ValueNode is a DLL node
});

const queue = new ValueList();

// push through the pool:
const node = pool.acquire();
node.value = task;
queue.pushBackNode(node);

// pop through the pool:
const done = queue.popFrontNode();
use(done.value);
pool.release(done);

Class FreeList

Methods:

Member Return type Description O
constructor(options) this constructs a new FreeList O(1)
size number the number of pooled objects O(1)
capacity number the pool bound, or Infinity O(1)
isEmpty boolean checks if the pool is empty O(1)
isFull boolean checks if the pool reached its capacity O(1)
acquire() node or undefined take a pooled object, or create() one; links come out nulled O(1)
release(node) this run reset, then pool the object (or drop it when full) O(1)
preallocate(count) this fill the pool with freshly created objects O(k)
clear() this drop all pooled objects (threading links nulled) O(n)
[Symbol.iterator]() iterator iterate over the pooled objects O(1)

Notes on properties and methods

The pool is intrusive: the free list lives in the pooled objects' own link property (nextName, default 'next') — dead nodes carry the pool structure, exactly like live nodes carry list structure. Recycled objects leave acquire() with nextName (and prevName, when configured) set to null, which the toolkit's lists adopt directly via pushFrontNode/pushBackNode.

Always scrub payloads in reset — a pooled ValueNode still holding its value would pin that object for the garbage collector as long as it sits in the pool.

When it pays off — honest guidance (measured on bench/bench-pool.js, 2026-07-17): on a raw push/pop churn microbench, pooling is ~15% slower than fresh allocation (676μs vs 586μs per 10k cycles) — modern engines allocate short-lived objects with a pointer bump, and the pooled path adds bookkeeping. A pool is not a throughput optimization; reach for it to reduce GC pressure and pause jitter in long-running processes with steady churn, to reuse objects that are expensive to construct, or to cap allocation in latency-sensitive loops. Measure in your real workload, not a microbench.

release() does not verify the object is outside a list — releasing a node that is still linked corrupts both structures (same ownership contract as list nodes).

Exports

FreeList is the default export and is also exported by name.

Clone this wiki locally