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

View File

@ -1,21 +1,58 @@
tinyTrackEntityAges = false
tinyLogSystemUpdateTime = false
tinyLogSystemChanges = false
tinyWarnWhenNonDataOnEntities = false
local tiny = require("lib.tiny")
require("lib.inspect")
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
end
ENTITY_INIT_MS = { "ENTITY_INIT_MS" }
if tinyTrackEntityAges then
function tinyGetEntityAgeMs(entity)
return entity[ENTITY_INIT_MS]
--- Only applies to already-created systems!
--- Performs a `world:refresh()` to ensure that all added systems are installed.
---@param world World
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
if tinyWarnWhenNonDataOnEntities then
function checkForNonData(e, nested, tableCache)
function tinyDebug.trackEntityAges()
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
tableCache = tableCache or {}
@ -39,4 +76,54 @@ if tinyWarnWhenNonDataOnEntities then
return valType
end
end
tiny._wrapAddEntity(function(_, entity)
local nonDataType = checkForNonData(entity)
if nonDataType then
print("Detected non-data type '" .. nonDataType .. "' on entity")
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