mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-06-25 17:10:43 +00:00
179 lines
4.9 KiB
TypeScript
179 lines
4.9 KiB
TypeScript
/*
|
|
* 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<string> {
|
|
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<string>((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}`));
|
|
}
|
|
});
|
|
});
|
|
}
|