summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCopilot <198982749+Copilot@users.noreply.github.com>2025-12-03 16:02:49 +0900
committerGitHub <noreply@github.com>2025-12-03 16:02:49 +0900
commit0b77dc8c483ea8cbbb719679da3ca438d0d92535 (patch)
treefb99f81376a16621257063f183aa6e08ceaaae37
parentadd DeepWiki badge to enable auto-refresh (diff)
downloadmisskey-0b77dc8c483ea8cbbb719679da3ca438d0d92535.tar.gz
misskey-0b77dc8c483ea8cbbb719679da3ca438d0d92535.tar.bz2
misskey-0b77dc8c483ea8cbbb719679da3ca438d0d92535.zip
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 6f76a121efd450c257167cce6e298c59936f4e37. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
-rw-r--r--.github/workflows/get-backend-memory.yml85
-rw-r--r--.github/workflows/report-backend-memory.yml122
-rw-r--r--packages/backend/scripts/measure-memory.mjs152
3 files changed, 359 insertions, 0 deletions
diff --git a/.github/workflows/get-backend-memory.yml b/.github/workflows/get-backend-memory.yml
new file mode 100644
index 0000000000..6f36c088f1
--- /dev/null
+++ b/.github/workflows/get-backend-memory.yml
@@ -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
diff --git a/.github/workflows/report-backend-memory.yml b/.github/workflows/report-backend-memory.yml
new file mode 100644
index 0000000000..8ae33bc582
--- /dev/null
+++ b/.github/workflows/report-backend-memory.yml
@@ -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.
diff --git a/packages/backend/scripts/measure-memory.mjs b/packages/backend/scripts/measure-memory.mjs
new file mode 100644
index 0000000000..017252d7ec
--- /dev/null
+++ b/packages/backend/scripts/measure-memory.mjs
@@ -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);
+});