enhance(dev): Improve mem report (#17119)

* wip

* Update report-backend-memory.yml

* Update report-backend-memory.yml

* Update measure-memory.mjs

* Update report-backend-memory.yml
This commit is contained in:
syuilo 2026-01-22 18:53:53 +09:00 committed by GitHub
parent 1adcb03b93
commit a168e7b648
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 79 additions and 14 deletions

View File

@ -85,7 +85,8 @@ jobs:
--argjson VmRSS "$(calc $1 VmRSS)" \ --argjson VmRSS "$(calc $1 VmRSS)" \
--argjson VmHWM "$(calc $1 VmHWM)" \ --argjson VmHWM "$(calc $1 VmHWM)" \
--argjson VmSize "$(calc $1 VmSize)" \ --argjson VmSize "$(calc $1 VmSize)" \
'{VmRSS: $VmRSS, VmHWM: $VmHWM, VmSize: $VmSize}') --argjson VmData "$(calc $1 VmData)" \
'{VmRSS: $VmRSS, VmHWM: $VmHWM, VmSize: $VmSize, VmData: $VmData}')
echo "$JSON" echo "$JSON"
} }
@ -93,7 +94,8 @@ jobs:
JSON=$(jq -c -n \ JSON=$(jq -c -n \
--argjson beforeGc "$(variation beforeGc)" \ --argjson beforeGc "$(variation beforeGc)" \
--argjson afterGc "$(variation afterGc)" \ --argjson afterGc "$(variation afterGc)" \
'{beforeGc: $beforeGc, afterGc: $afterGc}') --argjson afterRequest "$(variation afterRequest)" \
'{beforeGc: $beforeGc, afterGc: $afterGc, afterRequest: $afterRequest}')
echo "res=$JSON" >> "$GITHUB_OUTPUT" echo "res=$JSON" >> "$GITHUB_OUTPUT"
- id: build-comment - id: build-comment
@ -108,20 +110,37 @@ jobs:
echo >> ./output.md echo >> ./output.md
table() { table() {
echo "| Metric | base (MB) | head (MB) | Diff (MB) | Diff (%) |" >> ./output.md
echo "|--------|------:|------:|------:|------:|" >> ./output.md
line() { line() {
METRIC=$2
BASE=$(echo "$RES" | jq -r ".${1}.${2}.base") BASE=$(echo "$RES" | jq -r ".${1}.${2}.base")
HEAD=$(echo "$RES" | jq -r ".${1}.${2}.head") HEAD=$(echo "$RES" | jq -r ".${1}.${2}.head")
DIFF=$(echo "$RES" | jq -r ".${1}.${2}.diff") DIFF=$(echo "$RES" | jq -r ".${1}.${2}.diff")
DIFF_PERCENT=$(echo "$RES" | jq -r ".${1}.${2}.diff_percent") DIFF_PERCENT=$(echo "$RES" | jq -r ".${1}.${2}.diff_percent")
echo "| ${2} | ${BASE} MB | ${HEAD} MB | ${DIFF} MB (${DIFF_PERCENT}%) |" >> ./output.md if (( $(echo "$DIFF_PERCENT > 0" | bc -l) )); then
DIFF="+$DIFF"
DIFF_PERCENT="+$DIFF_PERCENT"
fi
# highlight VmRSS
if [ "$2" = "VmRSS" ]; then
METRIC="**${METRIC}**"
BASE="**${BASE}**"
HEAD="**${HEAD}**"
DIFF="**${DIFF}**"
DIFF_PERCENT="**${DIFF_PERCENT}**"
fi
echo "| ${METRIC} | ${BASE} MB | ${HEAD} MB | ${DIFF} MB | ${DIFF_PERCENT}% |" >> ./output.md
} }
echo "| Metric | base | head | Diff |" >> ./output.md
echo "|--------|------|------|------|" >> ./output.md
line $1 VmRSS line $1 VmRSS
line $1 VmHWM line $1 VmHWM
line $1 VmSize line $1 VmSize
line $1 VmData
} }
echo "### Before GC" >> ./output.md echo "### Before GC" >> ./output.md
@ -132,6 +151,10 @@ jobs:
table afterGc table afterGc
echo >> ./output.md echo >> ./output.md
echo "### After Request" >> ./output.md
table afterRequest
echo >> ./output.md
# Determine if this is a significant change (more than 5% increase) # Determine if this is a significant change (more than 5% increase)
if [ "$(echo "$RES" | jq -r '.afterGc.VmRSS.diff_percent | tonumber > 5')" = "true" ]; then if [ "$(echo "$RES" | jq -r '.afterGc.VmRSS.diff_percent | tonumber > 5')" = "true" ]; then
echo "⚠️ **Warning**: Memory usage has increased by more than 5%. Please verify this is not an unintended change." >> ./output.md echo "⚠️ **Warning**: Memory usage has increased by more than 5%. Please verify this is not an unintended change." >> ./output.md

View File

@ -14,6 +14,7 @@ import { fork } from 'node:child_process';
import { setTimeout } from 'node:timers/promises'; import { setTimeout } from 'node:timers/promises';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path'; import { dirname, join } from 'node:path';
import * as http from 'node:http';
import * as fs from 'node:fs/promises'; import * as fs from 'node:fs/promises';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
@ -88,6 +89,40 @@ async function measureMemory() {
process.stderr.write(`[server error] ${err}\n`); process.stderr.write(`[server error] ${err}\n`);
}); });
async function triggerGc() {
const ok = new Promise((resolve) => {
serverProcess.once('message', (message) => {
if (message === 'gc ok') resolve();
});
});
serverProcess.send('gc');
await ok;
await setTimeout(1000);
}
function createRequest() {
return new Promise((resolve, reject) => {
const req = http.request({
host: 'localhost',
port: 61812,
path: '/api/meta',
method: 'POST',
}, (res) => {
res.on('data', () => { });
res.on('end', () => {
resolve();
});
});
req.on('error', (err) => {
reject(err);
});
req.end();
});
}
// Wait for server to be ready or timeout // Wait for server to be ready or timeout
const startupStartTime = Date.now(); const startupStartTime = Date.now();
while (!serverReady) { while (!serverReady) {
@ -108,18 +143,20 @@ async function measureMemory() {
const beforeGc = await getMemoryUsage(pid); const beforeGc = await getMemoryUsage(pid);
serverProcess.send('gc'); await triggerGc();
await new Promise((resolve) => {
serverProcess.once('message', (message) => {
if (message === 'gc ok') resolve();
});
});
await setTimeout(1000);
const afterGc = await getMemoryUsage(pid); const afterGc = await getMemoryUsage(pid);
// create some http requests to simulate load
const REQUEST_COUNT = 10;
await Promise.all(
Array.from({ length: REQUEST_COUNT }).map(() => createRequest()),
);
await triggerGc();
const afterRequest = await getMemoryUsage(pid);
// Stop the server // Stop the server
serverProcess.kill('SIGTERM'); serverProcess.kill('SIGTERM');
@ -143,6 +180,7 @@ async function measureMemory() {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
beforeGc, beforeGc,
afterGc, afterGc,
afterRequest,
}; };
return result; return result;
@ -159,21 +197,25 @@ async function main() {
// Calculate averages // Calculate averages
const beforeGc = structuredClone(keys); const beforeGc = structuredClone(keys);
const afterGc = structuredClone(keys); const afterGc = structuredClone(keys);
const afterRequest = structuredClone(keys);
for (const res of results) { for (const res of results) {
for (const key of Object.keys(keys)) { for (const key of Object.keys(keys)) {
beforeGc[key] += res.beforeGc[key]; beforeGc[key] += res.beforeGc[key];
afterGc[key] += res.afterGc[key]; afterGc[key] += res.afterGc[key];
afterRequest[key] += res.afterRequest[key];
} }
} }
for (const key of Object.keys(keys)) { for (const key of Object.keys(keys)) {
beforeGc[key] = Math.round(beforeGc[key] / SAMPLE_COUNT); beforeGc[key] = Math.round(beforeGc[key] / SAMPLE_COUNT);
afterGc[key] = Math.round(afterGc[key] / SAMPLE_COUNT); afterGc[key] = Math.round(afterGc[key] / SAMPLE_COUNT);
afterRequest[key] = Math.round(afterRequest[key] / SAMPLE_COUNT);
} }
const result = { const result = {
timestamp: new Date().toISOString(), timestamp: new Date().toISOString(),
beforeGc, beforeGc,
afterGc, afterGc,
afterRequest,
}; };
// Output as JSON to stdout // Output as JSON to stdout