diff --git a/.github/scripts/measure-backend-memory-comparison.mts b/.github/scripts/backend-diagnostics.inspect.mts
similarity index 96%
rename from .github/scripts/measure-backend-memory-comparison.mts
rename to .github/scripts/backend-diagnostics.inspect.mts
index a4e93b3ce4..5bf0f3ee99 100644
--- a/.github/scripts/measure-backend-memory-comparison.mts
+++ b/.github/scripts/backend-diagnostics.inspect.mts
@@ -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,
diff --git a/.github/scripts/backend-diagnostics.render-md.mts b/.github/scripts/backend-diagnostics.render-md.mts
new file mode 100644
index 0000000000..e73c22a722
--- /dev/null
+++ b/.github/scripts/backend-diagnostics.render-md.mts
@@ -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)}
${util.formatDeltaPercent(percent, 0.1).replaceAll('\\%', '\\\\%')}`;
+
+ lines.push(`| **${formatMemoryMetricName(metric)}** | ${formatMemoryMb(baseValue)}
± ${formatMemoryMb(baseSpread)} | ${formatMemoryMb(headValue)}
± ${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`);
diff --git a/.github/scripts/backend-js-footprint-loader.mjs b/.github/scripts/backend-js-footprint-loader.mjs
deleted file mode 100644
index cd2c0af2e6..0000000000
--- a/.github/scripts/backend-js-footprint-loader.mjs
+++ /dev/null
@@ -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;
-}
diff --git a/.github/scripts/backend-js-footprint-require.cjs b/.github/scripts/backend-js-footprint-require.cjs
deleted file mode 100644
index 1adab8bc05..0000000000
--- a/.github/scripts/backend-js-footprint-require.cjs
+++ /dev/null
@@ -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;
-};
diff --git a/.github/scripts/backend-js-footprint.mjs b/.github/scripts/backend-js-footprint.mjs
deleted file mode 100644
index 9c98357ad6..0000000000
--- a/.github/scripts/backend-js-footprint.mjs
+++ /dev/null
@@ -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`);
diff --git a/.github/scripts/backend-memory-report.mts b/.github/scripts/backend-memory-report.mts
deleted file mode 100644
index 4a701655ec..0000000000
--- a/.github/scripts/backend-memory-report.mts
+++ /dev/null
@@ -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)}
${util.formatDeltaPercent(percent, 0.1).replaceAll('\\%', '\\\\%')}`;
-
- lines.push(`| **${formatMetricName(metric)}** | ${formatMemoryMb(baseValue)}
± ${formatMemoryMb(baseSpread)} | ${formatMemoryMb(headValue)}
± ${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',
- '',
- 'Click to show
',
- '',
- 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(' ');
- 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`);
diff --git a/.github/scripts/chrome.mts b/.github/scripts/chrome.mts
index 5d07ac6efe..b40c82a263 100644
--- a/.github/scripts/chrome.mts
+++ b/.github/scripts/chrome.mts
@@ -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,
+ };
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 {
@@ -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 {
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]));
diff --git a/.github/scripts/measure-frontend-browser-comparison.mts b/.github/scripts/frontend-browser-diagnostics.inspect.mts
similarity index 85%
rename from .github/scripts/measure-frontend-browser-comparison.mts
rename to .github/scripts/frontend-browser-diagnostics.inspect.mts
index 0825133b1d..1f0d74875f 100644
--- a/.github/scripts/measure-frontend-browser-comparison.mts
+++ b/.github/scripts/frontend-browser-diagnostics.inspect.mts
@@ -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 | 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();
diff --git a/.github/scripts/frontend-browser-detailed-html.mts b/.github/scripts/frontend-browser-diagnostics.render-additional-html.mts
similarity index 98%
rename from .github/scripts/frontend-browser-detailed-html.mts
rename to .github/scripts/frontend-browser-diagnostics.render-additional-html.mts
index 34240eefc6..568f4257e2 100644
--- a/.github/scripts/frontend-browser-detailed-html.mts
+++ b/.github/scripts/frontend-browser-diagnostics.render-additional-html.mts
@@ -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 ');
- }
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();
}
diff --git a/.github/scripts/frontend-browser-report.mts b/.github/scripts/frontend-browser-diagnostics.render-md.mts
similarity index 90%
rename from .github/scripts/frontend-browser-report.mts
rename to .github/scripts/frontend-browser-diagnostics.render-md.mts
index 755b29bc88..76ff29b911 100644
--- a/.github/scripts/frontend-browser-report.mts
+++ b/.github/scripts/frontend-browser-diagnostics.render-md.mts
@@ -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.`,
- //'',
+ 'Only metrics showing significant changes are displayed.',
+ '',
detailedHtmlUrl == null || detailedHtmlUrl === '' ? null : `[View details](${detailedHtmlUrl})`,
detailedHtmlUrl == null || detailedHtmlUrl === '' ? null : '',
'',
@@ -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})`,
' ',
'',
];
@@ -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 ');
- }
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();
}
diff --git a/.github/scripts/frontend-js-size.mts b/.github/scripts/frontend-bundle-diagnostics.render-md.mts
similarity index 99%
rename from .github/scripts/frontend-js-size.mts
rename to .github/scripts/frontend-bundle-diagnostics.render-md.mts
index db134bab79..1e8cf37f24 100644
--- a/.github/scripts/frontend-js-size.mts
+++ b/.github/scripts/frontend-bundle-diagnostics.render-md.mts
@@ -7,9 +7,9 @@ import { promises as fs } from 'node:fs';
import path from 'node:path';
import * as util from './utility.mts';
-const marker = '';
+const marker = '';
-const locale = process.env.FRONTEND_JS_SIZE_LOCALE ?? 'ja-JP';
+const locale = 'ja-JP';
//function sharePercent(value, total) {
// if (total === 0) return '0%';
diff --git a/.github/scripts/frontend-js-size.test.mts b/.github/scripts/frontend-js-size.test.mts
deleted file mode 100644
index d3b3db9af6..0000000000
--- a/.github/scripts/frontend-js-size.test.mts
+++ /dev/null
@@ -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;
-
-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) {
- 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 }, after: { manifest: Manifest; sizes: Record }, 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((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, /`(?:dist|esm)`<\/summary>/);
- assert.match(report, /`src\/_boot_\.ts`<\/summary>/);
- assert.match(report, /`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, /Chunk size diff \(2 updated, 1 added, 1 removed\)<\/summary>/);
- assert.match(report, /Startup chunk size \(2 updated, 1 added, 1 removed\)<\/summary>/);
- assert.equal(report.match(/`src\/_boot_\.ts`<\/summary>/g)?.length, 2);
- assert.doesNotMatch(report, /`(?:vue|i18n|src\/added-small\.ts|src\/removed-small\.ts)`<\/summary>/);
- assert.match(report, /\| \(total\) \| 225 B \| 276 B \|[^\n]*\n\| \| \| \| \| \|\n\| `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\| `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, /`src\/added\.ts`<\/summary> `ja-JP\/added-after\.js` <\/details> \| 0 B \| 13 B \|/);
- assert.match(report, /`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, /Chunk size diff \(0 updated, 0 added, 0 removed\)<\/summary>/);
- assert.match(report, /Startup chunk size \(0 updated, 0 added, 0 removed\)<\/summary>/);
- assert.equal(report.match(/\| \(other generated chunks\) \| 30 B \| 70 B \|/g)?.length, 2);
-});
diff --git a/.github/workflows/report-backend-memory.yml b/.github/workflows/backend-diagnostics.comment.yml
similarity index 89%
rename from .github/workflows/report-backend-memory.yml
rename to .github/workflows/backend-diagnostics.comment.yml
index a6c0b4afdf..a19166a319 100644
--- a/.github/workflows/report-backend-memory.yml
+++ b/.github/workflows/backend-diagnostics.comment.yml
@@ -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 }}
diff --git a/.github/workflows/get-backend-memory.yml b/.github/workflows/backend-diagnostics.inspect.yml
similarity index 78%
rename from .github/workflows/get-backend-memory.yml
rename to .github/workflows/backend-diagnostics.inspect.yml
index 1f933444bd..d5a47b804f 100644
--- a/.github/workflows/get-backend-memory.yml
+++ b/.github/workflows/backend-diagnostics.inspect.yml
@@ -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
diff --git a/.github/workflows/frontend-browser-metrics-report-comment.yml b/.github/workflows/frontend-browser-diagnostics.comment.yml
similarity index 62%
rename from .github/workflows/frontend-browser-metrics-report-comment.yml
rename to .github/workflows/frontend-browser-diagnostics.comment.yml
index 0312092a5b..5098e618ad 100644
--- a/.github/workflows/frontend-browser-metrics-report-comment.yml
+++ b/.github/workflows/frontend-browser-diagnostics.comment.yml
@@ -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
diff --git a/.github/workflows/frontend-browser-metrics-report.yml b/.github/workflows/frontend-browser-diagnostics.inspect.yml
similarity index 65%
rename from .github/workflows/frontend-browser-metrics-report.yml
rename to .github/workflows/frontend-browser-diagnostics.inspect.yml
index 863c7bb155..92f645ab26 100644
--- a/.github/workflows/frontend-browser-metrics-report.yml
+++ b/.github/workflows/frontend-browser-diagnostics.inspect.yml
@@ -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
diff --git a/.github/workflows/frontend-bundle-diagnostics.comment.yml b/.github/workflows/frontend-bundle-diagnostics.comment.yml
new file mode 100644
index 0000000000..709c735e69
--- /dev/null
+++ b/.github/workflows/frontend-bundle-diagnostics.comment.yml
@@ -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
diff --git a/.github/workflows/frontend-bundle-report.yml b/.github/workflows/frontend-bundle-diagnostics.inspect.yml
similarity index 69%
rename from .github/workflows/frontend-bundle-report.yml
rename to .github/workflows/frontend-bundle-diagnostics.inspect.yml
index b5af3bb0e1..6aa4f54fb4 100644
--- a/.github/workflows/frontend-bundle-report.yml
+++ b/.github/workflows/frontend-bundle-diagnostics.inspect.yml
@@ -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
diff --git a/.github/workflows/frontend-bundle-report-comment.yml b/.github/workflows/frontend-bundle-report-comment.yml
deleted file mode 100644
index b93f8a320f..0000000000
--- a/.github/workflows/frontend-bundle-report-comment.yml
+++ /dev/null
@@ -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 = '';
- const visualizerMarker = '';
- 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,
- });
- }
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 63f8684c33..0e77f896a2 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -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
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index a2effe339a..634ee1f02c 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -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
diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts
index 77cec5fe43..4fabe4f096 100644
--- a/packages/misskey-js/src/entities.ts
+++ b/packages/misskey-js/src/entities.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;