@pixi-essentials/object-pool
Version:
Custom-tailored object pool for PixiJS-based applications
499 lines (403 loc) • 14.1 kB
JavaScript
/* eslint-disable */
/*!
* @pixi-essentials/object-pool - v0.1.0
* Compiled Sun, 05 Mar 2023 03:07:48 UTC
*
* @pixi-essentials/object-pool is licensed under the MIT License.
* http://www.opensource.org/licenses/mit-license
*
* Copyright 2019-2020, Shukant Pal <shukantpal@outlook.com>, All Rights Reserved
*/
this.PIXI = this.PIXI || {};
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@pixi/ticker')) :
typeof define === 'function' && define.amd ? define(['exports', '@pixi/ticker'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global._pixi_essentials_object_pool = {}, global.PIXI));
}(this, (function (exports, ticker) { 'use strict';
/**
* Provides the exponential moving average of a sequence.
*
* Ignored because not directly exposed.
*
* @internal
* @ignore
* @class
*/
class AverageProvider
{
/**
* @ignore
* @param {number} windowSize - no. of inputs used to calculate window
* @param {number} decayRatio - quantifies the weight of previous values (b/w 0 and 1)
*/
constructor(windowSize, decayRatio)
{
this._history = new Array(windowSize);
this._decayRatio = decayRatio;
this._currentIndex = 0;
for (let i = 0; i < windowSize; i++)
{
this._history[i] = 0;
}
}
/**
* @ignore
* @param {number} input - the next value in the sequence
* @returns {number} - the moving average
*/
next(input)
{
const { _history: history, _decayRatio: decayRatio } = this;
const historyLength = history.length;
this._currentIndex = this._currentIndex < historyLength - 1 ? this._currentIndex + 1 : 0;
history[this._currentIndex] = input;
let weightedSum = 0;
let weight = 0;
for (let i = this._currentIndex + 1; i < historyLength; i++)
{
weightedSum = (weightedSum + history[i]) * decayRatio;
weight = (weight + 1) * decayRatio;
}
for (let i = 0; i <= this._currentIndex; i++)
{
weightedSum = (weightedSum + history[i]) * decayRatio;
weight = (weight + 1) * decayRatio;
}
this._average = weightedSum / weight;
return this._average;
}
absDev()
{
let errSum = 0;
for (let i = 0, j = this._history.length; i < j; i++)
{
errSum += Math.abs(this._history[i] - this._average);
}
return errSum / this._history.length;
}
}
/**
* @interface
* @public
*/
/**
* `ObjectPool` provides the framework necessary for pooling minus the object instantiation
* method. You can use `ObjectPoolFactory` for objects that can be created using a default
* constructor.
*
* @template T
* @class
* @public
*/
class ObjectPool
{
/**
* @param {IObjectPoolOptions} options
*/
constructor(options = {})
{;ObjectPool.prototype.__init.call(this);
/**
* Supply pool of objects that can be used to immediately lend.
*
* @member {Array<T>}
* @protected
*/
this._freeList = [];
/**
* Number of objects in the pool. This is less than or equal to `_pool.length`.
*
* @member {number}
* @protected
*/
this._freeCount = 0;
this._borrowRate = 0;
this._returnRate = 0;
this._flowRate = 0;
this._borrowRateAverage = 0;
this._reserveCount = options.reserve || 0;
this._capacityRatio = options.capacityRatio || 1.2;
this._decayRatio = options.decayRatio || 0.67;
this._marginAverage = 0;
this._borrowRateAverageProvider = new AverageProvider(128, this._decayRatio);
this._marginAverageProvider = new AverageProvider(128, this._decayRatio);
}
/**
* Instantiates a new object of type `T`.
*
* @abstract
* @returns {T}
*/
// TODO: Support object destruction. It might not be so good for perf tho.
// /**
// * Destroys the object before discarding it.
// *
// * @param {T} object
// */
// abstract destroyObject(object: T): void;
/**
* The number of objects that can be stored in the pool without allocating more space.
*
* @member {number}
*/
get capacity()
{
return this._freeList.length;
}
set capacity(cp)
{
this._freeList.length = Math.ceil(cp);
}
/**
* Obtains an instance from this pool.
*
* @returns {T}
*/
allocate()
{
++this._borrowRate;
++this._flowRate;
if (this._freeCount > 0)
{
return this._freeList[--this._freeCount];
}
return this.create();
}
/**
* Obtains an array of instances from this pool. This is faster than allocating multiple objects
* separately from this pool.
*
* @param {number | T[]} lengthOrArray - no. of objects to allocate OR the array itself into which
* objects are inserted. The amount to allocate is inferred from the array's length.
* @returns {T[]} array of allocated objects
*/
allocateArray(lengthOrArray)
{
let array;
let length;
if (Array.isArray(lengthOrArray))
{
array = lengthOrArray;
length = lengthOrArray.length;
}
else
{
length = lengthOrArray;
array = new Array(length);
}
this._borrowRate += length;
this._flowRate += length;
let filled = 0;
// Allocate as many objects from the existing pool
if (this._freeCount > 0)
{
const pool = this._freeList;
const poolFilled = Math.min(this._freeCount, length);
let poolSize = this._freeCount;
for (let i = 0; i < poolFilled; i++)
{
array[filled] = pool[poolSize - 1];
++filled;
--poolSize;
}
this._freeCount = poolSize;
}
// Construct the rest of the allocation
while (filled < length)
{
array[filled] = this.create();
++filled;
}
return array;
}
/**
* Returns the object to the pool.
*
* @param {T} object
*/
release(object)
{
++this._returnRate;
--this._flowRate;
if (this._freeCount === this.capacity)
{
this.capacity *= this._capacityRatio;
}
this._freeList[this._freeCount] = object;
++this._freeCount;
}
/**
* Releases all of the objects in the passed array. These need not be allocated using `allocateArray`, however.
*
* @param {T[]} array
*/
releaseArray(array)
{
this._returnRate += array.length;
this._flowRate -= array.length;
if (this._freeCount + array.length > this.capacity)
{
// Ensure we have enough capacity to insert the release objects
this.capacity = Math.max(this.capacity * this._capacityRatio, this._freeCount + array.length);
}
// Place objects into pool list
for (let i = 0, j = array.length; i < j; i++)
{
this._freeList[this._freeCount] = array[i];
++this._freeCount;
}
}
/**
* Preallocates objects so that the pool size is at least `count`.
*
* @param {number} count
*/
reserve(count)
{
this._reserveCount = count;
if (this._freeCount < count)
{
const diff = this._freeCount - count;
for (let i = 0; i < diff; i++)
{
this._freeList[this._freeCount] = this.create();
++this._freeCount;
}
}
}
/**
* Dereferences objects for the GC to collect and brings the pool size down to `count`.
*
* @param {number} count
*/
limit(count)
{
if (this._freeCount > count)
{
const oldCapacity = this.capacity;
if (oldCapacity > count * this._capacityRatio)
{
this.capacity = count * this._capacityRatio;
}
const excessBound = Math.min(this._freeCount, this.capacity);
for (let i = count; i < excessBound; i++)
{
this._freeList[i] = null;
}
}
}
/**
* Install the GC on the shared ticker.
*
* @param {Ticker}[ticker=Ticker.shared]
*/
startGC(ticker$1 = ticker.Ticker.shared)
{
ticker$1.add(this._gcTick, null, ticker.UPDATE_PRIORITY.UTILITY);
}
/**
* Stops running the GC on the pool.
*
* @param {Ticker}[ticker=Ticker.shared]
*/
stopGC(ticker$1 = ticker.Ticker.shared)
{
ticker$1.remove(this._gcTick);
}
__init() {this._gcTick = () =>
{
this._borrowRateAverage = this._borrowRateAverageProvider.next(this._borrowRate);
this._marginAverage = this._marginAverageProvider.next(this._freeCount - this._borrowRate);
const absDev = this._borrowRateAverageProvider.absDev();
this._flowRate = 0;
this._borrowRate = 0;
this._returnRate = 0;
const poolSize = this._freeCount;
const poolCapacity = this._freeList.length;
// If the pool is small enough, it shouldn't really matter
if (poolSize < 128 && this._borrowRateAverage < 128 && poolCapacity < 128)
{
return;
}
// If pool is say, 2x, larger than borrowing rate on average (adjusted for variance/abs-dev), then downsize.
const threshold = Math.max(this._borrowRateAverage * (this._capacityRatio - 1), this._reserveCount);
if (this._freeCount > threshold + absDev)
{
const newCap = threshold + absDev;
this.capacity = Math.min(this._freeList.length, Math.ceil(newCap));
this._freeCount = this._freeList.length;
}
};}
}
var _class;
/**
* This stores existing object pools created for class-constructed objects.
*
* @ignore
*/
const poolMap = new Map();
/**
* Factory for creating pools of objects with default constructors. It will store the pool of
* a given type and reuse it on further builds.
*
* @class
* @public
* @example
* ```js
* import { ObjectPool, ObjectPoolFactory } from '@pixi-essentials/object-pool';
*
* class AABB {}
*
* const opool: ObjectPool<AABB> = ObjectPoolFactory.build(AABB) as ObjectPool<AABB>;
*
* const temp = opool.borrowObject();
* // do something
* opool.returnObject(temp);
* ```
*/
class ObjectPoolFactory
{
/**
* Builds an object-pool for objects constructed from the given class with a default constructor. If an
* object pool for that class was already created, an existing instance is returned.
*
* @param classConstructor
*/
static build(Type)
{
let pool = poolMap.get(Type);
if (pool)
{
return pool;
}
pool = new (class DefaultObjectPool extends ObjectPool
{
create()
{
return new Type();
}
})();
poolMap.set(Type, pool);
return pool;
}
/**
* Builds an object-pool for objects built using a factory function. The factory function's context will be the
* object-pool.
*
* These types of pools are not cached and should only be used on internal data structures.
*
* @param factoryFunction
*/
static buildFunctional(factoryFunction)
{
return new ( (_class =class DefaultObjectPool extends ObjectPool
{constructor(...args) { super(...args); _class.prototype.__init.call(this); }
__init() {this.create = factoryFunction;}
}, _class))();
}
}
exports.ObjectPool = ObjectPool;
exports.ObjectPoolFactory = ObjectPoolFactory;
Object.defineProperty(exports, '__esModule', { value: true });
})));
if (typeof _pixi_essentials_object_pool !== 'undefined') { Object.assign(this.PIXI, _pixi_essentials_object_pool); }
//# sourceMappingURL=pixi-object-pool.js.map