Extract invasive debugging from tiny

Debugging now uses more injection-based techniques. It may be slightly slower when enabled, but will have slightly *less* impact when disabled.
In fact, the only debugging-specific code now in tiny proper should be tiny._wrapAddEntity(), which is never called within the library itself.
This commit is contained in:
Sage Vaillancourt 2025-03-21 16:40:47 -04:00
parent 69bf5b60aa
commit 7cd6bcc090
2 changed files with 102 additions and 52 deletions

View File

@ -478,8 +478,6 @@ end
--- Adds an Entity to the world. --- Adds an Entity to the world.
-- Also call this on Entities that have changed Components such that they -- Also call this on Entities that have changed Components such that they
-- match different Filters. Returns the Entity. -- match different Filters. Returns the Entity.
-- TODO: Track entity age when debugging?
-- TODO: Track debugName field when debugging?
function tiny.addEntity(world, entity) function tiny.addEntity(world, entity)
local e2c = world.entitiesToChange local e2c = world.entitiesToChange
e2c[#e2c + 1] = entity e2c[#e2c + 1] = entity
@ -487,37 +485,18 @@ function tiny.addEntity(world, entity)
end end
tiny_addEntity = tiny.addEntity tiny_addEntity = tiny.addEntity
if tinyTrackEntityAges then function tiny._wrapAddEntity(f)
local wrapped = tiny.addEntity local wrapped = tiny.addEntity
function tiny.addEntity(world, entity) function tiny.addEntity(world, entity)
local added = wrapped(world, entity) local ret = wrapped(world, entity)
added[ENTITY_INIT_MS] = getCurrentTimeMilliseconds() f(world, entity)
return added return ret
end
tiny_addEntity = tiny.addEntity
end
if tinyWarnWhenNonDataOnEntities then
local wrapped = tiny.addEntity
function tiny.addEntity(world, entity)
local added = wrapped(world, entity)
local nonDataType = checkForNonData(added)
if nonDataType then
print("Detected non-data type '" .. nonDataType .. "' on entity")
end
return added
end end
tiny_addEntity = tiny.addEntity tiny_addEntity = tiny.addEntity
end end
--- Adds a System to the world. Returns the System. --- Adds a System to the world. Returns the System.
function tiny.addSystem(world, system) function tiny.addSystem(world, system)
if tinyLogSystemChanges then
print("addSystem '" .. (system.name or "unnamed") .. "'")
end
if system.world ~= nil then
error("System " .. system.name .. " already belongs to a World.")
end
local s2a = world.systemsToAdd local s2a = world.systemsToAdd
s2a[#s2a + 1] = system s2a[#s2a + 1] = system
system.world = world system.world = world
@ -552,9 +531,6 @@ tiny_removeEntity = tiny.removeEntity
--- Removes a System from the world. Returns the System. --- Removes a System from the world. Returns the System.
function tiny.removeSystem(world, system) function tiny.removeSystem(world, system)
if tinyLogSystemChanges then
print("removeSystem '" .. (system.name or "unnamed") .. "'")
end
if system.world ~= world then if system.world ~= world then
error("System " .. system.name .. " does not belong to this World.") error("System " .. system.name .. " does not belong to this World.")
end end
@ -616,10 +592,6 @@ function tiny_manageSystems(world)
end end
s2r[i] = nil s2r[i] = nil
-- Clean up System
if tinyLogSystemChanges then
print("Cleaning up system '" .. (system.name or "unnamed") .. "'")
end
system.world = nil system.world = nil
system.entities = nil system.entities = nil
system.indices = nil system.indices = nil
@ -814,7 +786,6 @@ function tiny.update(world, dt, filter)
end end
end end
local tinyLogSystemUpdateTime = tinyLogSystemUpdateTime
-- Iterate through Systems IN ORDER -- Iterate through Systems IN ORDER
for i = 1, #systems do for i = 1, #systems do
local system = systems[i] local system = systems[i]
@ -822,7 +793,6 @@ function tiny.update(world, dt, filter)
-- Update Systems that have an update method (most Systems) -- Update Systems that have an update method (most Systems)
local update = system.update local update = system.update
if update then if update then
local currentMs = tinyLogSystemUpdateTime and getCurrentTimeMilliseconds()
local interval = system.interval local interval = system.interval
if interval then if interval then
local bufferedTime = (system.bufferedTime or 0) + dt local bufferedTime = (system.bufferedTime or 0) + dt
@ -834,18 +804,11 @@ function tiny.update(world, dt, filter)
else else
update(system, dt) update(system, dt)
end end
if tinyLogSystemUpdateTime then
local endTimeMs = getCurrentTimeMilliseconds()
print(tostring(endTimeMs - currentMs) .. "ms taken to update system '" .. system.name .. "'")
end
end end
system.modified = false system.modified = false
end end
end end
if tinyLogSystemUpdateTime then
print("")
end
-- Iterate through Systems IN ORDER AGAIN -- Iterate through Systems IN ORDER AGAIN
for i = 1, #systems do for i = 1, #systems do

View File

@ -1,21 +1,58 @@
tinyTrackEntityAges = false local tiny = require("lib.tiny")
tinyLogSystemUpdateTime = false require("lib.inspect")
tinyLogSystemChanges = false
tinyWarnWhenNonDataOnEntities = false
getCurrentTimeMilliseconds = function() ---@class TinyDebug
local tinyDebug = {}
-- TODO: Track some debugName field when debugging?
local function worldMetaIndex()
local worldMetaTable = getmetatable(tiny.world())
return worldMetaTable.__index
end
local function getCurrentTimeMilliseconds()
return love.timer.getTime() * 1000 return love.timer.getTime() * 1000
end end
ENTITY_INIT_MS = { "ENTITY_INIT_MS" } --- Only applies to already-created systems!
if tinyTrackEntityAges then --- Performs a `world:refresh()` to ensure that all added systems are installed.
function tinyGetEntityAgeMs(entity) ---@param world World
return entity[ENTITY_INIT_MS] function tinyDebug.logSystemUpdateTime(world)
world:refresh()
local systems = world.systems
for i = 1, #systems do
local system = systems[i]
local wrappedUpdate = system.update
system.update = function(...)
local currentMs = getCurrentTimeMilliseconds()
local ret = wrappedUpdate(...)
local endTimeMs = getCurrentTimeMilliseconds()
print(tostring(endTimeMs - currentMs) .. "ms taken to update system '" .. (system.name or i) .. "'")
return ret
end
end end
end end
if tinyWarnWhenNonDataOnEntities then function tinyDebug.trackEntityAges()
function checkForNonData(e, nested, tableCache) ENTITY_INIT_MS = { "ENTITY_INIT_MS" }
-- tiny._trackEntityAges = true
tiny._wrapAddEntity(function(_, entity)
entity[ENTITY_INIT_MS] = getCurrentTimeMilliseconds()
end)
end
--- Throws an error if trackEntityAges() has not been called
function tinyDebug.getEntityAgeMs(entity)
if ENTITY_INIT_MS == nil then
error("trackEntityAges() has not been enabled!")
end
return entity[ENTITY_INIT_MS]
end
function tinyDebug.warnWhenNonDataOnEntities()
local function checkForNonData(e, nested, tableCache)
nested = nested or false nested = nested or false
tableCache = tableCache or {} tableCache = tableCache or {}
@ -39,4 +76,54 @@ if tinyWarnWhenNonDataOnEntities then
return valType return valType
end end
end end
tiny._wrapAddEntity(function(_, entity)
local nonDataType = checkForNonData(entity)
if nonDataType then
print("Detected non-data type '" .. nonDataType .. "' on entity")
end
end)
end end
function tinyDebug.throwOnUnusedEntities()
---@return boolean
local function entityMatchesASystem(world, entity)
local systems = world.systems
for i = 1, #systems do
local system = systems[i]
local filter = system.filter
if filter and filter(system, entity) then
return true
end
end
return false
end
---@return Entity[]
local function anyUnmatchedEntities(world)
local unmatchedEntities = {}
local el = world.entities
for i = 1, #el do
if not entityMatchesASystem(world, el[i]) then
unmatchedEntities[#unmatchedEntities + 1] = el[i]
end
end
return unmatchedEntities
end
local tinyUpdate = tiny.update
worldMetaIndex().update = function(world, dt, filter)
local ret = tinyUpdate(world, dt, filter)
local ecAll = world:getEntityCount()
local unmatchedEntities = anyUnmatchedEntities(world)
if #unmatchedEntities ~= 0 then
error("UNUSED ENTITY COUNT: " .. #unmatchedEntities .. "/" .. ecAll .. " " .. Inspect(unmatchedEntities, { depth = 2 }))
end
return ret
end
end
return tinyDebug