From 875554003e7c4b039ce6ed1a58186e4a1089bb6e Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 16 Jul 2026 19:34:48 +0900 Subject: [PATCH 01/18] feat(dev): report frontend browser diagnostics --- .github/scripts/chrome.mts | 69 +++++++++++++ .github/scripts/frontend-browser-report.mts | 14 ++- .../scripts/frontend-browser-report.test.mts | 98 +++++++++++++++++++ .../measure-frontend-browser-comparison.mts | 4 +- .github/workflows/lint.yml | 1 + 5 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 .github/scripts/frontend-browser-report.test.mts diff --git a/.github/scripts/chrome.mts b/.github/scripts/chrome.mts index 5d07ac6efe..2be10bd261 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,65 @@ export type TabMemory = { totalBytes: number; }; +export type BrowserConsoleMetrics = { + log: number; + warn: number; + error: number; + info: number; +}; + +export type BrowserDiagnostics = { + pageErrorCount: number; + console: BrowserConsoleMetrics; +}; + +export function createBrowserDiagnostics(): BrowserDiagnostics { + return { + pageErrorCount: 0, + console: { + log: 0, + warn: 0, + error: 0, + info: 0, + }, + }; +} + +export function recordPageError(diagnostics: BrowserDiagnostics) { + diagnostics.pageErrorCount += 1; +} + +function isBrowserConsoleMetricKey(value: string): value is keyof BrowserConsoleMetrics { + return value === 'log' || value === 'warn' || value === 'error' || value === 'info'; +} + +export function recordConsoleMessageType(diagnostics: BrowserDiagnostics, messageType: string) { + const metricKey = messageType === 'warning' ? 'warn' : messageType; + if (!isBrowserConsoleMetricKey(metricKey)) return; + diagnostics.console[metricKey] += 1; +} + +export function summarizeBrowserDiagnostics(samples: BrowserDiagnostics[]): BrowserDiagnostics { + if (samples.length === 0) throw new Error('No browser diagnostic samples'); + const median = (select: (sample: BrowserDiagnostics) => number) => util.median(samples.map(select)); + + return { + pageErrorCount: median(sample => sample.pageErrorCount), + console: { + log: median(sample => sample.console.log), + warn: median(sample => sample.console.warn), + 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 +204,7 @@ type PlaywrightBrowserOptions = { export class HeadlessChromeController { public networkRequests: NetworkRequest[] = []; public webSocketConnections: WebSocketConnection[] = []; + private readonly diagnostics = createBrowserDiagnostics(); private readonly browser: Browser; private readonly context: BrowserContext; public readonly page: Page; @@ -168,6 +224,12 @@ export class HeadlessChromeController { this.cdp = cdp; this.page.setDefaultTimeout(options.scenarioTimeoutMs); this.page.setDefaultNavigationTimeout(options.scenarioTimeoutMs); + this.page.on('pageerror', () => { + recordPageError(this.diagnostics); + }); + this.page.on('console', message => { + recordConsoleMessageType(this.diagnostics, message.type()); + }); } static async create(label: string, options: PlaywrightBrowserOptions): Promise { @@ -376,6 +438,13 @@ export class HeadlessChromeController { ]) as T; } + public collectDiagnostics(): BrowserDiagnostics { + return { + pageErrorCount: this.diagnostics.pageErrorCount, + console: { ...this.diagnostics.console }, + }; + } + 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/frontend-browser-report.mts b/.github/scripts/frontend-browser-report.mts index 755b29bc88..829041f1d0 100644 --- a/.github/scripts/frontend-browser-report.mts +++ b/.github/scripts/frontend-browser-report.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; @@ -171,8 +172,19 @@ function getMetric(report: BrowserMeasurement, key: string) { return report.performance.cdpMetrics[key]; } +export function renderBrowserDiagnosticsRows(base: BrowserMetricsReport, head: BrowserMetricsReport, all = false) { + return [ + 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.warn, sample => sample.diagnostics.console.warn, 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), + ].filter(row => row != null); +} + function renderSummaryTable(base: BrowserMetricsReport, head: BrowserMetricsReport, all = false) { const rows = [ + ...renderBrowserDiagnosticsRows(base, head, all), //metricRow('Scenario duration', base, head, summary => summary.durationMs, sample => sample.durationMs, formatMs), metricRow('Requests', base, head, summary => summary.network.requestCount, sample => sample.network.requestCount, util.formatNumber, 1, !all), //metricRow('Failed requests', base, head, summary => summary.network.failedRequestCount, sample => sample.network.failedRequestCount, util.formatNumber), diff --git a/.github/scripts/frontend-browser-report.test.mts b/.github/scripts/frontend-browser-report.test.mts new file mode 100644 index 0000000000..4d4f130504 --- /dev/null +++ b/.github/scripts/frontend-browser-report.test.mts @@ -0,0 +1,98 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + createBrowserDiagnostics, + recordConsoleMessageType, + recordPageError, + summarizeBrowserDiagnostics, + type BrowserDiagnostics, +} from './chrome.mts'; +import { + renderBrowserDiagnosticsRows, + type BrowserMeasurement, + type BrowserMeasurementSample, + type BrowserMetricsReport, +} from './frontend-browser-report.mts'; + +function diagnostics(pageErrorCount: number, log: number, warn: number, error: number, info: number): BrowserDiagnostics { + return { + pageErrorCount, + console: { log, warn, error, info }, + }; +} + +function browserReport(label: 'base' | 'head', values: BrowserDiagnostics[]): BrowserMetricsReport { + const samples = values.map((value, index) => ({ + round: index + 1, + diagnostics: value, + } as BrowserMeasurementSample)); + + return { + label, + timestamp: '2026-07-16T00:00:00.000Z', + url: 'http://127.0.0.1:61812', + scenario: 'test', + sampleCount: samples.length, + aggregation: 'median', + summary: { + diagnostics: summarizeBrowserDiagnostics(values), + } as BrowserMeasurement, + samples, + }; +} + +test('records page errors and only the requested console message types', () => { + const result = createBrowserDiagnostics(); + recordPageError(result); + recordPageError(result); + for (const type of ['log', 'warning', 'error', 'info', 'debug', 'trace']) { + recordConsoleMessageType(result, type); + } + + assert.deepEqual(result, diagnostics(2, 1, 1, 1, 1)); +}); + +test('summarizes browser diagnostics with a median for every counter', () => { + const result = summarizeBrowserDiagnostics([ + diagnostics(0, 10, 20, 30, 40), + diagnostics(4, 14, 24, 34, 44), + diagnostics(2, 12, 22, 32, 42), + diagnostics(3, 13, 23, 33, 43), + diagnostics(1, 11, 21, 31, 41), + ]); + + assert.deepEqual(result, diagnostics(2, 12, 22, 32, 42)); +}); + +test('renders each changed browser diagnostics metric as a separate row', () => { + const base = browserReport('base', Array.from({ length: 5 }, () => diagnostics(0, 0, 0, 0, 0))); + const head = browserReport('head', Array.from({ length: 5 }, () => diagnostics(1, 1, 1, 1, 1))); + const rows = renderBrowserDiagnosticsRows(base, head).join('\n'); + + for (const label of ['Page errors', 'Console log', 'Console warnings', 'Console errors', 'Console info']) { + assert.match(rows, new RegExp(`\\*\\*${label}\\*\\*`)); + } +}); + +test('renders browser diagnostics deltas from paired rounds instead of summary medians', () => { + const base = browserReport('base', [0, 9, 10, 100, 101].map(value => diagnostics(value, 0, 0, 0, 0))); + const head = browserReport('head', [10, 100, 101, 0, 9].map(value => diagnostics(value, 0, 0, 0, 0))); + const rows = renderBrowserDiagnosticsRows(base, head); + + assert.equal(rows.length, 1); + assert.match(rows[0], /\*\*Page errors\*\*/); + assert.match(rows[0], /\\text\{\+10\}/); +}); + +test('omits browser diagnostics rows whose paired median does not change', () => { + const values = Array.from({ length: 5 }, () => diagnostics(2, 3, 4, 5, 6)); + const base = browserReport('base', values); + const head = browserReport('head', values); + + assert.deepEqual(renderBrowserDiagnosticsRows(base, head), []); +}); diff --git a/.github/scripts/measure-frontend-browser-comparison.mts b/.github/scripts/measure-frontend-browser-comparison.mts index 0825133b1d..3e357c3396 100644 --- a/.github/scripts/measure-frontend-browser-comparison.mts +++ b/.github/scripts/measure-frontend-browser-comparison.mts @@ -7,7 +7,7 @@ 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'; @@ -173,6 +173,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 +211,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, diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 63f8684c33..c1a3aeda6e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -152,3 +152,4 @@ jobs: 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 + - run: node --test .github/scripts/frontend-browser-report.test.mts From 93bbc89c7adfba9d580c433dd07b19ab0235838b Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 16 Jul 2026 19:58:24 +0900 Subject: [PATCH 02/18] chore(dev): tweak frontend browser diagnostics --- .github/scripts/chrome.mts | 58 ++++------- .github/scripts/frontend-browser-report.mts | 18 ++-- .../scripts/frontend-browser-report.test.mts | 98 ------------------- .github/workflows/lint.yml | 1 - 4 files changed, 24 insertions(+), 151 deletions(-) delete mode 100644 .github/scripts/frontend-browser-report.test.mts diff --git a/.github/scripts/chrome.mts b/.github/scripts/chrome.mts index 2be10bd261..b40c82a263 100644 --- a/.github/scripts/chrome.mts +++ b/.github/scripts/chrome.mts @@ -90,53 +90,19 @@ export type TabMemory = { totalBytes: number; }; -export type BrowserConsoleMetrics = { - log: number; - warn: number; - error: number; - info: number; -}; - export type BrowserDiagnostics = { pageErrorCount: number; - console: BrowserConsoleMetrics; + console: Record<'log' | 'warning' | 'error' | 'info', number>; }; -export function createBrowserDiagnostics(): BrowserDiagnostics { - return { - pageErrorCount: 0, - console: { - log: 0, - warn: 0, - error: 0, - info: 0, - }, - }; -} - -export function recordPageError(diagnostics: BrowserDiagnostics) { - diagnostics.pageErrorCount += 1; -} - -function isBrowserConsoleMetricKey(value: string): value is keyof BrowserConsoleMetrics { - return value === 'log' || value === 'warn' || value === 'error' || value === 'info'; -} - -export function recordConsoleMessageType(diagnostics: BrowserDiagnostics, messageType: string) { - const metricKey = messageType === 'warning' ? 'warn' : messageType; - if (!isBrowserConsoleMetricKey(metricKey)) return; - diagnostics.console[metricKey] += 1; -} - export function summarizeBrowserDiagnostics(samples: BrowserDiagnostics[]): BrowserDiagnostics { - if (samples.length === 0) throw new Error('No browser diagnostic samples'); const median = (select: (sample: BrowserDiagnostics) => number) => util.median(samples.map(select)); return { pageErrorCount: median(sample => sample.pageErrorCount), console: { log: median(sample => sample.console.log), - warn: median(sample => sample.console.warn), + warning: median(sample => sample.console.warning), error: median(sample => sample.console.error), info: median(sample => sample.console.info), }, @@ -204,7 +170,10 @@ type PlaywrightBrowserOptions = { export class HeadlessChromeController { public networkRequests: NetworkRequest[] = []; public webSocketConnections: WebSocketConnection[] = []; - private readonly diagnostics = createBrowserDiagnostics(); + private readonly diagnostics = { + pageErrorCount: 0, + console: {} as Record, + }; private readonly browser: Browser; private readonly context: BrowserContext; public readonly page: Page; @@ -225,10 +194,14 @@ export class HeadlessChromeController { this.page.setDefaultTimeout(options.scenarioTimeoutMs); this.page.setDefaultNavigationTimeout(options.scenarioTimeoutMs); this.page.on('pageerror', () => { - recordPageError(this.diagnostics); + this.diagnostics.pageErrorCount++; }); this.page.on('console', message => { - recordConsoleMessageType(this.diagnostics, message.type()); + if (this.diagnostics.console[message.type()] == null) { + this.diagnostics.console[message.type()] = 1; + } else { + this.diagnostics.console[message.type()]++; + } }); } @@ -441,7 +414,12 @@ export class HeadlessChromeController { public collectDiagnostics(): BrowserDiagnostics { return { pageErrorCount: this.diagnostics.pageErrorCount, - console: { ...this.diagnostics.console }, + 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, + }, }; } diff --git a/.github/scripts/frontend-browser-report.mts b/.github/scripts/frontend-browser-report.mts index 829041f1d0..cfb37ccf3b 100644 --- a/.github/scripts/frontend-browser-report.mts +++ b/.github/scripts/frontend-browser-report.mts @@ -172,19 +172,8 @@ function getMetric(report: BrowserMeasurement, key: string) { return report.performance.cdpMetrics[key]; } -export function renderBrowserDiagnosticsRows(base: BrowserMetricsReport, head: BrowserMetricsReport, all = false) { - return [ - 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.warn, sample => sample.diagnostics.console.warn, 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), - ].filter(row => row != null); -} - function renderSummaryTable(base: BrowserMetricsReport, head: BrowserMetricsReport, all = false) { const rows = [ - ...renderBrowserDiagnosticsRows(base, head, all), //metricRow('Scenario duration', base, head, summary => summary.durationMs, sample => sample.durationMs, formatMs), metricRow('Requests', base, head, summary => summary.network.requestCount, sample => sample.network.requestCount, util.formatNumber, 1, !all), //metricRow('Failed requests', base, head, summary => summary.network.failedRequestCount, sample => sample.network.failedRequestCount, util.formatNumber), @@ -215,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.warn, sample => sample.diagnostics.console.warn, 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 [ diff --git a/.github/scripts/frontend-browser-report.test.mts b/.github/scripts/frontend-browser-report.test.mts deleted file mode 100644 index 4d4f130504..0000000000 --- a/.github/scripts/frontend-browser-report.test.mts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import assert from 'node:assert/strict'; -import test from 'node:test'; -import { - createBrowserDiagnostics, - recordConsoleMessageType, - recordPageError, - summarizeBrowserDiagnostics, - type BrowserDiagnostics, -} from './chrome.mts'; -import { - renderBrowserDiagnosticsRows, - type BrowserMeasurement, - type BrowserMeasurementSample, - type BrowserMetricsReport, -} from './frontend-browser-report.mts'; - -function diagnostics(pageErrorCount: number, log: number, warn: number, error: number, info: number): BrowserDiagnostics { - return { - pageErrorCount, - console: { log, warn, error, info }, - }; -} - -function browserReport(label: 'base' | 'head', values: BrowserDiagnostics[]): BrowserMetricsReport { - const samples = values.map((value, index) => ({ - round: index + 1, - diagnostics: value, - } as BrowserMeasurementSample)); - - return { - label, - timestamp: '2026-07-16T00:00:00.000Z', - url: 'http://127.0.0.1:61812', - scenario: 'test', - sampleCount: samples.length, - aggregation: 'median', - summary: { - diagnostics: summarizeBrowserDiagnostics(values), - } as BrowserMeasurement, - samples, - }; -} - -test('records page errors and only the requested console message types', () => { - const result = createBrowserDiagnostics(); - recordPageError(result); - recordPageError(result); - for (const type of ['log', 'warning', 'error', 'info', 'debug', 'trace']) { - recordConsoleMessageType(result, type); - } - - assert.deepEqual(result, diagnostics(2, 1, 1, 1, 1)); -}); - -test('summarizes browser diagnostics with a median for every counter', () => { - const result = summarizeBrowserDiagnostics([ - diagnostics(0, 10, 20, 30, 40), - diagnostics(4, 14, 24, 34, 44), - diagnostics(2, 12, 22, 32, 42), - diagnostics(3, 13, 23, 33, 43), - diagnostics(1, 11, 21, 31, 41), - ]); - - assert.deepEqual(result, diagnostics(2, 12, 22, 32, 42)); -}); - -test('renders each changed browser diagnostics metric as a separate row', () => { - const base = browserReport('base', Array.from({ length: 5 }, () => diagnostics(0, 0, 0, 0, 0))); - const head = browserReport('head', Array.from({ length: 5 }, () => diagnostics(1, 1, 1, 1, 1))); - const rows = renderBrowserDiagnosticsRows(base, head).join('\n'); - - for (const label of ['Page errors', 'Console log', 'Console warnings', 'Console errors', 'Console info']) { - assert.match(rows, new RegExp(`\\*\\*${label}\\*\\*`)); - } -}); - -test('renders browser diagnostics deltas from paired rounds instead of summary medians', () => { - const base = browserReport('base', [0, 9, 10, 100, 101].map(value => diagnostics(value, 0, 0, 0, 0))); - const head = browserReport('head', [10, 100, 101, 0, 9].map(value => diagnostics(value, 0, 0, 0, 0))); - const rows = renderBrowserDiagnosticsRows(base, head); - - assert.equal(rows.length, 1); - assert.match(rows[0], /\*\*Page errors\*\*/); - assert.match(rows[0], /\\text\{\+10\}/); -}); - -test('omits browser diagnostics rows whose paired median does not change', () => { - const values = Array.from({ length: 5 }, () => diagnostics(2, 3, 4, 5, 6)); - const base = browserReport('base', values); - const head = browserReport('head', values); - - assert.deepEqual(renderBrowserDiagnosticsRows(base, head), []); -}); diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c1a3aeda6e..63f8684c33 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -152,4 +152,3 @@ jobs: 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 - - run: node --test .github/scripts/frontend-browser-report.test.mts From 12be1fe5b5002d195e4817c888ee052cbc5a1ea7 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:23:01 +0900 Subject: [PATCH 03/18] [skip ci] chore(dev): tweak backend-memory-report --- .github/scripts/backend-memory-report.mts | 277 ++-------------------- 1 file changed, 16 insertions(+), 261 deletions(-) diff --git a/.github/scripts/backend-memory-report.mts b/.github/scripts/backend-memory-report.mts index 4a701655ec..85630f87b2 100644 --- a/.github/scripts/backend-memory-report.mts +++ b/.github/scripts/backend-memory-report.mts @@ -8,34 +8,7 @@ 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 [baseFile, headFile, outputFile] = process.argv.slice(2); const memoryReportPhases = [ { @@ -44,7 +17,7 @@ const memoryReportPhases = [ }, ] as const; -const metrics = [ +const memoryMetrics = [ 'HeapUsed', 'Pss', 'USS', @@ -56,26 +29,26 @@ function formatMemoryMb(valueKiB: number | null | undefined) { return `${util.formatNumber(valueKiB / 1000)} MB`; } -function formatMetricName(metric: typeof metrics[number]) { +function formatMemoryMetricName(metric: typeof memoryMetrics[number]) { return metric === 'Pss' ? 'PSS' : metric; } -function getMemoryValueFromSample(sample: MemoryReport['samples'][number], phase: typeof memoryReportPhases[number]['key'], metric: typeof metrics[number]) { +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 metrics[number]) { +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 metrics[number]) { +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 metrics[number]) { +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; @@ -93,7 +66,7 @@ function renderMainTableForPhase(base: MemoryReport, head: MemoryReport, phase: return util.formatColoredDelta(deltaKiB, v => formatMemoryMb(v), 100); // 0.1 MB threshold } - for (const metric of metrics) { + for (const metric of memoryMetrics) { const baseValue = getMemoryValue(base, phase, metric); const headValue = getMemoryValue(head, phase, metric); @@ -103,29 +76,12 @@ function renderMainTableForPhase(base: MemoryReport, head: MemoryReport, phase: 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)} |`); + 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 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!, @@ -165,204 +121,11 @@ function renderHeapSnapshotSection(base: MemoryReport, head: MemoryReport) { 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', + '## ⚙️ Backend Diagnostics Report', '', ]; @@ -373,7 +136,7 @@ const lines = [ //} for (const phase of memoryReportPhases) { - lines.push(`### ${phase.title}`); + lines.push(`### Memory: ${phase.title}`); lines.push(renderMainTableForPhase(base, head, phase.key)); lines.push(''); } @@ -386,22 +149,16 @@ if (heapSnapshotSection != null) { 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(`You can download the representative 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]) { +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 metrics[number]) { +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); @@ -419,10 +176,8 @@ function isBeyondSampleNoise(base: MemoryReport, head: MemoryReport, phase: type 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(`⚠️ **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(''); } -//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`); From 1a04d4c2d91b09324938ea9956d6d26353179e7c Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:36:55 +0900 Subject: [PATCH 04/18] refacotr(gh): rename some files --- ...ry-report.mts => backend-diagnostics.render-md.mts} | 0 .github/scripts/frontend-browser-report.mts | 2 +- ...kend-memory.yml => backend-diagnostics.inspect.yml} | 10 +++++----- ...ckend-memory.yml => backend-diagnostics.report.yml} | 6 +++--- 4 files changed, 9 insertions(+), 9 deletions(-) rename .github/scripts/{backend-memory-report.mts => backend-diagnostics.render-md.mts} (100%) rename .github/workflows/{get-backend-memory.yml => backend-diagnostics.inspect.yml} (93%) rename .github/workflows/{report-backend-memory.yml => backend-diagnostics.report.yml} (90%) diff --git a/.github/scripts/backend-memory-report.mts b/.github/scripts/backend-diagnostics.render-md.mts similarity index 100% rename from .github/scripts/backend-memory-report.mts rename to .github/scripts/backend-diagnostics.render-md.mts diff --git a/.github/scripts/frontend-browser-report.mts b/.github/scripts/frontend-browser-report.mts index cfb37ccf3b..1ecfbe14db 100644 --- a/.github/scripts/frontend-browser-report.mts +++ b/.github/scripts/frontend-browser-report.mts @@ -206,7 +206,7 @@ function renderSummaryTable(base: BrowserMetricsReport, head: BrowserMetricsRepo metricRow('WebSocket received', base, head, summary => summary.network.webSocketReceivedBytes, sample => sample.network.webSocketReceivedBytes, 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.warn, sample => sample.diagnostics.console.warn, 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), diff --git a/.github/workflows/get-backend-memory.yml b/.github/workflows/backend-diagnostics.inspect.yml similarity index 93% rename from .github/workflows/get-backend-memory.yml rename to .github/workflows/backend-diagnostics.inspect.yml index 1f933444bd..65412bafa8 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.report.yml so be careful when change name +name: Backend diagnostics (inspect) on: pull_request: @@ -10,14 +10,14 @@ on: - packages/backend/** - packages/misskey-js/** - .github/scripts/utility.mts - - .github/scripts/backend-memory-report.mts + - .github/scripts/backend-diagnostics.render-md.mts - .github/scripts/measure-backend-memory-comparison.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.report.yml jobs: get-memory-usage: diff --git a/.github/workflows/report-backend-memory.yml b/.github/workflows/backend-diagnostics.report.yml similarity index 90% rename from .github/workflows/report-backend-memory.yml rename to .github/workflows/backend-diagnostics.report.yml index a6c0b4afdf..f9fded83d7 100644 --- a/.github/workflows/report-backend-memory.yml +++ b/.github/workflows/backend-diagnostics.report.yml @@ -1,10 +1,10 @@ -name: Report backend memory +name: Backend diagnostics (report) on: workflow_run: types: [completed] workflows: - - Get backend memory usage # get-backend-memory.yml + - Backend diagnostics (inspect) # backend-diagnostics.inspect.yml jobs: compare-memory: @@ -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 ./artifacts/js-footprint-base.json ./artifacts/js-footprint-head.json - uses: thollander/actions-comment-pull-request@v3 with: pr-number: ${{ steps.load-pr-num.outputs.pr-number }} From 77c2f54fa54ab0e4a76fc307b1fc00552664c262 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 16 Jul 2026 20:42:58 +0900 Subject: [PATCH 05/18] refacotr(gh): refactor --- ...on.mts => backend-diagnostics.inspect.mts} | 0 .../scripts/backend-diagnostics.render-md.mts | 2 +- .../scripts/backend-js-footprint-loader.mjs | 46 -- .../scripts/backend-js-footprint-require.cjs | 46 -- .github/scripts/backend-js-footprint.mjs | 473 ------------------ .../workflows/backend-diagnostics.inspect.yml | 13 +- .../workflows/backend-diagnostics.report.yml | 2 +- 7 files changed, 4 insertions(+), 578 deletions(-) rename .github/scripts/{measure-backend-memory-comparison.mts => backend-diagnostics.inspect.mts} (100%) delete mode 100644 .github/scripts/backend-js-footprint-loader.mjs delete mode 100644 .github/scripts/backend-js-footprint-require.cjs delete mode 100644 .github/scripts/backend-js-footprint.mjs diff --git a/.github/scripts/measure-backend-memory-comparison.mts b/.github/scripts/backend-diagnostics.inspect.mts similarity index 100% rename from .github/scripts/measure-backend-memory-comparison.mts rename to .github/scripts/backend-diagnostics.inspect.mts diff --git a/.github/scripts/backend-diagnostics.render-md.mts b/.github/scripts/backend-diagnostics.render-md.mts index 85630f87b2..e73c22a722 100644 --- a/.github/scripts/backend-diagnostics.render-md.mts +++ b/.github/scripts/backend-diagnostics.render-md.mts @@ -6,7 +6,7 @@ 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'; +import type { MemoryReport } from './backend-diagnostics.inspect.mts'; const [baseFile, headFile, outputFile] = process.argv.slice(2); 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/workflows/backend-diagnostics.inspect.yml b/.github/workflows/backend-diagnostics.inspect.yml index 65412bafa8..1474c78f0c 100644 --- a/.github/workflows/backend-diagnostics.inspect.yml +++ b/.github/workflows/backend-diagnostics.inspect.yml @@ -11,11 +11,8 @@ on: - packages/misskey-js/** - .github/scripts/utility.mts - .github/scripts/backend-diagnostics.render-md.mts - - .github/scripts/measure-backend-memory-comparison.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/backend-diagnostics.inspect.yml - .github/workflows/backend-diagnostics.report.yml @@ -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/backend-diagnostics.report.yml b/.github/workflows/backend-diagnostics.report.yml index f9fded83d7..f358216a62 100644 --- a/.github/workflows/backend-diagnostics.report.yml +++ b/.github/workflows/backend-diagnostics.report.yml @@ -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-diagnostics.render-md.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 }} From ab92f984729d5a0dbb4f641939e6d4d36ef07ba9 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:02:45 +0900 Subject: [PATCH 06/18] refacotr(gh): rename some files --- ... frontend-browser-diagnostics.inspect.mts} | 0 ...er-diagnostics.render-additional-html.mts} | 5 +- ...rontend-browser-diagnostics.render-md.mts} | 9 ++-- ... frontend-browser-diagnostics.inspect.yml} | 52 +++++++++---------- ...> frontend-browser-diagnostics.report.yml} | 18 +++---- 5 files changed, 39 insertions(+), 45 deletions(-) rename .github/scripts/{measure-frontend-browser-comparison.mts => frontend-browser-diagnostics.inspect.mts} (100%) rename .github/scripts/{frontend-browser-detailed-html.mts => frontend-browser-diagnostics.render-additional-html.mts} (98%) rename .github/scripts/{frontend-browser-report.mts => frontend-browser-diagnostics.render-md.mts} (98%) rename .github/workflows/{frontend-browser-metrics-report.yml => frontend-browser-diagnostics.inspect.yml} (69%) rename .github/workflows/{frontend-browser-metrics-report-comment.yml => frontend-browser-diagnostics.report.yml} (62%) diff --git a/.github/scripts/measure-frontend-browser-comparison.mts b/.github/scripts/frontend-browser-diagnostics.inspect.mts similarity index 100% rename from .github/scripts/measure-frontend-browser-comparison.mts rename to .github/scripts/frontend-browser-diagnostics.inspect.mts 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..86f70e7ebd 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,9 +434,6 @@ 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; diff --git a/.github/scripts/frontend-browser-report.mts b/.github/scripts/frontend-browser-diagnostics.render-md.mts similarity index 98% rename from .github/scripts/frontend-browser-report.mts rename to .github/scripts/frontend-browser-diagnostics.render-md.mts index 1ecfbe14db..b67a790102 100644 --- a/.github/scripts/frontend-browser-report.mts +++ b/.github/scripts/frontend-browser-diagnostics.render-md.mts @@ -324,12 +324,12 @@ export function renderFrontendBrowserReport(base: BrowserMetricsReport, head: Br : `${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), '', + 'Only metrics showing significant changes are displayed.', + '', //`> 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.`, //'', detailedHtmlUrl == null || detailedHtmlUrl === '' ? null : `[View details](${detailedHtmlUrl})`, @@ -367,9 +367,6 @@ 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; diff --git a/.github/workflows/frontend-browser-metrics-report.yml b/.github/workflows/frontend-browser-diagnostics.inspect.yml similarity index 69% rename from .github/workflows/frontend-browser-metrics-report.yml rename to .github/workflows/frontend-browser-diagnostics.inspect.yml index 863c7bb155..444c54040b 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.report.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: @@ -125,33 +125,33 @@ 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" "$REPORT_DIR/head-heap-snapshot.heapsnapshot" - 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: ${{ runner.temp }}/frontend-browser-diagnostics/head-heap-snapshot.heapsnapshot 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 @@ -165,10 +165,10 @@ jobs: 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 +177,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-browser-metrics-report-comment.yml b/.github/workflows/frontend-browser-diagnostics.report.yml similarity index 62% rename from .github/workflows/frontend-browser-metrics-report-comment.yml rename to .github/workflows/frontend-browser-diagnostics.report.yml index 0312092a5b..7edc9f36c6 100644 --- a/.github/workflows/frontend-browser-metrics-report-comment.yml +++ b/.github/workflows/frontend-browser-diagnostics.report.yml @@ -1,9 +1,9 @@ -name: frontend-browser-metrics-report-comment +name: Frontend browser diagnostics (report) 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 From 64f98faca93e45ddbe137ca366b623d6a802b6e1 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:11:41 +0900 Subject: [PATCH 07/18] refacotr(gh): refactor --- .github/scripts/backend-diagnostics.inspect.mts | 6 +++--- .github/scripts/frontend-browser-diagnostics.inspect.mts | 6 +++--- .../scripts/frontend-browser-diagnostics.render-md.mts | 9 ++------- 3 files changed, 8 insertions(+), 13 deletions(-) diff --git a/.github/scripts/backend-diagnostics.inspect.mts b/.github/scripts/backend-diagnostics.inspect.mts index a4e93b3ce4..5bf0f3ee99 100644 --- a/.github/scripts/backend-diagnostics.inspect.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/frontend-browser-diagnostics.inspect.mts b/.github/scripts/frontend-browser-diagnostics.inspect.mts index 3e357c3396..545149df24 100644 --- a/.github/scripts/frontend-browser-diagnostics.inspect.mts +++ b/.github/scripts/frontend-browser-diagnostics.inspect.mts @@ -234,7 +234,7 @@ async function saveRepresentativeHeadHeapSnapshot(report: BrowserMetricsReport, await rm(headHeapSnapshotWorkDir, { 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, heapSnapshotSavePath?: string) { let server: ReturnType | null = null; try { @@ -269,8 +269,8 @@ async function measureRepo(label: 'base' | 'head', repoDir: string, outputPath: } 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), resolve(headHeapSnapshotOutputArg)); } await main(); diff --git a/.github/scripts/frontend-browser-diagnostics.render-md.mts b/.github/scripts/frontend-browser-diagnostics.render-md.mts index b67a790102..c18ef63f46 100644 --- a/.github/scripts/frontend-browser-diagnostics.render-md.mts +++ b/.github/scripts/frontend-browser-diagnostics.render-md.mts @@ -313,15 +313,12 @@ function toHeapSnapshotReport(report: BrowserMetricsReport): HeapSnapshotReport }; } -export function renderFrontendBrowserReport(base: BrowserMetricsReport, head: BrowserMetricsReport, options: { +function renderMd(base: BrowserMetricsReport, head: BrowserMetricsReport, options: { 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 Diagnostics Report', @@ -330,8 +327,6 @@ export function renderFrontendBrowserReport(base: BrowserMetricsReport, head: Br '', 'Only metrics showing significant changes are displayed.', '', - //`> 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.`, - //'', detailedHtmlUrl == null || detailedHtmlUrl === '' ? null : `[View details](${detailedHtmlUrl})`, detailedHtmlUrl == null || detailedHtmlUrl === '' ? null : '', '
', @@ -370,7 +365,7 @@ async function main() { 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, { + await writeFile(outputFile, renderMd(base, head, { headHeapSnapshotUrl: process.env.FRONTEND_BROWSER_HEAD_HEAP_SNAPSHOT_ARTIFACT_URL, detailedHtmlUrl: process.env.FRONTEND_BROWSER_DETAILED_HTML_ARTIFACT_URL, })); From fd83c9cb641e5bf82468b3f33fffda99d4562216 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:25:32 +0900 Subject: [PATCH 08/18] enhance(gh): tweak frontend-browser-diagnostics --- .../frontend-browser-diagnostics.inspect.mts | 35 ++++++++++--------- ...frontend-browser-diagnostics.render-md.mts | 11 +++--- .../frontend-browser-diagnostics.inspect.yml | 16 +++++++-- 3 files changed, 37 insertions(+), 25 deletions(-) diff --git a/.github/scripts/frontend-browser-diagnostics.inspect.mts b/.github/scripts/frontend-browser-diagnostics.inspect.mts index 545149df24..ec0e08857d 100644 --- a/.github/scripts/frontend-browser-diagnostics.inspect.mts +++ b/.github/scripts/frontend-browser-diagnostics.inspect.mts @@ -11,12 +11,15 @@ import { HeadlessChromeController, summarizeBrowserDiagnostics, summarizeNetwork 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, baseHeapSnapshotOutputArg, headHeapSnapshotOutputArg] = 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; type BrowserMeasurementSample = BrowserMeasurement & { round: number; @@ -223,28 +226,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, outputPath: string) { 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), outputPath); + process.stderr.write(`[${label}] Selected round ${representative.round} heap snapshot for artifact\n`); + await rm(heapSnapshotWorkDirs[label], { recursive: true, force: true }); } -async function genReport(label: 'base' | 'head', repoDir: string, outputPath: string, heapSnapshotSavePath?: string) { +async function genReport(label: 'base' | 'head', repoDir: string, outputPath: string, heapSnapshotSavePath: string) { let server: ReturnType | null = null; try { server = util.startServer(label, repoDir); 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++) { @@ -252,7 +253,7 @@ async function genReport(label: 'base' | 'head', repoDir: string, outputPath: st samples.push(await measureSample( label, round, - label === 'head' && heapSnapshotSavePath != null ? headHeapSnapshotPath(round) : undefined, + heapSnapshotPath(label, round), )); } @@ -260,8 +261,8 @@ async function genReport(label: 'base' | 'head', repoDir: string, outputPath: st 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); + if (heapSnapshotSavePath != null) { + await saveRepresentativeHeapSnapshot(label, report, heapSnapshotSavePath); } } finally { if (server != null) await util.stopServer(server); @@ -269,7 +270,7 @@ async function genReport(label: 'base' | 'head', repoDir: string, outputPath: st } async function main() { - await genReport('base', resolve(baseDirArg), resolve(baseOutputArg)); + await genReport('base', resolve(baseDirArg), resolve(baseOutputArg), resolve(baseHeapSnapshotOutputArg)); await genReport('head', resolve(headDirArg), resolve(headOutputArg), resolve(headHeapSnapshotOutputArg)); } diff --git a/.github/scripts/frontend-browser-diagnostics.render-md.mts b/.github/scripts/frontend-browser-diagnostics.render-md.mts index c18ef63f46..f0890e65ef 100644 --- a/.github/scripts/frontend-browser-diagnostics.render-md.mts +++ b/.github/scripts/frontend-browser-diagnostics.render-md.mts @@ -314,10 +314,10 @@ function toHeapSnapshotReport(report: BrowserMetricsReport): HeapSnapshotReport } function renderMd(base: BrowserMetricsReport, head: BrowserMetricsReport, options: { - headHeapSnapshotUrl?: string; + baseHeapSnapshotUrl: string; + headHeapSnapshotUrl: string; detailedHtmlUrl?: string; -} = {}) { - const headHeapSnapshotUrl = options.headHeapSnapshotUrl; +}) { const detailedHtmlUrl = options.detailedHtmlUrl; const heapSnapshotTable = heapSnapshotUtil.renderHeapSnapshotTable(toHeapSnapshotReport(base), toHeapSnapshotReport(head)); const lines = [ @@ -343,7 +343,7 @@ function renderMd(base: BrowserMetricsReport, head: BrowserMetricsReport, option '', heapSnapshotUtil.renderHeapSnapshotSankey(toHeapSnapshotReport(head), 'Head'), '', - `[Download representative head heap snapshot](${headHeapSnapshotUrl})`, + `Download representative heap snapshot: [base](${options.baseHeapSnapshotUrl}) / [head](${options.headHeapSnapshotUrl})`, '
', '', ]; @@ -366,7 +366,8 @@ async function main() { const base = JSON.parse(await readFile(baseFile, 'utf8')) as BrowserMetricsReport; const head = JSON.parse(await readFile(headFile, 'utf8')) as BrowserMetricsReport; await writeFile(outputFile, renderMd(base, head, { - headHeapSnapshotUrl: process.env.FRONTEND_BROWSER_HEAD_HEAP_SNAPSHOT_ARTIFACT_URL, + 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, })); } diff --git a/.github/workflows/frontend-browser-diagnostics.inspect.yml b/.github/workflows/frontend-browser-diagnostics.inspect.yml index 444c54040b..76af11bec6 100644 --- a/.github/workflows/frontend-browser-diagnostics.inspect.yml +++ b/.github/workflows/frontend-browser-diagnostics.inspect.yml @@ -127,14 +127,23 @@ jobs: run: | REPORT_DIR="$RUNNER_TEMP/frontend-browser-diagnostics" mkdir -p "$REPORT_DIR" - node after/.github/scripts/frontend-browser-diagnostics.inspect.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" "$REPORT_DIR/base-heap-snapshot.heapsnapshot" "$REPORT_DIR/head-heap-snapshot.heapsnapshot" + + - 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-diagnostics/head-heap-snapshot.heapsnapshot + path: head-heap-snapshot.heapsnapshot + archive: false if-no-files-found: error retention-days: 7 @@ -162,6 +171,7 @@ 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: | From 153875e1d96a472e05277dbba77c654d21d022f0 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:28:05 +0900 Subject: [PATCH 09/18] [skip ci] refacotr(gh): add note --- .../frontend-browser-diagnostics.render-additional-html.mts | 1 + .github/scripts/frontend-browser-diagnostics.render-md.mts | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/scripts/frontend-browser-diagnostics.render-additional-html.mts b/.github/scripts/frontend-browser-diagnostics.render-additional-html.mts index 86f70e7ebd..568f4257e2 100644 --- a/.github/scripts/frontend-browser-diagnostics.render-additional-html.mts +++ b/.github/scripts/frontend-browser-diagnostics.render-additional-html.mts @@ -440,6 +440,7 @@ async function main() { 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-diagnostics.render-md.mts b/.github/scripts/frontend-browser-diagnostics.render-md.mts index f0890e65ef..76ff29b911 100644 --- a/.github/scripts/frontend-browser-diagnostics.render-md.mts +++ b/.github/scripts/frontend-browser-diagnostics.render-md.mts @@ -372,6 +372,7 @@ async function main() { })); } +// 直接実行されたときだけ呼ぶための判定(テストなどでこのファイル内の関数をimportするだけのとき用) if (process.argv[1] != null && import.meta.url === pathToFileURL(process.argv[1]).href) { await main(); } From 8aff44ae2e23f34fabc627dfed4658c03df21bc2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:37:07 +0900 Subject: [PATCH 10/18] =?UTF-8?q?refactor(misskey-js):=20=E3=83=A2?= =?UTF-8?q?=E3=83=87=E3=83=AC=E3=83=BC=E3=82=B7=E3=83=A7=E3=83=B3=E3=83=AD?= =?UTF-8?q?=E3=82=B0=E3=81=AE=E5=9E=8B=E3=82=92=E6=89=8B=E5=8B=95=E3=81=A7?= =?UTF-8?q?=E6=9B=B8=E3=81=8B=E3=81=9A=E3=81=AB=E3=81=99=E3=82=80=E3=82=88?= =?UTF-8?q?=E3=81=86=E3=81=AB=20(#17729)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(misskey-js): モデレーションログの型を手動で書かずにすむように * fix lint --- packages/misskey-js/etc/misskey-js.api.md | 167 +--------------------- packages/misskey-js/src/entities.ts | 164 +-------------------- 2 files changed, 12 insertions(+), 319 deletions(-) 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; From 2874a6d5ba903fb752ca7c1dad98c81964aff95e Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:44:55 +0900 Subject: [PATCH 11/18] refacotr(gh): refactor --- ...frontend-bundle-diagnostics.render-md.mts} | 2 +- .github/scripts/frontend-js-size.test.mts | 252 -------------- ...> frontend-bundle-diagnostics.inspect.yml} | 16 +- .../frontend-bundle-diagnostics.report.yml | 179 ++++++++++ .../frontend-bundle-report-comment.yml | 318 ------------------ .github/workflows/lint.yml | 16 - 6 files changed, 188 insertions(+), 595 deletions(-) rename .github/scripts/{frontend-js-size.mts => frontend-bundle-diagnostics.render-md.mts} (99%) delete mode 100644 .github/scripts/frontend-js-size.test.mts rename .github/workflows/{frontend-bundle-report.yml => frontend-bundle-diagnostics.inspect.yml} (89%) create mode 100644 .github/workflows/frontend-bundle-diagnostics.report.yml delete mode 100644 .github/workflows/frontend-bundle-report-comment.yml 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..7ec0cfc61d 100644 --- a/.github/scripts/frontend-js-size.mts +++ b/.github/scripts/frontend-bundle-diagnostics.render-md.mts @@ -7,7 +7,7 @@ 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'; 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/frontend-bundle-report.yml b/.github/workflows/frontend-bundle-diagnostics.inspect.yml similarity index 89% rename from .github/workflows/frontend-bundle-report.yml rename to .github/workflows/frontend-bundle-diagnostics.inspect.yml index b5af3bb0e1..31faaed79b 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,16 +21,16 @@ 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.report.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: @@ -145,7 +145,7 @@ 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" @@ -157,8 +157,8 @@ jobs: 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' diff --git a/.github/workflows/frontend-bundle-diagnostics.report.yml b/.github/workflows/frontend-bundle-diagnostics.report.yml new file mode 100644 index 0000000000..260217ee53 --- /dev/null +++ b/.github/workflows/frontend-bundle-diagnostics.report.yml @@ -0,0 +1,179 @@ +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-bundle-diagnostics.render-md.mts + - .github/workflows/frontend-bundle-diagnostics.inspect.yml + - .github/workflows/frontend-bundle-diagnostics.report.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-diagnostics-report-${{ 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 + 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-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..63ae5c7397 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: @@ -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 From 68193dd38c4ad77473055e26bc735b32337544fc Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:46:59 +0900 Subject: [PATCH 12/18] refacotr(gh): refactor --- .github/workflows/frontend-bundle-diagnostics.report.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/frontend-bundle-diagnostics.report.yml b/.github/workflows/frontend-bundle-diagnostics.report.yml index 260217ee53..be63dd4575 100644 --- a/.github/workflows/frontend-bundle-diagnostics.report.yml +++ b/.github/workflows/frontend-bundle-diagnostics.report.yml @@ -1,9 +1,9 @@ -name: frontend-bundle-report-comment +name: Frontend bundle diagnostics (report) on: workflow_run: workflows: - - frontend-bundle-report + - Frontend bundle diagnostics (inspect) types: - completed pull_request_target: From b028d4ad5bbe132446e03022e66deefd624aa93c Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:53:36 +0900 Subject: [PATCH 13/18] refacotr(gh): rename some files --- ...diagnostics.report.yml => backend-diagnostics.comment.yml} | 2 +- .github/workflows/backend-diagnostics.inspect.yml | 4 ++-- ...cs.report.yml => frontend-browser-diagnostics.comment.yml} | 2 +- .github/workflows/frontend-browser-diagnostics.inspect.yml | 2 +- ...ics.report.yml => frontend-bundle-diagnostics.comment.yml} | 4 ++-- .github/workflows/frontend-bundle-diagnostics.inspect.yml | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) rename .github/workflows/{backend-diagnostics.report.yml => backend-diagnostics.comment.yml} (98%) rename .github/workflows/{frontend-browser-diagnostics.report.yml => frontend-browser-diagnostics.comment.yml} (96%) rename .github/workflows/{frontend-bundle-diagnostics.report.yml => frontend-bundle-diagnostics.comment.yml} (98%) diff --git a/.github/workflows/backend-diagnostics.report.yml b/.github/workflows/backend-diagnostics.comment.yml similarity index 98% rename from .github/workflows/backend-diagnostics.report.yml rename to .github/workflows/backend-diagnostics.comment.yml index f358216a62..4aa7eef68d 100644 --- a/.github/workflows/backend-diagnostics.report.yml +++ b/.github/workflows/backend-diagnostics.comment.yml @@ -1,4 +1,4 @@ -name: Backend diagnostics (report) +name: Backend diagnostics (comment) on: workflow_run: diff --git a/.github/workflows/backend-diagnostics.inspect.yml b/.github/workflows/backend-diagnostics.inspect.yml index 1474c78f0c..d5a47b804f 100644 --- a/.github/workflows/backend-diagnostics.inspect.yml +++ b/.github/workflows/backend-diagnostics.inspect.yml @@ -1,4 +1,4 @@ -# this name is used in backend-diagnostics.report.yml so be careful when change name +# this name is used in backend-diagnostics.comment.yml so be careful when change name name: Backend diagnostics (inspect) on: @@ -14,7 +14,7 @@ on: - .github/scripts/backend-diagnostics.inspect.mts - .github/scripts/memory-stability-util*.mts - .github/workflows/backend-diagnostics.inspect.yml - - .github/workflows/backend-diagnostics.report.yml + - .github/workflows/backend-diagnostics.comment.yml jobs: get-memory-usage: diff --git a/.github/workflows/frontend-browser-diagnostics.report.yml b/.github/workflows/frontend-browser-diagnostics.comment.yml similarity index 96% rename from .github/workflows/frontend-browser-diagnostics.report.yml rename to .github/workflows/frontend-browser-diagnostics.comment.yml index 7edc9f36c6..6b2f2b5a56 100644 --- a/.github/workflows/frontend-browser-diagnostics.report.yml +++ b/.github/workflows/frontend-browser-diagnostics.comment.yml @@ -1,4 +1,4 @@ -name: Frontend browser diagnostics (report) +name: Frontend browser diagnostics (comment) on: workflow_run: diff --git a/.github/workflows/frontend-browser-diagnostics.inspect.yml b/.github/workflows/frontend-browser-diagnostics.inspect.yml index 76af11bec6..63b43a05ae 100644 --- a/.github/workflows/frontend-browser-diagnostics.inspect.yml +++ b/.github/workflows/frontend-browser-diagnostics.inspect.yml @@ -29,7 +29,7 @@ on: - .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.report.yml + - .github/workflows/frontend-browser-diagnostics.comment.yml permissions: contents: read diff --git a/.github/workflows/frontend-bundle-diagnostics.report.yml b/.github/workflows/frontend-bundle-diagnostics.comment.yml similarity index 98% rename from .github/workflows/frontend-bundle-diagnostics.report.yml rename to .github/workflows/frontend-bundle-diagnostics.comment.yml index be63dd4575..ecc6f1dcfa 100644 --- a/.github/workflows/frontend-bundle-diagnostics.report.yml +++ b/.github/workflows/frontend-bundle-diagnostics.comment.yml @@ -1,4 +1,4 @@ -name: Frontend bundle diagnostics (report) +name: Frontend bundle diagnostics (comment) on: workflow_run: @@ -28,7 +28,7 @@ on: - .github/scripts/utility.mts - .github/scripts/frontend-bundle-diagnostics.render-md.mts - .github/workflows/frontend-bundle-diagnostics.inspect.yml - - .github/workflows/frontend-bundle-diagnostics.report.yml + - .github/workflows/frontend-bundle-diagnostics.comment.yml permissions: actions: read diff --git a/.github/workflows/frontend-bundle-diagnostics.inspect.yml b/.github/workflows/frontend-bundle-diagnostics.inspect.yml index 31faaed79b..3572f77ea8 100644 --- a/.github/workflows/frontend-bundle-diagnostics.inspect.yml +++ b/.github/workflows/frontend-bundle-diagnostics.inspect.yml @@ -23,7 +23,7 @@ on: - .github/scripts/utility.mts - .github/scripts/frontend-bundle-diagnostics.render-md.mts - .github/workflows/frontend-bundle-diagnostics.inspect.yml - - .github/workflows/frontend-bundle-diagnostics.report.yml + - .github/workflows/frontend-bundle-diagnostics.comment.yml permissions: contents: read From ceb31f16c842646bdaedcee0e5a2648663c54971 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:58:01 +0900 Subject: [PATCH 14/18] refacotr(gh): refactor --- .github/workflows/frontend-browser-diagnostics.comment.yml | 2 +- .github/workflows/frontend-bundle-diagnostics.comment.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/frontend-browser-diagnostics.comment.yml b/.github/workflows/frontend-browser-diagnostics.comment.yml index 6b2f2b5a56..5098e618ad 100644 --- a/.github/workflows/frontend-browser-diagnostics.comment.yml +++ b/.github/workflows/frontend-browser-diagnostics.comment.yml @@ -40,5 +40,5 @@ jobs: uses: thollander/actions-comment-pull-request@v3 with: pr-number: ${{ steps.load-pr-number.outputs.pr-number }} - comment-tag: frontend_browser_diagnostics + comment-tag: frontend-browser-diagnostics file-path: ${{ runner.temp }}/frontend-browser-diagnostics/frontend-browser-diagnostics.md diff --git a/.github/workflows/frontend-bundle-diagnostics.comment.yml b/.github/workflows/frontend-bundle-diagnostics.comment.yml index ecc6f1dcfa..3da6d0dcbc 100644 --- a/.github/workflows/frontend-bundle-diagnostics.comment.yml +++ b/.github/workflows/frontend-bundle-diagnostics.comment.yml @@ -175,5 +175,5 @@ jobs: uses: thollander/actions-comment-pull-request@v3 with: pr-number: ${{ steps.load-pr-number.outputs.pr-number }} - comment-tag: frontend_bundle_diagnostics + comment-tag: frontend-bundle-diagnostics file-path: ${{ runner.temp }}/frontend-bundle-report/frontend-bundle-diagnostics-report.md From 5d0ecf976d21a96610984abd6dd98ac15a270623 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:12:15 +0900 Subject: [PATCH 15/18] refacotr(gh): refactor --- .../frontend-bundle-diagnostics.comment.yml | 149 +----------------- 1 file changed, 7 insertions(+), 142 deletions(-) diff --git a/.github/workflows/frontend-bundle-diagnostics.comment.yml b/.github/workflows/frontend-bundle-diagnostics.comment.yml index 3da6d0dcbc..709c735e69 100644 --- a/.github/workflows/frontend-bundle-diagnostics.comment.yml +++ b/.github/workflows/frontend-bundle-diagnostics.comment.yml @@ -6,29 +6,6 @@ on: - Frontend bundle diagnostics (inspect) 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-bundle-diagnostics.render-md.mts - - .github/workflows/frontend-bundle-diagnostics.inspect.yml - - .github/workflows/frontend-bundle-diagnostics.comment.yml permissions: actions: read @@ -39,120 +16,13 @@ permissions: 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') + 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.pull_request.number || github.event.workflow_run.id }} + group: frontend-bundle-diagnostics-report-${{ 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' + - name: Download bundle report uses: actions/download-artifact@v8 with: name: frontend-bundle-report @@ -161,15 +31,10 @@ jobs: 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: 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 From ab21fc725b71f78a96a22e792f6f4af6376a4723 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:27:32 +0900 Subject: [PATCH 16/18] refacotr(gh): refactor --- .../scripts/frontend-browser-diagnostics.inspect.mts | 6 ++---- .github/workflows/backend-diagnostics.comment.yml | 2 +- .../workflows/frontend-browser-diagnostics.inspect.yml | 10 +++++----- .github/workflows/lint.yml | 4 ++-- 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/.github/scripts/frontend-browser-diagnostics.inspect.mts b/.github/scripts/frontend-browser-diagnostics.inspect.mts index ec0e08857d..eb3a228840 100644 --- a/.github/scripts/frontend-browser-diagnostics.inspect.mts +++ b/.github/scripts/frontend-browser-diagnostics.inspect.mts @@ -242,7 +242,7 @@ async function genReport(label: 'base' | 'head', repoDir: string, outputPath: st try { server = util.startServer(label, repoDir); - await util.waitForServer(baseUrl, server!); + await util.waitForServer(baseUrl, server); await rm(heapSnapshotWorkDirs[label], { recursive: true, force: true }); await mkdir(heapSnapshotWorkDirs[label], { recursive: true }); @@ -261,9 +261,7 @@ async function genReport(label: 'base' | 'head', repoDir: string, outputPath: st await writeFile(outputPath, JSON.stringify(report, null, '\t')); process.stderr.write(`[${label}] Wrote browser metrics report to ${outputPath}\n`); - if (heapSnapshotSavePath != null) { - await saveRepresentativeHeapSnapshot(label, report, heapSnapshotSavePath); - } + await saveRepresentativeHeapSnapshot(label, report, heapSnapshotSavePath); } finally { if (server != null) await util.stopServer(server); } diff --git a/.github/workflows/backend-diagnostics.comment.yml b/.github/workflows/backend-diagnostics.comment.yml index 4aa7eef68d..a19166a319 100644 --- a/.github/workflows/backend-diagnostics.comment.yml +++ b/.github/workflows/backend-diagnostics.comment.yml @@ -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; diff --git a/.github/workflows/frontend-browser-diagnostics.inspect.yml b/.github/workflows/frontend-browser-diagnostics.inspect.yml index 63b43a05ae..4841cb3822 100644 --- a/.github/workflows/frontend-browser-diagnostics.inspect.yml +++ b/.github/workflows/frontend-browser-diagnostics.inspect.yml @@ -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 @@ -133,7 +133,7 @@ jobs: id: upload-browser-base-heap-snapshot uses: actions/upload-artifact@v7 with: - path: base-heap-snapshot.heapsnapshot + path: ${{ runner.temp }}/frontend-browser-diagnostics/base-heap-snapshot.heapsnapshot archive: false if-no-files-found: error retention-days: 7 @@ -142,7 +142,7 @@ jobs: id: upload-browser-head-heap-snapshot uses: actions/upload-artifact@v7 with: - path: head-heap-snapshot.heapsnapshot + path: ${{ runner.temp }}/frontend-browser-diagnostics/head-heap-snapshot.heapsnapshot archive: false if-no-files-found: error retention-days: 7 diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 63ae5c7397..0e77f896a2 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -123,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' From 0272ee73eba065f4db873c7c602a877fbb720a25 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 16 Jul 2026 22:40:45 +0900 Subject: [PATCH 17/18] refacotr(gh): refactor --- .../frontend-bundle-diagnostics.render-md.mts | 2 +- .../frontend-bundle-diagnostics.inspect.yml | 26 ------------------- 2 files changed, 1 insertion(+), 27 deletions(-) diff --git a/.github/scripts/frontend-bundle-diagnostics.render-md.mts b/.github/scripts/frontend-bundle-diagnostics.render-md.mts index 7ec0cfc61d..1e8cf37f24 100644 --- a/.github/scripts/frontend-bundle-diagnostics.render-md.mts +++ b/.github/scripts/frontend-bundle-diagnostics.render-md.mts @@ -9,7 +9,7 @@ import * as util from './utility.mts'; 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/workflows/frontend-bundle-diagnostics.inspect.yml b/.github/workflows/frontend-bundle-diagnostics.inspect.yml index 3572f77ea8..6aa4f54fb4 100644 --- a/.github/workflows/frontend-bundle-diagnostics.inspect.yml +++ b/.github/workflows/frontend-bundle-diagnostics.inspect.yml @@ -37,8 +37,6 @@ 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 }} @@ -152,7 +128,6 @@ jobs: 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" @@ -161,7 +136,6 @@ jobs: 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 From 88c6cd1766b176821cebf996b76085840591ee12 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Thu, 16 Jul 2026 23:19:25 +0900 Subject: [PATCH 18/18] refactor(gh): unify heap snapshot output paths --- .../frontend-browser-diagnostics.inspect.mts | 18 +++++++++++------- .../frontend-browser-diagnostics.inspect.yml | 6 +++--- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/.github/scripts/frontend-browser-diagnostics.inspect.mts b/.github/scripts/frontend-browser-diagnostics.inspect.mts index eb3a228840..1f0d74875f 100644 --- a/.github/scripts/frontend-browser-diagnostics.inspect.mts +++ b/.github/scripts/frontend-browser-diagnostics.inspect.mts @@ -11,7 +11,7 @@ import { HeadlessChromeController, summarizeBrowserDiagnostics, summarizeNetwork 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, baseHeapSnapshotOutputArg, 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); @@ -20,6 +20,10 @@ 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; @@ -230,14 +234,14 @@ function heapSnapshotPath(label: 'base' | 'head', round: number) { return join(heapSnapshotWorkDirs[label], `round-${round}.heapsnapshot`); } -async function saveRepresentativeHeapSnapshot(label: 'base' | 'head', report: BrowserMetricsReport, outputPath: string) { +async function saveRepresentativeHeapSnapshot(label: 'base' | 'head', report: BrowserMetricsReport) { const representative = selectRepresentativeSample(report.samples, sample => sample.heapSnapshot.categories.total); - await copyFile(heapSnapshotPath(label, representative.round), outputPath); + 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 genReport(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 { @@ -261,15 +265,15 @@ async function genReport(label: 'base' | 'head', repoDir: string, outputPath: st await writeFile(outputPath, JSON.stringify(report, null, '\t')); process.stderr.write(`[${label}] Wrote browser metrics report to ${outputPath}\n`); - await saveRepresentativeHeapSnapshot(label, report, heapSnapshotSavePath); + await saveRepresentativeHeapSnapshot(label, report); } finally { if (server != null) await util.stopServer(server); } } async function main() { - await genReport('base', resolve(baseDirArg), resolve(baseOutputArg), resolve(baseHeapSnapshotOutputArg)); - await genReport('head', resolve(headDirArg), resolve(headOutputArg), resolve(headHeapSnapshotOutputArg)); + await genReport('base', resolve(baseDirArg), resolve(baseOutputArg)); + await genReport('head', resolve(headDirArg), resolve(headOutputArg)); } await main(); diff --git a/.github/workflows/frontend-browser-diagnostics.inspect.yml b/.github/workflows/frontend-browser-diagnostics.inspect.yml index 4841cb3822..92f645ab26 100644 --- a/.github/workflows/frontend-browser-diagnostics.inspect.yml +++ b/.github/workflows/frontend-browser-diagnostics.inspect.yml @@ -127,13 +127,13 @@ jobs: run: | REPORT_DIR="$RUNNER_TEMP/frontend-browser-diagnostics" mkdir -p "$REPORT_DIR" - node after/.github/scripts/frontend-browser-diagnostics.inspect.mts before after "$REPORT_DIR/before-browser.json" "$REPORT_DIR/after-browser.json" "$REPORT_DIR/base-heap-snapshot.heapsnapshot" "$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: ${{ runner.temp }}/frontend-browser-diagnostics/base-heap-snapshot.heapsnapshot + path: base-heap-snapshot.heapsnapshot archive: false if-no-files-found: error retention-days: 7 @@ -142,7 +142,7 @@ jobs: id: upload-browser-head-heap-snapshot uses: actions/upload-artifact@v7 with: - path: ${{ runner.temp }}/frontend-browser-diagnostics/head-heap-snapshot.heapsnapshot + path: head-heap-snapshot.heapsnapshot archive: false if-no-files-found: error retention-days: 7