1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-07-25 07:25:04 +02:00

enhance(dev): tweak get-backend-memory

This commit is contained in:
syuilo
2026-07-16 12:20:34 +09:00
parent 3c1f01af10
commit df063d9386
5 changed files with 131 additions and 153 deletions

View File

@@ -8,30 +8,19 @@ import { copyFile, rm, writeFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
import * as util from './utility.mts';
import * as heapSnapshotUtil from './heap-snapshot-util.mts';
import type { MemoryReportRaw } from '../../packages/backend/scripts/measure-memory.mts';
import type { MemorySample } from '../../packages/backend/scripts/measure-memory.mts';
const phases = ['afterGc'] as const;
export type MemoryReport = {
timestamp: string;
sampleCount: any;
sampleCount: number;
aggregation: string;
measurement: {
startupTimeoutMs: any;
memorySettleTimeMs: any;
ipcTimeoutMs: any;
requestCount: any;
heapSnapshot: {
enabled: any;
timeoutMs: any;
breakdownTopN: any;
};
};
summary: Record<typeof phases[number], {
memoryUsage: Record<string, number>;
heapSnapshot?: heapSnapshotUtil.HeapSnapshotData;
}>;
samples: (MemoryReportRaw['samples'][number] & {
samples: (MemorySample & {
round: number;
})[];
};
@@ -41,6 +30,8 @@ const [baseDirArg, headDirArg, baseOutputArg, headOutputArg] = process.argv.slic
const HEAP_SNAPSHOT_BREAKDOWN_TOP_N = util.readIntegerEnv('MK_MEMORY_HEAP_SNAPSHOT_BREAKDOWN_TOP_N', heapSnapshotUtil.defaultHeapSnapshotBreakdownTopN, 1);
const HEAD_HEAP_SNAPSHOT_WORK_DIR = resolve('head-heap-snapshots');
const HEAD_HEAP_SNAPSHOT_OUTPUT_PATH = resolve('head-heap-snapshot.heapsnapshot');
// Use the head checkout's measurement harness for both targets so only the built backend differs.
const MEASURE_MEMORY_SCRIPT = resolve(import.meta.dirname, '../../packages/backend/scripts/measure-memory.mts');
async function resetState(repoDir: string) {
const require = createRequire(join(repoDir, 'packages/backend/package.json'));
@@ -115,20 +106,17 @@ async function measureRepo(label: string, repoDir: string, round: number, option
process.stderr.write(`[${label}] Measuring memory\n`);
const measureEnv = {
...process.env,
MK_MEMORY_SAMPLE_COUNT: '1',
MK_MEMORY_BACKEND_DIR: resolve(repoDir, 'packages/backend'),
} as NodeJS.ProcessEnv;
if (round <= 0) measureEnv.MK_MEMORY_HEAP_SNAPSHOT = '0';
if (options.heapSnapshotSavePath != null) measureEnv.MK_MEMORY_HEAP_SNAPSHOT_SAVE_PATH = options.heapSnapshotSavePath;
const stdout = await util.run('node', ['packages/backend/scripts/measure-memory.mts'], {
const stdout = await util.run('node', [MEASURE_MEMORY_SCRIPT], {
cwd: repoDir,
env: measureEnv,
});
const report = JSON.parse(stdout) as MemoryReportRaw;
const sample = report.samples[0];
return sample;
return JSON.parse(stdout) as MemorySample;
}
function headHeapSnapshotPath(round: number) {

View File

@@ -0,0 +1,75 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { setTimeout } from 'node:timers/promises';
type MemoryStabilityTimer = {
now: () => number;
wait: (durationMs: number) => Promise<void>;
};
const intervalMs = 2000;
const maxWaitMs = 10000;
const windowSize = 3;
const slopeThresholdKiBPerSecond = 256;
const stabilityMetrics = ['Pss', 'Private_Dirty'] as const;
const defaultTimer: MemoryStabilityTimer = {
now: () => performance.now(),
wait: durationMs => setTimeout(durationMs),
};
function getMaxAbsoluteSlopes<T extends Record<string, number>>(readings: { elapsedMs: number; memoryUsage: T }[]) {
const result = {} as Record<typeof stabilityMetrics[number], number>;
for (const metric of stabilityMetrics) {
let maxAbsoluteSlope = 0;
for (let i = 1; i < readings.length; i++) {
const previous = readings[i - 1];
const current = readings[i];
const durationSeconds = (current.elapsedMs - previous.elapsedMs) / 1000;
maxAbsoluteSlope = Math.max(maxAbsoluteSlope, Math.abs(current.memoryUsage[metric] - previous.memoryUsage[metric]) / durationSeconds);
}
result[metric] = maxAbsoluteSlope;
}
return result;
}
export async function measureMemoryUntilStable<T extends Record<string, number>>(
readMemoryUsage: () => Promise<T>,
timer: MemoryStabilityTimer = defaultTimer,
) {
const startedAt = timer.now();
const readings: { elapsedMs: number; memoryUsage: T }[] = [];
let maxAbsoluteSlopesKiBPerSecond: Record<typeof stabilityMetrics[number], number> | null = null;
while (true) {
const memoryUsage = await readMemoryUsage();
const elapsedMs = timer.now() - startedAt;
readings.push({ elapsedMs, memoryUsage });
let converged = false;
if (readings.length >= windowSize) {
const latestSlopes = getMaxAbsoluteSlopes(readings.slice(-windowSize));
maxAbsoluteSlopesKiBPerSecond = latestSlopes;
converged = stabilityMetrics.every(metric => latestSlopes[metric] <= slopeThresholdKiBPerSecond);
}
if (converged || elapsedMs >= maxWaitMs) {
return {
memoryUsage,
stability: {
converged,
readingCount: readings.length,
elapsedMs,
maxAbsoluteSlopesKiBPerSecond,
},
};
}
await timer.wait(Math.min(intervalMs, maxWaitMs - elapsedMs));
}
}

View File

@@ -12,6 +12,7 @@ on:
- .github/scripts/utility.mts
- .github/scripts/backend-memory-report.mts
- .github/scripts/measure-backend-memory-comparison.mts
- .github/scripts/memory-stability-util*.mts
- .github/scripts/backend-js-footprint.mjs
- .github/scripts/backend-js-footprint-loader.mjs
- .github/scripts/backend-js-footprint-require.cjs

View File

@@ -19,6 +19,7 @@ on:
- packages/shared/eslint.config.js
- scripts/check-dts*.mjs
- .github/scripts/frontend-js-size*.mts
- .github/scripts/memory-stability-util*.mts
- .github/scripts/utility.mts
- .github/workflows/lint.yml
- package.json
@@ -37,6 +38,7 @@ on:
- packages/shared/eslint.config.js
- scripts/check-dts*.mjs
- .github/scripts/frontend-js-size*.mts
- .github/scripts/memory-stability-util*.mts
- .github/scripts/utility.mts
- .github/workflows/lint.yml
- package.json

View File

@@ -4,16 +4,15 @@
*/
import { ChildProcess, fork } from 'node:child_process';
import { setTimeout } from 'node:timers/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
import { dirname, join, resolve } from 'node:path';
import { tmpdir } from 'node:os';
//import * as http from 'node:http';
import * as fs from 'node:fs/promises';
import { analyzeHeapSnapshot, defaultHeapSnapshotBreakdownTopN, type HeapSnapshotData } from '../../../.github/scripts/heap-snapshot-util.mts';
import { measureMemoryUntilStable } from '../../../.github/scripts/memory-stability-util.mts';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const backendDir = process.env.MK_MEMORY_BACKEND_DIR == null || process.env.MK_MEMORY_BACKEND_DIR === ''
? join(import.meta.dirname, '..')
: resolve(process.env.MK_MEMORY_BACKEND_DIR);
function readIntegerEnv(name, defaultValue, min) {
const rawValue = process.env[name];
@@ -33,11 +32,8 @@ function readBooleanEnv(name, defaultValue) {
throw new Error(`${name} must be one of: 1, 0, true, false`);
}
const SAMPLE_COUNT = readIntegerEnv('MK_MEMORY_SAMPLE_COUNT', 3, 1); // Number of samples to measure
const STARTUP_TIMEOUT = readIntegerEnv('MK_MEMORY_STARTUP_TIMEOUT_MS', 120000, 1); // Timeout for server startup
const MEMORY_SETTLE_TIME = readIntegerEnv('MK_MEMORY_SETTLE_TIME_MS', 10000, 0); // Wait after startup for memory to settle
const IPC_TIMEOUT = readIntegerEnv('MK_MEMORY_IPC_TIMEOUT_MS', 30000, 1); // Timeout for IPC responses
const REQUEST_COUNT = readIntegerEnv('MK_MEMORY_REQUEST_COUNT', 10, 0);
const HEAP_SNAPSHOT = readBooleanEnv('MK_MEMORY_HEAP_SNAPSHOT', false);
const HEAP_SNAPSHOT_TIMEOUT = readIntegerEnv('MK_MEMORY_HEAP_SNAPSHOT_TIMEOUT_MS', 120000, 1);
const HEAP_SNAPSHOT_BREAKDOWN_TOP_N = readIntegerEnv('MK_MEMORY_HEAP_SNAPSHOT_BREAKDOWN_TOP_N', defaultHeapSnapshotBreakdownTopN, 1);
@@ -187,17 +183,23 @@ async function getHeapSnapshotStatistics(serverProcess: ChildProcess): Promise<H
async function getAllMemoryUsage(serverProcess: ChildProcess) {
const pid = serverProcess.pid!;
const stableSmapsRollup = await measureMemoryUntilStable(
() => getSmapsRollupMemoryUsage(pid),
);
return {
...await getMemoryUsage(pid),
...await getSmapsRollupMemoryUsage(pid),
...await getRuntimeMemoryUsage(serverProcess),
memoryUsage: {
...await getMemoryUsage(pid),
...stableSmapsRollup.memoryUsage,
...await getRuntimeMemoryUsage(serverProcess),
},
stability: stableSmapsRollup.stability,
};
}
async function measureMemory() {
// Start the Misskey backend server using fork to enable IPC
const serverProcess = fork(join(__dirname, '../built/entry.js'), [], {
cwd: join(__dirname, '..'),
const serverProcess = fork(join(backendDir, 'built/entry.js'), [], {
cwd: backendDir,
env: {
...process.env,
NODE_ENV: 'production',
@@ -209,16 +211,13 @@ async function measureMemory() {
execArgv: [...process.execArgv, '--expose-gc'],
});
let serverReady = false;
const serverReady = waitForMessage(
serverProcess,
(message): message is 'ok' => message === 'ok',
'server startup',
STARTUP_TIMEOUT,
);
// 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}`);
});
@@ -227,7 +226,6 @@ async function measureMemory() {
process.stderr.write(`[server stderr] ${data}`);
});
// Handle server error
serverProcess.on('error', (err) => {
process.stderr.write(`[server error] ${err}\n`);
});
@@ -245,141 +243,55 @@ async function measureMemory() {
if (message === 'gc unavailable') {
throw new Error('GC is unavailable. Start the process with --expose-gc to enable this feature.');
}
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);
try {
await serverReady;
} catch (err) {
serverProcess.kill('SIGTERM');
throw err;
}
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 beforeGc = await getAllMemoryUsage(serverProcess);
await triggerGc();
const memoryUsageAfterGC = await getAllMemoryUsage(serverProcess);
//// create some http requests to simulate load
//await Promise.all(
// Array.from({ length: REQUEST_COUNT }).map(() => createRequest()),
//);
//await triggerGc();
//const afterRequest = await getAllMemoryUsage(serverProcess);
const afterGc = await getAllMemoryUsage(serverProcess);
process.stderr.write(`Memory ${afterGc.stability.converged ? 'stabilized' : 'did not stabilize'} after ${afterGc.stability.readingCount} readings over ${Math.round(afterGc.stability.elapsedMs)}ms\n`);
const heapSnapshotAfterGc = await getHeapSnapshotStatistics(serverProcess);
// 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 serverExited = new Promise<void>(resolve => {
const timer = globalThis.setTimeout(() => {
serverProcess.kill('SIGKILL');
resolve();
}, 10000);
serverProcess.once('exit', () => {
globalThis.clearTimeout(timer);
resolve();
});
});
serverProcess.kill('SIGTERM');
await serverExited;
const result = {
return {
timestamp: new Date().toISOString(),
phases: {
//beforeGc,
afterGc: {
memoryUsage: memoryUsageAfterGC,
memoryUsage: afterGc.memoryUsage,
memoryStability: afterGc.stability,
heapSnapshot: heapSnapshotAfterGc,
},
//afterRequest,
},
};
return result;
}
export type MemoryReportRaw = {
timestamp: string;
sampleCount: number;
measurement: {
startupTimeoutMs: number;
memorySettleTimeMs: number;
ipcTimeoutMs: number;
requestCount: number;
heapSnapshot: {
enabled: boolean;
timeoutMs: number;
breakdownTopN: number;
};
};
samples: Awaited<ReturnType<typeof measureMemory>>[];
};
export type MemorySample = Awaited<ReturnType<typeof measureMemory>>;
async function main() {
const results = [];
for (let i = 0; i < SAMPLE_COUNT; i++) {
process.stderr.write(`Starting sample ${i + 1}/${SAMPLE_COUNT}\n`);
const res = await measureMemory();
results.push(res);
}
const result: MemoryReportRaw = {
timestamp: new Date().toISOString(),
sampleCount: SAMPLE_COUNT,
measurement: {
startupTimeoutMs: STARTUP_TIMEOUT,
memorySettleTimeMs: MEMORY_SETTLE_TIME,
ipcTimeoutMs: IPC_TIMEOUT,
requestCount: REQUEST_COUNT,
heapSnapshot: {
enabled: HEAP_SNAPSHOT,
timeoutMs: HEAP_SNAPSHOT_TIMEOUT,
breakdownTopN: HEAP_SNAPSHOT_BREAKDOWN_TOP_N,
},
},
samples: results,
};
// Output as JSON to stdout
console.log(JSON.stringify(result, null, 2));
console.log(JSON.stringify(await measureMemory(), null, 2));
}
main().catch((err) => {