cleanup stale tests, profiling reports
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
4
profiling/reports/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
# Profiling reports - ignore all report files but keep README
|
||||
/*
|
||||
!README.md
|
||||
!.gitignore
|
||||
69
profiling/reports/README.md
Normal file
69
profiling/reports/README.md
Normal 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)
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user