Resource pool design pattern in JavaScript

Object pool, also known as resource pool, is a design pattern in which, when an object is requested, it is returned from the pool of available objects; if the object is not available, it will be created.

Objects whose work is done can be released back to the pool so that they can be returned.

Why a resource pool?

Each programming language uses a resource (memory) to function, and every object or variable defined consumes memory, so it is important to manage the memory properly to make the application’s performance efficient.

When initializing a class instance is expensive, there is a high rate of class instantiation, and there are few instantiations in use at any given time, object pooling can result in significant performance gains.

The memory is periodically cleaned with the help of garbage collection.

For example,

let arr = new Array(1000).fill(0);
let newArray = arr;

In this, we have created a new array, arr and the variable newArray is pointing to arr, which means newArray is pointing to the memory address of the arr.

In order for garbage collection to collect memory, we will have to set both instances to null.

arr = null;
newArray = null;

Similar to objects, we have to delete them, in order for garbage collection.

const obj = {
  name: "learnersbucket"
};

delete obj;

Thus, it is better to maintain a pool of resources and reuse them rather than create a new one and release them when work is done.

How does a resource pool work?

Let us try to understand the resource pool with a real world example.

Imagine you are running the resource department in an organization. For a new employee, when they seek items (objects), you first check all the available items in the inventory; if they are available, you provide them to the employee; otherwise, you procure new items in your inventory in case of shortage and then give them to the employees, making an entry into the inventory; similarly, when the employee leaves the organization, they release the items back to the inventory, and the cycle continues.

The same way resource pools also function, follow this flow diagram to get a clear understanding.

Flow diagram of resource pool design pattern

Flow diagram of resource pool design pattern

Implementing a resource pool or object pool design pattern in JavaScript.

With an understanding of how resource pools work, let us try to implement them.

The resource pool is only concerned with managing the pool of resources and how to release them. What to maintain and how to reset the resource during the release are handled externally, making the resource pool flexible and reusable to handle any type of object.

A resource pool will always have a fixed size and will increase gradually. We will see both implementations.

Remember, we are designing the resource pool only to make the application memory efficient and handle garbage collection effectively.

Considering this, let us first see the difference between array initialization and its performance impact.

The normal way: declare the array and push the data.

const normalArray = (n) => {
  const arr = [];
  for (let i = 0; i < n; i++) {
    arr.push(i);
  }
}

Efficient way: declare the array, initialize it, and then push the data.

const arrayWithPreAllocation = (n) => {
  const arr = new Array(n).fill(0);
  for (let i = 0; i < n; i++) {
    arr[i] = i;
  }
}

In the majority of cases, arrayWithPreAllocation will outperform normalArray.

You can run the benchmark and check it yourself.

Thus, we are going to go with the second way and pre allocate the array with the default value. Also because resoruce pool has to be only, we will follow the Signleton design pattern to share the resource pool instance accross the codebase.

Initialize the resource pool class.

class ResourcePool {
  poolArray = null;
  constructor(constructorFunction, initialSize = 1000) {
    this.poolArray = new Array(initialSize).fill(0).map(constructorFunction);
  }
}

We will accept the constructor function in the input, and it will return us an object or any resource that will be managed.

The two important methods that this ResourcePool class has are getElement and releaseElement.

There are two ways of releasing an element:

  • By manually releasing the element
  • Duration-based: the resource will be released after the specified duration.

For either of these, we will need to maintain an object with the data and the flag to check if it is free or not.

class ResourcePoolMember {
  constructor(data) {
    this.data = data;
    this.available = true;
  }
};

Using this, we can create a new object or resource every time and track it.

Resource pool with manual release function

class ResourcePool {
  poolArray = null;
  
  // this two will be provided externally
  // this is default delcaration:
  creatorFunc = () => {};
  resetFunction = () => {};
  
  constructor(creatorFunc, resetFunction = (any) => any, size = 1000) {
    this.resetFunction = resetFunction;
    this.creatorFunc = creatorFunc;
    this.poolArray = new Array(size)
      .fill(0)
      .map(() => this.createElement());
  }
  
  // this will create a fresh instance
  // reset for safer side
  createElement() {
    const data = this.resetFunction(this.creatorFunc());
    return new ResourcePoolMember(data);
  };
  
  // returns the free resource from the pool
  getElement() {
    for (let i = 0; i < this.poolArray.length; i++) {
      if (this.poolArray[i].available) {
        this.poolArray[i].available = false;
        return this.poolArray[i];
      }
    }
  };
  
  // releases an element
  releaseElement(element) {
    element.available = true;
    this.resetFunction(element.data);
  }
}

We can test this by creating a creator function and a reset function.

const creatorFunc = () => {return {counter: 0};};
const resetFunc = (coolThing) => {
  coolThing.counter = 0;
  delete coolThing.name;
  return coolThing;
};
const myPool = new ResourcePool(creatorFunc, resetFunc, 1);
const objectThatIsReadyToUse = myPool.getElement();

console.log(objectThatIsReadyToUse);
// {
//   "free": false,
//   "data": {
//     "counter": 0
//   }
// }

// ... doing stuff with objectThatIsReadyToUse.data
objectThatIsReadyToUse.data.counter++;
objectThatIsReadyToUse.data.name = "Prashant";
console.log(objectThatIsReadyToUse);
// {
//   "free": false,
//   "data": {
//     "counter": 1,
//     "name": "Prashant"
//   }
// }

myPool.releaseElement(objectThatIsReadyToUse);
console.log(objectThatIsReadyToUse);
// {
//   "free": true,
//   "data": {
//     "counter": 0
//   }
// }

Here once the object is released, it will be reset according to the logic present in the reset function.

Note: You would restrict the mutation of certain keys of the object, like the available flag, externally.

Resource pool with duration based allocation.

In the duration based allocation, rather than maintaining the flag, we will maintain the time in milliseconds, and every time a new resource is asked, we will. Check if the previous resources are expired or not; if they are expired, we will reset them and return them.

class ResourcePoolMember {
  constructor(data) {
    this.data = data;
    this.time = 0;
  }
};

const DURATION = 3000;

class ResourcePool {
  poolArray = null;
  resetFunction = () => {};
  creatorFunc = () => {};
  
  constructor(creatorFunc, resetFunction = (any) => any, size = 1000) {
    this.resetFunction = resetFunction;
    this.creatorFunc = creatorFunc;
    this.poolArray = new Array(size)
      .fill(0)
      .map(() => this.createElement());
  }
  
  createElement() {
    const data = this.resetFunction(this.creatorFunc());
    return new ResourcePoolMember(data);
  };
  
  getElement() {
    for (let i = 0; i < this.poolArray.length; i++) {
      // check if the resource allocation duration has expired
      if ((Date.now() - this.poolArray[i].time) > DURATION) {
        // release the element
        this.releaseElement(this.poolArray[i]);
        // assign the current time
        this.poolArray[i].time = Date.now();
        // return it
        return this.poolArray[i];
      }
    }
  };
  
  releaseElement(element) {
    element.time = 0;
    this.resetFunction(element.data);
  }
}

We can test this asking for resource after the duration of any previous resource.

const creatorFunc = () => {return { counter:0 };};
const resetFunc = (coolThing) => {coolThing.counter = 0; return coolThing;};
const myPool = new ResourcePool(creatorFunc, resetFunc, 10);
const objectThatIsReadyToUse = myPool.getElement();

objectThatIsReadyToUse.data.counter++;
console.log(objectThatIsReadyToUse);
// {
//   "data": {
//     "counter": 1
//   },
//   "time": 1710445681593
// }

setTimeout(() => {
  const objectThatIsReadyToUse2 = myPool.getElement();
  
  console.log(objectThatIsReadyToUse === objectThatIsReadyToUse2);
  // true
  // same object is returned
  
  console.log(objectThatIsReadyToUse2);
  // {
  //   "data": {
  //     "counter": 0
  //   },
  //   "time": 1710445685157
  // }
}, 3500);

Increasing the size of the resource pool

There is a chance that you encounter a scenario where you want to increase the resource pool size, but you want to do it intelligently. You don't want to create a large pool that is never used.

There are two ways of doing it:

  • You increase the size of the pool by a percentage X, when only Y percent of the pool is available if you are using the array, called amortized space increase, which works in linear time O(N).
  • Second, use the doubly linked list or double ended queue and increase the size as and when required. This is the best approach, as you won't have to worry about reducing the size; it will be adjusted as per the requirements and the changes.

Please enroll into the alpha.learnersbucket.com to get the complete implementation. Pay once and get lifetime access.