mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-06-20 16:44:47 +02:00
Compare commits
14 Commits
improve-ba
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
053e244582 | ||
|
|
c0a8c7f93a | ||
|
|
1d0b27b4c5 | ||
|
|
3c003d73c4 | ||
|
|
36d78a788d | ||
|
|
09f058f29a | ||
|
|
ad8b194643 | ||
|
|
4c9dd0e5ff | ||
|
|
0ced35ae6c | ||
|
|
dcced940af | ||
|
|
dc97a72fdb | ||
|
|
cc7f1e7366 | ||
|
|
77878256a8 | ||
|
|
662129f414 |
164
.github/workflows/frontend-js-size-comment.yml
vendored
164
.github/workflows/frontend-js-size-comment.yml
vendored
@@ -6,60 +6,180 @@ on:
|
||||
- Frontend JS size
|
||||
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/workflows/frontend-js-size.yml
|
||||
- .github/workflows/frontend-js-size-comment.yml
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
name: Comment frontend JS size
|
||||
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
|
||||
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-js-size-comment-${{ github.event.pull_request.number || github.event.workflow_run.id }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Download size report
|
||||
- name: Find size report run
|
||||
if: github.event_name == 'pull_request_target'
|
||||
id: find-report-run
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const workflow_id = 'frontend-js-size.yml';
|
||||
const artifactName = 'frontend-js-size-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 listSizeWorkflowRuns() {
|
||||
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 listSizeWorkflowRuns())
|
||||
.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 JS size 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 };
|
||||
}
|
||||
|
||||
return { done: false, run: null };
|
||||
}
|
||||
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
const { done, run } = await findReportRun();
|
||||
if (run) {
|
||||
core.info(`Found frontend JS size report on workflow run ${run.id}.`);
|
||||
core.setOutput('run-id', String(run.id));
|
||||
return;
|
||||
}
|
||||
if (done) {
|
||||
return;
|
||||
}
|
||||
|
||||
core.info('Waiting for frontend JS size report artifact...');
|
||||
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
||||
}
|
||||
|
||||
core.warning(`Timed out waiting for ${artifactName} from ${workflow_id} for ${headSha}.`);
|
||||
|
||||
- name: Download size report from workflow_run
|
||||
if: github.event_name == 'workflow_run'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: frontend-js-size-report
|
||||
path: frontend-js-size-report
|
||||
path: ${{ runner.temp }}/frontend-js-size-report
|
||||
github-token: ${{ github.token }}
|
||||
repository: ${{ github.repository }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
- name: Download size 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-js-size-report
|
||||
path: ${{ runner.temp }}/frontend-js-size-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-run.outputs.run-id != ''
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ secrets.FRONTEND_JS_SIZE_COMMENT_TOKEN || github.token }}
|
||||
script: |
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const marker = '<!-- misskey-frontend-js-size -->';
|
||||
const body = fs.readFileSync('frontend-js-size-report/frontend-js-size-report.md', 'utf8');
|
||||
const reportDir = path.join(process.env.RUNNER_TEMP, 'frontend-js-size-report');
|
||||
const reportPath = [
|
||||
path.join(reportDir, 'report.md'),
|
||||
path.join(reportDir, 'frontend-js-size-report.md'),
|
||||
].find((file) => fs.existsSync(file));
|
||||
if (reportPath == null) {
|
||||
core.setFailed('The frontend JS size report artifact does not contain a report markdown file.');
|
||||
return;
|
||||
}
|
||||
|
||||
const body = fs.readFileSync(reportPath, 'utf8');
|
||||
const prNumberPath = path.join(reportDir, 'pr-number.txt');
|
||||
let issue_number = fs.existsSync(prNumberPath)
|
||||
? Number(fs.readFileSync(prNumberPath, 'utf8').trim())
|
||||
: context.payload.pull_request?.number;
|
||||
if (!body.includes(marker)) {
|
||||
core.setFailed('The frontend JS size report is missing the expected marker.');
|
||||
return;
|
||||
}
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
const workflowRun = context.payload.workflow_run;
|
||||
let issue_number = workflowRun.pull_requests?.[0]?.number;
|
||||
|
||||
if (issue_number == null) {
|
||||
const { data: pullRequests } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
|
||||
owner,
|
||||
repo,
|
||||
commit_sha: workflowRun.head_sha,
|
||||
});
|
||||
issue_number = pullRequests.find((pr) => pr.head.sha === workflowRun.head_sha)?.number
|
||||
?? pullRequests[0]?.number;
|
||||
}
|
||||
|
||||
if (issue_number == null) {
|
||||
core.info(`No pull request found for workflow run ${workflowRun.id}.`);
|
||||
if (!Number.isInteger(issue_number)) {
|
||||
core.setFailed('The frontend JS size report is missing a valid pull request number.');
|
||||
return;
|
||||
}
|
||||
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
const comments = await github.paginate(github.rest.issues.listComments, {
|
||||
owner,
|
||||
repo,
|
||||
|
||||
228
.github/workflows/frontend-js-size.yml
vendored
228
.github/workflows/frontend-js-size.yml
vendored
@@ -92,7 +92,6 @@ jobs:
|
||||
|
||||
const marker = '<!-- misskey-frontend-js-size -->';
|
||||
const locale = process.env.FRONTEND_JS_SIZE_LOCALE || 'ja-JP';
|
||||
const topLimit = 10;
|
||||
|
||||
function normalizePath(filePath) {
|
||||
return filePath.split(path.sep).join('/');
|
||||
@@ -126,15 +125,38 @@ jobs:
|
||||
function formatBytes(size) {
|
||||
if (size == null) return '-';
|
||||
if (size < 1024) return `${size} B`;
|
||||
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KiB`;
|
||||
return `${(size / 1024 / 1024).toFixed(2)} MiB`;
|
||||
if (size < 1024 * 1024) return `${stripTrailingZeros((size / 1024).toFixed(1))} KiB`;
|
||||
return `${stripTrailingZeros((size / 1024 / 1024).toFixed(2))} MiB`;
|
||||
}
|
||||
|
||||
function stripTrailingZeros(value) {
|
||||
return value.replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1');
|
||||
}
|
||||
|
||||
function formatMathText(text) {
|
||||
return text
|
||||
.replaceAll('\\', '\\\\')
|
||||
.replaceAll('{', '\\{')
|
||||
.replaceAll('}', '\\}')
|
||||
.replaceAll('%', '\\\\%');
|
||||
}
|
||||
|
||||
function formatDiff(diff) {
|
||||
if (diff == null) return '-';
|
||||
if (diff === 0) return '0 B';
|
||||
const sign = diff > 0 ? '+' : '-';
|
||||
return `${sign}${formatBytes(Math.abs(diff))}`;
|
||||
const text = `${sign}${formatBytes(Math.abs(diff))}`;
|
||||
const color = diff > 0 ? 'orange' : 'green';
|
||||
return `$\\color{${color}}{\\text{${formatMathText(text)}}}$`;
|
||||
}
|
||||
|
||||
function formatDiffPercent(beforeSize, afterSize) {
|
||||
if (beforeSize == null || beforeSize === 0 || afterSize == null || afterSize === 0) return '-';
|
||||
const diff = afterSize - beforeSize;
|
||||
if (diff === 0) return `0%`;
|
||||
const percent = Math.round(diff / beforeSize * 100);
|
||||
const color = diff > 0 ? 'orange' : 'green';
|
||||
return `$\\color{${color}}{\\text{${formatMathText(percent.toString() + '%')}}}$`;
|
||||
}
|
||||
|
||||
function escapeCell(value) {
|
||||
@@ -143,9 +165,7 @@ jobs:
|
||||
|
||||
function entryDisplayName(entry) {
|
||||
if (!entry) return '';
|
||||
return entry.displayName === entry.file
|
||||
? entry.displayName
|
||||
: `${entry.displayName} (${entry.file})`;
|
||||
return entry.displayName || entry.file;
|
||||
}
|
||||
|
||||
function findEntryKey(manifest) {
|
||||
@@ -180,7 +200,6 @@ jobs:
|
||||
}
|
||||
|
||||
async function resolveBuiltFile(outDir, file) {
|
||||
const originalPath = path.join(outDir, file);
|
||||
if (file.startsWith('scripts/')) {
|
||||
const localizedFile = file.slice('scripts/'.length);
|
||||
const localizedPath = path.join(outDir, locale, localizedFile);
|
||||
@@ -190,9 +209,11 @@ jobs:
|
||||
relativePath: `${locale}/${localizedFile}`,
|
||||
};
|
||||
}
|
||||
|
||||
throw new Error(`Expected ${locale} localized chunk for ${file}, but ${localizedPath} was not found.`);
|
||||
}
|
||||
return {
|
||||
absolutePath: originalPath,
|
||||
absolutePath: path.join(outDir, file),
|
||||
relativePath: file,
|
||||
};
|
||||
}
|
||||
@@ -219,18 +240,20 @@ jobs:
|
||||
byFile.add(builtFile.relativePath);
|
||||
}
|
||||
|
||||
for await (const fullPath of walk(outDir)) {
|
||||
if (!fullPath.endsWith('.js')) continue;
|
||||
const relativePath = normalizePath(path.relative(outDir, fullPath));
|
||||
if (byFile.has(relativePath)) continue;
|
||||
if (relativePath.startsWith('scripts/') || relativePath.startsWith(`${locale}/`)) continue;
|
||||
const size = await fileSize(fullPath);
|
||||
byKey.set(relativePath, {
|
||||
key: relativePath,
|
||||
displayName: relativePath,
|
||||
file: relativePath,
|
||||
size,
|
||||
});
|
||||
const localeDir = path.join(outDir, locale);
|
||||
if (await exists(localeDir)) {
|
||||
for await (const fullPath of walk(localeDir)) {
|
||||
if (!fullPath.endsWith('.js')) continue;
|
||||
const relativePath = normalizePath(path.relative(outDir, fullPath));
|
||||
if (byFile.has(relativePath)) continue;
|
||||
const size = await fileSize(fullPath);
|
||||
byKey.set(relativePath, {
|
||||
key: relativePath,
|
||||
displayName: relativePath,
|
||||
file: relativePath,
|
||||
size,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -240,47 +263,78 @@ jobs:
|
||||
};
|
||||
}
|
||||
|
||||
function compareRows(keys, before, after) {
|
||||
function commonKeys(before, after) {
|
||||
return Object.keys(before.chunks)
|
||||
.filter((key) => after.chunks[key] != null);
|
||||
}
|
||||
|
||||
function addedKeys(before, after) {
|
||||
return Object.keys(after.chunks)
|
||||
.filter((key) => before.chunks[key] == null);
|
||||
}
|
||||
|
||||
function removedKeys(before, after) {
|
||||
return Object.keys(before.chunks)
|
||||
.filter((key) => after.chunks[key] == null);
|
||||
}
|
||||
|
||||
function getChunkComparisonRows(keys, before, after) {
|
||||
return keys.map((key) => {
|
||||
const beforeEntry = before.chunks[key];
|
||||
const afterEntry = after.chunks[key];
|
||||
const beforeSize = beforeEntry?.size ?? null;
|
||||
const afterSize = afterEntry?.size ?? null;
|
||||
const beforeSize = beforeEntry?.size ?? 0;
|
||||
const afterSize = afterEntry?.size ?? 0;
|
||||
return {
|
||||
key,
|
||||
file: entryDisplayName(afterEntry ?? beforeEntry),
|
||||
name: entryDisplayName(beforeEntry ?? afterEntry),
|
||||
chunkFile: beforeEntry?.file ?? afterEntry?.file,
|
||||
beforeSize,
|
||||
afterSize,
|
||||
diff: beforeSize == null || afterSize == null ? null : afterSize - beforeSize,
|
||||
sortSize: Math.max(beforeSize ?? 0, afterSize ?? 0),
|
||||
sortSize: Math.max(beforeSize, afterSize),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function markdownTable(rows) {
|
||||
if (rows.length === 0) {
|
||||
return '_No JavaScript chunks found._';
|
||||
}
|
||||
function markdownTable(rows, total) {
|
||||
if (rows.length === 0) return '_No data_';
|
||||
|
||||
const lines = [
|
||||
'| File | Size (before) | Size (after) | Size (diff) |',
|
||||
'| --- | ---: | ---: | ---: |',
|
||||
'| Chunk | Before | After | Diff | Diff (%) |',
|
||||
'| --- | ---: | ---: | ---: | ---: |',
|
||||
];
|
||||
if (total != null) {
|
||||
lines.push(`| (total) | ${formatBytes(total.beforeSize)} | ${formatBytes(total.afterSize)} | ${formatDiff(total.afterSize - total.beforeSize)} | ${formatDiffPercent(total.beforeSize, total.afterSize)} |`);
|
||||
lines.push('| | | | | |');
|
||||
}
|
||||
for (const row of rows) {
|
||||
lines.push(`| ${escapeCell(row.file)} | ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${formatDiff(row.diff)} |`);
|
||||
lines.push(`| <details><summary>\`${escapeCell(row.name)}\`</summary> \`${escapeCell(row.chunkFile)}\` </details> | ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${formatDiff(row.afterSize - row.beforeSize)} | ${formatDiffPercent(row.beforeSize, row.afterSize)} |`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function unionTopKeys(before, after) {
|
||||
const allKeys = new Set([
|
||||
...Object.keys(before.chunks),
|
||||
...Object.keys(after.chunks),
|
||||
]);
|
||||
return compareRows([...allKeys], before, after)
|
||||
.sort((a, b) => b.sortSize - a.sortSize || a.file.localeCompare(b.file))
|
||||
.slice(0, topLimit)
|
||||
.map((row) => row.key);
|
||||
function chunkRows(keys, report) {
|
||||
return keys.map((key) => {
|
||||
const entry = report.chunks[key];
|
||||
return {
|
||||
key,
|
||||
name: entryDisplayName(entry),
|
||||
chunkFile: entry.file,
|
||||
size: entry.size,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function markdownChunkTable(rows) {
|
||||
if (rows.length === 0) return '_No data_';
|
||||
|
||||
const lines = [
|
||||
'| Chunk | Size |',
|
||||
'| --- | ---: |',
|
||||
];
|
||||
for (const row of rows) {
|
||||
lines.push(`| <details><summary>\`${escapeCell(row.name)}\`</summary> \`${escapeCell(row.chunkFile)}\` </details> | ${formatBytes(row.size)} |`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
const beforeDir = process.argv[2];
|
||||
@@ -292,33 +346,87 @@ jobs:
|
||||
const before = await collectReport(beforeDir);
|
||||
const after = await collectReport(afterDir);
|
||||
|
||||
const topRows = compareRows(unionTopKeys(before, after), before, after)
|
||||
.sort((a, b) => b.sortSize - a.sortSize || a.file.localeCompare(b.file));
|
||||
const commonChunkKeys = commonKeys(before, after);
|
||||
const comparisonRows = getChunkComparisonRows(commonChunkKeys, before, after);
|
||||
|
||||
const diffRows = comparisonRows
|
||||
.filter((row) => row.beforeSize !== row.afterSize)
|
||||
.sort((a, b) => Math.abs(b.afterSize - b.beforeSize) - Math.abs(a.afterSize - a.beforeSize)
|
||||
|| (b.afterSize - b.beforeSize) - (a.afterSize - a.beforeSize)
|
||||
|| b.sortSize - a.sortSize
|
||||
|| a.name.localeCompare(b.name))
|
||||
.slice(0, 30);
|
||||
|
||||
const diffTotal = {
|
||||
beforeSize: comparisonRows.reduce((sum, row) => sum + row.beforeSize, 0),
|
||||
afterSize: comparisonRows.reduce((sum, row) => sum + row.afterSize, 0),
|
||||
};
|
||||
|
||||
const addedRows = chunkRows(addedKeys(before, after), after)
|
||||
.sort((a, b) => b.size - a.size || a.name.localeCompare(b.name));
|
||||
|
||||
const removedRows = chunkRows(removedKeys(before, after), before)
|
||||
.sort((a, b) => b.size - a.size || a.name.localeCompare(b.name));
|
||||
|
||||
const startupKeys = new Set([
|
||||
...before.startupKeys,
|
||||
...after.startupKeys,
|
||||
]);
|
||||
const startupRows = compareRows([...startupKeys], before, after)
|
||||
.sort((a, b) => b.sortSize - a.sortSize || a.file.localeCompare(b.file));
|
||||
const startupComparisonRows = getChunkComparisonRows([...startupKeys], before, after);
|
||||
const startupRows = startupComparisonRows
|
||||
.sort((a, b) => Math.abs(b.afterSize - b.beforeSize) - Math.abs(a.afterSize - a.beforeSize)
|
||||
|| (b.afterSize - b.beforeSize) - (a.afterSize - a.beforeSize)
|
||||
|| b.sortSize - a.sortSize
|
||||
|| a.name.localeCompare(b.name));
|
||||
const startupTotal = {
|
||||
beforeSize: startupComparisonRows.reduce((sum, row) => sum + row.beforeSize, 0),
|
||||
afterSize: startupComparisonRows.reduce((sum, row) => sum + row.afterSize, 0),
|
||||
};
|
||||
|
||||
const largeRows = comparisonRows
|
||||
.sort((a, b) => b.sortSize - a.sortSize || a.name.localeCompare(b.name))
|
||||
.slice(0, 30);
|
||||
|
||||
const body = [
|
||||
marker,
|
||||
'## Frontend JavaScript size',
|
||||
`## Frontend chunk size report (${locale})`,
|
||||
'',
|
||||
`Compared locale: \`${locale}\``,
|
||||
`Before: \`${beforeSha}\``,
|
||||
`After: \`${afterSha}\``,
|
||||
'<details open>',
|
||||
`<summary>Diffs</summary>`,
|
||||
'',
|
||||
'### Top 10 largest JS chunks',
|
||||
markdownTable(diffRows, diffTotal),
|
||||
'',
|
||||
markdownTable(topRows),
|
||||
'</details>',
|
||||
'',
|
||||
'### Startup JS chunks',
|
||||
'<details>',
|
||||
`<summary>Added (${addedRows.length})</summary>`,
|
||||
'',
|
||||
markdownTable(startupRows),
|
||||
markdownChunkTable(addedRows),
|
||||
'',
|
||||
'_Top 10 is sorted by max(before, after) size. Startup chunks are the Vite entry for `src/_boot_.ts` and its static imports._',
|
||||
'</details>',
|
||||
'',
|
||||
'<details>',
|
||||
`<summary>Removed (${removedRows.length})</summary>`,
|
||||
'',
|
||||
markdownChunkTable(removedRows),
|
||||
'',
|
||||
'</details>',
|
||||
'',
|
||||
'<details>',
|
||||
`<summary>Startup</summary>`,
|
||||
'',
|
||||
markdownTable(startupRows, startupTotal),
|
||||
'',
|
||||
`_Only ${locale} localized chunks are reported. Size comparison tables include chunks that exist in both builds. Added and removed chunks are listed separately. Top 10 is sorted by max(before, after) size. Diff top 10 is sorted by absolute size diff. Startup chunks are the Vite entry for \`src/_boot_.ts\` and its static imports._`,
|
||||
'',
|
||||
'</details>',
|
||||
'',
|
||||
'<details>',
|
||||
`<summary>Largest</summary>`,
|
||||
'',
|
||||
markdownTable(largeRows),
|
||||
'',
|
||||
'</details>',
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
@@ -330,14 +438,18 @@ jobs:
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
run: |
|
||||
mkdir -p frontend-js-size-report
|
||||
node .github/tmp/frontend-js-size-report.mjs before after frontend-js-size-report.md
|
||||
cat frontend-js-size-report.md >> "$GITHUB_STEP_SUMMARY"
|
||||
mv frontend-js-size-report.md frontend-js-size-report/report.md
|
||||
printf '%s\n' "$PR_NUMBER" > frontend-js-size-report/pr-number.txt
|
||||
cat frontend-js-size-report/report.md >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload size report
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: frontend-js-size-report
|
||||
path: frontend-js-size-report.md
|
||||
path: frontend-js-size-report/
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
120
.github/workflows/get-backend-memory.yml
vendored
120
.github/workflows/get-backend-memory.yml
vendored
@@ -10,7 +10,6 @@ on:
|
||||
- packages/backend/**
|
||||
- packages/misskey-js/**
|
||||
- .github/workflows/get-backend-memory.yml
|
||||
- .github/workflows/report-backend-memory.yml
|
||||
|
||||
jobs:
|
||||
get-memory-usage:
|
||||
@@ -18,6 +17,15 @@ jobs:
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
memory-json-name: [memory-base.json, memory-head.json]
|
||||
include:
|
||||
- memory-json-name: memory-base.json
|
||||
ref: ${{ github.base_ref }}
|
||||
- memory-json-name: memory-head.json
|
||||
ref: refs/pull/${{ github.event.number }}/merge
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:18
|
||||
@@ -32,113 +40,37 @@ jobs:
|
||||
- 56312:6379
|
||||
|
||||
steps:
|
||||
- name: Checkout base
|
||||
uses: actions/checkout@v6.0.2
|
||||
- uses: actions/checkout@v6.0.2
|
||||
with:
|
||||
ref: ${{ github.base_ref }}
|
||||
path: base
|
||||
submodules: true
|
||||
- name: Checkout head
|
||||
uses: actions/checkout@v6.0.2
|
||||
with:
|
||||
ref: refs/pull/${{ github.event.number }}/merge
|
||||
path: head
|
||||
ref: ${{ matrix.ref }}
|
||||
submodules: true
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
with:
|
||||
package_json_file: head/package.json
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: 'head/.node-version'
|
||||
node-version-file: '.node-version'
|
||||
cache: 'pnpm'
|
||||
cache-dependency-path: |
|
||||
base/pnpm-lock.yaml
|
||||
head/pnpm-lock.yaml
|
||||
- name: Install base dependencies
|
||||
working-directory: base
|
||||
run: pnpm i --frozen-lockfile
|
||||
- name: Check base pnpm-lock.yaml
|
||||
working-directory: base
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- name: Check pnpm-lock.yaml
|
||||
run: git diff --exit-code pnpm-lock.yaml
|
||||
- name: Configure base
|
||||
working-directory: base
|
||||
run: |
|
||||
cp .github/misskey/test.yml .config/default.yml
|
||||
pnpm compile-config
|
||||
- name: Build base
|
||||
working-directory: base
|
||||
- name: Copy Configure
|
||||
run: cp .github/misskey/test.yml .config/default.yml
|
||||
- name: Compile Configure
|
||||
run: pnpm compile-config
|
||||
- name: Build
|
||||
run: pnpm build
|
||||
- name: Install head dependencies
|
||||
working-directory: head
|
||||
run: pnpm i --frozen-lockfile
|
||||
- name: Check head pnpm-lock.yaml
|
||||
working-directory: head
|
||||
run: git diff --exit-code pnpm-lock.yaml
|
||||
- name: Configure head
|
||||
working-directory: head
|
||||
- name: Run migrations
|
||||
run: pnpm --filter backend migrate
|
||||
- name: Measure memory usage
|
||||
run: |
|
||||
cp .github/misskey/test.yml .config/default.yml
|
||||
pnpm compile-config
|
||||
- name: Build head
|
||||
working-directory: head
|
||||
run: pnpm build
|
||||
- name: Measure base memory usage
|
||||
working-directory: base
|
||||
run: |
|
||||
node --input-type=module <<'EOF'
|
||||
import pg from 'pg';
|
||||
import Redis from 'ioredis';
|
||||
|
||||
const postgres = new pg.Client({
|
||||
host: '127.0.0.1',
|
||||
port: 54312,
|
||||
database: 'postgres',
|
||||
user: 'postgres',
|
||||
});
|
||||
await postgres.connect();
|
||||
await postgres.query('DROP DATABASE IF EXISTS "test-misskey" WITH (FORCE)');
|
||||
await postgres.query('CREATE DATABASE "test-misskey"');
|
||||
await postgres.end();
|
||||
|
||||
const redis = new Redis({ host: '127.0.0.1', port: 56312 });
|
||||
await redis.flushall();
|
||||
redis.disconnect();
|
||||
EOF
|
||||
pnpm --filter backend migrate
|
||||
node packages/backend/scripts/measure-memory.mjs > ../memory-base.json
|
||||
- name: Measure head memory usage
|
||||
working-directory: head
|
||||
run: |
|
||||
node --input-type=module <<'EOF'
|
||||
import pg from 'pg';
|
||||
import Redis from 'ioredis';
|
||||
|
||||
const postgres = new pg.Client({
|
||||
host: '127.0.0.1',
|
||||
port: 54312,
|
||||
database: 'postgres',
|
||||
user: 'postgres',
|
||||
});
|
||||
await postgres.connect();
|
||||
await postgres.query('DROP DATABASE IF EXISTS "test-misskey" WITH (FORCE)');
|
||||
await postgres.query('CREATE DATABASE "test-misskey"');
|
||||
await postgres.end();
|
||||
|
||||
const redis = new Redis({ host: '127.0.0.1', port: 56312 });
|
||||
await redis.flushall();
|
||||
redis.disconnect();
|
||||
EOF
|
||||
pnpm --filter backend migrate
|
||||
node packages/backend/scripts/measure-memory.mjs > ../memory-head.json
|
||||
# Start the server and measure memory usage
|
||||
node packages/backend/scripts/measure-memory.mjs > ${{ matrix.memory-json-name }}
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: memory-artifact-results
|
||||
path: |
|
||||
memory-base.json
|
||||
memory-head.json
|
||||
name: memory-artifact-${{ matrix.memory-json-name }}
|
||||
path: ${{ matrix.memory-json-name }}
|
||||
|
||||
save-pr-number:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
52
.github/workflows/report-backend-memory.yml
vendored
52
.github/workflows/report-backend-memory.yml
vendored
@@ -56,25 +56,20 @@ jobs:
|
||||
|
||||
variation() {
|
||||
calc() {
|
||||
BASE=$(echo "$BASE_MEMORY" | jq -r ".${1}.${2} // empty")
|
||||
HEAD=$(echo "$HEAD_MEMORY" | jq -r ".${1}.${2} // empty")
|
||||
|
||||
if [ -z "$BASE" ] || [ -z "$HEAD" ]; then
|
||||
echo "null"
|
||||
return
|
||||
fi
|
||||
BASE=$(echo "$BASE_MEMORY" | jq -r ".${1}.${2} // 0")
|
||||
HEAD=$(echo "$HEAD_MEMORY" | jq -r ".${1}.${2} // 0")
|
||||
|
||||
DIFF=$((HEAD - BASE))
|
||||
if [ "$BASE" -gt 0 ]; then
|
||||
DIFF_PERCENT=$(awk -v diff="$DIFF" -v base="$BASE" 'BEGIN { printf "%.2f", (diff * 100) / base }')
|
||||
DIFF_PERCENT=$(echo "scale=2; ($DIFF * 100) / $BASE" | bc)
|
||||
else
|
||||
DIFF_PERCENT=0.00
|
||||
DIFF_PERCENT=0
|
||||
fi
|
||||
|
||||
# Convert KB to MB for readability
|
||||
BASE_MB=$(awk -v value="$BASE" 'BEGIN { printf "%.2f", value / 1024 }')
|
||||
HEAD_MB=$(awk -v value="$HEAD" 'BEGIN { printf "%.2f", value / 1024 }')
|
||||
DIFF_MB=$(awk -v value="$DIFF" 'BEGIN { printf "%.2f", value / 1024 }')
|
||||
BASE_MB=$(echo "scale=2; $BASE / 1024" | bc)
|
||||
HEAD_MB=$(echo "scale=2; $HEAD / 1024" | bc)
|
||||
DIFF_MB=$(echo "scale=2; $DIFF / 1024" | bc)
|
||||
|
||||
JSON=$(jq -c -n \
|
||||
--argjson base "$BASE_MB" \
|
||||
@@ -87,20 +82,11 @@ jobs:
|
||||
}
|
||||
|
||||
JSON=$(jq -c -n \
|
||||
--argjson HeapUsed "$(calc $1 HeapUsed)" \
|
||||
--argjson HeapTotal "$(calc $1 HeapTotal)" \
|
||||
--argjson External "$(calc $1 External)" \
|
||||
--argjson ArrayBuffers "$(calc $1 ArrayBuffers)" \
|
||||
--argjson Pss "$(calc $1 Pss)" \
|
||||
--argjson Private_Dirty "$(calc $1 Private_Dirty)" \
|
||||
--argjson Private_Clean "$(calc $1 Private_Clean)" \
|
||||
--argjson Shared_Dirty "$(calc $1 Shared_Dirty)" \
|
||||
--argjson Shared_Clean "$(calc $1 Shared_Clean)" \
|
||||
--argjson VmRSS "$(calc $1 VmRSS)" \
|
||||
--argjson VmHWM "$(calc $1 VmHWM)" \
|
||||
--argjson VmSize "$(calc $1 VmSize)" \
|
||||
--argjson VmData "$(calc $1 VmData)" \
|
||||
'{HeapUsed: $HeapUsed, HeapTotal: $HeapTotal, External: $External, ArrayBuffers: $ArrayBuffers, Pss: $Pss, Private_Dirty: $Private_Dirty, Private_Clean: $Private_Clean, Shared_Dirty: $Shared_Dirty, Shared_Clean: $Shared_Clean, VmRSS: $VmRSS, VmHWM: $VmHWM, VmSize: $VmSize, VmData: $VmData}')
|
||||
'{VmRSS: $VmRSS, VmHWM: $VmHWM, VmSize: $VmSize, VmData: $VmData}')
|
||||
|
||||
echo "$JSON"
|
||||
}
|
||||
@@ -128,10 +114,6 @@ jobs:
|
||||
echo "|--------|------:|------:|------:|------:|" >> ./output.md
|
||||
|
||||
line() {
|
||||
if [ "$(echo "$RES" | jq -r ".${1}.${2} == null")" = "true" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
METRIC=$2
|
||||
BASE=$(echo "$RES" | jq -r ".${1}.${2}.base")
|
||||
HEAD=$(echo "$RES" | jq -r ".${1}.${2}.head")
|
||||
@@ -143,8 +125,8 @@ jobs:
|
||||
DIFF_PERCENT="+$DIFF_PERCENT"
|
||||
fi
|
||||
|
||||
# highlight the most useful process and OS memory metrics
|
||||
if [ "$2" = "HeapUsed" ] || [ "$2" = "Pss" ]; then
|
||||
# highlight VmRSS
|
||||
if [ "$2" = "VmRSS" ]; then
|
||||
METRIC="**${METRIC}**"
|
||||
BASE="**${BASE}**"
|
||||
HEAD="**${HEAD}**"
|
||||
@@ -155,15 +137,6 @@ jobs:
|
||||
echo "| ${METRIC} | ${BASE} MB | ${HEAD} MB | ${DIFF} MB | ${DIFF_PERCENT}% |" >> ./output.md
|
||||
}
|
||||
|
||||
line $1 HeapUsed
|
||||
line $1 HeapTotal
|
||||
line $1 External
|
||||
line $1 ArrayBuffers
|
||||
line $1 Pss
|
||||
line $1 Private_Dirty
|
||||
line $1 Private_Clean
|
||||
line $1 Shared_Dirty
|
||||
line $1 Shared_Clean
|
||||
line $1 VmRSS
|
||||
line $1 VmHWM
|
||||
line $1 VmSize
|
||||
@@ -183,9 +156,8 @@ jobs:
|
||||
echo >> ./output.md
|
||||
|
||||
# Determine if this is a significant change (more than 5% increase)
|
||||
WARNING_METRIC=$(echo "$RES" | jq -r 'if .afterGc.Pss != null then "Pss" elif .afterGc.VmRSS != null then "VmRSS" else empty end')
|
||||
if [ -n "$WARNING_METRIC" ] && [ "$(echo "$RES" | jq -r ".afterGc.${WARNING_METRIC}.diff_percent | tonumber > 5")" = "true" ]; then
|
||||
echo "⚠️ **Warning**: Memory usage (${WARNING_METRIC}) has increased by more than 5%. Please verify this is not an unintended change." >> ./output.md
|
||||
if [ "$(echo "$RES" | jq -r '.afterGc.VmRSS.diff_percent | tonumber > 5')" = "true" ]; then
|
||||
echo "⚠️ **Warning**: Memory usage has increased by more than 5%. Please verify this is not an unintended change." >> ./output.md
|
||||
echo >> ./output.md
|
||||
fi
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
- Enhance: リモートノートクリーニングジョブのスキップ処理のパフォーマンス改善
|
||||
- Enhance: リモートノートクリーニングジョブの削除対象検索処理のパフォーマンス改善
|
||||
- Enhance: ActivityPub の画像添付に width/height を含めるように
|
||||
- Enhance: URLプレビューのデフォルトの User Agent に Misskey サーバーのURLを含めるように
|
||||
- Fix: backend バンドルで `@tensorflow/tfjs-node` を external に含めず、起動時に `@mapbox/node-pre-gyp` の `find()` が backend の package.json を誤検出して `is not node-pre-gyp ready` エラーを永続的に吐く問題を修正
|
||||
- Fix: MemoryKVCacheのキャッシュGC処理において、更新されたキャッシュが期限切れにならないことがある問題を修正
|
||||
- Fix: PerUserDriveChart がシステム所有ファイル (userId が null) の更新で `"group"` の非NULL制約違反によりクラッシュする問題を修正 (#17498)
|
||||
|
||||
@@ -64,7 +64,7 @@
|
||||
"@misskey-dev/emoji-assets": "17.0.3",
|
||||
"@misskey-dev/emoji-data": "17.0.3",
|
||||
"@misskey-dev/sharp-read-bmp": "1.2.0",
|
||||
"@misskey-dev/summaly": "5.3.0",
|
||||
"@misskey-dev/summaly": "5.5.1",
|
||||
"@napi-rs/canvas": "1.0.0",
|
||||
"@nestjs/common": "11.1.26",
|
||||
"@nestjs/core": "11.1.26",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineConfig } from 'rolldown';
|
||||
import { version as summalyVersion } from '@misskey-dev/summaly';
|
||||
import type { Plugin, ExternalOption } from 'rolldown';
|
||||
import { execa, execaNode } from 'execa';
|
||||
import type { ResultPromise } from 'execa';
|
||||
@@ -84,6 +85,11 @@ export default defineConfig((args) => {
|
||||
'file-type',
|
||||
];
|
||||
|
||||
const define: Record<string, string> = {
|
||||
// Summalyのバージョンを埋め込む
|
||||
'_SUMMALY_VERSION_': JSON.stringify(summalyVersion),
|
||||
};
|
||||
|
||||
if (isE2E) {
|
||||
return {
|
||||
input: './test-server/entry.ts',
|
||||
@@ -92,6 +98,9 @@ export default defineConfig((args) => {
|
||||
plugins: [
|
||||
esmShim(),
|
||||
],
|
||||
transform: {
|
||||
define,
|
||||
},
|
||||
output: {
|
||||
keepNames: true,
|
||||
sourcemap: true,
|
||||
@@ -116,6 +125,9 @@ export default defineConfig((args) => {
|
||||
esmShim(),
|
||||
(isWatchMode ? backendDevServerPlugin() : undefined),
|
||||
],
|
||||
transform: {
|
||||
define,
|
||||
},
|
||||
output: {
|
||||
keepNames: true,
|
||||
minify: !isWatchMode,
|
||||
|
||||
@@ -23,9 +23,8 @@ const __dirname = dirname(__filename);
|
||||
const SAMPLE_COUNT = 3; // Number of samples to measure
|
||||
const STARTUP_TIMEOUT = 120000; // 120 seconds timeout for server startup
|
||||
const MEMORY_SETTLE_TIME = 10000; // Wait 10 seconds after startup for memory to settle
|
||||
const IPC_TIMEOUT = 30000; // 30 seconds timeout for IPC responses
|
||||
|
||||
const procStatusKeys = {
|
||||
const keys = {
|
||||
VmPeak: 0,
|
||||
VmSize: 0,
|
||||
VmHWM: 0,
|
||||
@@ -38,152 +37,30 @@ const procStatusKeys = {
|
||||
VmSwap: 0,
|
||||
};
|
||||
|
||||
const smapsRollupKeys = {
|
||||
Pss: 0,
|
||||
Shared_Clean: 0,
|
||||
Shared_Dirty: 0,
|
||||
Private_Clean: 0,
|
||||
Private_Dirty: 0,
|
||||
Swap: 0,
|
||||
SwapPss: 0,
|
||||
};
|
||||
async function getMemoryUsage(pid) {
|
||||
const status = await fs.readFile(`/proc/${pid}/status`, 'utf-8');
|
||||
|
||||
const runtimeKeys = {
|
||||
HeapTotal: 0,
|
||||
HeapUsed: 0,
|
||||
External: 0,
|
||||
ArrayBuffers: 0,
|
||||
};
|
||||
|
||||
const memoryKeys = {
|
||||
...procStatusKeys,
|
||||
...smapsRollupKeys,
|
||||
...runtimeKeys,
|
||||
};
|
||||
|
||||
const phases = ['beforeGc', 'afterGc', 'afterRequest'];
|
||||
|
||||
function parseMemoryFile(content, keys, path, required) {
|
||||
const result = {};
|
||||
for (const key of Object.keys(keys)) {
|
||||
const match = content.match(new RegExp(`${key}:\\s+(\\d+)\\s+kB`));
|
||||
const match = status.match(new RegExp(`${key}:\\s+(\\d+)\\s+kB`));
|
||||
if (match) {
|
||||
result[key] = parseInt(match[1], 10);
|
||||
} else if (required) {
|
||||
throw new Error(`Failed to parse ${key} from ${path}`);
|
||||
} else {
|
||||
throw new Error(`Failed to parse ${key} from /proc/${pid}/status`);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function bytesToKiB(value) {
|
||||
return Math.round(value / 1024);
|
||||
}
|
||||
|
||||
async function getMemoryUsage(pid) {
|
||||
const path = `/proc/${pid}/status`;
|
||||
const status = await fs.readFile(path, 'utf-8');
|
||||
|
||||
return parseMemoryFile(status, procStatusKeys, path, true);
|
||||
}
|
||||
|
||||
async function getSmapsRollupMemoryUsage(pid) {
|
||||
const path = `/proc/${pid}/smaps_rollup`;
|
||||
try {
|
||||
const smapsRollup = await fs.readFile(path, 'utf-8');
|
||||
return parseMemoryFile(smapsRollup, smapsRollupKeys, path, false);
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT' || err.code === 'EACCES') {
|
||||
process.stderr.write(`Failed to read ${path}: ${err.message}\n`);
|
||||
return {};
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function waitForMessage(serverProcess, predicate, description, timeout = IPC_TIMEOUT) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = globalThis.setTimeout(() => {
|
||||
serverProcess.off('message', onMessage);
|
||||
reject(new Error(`Timed out waiting for ${description}`));
|
||||
}, timeout);
|
||||
|
||||
const onMessage = (message) => {
|
||||
if (!predicate(message)) return;
|
||||
globalThis.clearTimeout(timer);
|
||||
serverProcess.off('message', onMessage);
|
||||
resolve(message);
|
||||
};
|
||||
|
||||
serverProcess.on('message', onMessage);
|
||||
});
|
||||
}
|
||||
|
||||
async function getRuntimeMemoryUsage(serverProcess) {
|
||||
const response = waitForMessage(
|
||||
serverProcess,
|
||||
message => message != null && typeof message === 'object' && message.type === 'memory usage',
|
||||
'memory usage',
|
||||
);
|
||||
|
||||
serverProcess.send('memory usage');
|
||||
|
||||
const message = await response;
|
||||
const memoryUsage = message.value;
|
||||
|
||||
return {
|
||||
HeapTotal: bytesToKiB(memoryUsage.heapTotal),
|
||||
HeapUsed: bytesToKiB(memoryUsage.heapUsed),
|
||||
External: bytesToKiB(memoryUsage.external),
|
||||
ArrayBuffers: bytesToKiB(memoryUsage.arrayBuffers),
|
||||
};
|
||||
}
|
||||
|
||||
async function getAllMemoryUsage(serverProcess) {
|
||||
const pid = serverProcess.pid;
|
||||
return {
|
||||
...await getMemoryUsage(pid),
|
||||
...await getSmapsRollupMemoryUsage(pid),
|
||||
...await getRuntimeMemoryUsage(serverProcess),
|
||||
};
|
||||
}
|
||||
|
||||
function median(values) {
|
||||
const sorted = values.toSorted((a, b) => a - b);
|
||||
const center = Math.floor(sorted.length / 2);
|
||||
if (sorted.length % 2 === 1) return sorted[center];
|
||||
return Math.round((sorted[center - 1] + sorted[center]) / 2);
|
||||
}
|
||||
|
||||
function summarizeResults(results) {
|
||||
const summary = {};
|
||||
|
||||
for (const phase of phases) {
|
||||
summary[phase] = {};
|
||||
for (const key of Object.keys(memoryKeys)) {
|
||||
const values = results
|
||||
.map(result => result[phase][key])
|
||||
.filter(value => Number.isFinite(value));
|
||||
|
||||
if (values.length > 0) {
|
||||
summary[phase][key] = median(values);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
async function measureMemory() {
|
||||
// Start the Misskey backend server using fork to enable IPC
|
||||
const serverProcess = fork(join(__dirname, '../built/entry.js'), [], {
|
||||
const serverProcess = fork(join(__dirname, '../built/entry.js'), ['expose-gc'], {
|
||||
cwd: join(__dirname, '..'),
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: 'production',
|
||||
MK_DISABLE_CLUSTERING: '1',
|
||||
MK_ONLY_SERVER: '1',
|
||||
MK_NO_DAEMONS: '1',
|
||||
},
|
||||
stdio: ['pipe', 'pipe', 'pipe', 'ipc'],
|
||||
execArgv: [...process.execArgv, '--expose-gc'],
|
||||
@@ -213,18 +90,15 @@ async function measureMemory() {
|
||||
});
|
||||
|
||||
async function triggerGc() {
|
||||
const ok = waitForMessage(
|
||||
serverProcess,
|
||||
message => message === 'gc ok' || message === 'gc unavailable',
|
||||
'GC completion',
|
||||
);
|
||||
const ok = new Promise((resolve) => {
|
||||
serverProcess.once('message', (message) => {
|
||||
if (message === 'gc ok') resolve();
|
||||
});
|
||||
});
|
||||
|
||||
serverProcess.send('gc');
|
||||
|
||||
const message = await ok;
|
||||
if (message === 'gc unavailable') {
|
||||
throw new Error('GC is unavailable. Start the process with --expose-gc to enable this feature.');
|
||||
}
|
||||
await ok;
|
||||
|
||||
await setTimeout(1000);
|
||||
}
|
||||
@@ -265,11 +139,13 @@ async function measureMemory() {
|
||||
// Wait for memory to settle
|
||||
await setTimeout(MEMORY_SETTLE_TIME);
|
||||
|
||||
const beforeGc = await getAllMemoryUsage(serverProcess);
|
||||
const pid = serverProcess.pid;
|
||||
|
||||
const beforeGc = await getMemoryUsage(pid);
|
||||
|
||||
await triggerGc();
|
||||
|
||||
const afterGc = await getAllMemoryUsage(serverProcess);
|
||||
const afterGc = await getMemoryUsage(pid);
|
||||
|
||||
// create some http requests to simulate load
|
||||
const REQUEST_COUNT = 10;
|
||||
@@ -279,7 +155,7 @@ async function measureMemory() {
|
||||
|
||||
await triggerGc();
|
||||
|
||||
const afterRequest = await getAllMemoryUsage(serverProcess);
|
||||
const afterRequest = await getMemoryUsage(pid);
|
||||
|
||||
// Stop the server
|
||||
serverProcess.kill('SIGTERM');
|
||||
@@ -311,21 +187,35 @@ async function measureMemory() {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// 直列の方が時間的に分散されて正確そうだから直列でやる
|
||||
const results = [];
|
||||
for (let i = 0; i < SAMPLE_COUNT; i++) {
|
||||
process.stderr.write(`Starting sample ${i + 1}/${SAMPLE_COUNT}\n`);
|
||||
const res = await measureMemory();
|
||||
results.push(res);
|
||||
}
|
||||
|
||||
const summary = summarizeResults(results);
|
||||
// Calculate averages
|
||||
const beforeGc = structuredClone(keys);
|
||||
const afterGc = structuredClone(keys);
|
||||
const afterRequest = structuredClone(keys);
|
||||
for (const res of results) {
|
||||
for (const key of Object.keys(keys)) {
|
||||
beforeGc[key] += res.beforeGc[key];
|
||||
afterGc[key] += res.afterGc[key];
|
||||
afterRequest[key] += res.afterRequest[key];
|
||||
}
|
||||
}
|
||||
for (const key of Object.keys(keys)) {
|
||||
beforeGc[key] = Math.round(beforeGc[key] / SAMPLE_COUNT);
|
||||
afterGc[key] = Math.round(afterGc[key] / SAMPLE_COUNT);
|
||||
afterRequest[key] = Math.round(afterRequest[key] / SAMPLE_COUNT);
|
||||
}
|
||||
|
||||
const result = {
|
||||
timestamp: new Date().toISOString(),
|
||||
sampleCount: SAMPLE_COUNT,
|
||||
aggregation: 'median',
|
||||
...summary,
|
||||
samples: results,
|
||||
beforeGc,
|
||||
afterGc,
|
||||
afterRequest,
|
||||
};
|
||||
|
||||
// Output as JSON to stdout
|
||||
|
||||
6
packages/backend/src/@types/global.d.ts
vendored
Normal file
6
packages/backend/src/@types/global.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
declare const _SUMMALY_VERSION_: string;
|
||||
@@ -6,7 +6,6 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { init } from 'slacc';
|
||||
import { NestLogger } from '@/NestLogger.js';
|
||||
import { envOption } from '@/env.js';
|
||||
import type { Config } from '@/config.js';
|
||||
|
||||
let slaccInitialized = false;
|
||||
@@ -32,7 +31,7 @@ export async function server() {
|
||||
const serverService = app.get(ServerService);
|
||||
await serverService.launch();
|
||||
|
||||
if (process.env.NODE_ENV !== 'test' && !envOption.noDaemons) {
|
||||
if (process.env.NODE_ENV !== 'test') {
|
||||
const { ChartManagementService } = await import('../core/chart/ChartManagementService.js');
|
||||
const { QueueStatsService } = await import('../daemons/QueueStatsService.js');
|
||||
const { ServerStatsService } = await import('../daemons/ServerStatsService.js');
|
||||
@@ -55,9 +54,7 @@ export async function jobQueue() {
|
||||
});
|
||||
|
||||
jobQueue.get(QueueProcessorService).start();
|
||||
if (!envOption.noDaemons) {
|
||||
jobQueue.get(ChartManagementService).start();
|
||||
}
|
||||
jobQueue.get(ChartManagementService).start();
|
||||
|
||||
return jobQueue;
|
||||
}
|
||||
|
||||
@@ -91,20 +91,10 @@ process.on('message', msg => {
|
||||
if (msg === 'gc') {
|
||||
if (global.gc != null) {
|
||||
logger.info('Manual GC triggered');
|
||||
for (let i = 0; i < 3; i++) {
|
||||
global.gc();
|
||||
}
|
||||
global.gc();
|
||||
if (process.send != null) process.send('gc ok');
|
||||
} else {
|
||||
logger.warn('Manual GC requested but gc is not available. Start the process with --expose-gc to enable this feature.');
|
||||
if (process.send != null) process.send('gc unavailable');
|
||||
}
|
||||
} else if (msg === 'memory usage') {
|
||||
if (process.send != null) {
|
||||
process.send({
|
||||
type: 'memory usage',
|
||||
value: process.memoryUsage(),
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -19,6 +19,7 @@ import type { FastifyRequest, FastifyReply } from 'fastify';
|
||||
@Injectable()
|
||||
export class UrlPreviewService {
|
||||
private logger: Logger;
|
||||
private readonly summalyDefaultUserAgent: string;
|
||||
|
||||
constructor(
|
||||
@Inject(DI.config)
|
||||
@@ -31,6 +32,7 @@ export class UrlPreviewService {
|
||||
private loggerService: LoggerService,
|
||||
) {
|
||||
this.logger = this.loggerService.getLogger('url-preview');
|
||||
this.summalyDefaultUserAgent = `SummalyBot/${_SUMMALY_VERSION_} (${this.config.url}; +https://github.com/misskey-dev/summaly/blob/master/README.md)`;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
@@ -126,7 +128,7 @@ export class UrlPreviewService {
|
||||
followRedirects: this.meta.urlPreviewAllowRedirect,
|
||||
lang: lang ?? 'ja-JP',
|
||||
agent: agent,
|
||||
userAgent: meta.urlPreviewUserAgent ?? undefined,
|
||||
userAgent: meta.urlPreviewUserAgent ?? this.summalyDefaultUserAgent,
|
||||
operationTimeout: meta.urlPreviewTimeout,
|
||||
contentLengthLimit: meta.urlPreviewMaximumContentLength,
|
||||
contentLengthRequired: meta.urlPreviewRequireContentLength,
|
||||
@@ -139,7 +141,7 @@ export class UrlPreviewService {
|
||||
url: url,
|
||||
lang: lang ?? 'ja-JP',
|
||||
followRedirects: this.meta.urlPreviewAllowRedirect,
|
||||
userAgent: meta.urlPreviewUserAgent ?? undefined,
|
||||
userAgent: meta.urlPreviewUserAgent ?? this.summalyDefaultUserAgent,
|
||||
operationTimeout: meta.urlPreviewTimeout,
|
||||
contentLengthLimit: meta.urlPreviewMaximumContentLength,
|
||||
contentLengthRequired: meta.urlPreviewRequireContentLength,
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/emoji-assets": "17.0.3",
|
||||
"@misskey-dev/summaly": "5.3.0",
|
||||
"@misskey-dev/summaly": "5.5.1",
|
||||
"@tabler/icons-webfont": "3.35.0",
|
||||
"@testing-library/vue": "8.1.0",
|
||||
"@types/estree": "1.0.9",
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@misskey-dev/emoji-assets": "17.0.3",
|
||||
"@misskey-dev/summaly": "5.3.0",
|
||||
"@misskey-dev/summaly": "5.5.1",
|
||||
"@rollup/plugin-json": "6.1.0",
|
||||
"@rollup/pluginutils": "5.4.0",
|
||||
"@storybook/addon-essentials": "8.6.18",
|
||||
|
||||
615
pnpm-lock.yaml
generated
615
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user