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.