2
\$\begingroup\$

I recently rewrote my object pool design to \$O(n)\$ complexity. I did this using a linked list, which is not native in JavaScript. It does not, unfortunately, automatically assign null to a value when you free it.

const Pool = function() {
  const Free = new WeakMap,
        Next = new WeakMap,
        Default = new WeakMap,
        {setPrototypeOf, defineProperty, getOwnPropertyNames, freeze} = Object,
        {toString, hasOwnProperty} = {},
  empty = ( obj, template ) => {
        let templateNames = getOwnPropertyNames( template ),
     ownNames = getOwnPropertyNames( obj );
        for ( let name of [...templateNames, ...ownNames] ) {
            if( hasOwnProperty.call( template, name ) ) {
                if( typeof template[name] === 'object' ) {
                    obj[name] = empty( obj[name], template[name] );
                } else {
                    obj[name] = template[name];
                }
            } else {
                delete obj[name];
            }
        }
        return obj;
    },
  deepFreeze = (object) => {
    let propNames = getOwnPropertyNames(object);
    for (const name of propNames) {
      let value = object[name];
      object[name] = value && typeof value === "object" ? deepFreeze(value) : value;
    }
    return Object.freeze(object);
  }, deepClone = (object) => {
    let result = {}, propNames = getOwnPropertyNames(object);
    for (let name of propNames) {
      let value = object[name];
      result[name] = value && typeof value === "object" ? deepClone(value) : value;
    }
    return setPrototypeOf(result, object.__proto__);
  };
  class Pool {
    constructor(obj, size) {
      Default.set(this, deepClone(obj));
      Free.set(this, Array(size).fill().map((item) => {
        return deepClone(obj);
      }).map((item, index, array) => {
        return Next.set(item, array[index + 1] || null), item;
      })[0]);
    }
    allocate() {
      let result = Free.get(this);
      Free.set(this, Next.get(result));
      return result;
    }
    free(obj) {
      if (Next.has(obj)) {
        Next.set(obj, Free.get(this));
        Free.set(this, obj);
        Promise.resolve().then(empty.bind(null, obj, Default.get(this)));
        return;
      }
    }
  }
  return Pool;
}();
\$\endgroup\$
2
  • \$\begingroup\$ Could you provide a usage example. Object pools are used to increase performance by side stepping allocation and GC overheads. For traditional pools the performance margins are small and at face value your approach would negate any advantage. An example (real world) usage would allow for assessment of performance (as you have tagged) \$\endgroup\$
    – Blindman67
    Oct 18 '18 at 0:08
  • \$\begingroup\$ @Blindman67 Sure thing... I'll have to set up a test. I also think I meant memory... not performance. \$\endgroup\$ Oct 18 '18 at 11:28
2
\$\begingroup\$

Some problems

Complex, indeterminate, non intuitive behaviour

There is a fundamental flaw in the Pool object due to the use of a resolved promise to reset the object.

Calling free and allocate in the same execution context will result in allocated object having either a reset content or assigned content in later execution context. As event and call stack order is indeterminate this means the content of any allocated objects is also indeterminate after the function using pool exits.

Consider the example below

  • Create pool with default object const pool = new Pool({a: 1}, 10)

  • Get pooled object with pool.allocate() assign to o1 then free it with pool.free(o1).

  • In the same execution, get another pooled object Pool.allocate() assigned to o2 = {a : 3} which will be the free object o1 so that o1===o2 is true. The promise to reset o1 is still pending and will not execute until execution is idle and event queue empty of any events registered before the promise was created.

  • In separate execution context the content of o2 will not be as casually expected {a : 3} but {a : 1}

setTimeout(() => {
  const test = {a: 1};
  const pool = new Pool({a: 1}, 10);

  function logObject(name, obj, mess) {
    console.log("Ref " + name + " {a : " + obj.a +"}; // " + mess);
  }

  function createTest(val) { // function to allocate
    const ref = pool.allocate();
    ref.a = val;
    return ref
  }
  console.log("Call pool.allocate();");
  var o1 = createTest("2");
  logObject("o1", o1, "Ref assigned to o1");
  
  console.log("Call pool.free(o1);");
  pool.free(o1);
  logObject("o1", o1, "After pool.free(o1).");
  
  // Allocate another
  console.log("Call pool.allocate();");
  var o2 = createTest("3");
  logObject("o2", o2, "Ref assigned to o2");

  // however the pending promise will reset the object 
  setTimeout(() => {
    // 
    logObject("o2", o2, "o2 in new execution context reset, expected {a: 3}"); 
  });
  console.log("Exit current execution context.");
}, 0);





/* OP's Pool */
const Pool = function() {
  const Free = new WeakMap,
        Next = new WeakMap,
        Default = new WeakMap,
        {setPrototypeOf, defineProperty, getOwnPropertyNames, freeze} = Object,
        {toString, hasOwnProperty} = {},
  empty = ( obj, template ) => {
        let templateNames = getOwnPropertyNames( template ),
     ownNames = getOwnPropertyNames( obj );
        for ( let name of [...templateNames, ...ownNames] ) {
            if( hasOwnProperty.call( template, name ) ) {
                if( typeof template[name] === 'object' ) {
                    obj[name] = empty( obj[name], template[name] );
                } else {
                    obj[name] = template[name];
                }
            } else {
                delete obj[name];
            }
        }
        return obj;
    },
  deepFreeze = (object) => {
    let propNames = getOwnPropertyNames(object);
    for (const name of propNames) {
      let value = object[name];
      object[name] = value && typeof value === "object" ? deepFreeze(value) : value;
    }
    return Object.freeze(object);
  }, deepClone = (object) => {
    let result = {}, propNames = getOwnPropertyNames(object);
    for (let name of propNames) {
      let value = object[name];
      result[name] = value && typeof value === "object" ? deepClone(value) : value;
    }
    return setPrototypeOf(result, object.__proto__);
  };
  class Pool {
    constructor(obj, size) {
      Default.set(this, deepClone(obj));
      Free.set(this, Array(size).fill().map((item) => {
        return deepClone(obj);
      }).map((item, index, array) => {
        return Next.set(item, array[index + 1] || null), item;
      })[0]);
    }
    allocate() {
      let result = Free.get(this);
      Free.set(this, Next.get(result));
      return result;
    }
    free(obj) {
      if (Next.has(obj)) {
        Next.set(obj, Free.get(this));
        Free.set(this, obj);
        Promise.resolve().then(empty.bind(null, obj, Default.get(this)));
        return;
      }
    }
  }
  return Pool;
}();

It can become very complex as the execution order of events can not be guaranteed, thus what the content of the second allocated object will be is indeterminate.

Personally I would not consider the responsibility of the Pool to reset the content of pooled objects.

Cyclic reference.

Your deep clone is unsafe and will result in a call stack overflow for any object that contains a cyclic reference.

Example

const test = {};
test.test = test;
const pool = new Pool(test,1);  // throws RangeError max call stack size exceeded

You should safeguard the constructor either catching the RangeError, or correctly handling cyclic references.

Why weakMaps?

I can not workout why you use 3 weakMaps, it seems overly complex and limits the usefulness of the pool restricting the pool to hold only the objects created when pool instantiated.

Once a reference to an allocated object is lost the pool is reduced with no method available to replenish the pool.

Would it not be far simpler to use a stack or queue to hold the pool. With Pool.allocate popping from the stack if an object available (undefined if empty). Pool.free(obj) pushing back to the pool and also to preallocate the pool with objects. This would be much simpler.

Should the pool construct?

Is the pool's role to construct default objects? My view is that it should not be the pool's job to instantiate objects, and as pointed out above, assign default values to free objects.

There are also some style issues

To name a few...

  • Don't capitalize variables that are not constructors (eg Free, Next, and Default should be lowercase).
  • Use const for variables that do not change.
  • Space between of and [ in for of loop
  • Keep source code complexity down and avoid one use variables, var propNames = getOwnPropertyNames(object); for (const name of propNames) { can be for (const name of getOwnPropertyNames(object)) {
  • Use consistent naming. You use object and obj to reference the same type of argument (preference to the idiomatic abbreviation)
  • Don't add code that is redundant. In Pool.free the return is redundant and should not be there.
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.