/* * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ import { spawn } from 'node:child_process'; import { promises as fs } from 'node:fs'; import path from 'node:path'; export const heapSnapshotCategories = [ 'Total', 'Code', 'Strings', 'JS arrays', 'Typed arrays', 'System objects', 'Other JS objects', 'Other non-JS objects', ] as const; export function median(values: number[]) { const sorted = values.toSorted((a, b) => a - b); const center = Math.floor(sorted.length / 2); if (sorted.length % 2 === 1) return sorted[center]; return Math.round((sorted[center - 1] + sorted[center]) / 2); } export function mad(values: number[]) { if (values.length < 2) return null; const center = median(values); return median(values.map(value => Math.abs(value - center))); } export function normalizePath(filePath: string) { return filePath.split(path.sep).join('/'); } export async function fileExists(filePath: string) { try { await fs.access(filePath); return true; } catch { return false; } } export async function fileSize(filePath: string) { const stat = await fs.stat(filePath); return stat.size; } export async function* traverseDirectory(dir: string): AsyncGenerator { for (const entry of await fs.readdir(dir, { withFileTypes: true })) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { yield* traverseDirectory(fullPath); } else if (entry.isFile()) { yield fullPath; } } } export function escapeLatex(text: string) { return text .replaceAll('\\', '\\\\') .replaceAll('{', '\\{') .replaceAll('}', '\\}') .replaceAll('%', '\\%'); } export function formatColoredDelta(text: string, delta: number) { if (delta === 0) return text; const color = delta > 0 ? 'orange' : 'green'; const sign = delta > 0 ? '+' : '-'; return `$\\color{${color}}{\\text{${sign}${escapeLatex(text)}}}$`; } const numberFormatter = new Intl.NumberFormat('en-US', { maximumFractionDigits: 1, }); export function formatNumber(value: number) { return numberFormatter.format(value); } export function formatBytes(value: number) { if (value === 0) return '0 B'; const units = ['B', 'KiB', 'MiB', 'GiB']; let unitIndex = 0; let size = value; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex += 1; } const maximumFractionDigits = size >= 10 || unitIndex === 0 ? 0 : 1; return `${numberFormatter.format(Number(size.toFixed(maximumFractionDigits)))} ${units[unitIndex]}`; } export function calcAndFormatDeltaNumber(before: number, after: number) { if (before == null || after == null) return '-'; const delta = after - before; return formatColoredDelta(formatNumber(Math.abs(delta)), delta); } export function formatDeltaBytes(deltaBytes: number) { return formatColoredDelta(formatBytes(Math.abs(deltaBytes)), deltaBytes); } export function calcAndFormatDeltaBytes(before: number, after: number) { if (before == null || after == null) return '-'; const delta = after - before; return formatDeltaBytes(delta); } export function formatPercent(value: number) { return `${formatNumber(value)}%`; } export function formatDeltaPercent(deltaPercent: number) { if (deltaPercent === 0) return '0%'; return formatColoredDelta(formatPercent(Math.abs(deltaPercent)), deltaPercent); } export function calcAndFormatDeltaPercent(before: number, after: number) { if (before == null || before === 0 || after == null || after === 0) return '-'; const delta = after - before; return formatDeltaPercent(delta / before * 100); } export function commandName(command: string) { if (process.platform !== 'win32') return command; if (command === 'pnpm') return 'pnpm.cmd'; return command; } export function readIntegerEnv(name: string, defaultValue: number, min: number) { const rawValue = process.env[name]; if (rawValue == null || rawValue === '') return defaultValue; if (!/^\d+$/.test(rawValue)) throw new Error(`${name} must be an integer`); const value = Number(rawValue); if (!Number.isSafeInteger(value) || value < min) throw new Error(`${name} must be >= ${min}`); return value; } export function run(command: string, args: string[], options: { cwd?: string; env?: NodeJS.ProcessEnv; logStdout?: boolean } = {}) { return new Promise((resolvePromise, reject) => { const child = spawn(commandName(command), args, { cwd: options.cwd, env: options.env, stdio: ['ignore', 'pipe', 'pipe'], }); let stdout = ''; let stderr = ''; child.stdout.on('data', data => { stdout += data; if (options.logStdout) process.stderr.write(data); }); child.stderr.on('data', data => { stderr += data; process.stderr.write(data); }); child.on('error', reject); child.on('close', code => { if (code === 0) { resolvePromise(stdout); } else { reject(new Error(`${command} ${args.join(' ')} failed with exit code ${code}\n${stderr}`)); } }); }); }