summaryrefslogtreecommitdiff
path: root/packages/backend/scripts/measure-memory.mjs
blob: 3f30e24fb46ccfca2c49a54cd2262232cd40dbaa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
/*
 * 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);
});