Add backend memory usage comparison action for PRs (#16926)
* Initial plan
* Add backend memory usage comparison action
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
* Fix deprecated serverProcess.killed usage
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
* Add explicit permissions to save-pr-number job
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
* Change PR comment text from Japanese to English
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
* Inline memory measurement script to fix base ref compatibility
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
* Revert "Inline memory measurement script to fix base ref compatibility"
This reverts commit 6f76a121ef.
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
This commit is contained in:
parent
9900b3492a
commit
0b77dc8c48
|
|
@ -0,0 +1,85 @@
|
||||||
|
# this name is used in report-backend-memory.yml so be careful when change name
|
||||||
|
name: Get backend memory usage
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- packages/backend/**
|
||||||
|
- packages/misskey-js/**
|
||||||
|
- .github/workflows/get-backend-memory.yml
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
get-memory-usage:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
memory-json-name: [memory-base.json, memory-head.json]
|
||||||
|
include:
|
||||||
|
- memory-json-name: memory-base.json
|
||||||
|
ref: ${{ github.base_ref }}
|
||||||
|
- memory-json-name: memory-head.json
|
||||||
|
ref: refs/pull/${{ github.event.number }}/merge
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:18
|
||||||
|
ports:
|
||||||
|
- 54312:5432
|
||||||
|
env:
|
||||||
|
POSTGRES_DB: test-misskey
|
||||||
|
POSTGRES_HOST_AUTH_METHOD: trust
|
||||||
|
redis:
|
||||||
|
image: redis:7
|
||||||
|
ports:
|
||||||
|
- 56312:6379
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4.3.0
|
||||||
|
with:
|
||||||
|
ref: ${{ matrix.ref }}
|
||||||
|
submodules: true
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4.2.0
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v4.4.0
|
||||||
|
with:
|
||||||
|
node-version-file: '.node-version'
|
||||||
|
cache: 'pnpm'
|
||||||
|
- run: pnpm i --frozen-lockfile
|
||||||
|
- name: Check pnpm-lock.yaml
|
||||||
|
run: git diff --exit-code pnpm-lock.yaml
|
||||||
|
- name: Copy Configure
|
||||||
|
run: cp .github/misskey/test.yml .config/default.yml
|
||||||
|
- name: Build
|
||||||
|
run: pnpm build
|
||||||
|
- name: Run migrations
|
||||||
|
run: pnpm --filter backend migrate
|
||||||
|
- name: Measure memory usage
|
||||||
|
run: |
|
||||||
|
# Start the server and measure memory usage
|
||||||
|
node packages/backend/scripts/measure-memory.mjs > ${{ matrix.memory-json-name }}
|
||||||
|
- name: Upload Artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: memory-artifact-${{ matrix.memory-json-name }}
|
||||||
|
path: ${{ matrix.memory-json-name }}
|
||||||
|
|
||||||
|
save-pr-number:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions: {}
|
||||||
|
steps:
|
||||||
|
- name: Save PR number
|
||||||
|
env:
|
||||||
|
PR_NUMBER: ${{ github.event.number }}
|
||||||
|
run: |
|
||||||
|
echo "$PR_NUMBER" > ./pr_number
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: memory-artifact-pr-number
|
||||||
|
path: pr_number
|
||||||
|
|
@ -0,0 +1,122 @@
|
||||||
|
name: Report backend memory
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
types: [completed]
|
||||||
|
workflows:
|
||||||
|
- Get backend memory usage # get-backend-memory.yml
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
compare-memory:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download artifact
|
||||||
|
uses: actions/github-script@v7.1.0
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const fs = require('fs');
|
||||||
|
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
run_id: context.payload.workflow_run.id,
|
||||||
|
});
|
||||||
|
let matchArtifacts = allArtifacts.data.artifacts.filter((artifact) => {
|
||||||
|
return artifact.name.startsWith("memory-artifact-") || artifact.name == "memory-artifact"
|
||||||
|
});
|
||||||
|
await Promise.all(matchArtifacts.map(async (artifact) => {
|
||||||
|
let download = await github.rest.actions.downloadArtifact({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
artifact_id: artifact.id,
|
||||||
|
archive_format: 'zip',
|
||||||
|
});
|
||||||
|
await fs.promises.writeFile(`${process.env.GITHUB_WORKSPACE}/${artifact.name}.zip`, Buffer.from(download.data));
|
||||||
|
}));
|
||||||
|
- name: Extract all artifacts
|
||||||
|
run: |
|
||||||
|
find . -mindepth 1 -maxdepth 1 -type f -name '*.zip' -exec unzip {} -d artifacts ';'
|
||||||
|
ls -la artifacts/
|
||||||
|
- name: Load PR Number
|
||||||
|
id: load-pr-num
|
||||||
|
run: echo "pr-number=$(cat artifacts/pr_number)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Output base
|
||||||
|
run: cat ./artifacts/memory-base.json
|
||||||
|
- name: Output head
|
||||||
|
run: cat ./artifacts/memory-head.json
|
||||||
|
- name: Compare memory usage
|
||||||
|
id: compare
|
||||||
|
run: |
|
||||||
|
BASE_MEMORY=$(cat ./artifacts/memory-base.json)
|
||||||
|
HEAD_MEMORY=$(cat ./artifacts/memory-head.json)
|
||||||
|
|
||||||
|
BASE_RSS=$(echo "$BASE_MEMORY" | jq -r '.memory.rss // 0')
|
||||||
|
HEAD_RSS=$(echo "$HEAD_MEMORY" | jq -r '.memory.rss // 0')
|
||||||
|
|
||||||
|
# Calculate difference
|
||||||
|
if [ "$BASE_RSS" -gt 0 ] && [ "$HEAD_RSS" -gt 0 ]; then
|
||||||
|
DIFF=$((HEAD_RSS - BASE_RSS))
|
||||||
|
DIFF_PERCENT=$(echo "scale=2; ($DIFF * 100) / $BASE_RSS" | bc)
|
||||||
|
|
||||||
|
# Convert to MB for readability
|
||||||
|
BASE_MB=$(echo "scale=2; $BASE_RSS / 1048576" | bc)
|
||||||
|
HEAD_MB=$(echo "scale=2; $HEAD_RSS / 1048576" | bc)
|
||||||
|
DIFF_MB=$(echo "scale=2; $DIFF / 1048576" | bc)
|
||||||
|
|
||||||
|
echo "base_mb=$BASE_MB" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "head_mb=$HEAD_MB" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "diff_mb=$DIFF_MB" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "diff_percent=$DIFF_PERCENT" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "has_data=true" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
# Determine if this is a significant change (more than 5% increase)
|
||||||
|
if [ "$(echo "$DIFF_PERCENT > 5" | bc)" -eq 1 ]; then
|
||||||
|
echo "significant_increase=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "significant_increase=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "has_data=false" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
- id: build-comment
|
||||||
|
name: Build memory comment
|
||||||
|
run: |
|
||||||
|
HEADER="## Backend Memory Usage Comparison"
|
||||||
|
FOOTER="[See workflow logs for details](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})"
|
||||||
|
|
||||||
|
echo "$HEADER" > ./output.md
|
||||||
|
echo >> ./output.md
|
||||||
|
|
||||||
|
if [ "${{ steps.compare.outputs.has_data }}" == "true" ]; then
|
||||||
|
echo "| Metric | base | head | Diff |" >> ./output.md
|
||||||
|
echo "|--------|------|------|------|" >> ./output.md
|
||||||
|
echo "| RSS | ${{ steps.compare.outputs.base_mb }} MB | ${{ steps.compare.outputs.head_mb }} MB | ${{ steps.compare.outputs.diff_mb }} MB (${{ steps.compare.outputs.diff_percent }}%) |" >> ./output.md
|
||||||
|
echo >> ./output.md
|
||||||
|
|
||||||
|
if [ "${{ steps.compare.outputs.significant_increase }}" == "true" ]; then
|
||||||
|
echo "⚠️ **Warning**: Memory usage has increased by more than 5%. Please verify this is not an unintended change." >> ./output.md
|
||||||
|
echo >> ./output.md
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Could not retrieve memory usage data." >> ./output.md
|
||||||
|
echo >> ./output.md
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$FOOTER" >> ./output.md
|
||||||
|
- uses: thollander/actions-comment-pull-request@v2
|
||||||
|
with:
|
||||||
|
pr_number: ${{ steps.load-pr-num.outputs.pr-number }}
|
||||||
|
comment_tag: show_memory_diff
|
||||||
|
filePath: ./output.md
|
||||||
|
- name: Tell error to PR
|
||||||
|
uses: thollander/actions-comment-pull-request@v2
|
||||||
|
if: failure() && steps.load-pr-num.outputs.pr-number
|
||||||
|
with:
|
||||||
|
pr_number: ${{ steps.load-pr-num.outputs.pr-number }}
|
||||||
|
comment_tag: show_memory_diff_error
|
||||||
|
message: |
|
||||||
|
An error occurred while comparing backend memory usage. See [workflow logs](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}) for details.
|
||||||
|
|
@ -0,0 +1,152 @@
|
||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This script starts the Misskey backend server, waits for it to be ready,
|
||||||
|
* measures memory usage, and outputs the result as JSON.
|
||||||
|
*
|
||||||
|
* Usage: node scripts/measure-memory.mjs
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { fork } from 'node:child_process';
|
||||||
|
import { setTimeout } from 'node:timers/promises';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
const STARTUP_TIMEOUT = 120000; // 120 seconds timeout for server startup
|
||||||
|
const MEMORY_SETTLE_TIME = 10000; // Wait 10 seconds after startup for memory to settle
|
||||||
|
|
||||||
|
async function measureMemory() {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// Start the Misskey backend server using fork to enable IPC
|
||||||
|
const serverProcess = fork(join(__dirname, '../built/boot/entry.js'), [], {
|
||||||
|
cwd: join(__dirname, '..'),
|
||||||
|
env: {
|
||||||
|
...process.env,
|
||||||
|
NODE_ENV: 'test',
|
||||||
|
},
|
||||||
|
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
|
||||||
|
});
|
||||||
|
|
||||||
|
let serverReady = false;
|
||||||
|
|
||||||
|
// Listen for the 'ok' message from the server indicating it's ready
|
||||||
|
serverProcess.on('message', (message) => {
|
||||||
|
if (message === 'ok') {
|
||||||
|
serverReady = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle server output
|
||||||
|
serverProcess.stdout?.on('data', (data) => {
|
||||||
|
process.stderr.write(`[server stdout] ${data}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
serverProcess.stderr?.on('data', (data) => {
|
||||||
|
process.stderr.write(`[server stderr] ${data}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle server error
|
||||||
|
serverProcess.on('error', (err) => {
|
||||||
|
process.stderr.write(`[server error] ${err}\n`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for server to be ready or timeout
|
||||||
|
const startupStartTime = Date.now();
|
||||||
|
while (!serverReady) {
|
||||||
|
if (Date.now() - startupStartTime > STARTUP_TIMEOUT) {
|
||||||
|
serverProcess.kill('SIGTERM');
|
||||||
|
throw new Error('Server startup timeout');
|
||||||
|
}
|
||||||
|
await setTimeout(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
const startupTime = Date.now() - startupStartTime;
|
||||||
|
process.stderr.write(`Server started in ${startupTime}ms\n`);
|
||||||
|
|
||||||
|
// Wait for memory to settle
|
||||||
|
await setTimeout(MEMORY_SETTLE_TIME);
|
||||||
|
|
||||||
|
// Get memory usage from the server process via /proc
|
||||||
|
const pid = serverProcess.pid;
|
||||||
|
let memoryInfo;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fs = await import('node:fs/promises');
|
||||||
|
|
||||||
|
// Read /proc/[pid]/status for detailed memory info
|
||||||
|
const status = await fs.readFile(`/proc/${pid}/status`, 'utf-8');
|
||||||
|
const vmRssMatch = status.match(/VmRSS:\s+(\d+)\s+kB/);
|
||||||
|
const vmDataMatch = status.match(/VmData:\s+(\d+)\s+kB/);
|
||||||
|
const vmSizeMatch = status.match(/VmSize:\s+(\d+)\s+kB/);
|
||||||
|
|
||||||
|
memoryInfo = {
|
||||||
|
rss: vmRssMatch ? parseInt(vmRssMatch[1], 10) * 1024 : null,
|
||||||
|
heapUsed: vmDataMatch ? parseInt(vmDataMatch[1], 10) * 1024 : null,
|
||||||
|
vmSize: vmSizeMatch ? parseInt(vmSizeMatch[1], 10) * 1024 : null,
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
// Fallback: use ps command
|
||||||
|
process.stderr.write(`Warning: Could not read /proc/${pid}/status: ${err}\n`);
|
||||||
|
|
||||||
|
const { execSync } = await import('node:child_process');
|
||||||
|
try {
|
||||||
|
const ps = execSync(`ps -o rss= -p ${pid}`, { encoding: 'utf-8' });
|
||||||
|
const rssKb = parseInt(ps.trim(), 10);
|
||||||
|
memoryInfo = {
|
||||||
|
rss: rssKb * 1024,
|
||||||
|
heapUsed: null,
|
||||||
|
vmSize: null,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
memoryInfo = {
|
||||||
|
rss: null,
|
||||||
|
heapUsed: null,
|
||||||
|
vmSize: null,
|
||||||
|
error: 'Could not measure memory',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop the server
|
||||||
|
serverProcess.kill('SIGTERM');
|
||||||
|
|
||||||
|
// Wait for process to exit
|
||||||
|
let exited = false;
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
serverProcess.on('exit', () => {
|
||||||
|
exited = true;
|
||||||
|
resolve(undefined);
|
||||||
|
});
|
||||||
|
// Force kill after 10 seconds if not exited
|
||||||
|
setTimeout(10000).then(() => {
|
||||||
|
if (!exited) {
|
||||||
|
serverProcess.kill('SIGKILL');
|
||||||
|
}
|
||||||
|
resolve(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
startupTimeMs: startupTime,
|
||||||
|
memory: memoryInfo,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Output as JSON to stdout
|
||||||
|
console.log(JSON.stringify(result, null, 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
measureMemory().catch((err) => {
|
||||||
|
console.error(JSON.stringify({
|
||||||
|
error: err.message,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
}));
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue