Less memory footprint

This commit is contained in:
Ukendio 2024-07-14 05:38:44 +02:00
parent 8bea43a9fc
commit 85a970e9ff
5 changed files with 1089 additions and 579 deletions

View file

@ -1,6 +1,7 @@
{ {
"aliases": { "aliases": {
"jecs": "src", "jecs": "src",
"testkit": "testkit" "testkit": "testkit",
"mirror": "mirror"
} }
} }

View file

@ -5,14 +5,14 @@
"StarterPlayer": { "StarterPlayer": {
"$className": "StarterPlayer", "$className": "StarterPlayer",
"StarterPlayerScripts": { "StarterPlayerScripts": {
"$className": "StarterPlayerScripts", "$className": "StarterPlayerScripts",
"$path": "tests" "$path": "tests"
} }
}, },
"ReplicatedStorage": { "ReplicatedStorage": {
"$className": "ReplicatedStorage", "$className": "ReplicatedStorage",
"Lib": { "Lib": {
"$path": "lib" "$path": "src"
}, },
"rgb": { "rgb": {
"$path": "rgb.luau" "$path": "rgb.luau"
@ -28,4 +28,4 @@
} }
} }
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -688,234 +688,226 @@ local function replaceMult(row, columns, ...)
end end
end end
local function preparedQuery(compatibleArchetypes: { Archetype }, local query: (World, ...i53) -> Query
components: { i53? }, indices: { { number } }) do
local indices: { { number } }
local compatibleArchetypes: { Archetype }
local length
local components: { number }
local queryLength: number
local lastArchetype: number
local archetype: Archetype
local queryLength = #components local queryOutput: { any }
local lastArchetype = 1 local entities: {}
local archetype: Archetype = compatibleArchetypes[lastArchetype] local i: number
if not archetype then local function query_next()
return EmptyQuery local entityId = entities[i]
end while entityId == nil do
lastArchetype += 1
archetype = compatibleArchetypes[lastArchetype]
local queryOutput = {} if not archetype then
return
end
local entities = archetype.entities entities = archetype.entities
local i = #entities i = #entities
entityId = entities[i]
end
local function queryNext(): ...any local row = i
local entityId = entities[i] i-=1
while entityId == nil do
lastArchetype += 1
archetype = compatibleArchetypes[lastArchetype]
if not archetype then local columns = archetype.columns
return local tr = indices[lastArchetype]
end
entities = archetype.entities if queryLength == 1 then
i = #entities return entityId, columns[tr[1]][row]
entityId = entities[i] elseif queryLength == 2 then
end 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]
elseif queryLength == 4 then
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], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row],
columns[tr[5]][row]
elseif queryLength == 6 then
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row],
columns[tr[5]][row],
columns[tr[6]][row]
elseif queryLength == 7 then
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row],
columns[tr[5]][row],
columns[tr[6]][row],
columns[tr[7]][row]
elseif queryLength == 8 then
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row], columns[tr[4]][row],
columns[tr[5]][row],
columns[tr[6]][row],
columns[tr[7]][row],
columns[tr[8]][row]
end
local row = i for i in components do
i-=1 queryOutput[i] = columns[tr[i]][row]
end
local columns = archetype.columns return entityId, unpack(queryOutput, 1, queryLength)
local tr = indices[lastArchetype] end
if queryLength == 1 then local function query_without(self, ...): Query
return entityId, columns[tr[1]][row] local withoutComponents = { ... }
elseif queryLength == 2 then for i = #compatibleArchetypes, 1, -1 do
return entityId, columns[tr[1]][row], columns[tr[2]][row] local archetype = compatibleArchetypes[i]
elseif queryLength == 3 then local records = archetype.records
return entityId, columns[tr[1]][row], columns[tr[2]][row], columns[tr[3]][row] local shouldRemove = false
elseif queryLength == 4 then
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],
columns[tr[2]][row],
columns[tr[3]][row],
columns[tr[4]][row],
columns[tr[5]][row]
elseif queryLength == 6 then
return entityId,
columns[tr[1]][row],
columns[tr[2]][row],
columns[tr[3]][row],
columns[tr[4]][row],
columns[tr[5]][row],
columns[tr[6]][row]
elseif queryLength == 7 then
return entityId,
columns[tr[1]][row],
columns[tr[2]][row],
columns[tr[3]][row],
columns[tr[4]][row],
columns[tr[5]][row],
columns[tr[6]][row],
columns[tr[7]][row]
elseif queryLength == 8 then
return entityId,
columns[tr[1]][row],
columns[tr[2]][row],
columns[tr[3]][row],
columns[tr[4]][row],
columns[tr[5]][row],
columns[tr[6]][row],
columns[tr[7]][row],
columns[tr[8]][row]
end
for i in components do for _, componentId in withoutComponents do
queryOutput[i] = columns[tr[i]][row] if records[componentId] then
end shouldRemove = true
break
end
end
return entityId, unpack(queryOutput, 1, queryLength) if shouldRemove then
end table.remove(compatibleArchetypes, i)
end
end
local function without(self, ...): Query if #compatibleArchetypes == 0 then
local withoutComponents = { ... } return EmptyQuery
for i = #compatibleArchetypes, 1, -1 do end
local archetype = compatibleArchetypes[i]
local records = archetype.records
local shouldRemove = false
for _, componentId in withoutComponents do return self
if records[componentId] then end
shouldRemove = true
break
end
end
if shouldRemove then local function query_iter()
table.remove(compatibleArchetypes, i)
end
end
if #compatibleArchetypes == 0 then
return EmptyQuery
end
return self
end
local function iter()
lastArchetype = 1 lastArchetype = 1
archetype = compatibleArchetypes[1] archetype = compatibleArchetypes[1]
entities = archetype.entities entities = archetype.entities
i = #entities i = #entities
return queryNext return query_next
end end
local function replace(_, fn: any) local function query_replace(_, fn: any)
for i, archetype in compatibleArchetypes do for i, archetype in compatibleArchetypes do
local tr = indices[i] local tr = indices[i]
local columns = archetype.columns local columns = archetype.columns
for row in archetype.entities do for row in archetype.entities do
if queryLength == 1 then if queryLength == 1 then
local a = columns[tr[1]] local a = columns[tr[1]]
local pa = fn(a[row]) local pa = fn(a[row])
a[row] = pa a[row] = pa
elseif queryLength == 2 then elseif queryLength == 2 then
local a = columns[tr[1]] local a = columns[tr[1]]
local b = columns[tr[2]] local b = columns[tr[2]]
a[row], b[row] = fn(a[row], b[row]) a[row], b[row] = fn(a[row], b[row])
elseif queryLength == 3 then elseif queryLength == 3 then
local a = columns[tr[1]] local a = columns[tr[1]]
local b = columns[tr[2]] local b = columns[tr[2]]
local c = columns[tr[3]] local c = columns[tr[3]]
a[row], b[row], c[row] = fn(a[row], b[row], c[row]) a[row], b[row], c[row] = fn(a[row], b[row], c[row])
elseif queryLength == 4 then elseif queryLength == 4 then
local a = columns[tr[1]] local a = columns[tr[1]]
local b = columns[tr[2]] local b = columns[tr[2]]
local c = columns[tr[3]] local c = columns[tr[3]]
local d = columns[tr[4]] local d = columns[tr[4]]
a[row], b[row], c[row], d[row] = fn( a[row], b[row], c[row], d[row] = fn(
a[row], b[row], c[row], d[row]) a[row], b[row], c[row], d[row])
else else
for i = 1, queryLength do for i = 1, queryLength do
queryOutput[i] = columns[tr[i]][row] queryOutput[i] = columns[tr[i]][row]
end end
replaceMult(row, columns, fn(unpack(queryOutput))) replaceMult(row, columns, fn(unpack(queryOutput)))
end end
end end
end end
end end
local it = { function query(world: World, ...: number): Query
__iter = iter, -- breaking?
next = queryNext, if (...) == nil then
without = without, error("Missing components")
replace = replace end
}
return setmetatable(it, it) :: any indices = {}
end compatibleArchetypes = {}
length = 0
components = { ... }
local function query(world: World, ...: number): Query local archetypes: { Archetype } = world.archetypes :: any
-- breaking? local firstArchetypeMap: ArchetypeMap
if (...) == nil then local componentIndex = world.componentIndex
error("Missing components")
end
local indices: { { number } } = {} for _, componentId in components do
local compatibleArchetypes: { Archetype } = {} local map: ArchetypeMap = componentIndex[componentId] :: any
local length = 0 if not map then
return EmptyQuery
end
local components: { number } = { ... } if (firstArchetypeMap :: any) == nil or firstArchetypeMap.size < map.size then
local archetypes: { Archetype } = world.archetypes :: any firstArchetypeMap = map
end
end
local firstArchetypeMap: ArchetypeMap for id in firstArchetypeMap.cache do
local componentIndex = world.componentIndex local compatibleArchetype = archetypes[id]
local archetypeRecords = compatibleArchetype.records
for _, componentId in components do local records: { number } = {}
local map: ArchetypeMap = componentIndex[componentId] :: any local skip = false
if not map then
return EmptyQuery
end
if (firstArchetypeMap :: any) == nil or firstArchetypeMap.size < map.size then for i, componentId in components do
firstArchetypeMap = map local index = archetypeRecords[componentId]
end if not index then
end skip = true
break
end
-- index should be index.offset
records[i] = index
end
for id in firstArchetypeMap.cache do if skip then
local archetype = archetypes[id] continue
local archetypeRecords = archetype.records end
local records: { number } = {} length += 1
local skip = false compatibleArchetypes[length] = compatibleArchetype
indices[length] = records
end
for i, componentId in components do lastArchetype = 1
local index = archetypeRecords[componentId] archetype = compatibleArchetypes[lastArchetype]
if not index then
skip = true
break
end
-- index should be index.offset
records[i] = index
end
if skip then if not archetype then
continue return EmptyQuery
end end
length += 1 queryOutput = {}
compatibleArchetypes[length] = archetype queryLength = #components
indices[length] = records
end
return preparedQuery(compatibleArchetypes, components, indices) entities = archetype.entities
i = #entities
local it = {
__iter = query_iter,
next = query_next,
without = query_without,
replace = query_replace
}
return setmetatable(it, it) :: any
end
end end
type WorldIterator = (() -> (i53, { [unknown]: unknown? })) & (() -> ()) & (() -> i53) type WorldIterator = (() -> (i53, { [unknown]: unknown? })) & (() -> ()) & (() -> i53)
@ -1055,31 +1047,6 @@ export type WorldShim = typeof(setmetatable(
local World = {} local World = {}
World.__index = World World.__index = World
function World.new()
local self = setmetatable({
archetypeIndex = {} :: { [string]: Archetype },
archetypes = {} :: Archetypes,
componentIndex = {} :: ComponentIndex,
entityIndex = {
dense = {} :: { [i24]: i53 },
sparse = {} :: { [i53]: Record },
} :: EntityIndex,
hooks = {
[EcsOnAdd] = {},
},
nextArchetypeId = 0,
nextComponentId = 0,
nextEntityId = 0,
ROOT_ARCHETYPE = (nil :: any) :: Archetype,
}, World)
self.ROOT_ARCHETYPE = archetypeOf(self, {})
-- Initialize built-in components
nextEntityId(self.entityIndex, EcsChildOf)
return self
end
World.entity = entity World.entity = entity
World.query = query World.query = query
World.remove = remove World.remove = remove
@ -1092,8 +1059,34 @@ World.get = get
World.target = target World.target = target
World.parent = parent World.parent = parent
function World.new()
local self = setmetatable({
archetypeIndex = {} :: { [string]: Archetype },
archetypes = {} :: Archetypes,
componentIndex = {} :: ComponentIndex,
entityIndex = {
dense = {} :: { [i24]: i53 },
sparse = {} :: { [i53]: Record },
} :: EntityIndex,
hooks = {
[EcsOnAdd] = {},
},
nextArchetypeId = 0,
nextComponentId = 0,
nextEntityId = 0,
ROOT_ARCHETYPE = (nil :: any) :: Archetype,
}, World)
self.ROOT_ARCHETYPE = archetypeOf(self, {})
-- Initialize built-in components
nextEntityId(self.entityIndex, EcsChildOf)
return self
end
return { return {
World = World :: { new: () -> WorldShim }, World = World :: { new: () -> WorldShim } ,
OnAdd = EcsOnAdd :: Entity, OnAdd = EcsOnAdd :: Entity,
OnRemove = EcsOnRemove :: Entity, OnRemove = EcsOnRemove :: Entity,

56
test/leaky.luau Normal file
View file

@ -0,0 +1,56 @@
local function calculateAverage(times)
local sum = 0
for _, time in ipairs(times) do
sum = sum + time
end
return sum / #times
end
-- Main logic to time the test function
local CASES = {
jecs = function(world, ...)
for i = 1, 100 do
local q = world:query(...)
for _ in q do end
end
end,
mirror = function(world, ...)
for i = 1, 100 do
local q = world:query(...)
for _ in q do end
end
end
}
for name, fn in CASES do
local times = {}
local allocations = {}
local ecs = require("@"..name)
local world = ecs.World.new()
local A, B, C = world:component(), world:component(), world:component()
for i = 1, 5 do
local e = world:entity()
world:add(e, A)
world:add(e, B)
world:add(e, C)
end
collectgarbage("collect")
local count = collectgarbage("count")
for i = 1, 50000 do
local startTime = os.clock()
fn(world, A, B, C)
local allocated = collectgarbage("count")
collectgarbage("collect")
local endTime = os.clock()
table.insert(times, endTime - startTime)
table.insert(allocations, allocated)
end
print(name, "gc cycle time", calculateAverage(times))
print(name, "memory allocated", calculateAverage(allocations))
end