cleanup stale tests, profiling reports

This commit is contained in:
Michael Freno
2025-11-20 11:36:41 -05:00
parent 32009185e9
commit d0357672db
31 changed files with 994 additions and 446 deletions

View File

@@ -34,17 +34,19 @@ function profile.buildLayout()
profile.root = FlexLove.new({
width = "100%",
height = "100%",
backgroundColor = {0.05, 0.05, 0.1, 1},
flexDirection = "column",
overflow = "scroll",
padding = 20,
backgroundColor = FlexLove.Color.new(0.05, 0.05, 0.1, 1),
positioning = "flex",
flexDirection = "vertical",
overflowY = "scroll",
padding = { horizontal = 20, vertical = 20 },
gap = 10,
})
-- Create animated elements container
local animationContainer = FlexLove.new({
width = "100%",
flexDirection = "row",
positioning = "flex",
flexDirection = "horizontal",
flexWrap = "wrap",
gap = 10,
marginBottom = 20,
@@ -55,12 +57,12 @@ function profile.buildLayout()
for i = 1, profile.animationCount do
local hue = (i / profile.animationCount) * 360
local baseColor = {
local baseColor = FlexLove.Color.new(
0.3 + 0.5 * math.sin(hue * math.pi / 180),
0.3 + 0.5 * math.sin((hue + 120) * math.pi / 180),
0.3 + 0.5 * math.sin((hue + 240) * math.pi / 180),
1
}
)
-- Choose random easing function
local easingFunc = profile.easingFunctions[math.random(#profile.easingFunctions)]
@@ -70,7 +72,7 @@ function profile.buildLayout()
height = 60,
backgroundColor = baseColor,
borderRadius = 8,
margin = 5,
margin = { horizontal = 5, vertical = 5 },
})
-- Store base values for animation
@@ -120,35 +122,36 @@ function profile.buildLayout()
-- Info panel
local infoPanel = FlexLove.new({
width = "100%",
padding = 15,
backgroundColor = {0.1, 0.1, 0.2, 0.9},
padding = { horizontal = 15, vertical = 15 },
backgroundColor = FlexLove.Color.new(0.1, 0.1, 0.2, 0.9),
borderRadius = 8,
flexDirection = "column",
positioning = "flex",
flexDirection = "vertical",
gap = 5,
})
infoPanel:addChild(FlexLove.new({
textContent = string.format("Animated Elements: %d (Press +/- to adjust)", profile.animationCount),
text = string.format("Animated Elements: %d (Press +/- to adjust)", profile.animationCount),
fontSize = 18,
color = {1, 1, 1, 1},
textColor = FlexLove.Color.new(1, 1, 1, 1),
}))
infoPanel:addChild(FlexLove.new({
textContent = string.format("Active Animations: %d", #profile.animations),
text = string.format("Active Animations: %d", #profile.animations),
fontSize = 14,
color = {0.8, 0.8, 0.8, 1},
textColor = FlexLove.Color.new(0.8, 0.8, 0.8, 1),
}))
infoPanel:addChild(FlexLove.new({
textContent = "Animating: position, opacity, borderRadius",
text = "Animating: position, opacity, borderRadius",
fontSize = 14,
color = {0.8, 0.8, 0.8, 1},
textColor = FlexLove.Color.new(0.8, 0.8, 0.8, 1),
}))
infoPanel:addChild(FlexLove.new({
textContent = string.format("Easing Functions: %d variations", #profile.easingFunctions),
text = string.format("Easing Functions: %d variations", #profile.easingFunctions),
fontSize = 14,
color = {0.8, 0.8, 0.8, 1},
textColor = FlexLove.Color.new(0.8, 0.8, 0.8, 1),
}))
profile.root:addChild(infoPanel)

View File

@@ -29,17 +29,19 @@ function profile.buildLayout()
profile.root = FlexLove.new({
width = "100%",
height = "100%",
backgroundColor = {0.05, 0.05, 0.1, 1},
flexDirection = "column",
overflow = "scroll",
padding = 20,
backgroundColor = FlexLove.Color.new(0.05, 0.05, 0.1, 1),
positioning = "flex",
flexDirection = "vertical",
overflowY = "scroll",
padding = { horizontal = 20, vertical = 20 },
gap = 10,
})
-- Interactive elements container
local interactiveContainer = FlexLove.new({
width = "100%",
flexDirection = "row",
positioning = "flex",
flexDirection = "horizontal",
flexWrap = "wrap",
gap = 5,
marginBottom = 20,
@@ -47,12 +49,12 @@ function profile.buildLayout()
for i = 1, profile.elementCount do
local hue = (i / profile.elementCount) * 360
local baseColor = {
local baseColor = FlexLove.Color.new(
0.3 + 0.5 * math.sin(hue * math.pi / 180),
0.3 + 0.5 * math.sin((hue + 120) * math.pi / 180),
0.3 + 0.5 * math.sin((hue + 240) * math.pi / 180),
1
}
)
-- Create nested interactive hierarchy
local outerBox = FlexLove.new({
@@ -60,19 +62,20 @@ function profile.buildLayout()
height = 60,
backgroundColor = baseColor,
borderRadius = 8,
margin = 2,
margin = { horizontal = 2, vertical = 2 },
positioning = "flex",
justifyContent = "center",
alignItems = "center",
onEvent = function(element, event)
if event.type == "hover" then
profile.eventMetrics.hoverCount = profile.eventMetrics.hoverCount + 1
profile.eventMetrics.eventsThisFrame = profile.eventMetrics.eventsThisFrame + 1
element.backgroundColor = {
math.min(1, baseColor[1] * 1.3),
math.min(1, baseColor[2] * 1.3),
math.min(1, baseColor[3] * 1.3),
element.backgroundColor = FlexLove.Color.new(
math.min(1, baseColor.r * 1.3),
math.min(1, baseColor.g * 1.3),
math.min(1, baseColor.b * 1.3),
1
}
)
elseif event.type == "unhover" then
element.backgroundColor = baseColor
elseif event.type == "press" then
@@ -89,19 +92,19 @@ function profile.buildLayout()
local innerBox = FlexLove.new({
width = "60%",
height = "60%",
backgroundColor = {baseColor[1] * 0.6, baseColor[2] * 0.6, baseColor[3] * 0.6, 1},
backgroundColor = FlexLove.Color.new(baseColor.r * 0.6, baseColor.g * 0.6, baseColor.b * 0.6, 1),
borderRadius = 5,
onEvent = function(element, event)
if event.type == "hover" then
profile.eventMetrics.eventsThisFrame = profile.eventMetrics.eventsThisFrame + 1
element.backgroundColor = {
math.min(1, baseColor[1] * 1.5),
math.min(1, baseColor[2] * 1.5),
math.min(1, baseColor[3] * 1.5),
element.backgroundColor = FlexLove.Color.new(
math.min(1, baseColor.r * 1.5),
math.min(1, baseColor.g * 1.5),
math.min(1, baseColor.b * 1.5),
1
}
)
elseif event.type == "unhover" then
element.backgroundColor = {baseColor[1] * 0.6, baseColor[2] * 0.6, baseColor[3] * 0.6, 1}
element.backgroundColor = FlexLove.Color.new(baseColor.r * 0.6, baseColor.g * 0.6, baseColor.b * 0.6, 1)
elseif event.type == "release" then
profile.eventMetrics.eventsThisFrame = profile.eventMetrics.eventsThisFrame + 1
end
@@ -117,35 +120,36 @@ function profile.buildLayout()
-- Metrics panel
local metricsPanel = FlexLove.new({
width = "100%",
padding = 15,
backgroundColor = {0.1, 0.1, 0.2, 0.9},
padding = { horizontal = 15, vertical = 15 },
backgroundColor = FlexLove.Color.new(0.1, 0.1, 0.2, 0.9),
borderRadius = 8,
flexDirection = "column",
positioning = "flex",
flexDirection = "vertical",
gap = 5,
})
metricsPanel:addChild(FlexLove.new({
textContent = string.format("Interactive Elements: %d (Press +/- to adjust)", profile.elementCount),
text = string.format("Interactive Elements: %d (Press +/- to adjust)", profile.elementCount),
fontSize = 18,
color = {1, 1, 1, 1},
textColor = FlexLove.Color.new(1, 1, 1, 1),
}))
metricsPanel:addChild(FlexLove.new({
textContent = string.format("Total Hovers: %d", profile.eventMetrics.hoverCount),
text = string.format("Total Hovers: %d", profile.eventMetrics.hoverCount),
fontSize = 14,
color = {0.8, 0.8, 0.8, 1},
textColor = FlexLove.Color.new(0.8, 0.8, 0.8, 1),
}))
metricsPanel:addChild(FlexLove.new({
textContent = string.format("Total Clicks: %d", profile.eventMetrics.clickCount),
text = string.format("Total Clicks: %d", profile.eventMetrics.clickCount),
fontSize = 14,
color = {0.8, 0.8, 0.8, 1},
textColor = FlexLove.Color.new(0.8, 0.8, 0.8, 1),
}))
metricsPanel:addChild(FlexLove.new({
textContent = string.format("Events/Frame: %d", profile.eventMetrics.eventsThisFrame),
text = string.format("Events/Frame: %d", profile.eventMetrics.eventsThisFrame),
fontSize = 14,
color = {0.8, 0.8, 0.8, 1},
textColor = FlexLove.Color.new(0.8, 0.8, 0.8, 1),
}))
profile.root:addChild(metricsPanel)

View File

@@ -24,10 +24,11 @@ function profile.buildUI()
id = "root", -- ID required for state persistence
width = "100%",
height = "100%",
backgroundColor = {0.05, 0.05, 0.1, 1},
flexDirection = "column",
overflow = "scroll",
padding = 20,
backgroundColor = FlexLove.Color.new(0.05, 0.05, 0.1, 1),
positioning = "flex",
flexDirection = "vertical",
overflowY = "scroll",
padding = { horizontal = 20, vertical = 20 },
gap = 10,
})
@@ -35,7 +36,8 @@ function profile.buildUI()
local content = FlexLove.new({
id = "content",
width = "100%",
flexDirection = "row",
positioning = "flex",
flexDirection = "horizontal",
flexWrap = "wrap",
gap = 5,
marginBottom = 20,
@@ -43,12 +45,12 @@ function profile.buildUI()
for i = 1, profile.elementCount do
local hue = (i / profile.elementCount) * 360
local baseColor = {
local baseColor = FlexLove.Color.new(
0.3 + 0.5 * math.sin(hue * math.pi / 180),
0.3 + 0.5 * math.sin((hue + 120) * math.pi / 180),
0.3 + 0.5 * math.sin((hue + 240) * math.pi / 180),
1
}
)
-- Each element needs a unique ID for state persistence
local box = FlexLove.new({
@@ -57,15 +59,15 @@ function profile.buildUI()
height = 60,
backgroundColor = baseColor,
borderRadius = 8,
margin = 2,
margin = { horizontal = 2, vertical = 2 },
onEvent = function(element, event)
if event.type == "hover" then
element.backgroundColor = {
math.min(1, baseColor[1] * 1.3),
math.min(1, baseColor[2] * 1.3),
math.min(1, baseColor[3] * 1.3),
element.backgroundColor = FlexLove.Color.new(
math.min(1, baseColor.r * 1.3),
math.min(1, baseColor.g * 1.3),
math.min(1, baseColor.b * 1.3),
1
}
)
elseif event.type == "unhover" then
element.backgroundColor = baseColor
elseif event.type == "press" then
@@ -85,39 +87,40 @@ function profile.buildUI()
local infoPanel = FlexLove.new({
id = "infoPanel",
width = "100%",
padding = 15,
backgroundColor = {0.1, 0.1, 0.2, 0.9},
padding = { horizontal = 15, vertical = 15 },
backgroundColor = FlexLove.Color.new(0.1, 0.1, 0.2, 0.9),
borderRadius = 8,
flexDirection = "column",
positioning = "flex",
flexDirection = "vertical",
gap = 5,
})
infoPanel:addChild(FlexLove.new({
id = "info_title",
textContent = string.format("Immediate Mode: %d Elements", profile.elementCount),
text = string.format("Immediate Mode: %d Elements", profile.elementCount),
fontSize = 18,
color = {1, 1, 1, 1},
textColor = FlexLove.Color.new(1, 1, 1, 1),
}))
infoPanel:addChild(FlexLove.new({
id = "info_frame",
textContent = string.format("Frame: %d", profile.frameCount),
text = string.format("Frame: %d", profile.frameCount),
fontSize = 14,
color = {0.8, 0.8, 0.8, 1},
textColor = FlexLove.Color.new(0.8, 0.8, 0.8, 1),
}))
infoPanel:addChild(FlexLove.new({
id = "info_states",
textContent = string.format("Active States: %d", FlexLove.getStateCount()),
text = string.format("Active States: %d", FlexLove.getStateCount()),
fontSize = 14,
color = {0.8, 0.8, 0.8, 1},
textColor = FlexLove.Color.new(0.8, 0.8, 0.8, 1),
}))
infoPanel:addChild(FlexLove.new({
id = "info_help",
textContent = "Press +/- to adjust element count",
text = "Press +/- to adjust element count",
fontSize = 12,
color = {0.7, 0.7, 0.7, 1},
textColor = FlexLove.Color.new(0.7, 0.7, 0.7, 1),
}))
root:addChild(infoPanel)

View File

@@ -26,10 +26,11 @@ function profile.buildLayout()
profile.root = FlexLove.new({
width = "100%",
height = "100%",
backgroundColor = {0.05, 0.05, 0.1, 1},
flexDirection = "column",
overflow = "scroll",
padding = 20,
backgroundColor = FlexLove.Color.new(0.05, 0.05, 0.1, 1),
positioning = "flex",
flexDirection = "vertical",
overflowY = "scroll",
padding = { horizontal = 20, vertical = 20 },
gap = 10,
})
@@ -38,7 +39,8 @@ function profile.buildLayout()
for r = 1, rows do
local row = FlexLove.new({
flexDirection = "row",
positioning = "flex",
flexDirection = "horizontal",
gap = 10,
flexWrap = "wrap",
})
@@ -46,19 +48,20 @@ function profile.buildLayout()
local itemsInRow = math.min(elementsPerRow, profile.elementCount - (r - 1) * elementsPerRow)
for c = 1, itemsInRow do
local hue = ((r - 1) * elementsPerRow + c) / profile.elementCount
local color = {
local color = FlexLove.Color.new(
0.3 + 0.5 * math.sin(hue * math.pi * 2),
0.3 + 0.5 * math.sin((hue + 0.33) * math.pi * 2),
0.3 + 0.5 * math.sin((hue + 0.66) * math.pi * 2),
1
}
)
local box = FlexLove.new({
width = 80,
height = 80,
backgroundColor = color,
borderRadius = 8,
justifyContent = "center",
positioning = "flex",
justifyContent = "center",
alignItems = "center",
})
@@ -67,9 +70,10 @@ function profile.buildLayout()
local innerBox = FlexLove.new({
width = "80%",
height = "80%",
backgroundColor = {color[1] * 0.8, color[2] * 0.8, color[3] * 0.8, color[4]},
backgroundColor = FlexLove.Color.new(color.r * 0.8, color.g * 0.8, color.b * 0.8, color.a),
borderRadius = 6,
justifyContent = "center",
positioning = "flex",
justifyContent = "center",
alignItems = "center",
})
nested:addChild(innerBox)
@@ -84,24 +88,25 @@ function profile.buildLayout()
local infoPanel = FlexLove.new({
width = "100%",
padding = 15,
backgroundColor = {0.1, 0.1, 0.2, 0.9},
padding = { horizontal = 15, vertical = 15 },
backgroundColor = FlexLove.Color.new(0.1, 0.1, 0.2, 0.9),
borderRadius = 8,
marginTop = 20,
flexDirection = "column",
positioning = "flex",
flexDirection = "vertical",
gap = 5,
})
infoPanel:addChild(FlexLove.new({
textContent = string.format("Elements: %d (Press +/- to adjust)", profile.elementCount),
text = string.format("Elements: %d (Press +/- to adjust)", profile.elementCount),
fontSize = 18,
color = {1, 1, 1, 1},
textColor = FlexLove.Color.new(1, 1, 1, 1),
}))
infoPanel:addChild(FlexLove.new({
textContent = string.format("Nesting Depth: %d", profile.nestingDepth),
text = string.format("Nesting Depth: %d", profile.nestingDepth),
fontSize = 14,
color = {0.8, 0.8, 0.8, 1},
textColor = FlexLove.Color.new(0.8, 0.8, 0.8, 1),
}))
profile.root:addChild(infoPanel)

View File

@@ -45,17 +45,19 @@ function profile.buildLayout()
profile.root = FlexLove.new({
width = "100%",
height = "100%",
backgroundColor = {0.05, 0.05, 0.1, 1},
flexDirection = "column",
overflow = "scroll",
padding = 20,
backgroundColor = FlexLove.Color.new(0.05, 0.05, 0.1, 1),
positioning = "flex",
flexDirection = "vertical",
overflowY = "scroll",
padding = { horizontal = 20, vertical = 20 },
gap = 10,
})
-- Create elements container
local elementsContainer = FlexLove.new({
width = "100%",
flexDirection = "row",
positioning = "flex",
flexDirection = "horizontal",
flexWrap = "wrap",
gap = 5,
marginBottom = 20,
@@ -63,19 +65,19 @@ function profile.buildLayout()
for i = 1, profile.elementCount do
local hue = (i / profile.elementCount) * 360
local color = {
local color = FlexLove.Color.new(
0.3 + 0.5 * math.sin(hue * math.pi / 180),
0.3 + 0.5 * math.sin((hue + 120) * math.pi / 180),
0.3 + 0.5 * math.sin((hue + 240) * math.pi / 180),
1
}
)
local box = FlexLove.new({
width = 50,
height = 50,
backgroundColor = color,
borderRadius = 8,
margin = 2,
margin = { horizontal = 2, vertical = 2 },
})
elementsContainer:addChild(box)
@@ -86,10 +88,11 @@ function profile.buildLayout()
-- Memory stats panel
local statsPanel = FlexLove.new({
width = "100%",
padding = 15,
backgroundColor = {0.1, 0.1, 0.2, 0.9},
padding = { horizontal = 15, vertical = 15 },
backgroundColor = FlexLove.Color.new(0.1, 0.1, 0.2, 0.9),
borderRadius = 8,
flexDirection = "column",
positioning = "flex",
flexDirection = "vertical",
gap = 5,
})
@@ -97,27 +100,27 @@ function profile.buildLayout()
local memGrowth = currentMem - profile.memoryStats.startMemory
statsPanel:addChild(FlexLove.new({
textContent = string.format("Memory Profile | Elements: %d", profile.elementCount),
text = string.format("Memory Profile | Elements: %d", profile.elementCount),
fontSize = 18,
color = {1, 1, 1, 1},
textColor = FlexLove.Color.new(1, 1, 1, 1),
}))
statsPanel:addChild(FlexLove.new({
textContent = string.format("Current: %.2f MB | Peak: %.2f MB", currentMem, profile.memoryStats.peakMemory),
text = string.format("Current: %.2f MB | Peak: %.2f MB", currentMem, profile.memoryStats.peakMemory),
fontSize = 14,
color = {0.8, 0.8, 0.8, 1},
textColor = FlexLove.Color.new(0.8, 0.8, 0.8, 1),
}))
statsPanel:addChild(FlexLove.new({
textContent = string.format("Growth: %.2f MB | GC Count: %d", memGrowth, profile.memoryStats.gcCount),
text = string.format("Growth: %.2f MB | GC Count: %d", memGrowth, profile.memoryStats.gcCount),
fontSize = 14,
color = {0.8, 0.8, 0.8, 1},
textColor = FlexLove.Color.new(0.8, 0.8, 0.8, 1),
}))
statsPanel:addChild(FlexLove.new({
textContent = "Press G to force GC | Press +/- to adjust elements",
text = "Press G to force GC | Press +/- to adjust elements",
fontSize = 12,
color = {0.7, 0.7, 0.7, 1},
textColor = FlexLove.Color.new(0.7, 0.7, 0.7, 1),
}))
profile.root:addChild(statsPanel)

View File

@@ -26,17 +26,19 @@ function profile.buildLayout()
profile.root = FlexLove.new({
width = "100%",
height = "100%",
backgroundColor = {0.05, 0.05, 0.1, 1},
flexDirection = "column",
overflow = "scroll",
padding = 20,
backgroundColor = FlexLove.Color.new(0.05, 0.05, 0.1, 1),
positioning = "flex",
flexDirection = "vertical",
overflowY = "scroll",
padding = { horizontal = 20, vertical = 20 },
gap = 10,
})
-- Render container
local renderContainer = FlexLove.new({
width = "100%",
flexDirection = "row",
positioning = "flex",
flexDirection = "horizontal",
flexWrap = "wrap",
gap = 5,
marginBottom = 20,
@@ -44,27 +46,27 @@ function profile.buildLayout()
for i = 1, profile.elementCount do
local hue = (i / profile.elementCount) * 360
local color = {
local color = FlexLove.Color.new(
0.3 + 0.5 * math.sin(hue * math.pi / 180),
0.3 + 0.5 * math.sin((hue + 120) * math.pi / 180),
0.3 + 0.5 * math.sin((hue + 240) * math.pi / 180),
1
}
)
local box = FlexLove.new({
width = 50,
height = 50,
backgroundColor = color,
borderRadius = profile.showRounded and (5 + math.random(20)) or 0,
margin = 2,
margin = { horizontal = 2, vertical = 2 },
})
-- Add text rendering if enabled
if profile.showText then
box:addChild(FlexLove.new({
textContent = tostring(i),
text = tostring(i),
fontSize = 12,
color = {1, 1, 1, 0.8},
textColor = FlexLove.Color.new(1, 1, 1, 0.8),
}))
end
@@ -73,9 +75,10 @@ function profile.buildLayout()
local innerBox = FlexLove.new({
width = "80%",
height = "80%",
backgroundColor = {color[1] * 0.5, color[2] * 0.5, color[3] * 0.5, 0.7},
backgroundColor = FlexLove.Color.new(color.r * 0.5, color.g * 0.5, color.b * 0.5, 0.7),
borderRadius = profile.showRounded and 8 or 0,
justifyContent = "center",
positioning = "flex",
justifyContent = "center",
alignItems = "center",
})
box:addChild(innerBox)
@@ -89,35 +92,36 @@ function profile.buildLayout()
-- Controls panel
local controlsPanel = FlexLove.new({
width = "100%",
padding = 15,
backgroundColor = {0.1, 0.1, 0.2, 0.9},
padding = { horizontal = 15, vertical = 15 },
backgroundColor = FlexLove.Color.new(0.1, 0.1, 0.2, 0.9),
borderRadius = 8,
flexDirection = "column",
positioning = "flex",
flexDirection = "vertical",
gap = 8,
})
controlsPanel:addChild(FlexLove.new({
textContent = string.format("Render Elements: %d (Press +/- to adjust)", profile.elementCount),
text = string.format("Render Elements: %d (Press +/- to adjust)", profile.elementCount),
fontSize = 18,
color = {1, 1, 1, 1},
textColor = FlexLove.Color.new(1, 1, 1, 1),
}))
controlsPanel:addChild(FlexLove.new({
textContent = string.format("[R] Rounded Rectangles: %s", profile.showRounded and "ON" or "OFF"),
text = string.format("[R] Rounded Rectangles: %s", profile.showRounded and "ON" or "OFF"),
fontSize = 14,
color = {0.8, 0.8, 0.8, 1},
textColor = FlexLove.Color.new(0.8, 0.8, 0.8, 1),
}))
controlsPanel:addChild(FlexLove.new({
textContent = string.format("[T] Text Rendering: %s", profile.showText and "ON" or "OFF"),
text = string.format("[T] Text Rendering: %s", profile.showText and "ON" or "OFF"),
fontSize = 14,
color = {0.8, 0.8, 0.8, 1},
textColor = FlexLove.Color.new(0.8, 0.8, 0.8, 1),
}))
controlsPanel:addChild(FlexLove.new({
textContent = string.format("[L] Layering/Overdraw: %s", profile.showLayering and "ON" or "OFF"),
text = string.format("[L] Layering/Overdraw: %s", profile.showLayering and "ON" or "OFF"),
fontSize = 14,
color = {0.8, 0.8, 0.8, 1},
textColor = FlexLove.Color.new(0.8, 0.8, 0.8, 1),
}))
profile.root:addChild(controlsPanel)

View File

@@ -8,6 +8,7 @@ local PerformanceProfiler = require("profiling.utils.PerformanceProfiler")
local state = {
mode = "menu", -- "menu" or "profile"
currentProfile = nil,
currentProfileInfo = nil,
profiler = nil,
profiles = {},
selectedIndex = 1,
@@ -25,13 +26,17 @@ local function discoverProfiles()
local name = file:gsub("%.lua$", "")
table.insert(profiles, {
name = name,
displayName = name:gsub("_", " "):gsub("(%a)(%w*)", function(a, b) return a:upper() .. b end),
displayName = name:gsub("_", " "):gsub("(%a)(%w*)", function(a, b)
return a:upper() .. b
end),
path = "__profiles__/" .. file,
})
end
end
table.sort(profiles, function(a, b) return a.name < b.name end)
table.sort(profiles, function(a, b)
return a.name < b.name
end)
return profiles
end
@@ -53,6 +58,7 @@ local function loadProfile(profileInfo)
end
state.currentProfile = profile
state.currentProfileInfo = profileInfo
state.profiler = PerformanceProfiler.new()
state.mode = "profile"
@@ -71,11 +77,27 @@ local function loadProfile(profileInfo)
end
local function returnToMenu()
-- Save profiling report before exiting
if state.profiler and state.currentProfileInfo then
local success, filepath = state.profiler:saveReport(state.currentProfileInfo.name)
if success then
print("\n========================================")
print("✓ Profiling report saved successfully!")
print(" Location: " .. filepath)
print("========================================\n")
else
print("\n✗ Failed to save report: " .. tostring(filepath) .. "\n")
end
end
if state.currentProfile and type(state.currentProfile.cleanup) == "function" then
pcall(function() state.currentProfile.cleanup() end)
pcall(function()
state.currentProfile.cleanup()
end)
end
state.currentProfile = nil
state.currentProfileInfo = nil
state.profiler = nil
state.mode = "menu"
collectgarbage("collect")
@@ -87,108 +109,113 @@ local function buildMenu()
local root = FlexLove.new({
width = "100%",
height = "100%",
backgroundColor = {0.1, 0.1, 0.15, 1},
flexDirection = "column",
backgroundColor = FlexLove.Color.new(0.1, 0.1, 0.15, 1),
positioning = "flex",
flexDirection = "vertical",
justifyContent = "flex-start",
alignItems = "center",
padding = 40,
padding = { horizontal = 40, vertical = 40 },
})
root:addChild(FlexLove.new({
flexDirection = "column",
local container = FlexLove.new({
parent = root,
positioning = "flex",
flexDirection = "vertical",
alignItems = "center",
gap = 30,
children = {
FlexLove.new({
width = 600,
height = 80,
backgroundColor = {0.15, 0.15, 0.25, 1},
borderRadius = 10,
justifyContent = "center",
alignItems = "center",
children = {
FlexLove.new({
textContent = "FlexLöve Performance Profiler",
fontSize = 32,
color = {0.3, 0.8, 1, 1},
})
}
}),
})
FlexLove.new({
textContent = "Select a profile to run:",
fontSize = 20,
color = {0.8, 0.8, 0.8, 1},
}),
-- Title
FlexLove.new({
parent = container,
width = 600,
height = 80,
backgroundColor = FlexLove.Color.new(0.15, 0.15, 0.25, 1),
borderRadius = 10,
positioning = "flex",
justifyContent = "center",
alignItems = "center",
text = "FlexLöve Performance Profiler",
textSize = "3xl",
textColor = FlexLove.Color.new(0.3, 0.8, 1, 1),
})
FlexLove.new({
width = 600,
flexDirection = "column",
gap = 10,
children = (function()
local items = {}
for i, profile in ipairs(state.profiles) do
local isSelected = i == state.selectedIndex
table.insert(items, FlexLove.new({
width = "100%",
height = 50,
backgroundColor = isSelected and {0.2, 0.4, 0.8, 1} or {0.15, 0.15, 0.25, 1},
borderRadius = 8,
justifyContent = "flex-start",
alignItems = "center",
padding = 15,
cursor = "pointer",
onClick = function()
state.selectedIndex = i
loadProfile(profile)
end,
onHover = function(element)
if not isSelected then
element.backgroundColor = {0.2, 0.2, 0.35, 1}
end
end,
onHoverEnd = function(element)
if not isSelected then
element.backgroundColor = {0.15, 0.15, 0.25, 1}
end
end,
children = {
FlexLove.new({
textContent = profile.displayName,
fontSize = 18,
color = isSelected and {1, 1, 1, 1} or {0.8, 0.8, 0.8, 1},
})
}
}))
end
return items
end)()
}),
-- Subtitle
FlexLove.new({
parent = container,
text = "Select a profile to run:",
textSize = "xl",
textColor = FlexLove.Color.new(0.8, 0.8, 0.8, 1),
})
FlexLove.new({
textContent = "Use ↑/↓ to select, ENTER to run, ESC to quit",
fontSize = 14,
color = {0.5, 0.5, 0.5, 1},
marginTop = 20,
}),
}
}))
-- Profile list
local profileList = FlexLove.new({
parent = container,
width = 600,
positioning = "flex",
flexDirection = "vertical",
gap = 10,
})
if state.error then
root:addChild(FlexLove.new({
width = 600,
padding = 15,
backgroundColor = {0.8, 0.2, 0.2, 1},
for i, profile in ipairs(state.profiles) do
local isSelected = i == state.selectedIndex
local button = FlexLove.new({
parent = profileList,
width = "100%",
height = 50,
backgroundColor = isSelected and FlexLove.Color.new(0.2, 0.4, 0.8, 1)
or FlexLove.Color.new(0.15, 0.15, 0.25, 1),
borderRadius = 8,
marginTop = 20,
children = {
FlexLove.new({
textContent = "Error: " .. state.error,
fontSize = 14,
color = {1, 1, 1, 1},
})
}
}))
positioning = "flex",
justifyContent = "flex-start",
alignItems = "center",
padding = { horizontal = 15, vertical = 15 },
onEvent = function(element, event)
if event.type == "release" then
state.selectedIndex = i
loadProfile(profile)
elseif event.type == "hover" and not isSelected then
element.backgroundColor = FlexLove.Color.new(0.2, 0.2, 0.35, 1)
elseif event.type == "unhover" and not isSelected then
element.backgroundColor = FlexLove.Color.new(0.15, 0.15, 0.25, 1)
end
end,
})
FlexLove.new({
parent = button,
text = profile.displayName,
textSize = "lg",
textColor = isSelected and FlexLove.Color.new(1, 1, 1, 1) or FlexLove.Color.new(0.8, 0.8, 0.8, 1),
})
end
-- Instructions
FlexLove.new({
parent = container,
text = "Use ↑/↓ to select, ENTER to run, ESC to quit",
textSize = "md",
textColor = FlexLove.Color.new(0.5, 0.5, 0.5, 1),
margin = { top = 20 },
})
-- Error display
if state.error then
local errorBox = FlexLove.new({
parent = container,
width = 600,
padding = { horizontal = 15, vertical = 15 },
backgroundColor = FlexLove.Color.new(0.8, 0.2, 0.2, 1),
borderRadius = 8,
margin = { top = 20 },
})
FlexLove.new({
parent = errorBox,
text = "Error: " .. state.error,
textSize = "md",
textColor = FlexLove.Color.new(1, 1, 1, 1),
})
end
FlexLove.endFrame()
@@ -198,6 +225,7 @@ function love.load(args)
FlexLove.init({
width = love.graphics.getWidth(),
height = love.graphics.getHeight(),
immediateMode = true,
})
state.profiles = discoverProfiles()
@@ -259,7 +287,7 @@ function love.draw()
end
love.graphics.setColor(1, 1, 1, 1)
love.graphics.print("Press R to reset | ESC to menu | F11 fullscreen", 10, love.graphics.getHeight() - 25)
love.graphics.print("Press R to reset | S to save report | ESC to menu | F11 fullscreen", 10, love.graphics.getHeight() - 25)
end
end
@@ -284,14 +312,31 @@ function love.keypressed(key)
state.profiler:reset()
end
if state.currentProfile and type(state.currentProfile.reset) == "function" then
pcall(function() state.currentProfile.reset() end)
pcall(function()
state.currentProfile.reset()
end)
end
elseif key == "s" then
-- Save report manually
if state.profiler and state.currentProfileInfo then
local success, filepath = state.profiler:saveReport(state.currentProfileInfo.name)
if success then
print("\n========================================")
print("✓ Profiling report saved successfully!")
print(" Location: " .. filepath)
print("========================================\n")
else
print("\n✗ Failed to save report: " .. tostring(filepath) .. "\n")
end
end
elseif key == "f11" then
love.window.setFullscreen(not love.window.getFullscreen())
end
if state.currentProfile and type(state.currentProfile.keypressed) == "function" then
pcall(function() state.currentProfile.keypressed(key) end)
pcall(function()
state.currentProfile.keypressed(key)
end)
end
end
end
@@ -299,7 +344,9 @@ end
function love.mousepressed(x, y, button)
if state.mode == "profile" and state.currentProfile then
if type(state.currentProfile.mousepressed) == "function" then
pcall(function() state.currentProfile.mousepressed(x, y, button) end)
pcall(function()
state.currentProfile.mousepressed(x, y, button)
end)
end
end
end
@@ -307,7 +354,9 @@ end
function love.mousereleased(x, y, button)
if state.mode == "profile" and state.currentProfile then
if type(state.currentProfile.mousereleased) == "function" then
pcall(function() state.currentProfile.mousereleased(x, y, button) end)
pcall(function()
state.currentProfile.mousereleased(x, y, button)
end)
end
end
end
@@ -315,7 +364,9 @@ end
function love.mousemoved(x, y, dx, dy)
if state.mode == "profile" and state.currentProfile then
if type(state.currentProfile.mousemoved) == "function" then
pcall(function() state.currentProfile.mousemoved(x, y, dx, dy) end)
pcall(function()
state.currentProfile.mousemoved(x, y, dx, dy)
end)
end
end
end
@@ -324,13 +375,17 @@ function love.resize(w, h)
FlexLove.resize(w, h)
if state.mode == "profile" and state.currentProfile then
if type(state.currentProfile.resize) == "function" then
pcall(function() state.currentProfile.resize(w, h) end)
pcall(function()
state.currentProfile.resize(w, h)
end)
end
end
end
function love.quit()
if state.currentProfile and type(state.currentProfile.cleanup) == "function" then
pcall(function() state.currentProfile.cleanup() end)
pcall(function()
state.currentProfile.cleanup()
end)
end
end

4
profiling/reports/.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
# Profiling reports - ignore all report files but keep README
/*
!README.md
!.gitignore

View File

@@ -0,0 +1,69 @@
# Profiling Reports
This directory contains performance profiling reports generated by the FlexLöve profiling system.
## Directory Structure
Reports are organized by profile name:
```
reports/
├── animation_stress/
│ ├── latest.md # Most recent report (Markdown)
│ ├── latest.json # Most recent report (JSON)
│ ├── 2025-11-20_10-30-00.md
│ └── 2025-11-20_10-30-00.json
├── event_stress/
│ ├── latest.md
│ └── latest.json
└── ...
```
## Report Format
Each test run generates two files:
- `<timestamp>.md` - Human-readable Markdown report with formatted tables
- `<timestamp>.json` - Machine-readable JSON data for programmatic analysis
- `latest.md` - Always contains the most recent report for quick access
- `latest.json` - Most recent JSON data
## Report Contents
### FPS Statistics
- **Average FPS**: Mean frames per second across the entire test
- **Median FPS**: Middle value of all FPS measurements
- **1% Worst FPS**: The FPS at which 1% of frames performed at or below this level (useful for identifying stutters)
- **0.1% Worst FPS**: The FPS at which 0.1% of frames performed at or below this level (worst case performance)
### Frame Time Statistics
- **Average**: Mean frame time in milliseconds
- **Median**: Middle value of all frame time measurements
- **95th/99th/99.9th Percentile**: Frame times that 95%/99%/99.9% of frames completed under
### Memory Usage
- **Average**: Mean memory usage across the test
- **Peak**: Maximum memory used during the test
- **95th/99th/99.9th Percentile**: Memory usage levels that 95%/99%/99.9% of samples were under
## How to Generate Reports
### Automatic (on exit)
Reports are automatically saved when you:
- Press ESC to return to the menu
- Quit the profiling application
### Manual
While a profile is running, press **S** to manually save a report without stopping the test.
## Interpreting Results
### Good Performance Indicators
- Average FPS close to target (60 FPS for most applications)
- Small difference between average and 1% worst FPS
- Low 99.9th percentile frame times (< 20ms for 60 FPS)
- Stable memory usage without continuous growth
### Performance Issues to Watch For
- Large gap between average and 1%/0.1% worst FPS (indicates frame drops/stutters)
- High 99.9th percentile frame times (indicates occasional severe lag spikes)
- Continuously growing memory usage (indicates memory leak)
- High memory peak compared to average (indicates GC pressure)

View File

@@ -16,10 +16,10 @@ PerformanceProfiler.__index = PerformanceProfiler
---@return PerformanceProfiler
function PerformanceProfiler.new(config)
local self = setmetatable({}, PerformanceProfiler)
config = config or {}
self._maxHistorySize = config.maxHistorySize or 300
self._frameCount = 0
self._startTime = love.timer.getTime()
self._frameTimes = {}
@@ -29,44 +29,46 @@ function PerformanceProfiler.new(config)
self._markers = {}
self._currentFrameStart = nil
self._lastGcCount = collectgarbage("count")
return self
end
---@return nil
function PerformanceProfiler:beginFrame()
self._currentFrameStart = love.timer.getTime()
local now = love.timer.getTime()
-- Calculate actual frame time from previous frame start to current frame start
if self._currentFrameStart then
local frameTime = (now - self._currentFrameStart) * 1000
table.insert(self._frameTimes, frameTime)
if #self._frameTimes > self._maxHistorySize then
table.remove(self._frameTimes, 1)
end
local fps = 1000 / frameTime
table.insert(self._fpsHistory, fps)
if #self._fpsHistory > self._maxHistorySize then
table.remove(self._fpsHistory, 1)
end
local memKb = collectgarbage("count")
table.insert(self._memoryHistory, memKb / 1024)
if #self._memoryHistory > self._maxHistorySize then
table.remove(self._memoryHistory, 1)
end
self._lastGcCount = memKb
end
self._currentFrameStart = now
self._frameCount = self._frameCount + 1
end
---@return nil
function PerformanceProfiler:endFrame()
if not self._currentFrameStart then
return
end
local now = love.timer.getTime()
local frameTime = (now - self._currentFrameStart) * 1000
table.insert(self._frameTimes, frameTime)
if #self._frameTimes > self._maxHistorySize then
table.remove(self._frameTimes, 1)
end
local fps = 1000 / frameTime
table.insert(self._fpsHistory, fps)
if #self._fpsHistory > self._maxHistorySize then
table.remove(self._fpsHistory, 1)
end
local memKb = collectgarbage("count")
table.insert(self._memoryHistory, memKb / 1024)
if #self._memoryHistory > self._maxHistorySize then
table.remove(self._memoryHistory, 1)
end
self._lastGcCount = memKb
self._currentFrameStart = nil
-- No longer needed - frame timing is done in beginFrame()
-- Keeping this method for API compatibility
end
---@param name string
@@ -81,7 +83,7 @@ function PerformanceProfiler:markBegin(name)
maxTime = 0,
}
end
self._markers[name].startTime = love.timer.getTime()
end
@@ -92,20 +94,20 @@ function PerformanceProfiler:markEnd(name)
if not marker or not marker.startTime then
return nil
end
local elapsed = (love.timer.getTime() - marker.startTime) * 1000
marker.startTime = nil
table.insert(marker.times, elapsed)
if #marker.times > self._maxHistorySize then
table.remove(marker.times, 1)
end
marker.totalTime = marker.totalTime + elapsed
marker.count = marker.count + 1
marker.minTime = math.min(marker.minTime, elapsed)
marker.maxTime = math.max(marker.maxTime, elapsed)
return elapsed
end
@@ -122,13 +124,13 @@ function PerformanceProfiler:recordMetric(name, value)
max = -math.huge,
}
end
local metric = self._customMetrics[name]
table.insert(metric.values, value)
if #metric.values > self._maxHistorySize then
table.remove(metric.values, 1)
end
metric.total = metric.total + value
metric.count = metric.count + 1
metric.min = math.min(metric.min, value)
@@ -138,7 +140,9 @@ end
---@param values table
---@return number
local function calculateMean(values)
if #values == 0 then return 0 end
if #values == 0 then
return 0
end
local sum = 0
for _, v in ipairs(values) do
sum = sum + v
@@ -149,14 +153,16 @@ end
---@param values table
---@return number
local function calculateMedian(values)
if #values == 0 then return 0 end
if #values == 0 then
return 0
end
local sorted = {}
for _, v in ipairs(values) do
table.insert(sorted, v)
end
table.sort(sorted)
local mid = math.floor(#sorted / 2) + 1
if #sorted % 2 == 0 then
return (sorted[mid - 1] + sorted[mid]) / 2
@@ -169,14 +175,16 @@ end
---@param percentile number
---@return number
local function calculatePercentile(values, percentile)
if #values == 0 then return 0 end
if #values == 0 then
return 0
end
local sorted = {}
for _, v in ipairs(values) do
table.insert(sorted, v)
end
table.sort(sorted)
local index = math.ceil(#sorted * percentile / 100)
index = math.max(1, math.min(index, #sorted))
return sorted[index]
@@ -186,12 +194,12 @@ end
function PerformanceProfiler:getReport()
local now = love.timer.getTime()
local totalTime = now - self._startTime
local report = {
totalTime = totalTime,
frameCount = self._frameCount,
averageFps = self._frameCount / totalTime,
frameTime = {
current = self._frameTimes[#self._frameTimes] or 0,
average = calculateMean(self._frameTimes),
@@ -200,45 +208,52 @@ function PerformanceProfiler:getReport()
max = 0,
p95 = calculatePercentile(self._frameTimes, 95),
p99 = calculatePercentile(self._frameTimes, 99),
p99_9 = calculatePercentile(self._frameTimes, 99.9),
},
fps = {
current = self._fpsHistory[#self._fpsHistory] or 0,
average = calculateMean(self._fpsHistory),
median = calculateMedian(self._fpsHistory),
min = math.huge,
max = 0,
-- For FPS, 1% and 0.1% worst are the LOWEST values (inverse of percentile)
worst_1_percent = calculatePercentile(self._fpsHistory, 1),
worst_0_1_percent = calculatePercentile(self._fpsHistory, 0.1),
},
memory = {
current = self._memoryHistory[#self._memoryHistory] or 0,
average = calculateMean(self._memoryHistory),
peak = -math.huge,
min = math.huge,
p95 = calculatePercentile(self._memoryHistory, 95),
p99 = calculatePercentile(self._memoryHistory, 99),
p99_9 = calculatePercentile(self._memoryHistory, 99.9),
},
markers = {},
customMetrics = {},
}
-- Calculate frame time min/max
for _, ft in ipairs(self._frameTimes) do
report.frameTime.min = math.min(report.frameTime.min, ft)
report.frameTime.max = math.max(report.frameTime.max, ft)
end
-- Calculate FPS min/max
for _, fps in ipairs(self._fpsHistory) do
report.fps.min = math.min(report.fps.min, fps)
report.fps.max = math.max(report.fps.max, fps)
end
-- Calculate memory min/max/peak
for _, mem in ipairs(self._memoryHistory) do
report.memory.min = math.min(report.memory.min, mem)
report.memory.peak = math.max(report.memory.peak, mem)
end
-- Add marker statistics
for name, marker in pairs(self._markers) do
report.markers[name] = {
@@ -251,7 +266,7 @@ function PerformanceProfiler:getReport()
p99 = calculatePercentile(marker.times, 99),
}
end
-- Add custom metrics
for name, metric in pairs(self._customMetrics) do
report.customMetrics[name] = {
@@ -262,7 +277,7 @@ function PerformanceProfiler:getReport()
count = metric.count,
}
end
return report
end
@@ -276,47 +291,47 @@ function PerformanceProfiler:draw(x, y, width, height)
y = y or 10
width = width or 320
height = height or 280
local report = self:getReport()
love.graphics.setColor(0, 0, 0, 0.85)
love.graphics.rectangle("fill", x, y, width, height)
love.graphics.setColor(1, 1, 1, 1)
local lineHeight = 18
local currentY = y + 10
local padding = 10
-- Title
love.graphics.setColor(0.3, 0.8, 1, 1)
love.graphics.print("Performance Profiler", x + padding, currentY)
currentY = currentY + lineHeight + 5
-- FPS
local fpsColor = {1, 1, 1}
local fpsColor = { 1, 1, 1 }
if report.frameTime.current > 16.67 then
fpsColor = {1, 0, 0}
fpsColor = { 1, 0, 0 }
elseif report.frameTime.current > 13.0 then
fpsColor = {1, 1, 0}
fpsColor = { 1, 1, 0 }
else
fpsColor = {0, 1, 0}
fpsColor = { 0, 1, 0 }
end
love.graphics.setColor(fpsColor)
love.graphics.print(string.format("FPS: %.0f (%.2fms)", report.fps.current, report.frameTime.current), x + padding, currentY)
currentY = currentY + lineHeight
-- Average FPS
love.graphics.setColor(0.8, 0.8, 0.8, 1)
love.graphics.print(string.format("Avg: %.0f fps (%.2fms)", report.fps.average, report.frameTime.average), x + padding, currentY)
currentY = currentY + lineHeight
-- Frame time stats
love.graphics.setColor(1, 1, 1, 1)
love.graphics.print(string.format("Min/Max: %.2f/%.2fms", report.frameTime.min, report.frameTime.max), x + padding, currentY)
currentY = currentY + lineHeight
love.graphics.print(string.format("P95/P99: %.2f/%.2fms", report.frameTime.p95, report.frameTime.p99), x + padding, currentY)
currentY = currentY + lineHeight + 3
-- Memory
love.graphics.setColor(0.5, 1, 0.5, 1)
love.graphics.print(string.format("Memory: %.2f MB", report.memory.current), x + padding, currentY)
@@ -324,24 +339,26 @@ function PerformanceProfiler:draw(x, y, width, height)
love.graphics.setColor(0.8, 0.8, 0.8, 1)
love.graphics.print(string.format("Peak: %.2f MB | Avg: %.2f MB", report.memory.peak, report.memory.average), x + padding, currentY)
currentY = currentY + lineHeight + 3
-- Total time and frames
love.graphics.setColor(0.7, 0.7, 1, 1)
love.graphics.print(string.format("Frames: %d | Time: %.1fs", report.frameCount, report.totalTime), x + padding, currentY)
currentY = currentY + lineHeight + 5
-- Markers (top 5 by average time)
if next(report.markers) then
love.graphics.setColor(1, 0.8, 0.4, 1)
love.graphics.print("Top Markers:", x + padding, currentY)
currentY = currentY + lineHeight
local sortedMarkers = {}
for name, data in pairs(report.markers) do
table.insert(sortedMarkers, {name = name, average = data.average})
table.insert(sortedMarkers, { name = name, average = data.average })
end
table.sort(sortedMarkers, function(a, b) return a.average > b.average end)
table.sort(sortedMarkers, function(a, b)
return a.average > b.average
end)
love.graphics.setColor(0.9, 0.9, 0.9, 1)
for i = 1, math.min(3, #sortedMarkers) do
local m = sortedMarkers[i]
@@ -367,16 +384,16 @@ end
---@return string
function PerformanceProfiler:exportJSON()
local report = self:getReport()
local function serializeValue(val, indent)
indent = indent or ""
local t = type(val)
if t == "table" then
local items = {}
local isArray = true
local count = 0
for k, _ in pairs(val) do
count = count + 1
if type(k) ~= "number" or k ~= count then
@@ -384,7 +401,7 @@ function PerformanceProfiler:exportJSON()
break
end
end
if isArray then
for _, v in ipairs(val) do
table.insert(items, serializeValue(v, indent .. " "))
@@ -414,8 +431,197 @@ function PerformanceProfiler:exportJSON()
return "null"
end
end
return serializeValue(report, "")
end
---@param profileName string
---@return boolean, string?
function PerformanceProfiler:saveReport(profileName)
local report = self:getReport()
local timestamp = os.date("%Y-%m-%d_%H-%M:%S")
local filename = string.format("%s.md", timestamp)
-- Get the actual project directory (where the profiling folder is)
local sourceDir = love.filesystem.getSource()
local profileDir, filepath
if sourceDir:match("%.love$") then
-- If running from a .love file, fall back to save directory
sourceDir = love.filesystem.getSaveDirectory()
profileDir = sourceDir .. "/reports/" .. profileName
filepath = profileDir .. "/" .. filename
-- Create profile-specific directory if it doesn't exist
love.filesystem.createDirectory("reports/" .. profileName)
return self:_saveWithLoveFS(filepath, profileName)
else
-- Running from source - sourceDir is the profiling directory
-- We want to save to profiling/reports/{profileName}/
profileDir = sourceDir .. "/reports/" .. profileName
filepath = profileDir .. "/" .. filename
-- Create profile-specific directory if it doesn't exist (using io module)
os.execute('mkdir -p "' .. profileDir .. '"')
return self:_saveWithIO(filepath, profileName)
end
end
---@param filepath string
---@param profileName string
---@return boolean, string?
function PerformanceProfiler:_saveWithIO(filepath, profileName)
local report = self:getReport()
local profileDir = filepath:match("(.*/)") -- Extract directory from path
-- Generate Markdown report
local lines = {}
table.insert(lines, "# Performance Profile Report: " .. profileName)
table.insert(lines, "")
table.insert(lines, "**Generated:** " .. os.date("%Y-%m-%d %H:%M:%S"))
table.insert(lines, "")
table.insert(lines, "---")
table.insert(lines, "")
-- Summary
table.insert(lines, "## Summary")
table.insert(lines, "")
table.insert(lines, string.format("- **Total Duration:** %.2f seconds", report.totalTime))
table.insert(lines, string.format("- **Total Frames:** %d", report.frameCount))
table.insert(lines, "")
-- FPS Statistics
table.insert(lines, "## FPS Statistics")
table.insert(lines, "")
table.insert(lines, "| Metric | Value |")
table.insert(lines, "|--------|-------|")
table.insert(lines, string.format("| Average FPS | %.2f |", report.fps.average))
table.insert(lines, string.format("| Median FPS | %.2f |", report.fps.median))
table.insert(lines, string.format("| Min FPS | %.2f |", report.fps.min))
table.insert(lines, string.format("| Max FPS | %.2f |", report.fps.max))
table.insert(lines, string.format("| **1%% Worst FPS** | **%.2f** |", report.fps.worst_1_percent))
table.insert(lines, string.format("| **0.1%% Worst FPS** | **%.2f** |", report.fps.worst_0_1_percent))
table.insert(lines, "")
table.insert(lines, "> 1% and 0.1% worst represent the FPS threshold at which 1% and 0.1% of frames performed at or below.")
table.insert(lines, "")
-- Frame Time Statistics
table.insert(lines, "## Frame Time Statistics")
table.insert(lines, "")
table.insert(lines, "| Metric | Value (ms) |")
table.insert(lines, "|--------|------------|")
table.insert(lines, string.format("| Average | %.2f |", report.frameTime.average))
table.insert(lines, string.format("| Median | %.2f |", report.frameTime.median))
table.insert(lines, string.format("| Min | %.2f |", report.frameTime.min))
table.insert(lines, string.format("| Max | %.2f |", report.frameTime.max))
table.insert(lines, string.format("| 95th Percentile | %.2f |", report.frameTime.p95))
table.insert(lines, string.format("| 99th Percentile | %.2f |", report.frameTime.p99))
table.insert(lines, string.format("| 99.9th Percentile | %.2f |", report.frameTime.p99_9))
table.insert(lines, "")
-- Memory Statistics
table.insert(lines, "## Memory Usage")
table.insert(lines, "")
table.insert(lines, "| Metric | Value (MB) |")
table.insert(lines, "|--------|------------|")
table.insert(lines, string.format("| Current | %.2f |", report.memory.current))
table.insert(lines, string.format("| Average | %.2f |", report.memory.average))
table.insert(lines, string.format("| Peak | %.2f |", report.memory.peak))
table.insert(lines, string.format("| Min | %.2f |", report.memory.min))
table.insert(lines, string.format("| 95th Percentile | %.2f |", report.memory.p95))
table.insert(lines, string.format("| 99th Percentile | %.2f |", report.memory.p99))
table.insert(lines, string.format("| 99.9th Percentile | %.2f |", report.memory.p99_9))
table.insert(lines, "")
-- Markers (if any)
if next(report.markers) then
table.insert(lines, "## Custom Markers")
table.insert(lines, "")
for name, data in pairs(report.markers) do
table.insert(lines, string.format("### %s", name))
table.insert(lines, "")
table.insert(lines, "| Metric | Value (ms) |")
table.insert(lines, "|--------|------------|")
table.insert(lines, string.format("| Average | %.3f |", data.average))
table.insert(lines, string.format("| Median | %.3f |", data.median))
table.insert(lines, string.format("| Min | %.3f |", data.min))
table.insert(lines, string.format("| Max | %.3f |", data.max))
table.insert(lines, string.format("| Count | %d |", data.count))
table.insert(lines, "")
end
end
-- Custom Metrics (if any)
if next(report.customMetrics) then
table.insert(lines, "## Custom Metrics")
table.insert(lines, "")
for name, data in pairs(report.customMetrics) do
table.insert(lines, string.format("### %s", name))
table.insert(lines, "")
table.insert(lines, "| Metric | Value |")
table.insert(lines, "|--------|-------|")
table.insert(lines, string.format("| Average | %.2f |", data.average))
table.insert(lines, string.format("| Median | %.2f |", data.median))
table.insert(lines, string.format("| Min | %.2f |", data.min))
table.insert(lines, string.format("| Max | %.2f |", data.max))
table.insert(lines, string.format("| Count | %d |", data.count))
table.insert(lines, "")
end
end
table.insert(lines, "---")
table.insert(lines, "")
-- Save to file using io module (writes to actual directory, not sandboxed)
local content = table.concat(lines, "\n")
local file, err = io.open(filepath, "w")
if not file then
return false, "Failed to open file: " .. tostring(err)
end
local success, writeErr = pcall(function()
file:write(content)
file:close()
end)
if not success then
return false, "Failed to write report: " .. tostring(writeErr)
end
-- Also save JSON version
local jsonFilepath = filepath:gsub("%.md$", ".json")
local jsonFile = io.open(jsonFilepath, "w")
if jsonFile then
pcall(function()
jsonFile:write(self:exportJSON())
jsonFile:close()
end)
end
-- Save as "latest" for easy access
local latestMarkdownPath = profileDir .. "/latest.md"
local latestJsonPath = profileDir .. "/latest.json"
local latestMdFile = io.open(latestMarkdownPath, "w")
if latestMdFile then
pcall(function()
latestMdFile:write(content)
latestMdFile:close()
end)
end
local latestJsonFile = io.open(latestJsonPath, "w")
if latestJsonFile then
pcall(function()
latestJsonFile:write(self:exportJSON())
latestJsonFile:close()
end)
end
return true, filepath
end
return PerformanceProfiler