Performance and reporting improvements

This commit is contained in:
Michael Freno
2025-11-17 17:41:01 -05:00
parent a8be1f5342
commit 2c04f69daa
18 changed files with 1987 additions and 82 deletions

View File

@@ -12,6 +12,7 @@ local Context = req("Context")
local StateManager = req("StateManager")
local ErrorCodes = req("ErrorCodes")
local ErrorHandler = req("ErrorHandler")
local Performance = req("Performance")
local ImageRenderer = req("ImageRenderer")
local ImageScaler = req("ImageScaler")
local NinePatch = req("NinePatch")
@@ -95,7 +96,7 @@ Color.initializeErrorHandler(ErrorHandler)
utils.initializeErrorHandler(ErrorHandler)
-- Add version and metadata
flexlove._VERSION = "0.2.2"
flexlove._VERSION = "0.2.3"
flexlove._DESCRIPTION = "0I Library for LÖVE Framework based on flexbox"
flexlove._URL = "https://github.com/mikefreno/FlexLove"
flexlove._LICENSE = [[
@@ -122,12 +123,27 @@ flexlove._LICENSE = [[
SOFTWARE.
]]
-- GC (Garbage Collection) configuration
flexlove._gcConfig = {
strategy = "auto", -- "auto", "periodic", "manual", "disabled"
memoryThreshold = 100, -- MB before forcing GC
interval = 60, -- Frames between GC steps (for periodic mode)
stepSize = 200, -- Work units per GC step (higher = more aggressive)
}
flexlove._gcState = {
framesSinceLastGC = 0,
lastMemory = 0,
gcCount = 0,
}
-- Deferred callback queue for operations that cannot run while Canvas is active
flexlove._deferredCallbacks = {}
--- Initialize FlexLove with configuration options, set refence scale for autoscaling on window resize, immediate mode, and error logging / error file path
---@param config {baseScale?: {width?:number, height?:number}, theme?: string|ThemeDefinition, immediateMode?: boolean, stateRetentionFrames?: number, maxStateEntries?: number, autoFrameManagement?: boolean, errorLogFile?: string, enableErrorLogging?: boolean}
---@param config {baseScale?: {width?:number, height?:number}, theme?: string|ThemeDefinition, immediateMode?: boolean, stateRetentionFrames?: number, maxStateEntries?: number, autoFrameManagement?: boolean, errorLogFile?: string, enableErrorLogging?: boolean, performanceMonitoring?: boolean, performanceWarnings?: boolean, performanceHudKey?: string, performanceHudPosition?: {x: number, y: number} }
function flexlove.init(config)
config = config or {}
-- Configure error logging
if config.errorLogFile then
ErrorHandler.setLogTarget("file")
ErrorHandler.setLogFile(config.errorLogFile)
@@ -137,6 +153,43 @@ function flexlove.init(config)
ErrorHandler.setLogFile("flexlove-errors.log")
end
-- Configure performance monitoring (default: true)
local enablePerfMonitoring = config.performanceMonitoring
if enablePerfMonitoring == nil then
enablePerfMonitoring = true
end
if enablePerfMonitoring then
Performance.enable()
else
Performance.disable()
end
local enablePerfWarnings = config.performanceWarnings or true
Performance.setConfig("warningsEnabled", enablePerfWarnings)
if enablePerfWarnings then
Performance.setConfig("logWarnings", true)
end
-- Configure performance HUD toggle key (default: "f3")
if config.performanceHudKey then
Performance.setConfig("hudToggleKey", config.performanceHudKey)
end
-- Configure performance HUD position (default: {x = 10, y = 10})
if config.performanceHudPosition then
Performance.setConfig("hudPosition", config.performanceHudPosition)
end
-- Configure memory profiling (default: false)
if config.memoryProfiling then
Performance.enableMemoryProfiling()
-- Register key tables for leak detection
Performance.registerTableForMonitoring("StateManager.stateStore", StateManager._getInternalState().stateStore)
Performance.registerTableForMonitoring("StateManager.stateMetadata", StateManager._getInternalState().stateMetadata)
Performance.registerTableForMonitoring("FONT_CACHE", utils.FONT_CACHE)
end
if config.baseScale then
flexlove.baseScale = {
width = config.baseScale.width or 1920,
@@ -171,6 +224,20 @@ function flexlove.init(config)
flexlove._autoFrameManagement = config.autoFrameManagement or false
-- Configure GC strategy
if config.gcStrategy then
flexlove._gcConfig.strategy = config.gcStrategy
end
if config.gcMemoryThreshold then
flexlove._gcConfig.memoryThreshold = config.gcMemoryThreshold
end
if config.gcInterval then
flexlove._gcConfig.interval = config.gcInterval
end
if config.gcStepSize then
flexlove._gcConfig.stepSize = config.gcStepSize
end
if config.stateRetentionFrames or config.maxStateEntries then
StateManager.configure({
stateRetentionFrames = config.stateRetentionFrames,
@@ -179,6 +246,45 @@ function flexlove.init(config)
end
end
--- Queue a callback to be executed after the current frame's canvas operations complete
--- This is necessary for operations that cannot run while a Canvas is active (e.g., love.window.setMode)
---@param callback function The callback to execute
function flexlove.deferCallback(callback)
if type(callback) ~= "function" then
ErrorHandler.warn("FlexLove", "deferCallback expects a function")
return
end
table.insert(flexlove._deferredCallbacks, callback)
end
--- Execute all deferred callbacks and clear the queue
--- IMPORTANT: This MUST be called at the very end of love.draw() after ALL canvases
--- have been released, including any canvases created by the application (not just FlexLove's canvases)
--- @usage
--- function love.draw()
--- love.graphics.setCanvas(myCanvas)
--- FlexLove.draw()
--- love.graphics.setCanvas() -- Release ALL canvases
--- FlexLove.executeDeferredCallbacks() -- Now safe to execute
--- end
function flexlove.executeDeferredCallbacks()
if #flexlove._deferredCallbacks == 0 then
return
end
-- Copy callbacks and clear queue before execution
-- This prevents infinite loops if callbacks defer more callbacks
local callbacks = flexlove._deferredCallbacks
flexlove._deferredCallbacks = {}
for _, callback in ipairs(callbacks) do
local success, err = xpcall(callback, debug.traceback)
if not success then
ErrorHandler.warn("FlexLove", string.format("Deferred callback failed: %s", tostring(err)))
end
end
end
function flexlove.resize()
local newWidth, newHeight = love.window.getMode()
@@ -189,6 +295,14 @@ function flexlove.resize()
Blur.clearCache()
-- Release old canvases explicitly
if flexlove._gameCanvas then
flexlove._gameCanvas:release()
end
if flexlove._backdropCanvas then
flexlove._backdropCanvas:release()
end
flexlove._gameCanvas = nil
flexlove._backdropCanvas = nil
flexlove._canvasDimensions = { width = 0, height = 0 }
@@ -230,6 +344,9 @@ function flexlove.beginFrame()
return
end
-- Start performance frame timing
Performance.startFrame()
flexlove._frameNumber = flexlove._frameNumber + 1
StateManager.incrementFrame()
flexlove._currentFrameElements = {}
@@ -301,6 +418,10 @@ function flexlove.endFrame()
StateManager.cleanup()
StateManager.forceCleanupIfNeeded()
flexlove._frameStarted = false
-- End performance frame timing
Performance.endFrame()
Performance.resetFrameCounters()
end
flexlove._gameCanvas = nil
@@ -322,6 +443,14 @@ function flexlove.draw(gameDrawFunc, postDrawFunc)
local width, height = love.graphics.getDimensions()
if not flexlove._gameCanvas or flexlove._canvasDimensions.width ~= width or flexlove._canvasDimensions.height ~= height then
-- Release old canvases before creating new ones
if flexlove._gameCanvas then
flexlove._gameCanvas:release()
end
if flexlove._backdropCanvas then
flexlove._backdropCanvas:release()
end
flexlove._gameCanvas = love.graphics.newCanvas(width, height)
flexlove._backdropCanvas = love.graphics.newCanvas(width, height)
flexlove._canvasDimensions.width = width
@@ -404,7 +533,15 @@ function flexlove.draw(gameDrawFunc, postDrawFunc)
postDrawFunc()
end
-- Render performance HUD if enabled
Performance.renderHUD()
love.graphics.setCanvas(outerCanvas)
-- NOTE: Deferred callbacks are NOT executed here because the calling code
-- (e.g., main.lua) might still have a canvas active. Callbacks must be
-- executed by calling FlexLove.executeDeferredCallbacks() at the very end
-- of love.draw() after ALL canvases have been released.
end
---@param element Element
@@ -519,6 +656,12 @@ end
---@param dt number
function flexlove.update(dt)
-- Update Performance module with actual delta time for accurate FPS
Performance.updateDeltaTime(dt)
-- Garbage collection management
flexlove._manageGC()
local mx, my = love.mouse.getPosition()
local topElement = flexlove.getElementAtPosition(mx, my)
@@ -551,6 +694,86 @@ function flexlove.update(dt)
end
end
--- Internal GC management function (called from update)
function flexlove._manageGC()
local strategy = flexlove._gcConfig.strategy
if strategy == "disabled" then
return
end
local currentMemory = collectgarbage("count") / 1024 -- Convert to MB
flexlove._gcState.lastMemory = currentMemory
flexlove._gcState.framesSinceLastGC = flexlove._gcState.framesSinceLastGC + 1
-- Check memory threshold (applies to all strategies except disabled)
if currentMemory > flexlove._gcConfig.memoryThreshold then
-- Force full GC when exceeding threshold
collectgarbage("collect")
flexlove._gcState.gcCount = flexlove._gcState.gcCount + 1
flexlove._gcState.framesSinceLastGC = 0
return
end
-- Strategy-specific GC
if strategy == "periodic" then
-- Run incremental GC step every N frames
if flexlove._gcState.framesSinceLastGC >= flexlove._gcConfig.interval then
collectgarbage("step", flexlove._gcConfig.stepSize)
flexlove._gcState.gcCount = flexlove._gcState.gcCount + 1
flexlove._gcState.framesSinceLastGC = 0
end
elseif strategy == "auto" then
-- Let Lua's automatic GC handle it, but help with incremental steps
-- Run a small step every frame to keep memory under control
if flexlove._gcState.framesSinceLastGC >= 5 then
collectgarbage("step", 50) -- Small steps to avoid frame drops
flexlove._gcState.framesSinceLastGC = 0
end
end
-- "manual" strategy: no automatic GC, user must call flexlove.collectGarbage()
end
--- Manual garbage collection control
---@param mode? string "collect" for full GC, "step" for incremental (default: "collect")
---@param stepSize? number Work units for step mode (default: 200)
function flexlove.collectGarbage(mode, stepSize)
mode = mode or "collect"
stepSize = stepSize or 200
if mode == "collect" then
collectgarbage("collect")
flexlove._gcState.gcCount = flexlove._gcState.gcCount + 1
flexlove._gcState.framesSinceLastGC = 0
elseif mode == "step" then
collectgarbage("step", stepSize)
elseif mode == "count" then
return collectgarbage("count") / 1024 -- Return memory in MB
end
end
--- Set GC strategy
---@param strategy string "auto", "periodic", "manual", or "disabled"
function flexlove.setGCStrategy(strategy)
if strategy == "auto" or strategy == "periodic" or strategy == "manual" or strategy == "disabled" then
flexlove._gcConfig.strategy = strategy
else
ErrorHandler.warn("FlexLove", "Invalid GC strategy: " .. tostring(strategy))
end
end
--- Get GC statistics
---@return table stats {gcCount, framesSinceLastGC, currentMemoryMB, strategy}
function flexlove.getGCStats()
return {
gcCount = flexlove._gcState.gcCount,
framesSinceLastGC = flexlove._gcState.framesSinceLastGC,
currentMemoryMB = flexlove._gcState.lastMemory,
strategy = flexlove._gcConfig.strategy,
threshold = flexlove._gcConfig.memoryThreshold,
}
end
---@param text string
function flexlove.textinput(text)
if flexlove._focusedElement then
@@ -562,6 +785,8 @@ end
---@param scancode string
---@param isrepeat boolean
function flexlove.keypressed(key, scancode, isrepeat)
-- Handle performance HUD toggle
Performance.keypressed(key)
if flexlove._focusedElement then
flexlove._focusedElement:keypressed(key, scancode, isrepeat)
end
@@ -702,6 +927,15 @@ function flexlove.destroy()
flexlove.baseScale = nil
flexlove.scaleFactors = { x = 1.0, y = 1.0 }
flexlove._cachedViewport = { width = 0, height = 0 }
-- Release canvases explicitly before destroying
if flexlove._gameCanvas then
flexlove._gameCanvas:release()
end
if flexlove._backdropCanvas then
flexlove._backdropCanvas:release()
end
flexlove._gameCanvas = nil
flexlove._backdropCanvas = nil
flexlove._canvasDimensions = { width = 0, height = 0 }