232 lines
5.3 KiB
JavaScript
232 lines
5.3 KiB
JavaScript
/*
|
|
* 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';
|
|
import * as http from 'node:http';
|
|
import * as fs from 'node:fs/promises';
|
|
|
|
const __filename = fileURLToPath(import.meta.url);
|
|
const __dirname = dirname(__filename);
|
|
|
|
const SAMPLE_COUNT = 3; // Number of samples to measure
|
|
const STARTUP_TIMEOUT = 120000; // 120 seconds timeout for server startup
|
|
const MEMORY_SETTLE_TIME = 10000; // Wait 10 seconds after startup for memory to settle
|
|
|
|
const keys = {
|
|
VmPeak: 0,
|
|
VmSize: 0,
|
|
VmHWM: 0,
|
|
VmRSS: 0,
|
|
VmData: 0,
|
|
VmStk: 0,
|
|
VmExe: 0,
|
|
VmLib: 0,
|
|
VmPTE: 0,
|
|
VmSwap: 0,
|
|
};
|
|
|
|
async function getMemoryUsage(pid) {
|
|
const status = await fs.readFile(`/proc/${pid}/status`, 'utf-8');
|
|
|
|
const result = {};
|
|
for (const key of Object.keys(keys)) {
|
|
const match = status.match(new RegExp(`${key}:\\s+(\\d+)\\s+kB`));
|
|
if (match) {
|
|
result[key] = parseInt(match[1], 10);
|
|
} else {
|
|
throw new Error(`Failed to parse ${key} from /proc/${pid}/status`);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
async function measureMemory() {
|
|
// Start the Misskey backend server using fork to enable IPC
|
|
const serverProcess = fork(join(__dirname, '../built/boot/entry.js'), ['expose-gc'], {
|
|
cwd: join(__dirname, '..'),
|
|
env: {
|
|
...process.env,
|
|
NODE_ENV: 'production',
|
|
MK_DISABLE_CLUSTERING: '1',
|
|
},
|
|
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
|
|
execArgv: [...process.execArgv, '--expose-gc'],
|
|
});
|
|
|
|
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`);
|
|
});
|
|
|
|
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
|
|
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);
|
|
|
|
const pid = serverProcess.pid;
|
|
|
|
const beforeGc = await getMemoryUsage(pid);
|
|
|
|
await triggerGc();
|
|
|
|
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
|
|
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(),
|
|
beforeGc,
|
|
afterGc,
|
|
afterRequest,
|
|
};
|
|
|
|
return result;
|
|
}
|
|
|
|
async function main() {
|
|
// 直列の方が時間的に分散されて正確そうだから直列でやる
|
|
const results = [];
|
|
for (let i = 0; i < SAMPLE_COUNT; i++) {
|
|
const res = await measureMemory();
|
|
results.push(res);
|
|
}
|
|
|
|
// Calculate averages
|
|
const beforeGc = structuredClone(keys);
|
|
const afterGc = structuredClone(keys);
|
|
const afterRequest = structuredClone(keys);
|
|
for (const res of results) {
|
|
for (const key of Object.keys(keys)) {
|
|
beforeGc[key] += res.beforeGc[key];
|
|
afterGc[key] += res.afterGc[key];
|
|
afterRequest[key] += res.afterRequest[key];
|
|
}
|
|
}
|
|
for (const key of Object.keys(keys)) {
|
|
beforeGc[key] = Math.round(beforeGc[key] / SAMPLE_COUNT);
|
|
afterGc[key] = Math.round(afterGc[key] / SAMPLE_COUNT);
|
|
afterRequest[key] = Math.round(afterRequest[key] / SAMPLE_COUNT);
|
|
}
|
|
|
|
const result = {
|
|
timestamp: new Date().toISOString(),
|
|
beforeGc,
|
|
afterGc,
|
|
afterRequest,
|
|
};
|
|
|
|
// Output as JSON to stdout
|
|
console.log(JSON.stringify(result, null, 2));
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(JSON.stringify({
|
|
error: err.message,
|
|
timestamp: new Date().toISOString(),
|
|
}));
|
|
process.exit(1);
|
|
});
|