From 7cd6bcc090c766a3e3b19e5eb98eabda613c94eb Mon Sep 17 00:00:00 2001 From: Sage Vaillancourt Date: Fri, 21 Mar 2025 16:40:47 -0400 Subject: [PATCH] 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. --- lib/tiny.lua | 45 ++------------------ tiny-debug.lua | 109 ++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 102 insertions(+), 52 deletions(-) diff --git a/lib/tiny.lua b/lib/tiny.lua index 290f938..cbf602f 100644 --- a/lib/tiny.lua +++ b/lib/tiny.lua @@ -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 diff --git a/tiny-debug.lua b/tiny-debug.lua index c1c0956..0b4ef8c 100644 --- a/tiny-debug.lua +++ b/tiny-debug.lua @@ -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