From 0b343857aae80d1f9a566bb3a189f33f998c3497 Mon Sep 17 00:00:00 2001 From: Kyle Simpson Date: Sun, 11 Dec 2016 14:49:44 -0600 Subject: [PATCH] initial commit of library, tests --- .gitignore | 2 + .npmignore | 2 + README.md | 146 +++++++++++++++++++++++++++++++++ build-core.js | 43 ++++++++++ lib/deePool.src.js | 151 ++++++++++++++++++++++++++++++++++ node-tests.js | 10 +++ package.json | 23 ++++++ qunit.config.js | 52 ++++++++++++ tests.html | 16 ++++ tests.js | 197 +++++++++++++++++++++++++++++++++++++++++++++ 10 files changed, 642 insertions(+) create mode 100644 .gitignore create mode 100644 .npmignore create mode 100644 README.md create mode 100755 build-core.js create mode 100644 lib/deePool.src.js create mode 100755 node-tests.js create mode 100644 package.json create mode 100644 qunit.config.js create mode 100644 tests.html create mode 100644 tests.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e7f1bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules +deePool.js diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..346be29 --- /dev/null +++ b/.npmignore @@ -0,0 +1,2 @@ +.gitignore +node_modules diff --git a/README.md b/README.md new file mode 100644 index 0000000..e3b37c7 --- /dev/null +++ b/README.md @@ -0,0 +1,146 @@ +# deePool + +A highly-efficient simple (no frills) object pool. + +## Explanation + +**deePool** (aka "deep-pool") is an object pool with efficiency (speed, low memory, little-to-no GC) as its primary design goal. + +As such, there are no configuration options to tweak behavior. It does one thing, and one thing well. If you want to re-configure the behavior, modify the code. :) + +Also, this library doesn't really try to do much in the way of graceful handling of errors or mistakes, as that stuff just slows it down. You should be careful in how you use *deePool*. + +## Environment Support + +This library requires ES6+ support. If you need to use it in ES<=5, transpile it with Babel yourself. + +## Library API + +There's only one method on the API: + +```js +deePool.create( objectFactory ) +``` + +* `create(..)`: produces a new pool instance. You can create as many separate pools as you need. + + `objectFactory` must be a function that produces a single new empty object (or array, etc) that you want available in the pool. Examples: + + ```js + var myArrays = deePool.create( function makeArray(){ + return [ + [ "foo", "bar", "baz" ], + [ 1, 2, 3 ] + ]; + } ); + + var myWidgets = deePool.create( function makeWidgets(){ + return new SomeCoolWidget(1,2,3); + } ); + ``` + +### Pool API + +Each pool has four simple methods on its API: + +```js +pool.use() + +pool.recycle( obj ) + +pool.grow( additionalSize ) + +pool.size() +``` + +* `pool.use()`: retrieves an available object instance from the pool. Example: + + ```js + var arr = myArrays.use(); + ``` + + **Note:** If the pool doesn't have any free instances, it will automatically grow (double in size, or set to `5` if currently empty) and then return one of the new instances. + +* `pool.recycle(..)`: inserts an object instance back into the pool. Example: + + ```js + myArrays.recycle( arr ); + ``` + + **Tips:** + - Don't forget to `recycle(..)` object instances after you're done using them. They are not automatically recycled, and your pool will run out of available instances, and thus keep growing unboundedly if you don't recycle. + - Don't `recycle(..)` an object instance until you're fully done with it. As objects are held by reference in JS, if you hold onto a reference and modify an already recycled object, you will cause difficult to track down bugs in your application! To avoid this pitfall, unset your reference(s) to a pooled object immediately after `recycle(..)`. + - Don't `recycle(..)` objects that weren't created for the pool and extracted by a previous `use()`. + - Don't `recycle(..)` an object more than once. This will end up creating multiple references in the pool to the same object, which will cause difficult to track down bugs in your application! To avoid this pitfall, unset your reference(s) to a pooled object immediately after `recycle(..)`. + - Don't create references between object instances in the pool, or you will cause difficult to track down bugs in your application! + - If you need to "condition" or "reset" an object instance for its later reuse, do so before passing it into `recycle(..)`. If a pooled object holds references to other objects, and you want that memory freed up, make sure to unset those references. + + **Note:** If you insert an object instance into a pool that has no empty slots (this is always a mistake, but is not disallowed!), the result will be that the pool grows in size by 1. + +* `pool.grow(..)`: A number passed will specify how many instances to add to the pool (created by `objectFactory()` as specified in the [`deePool.create(..)` call](#Library_API)). If no number is passed, the default is the current size of the pool (effectively doubling it in size). Examples: + + ```js + var myPool = deePool.create(makeArray); + + var arr = myPool.use(); // pool size now `5` + + myPool.grow( 3 ); // pool size now `8` + myPool.grow(); // pool size now `16` + ``` + + **Tips:** + - A new pool starts out empty (size: `0`). Always call `grow(..)` with a valid positive (non-zero) number to initialize the pool before using it. Otherwise, the call will have no effect; on an empty pool, this will confusingly leave the pool empty. + - An appropriate initial size for the pool will depend on the tasks you are performing; essentially, how many objects will you need to use concurrently? + + You should profile for this with your use-case(s) to determine what the most likely maximum pool size is. You'll want a pool size that's big enough so it doesn't have to grow very often, but not so big that it wastes memory. + - Don't grow the pool manually unless you are really sure you know what you're doing. It's better to set the pool size initially and let it grow automatically (via `use()`) only as it needs to. + +* `pool.size()`: Returns the number of overall slots in the pool (both used and unused). Example: + + ```js + var myPool = deePool.create(makeArray); + + myPool.size(); // 0 + + myPool.grow(5); + myPool.grow(10); + myPool.grow(5); + + myPool.size(); // 20 + ``` + +## Builds + +The core library file can be built (minified) with an included utility: + +``` +./build-core.js +``` + +However, the recommended way to invoke this utility is via npm: + +``` +npm run-script build-core +``` + +## Tests + +To run the tests, you must first [build the core library](#Builds). + +With `npm`, run: + +``` +npm test +``` + +Or, manually: + +``` +node node-tests.js +``` + +## License + +The code and all the documentation are released under the MIT license. + +http://getify.mit-license.org/ diff --git a/build-core.js b/build-core.js new file mode 100755 index 0000000..cdaf497 --- /dev/null +++ b/build-core.js @@ -0,0 +1,43 @@ +#!/usr/bin/env node + +var fs = require("fs"), + path = require("path"), + // ugly = require("uglify-js"), + + result +; + +console.log("*** Building Core ***"); +console.log("Minifying to deePool.js."); + +try { + // result = ugly.minify(path.join(__dirname,"lib","deePool.src.js"),{ + // mangle: { + // keep_fnames: true + // }, + // compress: { + // keep_fnames: true + // }, + // output: { + // comments: /^!/ + // } + // }); + + + // NOTE: since uglify doesn't yet support ES6, no minifying happening, + // just pass through copying. :( + fs.writeFileSync( + path.join(__dirname,"deePool.js"), + + fs.readFileSync(path.join(__dirname,"lib","deePool.src.js")), + + // result.code + "\n", + { encoding: "utf8" } + ); + + console.log("Complete."); +} +catch (err) { + console.error(err); + process.exit(1); +} diff --git a/lib/deePool.src.js b/lib/deePool.src.js new file mode 100644 index 0000000..ad5832e --- /dev/null +++ b/lib/deePool.src.js @@ -0,0 +1,151 @@ +(function UMD(name,context,definition){ + if (typeof define === "function" && define.amd) { define(definition); } + else if (typeof module !== "undefined" && module.exports) { module.exports = definition(); } + else { context[name] = definition(name,context); } +})("deePool",this,function DEF(name,context){ + "use strict"; + + const EMPTY_SLOT = Object.freeze(Object.create(null)); + const NOTHING = EMPTY_SLOT; + + return { create }; + + + // ****************************** + + // create a new pool + function create(objectFactory = ()=>({})) { + var objPool = []; + var nextFreeSlot = null; // pool location to look for a free object to use + var nextOpenSlot = null; // pool location to insert next recycled object + + return { + use, + recycle, + grow, + size, + }; + + + // ****************************** + + function use() { + var objToUse = NOTHING; + + // know where the next free object in the pool is? + if (nextFreeSlot != null) { + objToUse = objPool[nextFreeSlot]; + objPool[nextFreeSlot] = EMPTY_SLOT; + } + // otherwise, go looking + else { + // search starts with position + 1 (so, 0) + nextFreeSlot = -1; + } + + // look for the next free obj + for ( + var count = 0, i = nextFreeSlot + 1; + (count < objPool.length) && (i < objPool.length); + i = (i + 1) % objPool.length, count++ + ) { + // found free obj? + if (objPool[i] !== EMPTY_SLOT) { + // still need to free an obj? + if (objToUse === NOTHING) { + objToUse = objPool[i]; + objPool[i] = EMPTY_SLOT; + } + // this slot's obj can be used next time + else { + nextFreeSlot = i; + return objToUse; + } + } + } + + // no other free objs left + nextFreeSlot = null; + + // still haven't found a free obj to use? + if (objToUse === NOTHING) { + // double the pool size (or minimum 5 if empty) + grow(objPool.length || 5); + + objToUse = objPool[nextFreeSlot]; + objPool[nextFreeSlot] = EMPTY_SLOT; + nextFreeSlot++; + } + + return objToUse; + } + + function recycle(obj) { + // know where the next empty slot in the pool is? + if (nextOpenSlot != null) { + objPool[nextOpenSlot] = obj; + obj = NOTHING; + } + // otherwise, go looking + else { + // search starts with position + 1 (so, 0) + nextOpenSlot = -1; + } + + // look for the next empty slot + for ( + var count = 0, i = nextOpenSlot + 1; + (count < objPool.length) && (i < objPool.length); + i = (i + 1) % objPool.length, count++ + ) { + // found empty slot? + if (objPool[i] === EMPTY_SLOT) { + // obj still needs to be reinserted? + if (obj !== NOTHING) { + objPool[i] = obj; + obj = NOTHING; + } + // this empty slot can be used next time! + else { + nextOpenSlot = i; + return; + } + } + } + + // no empty slots left + nextOpenSlot = null; + + // still haven't recycled obj? + if (obj !== NOTHING) { + // insert at the end (growing size of pool by 1) + // NOTE: this is likely mis-use of the library + objPool[objPool.length] = obj; + } + } + + function grow(count = objPool.length) { + nextFreeSlot = objPool.length; + + for (var i = 0; i < count; i++) { + // add new obj to pool + objPool[objPool.length] = objectFactory(); + } + + // look for an open slot + nextOpenSlot = null; + for (var i = 0; i < nextFreeSlot; i++) { + // found an open slot? + if (objPool[i] === EMPTY_SLOT) { + nextOpenSlot = i; + break; + } + } + } + + function size() { + return objPool.length; + } + } + +}); diff --git a/node-tests.js b/node-tests.js new file mode 100755 index 0000000..9d93da9 --- /dev/null +++ b/node-tests.js @@ -0,0 +1,10 @@ +#!/usr/bin/env node + +global.deePool = require("./lib/deePool.src.js"); +global.QUnit = require("qunitjs"); + +require("./qunit.config.js"); +require("./tests.js"); + +// temporary hack per: https://github.com/qunitjs/qunit/issues/1081 +QUnit.load(); diff --git a/package.json b/package.json new file mode 100644 index 0000000..5d4f314 --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "deepool", + "version": "1.0.0", + "description": "deePool: highly-efficient pool for objects", + "main": "./lib/deePool.src.js", + "scripts": { + "test": "./node-tests.js", + "build-core": "./build-core.js", + "build": "npm run build-core", + "prepublish": "npm run build-core" + }, + "devDependencies": { + "qunitjs": "~2.1.0", + "uglify-js": "~2.7.5" + }, + "keywords": [ + "object", + "pool", + "performance" + ], + "author": "Kyle Simpson ", + "license": "MIT" +} diff --git a/qunit.config.js b/qunit.config.js new file mode 100644 index 0000000..a68776d --- /dev/null +++ b/qunit.config.js @@ -0,0 +1,52 @@ +QUnit.config.requireExpects = true; + +QUnit.begin(begin); +QUnit.testDone(testDone); +QUnit.done(done); + + +// ****************************** + +function begin(details){ + if (details.totalTests > 0) { + console.log(`deePool Test Suite (${details.totalTests})`); + console.log(""); + } + else { + console.log(`deePool Test Suite: empty!`); + process.exit(1); + } +} + +function testDone(results){ + if (results.failed > 0) { + console.log(`Failed: '${results.name}' (${results.failed}/${results.total})`); + for (let i = 0; i < results.assertions.length; i++) { + if (results.assertions[i].result === false) { + console.log(` ${results.assertions[i].message}`); + } + } + } + else if (results.passed > 0) { + console.log(`Passed: '${results.name}' (${results.passed}/${results.total})`); + } + else { + console.log(`No assertions run: '${results.name}'`); + } +} + +function done(results){ + console.log(""); + + if (results.failed > 0) { + console.log(`Failed (${results.failed}/${results.total})`); + process.exit(1); + } + else if (results.passed > 0) { + console.log(`Passed (${results.passed}/${results.total})`); + } + else { + console.log("No tests run!"); + process.exit(1); + } +} diff --git a/tests.html b/tests.html new file mode 100644 index 0000000..ec03f32 --- /dev/null +++ b/tests.html @@ -0,0 +1,16 @@ + + + + +deePool Test Suite + + + +
+
+ + + + + + diff --git a/tests.js b/tests.js new file mode 100644 index 0000000..40a1dff --- /dev/null +++ b/tests.js @@ -0,0 +1,197 @@ +function factory(){ + var publicAPI = { + id: 0, + make: function make(){ + return { + id: ++publicAPI.id + }; + } + }; + + return publicAPI; +} + + +QUnit.test("deePool.create()",function t1(assert){ + assert.expect(6); + + var inst = factory(); + var pool = deePool.create(inst.make); + + assert.ok(inst.id === 0,"make() not yet called"); + assert.ok(typeof pool == "object","`pool` created"); + assert.ok(typeof pool.use == "function","use()"); + assert.ok(typeof pool.recycle == "function","recycle()"); + assert.ok(typeof pool.grow == "function","grow()"); + assert.ok(typeof pool.size == "function","size()"); +}); + +QUnit.test("use(): basic",function t2(assert){ + assert.expect(14); + + var inst = factory(); + var pool = deePool.create(inst.make); + + var o1 = pool.use(); + var o2 = pool.use(); + var o3 = pool.use(); + var o4 = pool.use(); + var o5 = pool.use(); + + assert.ok(inst.id === 5,"make() called five times"); + assert.ok(o1.id === 1,"use() returned o1 with id:1"); + assert.ok(o2.id === 2,"use() returned o2 with id:2"); + assert.ok(o3.id === 3,"use() returned o3 with id:3"); + assert.ok(o4.id === 4,"use() returned o4 with id:4"); + assert.ok(o5.id === 5,"use() returned o5 with id:5"); + + var o6 = pool.use(); + var o7 = pool.use(); + var o8 = pool.use(); + var o9 = pool.use(); + var o10 = pool.use(); + + assert.ok(inst.id === 10,"make() called five more times"); + assert.ok(o6.id === 6,"use() returned o6 with id:6"); + assert.ok(o7.id === 7,"use() returned o7 with id:7"); + assert.ok(o8.id === 8,"use() returned o8 with id:8"); + assert.ok(o9.id === 9,"use() returned o9 with id:9"); + assert.ok(o10.id === 10,"use() returned o10 with id:10"); + + var o11 = pool.use(); + + assert.ok(inst.id === 20,"make() called ten more times"); + assert.ok(o11.id === 11,"use() return o11 with id:11"); +}); + +QUnit.test("recycle(): basic",function t3(assert){ + assert.expect(5); + + var inst = factory(); + var pool = deePool.create(inst.make); + + var o1 = pool.use(); + var o2 = pool.use(); + var o3 = pool.use(); + var o4 = pool.use(); + var o5 = pool.use(); + + pool.recycle(o1); + pool.recycle(o2); + pool.recycle(o3); + pool.recycle(o4); + pool.recycle(o5); + + var o6 = pool.use(); + var o7 = pool.use(); + var o8 = pool.use(); + var o9 = pool.use(); + var o10 = pool.use(); + + assert.ok(o6 === o1,"use() after recycle(): o6 === o1"); + assert.ok(o7 === o2,"use() after recycle(): o7 === o2"); + assert.ok(o8 === o3,"use() after recycle(): o8 === o3"); + assert.ok(o9 === o4,"use() after recycle(): o9 === o4"); + assert.ok(o10 === o5,"use() after recycle(): o10 === o5"); +}); + +QUnit.test("grow(): basic",function t4(assert){ + assert.expect(10); + + var inst = factory(); + var pool = deePool.create(inst.make); + + pool.grow(); + assert.ok(inst.id === 0,"grow() with no arg has no effect on empty pool"); + + pool.grow("nothing"); + assert.ok(inst.id === 0,"grow('nothing') has no effect on pool"); + + pool.grow(-3); + assert.ok(inst.id === 0,"grow(-3) has no effect on pool"); + + pool.grow(0); + assert.ok(inst.id === 0,"grow(0) has no effect on pool"); + + pool.grow(3); + assert.ok(inst.id === 3,"grow(3) called make() three times"); + + pool.grow(5); + assert.ok(inst.id === 8,"grow(5) called make() five more times"); + + pool.grow(); + assert.ok(inst.id === 16,"grow() with no arg doubles the current size of the pool"); + + pool.grow("nothing"); + assert.ok(inst.id === 16,"grow('nothing') still has no effect on pool"); + + pool.grow(-3); + assert.ok(inst.id === 16,"grow(-3) still has no effect on pool"); + + pool.grow(0); + assert.ok(inst.id === 16,"grow(0) still has no effect on pool"); +}); + +QUnit.test("size(): basic",function t5(assert){ + assert.expect(3); + + var inst = factory(); + var pool = deePool.create(inst.make); + + assert.ok(pool.size() === 0,"size() with empty pool is zero"); + + pool.grow(5); + assert.ok(pool.size() === 5,"size() after grow(5) is five"); + + pool.grow(); + assert.ok(pool.size() === 10,"size() after grow() is now ten"); +}); + +QUnit.test("use() + recycle(): complex",function t6(assert){ + assert.expect(12); + + var inst = factory(); + var pool = deePool.create(inst.make); + + pool.grow(4); + + var o1 = pool.use(); + var o2 = pool.use(); + pool.recycle(o1); + pool.recycle(o2); + + var o3 = pool.use(); + var o4 = pool.use(); + + assert.ok(o3.id === 3,"o3 is third element"); + assert.ok(o4.id === 4,"o4 is fourth element"); + + pool.recycle(o3); + var o5 = pool.use(); + var o6 = pool.use(); + var o7 = pool.use(); + + assert.ok(o5.id === 1,"o5 is first element"); + assert.ok(o6.id === 2,"o6 is second element"); + assert.ok(o7.id === 3,"o7 is third element"); + + // should grow pool + var o8 = pool.use(); + var o9 = pool.use(); + + assert.ok(inst.id === 8,"make() called four more times"); + assert.ok(o8.id === 5,"o8 is fifth element"); + assert.ok(o9.id === 6,"o9 is sixth element"); + + pool.recycle(o7); + pool.recycle(o5); + var o10 = pool.use(); + var o11 = pool.use(); + var o12 = pool.use(); + var o13 = pool.use(); + + assert.ok(o10.id === 7,"o10 is seventh element"); + assert.ok(o11.id === 8,"o11 is eighth element"); + assert.ok(o12.id === 3,"o12 is third element"); + assert.ok(o13.id === 1,"o13 is first element"); +});