mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-28 08:04:37 +02:00
Merge remote-tracking branch 'msky/develop' into feature/role-hidden-setting
This commit is contained in:
@@ -99,7 +99,7 @@ function summarizeSamples(samples: MemoryReport['samples']) {
|
||||
return summary;
|
||||
}
|
||||
|
||||
async function measureRepo(label: string, repoDir: string, round: number, options: { heapSnapshotSavePath?: string } = {}) {
|
||||
async function genSample(label: string, repoDir: string, round: number, options: { heapSnapshotSavePath?: string } = {}) {
|
||||
process.stderr.write(`[${label}] Resetting database and Redis\n`);
|
||||
await resetState(repoDir);
|
||||
|
||||
@@ -188,7 +188,7 @@ async function main() {
|
||||
for (let round = 1; round <= warmupRounds; round++) {
|
||||
process.stderr.write(`Starting warmup round ${round}/${warmupRounds}\n`);
|
||||
for (const label of heapSnapshotLabels) {
|
||||
await measureRepo(label, reports[label].dir, -round);
|
||||
await genSample(label, reports[label].dir, -round);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,7 +198,7 @@ async function main() {
|
||||
|
||||
for (const label of order) {
|
||||
const options = { heapSnapshotSavePath: heapSnapshotPath(label, round) };
|
||||
const sample = await measureRepo(label, reports[label].dir, round, options);
|
||||
const sample = await genSample(label, reports[label].dir, round, options);
|
||||
reports[label].samples.push({
|
||||
...sample,
|
||||
round,
|
||||
183
.github/scripts/backend-diagnostics.render-md.mts
vendored
Normal file
183
.github/scripts/backend-diagnostics.render-md.mts
vendored
Normal file
@@ -0,0 +1,183 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import * as util from './utility.mts';
|
||||
import * as heapSnapshotUtil from './heap-snapshot-util.mts';
|
||||
import type { MemoryReport } from './backend-diagnostics.inspect.mts';
|
||||
|
||||
const [baseFile, headFile, outputFile] = process.argv.slice(2);
|
||||
|
||||
const memoryReportPhases = [
|
||||
{
|
||||
key: 'afterGc',
|
||||
title: 'After GC',
|
||||
},
|
||||
] as const;
|
||||
|
||||
const memoryMetrics = [
|
||||
'HeapUsed',
|
||||
'Pss',
|
||||
'USS',
|
||||
'External',
|
||||
] as const;
|
||||
|
||||
function formatMemoryMb(valueKiB: number | null | undefined) {
|
||||
if (valueKiB == null) return '-';
|
||||
return `${util.formatNumber(valueKiB / 1000)} MB`;
|
||||
}
|
||||
|
||||
function formatMemoryMetricName(metric: typeof memoryMetrics[number]) {
|
||||
return metric === 'Pss' ? 'PSS' : metric;
|
||||
}
|
||||
|
||||
function getMemoryValueFromSample(sample: MemoryReport['samples'][number], phase: typeof memoryReportPhases[number]['key'], metric: typeof memoryMetrics[number]) {
|
||||
const memoryUsage = sample.phases[phase].memoryUsage;
|
||||
if (metric !== 'USS') return memoryUsage[metric];
|
||||
return memoryUsage.Private_Clean + memoryUsage.Private_Dirty;
|
||||
}
|
||||
|
||||
function getSampleValues(report: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof memoryMetrics[number]) {
|
||||
return report.samples.map(sample => getMemoryValueFromSample(sample, phase, metric));
|
||||
}
|
||||
|
||||
function getMemoryValue(report: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof memoryMetrics[number]) {
|
||||
if (metric !== 'USS') return report.summary[phase].memoryUsage[metric];
|
||||
return util.median(getSampleValues(report, phase, metric));
|
||||
}
|
||||
|
||||
function getSampleSpread(report: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof memoryMetrics[number]) {
|
||||
const values = getSampleValues(report, phase, metric);
|
||||
if (values.length < 2) return null;
|
||||
|
||||
const center = util.median(values);
|
||||
return util.median(values.map(value => Math.abs(value - center)));
|
||||
}
|
||||
|
||||
function renderMainTableForPhase(base: MemoryReport, head: MemoryReport, phase: typeof memoryReportPhases[number]['key']) {
|
||||
const lines = [
|
||||
'| Metric | Base | Head | Δ median | Δ MAD | Δ min | Δ max |',
|
||||
'| --- | ---: | ---: | ---: | ---: | ---: | ---: |',
|
||||
];
|
||||
|
||||
function formatDeltaMemory(deltaKiB: number) {
|
||||
return util.formatColoredDelta(deltaKiB, v => formatMemoryMb(v), 100); // 0.1 MB threshold
|
||||
}
|
||||
|
||||
for (const metric of memoryMetrics) {
|
||||
const baseValue = getMemoryValue(base, phase, metric);
|
||||
const headValue = getMemoryValue(head, phase, metric);
|
||||
|
||||
const baseSpread = getSampleSpread(base, phase, metric);
|
||||
const headSpread = getSampleSpread(head, phase, metric);
|
||||
const summary = util.pairedDeltaSummary(base.samples, head.samples, (sample) => getMemoryValueFromSample(sample, phase, metric));
|
||||
const percent = summary.median * 100 / baseValue;
|
||||
const deltaMedian = `${formatDeltaMemory(summary.median)}<br>${util.formatDeltaPercent(percent, 0.1).replaceAll('\\%', '\\\\%')}`;
|
||||
|
||||
lines.push(`| **${formatMemoryMetricName(metric)}** | ${formatMemoryMb(baseValue)} <br> ± ${formatMemoryMb(baseSpread)} | ${formatMemoryMb(headValue)} <br> ± ${formatMemoryMb(headSpread)} | ${deltaMedian} | ${formatMemoryMb(summary.mad)} | ${formatDeltaMemory(summary.min)} | ${formatDeltaMemory(summary.max)} |`);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function renderHeapSnapshotSection(base: MemoryReport, head: MemoryReport) {
|
||||
const baseHeapSnapshotReport = {
|
||||
summary: base.summary.afterGc.heapSnapshot!,
|
||||
samples: base.samples.map(sample => ({
|
||||
round: sample.round,
|
||||
data: sample.phases.afterGc.heapSnapshot!,
|
||||
})),
|
||||
};
|
||||
|
||||
const headHeapSnapshotReport = {
|
||||
summary: head.summary.afterGc.heapSnapshot!,
|
||||
samples: head.samples.map(sample => ({
|
||||
round: sample.round,
|
||||
data: sample.phases.afterGc.heapSnapshot!,
|
||||
})),
|
||||
};
|
||||
|
||||
const table = heapSnapshotUtil.renderHeapSnapshotTable(baseHeapSnapshotReport, headHeapSnapshotReport);
|
||||
if (table == null) return null;
|
||||
|
||||
const lines = [
|
||||
'### V8 Heap Snapshot Statistics',
|
||||
'',
|
||||
table,
|
||||
'',
|
||||
];
|
||||
|
||||
for (const graph of [
|
||||
//heapSnapshotUtil.renderHeapSnapshotSankey(baseHeapSnapshotReport, 'Base'),
|
||||
heapSnapshotUtil.renderHeapSnapshotSankey(headHeapSnapshotReport, 'Head'),
|
||||
]) {
|
||||
if (graph == null) continue;
|
||||
lines.push(graph);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
const base = JSON.parse(await readFile(baseFile, 'utf8')) as MemoryReport;
|
||||
const head = JSON.parse(await readFile(headFile, 'utf8')) as MemoryReport;
|
||||
|
||||
const lines = [
|
||||
'## ⚙️ Backend Diagnostics Report',
|
||||
'',
|
||||
];
|
||||
|
||||
//const summary = measurementSummary(base, head);
|
||||
//if (summary != null) {
|
||||
// lines.push(summary);
|
||||
// lines.push('');
|
||||
//}
|
||||
|
||||
for (const phase of memoryReportPhases) {
|
||||
lines.push(`### Memory: ${phase.title}`);
|
||||
lines.push(renderMainTableForPhase(base, head, phase.key));
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
const heapSnapshotSection = renderHeapSnapshotSection(base, head);
|
||||
if (heapSnapshotSection != null) {
|
||||
lines.push(heapSnapshotSection);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
const baseHeapSnapshotArtifactUrl = process.env.MK_MEMORY_HEAP_SNAPSHOT_ARTIFACT_URL_BASE!.trim();
|
||||
const headHeapSnapshotArtifactUrl = process.env.MK_MEMORY_HEAP_SNAPSHOT_ARTIFACT_URL_HEAD!.trim();
|
||||
lines.push(`You can download the representative heap snapshot: [base](${baseHeapSnapshotArtifactUrl}) / [head](${headHeapSnapshotArtifactUrl})`);
|
||||
lines.push('');
|
||||
|
||||
function getDiffPercent(base: MemoryReport, head: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof memoryMetrics[number]) {
|
||||
const baseValue = getMemoryValue(base, phase, metric);
|
||||
const headValue = getMemoryValue(head, phase, metric);
|
||||
return ((headValue - baseValue) * 100) / baseValue;
|
||||
}
|
||||
|
||||
function isBeyondSampleNoise(base: MemoryReport, head: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof memoryMetrics[number]) {
|
||||
const baseValue = getMemoryValue(base, phase, metric);
|
||||
const headValue = getMemoryValue(head, phase, metric);
|
||||
|
||||
const delta = headValue - baseValue;
|
||||
if (delta <= 0) return false;
|
||||
|
||||
const baseSpread = getSampleSpread(base, phase, metric);
|
||||
const headSpread = getSampleSpread(head, phase, metric);
|
||||
if (baseSpread == null || headSpread == null) return true;
|
||||
|
||||
const combinedSpread = Math.hypot(baseSpread, headSpread);
|
||||
return delta > combinedSpread * 3;
|
||||
}
|
||||
|
||||
const warningMetric = 'Pss';
|
||||
const warningDiffPercent = getDiffPercent(base, head, 'afterGc', warningMetric);
|
||||
if (warningDiffPercent > 5 && isBeyondSampleNoise(base, head, 'afterGc', warningMetric)) {
|
||||
lines.push(`⚠️ **Warning**: Memory usage (${formatMemoryMetricName(warningMetric)}) has increased by more than 5% and exceeds the observed sample noise. Please verify this is not an unintended change.`);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
await writeFile(outputFile, `${lines.join('\n')}\n`);
|
||||
46
.github/scripts/backend-js-footprint-loader.mjs
vendored
46
.github/scripts/backend-js-footprint-loader.mjs
vendored
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { appendFileSync, statSync } from 'node:fs';
|
||||
import { extname } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const traceFile = process.env.MK_BACKEND_JS_FOOTPRINT_TRACE;
|
||||
const jsExtensions = new Set(['.js', '.mjs', '.cjs']);
|
||||
|
||||
function recordLoadedFile(kind, url, format) {
|
||||
if (traceFile == null || !url.startsWith('file:')) return;
|
||||
|
||||
let filePath;
|
||||
try {
|
||||
filePath = fileURLToPath(url);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const extension = extname(filePath);
|
||||
if (!jsExtensions.has(extension)) return;
|
||||
|
||||
let size = null;
|
||||
try {
|
||||
size = statSync(filePath).size;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
appendFileSync(traceFile, `${JSON.stringify({
|
||||
kind,
|
||||
format,
|
||||
path: filePath,
|
||||
size,
|
||||
timestamp: Date.now(),
|
||||
})}\n`);
|
||||
}
|
||||
|
||||
export async function load(url, context, nextLoad) {
|
||||
const result = await nextLoad(url, context);
|
||||
recordLoadedFile('esm', url, result.format ?? context.format ?? null);
|
||||
return result;
|
||||
}
|
||||
46
.github/scripts/backend-js-footprint-require.cjs
vendored
46
.github/scripts/backend-js-footprint-require.cjs
vendored
@@ -1,46 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { appendFileSync, statSync } = require('node:fs');
|
||||
const Module = require('node:module');
|
||||
const { extname } = require('node:path');
|
||||
|
||||
const traceFile = process.env.MK_BACKEND_JS_FOOTPRINT_TRACE;
|
||||
const jsExtensions = new Set(['.js', '.mjs', '.cjs']);
|
||||
|
||||
function recordLoadedFile(kind, filePath, request) {
|
||||
if (traceFile == null || typeof filePath !== 'string') return;
|
||||
|
||||
const extension = extname(filePath);
|
||||
if (!jsExtensions.has(extension) && extension !== '.node') return;
|
||||
|
||||
let size = null;
|
||||
try {
|
||||
size = statSync(filePath).size;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
appendFileSync(traceFile, `${JSON.stringify({
|
||||
kind,
|
||||
format: extension === '.node' ? 'native' : 'commonjs',
|
||||
path: filePath,
|
||||
request,
|
||||
size,
|
||||
timestamp: Date.now(),
|
||||
})}\n`);
|
||||
}
|
||||
|
||||
const originalLoad = Module._load;
|
||||
const originalResolveFilename = Module._resolveFilename;
|
||||
|
||||
Module._load = function load(request, parent, isMain) {
|
||||
const resolved = originalResolveFilename.call(this, request, parent, isMain);
|
||||
const result = originalLoad.apply(this, arguments);
|
||||
recordLoadedFile('cjs', resolved, request);
|
||||
return result;
|
||||
};
|
||||
473
.github/scripts/backend-js-footprint.mjs
vendored
473
.github/scripts/backend-js-footprint.mjs
vendored
@@ -1,473 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { fork, spawn } from 'node:child_process';
|
||||
import { createRequire } from 'node:module';
|
||||
import { cpus, tmpdir } from 'node:os';
|
||||
import { dirname, extname, join, relative, resolve, sep } from 'node:path';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { gzipSync } from 'node:zlib';
|
||||
import * as fs from 'node:fs/promises';
|
||||
import * as fsSync from 'node:fs';
|
||||
import * as http from 'node:http';
|
||||
import * as util from './utility.mts';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const [repoDirArg, outputFileArg] = process.argv.slice(2);
|
||||
|
||||
const STARTUP_TIMEOUT = util.readIntegerEnv('MK_JS_FOOTPRINT_STARTUP_TIMEOUT_MS', 120000, 1);
|
||||
const SETTLE_TIME = util.readIntegerEnv('MK_JS_FOOTPRINT_SETTLE_TIME_MS', 10000, 0);
|
||||
const REQUEST_COUNT = util.readIntegerEnv('MK_JS_FOOTPRINT_REQUEST_COUNT', 10, 0);
|
||||
|
||||
const repoDir = resolve(repoDirArg);
|
||||
const outputFile = resolve(outputFileArg);
|
||||
const backendDir = join(repoDir, 'packages/backend');
|
||||
const backendBuiltDir = join(backendDir, 'built');
|
||||
const traceFile = join(tmpdir(), `misskey-backend-js-footprint-${process.pid}-${Date.now()}.jsonl`);
|
||||
const require = createRequire(join(repoDir, 'package.json'));
|
||||
const ts = require('typescript');
|
||||
const jsExtensions = new Set(['.js', '.mjs', '.cjs']);
|
||||
const fileMetricCache = new Map();
|
||||
const packageInfoCache = new Map();
|
||||
const nativePackageNames = new Set();
|
||||
|
||||
function isInside(parent, child) {
|
||||
const rel = relative(parent, child);
|
||||
return rel === '' || (!rel.startsWith('..') && !rel.includes(`..${sep}`));
|
||||
}
|
||||
|
||||
function bytesToKiB(value) {
|
||||
return Math.round(value / 1024);
|
||||
}
|
||||
|
||||
async function resetState() {
|
||||
const backendRequire = createRequire(join(backendDir, 'package.json'));
|
||||
const pg = backendRequire('pg');
|
||||
const Redis = backendRequire('ioredis');
|
||||
|
||||
const postgres = new pg.Client({
|
||||
host: '127.0.0.1',
|
||||
port: 54312,
|
||||
database: 'postgres',
|
||||
user: 'postgres',
|
||||
});
|
||||
|
||||
await postgres.connect();
|
||||
try {
|
||||
await postgres.query('DROP DATABASE IF EXISTS "test-misskey" WITH (FORCE)');
|
||||
await postgres.query('CREATE DATABASE "test-misskey"');
|
||||
} finally {
|
||||
await postgres.end();
|
||||
}
|
||||
|
||||
const redis = new Redis({ host: '127.0.0.1', port: 56312 });
|
||||
try {
|
||||
await redis.flushall();
|
||||
} finally {
|
||||
redis.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
function createRequest() {
|
||||
return new Promise((resolvePromise, reject) => {
|
||||
const req = http.request({
|
||||
host: 'localhost',
|
||||
port: 61812,
|
||||
path: '/api/meta',
|
||||
method: 'POST',
|
||||
}, res => {
|
||||
res.on('data', () => { });
|
||||
res.on('end', () => resolvePromise());
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
async function waitForServerReady(serverProcess) {
|
||||
let serverReady = false;
|
||||
serverProcess.on('message', message => {
|
||||
if (message === 'ok') serverReady = true;
|
||||
});
|
||||
|
||||
const startupStartTime = Date.now();
|
||||
while (!serverReady) {
|
||||
if (Date.now() - startupStartTime > STARTUP_TIMEOUT) {
|
||||
serverProcess.kill('SIGTERM');
|
||||
throw new Error('Server startup timeout');
|
||||
}
|
||||
await setTimeout(100);
|
||||
}
|
||||
}
|
||||
|
||||
async function stopServer(serverProcess) {
|
||||
serverProcess.kill('SIGTERM');
|
||||
|
||||
let exited = false;
|
||||
await new Promise(resolvePromise => {
|
||||
serverProcess.on('exit', () => {
|
||||
exited = true;
|
||||
resolvePromise(undefined);
|
||||
});
|
||||
|
||||
setTimeout(10000).then(() => {
|
||||
if (!exited) serverProcess.kill('SIGKILL');
|
||||
resolvePromise(undefined);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getPackageNameFromPath(filePath) {
|
||||
const normalized = util.normalizePath(filePath);
|
||||
const marker = '/node_modules/';
|
||||
const index = normalized.lastIndexOf(marker);
|
||||
if (index === -1) return null;
|
||||
|
||||
const rest = normalized.slice(index + marker.length).split('/');
|
||||
if (rest[0] === '.pnpm') {
|
||||
const nestedNodeModulesIndex = rest.indexOf('node_modules');
|
||||
if (nestedNodeModulesIndex === -1) return null;
|
||||
const packageParts = rest.slice(nestedNodeModulesIndex + 1);
|
||||
if (packageParts.length === 0) return null;
|
||||
return packageParts[0].startsWith('@') ? packageParts.slice(0, 2).join('/') : packageParts[0];
|
||||
}
|
||||
|
||||
return rest[0]?.startsWith('@') ? rest.slice(0, 2).join('/') : rest[0] ?? null;
|
||||
}
|
||||
|
||||
function findPackageDir(filePath, packageName) {
|
||||
const normalizedPackageName = packageName.split('/').join(sep);
|
||||
let current = dirname(filePath);
|
||||
|
||||
while (current !== dirname(current)) {
|
||||
if (current.endsWith(`${sep}${normalizedPackageName}`) && fsSync.existsSync(join(current, 'package.json'))) {
|
||||
return current;
|
||||
}
|
||||
|
||||
const parent = dirname(current);
|
||||
if (parent === current) break;
|
||||
current = parent;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function readPackageInfo(filePath) {
|
||||
const externalPackageName = getPackageNameFromPath(filePath);
|
||||
if (externalPackageName != null) {
|
||||
const packageDir = findPackageDir(filePath, externalPackageName);
|
||||
const cacheKey = packageDir ?? externalPackageName;
|
||||
if (packageInfoCache.has(cacheKey)) return packageInfoCache.get(cacheKey);
|
||||
|
||||
let version = null;
|
||||
if (packageDir != null) {
|
||||
try {
|
||||
const packageJson = JSON.parse(fsSync.readFileSync(join(packageDir, 'package.json'), 'utf8'));
|
||||
version = typeof packageJson.version === 'string' ? packageJson.version : null;
|
||||
} catch { }
|
||||
}
|
||||
|
||||
const info = {
|
||||
category: 'external',
|
||||
name: externalPackageName,
|
||||
version,
|
||||
dir: packageDir,
|
||||
};
|
||||
packageInfoCache.set(cacheKey, info);
|
||||
return info;
|
||||
}
|
||||
|
||||
if (isInside(backendBuiltDir, filePath)) {
|
||||
return {
|
||||
category: 'internal',
|
||||
name: 'backend',
|
||||
version: null,
|
||||
dir: backendDir,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
category: 'internal',
|
||||
name: 'workspace',
|
||||
version: null,
|
||||
dir: repoDir,
|
||||
};
|
||||
}
|
||||
|
||||
function analyzeSource(filePath, source) {
|
||||
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS);
|
||||
const metrics = {
|
||||
astNodeCount: 0,
|
||||
functionCount: 0,
|
||||
classCount: 0,
|
||||
stringLiteralBytes: 0,
|
||||
};
|
||||
|
||||
function visit(node) {
|
||||
metrics.astNodeCount += 1;
|
||||
|
||||
if (
|
||||
ts.isFunctionDeclaration(node) ||
|
||||
ts.isFunctionExpression(node) ||
|
||||
ts.isArrowFunction(node) ||
|
||||
ts.isMethodDeclaration(node) ||
|
||||
ts.isConstructorDeclaration(node) ||
|
||||
ts.isGetAccessorDeclaration(node) ||
|
||||
ts.isSetAccessorDeclaration(node)
|
||||
) {
|
||||
metrics.functionCount += 1;
|
||||
} else if (ts.isClassDeclaration(node) || ts.isClassExpression(node)) {
|
||||
metrics.classCount += 1;
|
||||
} else if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
||||
metrics.stringLiteralBytes += Buffer.byteLength(node.text);
|
||||
}
|
||||
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
|
||||
visit(sourceFile);
|
||||
return metrics;
|
||||
}
|
||||
|
||||
function readFileMetrics(filePath) {
|
||||
if (fileMetricCache.has(filePath)) return fileMetricCache.get(filePath);
|
||||
|
||||
const source = fsSync.readFileSync(filePath);
|
||||
const sourceText = source.toString('utf8');
|
||||
const astMetrics = analyzeSource(filePath, sourceText);
|
||||
const packageInfo = readPackageInfo(filePath);
|
||||
const metric = {
|
||||
path: filePath,
|
||||
displayPath: util.normalizePath(relative(repoDir, filePath)),
|
||||
sourceBytes: source.byteLength,
|
||||
gzipBytes: gzipSync(source).byteLength,
|
||||
...astMetrics,
|
||||
package: packageInfo,
|
||||
};
|
||||
|
||||
fileMetricCache.set(filePath, metric);
|
||||
return metric;
|
||||
}
|
||||
|
||||
async function readTraceRecords() {
|
||||
let content = '';
|
||||
try {
|
||||
content = await fs.readFile(traceFile, 'utf8');
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') return [];
|
||||
throw err;
|
||||
}
|
||||
|
||||
const records = [];
|
||||
for (const line of content.split('\n')) {
|
||||
if (line.trim() === '') continue;
|
||||
try {
|
||||
records.push(JSON.parse(line));
|
||||
} catch { }
|
||||
}
|
||||
return records;
|
||||
}
|
||||
|
||||
function emptyTotals() {
|
||||
return {
|
||||
loadedJsModules: 0,
|
||||
loadedJsSourceBytes: 0,
|
||||
loadedJsGzipBytes: 0,
|
||||
astNodeCount: 0,
|
||||
functionCount: 0,
|
||||
classCount: 0,
|
||||
stringLiteralBytes: 0,
|
||||
externalPackageCount: 0,
|
||||
nativeAddonPackageCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
function addFileMetrics(target, metric) {
|
||||
target.loadedJsModules += 1;
|
||||
target.loadedJsSourceBytes += metric.sourceBytes;
|
||||
target.loadedJsGzipBytes += metric.gzipBytes;
|
||||
target.astNodeCount += metric.astNodeCount;
|
||||
target.functionCount += metric.functionCount;
|
||||
target.classCount += metric.classCount;
|
||||
target.stringLiteralBytes += metric.stringLiteralBytes;
|
||||
}
|
||||
|
||||
function summarizeRecords(records, phase) {
|
||||
const jsPaths = new Set();
|
||||
const nativePaths = new Set();
|
||||
|
||||
for (const record of records) {
|
||||
if (typeof record.path !== 'string') continue;
|
||||
|
||||
const extension = extname(record.path);
|
||||
if (jsExtensions.has(extension)) {
|
||||
jsPaths.add(resolve(record.path));
|
||||
} else if (extension === '.node') {
|
||||
nativePaths.add(resolve(record.path));
|
||||
}
|
||||
}
|
||||
|
||||
for (const nativePath of nativePaths) {
|
||||
const packageInfo = readPackageInfo(nativePath);
|
||||
if (packageInfo.category === 'external') nativePackageNames.add(packageInfo.name);
|
||||
}
|
||||
|
||||
const totals = emptyTotals();
|
||||
const packages = new Map();
|
||||
const modules = [];
|
||||
|
||||
for (const filePath of [...jsPaths].toSorted()) {
|
||||
let metric;
|
||||
try {
|
||||
metric = readFileMetrics(filePath);
|
||||
} catch (err) {
|
||||
process.stderr.write(`Failed to analyze ${filePath}: ${err.message}\n`);
|
||||
continue;
|
||||
}
|
||||
|
||||
addFileMetrics(totals, metric);
|
||||
|
||||
const packageKey = metric.package.name;
|
||||
if (!packages.has(packageKey)) {
|
||||
packages.set(packageKey, {
|
||||
name: metric.package.name,
|
||||
version: metric.package.version,
|
||||
category: metric.package.category,
|
||||
sourceBytes: 0,
|
||||
gzipBytes: 0,
|
||||
modules: 0,
|
||||
astNodeCount: 0,
|
||||
functionCount: 0,
|
||||
classCount: 0,
|
||||
stringLiteralBytes: 0,
|
||||
nativeAddon: false,
|
||||
});
|
||||
}
|
||||
|
||||
const packageSummary = packages.get(packageKey);
|
||||
packageSummary.sourceBytes += metric.sourceBytes;
|
||||
packageSummary.gzipBytes += metric.gzipBytes;
|
||||
packageSummary.modules += 1;
|
||||
packageSummary.astNodeCount += metric.astNodeCount;
|
||||
packageSummary.functionCount += metric.functionCount;
|
||||
packageSummary.classCount += metric.classCount;
|
||||
packageSummary.stringLiteralBytes += metric.stringLiteralBytes;
|
||||
|
||||
modules.push({
|
||||
path: metric.displayPath,
|
||||
package: metric.package.name,
|
||||
category: metric.package.category,
|
||||
sourceBytes: metric.sourceBytes,
|
||||
gzipBytes: metric.gzipBytes,
|
||||
astNodeCount: metric.astNodeCount,
|
||||
functionCount: metric.functionCount,
|
||||
classCount: metric.classCount,
|
||||
stringLiteralBytes: metric.stringLiteralBytes,
|
||||
});
|
||||
}
|
||||
|
||||
for (const packageName of nativePackageNames) {
|
||||
const packageSummary = packages.get(packageName);
|
||||
if (packageSummary != null) packageSummary.nativeAddon = true;
|
||||
}
|
||||
|
||||
const externalPackages = [...packages.values()].filter(packageSummary => packageSummary.category === 'external');
|
||||
totals.externalPackageCount = externalPackages.length;
|
||||
totals.nativeAddonPackageCount = externalPackages.filter(packageSummary => packageSummary.nativeAddon).length;
|
||||
|
||||
return {
|
||||
totals: {
|
||||
...totals,
|
||||
loadedJsSourceKiB: bytesToKiB(totals.loadedJsSourceBytes),
|
||||
loadedJsGzipKiB: bytesToKiB(totals.loadedJsGzipBytes),
|
||||
stringLiteralKiB: bytesToKiB(totals.stringLiteralBytes),
|
||||
},
|
||||
packages: [...packages.values()].toSorted((a, b) => b.sourceBytes - a.sourceBytes),
|
||||
modules: modules.toSorted((a, b) => b.sourceBytes - a.sourceBytes),
|
||||
};
|
||||
}
|
||||
|
||||
async function measureFootprint() {
|
||||
await fs.writeFile(traceFile, '');
|
||||
|
||||
process.stderr.write('Resetting database and Redis\n');
|
||||
await resetState();
|
||||
|
||||
process.stderr.write('Running migrations\n');
|
||||
await util.run('pnpm', ['--filter', 'backend', 'migrate'], {
|
||||
cwd: repoDir,
|
||||
env: process.env,
|
||||
logStdout: true,
|
||||
});
|
||||
|
||||
const serverProcess = fork(join(backendBuiltDir, 'entry.js'), [], {
|
||||
cwd: backendDir,
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: 'production',
|
||||
MK_DISABLE_CLUSTERING: '1',
|
||||
MK_ONLY_SERVER: '1',
|
||||
MK_NO_DAEMONS: '1',
|
||||
MK_BACKEND_JS_FOOTPRINT_TRACE: traceFile,
|
||||
},
|
||||
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
|
||||
execArgv: [
|
||||
'--require',
|
||||
join(__dirname, 'backend-js-footprint-require.cjs'),
|
||||
'--experimental-loader',
|
||||
pathToFileURL(join(__dirname, 'backend-js-footprint-loader.mjs')).href,
|
||||
],
|
||||
});
|
||||
|
||||
serverProcess.stdout?.on('data', data => {
|
||||
process.stderr.write(`[server stdout] ${data}`);
|
||||
});
|
||||
|
||||
serverProcess.stderr?.on('data', data => {
|
||||
process.stderr.write(`[server stderr] ${data}`);
|
||||
});
|
||||
|
||||
serverProcess.on('error', err => {
|
||||
process.stderr.write(`[server error] ${err}\n`);
|
||||
});
|
||||
|
||||
try {
|
||||
await waitForServerReady(serverProcess);
|
||||
await setTimeout(SETTLE_TIME);
|
||||
|
||||
//const startup = summarizeRecords(await readTraceRecords(), 'startup');
|
||||
|
||||
await Promise.all(
|
||||
Array.from({ length: REQUEST_COUNT }).map(() => createRequest()),
|
||||
);
|
||||
await setTimeout(1000);
|
||||
|
||||
const afterRequest = summarizeRecords(await readTraceRecords(), 'afterRequest');
|
||||
|
||||
return {
|
||||
timestamp: new Date().toISOString(),
|
||||
measurement: {
|
||||
strategy: 'runtime-loader-trace',
|
||||
startupTimeoutMs: STARTUP_TIMEOUT,
|
||||
settleTimeMs: SETTLE_TIME,
|
||||
requestCount: REQUEST_COUNT,
|
||||
cpus: cpus().length,
|
||||
},
|
||||
phases: {
|
||||
//startup,
|
||||
afterRequest,
|
||||
},
|
||||
};
|
||||
} finally {
|
||||
await stopServer(serverProcess);
|
||||
await fs.rm(traceFile, { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
const result = await measureFootprint();
|
||||
await fs.writeFile(outputFile, `${JSON.stringify(result, null, 2)}\n`);
|
||||
428
.github/scripts/backend-memory-report.mts
vendored
428
.github/scripts/backend-memory-report.mts
vendored
@@ -1,428 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import * as util from './utility.mts';
|
||||
import * as heapSnapshotUtil from './heap-snapshot-util.mts';
|
||||
import type { MemoryReport } from './measure-backend-memory-comparison.mts';
|
||||
|
||||
const [baseFile, headFile, outputFile, baseJsFootprintFile, headJsFootprintFile] = process.argv.slice(2);
|
||||
|
||||
type RuntimeLoadedJsFootprintReport = {
|
||||
phases: Record<'afterRequest', {
|
||||
totals: {
|
||||
loadedJsModules: number;
|
||||
loadedJsSourceBytes: number;
|
||||
loadedJsGzipBytes: number;
|
||||
astNodeCount: number;
|
||||
functionCount: number;
|
||||
classCount: number;
|
||||
stringLiteralBytes: number;
|
||||
externalPackageCount: number;
|
||||
nativeAddonPackageCount: number;
|
||||
};
|
||||
modules: {
|
||||
path: string;
|
||||
package: string;
|
||||
category: string;
|
||||
sourceBytes: number;
|
||||
gzipBytes: number;
|
||||
astNodeCount: number;
|
||||
functionCount: number;
|
||||
classCount: number;
|
||||
stringLiteralBytes: number;
|
||||
}[];
|
||||
}>;
|
||||
};
|
||||
|
||||
const memoryReportPhases = [
|
||||
{
|
||||
key: 'afterGc',
|
||||
title: 'After GC',
|
||||
},
|
||||
] as const;
|
||||
|
||||
const metrics = [
|
||||
'HeapUsed',
|
||||
'Pss',
|
||||
'USS',
|
||||
'External',
|
||||
] as const;
|
||||
|
||||
function formatMemoryMb(valueKiB: number | null | undefined) {
|
||||
if (valueKiB == null) return '-';
|
||||
return `${util.formatNumber(valueKiB / 1000)} MB`;
|
||||
}
|
||||
|
||||
function formatMetricName(metric: typeof metrics[number]) {
|
||||
return metric === 'Pss' ? 'PSS' : metric;
|
||||
}
|
||||
|
||||
function getMemoryValueFromSample(sample: MemoryReport['samples'][number], phase: typeof memoryReportPhases[number]['key'], metric: typeof metrics[number]) {
|
||||
const memoryUsage = sample.phases[phase].memoryUsage;
|
||||
if (metric !== 'USS') return memoryUsage[metric];
|
||||
return memoryUsage.Private_Clean + memoryUsage.Private_Dirty;
|
||||
}
|
||||
|
||||
function getSampleValues(report: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof metrics[number]) {
|
||||
return report.samples.map(sample => getMemoryValueFromSample(sample, phase, metric));
|
||||
}
|
||||
|
||||
function getMemoryValue(report: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof metrics[number]) {
|
||||
if (metric !== 'USS') return report.summary[phase].memoryUsage[metric];
|
||||
return util.median(getSampleValues(report, phase, metric));
|
||||
}
|
||||
|
||||
function getSampleSpread(report: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof metrics[number]) {
|
||||
const values = getSampleValues(report, phase, metric);
|
||||
if (values.length < 2) return null;
|
||||
|
||||
const center = util.median(values);
|
||||
return util.median(values.map(value => Math.abs(value - center)));
|
||||
}
|
||||
|
||||
function renderMainTableForPhase(base: MemoryReport, head: MemoryReport, phase: typeof memoryReportPhases[number]['key']) {
|
||||
const lines = [
|
||||
'| Metric | Base | Head | Δ median | Δ MAD | Δ min | Δ max |',
|
||||
'| --- | ---: | ---: | ---: | ---: | ---: | ---: |',
|
||||
];
|
||||
|
||||
function formatDeltaMemory(deltaKiB: number) {
|
||||
return util.formatColoredDelta(deltaKiB, v => formatMemoryMb(v), 100); // 0.1 MB threshold
|
||||
}
|
||||
|
||||
for (const metric of metrics) {
|
||||
const baseValue = getMemoryValue(base, phase, metric);
|
||||
const headValue = getMemoryValue(head, phase, metric);
|
||||
|
||||
const baseSpread = getSampleSpread(base, phase, metric);
|
||||
const headSpread = getSampleSpread(head, phase, metric);
|
||||
const summary = util.pairedDeltaSummary(base.samples, head.samples, (sample) => getMemoryValueFromSample(sample, phase, metric));
|
||||
const percent = summary.median * 100 / baseValue;
|
||||
const deltaMedian = `${formatDeltaMemory(summary.median)}<br>${util.formatDeltaPercent(percent, 0.1).replaceAll('\\%', '\\\\%')}`;
|
||||
|
||||
lines.push(`| **${formatMetricName(metric)}** | ${formatMemoryMb(baseValue)} <br> ± ${formatMemoryMb(baseSpread)} | ${formatMemoryMb(headValue)} <br> ± ${formatMemoryMb(headSpread)} | ${deltaMedian} | ${formatMemoryMb(summary.mad)} | ${formatDeltaMemory(summary.min)} | ${formatDeltaMemory(summary.max)} |`);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/*
|
||||
function measurementSummary(base, head) {
|
||||
const baseCount = base?.sampleCount;
|
||||
const headCount = head?.sampleCount;
|
||||
const strategy = base?.comparison?.strategy;
|
||||
if (baseCount == null || headCount == null) return null;
|
||||
|
||||
if (strategy === 'interleaved-pairs') {
|
||||
const rounds = base?.comparison?.rounds ?? baseCount;
|
||||
const warmupRounds = base?.comparison?.warmupRounds ?? 0;
|
||||
return `_Measured as ${rounds} interleaved base/head pairs after ${warmupRounds} warmup pair(s). Values are medians; ± is median absolute deviation._`;
|
||||
}
|
||||
|
||||
return `_Sample count: base ${baseCount}, head ${headCount}. Values are medians; ± is median absolute deviation._`;
|
||||
}
|
||||
*/
|
||||
|
||||
function renderHeapSnapshotSection(base: MemoryReport, head: MemoryReport) {
|
||||
const baseHeapSnapshotReport = {
|
||||
summary: base.summary.afterGc.heapSnapshot!,
|
||||
samples: base.samples.map(sample => ({
|
||||
round: sample.round,
|
||||
data: sample.phases.afterGc.heapSnapshot!,
|
||||
})),
|
||||
};
|
||||
|
||||
const headHeapSnapshotReport = {
|
||||
summary: head.summary.afterGc.heapSnapshot!,
|
||||
samples: head.samples.map(sample => ({
|
||||
round: sample.round,
|
||||
data: sample.phases.afterGc.heapSnapshot!,
|
||||
})),
|
||||
};
|
||||
|
||||
const table = heapSnapshotUtil.renderHeapSnapshotTable(baseHeapSnapshotReport, headHeapSnapshotReport);
|
||||
if (table == null) return null;
|
||||
|
||||
const lines = [
|
||||
'### V8 Heap Snapshot Statistics',
|
||||
'',
|
||||
table,
|
||||
'',
|
||||
];
|
||||
|
||||
for (const graph of [
|
||||
//heapSnapshotUtil.renderHeapSnapshotSankey(baseHeapSnapshotReport, 'Base'),
|
||||
heapSnapshotUtil.renderHeapSnapshotSankey(headHeapSnapshotReport, 'Head'),
|
||||
]) {
|
||||
if (graph == null) continue;
|
||||
lines.push(graph);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function getJsFootprintValue(report: RuntimeLoadedJsFootprintReport, phase: 'afterRequest', key: keyof RuntimeLoadedJsFootprintReport['phases'][typeof phase]['totals']) {
|
||||
const value = report.phases[phase].totals[key];
|
||||
return Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
function renderJsFootprintMetricTable(base: RuntimeLoadedJsFootprintReport, head: RuntimeLoadedJsFootprintReport) {
|
||||
const metricRows = [
|
||||
['Loaded JS modules', 'loadedJsModules', util.formatNumber],
|
||||
['Loaded JS source', 'loadedJsSourceBytes', util.formatBytes],
|
||||
//['Loaded JS gzip estimate', 'loadedJsGzipBytes', util.formatBytes],
|
||||
//['AST nodes', 'astNodeCount', util.formatNumber],
|
||||
//['Functions', 'functionCount', util.formatNumber],
|
||||
//['Classes', 'classCount', util.formatNumber],
|
||||
//['String literals', 'stringLiteralBytes', util.formatBytes],
|
||||
['External packages loaded', 'externalPackageCount', util.formatNumber],
|
||||
['Native addon packages', 'nativeAddonPackageCount', util.formatNumber],
|
||||
] as const;
|
||||
|
||||
const lines = [
|
||||
'| Metric | Base | Head | Δ | Δ (%) |',
|
||||
'| --- | ---: | ---: | ---: | ---: |',
|
||||
];
|
||||
|
||||
for (const [title, key, formatter] of metricRows) {
|
||||
const baseValue = getJsFootprintValue(base, 'afterRequest', key);
|
||||
const headValue = getJsFootprintValue(head, 'afterRequest', key);
|
||||
if (baseValue == null || headValue == null) continue;
|
||||
|
||||
lines.push(`| **${title}** | ${formatter(baseValue)} | ${formatter(headValue)} | ${util.formatColoredDelta(headValue - baseValue, v => formatter(v))} | ${util.calcAndFormatDeltaPercent(baseValue, headValue).replaceAll('\\%', '\\\\%')} |`);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
/*
|
||||
function renderJsFootprintPhaseTable(base, head) {
|
||||
const lines = [
|
||||
'| Phase | Base modules | Head modules | Δ modules | Base source | Head source | Δ source |',
|
||||
'| --- | ---: | ---: | ---: | ---: | ---: | ---: |',
|
||||
];
|
||||
|
||||
for (const [phase, title] of [['startup', 'Startup'], ['afterRequest', 'After warmup requests']]) {
|
||||
const baseModules = getJsFootprintValue(base, phase, 'loadedJsModules');
|
||||
const headModules = getJsFootprintValue(head, phase, 'loadedJsModules');
|
||||
const baseSource = getJsFootprintValue(base, phase, 'loadedJsSourceBytes');
|
||||
const headSource = getJsFootprintValue(head, phase, 'loadedJsSourceBytes');
|
||||
if (baseModules == null || headModules == null || baseSource == null || headSource == null) continue;
|
||||
|
||||
lines.push(`| ${title} | ${util.formatNumber(baseModules)} | ${util.formatNumber(headModules)} | ${formatPlainDelta(baseModules, headModules)} | ${util.formatBytes(baseSource)} | ${util.formatBytes(headSource)} | ${formatPlainDelta(baseSource, headSource, util.formatBytes)} |`);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
*/
|
||||
|
||||
function packageMap(report: RuntimeLoadedJsFootprintReport) {
|
||||
const map = new Map();
|
||||
for (const packageSummary of report.phases.afterRequest.packages) {
|
||||
if (packageSummary?.category !== 'external' || typeof packageSummary.name !== 'string') continue;
|
||||
map.set(packageSummary.name, packageSummary);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
function packageDisplayName(packageSummary: { name: string; version?: string | null }) {
|
||||
if (packageSummary.version == null) return packageSummary.name;
|
||||
return `${packageSummary.name} ${packageSummary.version}`;
|
||||
}
|
||||
|
||||
function renderNewExternalPackages(base: RuntimeLoadedJsFootprintReport, head: RuntimeLoadedJsFootprintReport) {
|
||||
const basePackages = packageMap(base);
|
||||
const headPackages = packageMap(head);
|
||||
const newPackages = [...headPackages.values()]
|
||||
.filter(packageSummary => !basePackages.has(packageSummary.name))
|
||||
.toSorted((a, b) => b.sourceBytes - a.sourceBytes)
|
||||
.slice(0, 10);
|
||||
|
||||
if (newPackages.length === 0) return null;
|
||||
|
||||
const lines = [
|
||||
'#### Newly Loaded External Packages',
|
||||
'',
|
||||
'| Package | Loaded JS | Modules | Notes |',
|
||||
'| --- | ---: | ---: | --- |',
|
||||
];
|
||||
|
||||
for (const packageSummary of newPackages) {
|
||||
lines.push(`| ${packageDisplayName(packageSummary)} | ${util.formatBytes(packageSummary.sourceBytes)} | ${util.formatNumber(packageSummary.modules)} | ${packageSummary.nativeAddon ? 'native addon' : ''} |`);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function renderLargestPackageIncreases(base: RuntimeLoadedJsFootprintReport, head: RuntimeLoadedJsFootprintReport) {
|
||||
const basePackages = packageMap(base);
|
||||
const headPackages = packageMap(head);
|
||||
const increases = [...headPackages.values()]
|
||||
.map(headPackage => {
|
||||
const basePackage = basePackages.get(headPackage.name);
|
||||
const baseSourceBytes = basePackage?.sourceBytes ?? 0;
|
||||
const baseModules = basePackage?.modules ?? 0;
|
||||
return {
|
||||
...headPackage,
|
||||
baseSourceBytes,
|
||||
baseModules,
|
||||
sourceDiff: headPackage.sourceBytes - baseSourceBytes,
|
||||
moduleDiff: headPackage.modules - baseModules,
|
||||
};
|
||||
})
|
||||
.filter(packageSummary => packageSummary.sourceDiff > 0)
|
||||
.toSorted((a, b) => b.sourceDiff - a.sourceDiff)
|
||||
.slice(0, 10);
|
||||
|
||||
if (increases.length === 0) return null;
|
||||
|
||||
const lines = [
|
||||
'#### Largest Package Increases',
|
||||
'',
|
||||
'| Package | Base | Head | Δ | Modules Δ |',
|
||||
'| --- | ---: | ---: | ---: | ---: |',
|
||||
];
|
||||
|
||||
for (const packageSummary of increases) {
|
||||
lines.push(`| ${packageDisplayName(packageSummary)} | ${util.formatBytes(packageSummary.baseSourceBytes)} | ${util.formatBytes(packageSummary.sourceBytes)} | ${util.formatColoredDelta(packageSummary.sourceBytes - packageSummary.baseSourceBytes, v => util.formatBytes(v))} | ${util.formatColoredDelta(packageSummary.modules - packageSummary.baseModules, v => util.formatNumber(v))} |`);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function renderNewLoadedModules(base: RuntimeLoadedJsFootprintReport, head: RuntimeLoadedJsFootprintReport) {
|
||||
function moduleMap(report: RuntimeLoadedJsFootprintReport) {
|
||||
const map = new Map();
|
||||
for (const moduleSummary of report.phases.afterRequest.modules) {
|
||||
if (typeof moduleSummary.path !== 'string') continue;
|
||||
map.set(moduleSummary.path, moduleSummary);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
const baseModules = moduleMap(base);
|
||||
const headModules = moduleMap(head);
|
||||
const newModules = [...headModules.values()]
|
||||
.filter(moduleSummary => !baseModules.has(moduleSummary.path))
|
||||
.toSorted((a, b) => b.sourceBytes - a.sourceBytes)
|
||||
.slice(0, 10);
|
||||
|
||||
if (newModules.length === 0) return null;
|
||||
|
||||
const lines = [
|
||||
'#### Largest Newly Loaded Modules',
|
||||
'',
|
||||
'| Module | Package | Loaded JS |',
|
||||
'| --- | --- | ---: |',
|
||||
];
|
||||
|
||||
for (const moduleSummary of newModules) {
|
||||
lines.push(`| \`${moduleSummary.path}\` | ${moduleSummary.package} | ${util.formatBytes(moduleSummary.sourceBytes)} |`);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function renderJsFootprintSection(base: RuntimeLoadedJsFootprintReport, head: RuntimeLoadedJsFootprintReport) {
|
||||
const lines = [
|
||||
'### Runtime Loaded JS Footprint',
|
||||
'',
|
||||
'<details><summary>Click to show</summary>',
|
||||
'',
|
||||
renderJsFootprintMetricTable(base, head),
|
||||
'',
|
||||
//'#### Load Phase Breakdown',
|
||||
//'',
|
||||
//renderJsFootprintPhaseTable(base, head),
|
||||
//'',
|
||||
];
|
||||
|
||||
for (const block of [
|
||||
renderNewExternalPackages(base, head),
|
||||
renderLargestPackageIncreases(base, head),
|
||||
renderNewLoadedModules(base, head),
|
||||
]) {
|
||||
if (block == null) continue;
|
||||
lines.push(block);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
lines.push('</details>');
|
||||
lines.push('');
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
const base = JSON.parse(await readFile(baseFile, 'utf8')) as MemoryReport;
|
||||
const head = JSON.parse(await readFile(headFile, 'utf8')) as MemoryReport;
|
||||
const baseJsFootprint = JSON.parse(await readFile(baseJsFootprintFile, 'utf8')) as RuntimeLoadedJsFootprintReport;
|
||||
const headJsFootprint = JSON.parse(await readFile(headJsFootprintFile, 'utf8')) as RuntimeLoadedJsFootprintReport;
|
||||
const lines = [
|
||||
'## ⚙️ Backend Memory Usage Report',
|
||||
'',
|
||||
];
|
||||
|
||||
//const summary = measurementSummary(base, head);
|
||||
//if (summary != null) {
|
||||
// lines.push(summary);
|
||||
// lines.push('');
|
||||
//}
|
||||
|
||||
for (const phase of memoryReportPhases) {
|
||||
lines.push(`### ${phase.title}`);
|
||||
lines.push(renderMainTableForPhase(base, head, phase.key));
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
const heapSnapshotSection = renderHeapSnapshotSection(base, head);
|
||||
if (heapSnapshotSection != null) {
|
||||
lines.push(heapSnapshotSection);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
const baseHeapSnapshotArtifactUrl = process.env.MK_MEMORY_HEAP_SNAPSHOT_ARTIFACT_URL_BASE!.trim();
|
||||
const headHeapSnapshotArtifactUrl = process.env.MK_MEMORY_HEAP_SNAPSHOT_ARTIFACT_URL_HEAD!.trim();
|
||||
lines.push(`Download representative V8 heap snapshot [base](${baseHeapSnapshotArtifactUrl}) / [head](${headHeapSnapshotArtifactUrl})`);
|
||||
lines.push('');
|
||||
|
||||
const jsFootprintSection = renderJsFootprintSection(baseJsFootprint, headJsFootprint);
|
||||
if (jsFootprintSection != null) {
|
||||
lines.push(jsFootprintSection);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
function getDiffPercent(base: MemoryReport, head: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof metrics[number]) {
|
||||
const baseValue = getMemoryValue(base, phase, metric);
|
||||
const headValue = getMemoryValue(head, phase, metric);
|
||||
return ((headValue - baseValue) * 100) / baseValue;
|
||||
}
|
||||
|
||||
function isBeyondSampleNoise(base: MemoryReport, head: MemoryReport, phase: typeof memoryReportPhases[number]['key'], metric: typeof metrics[number]) {
|
||||
const baseValue = getMemoryValue(base, phase, metric);
|
||||
const headValue = getMemoryValue(head, phase, metric);
|
||||
|
||||
const delta = headValue - baseValue;
|
||||
if (delta <= 0) return false;
|
||||
|
||||
const baseSpread = getSampleSpread(base, phase, metric);
|
||||
const headSpread = getSampleSpread(head, phase, metric);
|
||||
if (baseSpread == null || headSpread == null) return true;
|
||||
|
||||
const combinedSpread = Math.hypot(baseSpread, headSpread);
|
||||
return delta > combinedSpread * 3;
|
||||
}
|
||||
|
||||
const warningMetric = 'Pss';
|
||||
const warningDiffPercent = getDiffPercent(base, head, 'afterGc', warningMetric);
|
||||
if (warningDiffPercent > 5 && isBeyondSampleNoise(base, head, 'afterGc', warningMetric)) {
|
||||
lines.push(`⚠️ **Warning**: Memory usage (${formatMetricName(warningMetric)}) has increased by more than 5% and exceeds the observed sample noise. Please verify this is not an unintended change.`);
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
//lines.push(`[See workflow logs for details](https://github.com/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID})`);
|
||||
|
||||
await writeFile(outputFile, `${lines.join('\n')}\n`);
|
||||
47
.github/scripts/chrome.mts
vendored
47
.github/scripts/chrome.mts
vendored
@@ -7,6 +7,7 @@ import { createRequire } from 'node:module';
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import type { Browser, BrowserContext, CDPSession, Page } from 'playwright';
|
||||
import type { HeapSnapshotData } from './heap-snapshot-util.mts';
|
||||
import * as util from './utility.mts';
|
||||
|
||||
export type NetworkRequest = {
|
||||
requestId: string;
|
||||
@@ -89,11 +90,31 @@ export type TabMemory = {
|
||||
totalBytes: number;
|
||||
};
|
||||
|
||||
export type BrowserDiagnostics = {
|
||||
pageErrorCount: number;
|
||||
console: Record<'log' | 'warning' | 'error' | 'info', number>;
|
||||
};
|
||||
|
||||
export function summarizeBrowserDiagnostics(samples: BrowserDiagnostics[]): BrowserDiagnostics {
|
||||
const median = (select: (sample: BrowserDiagnostics) => number) => util.median(samples.map(select));
|
||||
|
||||
return {
|
||||
pageErrorCount: median(sample => sample.pageErrorCount),
|
||||
console: {
|
||||
log: median(sample => sample.console.log),
|
||||
warning: median(sample => sample.console.warning),
|
||||
error: median(sample => sample.console.error),
|
||||
info: median(sample => sample.console.info),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export type BrowserMeasurement = {
|
||||
label: string;
|
||||
timestamp: string;
|
||||
url: string;
|
||||
scenario: string;
|
||||
diagnostics: BrowserDiagnostics;
|
||||
durationMs: number;
|
||||
network: NetworkSummary;
|
||||
performance: {
|
||||
@@ -149,6 +170,10 @@ type PlaywrightBrowserOptions = {
|
||||
export class HeadlessChromeController {
|
||||
public networkRequests: NetworkRequest[] = [];
|
||||
public webSocketConnections: WebSocketConnection[] = [];
|
||||
private readonly diagnostics = {
|
||||
pageErrorCount: 0,
|
||||
console: {} as Record<string, number | undefined>,
|
||||
};
|
||||
private readonly browser: Browser;
|
||||
private readonly context: BrowserContext;
|
||||
public readonly page: Page;
|
||||
@@ -168,6 +193,16 @@ export class HeadlessChromeController {
|
||||
this.cdp = cdp;
|
||||
this.page.setDefaultTimeout(options.scenarioTimeoutMs);
|
||||
this.page.setDefaultNavigationTimeout(options.scenarioTimeoutMs);
|
||||
this.page.on('pageerror', () => {
|
||||
this.diagnostics.pageErrorCount++;
|
||||
});
|
||||
this.page.on('console', message => {
|
||||
if (this.diagnostics.console[message.type()] == null) {
|
||||
this.diagnostics.console[message.type()] = 1;
|
||||
} else {
|
||||
this.diagnostics.console[message.type()]++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static async create(label: string, options: PlaywrightBrowserOptions): Promise<HeadlessChromeController> {
|
||||
@@ -376,6 +411,18 @@ export class HeadlessChromeController {
|
||||
]) as T;
|
||||
}
|
||||
|
||||
public collectDiagnostics(): BrowserDiagnostics {
|
||||
return {
|
||||
pageErrorCount: this.diagnostics.pageErrorCount,
|
||||
console: {
|
||||
log: this.diagnostics.console.log ?? 0,
|
||||
warning: this.diagnostics.console.warning ?? 0,
|
||||
error: this.diagnostics.console.error ?? 0,
|
||||
info: this.diagnostics.console.info ?? 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public async collectPerformance(): Promise<BrowserMeasurement['performance']> {
|
||||
const cdpMetricsResult = await this.cdp.send<{ metrics: { name: string; value: number }[] }>('Performance.getMetrics');
|
||||
const cdpMetrics = Object.fromEntries(cdpMetricsResult.metrics.map(metric => [metric.name, metric.value]));
|
||||
|
||||
@@ -7,16 +7,23 @@ import { copyFile, mkdir, 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 { HeadlessChromeController, summarizeNetwork } from './chrome.mts';
|
||||
import { HeadlessChromeController, summarizeBrowserDiagnostics, summarizeNetwork } from './chrome.mts';
|
||||
import type { BrowserMeasurement, NetworkRequest, NetworkSummary } from './chrome.mts';
|
||||
import { closeUserSetupDialog, postNote, signupThroughUi, visitHome } from '../../packages/frontend/test/e2e/shared.ts';
|
||||
|
||||
const [baseDirArg, headDirArg, baseOutputArg, headOutputArg, headHeapSnapshotOutputArg] = process.argv.slice(2);
|
||||
const [baseDirArg, headDirArg, baseOutputArg, headOutputArg] = process.argv.slice(2);
|
||||
|
||||
const baseUrl = process.env.FRONTEND_BROWSER_METRICS_URL ?? 'http://127.0.0.1:61812';
|
||||
const sampleCount = util.readIntegerEnv('FRONTEND_BROWSER_METRICS_SAMPLE_COUNT', 5, 1);
|
||||
const heapSnapshotBreakdownTopN = util.readIntegerEnv('FRONTEND_BROWSER_HEAP_SNAPSHOT_BREAKDOWN_TOP_N', heapSnapshotUtil.defaultHeapSnapshotBreakdownTopN, 1);
|
||||
const headHeapSnapshotWorkDir = resolve('frontend-browser-head-heap-snapshots');
|
||||
const heapSnapshotWorkDirs = {
|
||||
base: resolve('frontend-browser-base-heap-snapshots'),
|
||||
head: resolve('frontend-browser-head-heap-snapshots'),
|
||||
} as const;
|
||||
const heapSnapshotOutputPaths = {
|
||||
base: resolve('base-heap-snapshot.heapsnapshot'),
|
||||
head: resolve('head-heap-snapshot.heapsnapshot'),
|
||||
} as const;
|
||||
|
||||
type BrowserMeasurementSample = BrowserMeasurement & {
|
||||
round: number;
|
||||
@@ -173,6 +180,7 @@ function summarizeSamples(label: 'base' | 'head', samples: BrowserMeasurementSam
|
||||
timestamp: new Date().toISOString(),
|
||||
url: baseUrl,
|
||||
scenario: representative.scenario,
|
||||
diagnostics: summarizeBrowserDiagnostics(samples.map(sample => sample.diagnostics)),
|
||||
durationMs: finiteMedian(samples.map(sample => sample.durationMs)),
|
||||
network: summarizeNetworkSamples(samples),
|
||||
performance: summarizePerformanceSamples(samples),
|
||||
@@ -210,6 +218,7 @@ async function measureSample(label: 'base' | 'head', round: number, heapSnapshot
|
||||
timestamp: new Date().toISOString(),
|
||||
url: baseUrl,
|
||||
scenario: 'fresh browser signup, first timeline note, after the note becomes visible',
|
||||
diagnostics: chrome.collectDiagnostics(),
|
||||
durationMs,
|
||||
network: summarizeNetwork(chrome.networkRequests, baseUrl, chrome.webSocketConnections),
|
||||
networkRequests: chrome.networkRequests,
|
||||
@@ -221,28 +230,26 @@ async function measureSample(label: 'base' | 'head', round: number, heapSnapshot
|
||||
});
|
||||
}
|
||||
|
||||
function headHeapSnapshotPath(round: number) {
|
||||
return join(headHeapSnapshotWorkDir, `round-${round}.heapsnapshot`);
|
||||
function heapSnapshotPath(label: 'base' | 'head', round: number) {
|
||||
return join(heapSnapshotWorkDirs[label], `round-${round}.heapsnapshot`);
|
||||
}
|
||||
|
||||
async function saveRepresentativeHeadHeapSnapshot(report: BrowserMetricsReport, outputPath: string) {
|
||||
async function saveRepresentativeHeapSnapshot(label: 'base' | 'head', report: BrowserMetricsReport) {
|
||||
const representative = selectRepresentativeSample(report.samples, sample => sample.heapSnapshot.categories.total);
|
||||
await copyFile(headHeapSnapshotPath(representative.round), outputPath);
|
||||
process.stderr.write(`[head] Selected round ${representative.round} heap snapshot for artifact\n`);
|
||||
await rm(headHeapSnapshotWorkDir, { recursive: true, force: true });
|
||||
await copyFile(heapSnapshotPath(label, representative.round), heapSnapshotOutputPaths[label]);
|
||||
process.stderr.write(`[${label}] Selected round ${representative.round} heap snapshot for artifact\n`);
|
||||
await rm(heapSnapshotWorkDirs[label], { recursive: true, force: true });
|
||||
}
|
||||
|
||||
async function measureRepo(label: 'base' | 'head', repoDir: string, outputPath: string, heapSnapshotSavePath?: string) {
|
||||
async function genReport(label: 'base' | 'head', repoDir: string, outputPath: string) {
|
||||
let server: ReturnType<typeof util.startServer> | null = null;
|
||||
|
||||
try {
|
||||
server = util.startServer(label, repoDir);
|
||||
await util.waitForServer(baseUrl, server!);
|
||||
await util.waitForServer(baseUrl, server);
|
||||
|
||||
if (label === 'head' && heapSnapshotSavePath != null) {
|
||||
await rm(headHeapSnapshotWorkDir, { recursive: true, force: true });
|
||||
await mkdir(headHeapSnapshotWorkDir, { recursive: true });
|
||||
}
|
||||
await rm(heapSnapshotWorkDirs[label], { recursive: true, force: true });
|
||||
await mkdir(heapSnapshotWorkDirs[label], { recursive: true });
|
||||
|
||||
const samples: BrowserMeasurementSample[] = [];
|
||||
for (let round = 1; round <= sampleCount; round++) {
|
||||
@@ -250,7 +257,7 @@ async function measureRepo(label: 'base' | 'head', repoDir: string, outputPath:
|
||||
samples.push(await measureSample(
|
||||
label,
|
||||
round,
|
||||
label === 'head' && heapSnapshotSavePath != null ? headHeapSnapshotPath(round) : undefined,
|
||||
heapSnapshotPath(label, round),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -258,17 +265,15 @@ async function measureRepo(label: 'base' | 'head', repoDir: string, outputPath:
|
||||
await writeFile(outputPath, JSON.stringify(report, null, '\t'));
|
||||
process.stderr.write(`[${label}] Wrote browser metrics report to ${outputPath}\n`);
|
||||
|
||||
if (label === 'head' && heapSnapshotSavePath != null) {
|
||||
await saveRepresentativeHeadHeapSnapshot(report, heapSnapshotSavePath);
|
||||
}
|
||||
await saveRepresentativeHeapSnapshot(label, report);
|
||||
} finally {
|
||||
if (server != null) await util.stopServer(server);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await measureRepo('base', resolve(baseDirArg), resolve(baseOutputArg));
|
||||
await measureRepo('head', resolve(headDirArg), resolve(headOutputArg), headHeapSnapshotOutputArg == null ? undefined : resolve(headHeapSnapshotOutputArg));
|
||||
await genReport('base', resolve(baseDirArg), resolve(baseOutputArg));
|
||||
await genReport('head', resolve(headDirArg), resolve(headOutputArg));
|
||||
}
|
||||
|
||||
await main();
|
||||
@@ -6,7 +6,7 @@
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { pathToFileURL } from 'node:url';
|
||||
import * as util from './utility.mts';
|
||||
import type { BrowserMeasurementSample, BrowserMetricsReport } from './frontend-browser-report.mts';
|
||||
import type { BrowserMeasurementSample, BrowserMetricsReport } from './frontend-browser-diagnostics.render-md.mts';
|
||||
import type { NetworkRequest } from './chrome.mts';
|
||||
|
||||
type DiffDirection = 'added' | 'removed';
|
||||
@@ -434,15 +434,13 @@ function renderHtml(base: BrowserMetricsReport, head: BrowserMetricsReport) {
|
||||
|
||||
async function main() {
|
||||
const [baseFile, headFile, outputFile] = process.argv.slice(2);
|
||||
if (baseFile == null || headFile == null || outputFile == null) {
|
||||
throw new Error('Usage: node frontend-browser-detailed-html.mts <base-browser.json> <head-browser.json> <output.html>');
|
||||
}
|
||||
|
||||
const base = JSON.parse(await readFile(baseFile, 'utf8')) as BrowserMetricsReport;
|
||||
const head = JSON.parse(await readFile(headFile, 'utf8')) as BrowserMetricsReport;
|
||||
await writeFile(outputFile, renderHtml(base, head));
|
||||
}
|
||||
|
||||
// 直接実行されたときだけ呼ぶための判定(テストなどでこのファイル内の関数をimportするだけのとき用)
|
||||
if (process.argv[1] != null && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
await main();
|
||||
}
|
||||
@@ -8,13 +8,14 @@ import { pathToFileURL } from 'node:url';
|
||||
import * as util from './utility.mts';
|
||||
import * as heapSnapshotUtil from './heap-snapshot-util.mts';
|
||||
import type { HeapSnapshotData, HeapSnapshotReport } from './heap-snapshot-util.mts';
|
||||
import type { NetworkRequest } from './chrome.mts';
|
||||
import type { BrowserDiagnostics, NetworkRequest } from './chrome.mts';
|
||||
|
||||
export type BrowserMeasurement = {
|
||||
label: string;
|
||||
timestamp: string;
|
||||
url: string;
|
||||
scenario: string;
|
||||
diagnostics: BrowserDiagnostics;
|
||||
durationMs: number;
|
||||
network: {
|
||||
requestCount: number;
|
||||
@@ -203,7 +204,12 @@ function renderSummaryTable(base: BrowserMetricsReport, head: BrowserMetricsRepo
|
||||
metricRow('WebSocket connections', base, head, summary => summary.network.webSocketConnectionCount, sample => sample.network.webSocketConnectionCount, util.formatNumber, 1, !all),
|
||||
metricRow('WebSocket sent', base, head, summary => summary.network.webSocketSentBytes, sample => sample.network.webSocketSentBytes, util.formatBytes, 10000, !all),
|
||||
metricRow('WebSocket received', base, head, summary => summary.network.webSocketReceivedBytes, sample => sample.network.webSocketReceivedBytes, util.formatBytes, 10000, !all),
|
||||
metricRow('Tab memory', base, head, summary => summary.performance.tabMemory.totalBytes, sample => sample.performance.tabMemory.totalBytes, util.formatBytes, 10000, !all),
|
||||
metricRow('Page errors', base, head, summary => summary.diagnostics.pageErrorCount, sample => sample.diagnostics.pageErrorCount, util.formatNumber, 1, !all),
|
||||
metricRow('Console log', base, head, summary => summary.diagnostics.console.log, sample => sample.diagnostics.console.log, util.formatNumber, 1, !all),
|
||||
metricRow('Console warnings', base, head, summary => summary.diagnostics.console.warning, sample => sample.diagnostics.console.warning, util.formatNumber, 1, !all),
|
||||
metricRow('Console errors', base, head, summary => summary.diagnostics.console.error, sample => sample.diagnostics.console.error, util.formatNumber, 1, !all),
|
||||
metricRow('Console info', base, head, summary => summary.diagnostics.console.info, sample => sample.diagnostics.console.info, util.formatNumber, 1, !all),
|
||||
metricRow('Page-attributed memory', base, head, summary => summary.performance.tabMemory.totalBytes, sample => sample.performance.tabMemory.totalBytes, util.formatBytes, 10000, !all),
|
||||
].filter(row => row != null);
|
||||
|
||||
return [
|
||||
@@ -307,25 +313,20 @@ function toHeapSnapshotReport(report: BrowserMetricsReport): HeapSnapshotReport
|
||||
};
|
||||
}
|
||||
|
||||
export function renderFrontendBrowserReport(base: BrowserMetricsReport, head: BrowserMetricsReport, options: {
|
||||
headHeapSnapshotUrl?: string;
|
||||
function renderMd(base: BrowserMetricsReport, head: BrowserMetricsReport, options: {
|
||||
baseHeapSnapshotUrl: string;
|
||||
headHeapSnapshotUrl: string;
|
||||
detailedHtmlUrl?: string;
|
||||
} = {}) {
|
||||
const headHeapSnapshotUrl = options.headHeapSnapshotUrl;
|
||||
}) {
|
||||
const detailedHtmlUrl = options.detailedHtmlUrl;
|
||||
const sampleSummary = base.sampleCount === head.sampleCount
|
||||
? `${base.sampleCount} samples per side`
|
||||
: `${base.sampleCount} base sample(s), ${head.sampleCount} head sample(s)`;
|
||||
const heapSnapshotTable = heapSnapshotUtil.renderHeapSnapshotTable(toHeapSnapshotReport(base), toHeapSnapshotReport(head));
|
||||
const lines = [
|
||||
'## 🖥 Frontend Browser Metrics',
|
||||
'',
|
||||
'Only metrics showing significant changes are displayed.',
|
||||
'## 🖥 Frontend Browser Diagnostics Report',
|
||||
'',
|
||||
renderSummaryTable(base, head),
|
||||
'',
|
||||
//`> Measured ${sampleSummary} with fresh headless Chrome profiles, browser cache disabled, service workers bypassed, and forced V8 GC before each heap snapshot. Base/Head values are medians; Δ median is the median of paired Head - Base sample deltas; percent uses Δ median / Base median; ± and Δ MAD are median absolute deviations. Scenario: sign up, dismiss the initial account setup dialog, create the first timeline note, then wait until that note is visible.`,
|
||||
//'',
|
||||
'<i>Only metrics showing significant changes are displayed.</i>',
|
||||
'',
|
||||
detailedHtmlUrl == null || detailedHtmlUrl === '' ? null : `[View details](${detailedHtmlUrl})`,
|
||||
detailedHtmlUrl == null || detailedHtmlUrl === '' ? null : '',
|
||||
'<details>',
|
||||
@@ -342,7 +343,7 @@ export function renderFrontendBrowserReport(base: BrowserMetricsReport, head: Br
|
||||
'',
|
||||
heapSnapshotUtil.renderHeapSnapshotSankey(toHeapSnapshotReport(head), 'Head'),
|
||||
'',
|
||||
`[Download representative head heap snapshot](${headHeapSnapshotUrl})`,
|
||||
`Download representative heap snapshot: [base](${options.baseHeapSnapshotUrl}) / [head](${options.headHeapSnapshotUrl})`,
|
||||
'</details>',
|
||||
'',
|
||||
];
|
||||
@@ -361,18 +362,17 @@ export function renderFrontendBrowserReport(base: BrowserMetricsReport, head: Br
|
||||
|
||||
async function main() {
|
||||
const [baseFile, headFile, outputFile] = process.argv.slice(2);
|
||||
if (baseFile == null || headFile == null || outputFile == null) {
|
||||
throw new Error('Usage: node frontend-browser-report.mts <base-browser.json> <head-browser.json> <output.md>');
|
||||
}
|
||||
|
||||
const base = JSON.parse(await readFile(baseFile, 'utf8')) as BrowserMetricsReport;
|
||||
const head = JSON.parse(await readFile(headFile, 'utf8')) as BrowserMetricsReport;
|
||||
await writeFile(outputFile, renderFrontendBrowserReport(base, head, {
|
||||
headHeapSnapshotUrl: process.env.FRONTEND_BROWSER_HEAD_HEAP_SNAPSHOT_ARTIFACT_URL,
|
||||
await writeFile(outputFile, renderMd(base, head, {
|
||||
baseHeapSnapshotUrl: process.env.FRONTEND_BROWSER_BASE_HEAP_SNAPSHOT_ARTIFACT_URL!,
|
||||
headHeapSnapshotUrl: process.env.FRONTEND_BROWSER_HEAD_HEAP_SNAPSHOT_ARTIFACT_URL!,
|
||||
detailedHtmlUrl: process.env.FRONTEND_BROWSER_DETAILED_HTML_ARTIFACT_URL,
|
||||
}));
|
||||
}
|
||||
|
||||
// 直接実行されたときだけ呼ぶための判定(テストなどでこのファイル内の関数をimportするだけのとき用)
|
||||
if (process.argv[1] != null && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
await main();
|
||||
}
|
||||
@@ -7,9 +7,9 @@ import { promises as fs } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import * as util from './utility.mts';
|
||||
|
||||
const marker = '<!-- misskey-frontend-js-size -->';
|
||||
const marker = '<!-- misskey-frontend-bundle-diagnostics -->';
|
||||
|
||||
const locale = process.env.FRONTEND_JS_SIZE_LOCALE ?? 'ja-JP';
|
||||
const locale = 'ja-JP';
|
||||
|
||||
//function sharePercent(value, total) {
|
||||
// if (total === 0) return '0%';
|
||||
252
.github/scripts/frontend-js-size.test.mts
vendored
252
.github/scripts/frontend-js-size.test.mts
vendored
@@ -1,252 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import assert from 'node:assert/strict';
|
||||
import { execFile } from 'node:child_process';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import test, { type TestContext } from 'node:test';
|
||||
import * as util from './utility.mts';
|
||||
|
||||
type Manifest = Record<string, {
|
||||
file?: string;
|
||||
src?: string;
|
||||
name?: string;
|
||||
isEntry?: boolean;
|
||||
imports?: string[];
|
||||
}>;
|
||||
|
||||
const repoDir = path.resolve(import.meta.dirname, '../..');
|
||||
const reportScript = path.join(repoDir, '.github/scripts/frontend-js-size.mts');
|
||||
|
||||
async function writeBuild(repo: string, manifest: Manifest, sizes: Record<string, number>) {
|
||||
const outDir = path.join(repo, 'built/_frontend_vite_/');
|
||||
await fs.mkdir(path.join(outDir, 'ja-JP'), { recursive: true });
|
||||
await fs.writeFile(path.join(outDir, 'manifest.json'), JSON.stringify(manifest));
|
||||
for (const [file, size] of Object.entries(sizes)) {
|
||||
const localizedFile = file.replace(/^scripts\//, '');
|
||||
const outputFile = path.join(outDir, 'ja-JP', localizedFile);
|
||||
await fs.mkdir(path.dirname(outputFile), { recursive: true });
|
||||
await fs.writeFile(outputFile, Buffer.alloc(size));
|
||||
}
|
||||
}
|
||||
|
||||
async function runReport(t: TestContext, before: { manifest: Manifest; sizes: Record<string, number> }, after: { manifest: Manifest; sizes: Record<string, number> }, expectFailure = false) {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'frontend-js-size-'));
|
||||
t.after(() => fs.rm(root, { recursive: true, force: true }));
|
||||
const beforeDir = path.join(root, 'before');
|
||||
const afterDir = path.join(root, 'after');
|
||||
const beforeStats = path.join(root, 'before-stats.json');
|
||||
const afterStats = path.join(root, 'after-stats.json');
|
||||
const reportFile = path.join(root, 'report.md');
|
||||
await writeBuild(beforeDir, before.manifest, before.sizes);
|
||||
await writeBuild(afterDir, after.manifest, after.sizes);
|
||||
await fs.writeFile(beforeStats, '{}');
|
||||
await fs.writeFile(afterStats, '{}');
|
||||
const args = [reportScript, beforeDir, afterDir, beforeStats, afterStats, reportFile];
|
||||
if (expectFailure) {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
execFile(process.execPath, args, (error, _stdout, stderr) => {
|
||||
if (error == null) {
|
||||
reject(new Error('Expected frontend report script to fail'));
|
||||
} else {
|
||||
resolve(stderr);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
await util.run(process.execPath, args);
|
||||
return fs.readFile(reportFile, 'utf8');
|
||||
}
|
||||
|
||||
function fixture(suffix: string, generatedName: string, sizes: { entry: number; generatedA: number; generatedB: number; vue: number; i18n: number }) {
|
||||
const files = {
|
||||
entry: `scripts/entry-${suffix}.js`,
|
||||
generatedA: `scripts/generated-a-${suffix}.js`,
|
||||
generatedB: `scripts/generated-b-${suffix}.js`,
|
||||
vue: `scripts/vue-${suffix}.js`,
|
||||
i18n: `scripts/i18n-${suffix}.js`,
|
||||
};
|
||||
return {
|
||||
manifest: {
|
||||
'src/_boot_.ts': { file: files.entry, src: 'src/_boot_.ts', name: 'entry', isEntry: true, imports: ['_generatedA', '_generatedB', '_vue', '_i18n'] },
|
||||
_generatedA: { file: files.generatedA, name: generatedName },
|
||||
_generatedB: { file: files.generatedB, name: generatedName },
|
||||
_vue: { file: files.vue, name: 'vue' },
|
||||
_i18n: { file: files.i18n, name: 'i18n' },
|
||||
},
|
||||
sizes: Object.fromEntries(Object.entries(files).map(([key, file]) => [file, sizes[key as keyof typeof sizes]])),
|
||||
};
|
||||
}
|
||||
|
||||
test('groups generated chunks while preserving full and startup totals', async t => {
|
||||
const report = await runReport(
|
||||
t,
|
||||
fixture('before', 'dist', { entry: 100, generatedA: 10, generatedB: 20, vue: 40, i18n: 50 }),
|
||||
fixture('after', 'esm', { entry: 110, generatedA: 30, generatedB: 40, vue: 55, i18n: 50 }),
|
||||
);
|
||||
|
||||
assert.match(report, /\| \(total\) \| 220 B \| 285 B \|/);
|
||||
assert.equal(report.match(/\| \(other generated chunks\) \| 30 B \| 70 B \|/g)?.length, 2);
|
||||
assert.doesNotMatch(report, /generated chunks are grouped/);
|
||||
assert.doesNotMatch(report, /<summary>`(?:dist|esm)`<\/summary>/);
|
||||
assert.match(report, /<summary>`src\/_boot_\.ts`<\/summary>/);
|
||||
assert.match(report, /<summary>`vue`<\/summary>/);
|
||||
});
|
||||
|
||||
test('groups small deltas at the bottom while preserving all change counts', async t => {
|
||||
const before = fixture('before', 'dist', { entry: 100, generatedA: 10, generatedB: 20, vue: 40, i18n: 50 });
|
||||
before.manifest._removedSmall = { file: 'scripts/removed-small-before.js', src: 'src/removed-small.ts' };
|
||||
before.manifest['src/_boot_.ts'].imports?.push('_removedSmall');
|
||||
before.sizes['scripts/removed-small-before.js'] = 5;
|
||||
|
||||
const after = fixture('after', 'esm', { entry: 106, generatedA: 30, generatedB: 40, vue: 45, i18n: 50 });
|
||||
after.manifest._addedSmall = { file: 'scripts/added-small-after.js', src: 'src/added-small.ts' };
|
||||
after.manifest['src/_boot_.ts'].imports?.push('_addedSmall');
|
||||
after.sizes['scripts/added-small-after.js'] = 5;
|
||||
|
||||
const report = await runReport(t, before, after);
|
||||
|
||||
assert.match(report, /<summary>Chunk size diff \(2 updated, 1 added, 1 removed\)<\/summary>/);
|
||||
assert.match(report, /<summary>Startup chunk size \(2 updated, 1 added, 1 removed\)<\/summary>/);
|
||||
assert.equal(report.match(/<summary>`src\/_boot_\.ts`<\/summary>/g)?.length, 2);
|
||||
assert.doesNotMatch(report, /<summary>`(?:vue|i18n|src\/added-small\.ts|src\/removed-small\.ts)`<\/summary>/);
|
||||
assert.match(report, /\| \(total\) \| 225 B \| 276 B \|[^\n]*\n\| \| \| \| \| \|\n\| <details><summary>`src\/_boot_\.ts`<\/summary>[^\n]*\n\| \(other generated chunks\) \| 30 B \| 70 B \|[^\n]*\n\| \(other\) \| 45 B \| 50 B \|/);
|
||||
assert.match(report, /\| \(total\) \| 225 B \| 276 B \|[^\n]*\n\| \| \| \| \| \|\n\| <details><summary>`src\/_boot_\.ts`<\/summary>[^\n]*\n\| \(other generated chunks\) \| 30 B \| 70 B \|[^\n]*\n\| \(other\) \| 95 B \| 100 B \|/);
|
||||
});
|
||||
|
||||
test('fails instead of overwriting duplicate stable chunk keys', async t => {
|
||||
const duplicateVue = fixture('before', 'dist', { entry: 100, generatedA: 10, generatedB: 20, vue: 40, i18n: 50 });
|
||||
duplicateVue.manifest._vueDuplicate = { file: 'scripts/vue-duplicate-before.js', name: 'vue' };
|
||||
duplicateVue.sizes['scripts/vue-duplicate-before.js'] = 60;
|
||||
|
||||
const diagnostic = await runReport(
|
||||
t,
|
||||
duplicateVue,
|
||||
fixture('after', 'esm', { entry: 110, generatedA: 30, generatedB: 40, vue: 45, i18n: 50 }),
|
||||
true,
|
||||
);
|
||||
assert.match(diagnostic, /Duplicate stable chunk key "named:vue".*vue-before\.js.*vue-duplicate-before\.js/);
|
||||
});
|
||||
|
||||
test('shows both filenames for an updated stable chunk', async t => {
|
||||
const report = await runReport(
|
||||
t,
|
||||
fixture('before', 'dist', { entry: 100, generatedA: 10, generatedB: 20, vue: 40, i18n: 50 }),
|
||||
fixture('after', 'esm', { entry: 110, generatedA: 30, generatedB: 40, vue: 55, i18n: 50 }),
|
||||
);
|
||||
|
||||
assert.match(report, /`ja-JP\/entry-before\.js → ja-JP\/entry-after\.js`/);
|
||||
assert.match(report, /`ja-JP\/vue-before\.js → ja-JP\/vue-after\.js`/);
|
||||
});
|
||||
|
||||
test('fails descriptively when the startup entry is missing', async t => {
|
||||
const before = fixture('before', 'dist', { entry: 100, generatedA: 10, generatedB: 20, vue: 40, i18n: 50 });
|
||||
delete before.manifest['src/_boot_.ts'];
|
||||
delete before.sizes['scripts/entry-before.js'];
|
||||
|
||||
const diagnostic = await runReport(
|
||||
t,
|
||||
before,
|
||||
fixture('after', 'esm', { entry: 110, generatedA: 30, generatedB: 40, vue: 45, i18n: 50 }),
|
||||
true,
|
||||
);
|
||||
assert.match(diagnostic, /Unable to find frontend startup entry in Vite manifest/);
|
||||
});
|
||||
|
||||
test('fails descriptively when a static import is missing from the manifest', async t => {
|
||||
const before = fixture('before', 'dist', { entry: 100, generatedA: 10, generatedB: 20, vue: 40, i18n: 50 });
|
||||
before.manifest['src/_boot_.ts'].imports = ['_missing'];
|
||||
|
||||
const diagnostic = await runReport(
|
||||
t,
|
||||
before,
|
||||
fixture('after', 'esm', { entry: 110, generatedA: 30, generatedB: 40, vue: 45, i18n: 50 }),
|
||||
true,
|
||||
);
|
||||
assert.match(diagnostic, /Startup manifest key "_missing".*is missing/);
|
||||
});
|
||||
|
||||
test('fails descriptively when a static import has no output file', async t => {
|
||||
const before = fixture('before', 'dist', { entry: 100, generatedA: 10, generatedB: 20, vue: 40, i18n: 50 });
|
||||
before.manifest['src/_boot_.ts'].imports = ['_malformed'];
|
||||
before.manifest._malformed = { name: 'malformed' };
|
||||
|
||||
const diagnostic = await runReport(
|
||||
t,
|
||||
before,
|
||||
fixture('after', 'esm', { entry: 110, generatedA: 30, generatedB: 40, vue: 45, i18n: 50 }),
|
||||
true,
|
||||
);
|
||||
assert.match(diagnostic, /Startup manifest key "_malformed".*has no output file/);
|
||||
});
|
||||
|
||||
test('fails descriptively when a static import resolves to a non-JavaScript output', async t => {
|
||||
const before = fixture('before', 'dist', { entry: 100, generatedA: 10, generatedB: 20, vue: 40, i18n: 50 });
|
||||
before.manifest['src/_boot_.ts'].imports = ['_malformed'];
|
||||
before.manifest._malformed = { file: 'assets/malformed.css', name: 'malformed' };
|
||||
|
||||
const diagnostic = await runReport(
|
||||
t,
|
||||
before,
|
||||
fixture('after', 'esm', { entry: 110, generatedA: 30, generatedB: 40, vue: 45, i18n: 50 }),
|
||||
true,
|
||||
);
|
||||
assert.match(diagnostic, /Startup manifest key "_malformed".*non-JavaScript output "assets\/malformed\.css"/);
|
||||
});
|
||||
|
||||
test('keeps source-backed additions and removals as individual rows', async t => {
|
||||
const before = fixture('before', 'dist', { entry: 100, generatedA: 10, generatedB: 20, vue: 40, i18n: 50 });
|
||||
before.manifest._removed = { file: 'scripts/removed-before.js', src: 'src/removed.ts' };
|
||||
before.sizes['scripts/removed-before.js'] = 12;
|
||||
const after = fixture('after', 'esm', { entry: 110, generatedA: 30, generatedB: 40, vue: 45, i18n: 50 });
|
||||
after.manifest._added = { file: 'scripts/added-after.js', src: 'src/added.ts' };
|
||||
after.sizes['scripts/added-after.js'] = 13;
|
||||
|
||||
const report = await runReport(t, before, after);
|
||||
|
||||
assert.match(report, /<summary>`src\/added\.ts`<\/summary> `ja-JP\/added-after\.js` <\/details> \| 0 B \| 13 B \|/);
|
||||
assert.match(report, /<summary>`src\/removed\.ts`<\/summary> `ja-JP\/removed-before\.js` <\/details> \| 12 B \| 0 B \|/);
|
||||
});
|
||||
|
||||
test('counts an unmanifested localized JavaScript file in totals and the generated aggregate', async t => {
|
||||
const before = fixture('before', 'dist', { entry: 100, generatedA: 10, generatedB: 20, vue: 40, i18n: 50 });
|
||||
before.sizes['scripts/unmanifested-before.js'] = 15;
|
||||
|
||||
const report = await runReport(
|
||||
t,
|
||||
before,
|
||||
fixture('after', 'esm', { entry: 110, generatedA: 30, generatedB: 40, vue: 45, i18n: 50 }),
|
||||
);
|
||||
|
||||
assert.match(report, /\| \(total\) \| 235 B \| 275 B \|/);
|
||||
assert.match(report, /\| \(other generated chunks\) \| 45 B \| 70 B \|/);
|
||||
});
|
||||
|
||||
test('counts duplicate manifest entries for one physical output only once', async t => {
|
||||
const before = fixture('before', 'dist', { entry: 100, generatedA: 10, generatedB: 20, vue: 40, i18n: 50 });
|
||||
before.manifest._generatedAlias = { file: 'scripts/generated-a-before.js', name: 'dist' };
|
||||
|
||||
const report = await runReport(
|
||||
t,
|
||||
before,
|
||||
fixture('after', 'esm', { entry: 110, generatedA: 30, generatedB: 40, vue: 45, i18n: 50 }),
|
||||
);
|
||||
|
||||
assert.match(report, /\| \(total\) \| 220 B \| 275 B \|/);
|
||||
});
|
||||
|
||||
test('does not count generated-aggregate-only changes as individual chunk changes', async t => {
|
||||
const report = await runReport(
|
||||
t,
|
||||
fixture('before', 'dist', { entry: 100, generatedA: 10, generatedB: 20, vue: 40, i18n: 50 }),
|
||||
fixture('after', 'esm', { entry: 100, generatedA: 30, generatedB: 40, vue: 40, i18n: 50 }),
|
||||
);
|
||||
|
||||
assert.match(report, /<summary>Chunk size diff \(0 updated, 0 added, 0 removed\)<\/summary>/);
|
||||
assert.match(report, /<summary>Startup chunk size \(0 updated, 0 added, 0 removed\)<\/summary>/);
|
||||
assert.equal(report.match(/\| \(other generated chunks\) \| 30 B \| 70 B \|/g)?.length, 2);
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
name: Report backend memory
|
||||
name: Backend diagnostics (comment)
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
types: [completed]
|
||||
workflows:
|
||||
- Get backend memory usage # get-backend-memory.yml
|
||||
- Backend diagnostics (inspect) # backend-diagnostics.inspect.yml
|
||||
|
||||
jobs:
|
||||
compare-memory:
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
run: echo "pr-number=$(cat artifacts/pr_number)" >> "$GITHUB_OUTPUT"
|
||||
- name: Find heap snapshot artifacts
|
||||
id: find-heap-snapshot-artifacts
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const { owner, repo } = context.repo;
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
env:
|
||||
MK_MEMORY_HEAP_SNAPSHOT_ARTIFACT_URL_BASE: ${{ steps.find-heap-snapshot-artifacts.outputs.base-url }}
|
||||
MK_MEMORY_HEAP_SNAPSHOT_ARTIFACT_URL_HEAD: ${{ steps.find-heap-snapshot-artifacts.outputs.head-url }}
|
||||
run: node .github/scripts/backend-memory-report.mts ./artifacts/memory-base.json ./artifacts/memory-head.json ./output.md ./artifacts/js-footprint-base.json ./artifacts/js-footprint-head.json
|
||||
run: node .github/scripts/backend-diagnostics.render-md.mts ./artifacts/memory-base.json ./artifacts/memory-head.json ./output.md
|
||||
- uses: thollander/actions-comment-pull-request@v3
|
||||
with:
|
||||
pr-number: ${{ steps.load-pr-num.outputs.pr-number }}
|
||||
@@ -1,5 +1,5 @@
|
||||
# this name is used in report-backend-memory.yml so be careful when change name
|
||||
name: Get backend memory usage
|
||||
# this name is used in backend-diagnostics.comment.yml so be careful when change name
|
||||
name: Backend diagnostics (inspect)
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -10,14 +10,11 @@ on:
|
||||
- packages/backend/**
|
||||
- packages/misskey-js/**
|
||||
- .github/scripts/utility.mts
|
||||
- .github/scripts/backend-memory-report.mts
|
||||
- .github/scripts/measure-backend-memory-comparison.mts
|
||||
- .github/scripts/backend-diagnostics.render-md.mts
|
||||
- .github/scripts/backend-diagnostics.inspect.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
|
||||
- .github/workflows/get-backend-memory.yml
|
||||
- .github/workflows/report-backend-memory.yml
|
||||
- .github/workflows/backend-diagnostics.inspect.yml
|
||||
- .github/workflows/backend-diagnostics.comment.yml
|
||||
|
||||
jobs:
|
||||
get-memory-usage:
|
||||
@@ -96,7 +93,7 @@ jobs:
|
||||
MK_MEMORY_COMPARE_ROUNDS: 10
|
||||
MK_MEMORY_COMPARE_WARMUP_ROUNDS: 1
|
||||
MK_MEMORY_HEAP_SNAPSHOT: 1
|
||||
run: node head/.github/scripts/measure-backend-memory-comparison.mts base head memory-base.json memory-head.json
|
||||
run: node head/.github/scripts/backend-diagnostics.inspect.mts base head memory-base.json memory-head.json
|
||||
- name: Upload base heap snapshot
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
@@ -111,10 +108,6 @@ jobs:
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
- name: Measure backend loaded JS footprint
|
||||
run: |
|
||||
node head/.github/scripts/backend-js-footprint.mjs base js-footprint-base.json
|
||||
node head/.github/scripts/backend-js-footprint.mjs head js-footprint-head.json
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
@@ -122,8 +115,6 @@ jobs:
|
||||
path: |
|
||||
memory-base.json
|
||||
memory-head.json
|
||||
js-footprint-base.json
|
||||
js-footprint-head.json
|
||||
|
||||
save-pr-number:
|
||||
runs-on: ubuntu-latest
|
||||
@@ -1,9 +1,9 @@
|
||||
name: frontend-browser-metrics-report-comment
|
||||
name: Frontend browser diagnostics (comment)
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- frontend-browser-metrics-report
|
||||
- Frontend browser diagnostics (inspect)
|
||||
types:
|
||||
- completed
|
||||
|
||||
@@ -15,18 +15,18 @@ permissions:
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
name: Comment frontend browser metrics report
|
||||
name: Comment frontend browser diagnostics report
|
||||
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: frontend-browser-metrics-report-comment-${{ github.event.workflow_run.id }}
|
||||
group: frontend-browser-diagnostics-report-${{ github.event.workflow_run.id }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Download browser metrics report
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: frontend-browser-metrics-report
|
||||
path: ${{ runner.temp }}/frontend-browser-metrics-report
|
||||
name: frontend-browser-diagnostics
|
||||
path: ${{ runner.temp }}/frontend-browser-diagnostics
|
||||
github-token: ${{ github.token }}
|
||||
repository: ${{ github.repository }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
@@ -34,11 +34,11 @@ jobs:
|
||||
- name: Load PR number
|
||||
id: load-pr-number
|
||||
shell: bash
|
||||
run: echo "pr-number=$(cat "$RUNNER_TEMP/frontend-browser-metrics-report/pr-number.txt")" >> "$GITHUB_OUTPUT"
|
||||
run: echo "pr-number=$(cat "$RUNNER_TEMP/frontend-browser-diagnostics/pr-number.txt")" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Comment on pull request
|
||||
uses: thollander/actions-comment-pull-request@v3
|
||||
with:
|
||||
pr-number: ${{ steps.load-pr-number.outputs.pr-number }}
|
||||
comment-tag: frontend_browser_metrics_report
|
||||
file-path: ${{ runner.temp }}/frontend-browser-metrics-report/frontend-browser-metrics-report.md
|
||||
comment-tag: frontend-browser-diagnostics
|
||||
file-path: ${{ runner.temp }}/frontend-browser-diagnostics/frontend-browser-diagnostics.md
|
||||
@@ -1,4 +1,4 @@
|
||||
name: frontend-browser-metrics-report
|
||||
name: Frontend browser diagnostics (inspect)
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -23,20 +23,20 @@ on:
|
||||
- .node-version
|
||||
- .github/misskey/test.yml
|
||||
- .github/scripts/utility.mts
|
||||
- .github/scripts/frontend-browser-detailed-html.mts
|
||||
- .github/scripts/frontend-browser-report.mts
|
||||
- .github/scripts/heap-snapshot-util.mts
|
||||
- .github/scripts/measure-frontend-browser-comparison.mts
|
||||
- .github/scripts/chrome.mts
|
||||
- .github/workflows/frontend-browser-metrics-report.yml
|
||||
- .github/workflows/frontend-browser-metrics-report-comment.yml
|
||||
- .github/scripts/frontend-browser-diagnostics.render-additional-html.mts
|
||||
- .github/scripts/frontend-browser-diagnostics.render-md.mts
|
||||
- .github/scripts/frontend-browser-diagnostics.inspect.mts
|
||||
- .github/workflows/frontend-browser-diagnostics.inspect.yml
|
||||
- .github/workflows/frontend-browser-diagnostics.comment.yml
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: frontend-browser-metrics-report-${{ github.event.pull_request.number }}
|
||||
group: frontend-browser-diagnostics-inspect-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
@@ -60,7 +60,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout base
|
||||
uses: actions/checkout@v6.0.2
|
||||
uses: actions/checkout@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: ${{ github.event.pull_request.base.repo.full_name }}
|
||||
@@ -69,7 +69,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Checkout pull request
|
||||
uses: actions/checkout@v6.0.2
|
||||
uses: actions/checkout@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
@@ -78,7 +78,7 @@ jobs:
|
||||
submodules: true
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
with:
|
||||
package_json_file: after/package.json
|
||||
|
||||
@@ -125,33 +125,42 @@ jobs:
|
||||
FRONTEND_BROWSER_METRICS_SAMPLE_COUNT: 5
|
||||
MK_ENABLE_CROSS_ORIGIN_ISOLATION: "true"
|
||||
run: |
|
||||
REPORT_DIR="$RUNNER_TEMP/frontend-browser-metrics-report"
|
||||
REPORT_DIR="$RUNNER_TEMP/frontend-browser-diagnostics"
|
||||
mkdir -p "$REPORT_DIR"
|
||||
node after/.github/scripts/measure-frontend-browser-comparison.mts before after "$REPORT_DIR/before-browser.json" "$REPORT_DIR/after-browser.json" "$REPORT_DIR/head-heap-snapshot.heapsnapshot"
|
||||
node after/.github/scripts/frontend-browser-diagnostics.inspect.mts before after "$REPORT_DIR/before-browser.json" "$REPORT_DIR/after-browser.json"
|
||||
|
||||
- name: Upload browser base heap snapshot
|
||||
id: upload-browser-base-heap-snapshot
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
path: base-heap-snapshot.heapsnapshot
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload browser head heap snapshot
|
||||
id: upload-browser-head-heap-snapshot
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: frontend-browser-metrics-head-heap-snapshot
|
||||
path: ${{ runner.temp }}/frontend-browser-metrics-report/head-heap-snapshot.heapsnapshot
|
||||
path: head-heap-snapshot.heapsnapshot
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
- name: Generate browser detailed html
|
||||
shell: bash
|
||||
run: |
|
||||
REPORT_DIR="$RUNNER_TEMP/frontend-browser-metrics-report"
|
||||
REPORT_DIR="$RUNNER_TEMP/frontend-browser-diagnostics"
|
||||
test -s "$REPORT_DIR/before-browser.json"
|
||||
test -s "$REPORT_DIR/after-browser.json"
|
||||
node after/.github/scripts/frontend-browser-detailed-html.mts "$REPORT_DIR/before-browser.json" "$REPORT_DIR/after-browser.json" "$REPORT_DIR/frontend-browser-detailed-html.html"
|
||||
node after/.github/scripts/frontend-browser-diagnostics.render-additional-html.mts "$REPORT_DIR/before-browser.json" "$REPORT_DIR/after-browser.json" "$REPORT_DIR/frontend-browser-detailed-html.html"
|
||||
|
||||
- name: Upload browser detailed html
|
||||
id: upload-browser-detailed-html
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: frontend-browser-metrics-detailed-html
|
||||
path: ${{ runner.temp }}/frontend-browser-metrics-report/frontend-browser-detailed-html.html
|
||||
path: ${{ runner.temp }}/frontend-browser-diagnostics/frontend-browser-detailed-html.html
|
||||
if-no-files-found: error
|
||||
archive: false
|
||||
retention-days: 7
|
||||
@@ -162,13 +171,14 @@ jobs:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
FRONTEND_BROWSER_BASE_HEAP_SNAPSHOT_ARTIFACT_URL: ${{ steps.upload-browser-base-heap-snapshot.outputs.artifact-url }}
|
||||
FRONTEND_BROWSER_HEAD_HEAP_SNAPSHOT_ARTIFACT_URL: ${{ steps.upload-browser-head-heap-snapshot.outputs.artifact-url }}
|
||||
FRONTEND_BROWSER_DETAILED_HTML_ARTIFACT_URL: ${{ steps.upload-browser-detailed-html.outputs.artifact-url }}
|
||||
run: |
|
||||
REPORT_DIR="$RUNNER_TEMP/frontend-browser-metrics-report"
|
||||
REPORT_DIR="$RUNNER_TEMP/frontend-browser-diagnostics"
|
||||
test -s "$REPORT_DIR/before-browser.json"
|
||||
test -s "$REPORT_DIR/after-browser.json"
|
||||
node after/.github/scripts/frontend-browser-report.mts "$REPORT_DIR/before-browser.json" "$REPORT_DIR/after-browser.json" "$REPORT_DIR/frontend-browser-metrics-report.md"
|
||||
node after/.github/scripts/frontend-browser-diagnostics.render-md.mts "$REPORT_DIR/before-browser.json" "$REPORT_DIR/after-browser.json" "$REPORT_DIR/frontend-browser-diagnostics.md"
|
||||
printf '%s\n' "$PR_NUMBER" > "$REPORT_DIR/pr-number.txt"
|
||||
printf '%s\n' "$BASE_SHA" > "$REPORT_DIR/base-sha.txt"
|
||||
printf '%s\n' "$HEAD_SHA" > "$REPORT_DIR/head-sha.txt"
|
||||
@@ -177,24 +187,24 @@ jobs:
|
||||
- name: Check browser metrics report
|
||||
shell: bash
|
||||
run: |
|
||||
REPORT_DIR="$RUNNER_TEMP/frontend-browser-metrics-report"
|
||||
test -s "$REPORT_DIR/frontend-browser-metrics-report.md"
|
||||
REPORT_DIR="$RUNNER_TEMP/frontend-browser-diagnostics"
|
||||
test -s "$REPORT_DIR/frontend-browser-diagnostics.md"
|
||||
test -s "$REPORT_DIR/frontend-browser-detailed-html.html"
|
||||
test -s "$REPORT_DIR/pr-number.txt"
|
||||
test -s "$REPORT_DIR/head-sha.txt"
|
||||
cat "$REPORT_DIR/frontend-browser-metrics-report.md" >> "$GITHUB_STEP_SUMMARY"
|
||||
cat "$REPORT_DIR/frontend-browser-diagnostics.md" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload browser metrics report
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: frontend-browser-metrics-report
|
||||
name: frontend-browser-diagnostics
|
||||
path: |
|
||||
${{ runner.temp }}/frontend-browser-metrics-report/before-browser.json
|
||||
${{ runner.temp }}/frontend-browser-metrics-report/after-browser.json
|
||||
${{ runner.temp }}/frontend-browser-metrics-report/frontend-browser-metrics-report.md
|
||||
${{ runner.temp }}/frontend-browser-metrics-report/pr-number.txt
|
||||
${{ runner.temp }}/frontend-browser-metrics-report/base-sha.txt
|
||||
${{ runner.temp }}/frontend-browser-metrics-report/head-sha.txt
|
||||
${{ runner.temp }}/frontend-browser-metrics-report/pr-url.txt
|
||||
${{ runner.temp }}/frontend-browser-diagnostics/before-browser.json
|
||||
${{ runner.temp }}/frontend-browser-diagnostics/after-browser.json
|
||||
${{ runner.temp }}/frontend-browser-diagnostics/frontend-browser-diagnostics.md
|
||||
${{ runner.temp }}/frontend-browser-diagnostics/pr-number.txt
|
||||
${{ runner.temp }}/frontend-browser-diagnostics/base-sha.txt
|
||||
${{ runner.temp }}/frontend-browser-diagnostics/head-sha.txt
|
||||
${{ runner.temp }}/frontend-browser-diagnostics/pr-url.txt
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
44
.github/workflows/frontend-bundle-diagnostics.comment.yml
vendored
Normal file
44
.github/workflows/frontend-bundle-diagnostics.comment.yml
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
name: Frontend bundle diagnostics (comment)
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Frontend bundle diagnostics (inspect)
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
name: Comment frontend bundle report
|
||||
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: frontend-bundle-diagnostics-report-${{ github.event.workflow_run.id }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Download bundle report
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: frontend-bundle-report
|
||||
path: ${{ runner.temp }}/frontend-bundle-report
|
||||
github-token: ${{ github.token }}
|
||||
repository: ${{ github.repository }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
- name: Load PR number
|
||||
id: load-pr-number
|
||||
shell: bash
|
||||
run: echo "pr-number=$(cat "$RUNNER_TEMP/frontend-bundle-report/pr-number.txt")" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Comment on pull request
|
||||
uses: thollander/actions-comment-pull-request@v3
|
||||
with:
|
||||
pr-number: ${{ steps.load-pr-number.outputs.pr-number }}
|
||||
comment-tag: frontend-bundle-diagnostics
|
||||
file-path: ${{ runner.temp }}/frontend-bundle-report/frontend-bundle-diagnostics-report.md
|
||||
@@ -1,4 +1,4 @@
|
||||
name: frontend-bundle-report
|
||||
name: Frontend bundle diagnostics (inspect)
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -21,24 +21,22 @@ on:
|
||||
- pnpm-workspace.yaml
|
||||
- .node-version
|
||||
- .github/scripts/utility.mts
|
||||
- .github/scripts/frontend-js-size.mts
|
||||
- .github/workflows/frontend-bundle-report.yml
|
||||
- .github/workflows/frontend-bundle-report-comment.yml
|
||||
- .github/scripts/frontend-bundle-diagnostics.render-md.mts
|
||||
- .github/workflows/frontend-bundle-diagnostics.inspect.yml
|
||||
- .github/workflows/frontend-bundle-diagnostics.comment.yml
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: frontend-bundle-report-${{ github.event.pull_request.number }}
|
||||
group: frontend-bundle-diagnostics-inspect-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
report:
|
||||
name: Build frontend bundle report
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
FRONTEND_JS_SIZE_LOCALE: ja-JP
|
||||
steps:
|
||||
- name: Checkout base
|
||||
uses: actions/checkout@v6.0.3
|
||||
@@ -56,25 +54,12 @@ jobs:
|
||||
path: after
|
||||
submodules: true
|
||||
|
||||
- name: Check base visualizer support
|
||||
id: check-base-visualizer
|
||||
shell: bash
|
||||
run: |
|
||||
if grep -q 'FRONTEND_BUNDLE_VISUALIZER' before/packages/frontend/vite.config.ts; then
|
||||
echo 'supported=true' >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo 'supported=false' >> "$GITHUB_OUTPUT"
|
||||
echo 'Base commit does not support frontend bundle visualizer. Skipping frontend bundle report.' >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
- name: Setup pnpm
|
||||
if: steps.check-base-visualizer.outputs.supported == 'true'
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
with:
|
||||
package_json_file: after/package.json
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.check-base-visualizer.outputs.supported == 'true'
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: after/.node-version
|
||||
@@ -84,21 +69,17 @@ jobs:
|
||||
after/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies for base
|
||||
if: steps.check-base-visualizer.outputs.supported == 'true'
|
||||
working-directory: before
|
||||
run: pnpm i --frozen-lockfile
|
||||
|
||||
- name: Build frontend dependencies for base
|
||||
if: steps.check-base-visualizer.outputs.supported == 'true'
|
||||
working-directory: before
|
||||
run: pnpm --filter "frontend^..." run build
|
||||
|
||||
- name: Prepare report output
|
||||
if: steps.check-base-visualizer.outputs.supported == 'true'
|
||||
run: mkdir -p "$RUNNER_TEMP/frontend-bundle-report"
|
||||
|
||||
- name: Build frontend report for base
|
||||
if: steps.check-base-visualizer.outputs.supported == 'true'
|
||||
working-directory: before
|
||||
env:
|
||||
FRONTEND_BUNDLE_VISUALIZER: 'true'
|
||||
@@ -106,17 +87,14 @@ jobs:
|
||||
run: pnpm --filter frontend run build
|
||||
|
||||
- name: Install dependencies for pull request
|
||||
if: steps.check-base-visualizer.outputs.supported == 'true'
|
||||
working-directory: after
|
||||
run: pnpm i --frozen-lockfile
|
||||
|
||||
- name: Build frontend dependencies for pull request
|
||||
if: steps.check-base-visualizer.outputs.supported == 'true'
|
||||
working-directory: after
|
||||
run: pnpm --filter "frontend^..." run build
|
||||
|
||||
- name: Build frontend report for pull request
|
||||
if: steps.check-base-visualizer.outputs.supported == 'true'
|
||||
working-directory: after
|
||||
env:
|
||||
FRONTEND_BUNDLE_VISUALIZER: 'true'
|
||||
@@ -125,7 +103,6 @@ jobs:
|
||||
run: pnpm --filter frontend run build
|
||||
|
||||
- name: Upload bundle visualizer
|
||||
if: steps.check-base-visualizer.outputs.supported == 'true'
|
||||
id: upload-bundle-visualizer
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
@@ -136,7 +113,6 @@ jobs:
|
||||
retention-days: 7
|
||||
|
||||
- name: Generate report markdown
|
||||
if: steps.check-base-visualizer.outputs.supported == 'true'
|
||||
shell: bash
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
@@ -145,23 +121,21 @@ jobs:
|
||||
FRONTEND_BUNDLE_REPORT_ARTIFACT_URL: ${{ steps.upload-bundle-visualizer.outputs.artifact-url }}
|
||||
run: |
|
||||
REPORT_DIR="$RUNNER_TEMP/frontend-bundle-report"
|
||||
node after/.github/scripts/frontend-js-size.mts before after "$REPORT_DIR/before-stats.json" "$REPORT_DIR/after-stats.json" "$REPORT_DIR/frontend-js-size-report.md"
|
||||
node after/.github/scripts/frontend-bundle-diagnostics.render-md.mts before after "$REPORT_DIR/before-stats.json" "$REPORT_DIR/after-stats.json" "$REPORT_DIR/frontend-bundle-diagnostics-report.md"
|
||||
printf '%s\n' "$PR_NUMBER" > "$REPORT_DIR/pr-number.txt"
|
||||
printf '%s\n' "$BASE_SHA" > "$REPORT_DIR/base-sha.txt"
|
||||
printf '%s\n' "$HEAD_SHA" > "$REPORT_DIR/head-sha.txt"
|
||||
printf '%s\n' "${{ github.event.pull_request.html_url }}" > "$REPORT_DIR/pr-url.txt"
|
||||
|
||||
- name: Check report
|
||||
if: steps.check-base-visualizer.outputs.supported == 'true'
|
||||
run: |
|
||||
REPORT_DIR="$RUNNER_TEMP/frontend-bundle-report"
|
||||
test -s "$REPORT_DIR/before-stats.json"
|
||||
test -s "$REPORT_DIR/after-stats.json"
|
||||
test -s "$REPORT_DIR/frontend-js-size-report.md"
|
||||
cat "$REPORT_DIR/frontend-js-size-report.md" >> "$GITHUB_STEP_SUMMARY"
|
||||
test -s "$REPORT_DIR/frontend-bundle-diagnostics-report.md"
|
||||
cat "$REPORT_DIR/frontend-bundle-diagnostics-report.md" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload bundle report
|
||||
if: steps.check-base-visualizer.outputs.supported == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: frontend-bundle-report
|
||||
318
.github/workflows/frontend-bundle-report-comment.yml
vendored
318
.github/workflows/frontend-bundle-report-comment.yml
vendored
@@ -1,318 +0,0 @@
|
||||
name: frontend-bundle-report-comment
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- frontend-bundle-report
|
||||
types:
|
||||
- completed
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- ready_for_review
|
||||
paths:
|
||||
- packages/frontend/**
|
||||
- packages/frontend-shared/**
|
||||
- packages/frontend-builder/**
|
||||
- packages/i18n/**
|
||||
- packages/icons-subsetter/**
|
||||
- packages/misskey-js/**
|
||||
- packages/misskey-reversi/**
|
||||
- packages/misskey-bubble-game/**
|
||||
- package.json
|
||||
- pnpm-lock.yaml
|
||||
- pnpm-workspace.yaml
|
||||
- .node-version
|
||||
- .github/scripts/utility.mts
|
||||
- .github/scripts/frontend-js-size.mts
|
||||
- .github/workflows/frontend-bundle-report.yml
|
||||
- .github/workflows/frontend-bundle-report-comment.yml
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
name: Comment frontend bundle report
|
||||
if: github.event_name == 'pull_request_target' || (github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success')
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: frontend-bundle-report-comment-${{ github.event.pull_request.number || github.event.workflow_run.id }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Find bundle report run
|
||||
if: github.event_name == 'pull_request_target'
|
||||
id: find-report-run
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const workflow_id = 'frontend-bundle-report.yml';
|
||||
const artifactName = 'frontend-bundle-report';
|
||||
const headSha = context.payload.pull_request.head.sha;
|
||||
const prNumber = context.payload.pull_request.number;
|
||||
const pollIntervalMs = 30_000;
|
||||
const timeoutMs = 90 * 60_000;
|
||||
const startedAt = Date.now();
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
async function listReportWorkflowRuns() {
|
||||
const runsForHead = await github.paginate(github.rest.actions.listWorkflowRuns, {
|
||||
owner,
|
||||
repo,
|
||||
workflow_id,
|
||||
event: 'pull_request',
|
||||
head_sha: headSha,
|
||||
per_page: 100,
|
||||
});
|
||||
|
||||
if (runsForHead.length > 0) {
|
||||
return runsForHead;
|
||||
}
|
||||
|
||||
const recentRuns = await github.paginate(github.rest.actions.listWorkflowRuns, {
|
||||
owner,
|
||||
repo,
|
||||
workflow_id,
|
||||
event: 'pull_request',
|
||||
per_page: 100,
|
||||
});
|
||||
return recentRuns.filter((run) =>
|
||||
run.pull_requests?.some((pullRequest) => pullRequest.number === prNumber));
|
||||
}
|
||||
|
||||
async function findReportRun() {
|
||||
const runs = (await listReportWorkflowRuns())
|
||||
.sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at));
|
||||
|
||||
for (const run of runs) {
|
||||
if (run.status !== 'completed') continue;
|
||||
if (run.conclusion !== 'success') {
|
||||
core.warning(`Frontend bundle report run ${run.id} completed with conclusion: ${run.conclusion}`);
|
||||
return { done: true, run: null };
|
||||
}
|
||||
|
||||
const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
|
||||
owner,
|
||||
repo,
|
||||
run_id: run.id,
|
||||
per_page: 100,
|
||||
});
|
||||
const report = artifacts.find((artifact) => artifact.name === artifactName && !artifact.expired);
|
||||
if (report) return { done: true, run };
|
||||
|
||||
core.info(`Frontend bundle report run ${run.id} did not produce ${artifactName}.`);
|
||||
return { done: true, run: null };
|
||||
}
|
||||
|
||||
return { done: false, run: null };
|
||||
}
|
||||
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
const { done, run } = await findReportRun();
|
||||
if (run) {
|
||||
core.info(`Found frontend bundle report on workflow run ${run.id}.`);
|
||||
core.setOutput('run-id', String(run.id));
|
||||
return;
|
||||
}
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
|
||||
core.info('Waiting for frontend bundle report artifact...');
|
||||
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
||||
}
|
||||
|
||||
core.warning(`Timed out waiting for ${artifactName} from ${workflow_id} for ${headSha}.`);
|
||||
|
||||
- name: Find bundle report artifact
|
||||
if: github.event_name == 'workflow_run'
|
||||
id: find-report-artifact
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const artifactName = 'frontend-bundle-report';
|
||||
const { owner, repo } = context.repo;
|
||||
const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
|
||||
owner,
|
||||
repo,
|
||||
run_id: context.payload.workflow_run.id,
|
||||
per_page: 100,
|
||||
});
|
||||
const report = artifacts.find((artifact) => artifact.name === artifactName && !artifact.expired);
|
||||
if (report) {
|
||||
core.setOutput('exists', 'true');
|
||||
} else {
|
||||
core.info(`Workflow run ${context.payload.workflow_run.id} did not produce ${artifactName}.`);
|
||||
core.setOutput('exists', 'false');
|
||||
}
|
||||
|
||||
- name: Download bundle report from workflow_run
|
||||
if: github.event_name == 'workflow_run' && steps.find-report-artifact.outputs.exists == 'true'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: frontend-bundle-report
|
||||
path: ${{ runner.temp }}/frontend-bundle-report
|
||||
github-token: ${{ github.token }}
|
||||
repository: ${{ github.repository }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
- name: Download bundle report from pull_request_target
|
||||
if: github.event_name == 'pull_request_target' && steps.find-report-run.outputs.run-id != ''
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: frontend-bundle-report
|
||||
path: ${{ runner.temp }}/frontend-bundle-report
|
||||
github-token: ${{ github.token }}
|
||||
repository: ${{ github.repository }}
|
||||
run-id: ${{ steps.find-report-run.outputs.run-id }}
|
||||
|
||||
- name: Comment on pull request
|
||||
if: (github.event_name == 'workflow_run' && steps.find-report-artifact.outputs.exists == 'true') || steps.find-report-run.outputs.run-id != ''
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ secrets.FRONTEND_BUNDLE_REPORT_COMMENT_TOKEN || secrets.FRONTEND_JS_SIZE_COMMENT_TOKEN || secrets.FRONTEND_BUNDLE_VISUALIZER_COMMENT_TOKEN || github.token }}
|
||||
script: |
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const jsSizeMarker = '<!-- misskey-frontend-js-size -->';
|
||||
const visualizerMarker = '<!-- misskey-frontend-bundle-visualizer -->';
|
||||
const reportMarkers = [jsSizeMarker, visualizerMarker];
|
||||
const reportDir = path.join(process.env.RUNNER_TEMP, 'frontend-bundle-report');
|
||||
const jsSizeReportPath = path.join(reportDir, 'frontend-js-size-report.md');
|
||||
const prNumberPath = path.join(reportDir, 'pr-number.txt');
|
||||
const headShaPath = path.join(reportDir, 'head-sha.txt');
|
||||
const workflowRun = context.payload.workflow_run;
|
||||
const pullRequest = context.payload.pull_request;
|
||||
const eventHeadSha = workflowRun?.head_sha ?? pullRequest?.head?.sha ?? null;
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
if (!fs.existsSync(jsSizeReportPath)) {
|
||||
core.setFailed('The frontend bundle report artifact does not contain frontend-js-size-report.md.');
|
||||
return;
|
||||
}
|
||||
|
||||
const artifactHeadSha = fs.existsSync(headShaPath)
|
||||
? fs.readFileSync(headShaPath, 'utf8').trim()
|
||||
: null;
|
||||
if (eventHeadSha != null && artifactHeadSha != null && artifactHeadSha !== eventHeadSha) {
|
||||
core.info(`The artifact head SHA (${artifactHeadSha}) differs from the event head SHA (${eventHeadSha}). Using artifact metadata for PR validation.`);
|
||||
}
|
||||
const reportHeadSha = artifactHeadSha ?? eventHeadSha;
|
||||
|
||||
const artifactPrNumber = fs.existsSync(prNumberPath)
|
||||
? Number(fs.readFileSync(prNumberPath, 'utf8').trim())
|
||||
: null;
|
||||
let issue_number = null;
|
||||
if (pullRequest != null) {
|
||||
issue_number = pullRequest.number;
|
||||
if (Number.isInteger(artifactPrNumber) && artifactPrNumber !== issue_number) {
|
||||
core.setFailed(`The artifact pull request number (${artifactPrNumber}) does not match the event pull request number (${issue_number}).`);
|
||||
return;
|
||||
}
|
||||
} else if (workflowRun != null) {
|
||||
const associatedPullRequests = new Map();
|
||||
for (const pullRequest of workflowRun.pull_requests ?? []) {
|
||||
if (Number.isInteger(pullRequest.number)) {
|
||||
associatedPullRequests.set(pullRequest.number, pullRequest);
|
||||
}
|
||||
}
|
||||
|
||||
if (reportHeadSha != null) {
|
||||
const pullRequestsForCommit = await github.paginate(github.rest.repos.listPullRequestsAssociatedWithCommit, {
|
||||
owner,
|
||||
repo,
|
||||
commit_sha: reportHeadSha,
|
||||
per_page: 100,
|
||||
});
|
||||
for (const pullRequest of pullRequestsForCommit) {
|
||||
associatedPullRequests.set(pullRequest.number, pullRequest);
|
||||
}
|
||||
}
|
||||
|
||||
if (Number.isInteger(artifactPrNumber) && associatedPullRequests.has(artifactPrNumber)) {
|
||||
issue_number = artifactPrNumber;
|
||||
} else if (Number.isInteger(artifactPrNumber) && associatedPullRequests.size === 0) {
|
||||
issue_number = artifactPrNumber;
|
||||
} else if (!Number.isInteger(artifactPrNumber) && associatedPullRequests.size === 1) {
|
||||
issue_number = [...associatedPullRequests.keys()][0];
|
||||
} else if (Number.isInteger(artifactPrNumber)) {
|
||||
core.setFailed(`The artifact pull request number (${artifactPrNumber}) is not associated with ${reportHeadSha}.`);
|
||||
return;
|
||||
} else {
|
||||
core.setFailed(`Could not determine the pull request associated with ${reportHeadSha}.`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
core.setFailed('Could not determine the pull request event for this report.');
|
||||
return;
|
||||
}
|
||||
|
||||
const currentPullRequest = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: issue_number,
|
||||
});
|
||||
const currentHeadSha = currentPullRequest.data.head?.sha;
|
||||
if (reportHeadSha != null && currentHeadSha != null && reportHeadSha !== currentHeadSha) {
|
||||
core.info(`The report head SHA (${reportHeadSha}) is not the current pull request head SHA (${currentHeadSha}). Skipping stale frontend bundle report.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const jsSizeReport = fs.readFileSync(jsSizeReportPath, 'utf8').trim();
|
||||
if (!jsSizeReport.includes(jsSizeMarker)) {
|
||||
core.setFailed('The frontend JS size report is missing the expected marker.');
|
||||
return;
|
||||
}
|
||||
let body = `${jsSizeReport}\n`;
|
||||
|
||||
const maxCommentLength = 65_000;
|
||||
if (body.length > maxCommentLength) {
|
||||
const reportLocation = workflowRun?.html_url != null
|
||||
? `[workflow run](${workflowRun.html_url})`
|
||||
: 'workflow artifact';
|
||||
const footer = [
|
||||
'',
|
||||
'',
|
||||
`_Report truncated because it exceeded ${maxCommentLength.toLocaleString('en-US')} characters. See the ${reportLocation} for the full report._`,
|
||||
].join('\n');
|
||||
body = `${body.slice(0, maxCommentLength - footer.length)}${footer}`;
|
||||
}
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
per_page: 100,
|
||||
});
|
||||
const previousReports = comments.filter((comment) =>
|
||||
comment.user?.type === 'Bot' && reportMarkers.some((reportMarker) => comment.body?.includes(reportMarker)));
|
||||
|
||||
if (previousReports.length > 0) {
|
||||
const [previous, ...duplicates] = previousReports;
|
||||
await github.rest.issues.updateComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: previous.id,
|
||||
body,
|
||||
});
|
||||
for (const duplicate of duplicates) {
|
||||
await github.rest.issues.deleteComment({
|
||||
owner,
|
||||
repo,
|
||||
comment_id: duplicate.id,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await github.rest.issues.createComment({
|
||||
owner,
|
||||
repo,
|
||||
issue_number,
|
||||
body,
|
||||
});
|
||||
}
|
||||
20
.github/workflows/lint.yml
vendored
20
.github/workflows/lint.yml
vendored
@@ -18,9 +18,6 @@ on:
|
||||
- packages/misskey-reversi/**
|
||||
- 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
|
||||
pull_request:
|
||||
@@ -37,9 +34,6 @@ on:
|
||||
- packages/misskey-reversi/**
|
||||
- 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
|
||||
jobs:
|
||||
@@ -129,12 +123,12 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.2
|
||||
- uses: actions/checkout@v6.0.3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
- uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
@@ -142,13 +136,3 @@ jobs:
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- run: node --test scripts/check-dts.test.mjs
|
||||
- run: pnpm check-dts
|
||||
|
||||
frontend-bundle-report-test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
- uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: '.node-version'
|
||||
- run: node --test .github/scripts/frontend-js-size.test.mts
|
||||
- run: node --test .github/scripts/memory-stability-util.test.mts
|
||||
|
||||
@@ -2884,6 +2884,8 @@ type MiauthGenTokenRequest = operations['miauth___gen-token']['requestBody']['co
|
||||
// @public (undocumented)
|
||||
type MiauthGenTokenResponse = operations['miauth___gen-token']['responses']['200']['content']['application/json'];
|
||||
|
||||
// Warning: (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
||||
//
|
||||
// @public (undocumented)
|
||||
type ModerationLog = {
|
||||
id: ID;
|
||||
@@ -2891,165 +2893,11 @@ type ModerationLog = {
|
||||
userId: User['id'];
|
||||
user: UserDetailedNotMe;
|
||||
} & ({
|
||||
type: 'updateServerSettings';
|
||||
info: ModerationLogPayloads['updateServerSettings'];
|
||||
} | {
|
||||
type: 'suspend';
|
||||
info: ModerationLogPayloads['suspend'];
|
||||
} | {
|
||||
type: 'unsuspend';
|
||||
info: ModerationLogPayloads['unsuspend'];
|
||||
} | {
|
||||
type: 'updateUserNote';
|
||||
info: ModerationLogPayloads['updateUserNote'];
|
||||
} | {
|
||||
type: 'addCustomEmoji';
|
||||
info: ModerationLogPayloads['addCustomEmoji'];
|
||||
} | {
|
||||
type: 'updateCustomEmoji';
|
||||
info: ModerationLogPayloads['updateCustomEmoji'];
|
||||
} | {
|
||||
type: 'deleteCustomEmoji';
|
||||
info: ModerationLogPayloads['deleteCustomEmoji'];
|
||||
} | {
|
||||
type: 'assignRole';
|
||||
info: ModerationLogPayloads['assignRole'];
|
||||
} | {
|
||||
type: 'unassignRole';
|
||||
info: ModerationLogPayloads['unassignRole'];
|
||||
} | {
|
||||
type: 'createRole';
|
||||
info: ModerationLogPayloads['createRole'];
|
||||
} | {
|
||||
type: 'updateRole';
|
||||
info: ModerationLogPayloads['updateRole'];
|
||||
} | {
|
||||
type: 'deleteRole';
|
||||
info: ModerationLogPayloads['deleteRole'];
|
||||
} | {
|
||||
type: 'clearQueue';
|
||||
info: ModerationLogPayloads['clearQueue'];
|
||||
} | {
|
||||
type: 'promoteQueue';
|
||||
info: ModerationLogPayloads['promoteQueue'];
|
||||
} | {
|
||||
type: 'deleteDriveFile';
|
||||
info: ModerationLogPayloads['deleteDriveFile'];
|
||||
} | {
|
||||
type: 'deleteNote';
|
||||
info: ModerationLogPayloads['deleteNote'];
|
||||
} | {
|
||||
type: 'createGlobalAnnouncement';
|
||||
info: ModerationLogPayloads['createGlobalAnnouncement'];
|
||||
} | {
|
||||
type: 'createUserAnnouncement';
|
||||
info: ModerationLogPayloads['createUserAnnouncement'];
|
||||
} | {
|
||||
type: 'updateGlobalAnnouncement';
|
||||
info: ModerationLogPayloads['updateGlobalAnnouncement'];
|
||||
} | {
|
||||
type: 'updateUserAnnouncement';
|
||||
info: ModerationLogPayloads['updateUserAnnouncement'];
|
||||
} | {
|
||||
type: 'deleteGlobalAnnouncement';
|
||||
info: ModerationLogPayloads['deleteGlobalAnnouncement'];
|
||||
} | {
|
||||
type: 'deleteUserAnnouncement';
|
||||
info: ModerationLogPayloads['deleteUserAnnouncement'];
|
||||
} | {
|
||||
type: 'resetPassword';
|
||||
info: ModerationLogPayloads['resetPassword'];
|
||||
} | {
|
||||
type: 'suspendRemoteInstance';
|
||||
info: ModerationLogPayloads['suspendRemoteInstance'];
|
||||
} | {
|
||||
type: 'unsuspendRemoteInstance';
|
||||
info: ModerationLogPayloads['unsuspendRemoteInstance'];
|
||||
} | {
|
||||
type: 'updateRemoteInstanceNote';
|
||||
info: ModerationLogPayloads['updateRemoteInstanceNote'];
|
||||
} | {
|
||||
type: 'markSensitiveDriveFile';
|
||||
info: ModerationLogPayloads['markSensitiveDriveFile'];
|
||||
} | {
|
||||
type: 'unmarkSensitiveDriveFile';
|
||||
info: ModerationLogPayloads['unmarkSensitiveDriveFile'];
|
||||
} | {
|
||||
type: 'createInvitation';
|
||||
info: ModerationLogPayloads['createInvitation'];
|
||||
} | {
|
||||
type: 'createAd';
|
||||
info: ModerationLogPayloads['createAd'];
|
||||
} | {
|
||||
type: 'updateAd';
|
||||
info: ModerationLogPayloads['updateAd'];
|
||||
} | {
|
||||
type: 'deleteAd';
|
||||
info: ModerationLogPayloads['deleteAd'];
|
||||
} | {
|
||||
type: 'createAvatarDecoration';
|
||||
info: ModerationLogPayloads['createAvatarDecoration'];
|
||||
} | {
|
||||
type: 'updateAvatarDecoration';
|
||||
info: ModerationLogPayloads['updateAvatarDecoration'];
|
||||
} | {
|
||||
type: 'deleteAvatarDecoration';
|
||||
info: ModerationLogPayloads['deleteAvatarDecoration'];
|
||||
} | {
|
||||
type: 'resolveAbuseReport';
|
||||
info: ModerationLogPayloads['resolveAbuseReport'];
|
||||
} | {
|
||||
type: 'forwardAbuseReport';
|
||||
info: ModerationLogPayloads['forwardAbuseReport'];
|
||||
} | {
|
||||
type: 'updateAbuseReportNote';
|
||||
info: ModerationLogPayloads['updateAbuseReportNote'];
|
||||
} | {
|
||||
type: 'unsetMfa';
|
||||
info: ModerationLogPayloads['unsetMfa'];
|
||||
} | {
|
||||
type: 'unsetUserAvatar';
|
||||
info: ModerationLogPayloads['unsetUserAvatar'];
|
||||
} | {
|
||||
type: 'unsetUserBanner';
|
||||
info: ModerationLogPayloads['unsetUserBanner'];
|
||||
} | {
|
||||
type: 'createSystemWebhook';
|
||||
info: ModerationLogPayloads['createSystemWebhook'];
|
||||
} | {
|
||||
type: 'updateSystemWebhook';
|
||||
info: ModerationLogPayloads['updateSystemWebhook'];
|
||||
} | {
|
||||
type: 'deleteSystemWebhook';
|
||||
info: ModerationLogPayloads['deleteSystemWebhook'];
|
||||
} | {
|
||||
type: 'createAbuseReportNotificationRecipient';
|
||||
info: ModerationLogPayloads['createAbuseReportNotificationRecipient'];
|
||||
} | {
|
||||
type: 'updateAbuseReportNotificationRecipient';
|
||||
info: ModerationLogPayloads['updateAbuseReportNotificationRecipient'];
|
||||
} | {
|
||||
type: 'deleteAbuseReportNotificationRecipient';
|
||||
info: ModerationLogPayloads['deleteAbuseReportNotificationRecipient'];
|
||||
} | {
|
||||
type: 'deleteAccount';
|
||||
info: ModerationLogPayloads['deleteAccount'];
|
||||
} | {
|
||||
type: 'deletePage';
|
||||
info: ModerationLogPayloads['deletePage'];
|
||||
} | {
|
||||
type: 'deleteFlash';
|
||||
info: ModerationLogPayloads['deleteFlash'];
|
||||
} | {
|
||||
type: 'deleteGalleryPost';
|
||||
info: ModerationLogPayloads['deleteGalleryPost'];
|
||||
} | {
|
||||
type: 'deleteChatRoom';
|
||||
info: ModerationLogPayloads['deleteChatRoom'];
|
||||
} | {
|
||||
type: 'updateProxyAccountDescription';
|
||||
info: ModerationLogPayloads['updateProxyAccountDescription'];
|
||||
});
|
||||
[K in keyof ModerationLogPayloads]: {
|
||||
type: K;
|
||||
info: ModerationLogPayloads[K];
|
||||
};
|
||||
}[keyof ModerationLogPayloads]);
|
||||
|
||||
// @public (undocumented)
|
||||
export const moderationLogTypes: readonly ["updateServerSettings", "suspend", "unsuspend", "updateUserNote", "addCustomEmoji", "updateCustomEmoji", "deleteCustomEmoji", "assignRole", "unassignRole", "createRole", "updateRole", "deleteRole", "clearQueue", "promoteQueue", "deleteDriveFile", "deleteNote", "createGlobalAnnouncement", "createUserAnnouncement", "updateGlobalAnnouncement", "updateUserAnnouncement", "deleteGlobalAnnouncement", "deleteUserAnnouncement", "resetPassword", "suspendRemoteInstance", "unsuspendRemoteInstance", "updateRemoteInstanceNote", "markSensitiveDriveFile", "unmarkSensitiveDriveFile", "resolveAbuseReport", "forwardAbuseReport", "updateAbuseReportNote", "createInvitation", "createAd", "updateAd", "deleteAd", "createAvatarDecoration", "updateAvatarDecoration", "deleteAvatarDecoration", "unsetMfa", "unsetUserAvatar", "unsetUserBanner", "createSystemWebhook", "updateSystemWebhook", "deleteSystemWebhook", "createAbuseReportNotificationRecipient", "updateAbuseReportNotificationRecipient", "deleteAbuseReportNotificationRecipient", "deleteAccount", "deletePage", "deleteFlash", "deleteGalleryPost", "deleteChatRoom", "updateProxyAccountDescription"];
|
||||
@@ -3922,7 +3770,6 @@ type VerifyEmailRequest = operations['verify-email']['requestBody']['content']['
|
||||
|
||||
// Warnings were encountered during analysis:
|
||||
//
|
||||
// src/entities.ts:60:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
||||
// src/streaming.ts:57:3 - (ae-forgotten-export) The symbol "ReconnectingWebSocket" needs to be exported by the entry point index.d.ts
|
||||
// src/streaming.types.ts:226:4 - (ae-forgotten-export) The symbol "ReversiUpdateKey" needs to be exported by the entry point index.d.ts
|
||||
// src/streaming.types.ts:241:4 - (ae-forgotten-export) The symbol "ReversiUpdateSettings" needs to be exported by the entry point index.d.ts
|
||||
|
||||
@@ -56,165 +56,11 @@ export type ModerationLog = {
|
||||
userId: User['id'];
|
||||
user: UserDetailedNotMe;
|
||||
} & ({
|
||||
type: 'updateServerSettings';
|
||||
info: ModerationLogPayloads['updateServerSettings'];
|
||||
} | {
|
||||
type: 'suspend';
|
||||
info: ModerationLogPayloads['suspend'];
|
||||
} | {
|
||||
type: 'unsuspend';
|
||||
info: ModerationLogPayloads['unsuspend'];
|
||||
} | {
|
||||
type: 'updateUserNote';
|
||||
info: ModerationLogPayloads['updateUserNote'];
|
||||
} | {
|
||||
type: 'addCustomEmoji';
|
||||
info: ModerationLogPayloads['addCustomEmoji'];
|
||||
} | {
|
||||
type: 'updateCustomEmoji';
|
||||
info: ModerationLogPayloads['updateCustomEmoji'];
|
||||
} | {
|
||||
type: 'deleteCustomEmoji';
|
||||
info: ModerationLogPayloads['deleteCustomEmoji'];
|
||||
} | {
|
||||
type: 'assignRole';
|
||||
info: ModerationLogPayloads['assignRole'];
|
||||
} | {
|
||||
type: 'unassignRole';
|
||||
info: ModerationLogPayloads['unassignRole'];
|
||||
} | {
|
||||
type: 'createRole';
|
||||
info: ModerationLogPayloads['createRole'];
|
||||
} | {
|
||||
type: 'updateRole';
|
||||
info: ModerationLogPayloads['updateRole'];
|
||||
} | {
|
||||
type: 'deleteRole';
|
||||
info: ModerationLogPayloads['deleteRole'];
|
||||
} | {
|
||||
type: 'clearQueue';
|
||||
info: ModerationLogPayloads['clearQueue'];
|
||||
} | {
|
||||
type: 'promoteQueue';
|
||||
info: ModerationLogPayloads['promoteQueue'];
|
||||
} | {
|
||||
type: 'deleteDriveFile';
|
||||
info: ModerationLogPayloads['deleteDriveFile'];
|
||||
} | {
|
||||
type: 'deleteNote';
|
||||
info: ModerationLogPayloads['deleteNote'];
|
||||
} | {
|
||||
type: 'createGlobalAnnouncement';
|
||||
info: ModerationLogPayloads['createGlobalAnnouncement'];
|
||||
} | {
|
||||
type: 'createUserAnnouncement';
|
||||
info: ModerationLogPayloads['createUserAnnouncement'];
|
||||
} | {
|
||||
type: 'updateGlobalAnnouncement';
|
||||
info: ModerationLogPayloads['updateGlobalAnnouncement'];
|
||||
} | {
|
||||
type: 'updateUserAnnouncement';
|
||||
info: ModerationLogPayloads['updateUserAnnouncement'];
|
||||
} | {
|
||||
type: 'deleteGlobalAnnouncement';
|
||||
info: ModerationLogPayloads['deleteGlobalAnnouncement'];
|
||||
} | {
|
||||
type: 'deleteUserAnnouncement';
|
||||
info: ModerationLogPayloads['deleteUserAnnouncement'];
|
||||
} | {
|
||||
type: 'resetPassword';
|
||||
info: ModerationLogPayloads['resetPassword'];
|
||||
} | {
|
||||
type: 'suspendRemoteInstance';
|
||||
info: ModerationLogPayloads['suspendRemoteInstance'];
|
||||
} | {
|
||||
type: 'unsuspendRemoteInstance';
|
||||
info: ModerationLogPayloads['unsuspendRemoteInstance'];
|
||||
} | {
|
||||
type: 'updateRemoteInstanceNote';
|
||||
info: ModerationLogPayloads['updateRemoteInstanceNote'];
|
||||
} | {
|
||||
type: 'markSensitiveDriveFile';
|
||||
info: ModerationLogPayloads['markSensitiveDriveFile'];
|
||||
} | {
|
||||
type: 'unmarkSensitiveDriveFile';
|
||||
info: ModerationLogPayloads['unmarkSensitiveDriveFile'];
|
||||
} | {
|
||||
type: 'createInvitation';
|
||||
info: ModerationLogPayloads['createInvitation'];
|
||||
} | {
|
||||
type: 'createAd';
|
||||
info: ModerationLogPayloads['createAd'];
|
||||
} | {
|
||||
type: 'updateAd';
|
||||
info: ModerationLogPayloads['updateAd'];
|
||||
} | {
|
||||
type: 'deleteAd';
|
||||
info: ModerationLogPayloads['deleteAd'];
|
||||
} | {
|
||||
type: 'createAvatarDecoration';
|
||||
info: ModerationLogPayloads['createAvatarDecoration'];
|
||||
} | {
|
||||
type: 'updateAvatarDecoration';
|
||||
info: ModerationLogPayloads['updateAvatarDecoration'];
|
||||
} | {
|
||||
type: 'deleteAvatarDecoration';
|
||||
info: ModerationLogPayloads['deleteAvatarDecoration'];
|
||||
} | {
|
||||
type: 'resolveAbuseReport';
|
||||
info: ModerationLogPayloads['resolveAbuseReport'];
|
||||
} | {
|
||||
type: 'forwardAbuseReport';
|
||||
info: ModerationLogPayloads['forwardAbuseReport'];
|
||||
} | {
|
||||
type: 'updateAbuseReportNote';
|
||||
info: ModerationLogPayloads['updateAbuseReportNote'];
|
||||
} | {
|
||||
type: 'unsetMfa';
|
||||
info: ModerationLogPayloads['unsetMfa'];
|
||||
} | {
|
||||
type: 'unsetUserAvatar';
|
||||
info: ModerationLogPayloads['unsetUserAvatar'];
|
||||
} | {
|
||||
type: 'unsetUserBanner';
|
||||
info: ModerationLogPayloads['unsetUserBanner'];
|
||||
} | {
|
||||
type: 'createSystemWebhook';
|
||||
info: ModerationLogPayloads['createSystemWebhook'];
|
||||
} | {
|
||||
type: 'updateSystemWebhook';
|
||||
info: ModerationLogPayloads['updateSystemWebhook'];
|
||||
} | {
|
||||
type: 'deleteSystemWebhook';
|
||||
info: ModerationLogPayloads['deleteSystemWebhook'];
|
||||
} | {
|
||||
type: 'createAbuseReportNotificationRecipient';
|
||||
info: ModerationLogPayloads['createAbuseReportNotificationRecipient'];
|
||||
} | {
|
||||
type: 'updateAbuseReportNotificationRecipient';
|
||||
info: ModerationLogPayloads['updateAbuseReportNotificationRecipient'];
|
||||
} | {
|
||||
type: 'deleteAbuseReportNotificationRecipient';
|
||||
info: ModerationLogPayloads['deleteAbuseReportNotificationRecipient'];
|
||||
} | {
|
||||
type: 'deleteAccount';
|
||||
info: ModerationLogPayloads['deleteAccount'];
|
||||
} | {
|
||||
type: 'deletePage';
|
||||
info: ModerationLogPayloads['deletePage'];
|
||||
} | {
|
||||
type: 'deleteFlash';
|
||||
info: ModerationLogPayloads['deleteFlash'];
|
||||
} | {
|
||||
type: 'deleteGalleryPost';
|
||||
info: ModerationLogPayloads['deleteGalleryPost'];
|
||||
} | {
|
||||
type: 'deleteChatRoom';
|
||||
info: ModerationLogPayloads['deleteChatRoom'];
|
||||
} | {
|
||||
type: 'updateProxyAccountDescription';
|
||||
info: ModerationLogPayloads['updateProxyAccountDescription'];
|
||||
});
|
||||
[K in keyof ModerationLogPayloads]: {
|
||||
type: K;
|
||||
info: ModerationLogPayloads[K];
|
||||
};
|
||||
}[keyof ModerationLogPayloads]);
|
||||
|
||||
export type ServerStats = {
|
||||
cpu: number;
|
||||
|
||||
Reference in New Issue
Block a user