Fix style and add some micro optimizations (#27)

This commit is contained in:
howmanysmall 2024-05-04 17:52:01 -06:00 committed by GitHub
parent cda04ce5a9
commit 283243350f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 496 additions and 469 deletions

3
.gitignore vendored
View file

@ -50,3 +50,6 @@ WallyPatches
roblox.toml
sourcemap.json
drafts/*.lua
*.code-workspace
roblox.yml

View file

@ -1,7 +1,7 @@
--!optimize 2
--!native
local testkit = require('../testkit')
local testkit = require("../testkit")
local BENCH, START = testkit.benchmark()
local function TITLE(title: string)
print()
@ -15,21 +15,21 @@ local oldMatter = require("../oldMatter")
local newMatter = require("../newMatter")
type i53 = number
do TITLE (testkit.color.white_underline("Jecs query"))
do
TITLE(testkit.color.white_underline("Jecs query"))
local ecs = jecs.World.new()
do TITLE "one component in common"
local function view_bench(
world: jecs.World,
A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53
)
do
TITLE("one component in common")
local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53)
BENCH("1 component", function()
for _ in world:query(A) do end
for _ in world:query(A) do
end
end)
BENCH("2 component", function()
for _ in world:query(A, B) do end
for _ in world:query(A, B) do
end
end)
BENCH("4 component", function()
@ -38,7 +38,8 @@ do TITLE (testkit.color.white_underline("Jecs query"))
end)
BENCH("8 component", function()
for _ in world:query(A, B, C, D, E, F, G, H) do end
for _ in world:query(A, B, C, D, E, F, G, H) do
end
end)
end
@ -89,7 +90,6 @@ do TITLE (testkit.color.white_underline("Jecs query"))
if flip() then
combination ..= "H"
ecs:set(entity, D8, {value = true})
end
if #combination == 7 then
@ -100,29 +100,31 @@ do TITLE (testkit.color.white_underline("Jecs query"))
end
local a = 0
for _ in archetypes do a+= 1 end
for _ in archetypes do
a += 1
end
view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
end
end
do TITLE(testkit.color.white_underline("OldMatter query"))
do
TITLE(testkit.color.white_underline("OldMatter query"))
local ecs = oldMatter.World.new()
local component = oldMatter.component
do TITLE "one component in common"
local function view_bench(
world: jecs.World,
A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53
)
do
TITLE("one component in common")
local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53)
BENCH("1 component", function()
for _ in world:query(A) do end
for _ in world:query(A) do
end
end)
BENCH("2 component", function()
for _ in world:query(A, B) do end
for _ in world:query(A, B) do
end
end)
BENCH("4 component", function()
@ -131,7 +133,8 @@ do TITLE(testkit.color.white_underline("OldMatter query"))
end)
BENCH("8 component", function()
for _ in world:query(A, B, C, D, E, F, G, H) do end
for _ in world:query(A, B, C, D, E, F, G, H) do
end
end)
end
@ -174,12 +177,10 @@ do TITLE(testkit.color.white_underline("OldMatter query"))
if flip() then
combination ..= "F"
ecs:insert(entity, D6({value = true}))
end
if flip() then
combination ..= "G"
ecs:insert(entity, D7({value = true}))
end
if flip() then
combination ..= "H"
@ -189,36 +190,36 @@ do TITLE(testkit.color.white_underline("OldMatter query"))
if #combination == 7 then
added += 1
ecs:insert(entity, D1({value = true}))
end
archetypes[combination] = true
end
local a = 0
for _ in archetypes do a+= 1 end
for _ in archetypes do
a += 1
end
view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
end
end
do TITLE(testkit.color.white_underline("NewMatter query"))
do
TITLE(testkit.color.white_underline("NewMatter query"))
local ecs = newMatter.World.new()
local component = newMatter.component
do TITLE "one component in common"
local function view_bench(
world: jecs.World,
A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53
)
do
TITLE("one component in common")
local function view_bench(world: jecs.World, A: i53, B: i53, C: i53, D: i53, E: i53, F: i53, G: i53, H: i53)
BENCH("1 component", function()
for _ in world:query(A) do end
for _ in world:query(A) do
end
end)
BENCH("2 component", function()
for _ in world:query(A, B) do end
for _ in world:query(A, B) do
end
end)
BENCH("4 component", function()
@ -227,7 +228,8 @@ do TITLE(testkit.color.white_underline("NewMatter query"))
end)
BENCH("8 component", function()
for _ in world:query(A, B, C, D, E, F, G, H) do end
for _ in world:query(A, B, C, D, E, F, G, H) do
end
end)
end
@ -270,12 +272,10 @@ do TITLE(testkit.color.white_underline("NewMatter query"))
if flip() then
combination ..= "F"
ecs:insert(entity, D6({value = true}))
end
if flip() then
combination ..= "G"
ecs:insert(entity, D7({value = true}))
end
if flip() then
combination ..= "H"
@ -285,15 +285,15 @@ do TITLE(testkit.color.white_underline("NewMatter query"))
if #combination == 7 then
added += 1
ecs:insert(entity, D1({value = true}))
end
archetypes[combination] = true
end
local a = 0
for _ in archetypes do a+= 1 end
for _ in archetypes do
a += 1
end
view_bench(ecs, D1, D2, D3, D4, D5, D6, D7, D8)
end
end

View file

@ -2,41 +2,37 @@
--!native
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local rgb = require(ReplicatedStorage.rgb)
local Matter = require(ReplicatedStorage.DevPackages.Matter)
local jecs = require(ReplicatedStorage.Lib)
local ecr = require(ReplicatedStorage.DevPackages.ecr)
local jecs = require(ReplicatedStorage.Lib)
local rgb = require(ReplicatedStorage.rgb)
local newWorld = Matter.World.new()
local ecs = jecs.World.new()
return {
ParameterGenerator = function()
local registry2 = ecr.registry()
return registry2
end,
end;
Functions = {
Matter = function()
for i = 1, 1000 do
newWorld:spawn()
end
end,
end;
ECR = function(_, registry2)
for i = 1, 1000 do
registry2.create()
end
end,
end;
Jecs = function()
for i = 1, 1000 do
ecs:entity()
end
end
},
end;
};
}

View file

@ -82,24 +82,27 @@ local function transitionArchetype(
end
-- Move the entity from the source to the destination archetype.
destinationEntities[destinationRow] = sourceEntities[sourceRow]
entityIndex[sourceEntities[sourceRow]].row = destinationRow
local atSourceRow = sourceEntities[sourceRow]
destinationEntities[destinationRow] = atSourceRow
entityIndex[atSourceRow].row = destinationRow
-- Because we have swapped columns we now have to update the records
-- corresponding to the entities' rows that were swapped.
local movedAway = #sourceEntities
if sourceRow ~= movedAway then
sourceEntities[sourceRow] = sourceEntities[movedAway]
entityIndex[sourceEntities[movedAway]].row = sourceRow
local atMovedAway = sourceEntities[movedAway]
sourceEntities[sourceRow] = atMovedAway
entityIndex[atMovedAway].row = sourceRow
end
sourceEntities[movedAway] = nil
end
local function archetypeAppend(entity: i53, archetype: Archetype): i24
local function archetypeAppend(entity: number, archetype: Archetype): number
local entities = archetype.entities
table.insert(entities, entity)
return #entities
local length = #entities + 1
entities[length] = entity
return length
end
local function newEntity(entityId: i53, record: Record, archetype: Archetype)
@ -122,47 +125,49 @@ local function hash(arr): string | number
return table.concat(arr, "_")
end
local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, from: Archetype?)
local destinationCount = #to.types
local function createArchetypeRecords(componentIndex: ComponentIndex, to: Archetype, _from: Archetype?)
local destinationIds = to.types
local records = to.records
local id = to.id
for i = 1, destinationCount do
local destinationId = destinationIds[i]
for i, destinationId in destinationIds do
local archetypesMap = componentIndex[destinationId]
if not componentIndex[destinationId] then
componentIndex[destinationId] = { size = 0, sparse = {} }
if not archetypesMap then
archetypesMap = {size = 0, sparse = {}}
componentIndex[destinationId] = archetypesMap
end
local archetypesMap = componentIndex[destinationId]
archetypesMap.sparse[to.id] = i
to.records[destinationId] = i
archetypesMap.sparse[id] = i
records[destinationId] = i
end
end
local function archetypeOf(world: World, types: {i24}, prev: Archetype?): Archetype
local ty = hash(types)
world.nextArchetypeId = (world.nextArchetypeId::number)+ 1
local id = world.nextArchetypeId
local id = world.nextArchetypeId + 1
world.nextArchetypeId = id
local columns = {} :: { any }
local length = #types
local columns = table.create(length) :: {any}
for _ in types do
table.insert(columns, {})
for index in types do
columns[index] = {}
end
local archetype = {
id = id,
types = types,
type = ty,
columns = columns,
entities = {},
edges = {},
records = {},
columns = columns;
edges = {};
entities = {};
id = id;
records = {};
type = ty;
types = types;
}
world.archetypeIndex[ty] = archetype
world.archetypes[id] = archetype
if #types > 0 then
if length > 0 then
createArchetypeRecords(world.componentIndex, archetype, prev)
end
@ -173,17 +178,17 @@ local World = {}
World.__index = World
function World.new()
local self = setmetatable({
entityIndex = {},
componentIndex = {},
archetypes = {},
archetypeIndex = {},
ROOT_ARCHETYPE = (nil :: any) :: Archetype,
nextEntityId = 0,
nextComponentId = 0,
nextArchetypeId = 0,
archetypeIndex = {};
archetypes = {};
componentIndex = {};
entityIndex = {};
hooks = {
[ON_ADD] = {}
}
[ON_ADD] = {};
};
nextArchetypeId = 0;
nextComponentId = 0;
nextEntityId = 0;
ROOT_ARCHETYPE = (nil :: any) :: Archetype;
}, World)
return self
end
@ -192,21 +197,21 @@ local function emit(world, eventDescription)
local event = eventDescription.event
table.insert(world.hooks[event], {
ids = eventDescription.ids,
archetype = eventDescription.archetype,
otherArchetype = eventDescription.otherArchetype,
offset = eventDescription.offset
archetype = eventDescription.archetype;
ids = eventDescription.ids;
offset = eventDescription.offset;
otherArchetype = eventDescription.otherArchetype;
})
end
local function onNotifyAdd(world, archetype, otherArchetype, row: number, added: Ty)
if #added > 0 then
emit(world, {
event = ON_ADD,
ids = added,
archetype = archetype,
otherArchetype = otherArchetype,
offset = row,
archetype = archetype;
event = ON_ADD;
ids = added;
offset = row;
otherArchetype = otherArchetype;
})
end
end
@ -228,9 +233,7 @@ local function ensureArchetype(world: World, types, prev)
end
local function findInsert(types: {i53}, toAdd: i53)
local count = #types
for i = 1, count do
local id = types[i]
for i, id in types do
if id == toAdd then
return -1
end
@ -238,7 +241,7 @@ local function findInsert(types: { i53 }, toAdd: i53)
return i
end
end
return count + 1
return #types + 1
end
local function findArchetypeWith(world: World, node: Archetype, componentId: i53)
@ -259,38 +262,47 @@ local function findArchetypeWith(world: World, node: Archetype, componentId: i53
end
local function ensureEdge(archetype: Archetype, componentId: i53)
if not archetype.edges[componentId] then
archetype.edges[componentId] = {} :: any
local edges = archetype.edges
local edge = edges[componentId]
if not edge then
edge = {} :: any
edges[componentId] = edge
end
return archetype.edges[componentId]
return edge
end
local function archetypeTraverseAdd(world: World, componentId: i53, from: Archetype): Archetype
if not from then
-- If there was no source archetype then it should return the ROOT_ARCHETYPE
if not world.ROOT_ARCHETYPE then
local ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
world.ROOT_ARCHETYPE = ROOT_ARCHETYPE
local ROOT_ARCHETYPE = world.ROOT_ARCHETYPE
if not ROOT_ARCHETYPE then
ROOT_ARCHETYPE = archetypeOf(world, {}, nil)
world.ROOT_ARCHETYPE = ROOT_ARCHETYPE :: never
end
from = world.ROOT_ARCHETYPE
from = ROOT_ARCHETYPE
end
local edge = ensureEdge(from, componentId)
if not edge.add then
local edge = ensureEdge(from, componentId)
local add = edge.add
if not add then
-- Save an edge using the component ID to the archetype to allow
-- faster traversals to adjacent archetypes.
edge.add = findArchetypeWith(world, from, componentId)
add = findArchetypeWith(world, from, componentId)
edge.add = add :: never
end
return edge.add
return add
end
local function ensureRecord(entityIndex, entityId: i53): Record
local id = entityId
if not entityIndex[id] then
entityIndex[id] = {}
local record = entityIndex[entityId]
if not record then
record = {}
entityIndex[entityId] = record
end
return entityIndex[id] :: Record
return record :: Record
end
function World.set(world: World, entityId: i53, componentId: i53, data: unknown)
@ -326,28 +338,30 @@ local function archetypeTraverseRemove(world: World, componentId: i53, archetype
local from = (archetype or world.ROOT_ARCHETYPE) :: Archetype
local edge = ensureEdge(from, componentId)
if not edge.remove then
local remove = edge.remove
if not remove then
local to = table.clone(from.types)
table.remove(to, table.find(to, componentId))
edge.remove = ensureArchetype(world, to, from)
remove = ensureArchetype(world, to, from)
edge.remove = remove :: never
end
return edge.remove
return remove
end
function World.remove(world: World, entityId: i53, componentId: i53)
local record = ensureRecord(world.entityIndex, entityId)
local entityIndex = world.entityIndex
local record = ensureRecord(entityIndex, entityId)
local sourceArchetype = record.archetype
local destinationArchetype = archetypeTraverseRemove(world, componentId, sourceArchetype)
if sourceArchetype and not (sourceArchetype == destinationArchetype) then
moveEntity(world.entityIndex, entityId, record, destinationArchetype)
moveEntity(entityIndex, entityId, record, destinationArchetype)
end
end
-- Keeping the function as small as possible to enable inlining
local function get(componentIndex: { [i24]: ArchetypeMap }, record: Record, componentId: i24)
local function get(record: Record, componentId: i24)
local archetype = record.archetype
local archetypeRecord = archetype.records[componentId]
@ -360,35 +374,35 @@ end
function World.get(world: World, entityId: i53, a: i53, b: i53?, c: i53?, d: i53?, e: i53?)
local id = entityId
local componentIndex = world.componentIndex
local record = world.entityIndex[id]
if not record then
return nil
end
local va = get(componentIndex, record, a)
local va = get(record, a)
if b == nil then
return va
elseif c == nil then
return va, get(componentIndex, record, b)
return va, get(record, b)
elseif d == nil then
return va, get(componentIndex, record, b), get(componentIndex, record, c)
return va, get(record, b), get(record, c)
elseif e == nil then
return va, get(componentIndex, record, b), get(componentIndex, record, c), get(componentIndex, record, d)
return va, get(record, b), get(record, c), get(record, d)
else
error("args exceeded")
end
end
local function noop(self: Query, ...: i53): () -> (number, ...any)
return function()
end :: any
-- the less creation the better
local function actualNoOperation() end
local function noop(_self: Query, ...: i53): () -> (number, ...any)
return actualNoOperation :: any
end
local EmptyQuery = {
__iter = noop,
without = noop
__iter = noop;
without = noop;
}
EmptyQuery.__index = EmptyQuery
setmetatable(EmptyQuery, EmptyQuery)
@ -396,19 +410,22 @@ setmetatable(EmptyQuery, EmptyQuery)
export type Query = typeof(EmptyQuery)
function World.query(world: World, ...: i53): Query
-- breaking?
if (...) == nil then
error("Missing components")
end
local compatibleArchetypes = {}
local length = 0
local components = {...}
local archetypes = world.archetypes
local queryLength = #components
if queryLength == 0 then
error("Missing components")
end
local firstArchetypeMap
local componentIndex = world.componentIndex
for i, componentId in components do
for _, componentId in components do
local map = componentIndex[componentId]
if not map then
return EmptyQuery
@ -431,13 +448,15 @@ function World.query(world: World, ...: i53): Query
skip = true
break
end
indices[i] = archetypeRecords[componentId]
indices[i] = index
end
if skip then
continue
end
table.insert(compatibleArchetypes, { archetype, indices })
length += 1
compatibleArchetypes[length] = {archetype, indices}
end
local lastArchetype, compatibleArchetype = next(compatibleArchetypes)
@ -449,16 +468,19 @@ function World.query(world: World, ...: i53): Query
preparedQuery.__index = preparedQuery
function preparedQuery:without(...)
local components = { ... }
local withoutComponents = {...}
for i = #compatibleArchetypes, 1, -1 do
local archetype = compatibleArchetypes[i][1]
local records = archetype.records
local shouldRemove = false
for _, componentId in components do
if archetype.records[componentId] then
for _, componentId in withoutComponents do
if records[componentId] then
shouldRemove = true
break
end
end
if shouldRemove then
table.remove(compatibleArchetypes, i)
end
@ -475,7 +497,6 @@ function World.query(world: World, ...: i53): Query
local lastRow
local queryOutput = {}
function preparedQuery:__iter()
return function()
local archetype = compatibleArchetype[1]
@ -499,16 +520,9 @@ function World.query(world: World, ...: i53): Query
elseif queryLength == 2 then
return entityId, columns[tr[1]][row], columns[tr[2]][row]
elseif queryLength == 3 then
return entityId,
columns[tr[1]][row],
columns[tr[2]][row],
columns[tr[3]][row]
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row]
elseif queryLength == 4 then
return entityId,
columns[tr[1]][row],
columns[tr[2]][row],
columns[tr[3]][row],
columns[tr[4]][row]
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row]
elseif queryLength == 5 then
return entityId,
columns[tr[1]][row],
@ -568,8 +582,9 @@ function World.component(world: World)
end
function World.entity(world: World)
world.nextEntityId += 1
return world.nextEntityId + REST
local nextEntityId = world.nextEntityId + 1
world.nextEntityId = nextEntityId
return nextEntityId + REST
end
function World.delete(world: World, entityId: i53)
@ -585,11 +600,13 @@ end
function World.observer(world: World, ...)
local componentIds = {...}
local idsCount = #componentIds
local hooks = world.hooks
return {
event = function(event)
local hook = world.hooks[event]
world.hooks[event] = nil
local hook = hooks[event]
hooks[event] = nil
local last, change
return function()
@ -599,10 +616,11 @@ function World.observer(world: World, ...)
end
local matched = false
local ids = change.ids
while not matched do
local skip = false
for _, id in change.ids do
for _, id in ids do
if not table.find(componentIds, id) then
skip = true
break
@ -611,30 +629,31 @@ function World.observer(world: World, ...)
if skip then
last, change = next(hook, last)
ids = change.ids
continue
end
matched = true
end
local queryOutput = {}
local queryOutput = table.create(idsCount)
local row = change.offset
local archetype = change.archetype
local columns = archetype.columns
local archetypeRecords = archetype.records
for _, id in componentIds do
table.insert(queryOutput, columns[archetypeRecords[id]][row])
for index, id in componentIds do
queryOutput[index] = columns[archetypeRecords[id]][row]
end
return archetype.entities[row], unpack(queryOutput, 1, #queryOutput)
end
return archetype.entities[row], unpack(queryOutput, 1, idsCount)
end
end;
}
end
return table.freeze({
World = World,
ON_ADD = ON_ADD,
ON_REMOVE = ON_REMOVE,
ON_SET = ON_SET
World = World;
ON_ADD = ON_ADD;
ON_REMOVE = ON_REMOVE;
ON_SET = ON_SET;
})

4
selene.toml Normal file
View file

@ -0,0 +1,4 @@
std = "roblox"
[lints]
global_usage = "allow"

5
stylua.toml Normal file
View file

@ -0,0 +1,5 @@
column_width = 120
quote_style = "ForceDouble"
[sort_requires]
enabled = true

3
testez-companion.toml Normal file
View file

@ -0,0 +1,3 @@
roots = ["ServerStorage"]
[extraOptions]

View file

@ -10,6 +10,3 @@ include = ["default.project.json", "lib", "wally.toml", "README.md"]
TestEZ = "roblox/testez@0.4.1"
Matter = "matter-ecs/matter@0.8.0"
ecr = "centau/ecr@0.8.0"