Object Pools in JavaScript

Published at January 20, 2023

One important metric measured by the browser to decide when to schedule garbage collection is the rate of object churn. If lots of objects are being instantiated and then going out of scope, the browser will schedule garbage collections more aggressively, which of course will slow down the application.

The Object Pool pattern is used in order to improve performance by reducing runtime memory allocation and gabarge collection

Memory Allocation and garbage collection performance issue

The GC is a periodical process. It runs over all the stuff that we pull in memory and marks them for sweeping if they are not referenced anymore. Then internally it decides when to clean up.

let a = [1, 2, 3] //  the reference count is now 1
b = a //  the reference count is now 2

a = null // the reference count is now 1
a = null // the reference is now 0
// ready for garbage collection

Consider the following:

function addCounter(counter1: Counter , counter2 : Counter ) {
    let data = counter1.data + counter2.data
    let counter3 = new Counter(data)
    return counter3
}

When invoked, this function creates a new object on the heap, modifies it, and returns it to the caller. If the lifetime of this vector object is short, it will soon lose all its references and be eligible for garbage collection.

One strategy is to use an object pool. At some point in initialization, you will create an object pool that manages a collection of recyclable objects. Your application can request an object from this pool, set its properties, use it, and return it to the pool when it’s done.

Because no object instantiation is occurring, garbage collection heuristics won’t measure an uptick in object churn, and garbage collec- tion will occur less frequently.

Implementations


class Pool<T> {
  #constructFn: () => T;
  #resetFn: (o: T) => T;

  constructor(
    initSize: number,
    constructFn: () => T,
    resetFn: (o: T) => T,
  ) {

    this.#constructFn = constructFn;
    this.#resetFn = resetFn;
    for (let i = 0; i < initSize; i++) {
      this.#pool.push(this.#createMember());
    }
  }

The ContructorFn can be any function that return a value. For a meaningful usage , it would usually instantiated a class or create an object and return it. While resetFn can be use to return the state of the member object when we release it to the pool after use

Get Element and Release Elements

We will create a Member object that wrap the state of our value and also point to the next member to support faster search. To accomplish that, we will try the linked list structure

class PoolMember<T> {
	data: T
	_nextElement: PoolMember<T> | null

	constructor(data: T) {
		this.data = data
	}
}

We can now use our new wrapper to implement the getMember and releaseMember methods:

class Pool<T> {
	// Min capacity of the pool before we increase the size
	static MIN_PERCENT: number
	// We increase the size by the following percent
	static INCREASE_PERCENT: number
	// Pool size when we first create
	static POOL_SIZE: number

	// preallocated pool
	#pool: PoolMember<T>[] = []

	// current pool free size
	#freesize: number = 0

	// next available node in the pool
	#next: PoolMember<T>

	// last available node in the pool
	#last: PoolMember<T>

	#constructFn: () => T
	#resetFn: (o: T) => T

	constructor(
		initSize: number,
		min_percent: number,
		increase_percent: number,
		constructFn: () => T,
		resetFn: (o: T) => T
	) {
		Pool.POOL_SIZE = initSize
		Pool.MIN_PERCENT = min_percent
		Pool.INCREASE_PERCENT = increase_percent

		this.#constructFn = constructFn
		this.#resetFn = resetFn
		for (let i = 0; i < initSize; i++) {
			this.#pool.push(this.#createMember())
		}
		this.#next = this.#pool[0]
	}
	#linkMember(e: PoolMember<T>) {
		if (this.#last) {
			this.#last._nextElement = e
			this.#last = e
		} else {
			this.#last = e
		}
	}

	#createMember(): PoolMember<T> {
		let data = this.#resetFn(this.#constructFn())
		let member = new PoolMember(data)
		this.#linkMember(member)
		this.#freesize++
		return member
	}

	//  Unlink node to use and mark it unfree
	#unlink(e: PoolMember<T>) {
		this.#next = e._nextElement as PoolMember<T>
		e._nextElement = null
	}

	getMember(): PoolMember<T> {
		if (Math.round((this.#freesize / Pool.POOL_SIZE) * 100) <= Pool.MIN_PERCENT) {
			let increaseSize = Math.round((Pool.POOL_SIZE * Pool.INCREASE_PERCENT) / 100)
			console.log(`Pool size low , creating ${increaseSize} nodes`)

			for (let i = 0; i < increaseSize; i++) {
				this.#pool.push(this.#createMember())
			}
		}

		let member = this.#next
		this.#unlink(member)
		this.#freesize--
		console.log('Getting One node')
		console.log(`Current available nodes ${this.#freesize}`)
		return member
	}

	releaseMember(e: PoolMember<T>): void {
		this.#resetFn(e.data)
		this.#last._nextElement = e
		this.#last = e
		this.#freesize++
		console.log(`Release node`)
		console.log(`Current available nodes: ${this.#freesize}`)
	}
}

It will be hard to guess the right amount of Preallocation, so we introduce some variable to keep track of the min state, and increase size when its reach the state

For each of the space allocated , we created a new node with constructFn and make sure its have a fresh state by wrap it over resetFn Now we can get node with getMember and utilize the underlying object, after that the object will be release back to the pool with releaseMember.

Khanh Anh Trinh © 2024 ♥