395 lines
12 KiB
Lua
395 lines
12 KiB
Lua
local log = require("log")
|
|
---@configDefault false
|
|
|
|
local activeProfiles = setmetatable({}, {__mode = "k"})
|
|
|
|
local function createProfile()
|
|
return {
|
|
methods = {},
|
|
}
|
|
end
|
|
|
|
local function wrapMethod(element, methodName)
|
|
local originalMethod = element[methodName]
|
|
|
|
if not activeProfiles[element] then
|
|
activeProfiles[element] = createProfile()
|
|
end
|
|
if not activeProfiles[element].methods[methodName] then
|
|
activeProfiles[element].methods[methodName] = {
|
|
calls = 0,
|
|
totalTime = 0,
|
|
minTime = math.huge,
|
|
maxTime = 0,
|
|
lastTime = 0,
|
|
startTime = 0,
|
|
path = {},
|
|
methodName = methodName,
|
|
originalMethod = originalMethod
|
|
}
|
|
end
|
|
|
|
element[methodName] = function(self, ...)
|
|
self:startProfile(methodName)
|
|
local result = originalMethod(self, ...)
|
|
self:endProfile(methodName)
|
|
return result
|
|
end
|
|
end
|
|
|
|
---@splitClass
|
|
|
|
---@class BaseElement
|
|
local BaseElement = {}
|
|
|
|
--- Starts profiling a method
|
|
--- @shortDescription Starts timing a method call
|
|
--- @param methodName string The name of the method to profile
|
|
--- @return BaseElement self The element instance
|
|
function BaseElement:startProfile(methodName)
|
|
local profile = activeProfiles[self]
|
|
if not profile then
|
|
profile = createProfile()
|
|
activeProfiles[self] = profile
|
|
end
|
|
|
|
if not profile.methods[methodName] then
|
|
profile.methods[methodName] = {
|
|
calls = 0,
|
|
totalTime = 0,
|
|
minTime = math.huge,
|
|
maxTime = 0,
|
|
lastTime = 0,
|
|
startTime = 0,
|
|
path = {},
|
|
methodName = methodName
|
|
}
|
|
end
|
|
|
|
local methodProfile = profile.methods[methodName]
|
|
methodProfile.startTime = os.clock() * 1000
|
|
methodProfile.path = {}
|
|
|
|
local current = self
|
|
while current do
|
|
table.insert(methodProfile.path, 1, current.get("name") or current.get("id"))
|
|
current = current.parent
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- Ends profiling a method
|
|
--- @shortDescription Ends timing a method call and records statistics
|
|
--- @param methodName string The name of the method to stop profiling
|
|
--- @return BaseElement self The element instance
|
|
function BaseElement:endProfile(methodName)
|
|
local profile = activeProfiles[self]
|
|
if not profile or not profile.methods[methodName] then return self end
|
|
|
|
local methodProfile = profile.methods[methodName]
|
|
local endTime = os.clock() * 1000
|
|
local duration = endTime - methodProfile.startTime
|
|
|
|
methodProfile.calls = methodProfile.calls + 1
|
|
methodProfile.totalTime = methodProfile.totalTime + duration
|
|
methodProfile.minTime = math.min(methodProfile.minTime, duration)
|
|
methodProfile.maxTime = math.max(methodProfile.maxTime, duration)
|
|
methodProfile.lastTime = duration
|
|
|
|
return self
|
|
end
|
|
|
|
--- Enables benchmarking for a method
|
|
--- @shortDescription Enables performance measurement for a method
|
|
--- @param methodName string The name of the method to benchmark
|
|
--- @return BaseElement self The element instance
|
|
--- @usage element:benchmark("render")
|
|
function BaseElement:benchmark(methodName)
|
|
if not self[methodName] then
|
|
log.error("Method " .. methodName .. " does not exist")
|
|
return self
|
|
end
|
|
|
|
activeProfiles[self] = createProfile()
|
|
activeProfiles[self].methodName = methodName
|
|
activeProfiles[self].isRunning = true
|
|
|
|
wrapMethod(self, methodName)
|
|
return self
|
|
end
|
|
|
|
--- Logs benchmark statistics for a method
|
|
--- @shortDescription Logs benchmark statistics for a method
|
|
--- @param methodName string The name of the method to log
|
|
--- @return BaseElement self The element instance
|
|
function BaseElement:logBenchmark(methodName)
|
|
local profile = activeProfiles[self]
|
|
if not profile or not profile.methods[methodName] then return self end
|
|
|
|
local stats = profile.methods[methodName]
|
|
if stats then
|
|
local averageTime = stats.calls > 0 and (stats.totalTime / stats.calls) or 0
|
|
log.info(string.format(
|
|
"Benchmark results for %s.%s: " ..
|
|
"Path: %s " ..
|
|
"Calls: %d " ..
|
|
"Average time: %.2fms " ..
|
|
"Min time: %.2fms " ..
|
|
"Max time: %.2fms " ..
|
|
"Last time: %.2fms " ..
|
|
"Total time: %.2fms",
|
|
table.concat(stats.path, "."),
|
|
stats.methodName,
|
|
table.concat(stats.path, "/"),
|
|
stats.calls,
|
|
averageTime,
|
|
stats.minTime ~= math.huge and stats.minTime or 0,
|
|
stats.maxTime,
|
|
stats.lastTime,
|
|
stats.totalTime
|
|
))
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- Stops benchmarking for a method
|
|
--- @shortDescription Disables performance measurement for a method
|
|
--- @param methodName string The name of the method to stop benchmarking
|
|
--- @return BaseElement self The element instance
|
|
function BaseElement:stopBenchmark(methodName)
|
|
local profile = activeProfiles[self]
|
|
if not profile or not profile.methods[methodName] then return self end
|
|
|
|
local stats = profile.methods[methodName]
|
|
if stats and stats.originalMethod then
|
|
self[methodName] = stats.originalMethod
|
|
end
|
|
|
|
profile.methods[methodName] = nil
|
|
if not next(profile.methods) then
|
|
activeProfiles[self] = nil
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- Gets benchmark statistics for a method
|
|
--- @shortDescription Retrieves benchmark statistics for a method
|
|
--- @param methodName string The name of the method to get statistics for
|
|
--- @return table? stats The benchmark statistics or nil
|
|
function BaseElement:getBenchmarkStats(methodName)
|
|
local profile = activeProfiles[self]
|
|
if not profile or not profile.methods[methodName] then return nil end
|
|
|
|
local stats = profile.methods[methodName]
|
|
return {
|
|
averageTime = stats.totalTime / stats.calls,
|
|
totalTime = stats.totalTime,
|
|
calls = stats.calls,
|
|
minTime = stats.minTime,
|
|
maxTime = stats.maxTime,
|
|
lastTime = stats.lastTime
|
|
}
|
|
end
|
|
|
|
---@splitClass
|
|
|
|
---@class Container : VisualElement
|
|
local Container = {}
|
|
|
|
--- Enables benchmarking for a container and all its children
|
|
--- @shortDescription Recursively enables benchmarking
|
|
--- @param methodName string The method to benchmark
|
|
--- @return Container self The container instance
|
|
--- @usage container:benchmarkContainer("render")
|
|
function Container:benchmarkContainer(methodName)
|
|
self:benchmark(methodName)
|
|
|
|
for _, child in pairs(self.get("children")) do
|
|
child:benchmark(methodName)
|
|
|
|
if child:isType("Container") then
|
|
child:benchmarkContainer(methodName)
|
|
end
|
|
end
|
|
return self
|
|
end
|
|
|
|
--- Logs benchmark statistics for a container and all its children
|
|
--- @shortDescription Recursively logs benchmark statistics
|
|
--- @param methodName string The method to log
|
|
--- @return Container self The container instance
|
|
function Container:logContainerBenchmarks(methodName, depth)
|
|
depth = depth or 0
|
|
local indent = string.rep(" ", depth)
|
|
local childrenTotalTime = 0
|
|
local childrenStats = {}
|
|
|
|
for _, child in pairs(self.get("children")) do
|
|
local profile = activeProfiles[child]
|
|
if profile and profile.methods[methodName] then
|
|
local stats = profile.methods[methodName]
|
|
childrenTotalTime = childrenTotalTime + stats.totalTime
|
|
table.insert(childrenStats, {
|
|
element = child,
|
|
type = child.get("type"),
|
|
calls = stats.calls,
|
|
totalTime = stats.totalTime,
|
|
avgTime = stats.totalTime / stats.calls
|
|
})
|
|
end
|
|
end
|
|
|
|
local profile = activeProfiles[self]
|
|
if profile and profile.methods[methodName] then
|
|
local stats = profile.methods[methodName]
|
|
local selfTime = stats.totalTime - childrenTotalTime
|
|
local avgSelfTime = selfTime / stats.calls
|
|
|
|
log.info(string.format(
|
|
"%sBenchmark %s (%s): " ..
|
|
"%.2fms/call (Self: %.2fms/call) " ..
|
|
"[Total: %dms, Calls: %d]",
|
|
indent,
|
|
self.get("type"),
|
|
methodName,
|
|
stats.totalTime / stats.calls,
|
|
avgSelfTime,
|
|
stats.totalTime,
|
|
stats.calls
|
|
))
|
|
|
|
if #childrenStats > 0 then
|
|
for _, childStat in ipairs(childrenStats) do
|
|
if childStat.element:isType("Container") then
|
|
childStat.element:logContainerBenchmarks(methodName, depth + 1)
|
|
else
|
|
log.info(string.format("%s> %s: %.2fms/call [Total: %dms, Calls: %d]",
|
|
indent .. " ",
|
|
childStat.type,
|
|
childStat.avgTime,
|
|
childStat.totalTime,
|
|
childStat.calls
|
|
))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
return self
|
|
end
|
|
|
|
--- Stops benchmarking for a container and all its children
|
|
--- @shortDescription Recursively stops benchmarking
|
|
--- @param methodName string The method to stop benchmarking
|
|
--- @return Container self The container instance
|
|
function Container:stopContainerBenchmark(methodName)
|
|
for _, child in pairs(self.get("children")) do
|
|
if child:isType("Container") then
|
|
child:stopContainerBenchmark(methodName)
|
|
else
|
|
child:stopBenchmark(methodName)
|
|
end
|
|
end
|
|
|
|
self:stopBenchmark(methodName)
|
|
return self
|
|
end
|
|
|
|
--- This is the benchmark plugin. It provides performance measurement tools for elements and methods,
|
|
--- with support for hierarchical profiling and detailed statistics. The plugin is meant to be used for very big projects
|
|
--- where performance is critical. It allows you to measure the time taken by specific methods and log the results.
|
|
---@class Benchmark
|
|
local API = {}
|
|
|
|
--- Starts a custom benchmark
|
|
--- @shortDescription Starts timing a custom operation
|
|
--- @param name string The name of the benchmark
|
|
--- @param options? table Optional configuration
|
|
function API.start(name, options)
|
|
options = options or {}
|
|
local profile = createProfile()
|
|
profile.name = name
|
|
profile.startTime = os.clock() * 1000
|
|
profile.custom = true
|
|
profile.calls = 0
|
|
profile.totalTime = 0
|
|
profile.minTime = math.huge
|
|
profile.maxTime = 0
|
|
profile.lastTime = 0
|
|
activeProfiles[name] = profile
|
|
end
|
|
|
|
--- Stops a custom benchmark
|
|
--- @shortDescription Stops timing and logs results
|
|
--- @param name string The name of the benchmark to stop
|
|
function API.stop(name)
|
|
local profile = activeProfiles[name]
|
|
if not profile or not profile.custom then return end
|
|
|
|
local endTime = os.clock() * 1000
|
|
local duration = endTime - profile.startTime
|
|
|
|
profile.calls = profile.calls + 1
|
|
profile.totalTime = profile.totalTime + duration
|
|
profile.minTime = math.min(profile.minTime, duration)
|
|
profile.maxTime = math.max(profile.maxTime, duration)
|
|
profile.lastTime = duration
|
|
|
|
log.info(string.format(
|
|
"Custom Benchmark '%s': " ..
|
|
"Calls: %d " ..
|
|
"Average time: %.2fms " ..
|
|
"Min time: %.2fms " ..
|
|
"Max time: %.2fms " ..
|
|
"Last time: %.2fms " ..
|
|
"Total time: %.2fms",
|
|
name,
|
|
profile.calls,
|
|
profile.totalTime / profile.calls,
|
|
profile.minTime,
|
|
profile.maxTime,
|
|
profile.lastTime,
|
|
profile.totalTime
|
|
))
|
|
end
|
|
|
|
--- Gets statistics for a benchmark
|
|
--- @shortDescription Retrieves benchmark statistics
|
|
--- @param name string The name of the benchmark
|
|
--- @return table? stats The benchmark statistics or nil
|
|
function API.getStats(name)
|
|
local profile = activeProfiles[name]
|
|
if not profile then return nil end
|
|
|
|
return {
|
|
averageTime = profile.totalTime / profile.calls,
|
|
totalTime = profile.totalTime,
|
|
calls = profile.calls,
|
|
minTime = profile.minTime,
|
|
maxTime = profile.maxTime,
|
|
lastTime = profile.lastTime
|
|
}
|
|
end
|
|
|
|
--- Clears a specific benchmark
|
|
--- @shortDescription Removes a benchmark's data
|
|
--- @param name string The name of the benchmark to clear
|
|
function API.clear(name)
|
|
activeProfiles[name] = nil
|
|
end
|
|
|
|
--- Clears all custom benchmarks
|
|
--- @shortDescription Removes all custom benchmark data
|
|
function API.clearAll()
|
|
for k,v in pairs(activeProfiles) do
|
|
if v.custom then
|
|
activeProfiles[k] = nil
|
|
end
|
|
end
|
|
end
|
|
|
|
return {
|
|
BaseElement = BaseElement,
|
|
Container = Container,
|
|
API = API
|
|
} |