Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache #4

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 59 additions & 4 deletions lib/entity_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ var utils = require("./utils")
var arrayCreate = utils.arrayCreate
var arrayExpand = utils.arrayExpand
var typedArrayExpand = utils.typedArrayExpand
var FastMap = utils.FastMap

var INITIAL_CAPACITY = 1024
var ENTITY_DEAD = 0
Expand All @@ -22,6 +23,10 @@ function EntityManager(components, storage) {

this._entityPool = []
this._entityCounter = 0

this._entityCacheIndices = new Uint32Array(INITIAL_CAPACITY)
this._entityCache = FastMap()
this._entityCache[0] = []
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be optimized:

this._entityCache = FastMap()
this._entityCache[0] = []

See https://gist.github.com/ooflorent/c58c9a0f08923973a5e3

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So do you suggest I add fast.js to the project and require it?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. Next to utils.js.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting:

/home/user/Projects/mq_makr/makr/lib/fast.js:10
  ForceEfficientMap.prototype = self
  ^^^^^^^^^^^^^^^^^
SyntaxError: Unexpected identifier

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed on the gist.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, wow didn't notice the missing brackets

}

EntityManager.prototype.create = function() {
Expand All @@ -37,6 +42,8 @@ EntityManager.prototype.create = function() {
var entity = this._entityInst[id]
this._entityFlag[id] = ENTITY_ALIVE

this._entityCacheIndices[entity._id] = this._entityCache[0].push(entity) - 1

return entity
}

Expand All @@ -54,10 +61,26 @@ EntityManager.prototype.query = function() {
return []
}

var result = []
for (var id = 0; id < this._entityCounter; id++) {
if (this._entityFlag[id] === ENTITY_ALIVE && (this._entityMask[id] & mask) === mask) {
result.push(this._entityInst[id])
var allocLen = 0
var entityMasks = Object.keys(this._entityCache)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be a real performance killer.
If we have 32 components it may generate a lot of different masks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I will add something that removes the empty ones.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Want me to fix this before the merge?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure! But I won't merge it immediately. I want to add more tests and benchmarks.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried two approaches so far, changing the property descriptor to enumerable: false so it doesn't appear in Object.keys(). This resulted in quite bad performance when adding/removing components.

Also tried simply deleting the property which I know is a bad thing to do on objects. But the performance is much better. 10x faster when adding components, 100x faster when removing them.

Overall it increased the query performance by roughly 1.5-2x on 1000 entities.

Any other ideas on how to do this?

for (var index = 0; index < entityMasks.length; index++) {
var entityMask = entityMasks[index]
if((entityMask & mask) === mask) {
allocLen += this._entityCache[entityMask].length
}
}

var result = new Array(allocLen)
var resultIndex = 0

for (var index = 0; index < entityMasks.length; index++) {
var entityMask = entityMasks[index]
if((entityMask & mask) === mask) {
var cache = this._entityCache[entityMask]
for (var i = 0; i < cache.length; i++) {
result[resultIndex] = cache[i]
resultIndex++
}
}
}

Expand All @@ -76,6 +99,7 @@ EntityManager.prototype._accomodate = function(id) {

this._entityFlag = typedArrayExpand(this._entityFlag, capacity)
this._entityMask = typedArrayExpand(this._entityMask, capacity)
this._entityCacheIndices = typedArrayExpand(this._entityCacheIndices, capacity)
this._entityInst = arrayExpand(this._entityInst, capacity, null)
this._storage.resize(capacity)
}
Expand All @@ -84,13 +108,20 @@ EntityManager.prototype._accomodate = function(id) {
EntityManager.prototype._add = function(id, component) {
var ctor = component.constructor
var index = this._components.index(ctor)

this._cacheRemove(id)
this._entityMask[id] |= 1 << index
this._cacheAdd(id)
this._storage.set(id, index, component)
}

EntityManager.prototype._remove = function(id, C) {
var index = this._components.index(C)

this._cacheRemove(id)
this._entityMask[id] &= ~(1 << index)
this._cacheAdd(id)

this._storage.delete(id, index)
}

Expand All @@ -104,7 +135,31 @@ EntityManager.prototype._get = function(id, C) {
return this._storage.get(id, index)
}

EntityManager.prototype._cacheAdd = function(id) {
var entity = this._entityInst[id]
var cache = this._entityCache[this._entityMask[id]]
if (cache === undefined) {
this._entityCache[this._entityMask[id]] = []
}
this._entityCacheIndices[id] = this._entityCache[this._entityMask[id]].push(entity) - 1
}

EntityManager.prototype._cacheRemove = function(id) {
var entity = this._entityInst[id]
var cache = this._entityCache[this._entityMask[id]]
if (cache.length === 1 || this._entityCacheIndices[id] === cache.length - 1) {
cache.pop()
} else {
var lastCacheElement = cache.pop()
this._entityCacheIndices[lastCacheElement._id] = this._entityCacheIndices[entity._id]
cache[this._entityCacheIndices[lastCacheElement._id]] = lastCacheElement
}
}

EntityManager.prototype._destroy = function(id) {
if (this._entityFlag[id] !== ENTITY_DEAD) {
this._cacheRemove(id)
}
this._entityFlag[id] = ENTITY_DEAD
this._entityMask[id] = 0
this._entityPool.push(id)
Expand Down
18 changes: 18 additions & 0 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ exports.arrayCreate = arrayCreate
exports.arrayExpand = arrayExpand
exports.arrayFill = arrayFill
exports.typedArrayExpand = typedArrayExpand
exports.FastMap = FastMap

// Arrays
// ------
Expand Down Expand Up @@ -32,3 +33,20 @@ function typedArrayExpand(source, length) {
target.set(source)
return target
}

// Maps
// ------------
function map() {
this.x = 0
delete this.x
}

function FastMap() {
var self = new map()

function ForceEfficientMap() {}
ForceEfficientMap.prototype = self
new ForceEfficientMap()

return self
}
66 changes: 66 additions & 0 deletions test/entity_manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,72 @@ describe("EntityManager", function() {
expect(em.query(Tag)).to.have.length(10)
})

it("should return the correct entities when querying", function() {
var destroyedEntities = []
var aliveEntities = []

for (var i = 0; i < 10; i++) {
var e = em.create()
e.add(new Position(0, 0))
e.add(new Motion(0, 0))
aliveEntities.push(e)
}

// We destroy them in a separate loop to ensure entities aren't picked from the object pool
// as this would make the entities in the destroyEntities alive again
for (var i = 0; i < 10; i++) {
var e = aliveEntities[i]
if (i % 2 === 0) {
e.destroy()
destroyedEntities.push(e)
}
}

// It should not return destroyed entities
var queryEntities = em.query(Position, Motion)
for (var i = 0; i < queryEntities.length; i++) {
expect(queryEntities[i].get(Position)).not.to.be.null()
expect(queryEntities[i].has(Position)).not.to.be.false()
for (var j = 0; j < destroyedEntities.length; j++) {
expect(queryEntities[i]).not.to.equal(destroyedEntities[j])
}
}

// It should not return duplicate entities
var queryEntities = em.query(Position, Motion)
for (var i = 0; i < queryEntities.length; i++) {
for (var j = i + 1; j < queryEntities.length; j++) {
expect(queryEntities[i]).not.to.equal(queryEntities[j])
}
}

for (var i = 0; i < queryEntities.length; i++) {
queryEntities[i].remove(Position)
}

queryEntities = em.query(Motion)

// It should return entities with the correct components after removing components
for (var i = 0; i < queryEntities.length; i++) {
expect(queryEntities[i].get(Position)).to.be.null()
expect(queryEntities[i].has(Position)).to.be.false()
expect(queryEntities[i].get(Motion)).not.to.be.null()
expect(queryEntities[i].has(Motion)).not.to.be.false()
}

for (var i = 0; i < queryEntities.length; i++) {
queryEntities[i].add(new Position(0, 0))
}

queryEntities = em.query(Position, Motion)

// It should return entities with the correct components after adding components
for (var i = 0; i < queryEntities.length; i++) {
expect(queryEntities[i].get(Position)).not.to.be.null()
expect(queryEntities[i].has(Position)).not.to.be.false()
}
})

it("should expand", function() {
var capacity = em.capacity

Expand Down