Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CPU and memory usage to the performance metrics overlay #329

Merged
merged 3 commits into from
Jan 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion Core/NativeClient/Interface/PerformanceMetricsOverlay.lua
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ local tinsert = table.insert

local PerformanceMetricsOverlay = {
samples = {},
formatOverrides = {},
isEnabled = false,
messageStrings = {
NO_SAMPLES_AVAILABLE = "No performance metrics available at this time",
Expand Down Expand Up @@ -53,10 +54,33 @@ function PerformanceMetricsOverlay:GetFormattedMetricsString()

local isLastMetric = (index == #self.samples)
local separator = isLastMetric and "" or " | "
tinsert(sampleStrings, format("%s: %.2f ms%s", name, avg, separator))
local formatString = self.formatOverrides[name] or "%s: %.2f ms%s"
tinsert(sampleStrings, format(formatString, name, avg, separator))
end

return tconcat(sampleStrings, "")
end

local function toMicroseconds(time)
return time.sec * 1E6 + time.usec
end

function PerformanceMetricsOverlay:ComputeResourceUsageForInterval(
initialResourceUsage,
finalUsage,
measuredIntervalInMilliseconds
)
if measuredIntervalInMilliseconds <= 0 then
return 0
end

local initialTotal = toMicroseconds(initialResourceUsage.utime)
local finalTotal = toMicroseconds(finalUsage.utime)

local cpuTimeUsedInMicroseconds = finalTotal - initialTotal
local elapsedTimeInMicroseconds = measuredIntervalInMilliseconds * 1E3

return (cpuTimeUsedInMicroseconds / elapsedTimeInMicroseconds) * 100
end

return PerformanceMetricsOverlay
16 changes: 16 additions & 0 deletions Core/NativeClient/NativeClient.lua
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ function NativeClient:CreateMainWindow()
end

function NativeClient:StartRenderLoop()
PerformanceMetricsOverlay.formatOverrides.CPU = "%s: %.2f %%%s"
PerformanceMetricsOverlay.formatOverrides.Memory = "%s: %d MB%s"
PerformanceMetricsOverlay:StartMeasuring()
local initialResourceUsage = uv.getrusage()

-- Should probably replace with RML data binding or a similar approach later?
self.fpsDisplayTicker = C_Timer.NewTicker(2500, function()
Expand Down Expand Up @@ -115,14 +118,27 @@ function NativeClient:StartRenderLoop()
local frameEndTime = uv.hrtime()

local frameTimeInMilliseconds = (frameEndTime - frameStartTime) / 10E5
local lastMeasuredResourceUsage = uv.getrusage()

local cpuUsage = PerformanceMetricsOverlay:ComputeResourceUsageForInterval(
initialResourceUsage,
lastMeasuredResourceUsage,
frameTimeInMilliseconds
)
initialResourceUsage = lastMeasuredResourceUsage

local sample = {
CPU = cpuUsage,
Memory = collectgarbage("count") / 1024,
Frame = frameTimeInMilliseconds,
Render = cpuFrameTime / 10E5,
World = worldRenderTime / 10E5,
UI = uiRenderTime / 10E5,
Submit = commandSubmissionTime / 10E5,
UV = uvPollingTime / 10E5,
GLFW = (glfwPollingTime + replayTime) / 10E5,
"Memory",
"CPU",
"Frame",
"Render",
"World",
Expand Down
97 changes: 97 additions & 0 deletions Tests/NativeClient/Interface/PerformanceMetricsOverlay.spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -90,5 +90,102 @@ describe("PerformanceMetricsOverlay", function()
"totalFrameTime: 100.00 ms | cpuRenderTime: nan ms | worldRenderTime: 200.00 ms | interfaceRenderTime: 300.00 ms | commandSubmissionTime: 600.00 ms | uvPollingTime: 400.00 ms | glfwPollingTime: 500.00 ms"
assertEquals(actual, expected)
end)

it("should allow overriding the format for non-standard types of metrics", function()
PerformanceMetricsOverlay:StartMeasuring()

local metricsEntry = {
Memory = 1024,
Percentage = 56.75345,
Time = 250,
"Memory",
"Percentage",
"Time",
}
PerformanceMetricsOverlay:AddSample(metricsEntry)
PerformanceMetricsOverlay:AddSample(metricsEntry)

PerformanceMetricsOverlay.formatOverrides.Memory = "%s: %d MB%s"
PerformanceMetricsOverlay.formatOverrides.Percentage = "%s: %.2f %%%s"
PerformanceMetricsOverlay.formatOverrides.Time = "%s: %d milliseconds%s"

local actual = PerformanceMetricsOverlay:GetFormattedMetricsString()
local expected = "Memory: 1024 MB | Percentage: 56.75 % | Time: 250 milliseconds"
assertEquals(actual, expected)
end)

describe("ComputeResourceUsageForInterval", function()
local function uvMakeResourceUsage(seconds, microseconds)
return {
utime = { sec = seconds, usec = microseconds },
stime = { sec = 0, usec = 0 }, -- stime is ignored since it may be async background tasks etc.
}
end

it("should compute the resource usage if the measured interval is zero", function()
local initialUsage = uvMakeResourceUsage(1, 500000) -- 1.5 seconds
local finalUsage = uvMakeResourceUsage(1, 500000) -- 1.5 seconds
local measuredIntervalInMilliseconds = 0 -- 1 second
local expected = 0
local actual = PerformanceMetricsOverlay:ComputeResourceUsageForInterval(
initialUsage,
finalUsage,
measuredIntervalInMilliseconds
)
assertEquals(actual, expected)
end)

it("should compute the resource usage correctly if it's 0%", function()
local initialUsage = uvMakeResourceUsage(1, 500000) -- 1.5 seconds
local finalUsage = uvMakeResourceUsage(1, 500000) -- 1.5 seconds
local measuredIntervalInMilliseconds = 1000 -- 1 second
local expected = 0
local actual = PerformanceMetricsOverlay:ComputeResourceUsageForInterval(
initialUsage,
finalUsage,
measuredIntervalInMilliseconds
)
assertEquals(actual, expected)
end)

it("should compute the resource usage correctly if it's 100%", function()
local initialUsage = uvMakeResourceUsage(0, 500000) -- 0.5 seconds
local finalUsage = uvMakeResourceUsage(1, 0) -- 1 second
local measuredIntervalInMilliseconds = 500 -- 0.5 second
local expected = 100 -- 100% CPU usage
local actual = PerformanceMetricsOverlay:ComputeResourceUsageForInterval(
initialUsage,
finalUsage,
measuredIntervalInMilliseconds
)
assertEquals(actual, expected)
end)

it("should compute the resource usage correctly if it's more than 100%", function()
local initialUsage = uvMakeResourceUsage(0, 500000) -- 0.5 seconds
local finalUsage = uvMakeResourceUsage(2, 0) -- 1 second
local measuredIntervalInMilliseconds = 500 -- 0.5 second
local expected = 300 -- 300% CPU usage (probably a measurement error or timer inaccuracy - ignore it)
local actual = PerformanceMetricsOverlay:ComputeResourceUsageForInterval(
initialUsage,
finalUsage,
measuredIntervalInMilliseconds
)
assertEquals(actual, expected)
end)

it("should compute the resource usage correctly over the provided duration", function()
local initialUsage = uvMakeResourceUsage(1, 0) -- 1 second
local finalUsage = uvMakeResourceUsage(2, 0) -- 2 seconds
local measuredIntervalInMilliseconds = 2000 -- 2 seconds
local expected = 50 -- 50% CPU usage
local actual = PerformanceMetricsOverlay:ComputeResourceUsageForInterval(
initialUsage,
finalUsage,
measuredIntervalInMilliseconds
)
assertEquals(actual, expected)
end)
end)
end)
end)