/* * 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; }; 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>(readings: { elapsedMs: number; memoryUsage: T }[]) { const result = {} as Record; 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>( readMemoryUsage: () => Promise, timer: MemoryStabilityTimer = defaultTimer, ) { const startedAt = timer.now(); const readings: { elapsedMs: number; memoryUsage: T }[] = []; let maxAbsoluteSlopesKiBPerSecond: Record | 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)); } }