436 lines
11 KiB
Lua
436 lines
11 KiB
Lua
--- Performance monitoring module for FlexLove
|
|
--- Provides timing, profiling, and performance metrics
|
|
---@class Performance
|
|
local Performance = {}
|
|
|
|
-- Configuration
|
|
local config = {
|
|
enabled = false,
|
|
hudEnabled = false,
|
|
hudToggleKey = "f3",
|
|
warningThresholdMs = 13.0, -- Yellow warning
|
|
criticalThresholdMs = 16.67, -- Red warning (60 FPS)
|
|
logToConsole = false,
|
|
logWarnings = true,
|
|
}
|
|
|
|
-- State
|
|
local timers = {} -- Active timers {name -> startTime}
|
|
local metrics = {} -- Accumulated metrics {name -> {total, count, min, max}}
|
|
local frameMetrics = {
|
|
frameCount = 0,
|
|
totalTime = 0,
|
|
lastFrameTime = 0,
|
|
minFrameTime = math.huge,
|
|
maxFrameTime = 0,
|
|
fps = 0,
|
|
lastFpsUpdate = 0,
|
|
fpsUpdateInterval = 0.5, -- Update FPS every 0.5s
|
|
}
|
|
local memoryMetrics = {
|
|
current = 0,
|
|
peak = 0,
|
|
gcCount = 0,
|
|
lastGcCheck = 0,
|
|
}
|
|
local warnings = {}
|
|
local lastFrameStart = nil
|
|
|
|
--- Initialize performance monitoring
|
|
--- @param options table? Optional configuration overrides
|
|
function Performance.init(options)
|
|
if options then
|
|
for k, v in pairs(options) do
|
|
config[k] = v
|
|
end
|
|
end
|
|
Performance.reset()
|
|
end
|
|
|
|
--- Enable performance monitoring
|
|
function Performance.enable()
|
|
config.enabled = true
|
|
end
|
|
|
|
--- Disable performance monitoring
|
|
function Performance.disable()
|
|
config.enabled = false
|
|
end
|
|
|
|
--- Check if performance monitoring is enabled
|
|
--- @return boolean
|
|
function Performance.isEnabled()
|
|
return config.enabled
|
|
end
|
|
|
|
--- Toggle performance HUD
|
|
function Performance.toggleHUD()
|
|
config.hudEnabled = not config.hudEnabled
|
|
end
|
|
|
|
--- Reset all metrics
|
|
function Performance.reset()
|
|
timers = {}
|
|
metrics = {}
|
|
warnings = {}
|
|
frameMetrics.frameCount = 0
|
|
frameMetrics.totalTime = 0
|
|
frameMetrics.lastFrameTime = 0
|
|
frameMetrics.minFrameTime = math.huge
|
|
frameMetrics.maxFrameTime = 0
|
|
memoryMetrics.current = 0
|
|
memoryMetrics.peak = 0
|
|
memoryMetrics.gcCount = 0
|
|
end
|
|
|
|
--- Start a named timer
|
|
--- @param name string Timer name
|
|
function Performance.startTimer(name)
|
|
if not config.enabled then
|
|
return
|
|
end
|
|
timers[name] = love.timer.getTime()
|
|
end
|
|
|
|
--- Stop a named timer and record the elapsed time
|
|
--- @param name string Timer name
|
|
--- @return number? elapsedMs Elapsed time in milliseconds, or nil if timer not found
|
|
function Performance.stopTimer(name)
|
|
if not config.enabled then
|
|
return nil
|
|
end
|
|
|
|
local startTime = timers[name]
|
|
if not startTime then
|
|
if config.logWarnings then
|
|
print(string.format("[Performance] Warning: Timer '%s' was not started", name))
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local elapsed = (love.timer.getTime() - startTime) * 1000 -- Convert to ms
|
|
timers[name] = nil
|
|
|
|
-- Update metrics
|
|
if not metrics[name] then
|
|
metrics[name] = {
|
|
total = 0,
|
|
count = 0,
|
|
min = math.huge,
|
|
max = 0,
|
|
average = 0,
|
|
}
|
|
end
|
|
|
|
local m = metrics[name]
|
|
m.total = m.total + elapsed
|
|
m.count = m.count + 1
|
|
m.min = math.min(m.min, elapsed)
|
|
m.max = math.max(m.max, elapsed)
|
|
m.average = m.total / m.count
|
|
|
|
-- Check for warnings
|
|
if elapsed > config.criticalThresholdMs then
|
|
Performance.addWarning(name, elapsed, "critical")
|
|
elseif elapsed > config.warningThresholdMs then
|
|
Performance.addWarning(name, elapsed, "warning")
|
|
end
|
|
|
|
if config.logToConsole then
|
|
print(string.format("[Performance] %s: %.3fms", name, elapsed))
|
|
end
|
|
|
|
return elapsed
|
|
end
|
|
|
|
--- Wrap a function with performance timing
|
|
--- @param name string Metric name
|
|
--- @param fn function Function to measure
|
|
--- @return function Wrapped function
|
|
function Performance.measure(name, fn)
|
|
if not config.enabled then
|
|
return fn
|
|
end
|
|
|
|
return function(...)
|
|
Performance.startTimer(name)
|
|
local results = table.pack(fn(...))
|
|
Performance.stopTimer(name)
|
|
return table.unpack(results, 1, results.n)
|
|
end
|
|
end
|
|
|
|
--- Start frame timing (call at beginning of frame)
|
|
function Performance.startFrame()
|
|
if not config.enabled then
|
|
return
|
|
end
|
|
lastFrameStart = love.timer.getTime()
|
|
Performance.updateMemory()
|
|
end
|
|
|
|
--- End frame timing (call at end of frame)
|
|
function Performance.endFrame()
|
|
if not config.enabled or not lastFrameStart then
|
|
return
|
|
end
|
|
|
|
local now = love.timer.getTime()
|
|
local frameTime = (now - lastFrameStart) * 1000 -- ms
|
|
|
|
frameMetrics.lastFrameTime = frameTime
|
|
frameMetrics.totalTime = frameMetrics.totalTime + frameTime
|
|
frameMetrics.frameCount = frameMetrics.frameCount + 1
|
|
frameMetrics.minFrameTime = math.min(frameMetrics.minFrameTime, frameTime)
|
|
frameMetrics.maxFrameTime = math.max(frameMetrics.maxFrameTime, frameTime)
|
|
|
|
-- Update FPS
|
|
if now - frameMetrics.lastFpsUpdate >= frameMetrics.fpsUpdateInterval then
|
|
frameMetrics.fps = math.floor(1000 / frameTime + 0.5)
|
|
frameMetrics.lastFpsUpdate = now
|
|
end
|
|
|
|
-- Check for frame drops
|
|
if frameTime > config.criticalThresholdMs then
|
|
Performance.addWarning("frame", frameTime, "critical")
|
|
end
|
|
end
|
|
|
|
--- Update memory metrics
|
|
function Performance.updateMemory()
|
|
if not config.enabled then
|
|
return
|
|
end
|
|
|
|
local memKb = collectgarbage("count")
|
|
memoryMetrics.current = memKb
|
|
memoryMetrics.peak = math.max(memoryMetrics.peak, memKb)
|
|
|
|
-- Track GC cycles
|
|
local now = love.timer.getTime()
|
|
if now - memoryMetrics.lastGcCheck >= 1.0 then
|
|
memoryMetrics.gcCount = memoryMetrics.gcCount + 1
|
|
memoryMetrics.lastGcCheck = now
|
|
end
|
|
end
|
|
|
|
--- Add a performance warning
|
|
--- @param name string Metric name
|
|
--- @param value number Metric value
|
|
--- @param level "warning"|"critical" Warning level
|
|
function Performance.addWarning(name, value, level)
|
|
if not config.logWarnings then
|
|
return
|
|
end
|
|
|
|
table.insert(warnings, {
|
|
name = name,
|
|
value = value,
|
|
level = level,
|
|
time = love.timer.getTime(),
|
|
})
|
|
|
|
-- Keep only last 100 warnings
|
|
if #warnings > 100 then
|
|
table.remove(warnings, 1)
|
|
end
|
|
end
|
|
|
|
--- Get current FPS
|
|
--- @return number fps Frames per second
|
|
function Performance.getFPS()
|
|
return frameMetrics.fps
|
|
end
|
|
|
|
--- Get frame metrics
|
|
--- @return table frameMetrics Frame timing data
|
|
function Performance.getFrameMetrics()
|
|
return {
|
|
fps = frameMetrics.fps,
|
|
lastFrameTime = frameMetrics.lastFrameTime,
|
|
minFrameTime = frameMetrics.minFrameTime,
|
|
maxFrameTime = frameMetrics.maxFrameTime,
|
|
averageFrameTime = frameMetrics.frameCount > 0 and frameMetrics.totalTime / frameMetrics.frameCount or 0,
|
|
frameCount = frameMetrics.frameCount,
|
|
}
|
|
end
|
|
|
|
--- Get memory metrics
|
|
--- @return table memoryMetrics Memory usage data
|
|
function Performance.getMemoryMetrics()
|
|
Performance.updateMemory()
|
|
return {
|
|
currentKb = memoryMetrics.current,
|
|
currentMb = memoryMetrics.current / 1024,
|
|
peakKb = memoryMetrics.peak,
|
|
peakMb = memoryMetrics.peak / 1024,
|
|
gcCount = memoryMetrics.gcCount,
|
|
}
|
|
end
|
|
|
|
--- Get all performance metrics
|
|
--- @return table metrics All collected metrics
|
|
function Performance.getMetrics()
|
|
local result = {
|
|
frame = Performance.getFrameMetrics(),
|
|
memory = Performance.getMemoryMetrics(),
|
|
timings = {},
|
|
}
|
|
|
|
for name, data in pairs(metrics) do
|
|
result.timings[name] = {
|
|
average = data.average,
|
|
min = data.min,
|
|
max = data.max,
|
|
total = data.total,
|
|
count = data.count,
|
|
}
|
|
end
|
|
|
|
return result
|
|
end
|
|
|
|
--- Get recent warnings
|
|
--- @param count number? Number of warnings to return (default: 10)
|
|
--- @return table warnings Recent warnings
|
|
function Performance.getWarnings(count)
|
|
count = count or 10
|
|
local result = {}
|
|
local start = math.max(1, #warnings - count + 1)
|
|
for i = start, #warnings do
|
|
table.insert(result, warnings[i])
|
|
end
|
|
return result
|
|
end
|
|
|
|
--- Export metrics to JSON format
|
|
--- @return string json JSON string of metrics
|
|
function Performance.exportJSON()
|
|
local allMetrics = Performance.getMetrics()
|
|
-- Simple JSON encoding (for more complex needs, use a JSON library)
|
|
local json = "{\n"
|
|
json = json .. string.format(' "fps": %d,\n', allMetrics.frame.fps)
|
|
json = json .. string.format(' "averageFrameTime": %.3f,\n', allMetrics.frame.averageFrameTime)
|
|
json = json .. string.format(' "memoryMb": %.2f,\n', allMetrics.memory.currentMb)
|
|
json = json .. ' "timings": {\n'
|
|
|
|
local timingPairs = {}
|
|
for name, data in pairs(allMetrics.timings) do
|
|
table.insert(
|
|
timingPairs,
|
|
string.format(' "%s": {"average": %.3f, "min": %.3f, "max": %.3f, "count": %d}', name, data.average, data.min, data.max, data.count)
|
|
)
|
|
end
|
|
json = json .. table.concat(timingPairs, ",\n") .. "\n"
|
|
|
|
json = json .. " }\n"
|
|
json = json .. "}"
|
|
return json
|
|
end
|
|
|
|
--- Export metrics to CSV format
|
|
--- @return string csv CSV string of metrics
|
|
function Performance.exportCSV()
|
|
local csv = "Name,Average (ms),Min (ms),Max (ms),Count\n"
|
|
for name, data in pairs(metrics) do
|
|
csv = csv .. string.format("%s,%.3f,%.3f,%.3f,%d\n", name, data.average, data.min, data.max, data.count)
|
|
end
|
|
return csv
|
|
end
|
|
|
|
--- Render performance HUD
|
|
--- @param x number? X position (default: 10)
|
|
--- @param y number? Y position (default: 10)
|
|
function Performance.renderHUD(x, y)
|
|
if not config.hudEnabled then
|
|
return
|
|
end
|
|
|
|
x = x or 10
|
|
y = y or 10
|
|
|
|
local fm = Performance.getFrameMetrics()
|
|
local mm = Performance.getMemoryMetrics()
|
|
|
|
-- Background
|
|
love.graphics.setColor(0, 0, 0, 0.8)
|
|
love.graphics.rectangle("fill", x, y, 300, 200)
|
|
|
|
-- Text
|
|
love.graphics.setColor(1, 1, 1, 1)
|
|
local lineHeight = 18
|
|
local currentY = y + 10
|
|
|
|
-- FPS
|
|
local fpsColor = { 1, 1, 1 }
|
|
if fm.lastFrameTime > config.criticalThresholdMs then
|
|
fpsColor = { 1, 0, 0 } -- Red
|
|
elseif fm.lastFrameTime > config.warningThresholdMs then
|
|
fpsColor = { 1, 1, 0 } -- Yellow
|
|
end
|
|
love.graphics.setColor(fpsColor)
|
|
love.graphics.print(string.format("FPS: %d (%.2fms)", fm.fps, fm.lastFrameTime), x + 10, currentY)
|
|
currentY = currentY + lineHeight
|
|
|
|
-- Frame times
|
|
love.graphics.setColor(1, 1, 1, 1)
|
|
love.graphics.print(string.format("Avg Frame: %.2fms", fm.averageFrameTime), x + 10, currentY)
|
|
currentY = currentY + lineHeight
|
|
love.graphics.print(string.format("Min/Max: %.2f/%.2fms", fm.minFrameTime, fm.maxFrameTime), x + 10, currentY)
|
|
currentY = currentY + lineHeight
|
|
|
|
-- Memory
|
|
love.graphics.print(string.format("Memory: %.2f MB (peak: %.2f MB)", mm.currentMb, mm.peakMb), x + 10, currentY)
|
|
currentY = currentY + lineHeight
|
|
|
|
-- Separator
|
|
currentY = currentY + 5
|
|
|
|
-- Top timings
|
|
local sortedMetrics = {}
|
|
for name, data in pairs(metrics) do
|
|
table.insert(sortedMetrics, { name = name, average = data.average })
|
|
end
|
|
table.sort(sortedMetrics, function(a, b)
|
|
return a.average > b.average
|
|
end)
|
|
|
|
love.graphics.print("Top Timings:", x + 10, currentY)
|
|
currentY = currentY + lineHeight
|
|
|
|
for i = 1, math.min(5, #sortedMetrics) do
|
|
local m = sortedMetrics[i]
|
|
love.graphics.print(string.format(" %s: %.3fms", m.name, m.average), x + 10, currentY)
|
|
currentY = currentY + lineHeight
|
|
end
|
|
|
|
-- Warnings count
|
|
if #warnings > 0 then
|
|
love.graphics.setColor(1, 0.5, 0, 1)
|
|
love.graphics.print(string.format("Warnings: %d", #warnings), x + 10, currentY)
|
|
end
|
|
end
|
|
|
|
--- Handle keyboard input for HUD toggle
|
|
--- @param key string Key pressed
|
|
function Performance.keypressed(key)
|
|
if key == config.hudToggleKey then
|
|
Performance.toggleHUD()
|
|
end
|
|
end
|
|
|
|
--- Get configuration
|
|
--- @return table config Current configuration
|
|
function Performance.getConfig()
|
|
return config
|
|
end
|
|
|
|
--- Set configuration option
|
|
--- @param key string Configuration key
|
|
--- @param value any Configuration value
|
|
function Performance.setConfig(key, value)
|
|
config[key] = value
|
|
end
|
|
|
|
return Performance
|