mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-25 15:35:14 +02:00
frontend bundle diagnostics と frontend browser diagnostics を統合 (#17757)
* refactor(gh): unify frontend diagnostics * refactor(gh): unify frontend diagnostics markdown rendering * docs(gh): restore frontend diagnostics comments * wip * fix * refactor: 呼称をbase/headに統一 * tweak * Update types.ts * fix
This commit is contained in:
@@ -1,44 +0,0 @@
|
||||
name: Frontend browser diagnostics (comment)
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Frontend browser diagnostics (inspect)
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
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-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-diagnostics
|
||||
path: ${{ runner.temp }}/frontend-browser-diagnostics
|
||||
github-token: ${{ github.token }}
|
||||
repository: ${{ github.repository }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
- name: Load PR number
|
||||
id: load-pr-number
|
||||
shell: bash
|
||||
run: echo "pr-number=$(cat "$RUNNER_TEMP/frontend-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-diagnostics
|
||||
file-path: ${{ runner.temp }}/frontend-browser-diagnostics/frontend-browser-diagnostics.md
|
||||
@@ -1,44 +0,0 @@
|
||||
name: Frontend bundle diagnostics (comment)
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Frontend bundle diagnostics (inspect)
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
name: Comment frontend bundle report
|
||||
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: frontend-bundle-diagnostics-report-${{ github.event.workflow_run.id }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Download bundle report
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: frontend-bundle-report
|
||||
path: ${{ runner.temp }}/frontend-bundle-report
|
||||
github-token: ${{ github.token }}
|
||||
repository: ${{ github.repository }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
- name: Load PR number
|
||||
id: load-pr-number
|
||||
shell: bash
|
||||
run: echo "pr-number=$(cat "$RUNNER_TEMP/frontend-bundle-report/pr-number.txt")" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Comment on pull request
|
||||
uses: thollander/actions-comment-pull-request@v3
|
||||
with:
|
||||
pr-number: ${{ steps.load-pr-number.outputs.pr-number }}
|
||||
comment-tag: frontend-bundle-diagnostics
|
||||
file-path: ${{ runner.temp }}/frontend-bundle-report/frontend-bundle-diagnostics-report.md
|
||||
@@ -1,148 +0,0 @@
|
||||
name: Frontend bundle diagnostics (inspect)
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
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
|
||||
- packages-private/diagnostics-shared/**
|
||||
- packages-private/diagnostics-frontend-bundle/**
|
||||
- .github/workflows/frontend-bundle-diagnostics.inspect.yml
|
||||
- .github/workflows/frontend-bundle-diagnostics.comment.yml
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: frontend-bundle-diagnostics-inspect-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
report:
|
||||
name: Build frontend bundle report
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout base
|
||||
uses: actions/checkout@v6.0.3
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.base.repo.full_name }}
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
path: before
|
||||
submodules: true
|
||||
|
||||
- name: Checkout pull request
|
||||
uses: actions/checkout@v6.0.3
|
||||
with:
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
path: after
|
||||
submodules: true
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
with:
|
||||
package_json_file: after/package.json
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: after/.node-version
|
||||
cache: pnpm
|
||||
cache-dependency-path: |
|
||||
before/pnpm-lock.yaml
|
||||
after/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies for base
|
||||
working-directory: before
|
||||
run: pnpm i --frozen-lockfile
|
||||
|
||||
- name: Build frontend dependencies for base
|
||||
working-directory: before
|
||||
run: pnpm --filter "frontend^..." run build
|
||||
|
||||
- name: Prepare report output
|
||||
run: mkdir -p "$RUNNER_TEMP/frontend-bundle-report"
|
||||
|
||||
- name: Build frontend report for base
|
||||
working-directory: before
|
||||
env:
|
||||
FRONTEND_BUNDLE_VISUALIZER: 'true'
|
||||
FRONTEND_BUNDLE_VISUALIZER_FILE: ${{ runner.temp }}/frontend-bundle-report/before-stats.json
|
||||
run: pnpm --filter frontend run build
|
||||
|
||||
- name: Install dependencies for pull request
|
||||
working-directory: after
|
||||
run: pnpm i --frozen-lockfile
|
||||
|
||||
- name: Build frontend dependencies for pull request
|
||||
working-directory: after
|
||||
run: pnpm --filter "frontend^..." run build
|
||||
|
||||
- name: Build frontend report for pull request
|
||||
working-directory: after
|
||||
env:
|
||||
FRONTEND_BUNDLE_VISUALIZER: 'true'
|
||||
FRONTEND_BUNDLE_VISUALIZER_FILE: ${{ runner.temp }}/frontend-bundle-report/after-stats.json
|
||||
FRONTEND_BUNDLE_VISUALIZER_HTML_FILE: ${{ runner.temp }}/frontend-bundle-report/frontend-bundle-visualizer.html
|
||||
run: pnpm --filter frontend run build
|
||||
|
||||
- name: Upload bundle visualizer
|
||||
id: upload-bundle-visualizer
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: frontend-bundle-visualizer
|
||||
path: ${{ runner.temp }}/frontend-bundle-report/frontend-bundle-visualizer.html
|
||||
if-no-files-found: error
|
||||
archive: false
|
||||
retention-days: 7
|
||||
|
||||
- name: Generate report markdown
|
||||
shell: bash
|
||||
working-directory: after
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
FRONTEND_BUNDLE_REPORT_ARTIFACT_URL: ${{ steps.upload-bundle-visualizer.outputs.artifact-url }}
|
||||
run: |
|
||||
REPORT_DIR="$RUNNER_TEMP/frontend-bundle-report"
|
||||
pnpm --filter diagnostics-frontend-bundle run render-md \
|
||||
"$GITHUB_WORKSPACE/before" "$GITHUB_WORKSPACE/after" \
|
||||
"$REPORT_DIR/before-stats.json" "$REPORT_DIR/after-stats.json" \
|
||||
"$REPORT_DIR/frontend-bundle-diagnostics-report.md"
|
||||
printf '%s\n' "$PR_NUMBER" > "$REPORT_DIR/pr-number.txt"
|
||||
printf '%s\n' "$BASE_SHA" > "$REPORT_DIR/base-sha.txt"
|
||||
printf '%s\n' "$HEAD_SHA" > "$REPORT_DIR/head-sha.txt"
|
||||
printf '%s\n' "${{ github.event.pull_request.html_url }}" > "$REPORT_DIR/pr-url.txt"
|
||||
|
||||
- name: Check report
|
||||
run: |
|
||||
REPORT_DIR="$RUNNER_TEMP/frontend-bundle-report"
|
||||
test -s "$REPORT_DIR/before-stats.json"
|
||||
test -s "$REPORT_DIR/after-stats.json"
|
||||
test -s "$REPORT_DIR/frontend-bundle-diagnostics-report.md"
|
||||
cat "$REPORT_DIR/frontend-bundle-diagnostics-report.md" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload bundle report
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: frontend-bundle-report
|
||||
path: ${{ runner.temp }}/frontend-bundle-report/
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
75
.github/workflows/frontend-diagnostics.comment.yml
vendored
Normal file
75
.github/workflows/frontend-diagnostics.comment.yml
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
name: Frontend diagnostics (comment)
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows:
|
||||
- Frontend diagnostics (inspect)
|
||||
types:
|
||||
- completed
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
name: Comment frontend diagnostics report
|
||||
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
concurrency:
|
||||
group: frontend-diagnostics-report-${{ github.event.workflow_run.id }}
|
||||
cancel-in-progress: true
|
||||
steps:
|
||||
- name: Download frontend diagnostics report
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: frontend-diagnostics
|
||||
path: ${{ runner.temp }}/frontend-diagnostics
|
||||
github-token: ${{ github.token }}
|
||||
repository: ${{ github.repository }}
|
||||
run-id: ${{ github.event.workflow_run.id }}
|
||||
|
||||
- name: Resolve current pull request
|
||||
id: resolve-pr
|
||||
uses: actions/github-script@v9
|
||||
env:
|
||||
PR_NUMBER_FILE: ${{ runner.temp }}/frontend-diagnostics/pr-number.txt
|
||||
with:
|
||||
script: |
|
||||
const fs = require('fs/promises');
|
||||
const { owner, repo } = context.repo;
|
||||
const workflowRun = context.payload.workflow_run;
|
||||
const pullRequestNumberText = (await fs.readFile(process.env.PR_NUMBER_FILE, 'utf8')).trim();
|
||||
if (!/^[1-9]\d*$/.test(pullRequestNumberText)) {
|
||||
throw new Error('Invalid pull request number in frontend diagnostics artifact.');
|
||||
}
|
||||
|
||||
const pullRequestNumber = Number(pullRequestNumberText);
|
||||
if (!Number.isSafeInteger(pullRequestNumber)) {
|
||||
throw new Error('Pull request number in frontend diagnostics artifact is not a safe integer.');
|
||||
}
|
||||
|
||||
const { data: currentPullRequest } = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: pullRequestNumber,
|
||||
});
|
||||
|
||||
if (currentPullRequest.state !== 'open' || currentPullRequest.head.sha !== workflowRun.head_sha) {
|
||||
core.notice(`Skipping stale frontend diagnostics report for pull request #${pullRequestNumber}.`);
|
||||
core.setOutput('is-current', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
core.setOutput('is-current', 'true');
|
||||
core.setOutput('pr-number', String(pullRequestNumber));
|
||||
|
||||
- name: Comment on pull request
|
||||
if: steps.resolve-pr.outputs.is-current == 'true'
|
||||
uses: thollander/actions-comment-pull-request@v3
|
||||
with:
|
||||
pr-number: ${{ steps.resolve-pr.outputs.pr-number }}
|
||||
comment-tag: frontend-diagnostics
|
||||
file-path: ${{ runner.temp }}/frontend-diagnostics/frontend-diagnostics.md
|
||||
@@ -1,4 +1,4 @@
|
||||
name: Frontend browser diagnostics (inspect)
|
||||
name: Frontend diagnostics (inspect)
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -23,21 +23,21 @@ on:
|
||||
- .node-version
|
||||
- .github/misskey/test.yml
|
||||
- packages-private/diagnostics-shared/**
|
||||
- packages-private/diagnostics-frontend-browser/**
|
||||
- .github/workflows/frontend-browser-diagnostics.inspect.yml
|
||||
- .github/workflows/frontend-browser-diagnostics.comment.yml
|
||||
- packages-private/diagnostics-frontend/**
|
||||
- .github/workflows/frontend-diagnostics.inspect.yml
|
||||
- .github/workflows/frontend-diagnostics.comment.yml
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
concurrency:
|
||||
group: frontend-browser-diagnostics-inspect-${{ github.event.pull_request.number }}
|
||||
group: frontend-diagnostics-inspect-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
report:
|
||||
name: Measure frontend browser metrics
|
||||
name: Measure frontend diagnostics
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
|
||||
@@ -61,79 +61,100 @@ jobs:
|
||||
persist-credentials: false
|
||||
repository: ${{ github.event.pull_request.base.repo.full_name }}
|
||||
ref: ${{ github.event.pull_request.base.sha }}
|
||||
path: before
|
||||
path: base
|
||||
submodules: true
|
||||
|
||||
- name: Checkout pull request
|
||||
- name: Checkout head
|
||||
uses: actions/checkout@v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
path: after
|
||||
path: head
|
||||
submodules: true
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v6.0.9
|
||||
with:
|
||||
package_json_file: after/package.json
|
||||
package_json_file: head/package.json
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: after/.node-version
|
||||
node-version-file: head/.node-version
|
||||
cache: pnpm
|
||||
cache-dependency-path: |
|
||||
before/pnpm-lock.yaml
|
||||
after/pnpm-lock.yaml
|
||||
base/pnpm-lock.yaml
|
||||
head/pnpm-lock.yaml
|
||||
|
||||
- name: Prepare report output
|
||||
shell: bash
|
||||
run: mkdir -p "$RUNNER_TEMP/frontend-diagnostics"
|
||||
|
||||
- name: Install dependencies for base
|
||||
working-directory: before
|
||||
working-directory: base
|
||||
run: pnpm i --frozen-lockfile
|
||||
|
||||
- name: Configure base
|
||||
working-directory: before
|
||||
working-directory: base
|
||||
run: cp .github/misskey/test.yml .config
|
||||
|
||||
- name: Build base
|
||||
working-directory: before
|
||||
working-directory: base
|
||||
env:
|
||||
FRONTEND_BUNDLE_VISUALIZER: 'true'
|
||||
FRONTEND_BUNDLE_VISUALIZER_FILE: ${{ runner.temp }}/frontend-diagnostics/base-bundle-stats.json
|
||||
run: pnpm build
|
||||
|
||||
- name: Install dependencies for pull request
|
||||
working-directory: after
|
||||
- name: Install dependencies for head
|
||||
working-directory: head
|
||||
run: pnpm i --frozen-lockfile
|
||||
|
||||
- name: Configure pull request
|
||||
working-directory: after
|
||||
- name: Configure head
|
||||
working-directory: head
|
||||
run: cp .github/misskey/test.yml .config
|
||||
|
||||
- name: Build pull request
|
||||
working-directory: after
|
||||
- name: Build head
|
||||
working-directory: head
|
||||
env:
|
||||
FRONTEND_BUNDLE_VISUALIZER: 'true'
|
||||
FRONTEND_BUNDLE_VISUALIZER_FILE: ${{ runner.temp }}/frontend-diagnostics/head-bundle-stats.json
|
||||
FRONTEND_BUNDLE_VISUALIZER_HTML_FILE: ${{ runner.temp }}/frontend-diagnostics/frontend-bundle-visualizer.html
|
||||
run: pnpm build
|
||||
|
||||
- name: Upload bundle visualizer
|
||||
id: upload-bundle-visualizer
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: frontend-bundle-visualizer
|
||||
path: ${{ runner.temp }}/frontend-diagnostics/frontend-bundle-visualizer.html
|
||||
if-no-files-found: error
|
||||
archive: false
|
||||
retention-days: 7
|
||||
|
||||
- name: Install Playwright browsers
|
||||
working-directory: after/packages-private/diagnostics-frontend-browser
|
||||
working-directory: head/packages-private/diagnostics-frontend
|
||||
run: pnpm exec playwright install --with-deps --no-shell chromium
|
||||
|
||||
- name: Measure frontend browser metrics
|
||||
shell: bash
|
||||
working-directory: after
|
||||
working-directory: head
|
||||
env:
|
||||
FRONTEND_BROWSER_METRICS_SAMPLE_COUNT: 5
|
||||
MK_ENABLE_CROSS_ORIGIN_ISOLATION: "true"
|
||||
# 計測ハーネスはafter側のものを両方に使うが、成果物はrunnerのworkspace直下に出す
|
||||
# 計測ハーネスはhead側のものを両方に使うが、成果物はrunnerのworkspace直下に出す
|
||||
FRONTEND_BROWSER_HEAP_SNAPSHOT_OUTPUT_DIR: ${{ github.workspace }}
|
||||
run: |
|
||||
REPORT_DIR="$RUNNER_TEMP/frontend-browser-diagnostics"
|
||||
mkdir -p "$REPORT_DIR"
|
||||
pnpm --filter diagnostics-frontend-browser run inspect \
|
||||
"$GITHUB_WORKSPACE/before" "$GITHUB_WORKSPACE/after" \
|
||||
"$REPORT_DIR/before-browser.json" "$REPORT_DIR/after-browser.json"
|
||||
REPORT_DIR="$RUNNER_TEMP/frontend-diagnostics"
|
||||
pnpm --filter diagnostics-frontend run inspect-browser \
|
||||
"$GITHUB_WORKSPACE/base" "$GITHUB_WORKSPACE/head" \
|
||||
"$REPORT_DIR/base-browser.json" "$REPORT_DIR/head-browser.json"
|
||||
|
||||
- name: Upload browser base heap snapshot
|
||||
id: upload-browser-base-heap-snapshot
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: frontend-browser-base-heap-snapshot
|
||||
path: base-heap-snapshot.heapsnapshot
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
@@ -143,6 +164,7 @@ jobs:
|
||||
id: upload-browser-head-heap-snapshot
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: frontend-browser-head-heap-snapshot
|
||||
path: head-heap-snapshot.heapsnapshot
|
||||
archive: false
|
||||
if-no-files-found: error
|
||||
@@ -150,13 +172,13 @@ jobs:
|
||||
|
||||
- name: Generate browser detailed html
|
||||
shell: bash
|
||||
working-directory: after
|
||||
working-directory: head
|
||||
run: |
|
||||
REPORT_DIR="$RUNNER_TEMP/frontend-browser-diagnostics"
|
||||
test -s "$REPORT_DIR/before-browser.json"
|
||||
test -s "$REPORT_DIR/after-browser.json"
|
||||
pnpm --filter diagnostics-frontend-browser run render-html \
|
||||
"$REPORT_DIR/before-browser.json" "$REPORT_DIR/after-browser.json" \
|
||||
REPORT_DIR="$RUNNER_TEMP/frontend-diagnostics"
|
||||
test -s "$REPORT_DIR/base-browser.json"
|
||||
test -s "$REPORT_DIR/head-browser.json"
|
||||
pnpm --filter diagnostics-frontend run render-browser-html \
|
||||
"$REPORT_DIR/base-browser.json" "$REPORT_DIR/head-browser.json" \
|
||||
"$REPORT_DIR/frontend-browser-detailed-html.html"
|
||||
|
||||
- name: Upload browser detailed html
|
||||
@@ -164,54 +186,61 @@ jobs:
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: frontend-browser-metrics-detailed-html
|
||||
path: ${{ runner.temp }}/frontend-browser-diagnostics/frontend-browser-detailed-html.html
|
||||
path: ${{ runner.temp }}/frontend-diagnostics/frontend-browser-detailed-html.html
|
||||
if-no-files-found: error
|
||||
archive: false
|
||||
retention-days: 7
|
||||
|
||||
- name: Generate browser metrics report
|
||||
- name: Generate frontend diagnostics report
|
||||
shell: bash
|
||||
working-directory: after
|
||||
working-directory: head
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
FRONTEND_BUNDLE_REPORT_ARTIFACT_URL: ${{ steps.upload-bundle-visualizer.outputs.artifact-url }}
|
||||
FRONTEND_BROWSER_BASE_HEAP_SNAPSHOT_ARTIFACT_URL: ${{ steps.upload-browser-base-heap-snapshot.outputs.artifact-url }}
|
||||
FRONTEND_BROWSER_HEAD_HEAP_SNAPSHOT_ARTIFACT_URL: ${{ steps.upload-browser-head-heap-snapshot.outputs.artifact-url }}
|
||||
FRONTEND_BROWSER_DETAILED_HTML_ARTIFACT_URL: ${{ steps.upload-browser-detailed-html.outputs.artifact-url }}
|
||||
run: |
|
||||
REPORT_DIR="$RUNNER_TEMP/frontend-browser-diagnostics"
|
||||
test -s "$REPORT_DIR/before-browser.json"
|
||||
test -s "$REPORT_DIR/after-browser.json"
|
||||
pnpm --filter diagnostics-frontend-browser run render-md \
|
||||
"$REPORT_DIR/before-browser.json" "$REPORT_DIR/after-browser.json" \
|
||||
"$REPORT_DIR/frontend-browser-diagnostics.md"
|
||||
REPORT_DIR="$RUNNER_TEMP/frontend-diagnostics"
|
||||
test -s "$REPORT_DIR/base-bundle-stats.json"
|
||||
test -s "$REPORT_DIR/head-bundle-stats.json"
|
||||
test -s "$REPORT_DIR/base-browser.json"
|
||||
test -s "$REPORT_DIR/head-browser.json"
|
||||
pnpm --filter diagnostics-frontend run render-md \
|
||||
"$GITHUB_WORKSPACE/base" "$GITHUB_WORKSPACE/head" \
|
||||
"$REPORT_DIR/base-bundle-stats.json" "$REPORT_DIR/head-bundle-stats.json" \
|
||||
"$REPORT_DIR/base-browser.json" "$REPORT_DIR/head-browser.json" \
|
||||
"$REPORT_DIR/frontend-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"
|
||||
printf '%s\n' "${{ github.event.pull_request.html_url }}" > "$REPORT_DIR/pr-url.txt"
|
||||
|
||||
- name: Check browser metrics report
|
||||
- name: Check frontend diagnostics report
|
||||
shell: bash
|
||||
run: |
|
||||
REPORT_DIR="$RUNNER_TEMP/frontend-browser-diagnostics"
|
||||
test -s "$REPORT_DIR/frontend-browser-diagnostics.md"
|
||||
REPORT_DIR="$RUNNER_TEMP/frontend-diagnostics"
|
||||
test -s "$REPORT_DIR/frontend-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-diagnostics.md" >> "$GITHUB_STEP_SUMMARY"
|
||||
cat "$REPORT_DIR/frontend-diagnostics.md" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload browser metrics report
|
||||
- name: Upload frontend diagnostics report
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: frontend-browser-diagnostics
|
||||
name: frontend-diagnostics
|
||||
path: |
|
||||
${{ 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
|
||||
${{ runner.temp }}/frontend-diagnostics/base-bundle-stats.json
|
||||
${{ runner.temp }}/frontend-diagnostics/head-bundle-stats.json
|
||||
${{ runner.temp }}/frontend-diagnostics/base-browser.json
|
||||
${{ runner.temp }}/frontend-diagnostics/head-browser.json
|
||||
${{ runner.temp }}/frontend-diagnostics/frontend-diagnostics.md
|
||||
${{ runner.temp }}/frontend-diagnostics/pr-number.txt
|
||||
${{ runner.temp }}/frontend-diagnostics/base-sha.txt
|
||||
${{ runner.temp }}/frontend-diagnostics/head-sha.txt
|
||||
${{ runner.temp }}/frontend-diagnostics/pr-url.txt
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
3
.github/workflows/packages-private.yml
vendored
3
.github/workflows/packages-private.yml
vendored
@@ -33,8 +33,7 @@ jobs:
|
||||
workspace:
|
||||
- diagnostics-shared
|
||||
- diagnostics-backend
|
||||
- diagnostics-frontend-browser
|
||||
- diagnostics-frontend-bundle
|
||||
- diagnostics-frontend
|
||||
- changelog-checker
|
||||
steps:
|
||||
- uses: actions/checkout@v6.0.3
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
| --- | --- |
|
||||
| [`diagnostics-shared`](./diagnostics-shared) | 計測系パッケージが共有する統計・書式整形・V8 heap snapshot解析のユーティリティ |
|
||||
| [`diagnostics-backend`](./diagnostics-backend) | バックエンドのメモリ使用量をbase/headで比較し、PRコメント用のMarkdownを生成する |
|
||||
| [`diagnostics-frontend-browser`](./diagnostics-frontend-browser) | ヘッドレスChromeでフロントエンドを操作し、メモリ・ネットワーク等の指標をbase/headで比較する |
|
||||
| [`diagnostics-frontend-bundle`](./diagnostics-frontend-bundle) | フロントエンドのビルド成果物のchunkサイズをbase/headで比較する |
|
||||
| [`diagnostics-frontend`](./diagnostics-frontend) | フロントエンドをブラウザで起動した際のメトリクスと生成されたchunk情報等をbase/headで比較する |
|
||||
| [`changelog-checker`](./changelog-checker) | `CHANGELOG.md` の追記内容を検証する |
|
||||
|
||||
## GitHub Actionsからの呼び出し方
|
||||
@@ -20,11 +19,12 @@ CLIに渡すパスはすべて呼び出し側のカレントディレクトリ
|
||||
|
||||
```yaml
|
||||
- name: Generate report
|
||||
working-directory: after
|
||||
working-directory: head
|
||||
run: >-
|
||||
pnpm --filter diagnostics-frontend-bundle run render-md
|
||||
"$GITHUB_WORKSPACE/before" "$GITHUB_WORKSPACE/after"
|
||||
"$REPORT_DIR/before-stats.json" "$REPORT_DIR/after-stats.json"
|
||||
pnpm --filter diagnostics-frontend run render-md
|
||||
"$GITHUB_WORKSPACE/base" "$GITHUB_WORKSPACE/head"
|
||||
"$REPORT_DIR/base-bundle-stats.json" "$REPORT_DIR/head-bundle-stats.json"
|
||||
"$REPORT_DIR/base-browser.json" "$REPORT_DIR/head-browser.json"
|
||||
"$REPORT_DIR/report.md"
|
||||
```
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
import { readOptionalEnv, readRequiredEnv } from 'diagnostics-shared/env';
|
||||
import { renderMarkdown } from './report/markdown';
|
||||
import type { BrowserMetricsReport } from './types';
|
||||
|
||||
async function main() {
|
||||
const [baseFileArg, headFileArg, outputFileArg] = process.argv.slice(2);
|
||||
if (baseFileArg == null || headFileArg == null || outputFileArg == null) {
|
||||
throw new Error('Usage: render-md <baseJson> <headJson> <outMd>');
|
||||
}
|
||||
|
||||
const base = JSON.parse(await readFile(resolve(baseFileArg), 'utf8')) as BrowserMetricsReport;
|
||||
const head = JSON.parse(await readFile(resolve(headFileArg), 'utf8')) as BrowserMetricsReport;
|
||||
|
||||
await writeFile(resolve(outputFileArg), renderMarkdown(base, head, {
|
||||
baseHeapSnapshotUrl: readRequiredEnv('FRONTEND_BROWSER_BASE_HEAP_SNAPSHOT_ARTIFACT_URL'),
|
||||
headHeapSnapshotUrl: readRequiredEnv('FRONTEND_BROWSER_HEAD_HEAP_SNAPSHOT_ARTIFACT_URL'),
|
||||
detailedHtmlUrl: readOptionalEnv('FRONTEND_BROWSER_DETAILED_HTML_ARTIFACT_URL'),
|
||||
}));
|
||||
}
|
||||
|
||||
await main().catch(err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,69 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
|
||||
import { renderHtml } from '../src/report/html';
|
||||
import { renderMarkdown } from '../src/report/markdown';
|
||||
import type { BrowserMetricsReport } from '../src/types';
|
||||
|
||||
const fixturesDir = join(import.meta.dirname, 'fixtures');
|
||||
|
||||
async function loadFixture(name: string) {
|
||||
return JSON.parse(await readFile(join(fixturesDir, `${name}.json`), 'utf8')) as BrowserMetricsReport;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
// renderHtml() が生成時刻を埋め込むので、スナップショットが揺れないよう時刻を固定する
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-07-18T00:00:00.000Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
/**
|
||||
* 出力をゴールデンファイルで固定する。
|
||||
* 意図的に変更したときは `vitest -u` で更新し、__snapshots__ の差分もレビューすること。
|
||||
*/
|
||||
test('renders the browser diagnostics markdown report', async () => {
|
||||
const markdown = renderMarkdown(await loadFixture('base'), await loadFixture('head'), {
|
||||
baseHeapSnapshotUrl: 'https://example.invalid/base',
|
||||
headHeapSnapshotUrl: 'https://example.invalid/head',
|
||||
detailedHtmlUrl: 'https://example.invalid/html',
|
||||
});
|
||||
|
||||
await expect(markdown).toMatchFileSnapshot('./__snapshots__/render-md.md');
|
||||
});
|
||||
|
||||
test('omits the details link when no detailed html artifact was uploaded', async () => {
|
||||
const markdown = renderMarkdown(await loadFixture('base'), await loadFixture('head'), {
|
||||
baseHeapSnapshotUrl: 'https://example.invalid/base',
|
||||
headHeapSnapshotUrl: 'https://example.invalid/head',
|
||||
detailedHtmlUrl: null,
|
||||
});
|
||||
|
||||
expect(markdown).not.toContain('View details');
|
||||
});
|
||||
|
||||
test('renders the network request diff html report', async () => {
|
||||
const html = renderHtml(await loadFixture('base'), await loadFixture('head'));
|
||||
|
||||
await expect(html).toMatchFileSnapshot('./__snapshots__/render-html.html');
|
||||
});
|
||||
|
||||
test('escapes html metacharacters coming from the browser session', async () => {
|
||||
const base = await loadFixture('base');
|
||||
const head = await loadFixture('head');
|
||||
// CDP由来の値は原理的には任意の文字列になりうるので、生HTMLとして出ないことを確かめる
|
||||
head.samples[0].networkRequests[0].url = 'http://127.0.0.1:61812/"><script>alert(1)</script>';
|
||||
|
||||
const html = renderHtml(base, head);
|
||||
|
||||
expect(html).not.toContain('<script>alert(1)</script>');
|
||||
expect(html).toContain('<script>alert(1)</script>');
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
import sharedConfig from '../../packages/shared/eslint.config.js';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default [
|
||||
...sharedConfig,
|
||||
{
|
||||
ignores: [
|
||||
'**/node_modules',
|
||||
'**/__snapshots__',
|
||||
'test/fixtures',
|
||||
],
|
||||
},
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
parser: tsParser,
|
||||
project: ['./tsconfig.json'],
|
||||
sourceType: 'module',
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -1,22 +0,0 @@
|
||||
{
|
||||
"name": "diagnostics-frontend-bundle",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"eslint": "eslint './**/*.{js,jsx,ts,tsx}'",
|
||||
"lint": "pnpm typecheck && pnpm eslint",
|
||||
"render-md": "tsx src/render-md.ts",
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"diagnostics-shared": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "26.1.1",
|
||||
"tsx": "4.23.1",
|
||||
"typescript": "5.9.3",
|
||||
"vite": "8.1.4",
|
||||
"vitest": "4.1.10"
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { promises as fs } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { readRequiredEnv } from 'diagnostics-shared/env';
|
||||
import { collectReport } from './manifest';
|
||||
import { renderBundleReportMarkdown } from './report';
|
||||
import type { VisualizerReport } from './visualizer';
|
||||
|
||||
async function main() {
|
||||
const [beforeDir, afterDir, beforeStatsFile, afterStatsFile, outFile] = process.argv.slice(2).map(arg => path.resolve(arg));
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
if (outFile == null) throw new Error('Usage: render-md <beforeDir> <afterDir> <beforeStatsJson> <afterStatsJson> <outMd>');
|
||||
|
||||
// 未設定のまま `undefined` という文字列をコメントに埋め込まないよう、ここで落とす
|
||||
const visualizerArtifactUrl = readRequiredEnv('FRONTEND_BUNDLE_REPORT_ARTIFACT_URL');
|
||||
|
||||
const before = await collectReport(beforeDir);
|
||||
const after = await collectReport(afterDir);
|
||||
const beforeStats = JSON.parse(await fs.readFile(beforeStatsFile, 'utf8')) as VisualizerReport;
|
||||
const afterStats = JSON.parse(await fs.readFile(afterStatsFile, 'utf8')) as VisualizerReport;
|
||||
|
||||
await fs.writeFile(outFile, renderBundleReportMarkdown(before, after, beforeStats, afterStats, { visualizerArtifactUrl }));
|
||||
}
|
||||
|
||||
await main().catch(err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,33 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { renderFrontendChunkReport } from './chunk-report';
|
||||
import { collectVisualizerReport, renderVisualizerSummaryTable, type VisualizerReport } from './visualizer';
|
||||
import type { CollectedReport } from './manifest';
|
||||
|
||||
export type RenderBundleReportOptions = {
|
||||
/** rollup-plugin-visualizer が出力したtreemap HTMLのartifact URL */
|
||||
visualizerArtifactUrl: string;
|
||||
};
|
||||
|
||||
export function renderBundleReportMarkdown(
|
||||
before: CollectedReport,
|
||||
after: CollectedReport,
|
||||
beforeStats: VisualizerReport,
|
||||
afterStats: VisualizerReport,
|
||||
options: RenderBundleReportOptions,
|
||||
) {
|
||||
return [
|
||||
'## 📦 Frontend Bundle Report',
|
||||
'',
|
||||
renderFrontendChunkReport(before, after),
|
||||
'',
|
||||
'## Bundle Stats',
|
||||
'',
|
||||
renderVisualizerSummaryTable(collectVisualizerReport(beforeStats), collectVisualizerReport(afterStats)),
|
||||
'',
|
||||
`[Open treemap HTML](${options.visualizerArtifactUrl})`,
|
||||
].join('\n');
|
||||
}
|
||||
@@ -1,100 +0,0 @@
|
||||
## 📦 Frontend Bundle Report
|
||||
|
||||
<details>
|
||||
<summary>Chunk size diff (2 updated, 0 added, 0 removed)</summary>
|
||||
|
||||
| Chunk | Before | After | Δ | Δ (%) |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| (total) | 120 KB | 127 KB | $\color{orange}{\text{+6.3 KB}}$ | $\color{orange}{\text{+5.2\\%}}$ |
|
||||
| | | | | |
|
||||
| <details><summary>`vue`</summary> `assets/vue-b2.js` </details> | 90 KB | 96 KB | $\color{orange}{\text{+6 KB}}$ | $\color{orange}{\text{+6.7\\%}}$ |
|
||||
| (other generated chunks) | 1.2 KB | 1.5 KB | $\text{+300 B}$ | $\color{orange}{\text{+25\\%}}$ |
|
||||
| (other) | 20 KB | 20 KB | $\text{+3 B}$ | $\text{+0\\%}$ |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Startup chunk size (2 updated, 0 added, 0 removed)</summary>
|
||||
|
||||
| Chunk | Before | After | Δ | Δ (%) |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| (total) | 114 KB | 120 KB | $\color{orange}{\text{+6 KB}}$ | $\color{orange}{\text{+5.3\\%}}$ |
|
||||
| | | | | |
|
||||
| <details><summary>`vue`</summary> `assets/vue-b2.js` </details> | 90 KB | 96 KB | $\color{orange}{\text{+6 KB}}$ | $\color{orange}{\text{+6.7\\%}}$ |
|
||||
| (other) | 24 KB | 24 KB | $\text{+3 B}$ | $\text{+0\\%}$ |
|
||||
|
||||
_Startup chunks are the Vite entry for `src/_boot_.ts` and its static imports._
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
## Bundle Stats
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2"></th>
|
||||
<th rowspan="2">Bundles</th>
|
||||
<th rowspan="2">Modules</th>
|
||||
<th rowspan="2">Entries</th>
|
||||
<th colspan="2">Imports</th>
|
||||
<th colspan="3">Size</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Static</th>
|
||||
<th>Dynamic</th>
|
||||
<th>Rendered</th>
|
||||
<th>Gzip</th>
|
||||
<th>Brotli</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th><b>Before</b></th>
|
||||
<td>2</td>
|
||||
<td>6</td>
|
||||
<td>1</td>
|
||||
<td>2</td>
|
||||
<td>3</td>
|
||||
<td>21 KB</td>
|
||||
<td>6.3 KB</td>
|
||||
<td>5.3 KB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><b>After</b></th>
|
||||
<td>2</td>
|
||||
<td>7</td>
|
||||
<td>1</td>
|
||||
<td>3</td>
|
||||
<td>3</td>
|
||||
<td>31 KB</td>
|
||||
<td>8.4 KB</td>
|
||||
<td>7 KB</td>
|
||||
</tr>
|
||||
<tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr>
|
||||
<th><b>Δ</b></th>
|
||||
<td>0</td>
|
||||
<td>$\color{orange}{\text{+1}}$</td>
|
||||
<td>0</td>
|
||||
<td>$\color{orange}{\text{+1}}$</td>
|
||||
<td>0</td>
|
||||
<td>$\color{orange}{\text{+9.8 KB}}$</td>
|
||||
<td>$\color{orange}{\text{+2.1 KB}}$</td>
|
||||
<td>$\color{orange}{\text{+1.8 KB}}$</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><b>Δ (%)</b></th>
|
||||
<td>0%</td>
|
||||
<td>$\color{orange}{\text{+16.7\%}}$</td>
|
||||
<td>0%</td>
|
||||
<td>$\color{orange}{\text{+50\%}}$</td>
|
||||
<td>0%</td>
|
||||
<td>$\color{orange}{\text{+46.7\%}}$</td>
|
||||
<td>$\color{orange}{\text{+33.3\%}}$</td>
|
||||
<td>$\color{orange}{\text{+33.3\%}}$</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
[Open treemap HTML](https://example.invalid/treemap)
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictNullChecks": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"test/**/*.ts"
|
||||
],
|
||||
"exclude": []
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "diagnostics-frontend-browser",
|
||||
"name": "diagnostics-frontend",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"eslint": "eslint './**/*.{js,jsx,ts,tsx}'",
|
||||
"inspect": "tsx src/inspect.ts",
|
||||
"inspect-browser": "tsx src/inspect-browser.ts",
|
||||
"lint": "pnpm typecheck && pnpm eslint",
|
||||
"render-html": "tsx src/render-html.ts",
|
||||
"render-browser-html": "tsx src/render-browser-html.ts",
|
||||
"render-md": "tsx src/render-md.ts",
|
||||
"test": "vitest run",
|
||||
"typecheck": "tsc --noEmit"
|
||||
@@ -5,9 +5,9 @@
|
||||
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import { chromium } from 'playwright';
|
||||
import type { Browser, BrowserContext, CDPSession, Page } from 'playwright';
|
||||
import { enableNetworkTracking, type NetworkTracker } from './network';
|
||||
import type { BrowserDiagnostics, BrowserMeasurement, NetworkRequest, TabMemory, WebSocketConnection } from '../types';
|
||||
import type { Browser, BrowserContext, CDPSession, Page } from 'playwright';
|
||||
import type { BrowserDiagnostics, BrowserMeasurement, NetworkRequest, TabMemory, WebSocketConnection } from './types';
|
||||
|
||||
export type HeadlessChromeOptions = {
|
||||
scenarioTimeoutMs: number;
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { median } from 'diagnostics-shared/stats';
|
||||
import type { BrowserDiagnostics } from '../types';
|
||||
import type { BrowserDiagnostics } from './types';
|
||||
|
||||
export function summarizeBrowserDiagnostics(samples: BrowserDiagnostics[]): BrowserDiagnostics {
|
||||
const medianOf = (select: (sample: BrowserDiagnostics) => number) => median(samples.map(select));
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import type { CDPSession } from 'playwright';
|
||||
import type { NetworkRequest, NetworkSummary, WebSocketConnection } from '../types';
|
||||
import type { NetworkRequest, NetworkSummary, WebSocketConnection } from './types';
|
||||
|
||||
function normalizeHeaders(headers: Record<string, unknown> | undefined) {
|
||||
if (headers == null) return undefined;
|
||||
@@ -224,7 +224,7 @@ function renderRound(round: number, diffs: RequestDiff[]): Raw {
|
||||
</section>`;
|
||||
}
|
||||
|
||||
export function renderHtml(base: BrowserMetricsReport, head: BrowserMetricsReport) {
|
||||
export function renderBrowserDiagnosticsHtml(base: BrowserMetricsReport, head: BrowserMetricsReport) {
|
||||
const diffs = diffReports(base, head);
|
||||
const rounds = [...new Set(diffs.map(diff => diff.round))].toSorted((a, b) => a - b);
|
||||
const generatedAt = new Date().toISOString();
|
||||
@@ -3,9 +3,9 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { closeUserSetupDialog, postNote, registerUser, resetState, signupThroughUi, visitHome } from '../../../packages/frontend/test/e2e/shared';
|
||||
import { closeUserSetupDialog, postNote, registerUser, resetState, signupThroughUi, visitHome } from '../../../../packages/frontend/test/e2e/shared';
|
||||
import { sleep } from './server';
|
||||
import type { HeadlessChromeController } from './browser/controller';
|
||||
import type { HeadlessChromeController } from './controller';
|
||||
|
||||
export const scenarioDescription = 'fresh browser signup, first timeline note, after the note becomes visible';
|
||||
|
||||
@@ -34,10 +34,15 @@ export function startServer(label: string, repoDir: string) {
|
||||
|
||||
const serverStartupTimeoutMs = 120_000;
|
||||
|
||||
function hasExited(child: ChildProcess) {
|
||||
return child.exitCode != null || child.signalCode != null;
|
||||
}
|
||||
|
||||
export async function waitForServer(baseUrl: string, child: ChildProcess) {
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < serverStartupTimeoutMs) {
|
||||
if (child.exitCode != null) throw new Error(`Misskey server exited early with code ${child.exitCode}`);
|
||||
if (child.signalCode != null) throw new Error(`Misskey server exited early with signal ${child.signalCode}`);
|
||||
try {
|
||||
// 応答が返らないままだとfetchが待ち続け、外側の120秒の上限を超えてしまう
|
||||
const remainingMs = serverStartupTimeoutMs - (Date.now() - startedAt);
|
||||
@@ -55,7 +60,7 @@ export async function waitForServer(baseUrl: string, child: ChildProcess) {
|
||||
}
|
||||
|
||||
export async function stopServer(child: ChildProcess) {
|
||||
if (child.exitCode != null) return;
|
||||
if (hasExited(child)) return;
|
||||
|
||||
if (process.platform === 'win32') {
|
||||
spawnSync('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore' });
|
||||
@@ -69,14 +74,14 @@ export async function stopServer(child: ChildProcess) {
|
||||
}
|
||||
|
||||
await new Promise<void>(resolvePromise => {
|
||||
if (child.exitCode != null) {
|
||||
if (hasExited(child)) {
|
||||
resolvePromise();
|
||||
return;
|
||||
}
|
||||
|
||||
const forceKillTimer = setTimeout(() => {
|
||||
// 猶予の間に終了していれば、PIDが再利用されて無関係のプロセスを撃つ恐れがある
|
||||
if (child.exitCode == null && child.pid != null) {
|
||||
if (!hasExited(child) && child.pid != null) {
|
||||
try {
|
||||
if (process.platform === 'win32') {
|
||||
spawnSync('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore' });
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { finiteMedian } from 'diagnostics-shared/stats';
|
||||
import { summarizeHeapSnapshotDataSamples } from 'diagnostics-shared/heap-snapshot';
|
||||
import { summarizeBrowserDiagnostics } from './browser/diagnostics';
|
||||
import { summarizeBrowserDiagnostics } from './diagnostics';
|
||||
import type { BrowserMeasurement, BrowserMeasurementSample, BrowserMetricsReport, NetworkSummary } from './types';
|
||||
|
||||
export type SummarizeOptions = {
|
||||
@@ -5,6 +5,11 @@
|
||||
|
||||
import type { HeapSnapshotData } from 'diagnostics-shared/heap-snapshot';
|
||||
|
||||
// NOTE: (AI向け)
|
||||
// NetworkRequest がリクエスト本文と要求・応答ヘッダを保持し、APIキーやパスワード、Cookieなどの認証情報も含まれ得るが、
|
||||
// workflow上で作られるMisskeyアカウントはテスト用アカウントであり、誰かが実際に使用するプライベートなアカウントではないため、
|
||||
// それらの情報が第三者から閲覧できても何の問題もなく、脆弱性ではない。
|
||||
|
||||
export type NetworkRequest = {
|
||||
requestId: string;
|
||||
url: string;
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
escapeMdTableCell,
|
||||
formatBytes,
|
||||
} from 'diagnostics-shared/format';
|
||||
import type { CollectedReport, FileEntry } from './manifest';
|
||||
import type { CollectedBundleReport, FileEntry } from './manifest';
|
||||
|
||||
/**
|
||||
* この差分以下のチャンクは個別に出さず `(other)` にまとめる。
|
||||
@@ -25,21 +25,21 @@ function entryDisplayName(entry: FileEntry | undefined) {
|
||||
return entry.displayName || entry.file;
|
||||
}
|
||||
|
||||
export function getChunkComparisonRows(keys: string[], before: Partial<Record<string, FileEntry>>, after: Partial<Record<string, FileEntry>>) {
|
||||
export function getChunkComparisonRows(keys: string[], base: Partial<Record<string, FileEntry>>, head: Partial<Record<string, FileEntry>>) {
|
||||
return keys.map(key => {
|
||||
const beforeEntry = before[key];
|
||||
const afterEntry = after[key];
|
||||
const beforeSize = beforeEntry?.size ?? 0;
|
||||
const afterSize = afterEntry?.size ?? 0;
|
||||
const baseEntry = base[key];
|
||||
const headEntry = head[key];
|
||||
const baseSize = baseEntry?.size ?? 0;
|
||||
const headSize = headEntry?.size ?? 0;
|
||||
return {
|
||||
key,
|
||||
name: entryDisplayName(beforeEntry ?? afterEntry),
|
||||
beforeFile: beforeEntry?.file,
|
||||
afterFile: afterEntry?.file,
|
||||
beforeSize,
|
||||
afterSize,
|
||||
changeType: beforeEntry == null ? 'added' : afterEntry == null ? 'removed' : beforeSize !== afterSize ? 'updated' : 'unchanged',
|
||||
sortSize: Math.max(beforeSize, afterSize),
|
||||
name: entryDisplayName(baseEntry ?? headEntry),
|
||||
baseFile: baseEntry?.file,
|
||||
headFile: headEntry?.file,
|
||||
baseSize,
|
||||
headSize,
|
||||
changeType: baseEntry == null ? 'added' : headEntry == null ? 'removed' : baseSize !== headSize ? 'updated' : 'unchanged',
|
||||
sortSize: Math.max(baseSize, headSize),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -47,10 +47,10 @@ export function getChunkComparisonRows(keys: string[], before: Partial<Record<st
|
||||
export type ChunkComparisonRow = ReturnType<typeof getChunkComparisonRows>[number];
|
||||
|
||||
export type ChunkAggregate = {
|
||||
beforeSize: number;
|
||||
afterSize: number;
|
||||
beforeCount: number;
|
||||
afterCount: number;
|
||||
baseSize: number;
|
||||
headSize: number;
|
||||
baseCount: number;
|
||||
headCount: number;
|
||||
};
|
||||
|
||||
export function sumChunkSizes(chunks: FileEntry[]) {
|
||||
@@ -58,29 +58,29 @@ export function sumChunkSizes(chunks: FileEntry[]) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 比較キーを持たない (= before/after で対応付けできない) チャンクの合計。
|
||||
* 比較キーを持たない (= base/head で対応付けできない) チャンクの合計。
|
||||
*/
|
||||
export function generatedAggregate(before: FileEntry[], after: FileEntry[]): ChunkAggregate {
|
||||
const beforeGenerated = before.filter(chunk => chunk.comparisonKey == null);
|
||||
const afterGenerated = after.filter(chunk => chunk.comparisonKey == null);
|
||||
export function generatedAggregate(base: FileEntry[], head: FileEntry[]): ChunkAggregate {
|
||||
const baseGenerated = base.filter(chunk => chunk.comparisonKey == null);
|
||||
const headGenerated = head.filter(chunk => chunk.comparisonKey == null);
|
||||
return {
|
||||
beforeSize: sumChunkSizes(beforeGenerated),
|
||||
afterSize: sumChunkSizes(afterGenerated),
|
||||
beforeCount: beforeGenerated.length,
|
||||
afterCount: afterGenerated.length,
|
||||
baseSize: sumChunkSizes(baseGenerated),
|
||||
headSize: sumChunkSizes(headGenerated),
|
||||
baseCount: baseGenerated.length,
|
||||
headCount: headGenerated.length,
|
||||
};
|
||||
}
|
||||
|
||||
export function hasSmallDelta(row: ChunkComparisonRow) {
|
||||
return Math.abs(row.afterSize - row.beforeSize) <= smallDeltaThreshold;
|
||||
return Math.abs(row.headSize - row.baseSize) <= smallDeltaThreshold;
|
||||
}
|
||||
|
||||
export function comparisonRowsAggregate(rows: ChunkComparisonRow[]): ChunkAggregate {
|
||||
return {
|
||||
beforeSize: rows.reduce((sum, row) => sum + row.beforeSize, 0),
|
||||
afterSize: rows.reduce((sum, row) => sum + row.afterSize, 0),
|
||||
beforeCount: rows.filter(row => row.beforeFile != null).length,
|
||||
afterCount: rows.filter(row => row.afterFile != null).length,
|
||||
baseSize: rows.reduce((sum, row) => sum + row.baseSize, 0),
|
||||
headSize: rows.reduce((sum, row) => sum + row.headSize, 0),
|
||||
baseCount: rows.filter(row => row.baseFile != null).length,
|
||||
headCount: rows.filter(row => row.headFile != null).length,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -108,69 +108,69 @@ export function formatChunkChangeSummary(label: string, summary: ReturnType<type
|
||||
* 差分の絶対値が大きい順。同着は増加側・元サイズ・名前の順で決定的に並べる。
|
||||
*/
|
||||
export function compareChunkComparisonRows(a: ChunkComparisonRow, b: ChunkComparisonRow) {
|
||||
return Math.abs(b.afterSize - b.beforeSize) - Math.abs(a.afterSize - a.beforeSize)
|
||||
|| (b.afterSize - b.beforeSize) - (a.afterSize - a.beforeSize)
|
||||
return Math.abs(b.headSize - b.baseSize) - Math.abs(a.headSize - a.baseSize)
|
||||
|| (b.headSize - b.baseSize) - (a.headSize - a.baseSize)
|
||||
|| b.sortSize - a.sortSize
|
||||
|| a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
export function chunkFileDisplay(row: ChunkComparisonRow) {
|
||||
if (row.beforeFile == null) return row.afterFile ?? '';
|
||||
if (row.afterFile == null || row.beforeFile === row.afterFile) return row.beforeFile;
|
||||
return `${row.beforeFile} → ${row.afterFile}`;
|
||||
if (row.baseFile == null) return row.headFile ?? '';
|
||||
if (row.headFile == null || row.baseFile === row.headFile) return row.baseFile;
|
||||
return `${row.baseFile} → ${row.headFile}`;
|
||||
}
|
||||
|
||||
export function chunkMarkdownTable(
|
||||
rows: ChunkComparisonRow[],
|
||||
total?: { beforeSize: number; afterSize: number },
|
||||
total?: { baseSize: number; headSize: number },
|
||||
generated?: ChunkAggregate,
|
||||
other?: ChunkAggregate,
|
||||
) {
|
||||
const hasGenerated = generated != null && (generated.beforeCount > 0 || generated.afterCount > 0);
|
||||
const hasOther = other != null && (other.beforeCount > 0 || other.afterCount > 0);
|
||||
const hasGenerated = generated != null && (generated.baseCount > 0 || generated.headCount > 0);
|
||||
const hasOther = other != null && (other.baseCount > 0 || other.headCount > 0);
|
||||
if (rows.length === 0 && total == null && !hasGenerated && !hasOther) return '_No data_';
|
||||
|
||||
const lines = [
|
||||
'| Chunk | Before | After | Δ | Δ (%) |',
|
||||
'| Chunk | Base | Head | Δ | Δ (%) |',
|
||||
'| --- | ---: | ---: | ---: | ---: |',
|
||||
];
|
||||
if (total != null) {
|
||||
lines.push(`| (total) | ${formatBytes(total.beforeSize)} | ${formatBytes(total.afterSize)} | ${calcAndFormatDeltaBytes(total.beforeSize, total.afterSize, 1000)} | ${calcAndFormatDeltaPercentInMdTable(total.beforeSize, total.afterSize, 0.1)} |`);
|
||||
lines.push(`| (total) | ${formatBytes(total.baseSize)} | ${formatBytes(total.headSize)} | ${calcAndFormatDeltaBytes(total.baseSize, total.headSize, 1000)} | ${calcAndFormatDeltaPercentInMdTable(total.baseSize, total.headSize, 0.1)} |`);
|
||||
lines.push('| | | | | |');
|
||||
}
|
||||
|
||||
for (const row of rows) {
|
||||
const chunkFile = chunkFileDisplay(row);
|
||||
if (row.changeType === 'added') {
|
||||
lines.push(`| <details><summary>\`${escapeMdTableCell(row.name)}\`</summary> \`${escapeMdTableCell(chunkFile)}\` </details> | ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${calcAndFormatDeltaBytes(row.beforeSize, row.afterSize, 1000)} | $\\color{orange}{\\text{( + )}}$ |`);
|
||||
lines.push(`| <details><summary>\`${escapeMdTableCell(row.name)}\`</summary> \`${escapeMdTableCell(chunkFile)}\` </details> | ${formatBytes(row.baseSize)} | ${formatBytes(row.headSize)} | ${calcAndFormatDeltaBytes(row.baseSize, row.headSize, 1000)} | $\\color{orange}{\\text{( + )}}$ |`);
|
||||
} else if (row.changeType === 'removed') {
|
||||
lines.push(`| <details><summary>\`${escapeMdTableCell(row.name)}\`</summary> \`${escapeMdTableCell(chunkFile)}\` </details> | ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${calcAndFormatDeltaBytes(row.beforeSize, row.afterSize, 1000)} | $\\color{green}{\\text{( - )}}$ |`);
|
||||
lines.push(`| <details><summary>\`${escapeMdTableCell(row.name)}\`</summary> \`${escapeMdTableCell(chunkFile)}\` </details> | ${formatBytes(row.baseSize)} | ${formatBytes(row.headSize)} | ${calcAndFormatDeltaBytes(row.baseSize, row.headSize, 1000)} | $\\color{green}{\\text{( - )}}$ |`);
|
||||
} else {
|
||||
lines.push(`| <details><summary>\`${escapeMdTableCell(row.name)}\`</summary> \`${escapeMdTableCell(chunkFile)}\` </details> | ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${calcAndFormatDeltaBytes(row.beforeSize, row.afterSize, 1000)} | ${calcAndFormatDeltaPercentInMdTable(row.beforeSize, row.afterSize, 0.1)} |`);
|
||||
lines.push(`| <details><summary>\`${escapeMdTableCell(row.name)}\`</summary> \`${escapeMdTableCell(chunkFile)}\` </details> | ${formatBytes(row.baseSize)} | ${formatBytes(row.headSize)} | ${calcAndFormatDeltaBytes(row.baseSize, row.headSize, 1000)} | ${calcAndFormatDeltaPercentInMdTable(row.baseSize, row.headSize, 0.1)} |`);
|
||||
}
|
||||
}
|
||||
if (hasGenerated) {
|
||||
lines.push(`| (other generated chunks) | ${formatBytes(generated.beforeSize)} | ${formatBytes(generated.afterSize)} | ${calcAndFormatDeltaBytes(generated.beforeSize, generated.afterSize, 1000)} | ${calcAndFormatDeltaPercentInMdTable(generated.beforeSize, generated.afterSize, 0.1)} |`);
|
||||
lines.push(`| (other generated chunks) | ${formatBytes(generated.baseSize)} | ${formatBytes(generated.headSize)} | ${calcAndFormatDeltaBytes(generated.baseSize, generated.headSize, 1000)} | ${calcAndFormatDeltaPercentInMdTable(generated.baseSize, generated.headSize, 0.1)} |`);
|
||||
}
|
||||
if (hasOther) {
|
||||
lines.push(`| (other) | ${formatBytes(other.beforeSize)} | ${formatBytes(other.afterSize)} | ${calcAndFormatDeltaBytes(other.beforeSize, other.afterSize, 1000)} | ${calcAndFormatDeltaPercentInMdTable(other.beforeSize, other.afterSize, 0.1)} |`);
|
||||
lines.push(`| (other) | ${formatBytes(other.baseSize)} | ${formatBytes(other.headSize)} | ${calcAndFormatDeltaBytes(other.baseSize, other.headSize, 1000)} | ${calcAndFormatDeltaPercentInMdTable(other.baseSize, other.headSize, 0.1)} |`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
export function renderFrontendChunkReport(before: CollectedReport, after: CollectedReport) {
|
||||
const beforeComparable = before.comparableChunks;
|
||||
const afterComparable = after.comparableChunks;
|
||||
const allChunkKeys = [...new Set([...Object.keys(beforeComparable), ...Object.keys(afterComparable)])];
|
||||
const allComparisonRows = getChunkComparisonRows(allChunkKeys, beforeComparable, afterComparable);
|
||||
export function renderFrontendChunkReport(base: CollectedBundleReport, head: CollectedBundleReport) {
|
||||
const baseComparable = base.comparableChunks;
|
||||
const headComparable = head.comparableChunks;
|
||||
const allChunkKeys = [...new Set([...Object.keys(baseComparable), ...Object.keys(headComparable)])];
|
||||
const allComparisonRows = getChunkComparisonRows(allChunkKeys, baseComparable, headComparable);
|
||||
|
||||
const changedRows = allComparisonRows.filter((row) => row.changeType !== 'unchanged');
|
||||
const diffSummary = summarizeChunkChanges(changedRows);
|
||||
const diffTotal = {
|
||||
beforeSize: sumChunkSizes(before.chunks),
|
||||
afterSize: sumChunkSizes(after.chunks),
|
||||
baseSize: sumChunkSizes(base.chunks),
|
||||
headSize: sumChunkSizes(head.chunks),
|
||||
};
|
||||
const diffGenerated = generatedAggregate(before.chunks, after.chunks);
|
||||
const diffGenerated = generatedAggregate(base.chunks, head.chunks);
|
||||
const largeDeltaRows = changedRows.filter(row => !hasSmallDelta(row)).sort(compareChunkComparisonRows);
|
||||
const diffRows = largeDeltaRows.slice(0, diffRowLimit);
|
||||
// 表示上限で切り捨てた行も `(other)` に含める。落とすと合計が実際の変化量と合わなくなる
|
||||
@@ -179,22 +179,22 @@ export function renderFrontendChunkReport(before: CollectedReport, after: Collec
|
||||
...largeDeltaRows.slice(diffRowLimit),
|
||||
]);
|
||||
|
||||
const beforeStartupFiles = new Set(before.startupFiles);
|
||||
const afterStartupFiles = new Set(after.startupFiles);
|
||||
const beforeStartupChunks = before.chunks.filter(chunk => beforeStartupFiles.has(chunk.file));
|
||||
const afterStartupChunks = after.chunks.filter(chunk => afterStartupFiles.has(chunk.file));
|
||||
const beforeStartupComparable = comparableMap(beforeStartupChunks);
|
||||
const afterStartupComparable = comparableMap(afterStartupChunks);
|
||||
const startupKeys = [...new Set([...Object.keys(beforeStartupComparable), ...Object.keys(afterStartupComparable)])];
|
||||
const startupComparisonRows = getChunkComparisonRows(startupKeys, beforeStartupComparable, afterStartupComparable);
|
||||
const baseStartupFiles = new Set(base.startupFiles);
|
||||
const headStartupFiles = new Set(head.startupFiles);
|
||||
const baseStartupChunks = base.chunks.filter(chunk => baseStartupFiles.has(chunk.file));
|
||||
const headStartupChunks = head.chunks.filter(chunk => headStartupFiles.has(chunk.file));
|
||||
const baseStartupComparable = comparableMap(baseStartupChunks);
|
||||
const headStartupComparable = comparableMap(headStartupChunks);
|
||||
const startupKeys = [...new Set([...Object.keys(baseStartupComparable), ...Object.keys(headStartupComparable)])];
|
||||
const startupComparisonRows = getChunkComparisonRows(startupKeys, baseStartupComparable, headStartupComparable);
|
||||
const startupSummary = summarizeChunkChanges(startupComparisonRows);
|
||||
const startupOther = comparisonRowsAggregate(startupComparisonRows.filter(hasSmallDelta));
|
||||
const startupRows = startupComparisonRows.filter(row => !hasSmallDelta(row)).sort(compareChunkComparisonRows);
|
||||
const startupTotal = {
|
||||
beforeSize: sumChunkSizes(beforeStartupChunks),
|
||||
afterSize: sumChunkSizes(afterStartupChunks),
|
||||
baseSize: sumChunkSizes(baseStartupChunks),
|
||||
headSize: sumChunkSizes(headStartupChunks),
|
||||
};
|
||||
const startupGenerated = generatedAggregate(beforeStartupChunks, afterStartupChunks);
|
||||
const startupGenerated = generatedAggregate(baseStartupChunks, headStartupChunks);
|
||||
|
||||
return [
|
||||
'<details>',
|
||||
@@ -18,8 +18,9 @@ export async function fileExists(filePath: string) {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,16 @@
|
||||
import { promises as fs } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileExists, fileSize, normalizePath, traverseDirectory } from './fs-utils';
|
||||
import type { Manifest, ManifestChunk } from 'vite';
|
||||
|
||||
export type BundleManifestChunk = {
|
||||
file: string;
|
||||
src?: string;
|
||||
name?: string;
|
||||
isEntry?: boolean;
|
||||
imports?: string[];
|
||||
};
|
||||
|
||||
export type BundleManifest = Record<string, BundleManifestChunk>;
|
||||
|
||||
/**
|
||||
* 比較対象とするロケール。ロケール別チャンクは全ロケール分だと数が多すぎるため、
|
||||
@@ -27,15 +36,15 @@ export type FileEntry = {
|
||||
size: number;
|
||||
};
|
||||
|
||||
export type CollectedReport = {
|
||||
manifest: Manifest;
|
||||
export type CollectedBundleReport = {
|
||||
manifest: BundleManifest;
|
||||
chunks: FileEntry[];
|
||||
comparableChunks: Record<string, FileEntry>;
|
||||
chunksByManifestKey: Record<string, FileEntry>;
|
||||
startupFiles: string[];
|
||||
};
|
||||
|
||||
export function findEntryKey(manifest: Manifest) {
|
||||
export function findEntryKey(manifest: BundleManifest) {
|
||||
const entries = Object.entries(manifest);
|
||||
return entries.find(([key, chunk]) => key === 'src/_boot_.ts' || chunk.src === 'src/_boot_.ts')?.[0]
|
||||
?? entries.find(([, chunk]) => chunk.name === 'entry' && chunk.isEntry)?.[0]
|
||||
@@ -45,9 +54,9 @@ export function findEntryKey(manifest: Manifest) {
|
||||
|
||||
/**
|
||||
* ビルド間で安定するチャンク識別子。出力ファイル名はハッシュ付きで毎回変わるため、
|
||||
* これが取れないチャンクは before/after の対応付けができない。
|
||||
* これが取れないチャンクは base/head の対応付けができない。
|
||||
*/
|
||||
export function stableChunkKey(chunk: ManifestChunk) {
|
||||
export function stableChunkKey(chunk: BundleManifestChunk) {
|
||||
if (chunk.src != null) return `src:${normalizePath(chunk.src)}`;
|
||||
if (chunk.name != null && stableNamedChunks.has(chunk.name)) return `named:${chunk.name}`;
|
||||
return null;
|
||||
@@ -56,7 +65,7 @@ export function stableChunkKey(chunk: ManifestChunk) {
|
||||
/**
|
||||
* 起動時に必ず読み込まれるチャンク (entry とその静的 import) の manifest キーを集める。
|
||||
*/
|
||||
export function collectStartupManifestKeys(manifest: Manifest) {
|
||||
export function collectStartupManifestKeys(manifest: BundleManifest) {
|
||||
const entryKey = findEntryKey(manifest);
|
||||
const keys = new Set<string>();
|
||||
if (entryKey == null) throw new Error('Unable to find frontend startup entry in Vite manifest.');
|
||||
@@ -101,10 +110,10 @@ export async function resolveBuiltFile(outDir: string, file: string) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function collectReport(repoDir: string): Promise<CollectedReport> {
|
||||
export async function collectBundleReport(repoDir: string): Promise<CollectedBundleReport> {
|
||||
const outDir = path.join(repoDir, 'built/_frontend_vite_');
|
||||
const manifestPath = path.join(outDir, 'manifest.json');
|
||||
const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8')) as Manifest;
|
||||
const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8')) as BundleManifest;
|
||||
const chunksByFile = new Map<string, FileEntry>();
|
||||
const comparableChunks = new Map<string, FileEntry>();
|
||||
const chunksByManifestKey = new Map<string, FileEntry>();
|
||||
@@ -141,7 +141,7 @@ export function collectVisualizerReport(data: VisualizerReport) {
|
||||
* 要素として配列のまま埋め込まれていたため、テーブルがカンマ区切りの1行に潰れていた。
|
||||
* 呼び出し側で意識しなくて済むよう、ここで文字列にして返す。
|
||||
*/
|
||||
export function renderVisualizerSummaryTable(before: ReturnType<typeof collectVisualizerReport>, after: ReturnType<typeof collectVisualizerReport>) {
|
||||
export function renderVisualizerSummaryTable(base: ReturnType<typeof collectVisualizerReport>, head: ReturnType<typeof collectVisualizerReport>) {
|
||||
const summary = [
|
||||
'bundles',
|
||||
'modules',
|
||||
@@ -178,25 +178,25 @@ export function renderVisualizerSummaryTable(before: ReturnType<typeof collectVi
|
||||
'</thead>',
|
||||
'<tbody>',
|
||||
'<tr>',
|
||||
'<th><b>Before</b></th>',
|
||||
...summary.map((key) => `<td>${formatNumber(before.summary[key])}</td>`),
|
||||
...metrics.map((key) => `<td>${formatBytes(before.metrics[key])}</td>`),
|
||||
'<th><b>Base</b></th>',
|
||||
...summary.map((key) => `<td>${formatNumber(base.summary[key])}</td>`),
|
||||
...metrics.map((key) => `<td>${formatBytes(base.metrics[key])}</td>`),
|
||||
'</tr>',
|
||||
'<tr>',
|
||||
'<th><b>After</b></th>',
|
||||
...summary.map((key) => `<td>${formatNumber(after.summary[key])}</td>`),
|
||||
...metrics.map((key) => `<td>${formatBytes(after.metrics[key])}</td>`),
|
||||
'<th><b>Head</b></th>',
|
||||
...summary.map((key) => `<td>${formatNumber(head.summary[key])}</td>`),
|
||||
...metrics.map((key) => `<td>${formatBytes(head.metrics[key])}</td>`),
|
||||
'</tr>',
|
||||
'<tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>',
|
||||
'<tr>',
|
||||
'<th><b>Δ</b></th>',
|
||||
...summary.map((key) => `<td>${calcAndFormatDeltaNumber(before.summary[key], after.summary[key], 0)}</td>`),
|
||||
...metrics.map((key) => `<td>${calcAndFormatDeltaBytes(before.metrics[key], after.metrics[key], 1000)}</td>`),
|
||||
...summary.map((key) => `<td>${calcAndFormatDeltaNumber(base.summary[key], head.summary[key], 0)}</td>`),
|
||||
...metrics.map((key) => `<td>${calcAndFormatDeltaBytes(base.metrics[key], head.metrics[key], 1000)}</td>`),
|
||||
'</tr>',
|
||||
'<tr>',
|
||||
'<th><b>Δ (%)</b></th>',
|
||||
...summary.map((key) => `<td>${calcAndFormatDeltaPercent(before.summary[key], after.summary[key], 0.1)}</td>`),
|
||||
...metrics.map((key) => `<td>${calcAndFormatDeltaPercent(before.metrics[key], after.metrics[key], 0.1)}</td>`),
|
||||
...summary.map((key) => `<td>${calcAndFormatDeltaPercent(base.summary[key], head.summary[key], 0.1)}</td>`),
|
||||
...metrics.map((key) => `<td>${calcAndFormatDeltaPercent(base.metrics[key], head.metrics[key], 0.1)}</td>`),
|
||||
'</tr>',
|
||||
'</tbody>',
|
||||
'</table>',
|
||||
@@ -9,10 +9,10 @@ import { readIntegerEnv, readOptionalEnv } from 'diagnostics-shared/env';
|
||||
import { analyzeHeapSnapshot, defaultHeapSnapshotBreakdownTopN } from 'diagnostics-shared/heap-snapshot';
|
||||
import { HeadlessChromeController } from './browser/controller';
|
||||
import { summarizeNetwork } from './browser/network';
|
||||
import { prepareInstance, runSignupAndPostScenario, scenarioDescription } from './scenario';
|
||||
import { startServer, stopServer, waitForServer } from './server';
|
||||
import { selectRepresentativeSample, summarizeSamples } from './summarize';
|
||||
import type { BrowserMeasurementSample, BrowserMetricsReport } from './types';
|
||||
import { prepareInstance, runSignupAndPostScenario, scenarioDescription } from './browser/scenario';
|
||||
import { startServer, stopServer, waitForServer } from './browser/server';
|
||||
import { selectRepresentativeSample, summarizeSamples } from './browser/summarize';
|
||||
import type { BrowserMeasurementSample, BrowserMetricsReport } from './browser/types';
|
||||
|
||||
type Label = 'base' | 'head';
|
||||
|
||||
@@ -110,7 +110,7 @@ async function genReport(label: Label, repoDir: string, outputPath: string) {
|
||||
async function main() {
|
||||
const [baseDirArg, headDirArg, baseOutputArg, headOutputArg] = process.argv.slice(2);
|
||||
if (baseDirArg == null || headDirArg == null || baseOutputArg == null || headOutputArg == null) {
|
||||
throw new Error('Usage: inspect <baseDir> <headDir> <baseOutputJson> <headOutputJson>');
|
||||
throw new Error('Usage: inspect-browser <baseDir> <headDir> <baseOutputJson> <headOutputJson>');
|
||||
}
|
||||
|
||||
for (const label of labels) {
|
||||
@@ -5,19 +5,19 @@
|
||||
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
import { renderHtml } from './report/html';
|
||||
import type { BrowserMetricsReport } from './types';
|
||||
import { renderBrowserDiagnosticsHtml } from './browser/report/html';
|
||||
import type { BrowserMetricsReport } from './browser/types';
|
||||
|
||||
async function main() {
|
||||
const [baseFileArg, headFileArg, outputFileArg] = process.argv.slice(2);
|
||||
if (baseFileArg == null || headFileArg == null || outputFileArg == null) {
|
||||
throw new Error('Usage: render-html <baseJson> <headJson> <outHtml>');
|
||||
throw new Error('Usage: render-browser-html <baseJson> <headJson> <outHtml>');
|
||||
}
|
||||
|
||||
const base = JSON.parse(await readFile(resolve(baseFileArg), 'utf8')) as BrowserMetricsReport;
|
||||
const head = JSON.parse(await readFile(resolve(headFileArg), 'utf8')) as BrowserMetricsReport;
|
||||
|
||||
await writeFile(resolve(outputFileArg), renderHtml(base, head));
|
||||
await writeFile(resolve(outputFileArg), renderBrowserDiagnosticsHtml(base, head));
|
||||
}
|
||||
|
||||
await main().catch(err => {
|
||||
62
packages-private/diagnostics-frontend/src/render-md.ts
Normal file
62
packages-private/diagnostics-frontend/src/render-md.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { readFile, writeFile } from 'node:fs/promises';
|
||||
import { resolve } from 'node:path';
|
||||
import { readOptionalEnv, readRequiredEnv } from 'diagnostics-shared/env';
|
||||
import { collectBundleReport } from './bundle/manifest';
|
||||
import { renderFrontendDiagnosticsMarkdown } from './report';
|
||||
import type { BrowserMetricsReport } from './browser/types';
|
||||
import type { VisualizerReport } from './bundle/visualizer';
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
if (args.length !== 7) {
|
||||
throw new Error('Usage: render-md <baseDir> <headDir> <baseBundleStatsJson> <headBundleStatsJson> <baseBrowserJson> <headBrowserJson> <outMd>');
|
||||
}
|
||||
const [
|
||||
baseDirArg,
|
||||
headDirArg,
|
||||
baseBundleStatsFileArg,
|
||||
headBundleStatsFileArg,
|
||||
baseBrowserFileArg,
|
||||
headBrowserFileArg,
|
||||
outputFileArg,
|
||||
] = args as [string, string, string, string, string, string, string];
|
||||
|
||||
const [
|
||||
baseBundle,
|
||||
headBundle,
|
||||
baseBundleStatsJson,
|
||||
headBundleStatsJson,
|
||||
baseBrowserJson,
|
||||
headBrowserJson,
|
||||
] = await Promise.all([
|
||||
collectBundleReport(resolve(baseDirArg)),
|
||||
collectBundleReport(resolve(headDirArg)),
|
||||
readFile(resolve(baseBundleStatsFileArg), 'utf8'),
|
||||
readFile(resolve(headBundleStatsFileArg), 'utf8'),
|
||||
readFile(resolve(baseBrowserFileArg), 'utf8'),
|
||||
readFile(resolve(headBrowserFileArg), 'utf8'),
|
||||
]);
|
||||
|
||||
await writeFile(
|
||||
resolve(outputFileArg),
|
||||
renderFrontendDiagnosticsMarkdown({
|
||||
bundle: {
|
||||
base: baseBundle,
|
||||
head: headBundle,
|
||||
baseStats: JSON.parse(baseBundleStatsJson) as VisualizerReport,
|
||||
headStats: JSON.parse(headBundleStatsJson) as VisualizerReport,
|
||||
visualizerArtifactUrl: readRequiredEnv('FRONTEND_BUNDLE_REPORT_ARTIFACT_URL'),
|
||||
},
|
||||
browser: {
|
||||
base: JSON.parse(baseBrowserJson) as BrowserMetricsReport,
|
||||
head: JSON.parse(headBrowserJson) as BrowserMetricsReport,
|
||||
baseHeapSnapshotUrl: readRequiredEnv('FRONTEND_BROWSER_BASE_HEAP_SNAPSHOT_ARTIFACT_URL'),
|
||||
headHeapSnapshotUrl: readRequiredEnv('FRONTEND_BROWSER_HEAD_HEAP_SNAPSHOT_ARTIFACT_URL'),
|
||||
detailedHtmlUrl: readOptionalEnv('FRONTEND_BROWSER_DETAILED_HTML_ARTIFACT_URL'),
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -4,14 +4,29 @@
|
||||
*/
|
||||
|
||||
import { formatBytes, formatColoredDelta, formatNumber } from 'diagnostics-shared/format';
|
||||
import { pairedDeltaSummary, sampleSpread } from 'diagnostics-shared/stats';
|
||||
import { renderHeapSnapshotTable, type HeapSnapshotReport } from 'diagnostics-shared/heap-snapshot';
|
||||
import type { BrowserMeasurement, BrowserMeasurementSample, BrowserMetricsReport } from '../types';
|
||||
import { pairedDeltaSummary } from 'diagnostics-shared/stats';
|
||||
import { renderFrontendChunkReport } from './bundle/chunk-report';
|
||||
import { collectVisualizerReport, renderVisualizerSummaryTable, type VisualizerReport } from './bundle/visualizer';
|
||||
import type { CollectedBundleReport } from './bundle/manifest';
|
||||
import type { BrowserMeasurement, BrowserMeasurementSample, BrowserMetricsReport } from './browser/types';
|
||||
|
||||
export type RenderMarkdownOptions = {
|
||||
baseHeapSnapshotUrl: string;
|
||||
headHeapSnapshotUrl: string;
|
||||
detailedHtmlUrl?: string | null;
|
||||
export type FrontendDiagnosticsMarkdownInput = {
|
||||
bundle: {
|
||||
base: CollectedBundleReport;
|
||||
head: CollectedBundleReport;
|
||||
baseStats: VisualizerReport;
|
||||
headStats: VisualizerReport;
|
||||
/** rollup-plugin-visualizer が出力したtreemap HTMLのartifact URL */
|
||||
visualizerArtifactUrl: string;
|
||||
};
|
||||
browser: {
|
||||
base: BrowserMetricsReport;
|
||||
head: BrowserMetricsReport;
|
||||
baseHeapSnapshotUrl: string;
|
||||
headHeapSnapshotUrl: string;
|
||||
detailedHtmlUrl?: string | null;
|
||||
};
|
||||
};
|
||||
|
||||
function renderMetricRow(
|
||||
@@ -45,7 +60,7 @@ function resourceTypeSampleBytes(sample: BrowserMeasurementSample, resourceTypes
|
||||
return resourceTypeBytes(sample, resourceTypes);
|
||||
}
|
||||
|
||||
function renderSummaryTable(base: BrowserMetricsReport, head: BrowserMetricsReport, all = false) {
|
||||
function renderBrowserSummaryTable(base: BrowserMetricsReport, head: BrowserMetricsReport, all = false) {
|
||||
//function getMetric(report: BrowserMeasurement, key: string) {
|
||||
// return report.performance.cdpMetrics[key];
|
||||
//}
|
||||
@@ -154,13 +169,14 @@ function toHeapSnapshotReport(report: BrowserMetricsReport): HeapSnapshotReport
|
||||
};
|
||||
}
|
||||
|
||||
export function renderMarkdown(base: BrowserMetricsReport, head: BrowserMetricsReport, options: RenderMarkdownOptions) {
|
||||
const detailedHtmlUrl = options.detailedHtmlUrl;
|
||||
const heapSnapshotTable = renderHeapSnapshotTable(toHeapSnapshotReport(base), toHeapSnapshotReport(head));
|
||||
export function renderFrontendDiagnosticsMarkdown(input: FrontendDiagnosticsMarkdownInput) {
|
||||
const { bundle, browser } = input;
|
||||
const detailedHtmlUrl = browser.detailedHtmlUrl;
|
||||
const heapSnapshotTable = renderHeapSnapshotTable(toHeapSnapshotReport(browser.base), toHeapSnapshotReport(browser.head));
|
||||
const lines = [
|
||||
'## 🖥 Frontend Browser Diagnostics Report',
|
||||
'## 🖥 Frontend Diagnostics Report',
|
||||
'',
|
||||
renderSummaryTable(base, head),
|
||||
renderBrowserSummaryTable(browser.base, browser.head),
|
||||
'',
|
||||
'<i>Only metrics showing significant changes are displayed.</i>',
|
||||
'',
|
||||
@@ -169,7 +185,7 @@ export function renderMarkdown(base: BrowserMetricsReport, head: BrowserMetricsR
|
||||
'<details>',
|
||||
'<summary>Requests by resource type</summary>',
|
||||
'',
|
||||
renderResourceTypeTable(base, head),
|
||||
renderResourceTypeTable(browser.base, browser.head),
|
||||
'',
|
||||
'</details>',
|
||||
'',
|
||||
@@ -178,11 +194,19 @@ export function renderMarkdown(base: BrowserMetricsReport, head: BrowserMetricsR
|
||||
'',
|
||||
heapSnapshotTable ?? '_No V8 heap snapshot data._',
|
||||
'',
|
||||
//renderHeapSnapshotSankey(toHeapSnapshotReport(head), 'Head'),
|
||||
//renderHeapSnapshotSankey(toHeapSnapshotReport(browser.head), 'Head'),
|
||||
//'',
|
||||
`Download representative heap snapshot: [base](${options.baseHeapSnapshotUrl}) / [head](${options.headHeapSnapshotUrl})`,
|
||||
`Download representative heap snapshot: [base](${browser.baseHeapSnapshotUrl}) / [head](${browser.headHeapSnapshotUrl})`,
|
||||
'</details>',
|
||||
'',
|
||||
'### 📦 Bundle Stats',
|
||||
'',
|
||||
renderFrontendChunkReport(bundle.base, bundle.head),
|
||||
'',
|
||||
renderVisualizerSummaryTable(collectVisualizerReport(bundle.baseStats), collectVisualizerReport(bundle.headStats)),
|
||||
'',
|
||||
`[Open treemap HTML](${bundle.visualizerArtifactUrl})`,
|
||||
'',
|
||||
];
|
||||
|
||||
return lines.filter(line => line != null).join('\n').trimEnd() + '\n';
|
||||
@@ -1,4 +1,4 @@
|
||||
## 🖥 Frontend Browser Diagnostics Report
|
||||
## 🖥 Frontend Diagnostics Report
|
||||
|
||||
| Metric | Base | Head | Δ median | Δ MAD | Δ min | Δ max |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
@@ -81,3 +81,102 @@
|
||||
|
||||
Download representative heap snapshot: [base](https://example.invalid/base) / [head](https://example.invalid/head)
|
||||
</details>
|
||||
|
||||
### 📦 Bundle Stats
|
||||
|
||||
<details>
|
||||
<summary>Chunk size diff (2 updated, 0 added, 0 removed)</summary>
|
||||
|
||||
| Chunk | Base | Head | Δ | Δ (%) |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| (total) | 120 KB | 127 KB | $\color{orange}{\text{+6.3 KB}}$ | $\color{orange}{\text{+5.2\\%}}$ |
|
||||
| | | | | |
|
||||
| <details><summary>`vue`</summary> `assets/vue-b2.js` </details> | 90 KB | 96 KB | $\color{orange}{\text{+6 KB}}$ | $\color{orange}{\text{+6.7\\%}}$ |
|
||||
| (other generated chunks) | 1.2 KB | 1.5 KB | $\text{+300 B}$ | $\color{orange}{\text{+25\\%}}$ |
|
||||
| (other) | 20 KB | 20 KB | $\text{+3 B}$ | $\text{+0\\%}$ |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Startup chunk size (2 updated, 0 added, 0 removed)</summary>
|
||||
|
||||
| Chunk | Base | Head | Δ | Δ (%) |
|
||||
| --- | ---: | ---: | ---: | ---: |
|
||||
| (total) | 114 KB | 120 KB | $\color{orange}{\text{+6 KB}}$ | $\color{orange}{\text{+5.3\\%}}$ |
|
||||
| | | | | |
|
||||
| <details><summary>`vue`</summary> `assets/vue-b2.js` </details> | 90 KB | 96 KB | $\color{orange}{\text{+6 KB}}$ | $\color{orange}{\text{+6.7\\%}}$ |
|
||||
| (other) | 24 KB | 24 KB | $\text{+3 B}$ | $\text{+0\\%}$ |
|
||||
|
||||
_Startup chunks are the Vite entry for `src/_boot_.ts` and its static imports._
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th rowspan="2"></th>
|
||||
<th rowspan="2">Bundles</th>
|
||||
<th rowspan="2">Modules</th>
|
||||
<th rowspan="2">Entries</th>
|
||||
<th colspan="2">Imports</th>
|
||||
<th colspan="3">Size</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Static</th>
|
||||
<th>Dynamic</th>
|
||||
<th>Rendered</th>
|
||||
<th>Gzip</th>
|
||||
<th>Brotli</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th><b>Base</b></th>
|
||||
<td>2</td>
|
||||
<td>6</td>
|
||||
<td>1</td>
|
||||
<td>2</td>
|
||||
<td>3</td>
|
||||
<td>21 KB</td>
|
||||
<td>6.3 KB</td>
|
||||
<td>5.3 KB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><b>Head</b></th>
|
||||
<td>2</td>
|
||||
<td>7</td>
|
||||
<td>1</td>
|
||||
<td>3</td>
|
||||
<td>3</td>
|
||||
<td>31 KB</td>
|
||||
<td>8.4 KB</td>
|
||||
<td>7 KB</td>
|
||||
</tr>
|
||||
<tr><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td></tr>
|
||||
<tr>
|
||||
<th><b>Δ</b></th>
|
||||
<td>0</td>
|
||||
<td>$\color{orange}{\text{+1}}$</td>
|
||||
<td>0</td>
|
||||
<td>$\color{orange}{\text{+1}}$</td>
|
||||
<td>0</td>
|
||||
<td>$\color{orange}{\text{+9.8 KB}}$</td>
|
||||
<td>$\color{orange}{\text{+2.1 KB}}$</td>
|
||||
<td>$\color{orange}{\text{+1.8 KB}}$</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><b>Δ (%)</b></th>
|
||||
<td>0%</td>
|
||||
<td>$\color{orange}{\text{+16.7\%}}$</td>
|
||||
<td>0%</td>
|
||||
<td>$\color{orange}{\text{+50\%}}$</td>
|
||||
<td>0%</td>
|
||||
<td>$\color{orange}{\text{+46.7\%}}$</td>
|
||||
<td>$\color{orange}{\text{+33.3\%}}$</td>
|
||||
<td>$\color{orange}{\text{+33.3\%}}$</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
[Open treemap HTML](https://example.invalid/treemap)
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
|
||||
import { renderBrowserDiagnosticsHtml } from '../../src/browser/report/html';
|
||||
import type { BrowserMetricsReport } from '../../src/browser/types';
|
||||
|
||||
const fixturesDir = join(import.meta.dirname, 'fixtures');
|
||||
|
||||
async function loadFixture(name: string) {
|
||||
return JSON.parse(await readFile(join(fixturesDir, `${name}.json`), 'utf8')) as BrowserMetricsReport;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
// renderBrowserDiagnosticsHtml() が生成時刻を埋め込むので、スナップショットが揺れないよう時刻を固定する
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date('2026-07-18T00:00:00.000Z'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
test('renders the network request diff html report', async () => {
|
||||
const html = renderBrowserDiagnosticsHtml(await loadFixture('base'), await loadFixture('head'));
|
||||
|
||||
await expect(html).toMatchFileSnapshot('./__snapshots__/render-html.html');
|
||||
});
|
||||
|
||||
test('escapes html metacharacters coming from the browser session', async () => {
|
||||
const base = await loadFixture('base');
|
||||
const head = await loadFixture('head');
|
||||
// CDP由来の値は原理的には任意の文字列になりうるので、生HTMLとして出ないことを確かめる
|
||||
head.samples[0].networkRequests[0].url = 'http://127.0.0.1:61812/"><script>alert(1)</script>';
|
||||
|
||||
const html = renderBrowserDiagnosticsHtml(base, head);
|
||||
|
||||
expect(html).not.toContain('<script>alert(1)</script>');
|
||||
expect(html).toContain('<script>alert(1)</script>');
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { afterEach, expect, test, vi } from 'vitest';
|
||||
import { stopServer, waitForServer } from '../../src/browser/server';
|
||||
import type { ChildProcess } from 'node:child_process';
|
||||
|
||||
vi.mock('node:child_process', async () => {
|
||||
const actual = await vi.importActual<typeof import('node:child_process')>('node:child_process');
|
||||
return {
|
||||
...actual,
|
||||
spawnSync: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
function signaledChild() {
|
||||
return Object.assign(new EventEmitter(), {
|
||||
exitCode: null,
|
||||
signalCode: 'SIGTERM' as NodeJS.Signals,
|
||||
pid: undefined,
|
||||
kill: vi.fn(),
|
||||
}) as unknown as ChildProcess;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
test('waitForServer fails immediately when the server exited by signal', async () => {
|
||||
const fetchMock = vi.fn().mockResolvedValue({ status: 200 });
|
||||
vi.stubGlobal('fetch', fetchMock);
|
||||
|
||||
await expect(waitForServer('http://127.0.0.1:61812', signaledChild()))
|
||||
.rejects.toThrow('Misskey server exited early with signal SIGTERM');
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('stopServer returns immediately when the server already exited by signal', async () => {
|
||||
const child = signaledChild();
|
||||
const stopPromise = stopServer(child);
|
||||
|
||||
expect(child.listenerCount('exit')).toBe(0);
|
||||
await stopPromise;
|
||||
});
|
||||
@@ -1 +1 @@
|
||||
{"nodeParts": {"p0": {"renderedLength": 1000, "gzipLength": 300, "brotliLength": 250}, "p1": {"renderedLength": 2000, "gzipLength": 600, "brotliLength": 500}, "p2": {"renderedLength": 3000, "gzipLength": 900, "brotliLength": 750}, "p3": {"renderedLength": 4000, "gzipLength": 1200, "brotliLength": 1000}, "p4": {"renderedLength": 5000, "gzipLength": 1500, "brotliLength": 1250}, "p5": {"renderedLength": 6000, "gzipLength": 1800, "brotliLength": 1500}}, "nodeMetas": {"m0": {"id": "/src/mod0.ts", "isEntry": true, "importedBy": [], "imported": [{"id": "m1", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p0"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m1": {"id": "/src/mod1.ts", "isEntry": false, "importedBy": ["m0"], "imported": [{"id": "m2", "dynamic": false}], "moduleParts": {"assets/vue-b2.js": "p1"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m2": {"id": "/src/mod2.ts", "isEntry": false, "importedBy": ["m1"], "imported": [{"id": "m3", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p2"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m3": {"id": "/src/mod3.ts", "isEntry": false, "importedBy": ["m2"], "imported": [{"id": "m4", "dynamic": false}], "moduleParts": {"assets/vue-b2.js": "p3"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m4": {"id": "/src/mod4.ts", "isEntry": false, "importedBy": ["m3"], "imported": [{"id": "m5", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p4"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m5": {"id": "/src/mod5.ts", "isEntry": false, "importedBy": ["m4"], "imported": [], "moduleParts": {"assets/vue-b2.js": "p5"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}}, "options": {}}
|
||||
{"nodeParts": {"p0": {"renderedLength": 1000, "gzipLength": 300, "brotliLength": 250}, "p1": {"renderedLength": 2000, "gzipLength": 600, "brotliLength": 500}, "p2": {"renderedLength": 3000, "gzipLength": 900, "brotliLength": 750}, "p3": {"renderedLength": 4000, "gzipLength": 1200, "brotliLength": 1000}, "p4": {"renderedLength": 5000, "gzipLength": 1500, "brotliLength": 1250}, "p5": {"renderedLength": 6000, "gzipLength": 1800, "brotliLength": 1500}}, "nodeMetas": {"m0": {"id": "/src/mod0.ts", "isEntry": true, "importedBy": [], "imported": [{"id": "m1", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p0"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m1": {"id": "/src/mod1.ts", "isEntry": false, "importedBy": ["m0"], "imported": [{"id": "m2", "dynamic": false}], "moduleParts": {"assets/vue-b2.js": "p1"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m2": {"id": "/src/mod2.ts", "isEntry": false, "importedBy": ["m1"], "imported": [{"id": "m3", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p2"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m3": {"id": "/src/mod3.ts", "isEntry": false, "importedBy": ["m2"], "imported": [{"id": "m4", "dynamic": false}], "moduleParts": {"assets/vue-b2.js": "p3"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m4": {"id": "/src/mod4.ts", "isEntry": false, "importedBy": ["m3"], "imported": [{"id": "m5", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p4"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m5": {"id": "/src/mod5.ts", "isEntry": false, "importedBy": ["m4"], "imported": [], "moduleParts": {"assets/vue-b2.js": "p5"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}}, "options": {}}
|
||||
@@ -1 +1 @@
|
||||
{"nodeParts": {"p0": {"renderedLength": 1100.0, "gzipLength": 300, "brotliLength": 250}, "p1": {"renderedLength": 2200.0, "gzipLength": 600, "brotliLength": 500}, "p2": {"renderedLength": 3300.0000000000005, "gzipLength": 900, "brotliLength": 750}, "p3": {"renderedLength": 4400.0, "gzipLength": 1200, "brotliLength": 1000}, "p4": {"renderedLength": 5500.0, "gzipLength": 1500, "brotliLength": 1250}, "p5": {"renderedLength": 6600.000000000001, "gzipLength": 1800, "brotliLength": 1500}, "p6": {"renderedLength": 7700.000000000001, "gzipLength": 2100, "brotliLength": 1750}}, "nodeMetas": {"m0": {"id": "/src/mod0.ts", "isEntry": true, "importedBy": [], "imported": [{"id": "m1", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p0"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m1": {"id": "/src/mod1.ts", "isEntry": false, "importedBy": ["m0"], "imported": [{"id": "m2", "dynamic": false}], "moduleParts": {"assets/vue-b2.js": "p1"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m2": {"id": "/src/mod2.ts", "isEntry": false, "importedBy": ["m1"], "imported": [{"id": "m3", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p2"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m3": {"id": "/src/mod3.ts", "isEntry": false, "importedBy": ["m2"], "imported": [{"id": "m4", "dynamic": false}], "moduleParts": {"assets/vue-b2.js": "p3"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m4": {"id": "/src/mod4.ts", "isEntry": false, "importedBy": ["m3"], "imported": [{"id": "m5", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p4"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m5": {"id": "/src/mod5.ts", "isEntry": false, "importedBy": ["m4"], "imported": [{"id": "m6", "dynamic": false}], "moduleParts": {"assets/vue-b2.js": "p5"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m6": {"id": "/src/mod6.ts", "isEntry": false, "importedBy": ["m5"], "imported": [], "moduleParts": {"assets/boot-a1.js": "p6"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}}, "options": {}}
|
||||
{"nodeParts": {"p0": {"renderedLength": 1100.0, "gzipLength": 300, "brotliLength": 250}, "p1": {"renderedLength": 2200.0, "gzipLength": 600, "brotliLength": 500}, "p2": {"renderedLength": 3300.0000000000005, "gzipLength": 900, "brotliLength": 750}, "p3": {"renderedLength": 4400.0, "gzipLength": 1200, "brotliLength": 1000}, "p4": {"renderedLength": 5500.0, "gzipLength": 1500, "brotliLength": 1250}, "p5": {"renderedLength": 6600.000000000001, "gzipLength": 1800, "brotliLength": 1500}, "p6": {"renderedLength": 7700.000000000001, "gzipLength": 2100, "brotliLength": 1750}}, "nodeMetas": {"m0": {"id": "/src/mod0.ts", "isEntry": true, "importedBy": [], "imported": [{"id": "m1", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p0"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m1": {"id": "/src/mod1.ts", "isEntry": false, "importedBy": ["m0"], "imported": [{"id": "m2", "dynamic": false}], "moduleParts": {"assets/vue-b2.js": "p1"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m2": {"id": "/src/mod2.ts", "isEntry": false, "importedBy": ["m1"], "imported": [{"id": "m3", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p2"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m3": {"id": "/src/mod3.ts", "isEntry": false, "importedBy": ["m2"], "imported": [{"id": "m4", "dynamic": false}], "moduleParts": {"assets/vue-b2.js": "p3"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m4": {"id": "/src/mod4.ts", "isEntry": false, "importedBy": ["m3"], "imported": [{"id": "m5", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p4"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m5": {"id": "/src/mod5.ts", "isEntry": false, "importedBy": ["m4"], "imported": [{"id": "m6", "dynamic": false}], "moduleParts": {"assets/vue-b2.js": "p5"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m6": {"id": "/src/mod6.ts", "isEntry": false, "importedBy": ["m5"], "imported": [], "moduleParts": {"assets/boot-a1.js": "p6"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}}, "options": {}}
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { promises as fs } from 'node:fs';
|
||||
import { afterEach, expect, test, vi } from 'vitest';
|
||||
import { fileExists } from '../../src/bundle/fs-utils';
|
||||
|
||||
function fsError(code: string) {
|
||||
return Object.assign(new Error(`fs error: ${code}`), { code }) as NodeJS.ErrnoException;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('fileExists returns false for ENOENT', async () => {
|
||||
vi.spyOn(fs, 'access').mockRejectedValueOnce(fsError('ENOENT'));
|
||||
|
||||
await expect(fileExists('missing')).resolves.toBe(false);
|
||||
});
|
||||
|
||||
test('fileExists rethrows errors other than ENOENT', async () => {
|
||||
const error = fsError('EACCES');
|
||||
vi.spyOn(fs, 'access').mockRejectedValueOnce(error);
|
||||
|
||||
await expect(fileExists('restricted')).rejects.toBe(error);
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { mkdtemp, rm } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { afterAll, beforeAll, expect, test } from 'vitest';
|
||||
import { collectBundleReport } from '../../src/bundle/manifest';
|
||||
|
||||
let workDir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
workDir = await mkdtemp(join(tmpdir(), 'diagnostics-frontend-test-'));
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await rm(workDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('fails loudly when the built output is missing', async () => {
|
||||
await expect(collectBundleReport(join(workDir, 'nonexistent'))).rejects.toThrow();
|
||||
});
|
||||
@@ -7,16 +7,18 @@ import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { dirname, join } from 'node:path';
|
||||
import { afterAll, beforeAll, expect, test } from 'vitest';
|
||||
import { collectReport } from '../src/manifest';
|
||||
import { renderBundleReportMarkdown } from '../src/report';
|
||||
import type { VisualizerReport } from '../src/visualizer';
|
||||
import { collectBundleReport } from '../src/bundle/manifest';
|
||||
import { renderFrontendDiagnosticsMarkdown } from '../src/report';
|
||||
import type { BrowserMetricsReport } from '../src/browser/types';
|
||||
import type { VisualizerReport } from '../src/bundle/visualizer';
|
||||
|
||||
const fixturesDir = join(import.meta.dirname, 'fixtures');
|
||||
const bundleFixturesDir = join(import.meta.dirname, 'bundle/fixtures');
|
||||
const browserFixturesDir = join(import.meta.dirname, 'browser/fixtures');
|
||||
|
||||
/**
|
||||
* ビルド成果物のfixture。
|
||||
*
|
||||
* `collectReport` はファイルの中身を見ずサイズしか使わないので、実体は指定バイト数の
|
||||
* `collectBundleReport` はファイルの中身を見ずサイズしか使わないので、実体は指定バイト数の
|
||||
* 詰め物でよい。ディレクトリ名が `built` になるためリポジトリにはコミットできず
|
||||
* (ルートの .gitignore がビルド成果物として除外する)、テスト実行時に組み立てている。
|
||||
*/
|
||||
@@ -31,7 +33,7 @@ const manifest = {
|
||||
};
|
||||
|
||||
const fileSizes = {
|
||||
before: {
|
||||
base: {
|
||||
'assets/boot-a1.js': 20_000,
|
||||
'assets/vue-b2.js': 90_000,
|
||||
'assets/foo-d4.js': 5_000,
|
||||
@@ -39,7 +41,7 @@ const fileSizes = {
|
||||
'ja-JP/i18n-c3.js': 4_000,
|
||||
'ja-JP/orphan.js': 1_200,
|
||||
},
|
||||
after: {
|
||||
head: {
|
||||
// 差が小さすぎる (閾値5バイト以下) ので「(other)」に集約される
|
||||
'assets/boot-a1.js': 20_003,
|
||||
// 明確に増えるので diff表に行として出る
|
||||
@@ -50,15 +52,15 @@ const fileSizes = {
|
||||
// manifestに載らない出力なので「(other generated chunks)」に集約される
|
||||
'ja-JP/orphan.js': 1_500,
|
||||
},
|
||||
} as const satisfies Record<'before' | 'after', Record<string, number>>;
|
||||
} as const satisfies Record<'base' | 'head', Record<string, number>>;
|
||||
|
||||
let repoDirs: { before: string; after: string };
|
||||
let repoDirs: { base: string; head: string };
|
||||
let workDir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
workDir = await mkdtemp(join(tmpdir(), 'diagnostics-frontend-bundle-'));
|
||||
workDir = await mkdtemp(join(tmpdir(), 'diagnostics-frontend-report-test-'));
|
||||
|
||||
for (const label of ['before', 'after'] as const) {
|
||||
for (const label of ['base', 'head'] as const) {
|
||||
const outDir = join(workDir, label, 'built/_frontend_vite_');
|
||||
await mkdir(outDir, { recursive: true });
|
||||
await writeFile(join(outDir, 'manifest.json'), JSON.stringify(manifest));
|
||||
@@ -71,8 +73,8 @@ beforeAll(async () => {
|
||||
}
|
||||
|
||||
repoDirs = {
|
||||
before: join(workDir, 'before'),
|
||||
after: join(workDir, 'after'),
|
||||
base: join(workDir, 'base'),
|
||||
head: join(workDir, 'head'),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -80,26 +82,45 @@ afterAll(async () => {
|
||||
await rm(workDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
async function loadStats(name: string) {
|
||||
return JSON.parse(await readFile(join(fixturesDir, `${name}-stats.json`), 'utf8')) as VisualizerReport;
|
||||
async function loadBundleStats(name: 'base' | 'head') {
|
||||
return JSON.parse(await readFile(join(bundleFixturesDir, `${name}-stats.json`), 'utf8')) as VisualizerReport;
|
||||
}
|
||||
|
||||
async function loadBrowserReport(name: 'base' | 'head') {
|
||||
return JSON.parse(await readFile(join(browserFixturesDir, `${name}.json`), 'utf8')) as BrowserMetricsReport;
|
||||
}
|
||||
|
||||
async function renderReport(detailedHtmlUrl: string | null) {
|
||||
return renderFrontendDiagnosticsMarkdown({
|
||||
bundle: {
|
||||
base: await collectBundleReport(repoDirs.base),
|
||||
head: await collectBundleReport(repoDirs.head),
|
||||
baseStats: await loadBundleStats('base'),
|
||||
headStats: await loadBundleStats('head'),
|
||||
visualizerArtifactUrl: 'https://example.invalid/treemap',
|
||||
},
|
||||
browser: {
|
||||
base: await loadBrowserReport('base'),
|
||||
head: await loadBrowserReport('head'),
|
||||
baseHeapSnapshotUrl: 'https://example.invalid/base',
|
||||
headHeapSnapshotUrl: 'https://example.invalid/head',
|
||||
detailedHtmlUrl,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 出力をゴールデンファイルで固定する。
|
||||
* 意図的に変更したときは `vitest -u` で更新し、__snapshots__ の差分もレビューすること。
|
||||
*/
|
||||
test('renders the frontend bundle report', async () => {
|
||||
const markdown = renderBundleReportMarkdown(
|
||||
await collectReport(repoDirs.before),
|
||||
await collectReport(repoDirs.after),
|
||||
await loadStats('before'),
|
||||
await loadStats('after'),
|
||||
{ visualizerArtifactUrl: 'https://example.invalid/treemap' },
|
||||
);
|
||||
test('renders one frontend diagnostics markdown report from bundle and browser data', async () => {
|
||||
const markdown = await renderReport('https://example.invalid/html');
|
||||
|
||||
await expect(markdown).toMatchFileSnapshot('./__snapshots__/render-md.md');
|
||||
await expect(markdown).toMatchFileSnapshot('./__snapshots__/report.md');
|
||||
});
|
||||
|
||||
test('fails loudly when the built output is missing', async () => {
|
||||
await expect(collectReport(join(workDir, 'nonexistent'))).rejects.toThrow();
|
||||
test('omits the browser details link when no detailed html artifact was uploaded', async () => {
|
||||
const markdown = await renderReport(null);
|
||||
|
||||
expect(markdown).not.toContain('View details');
|
||||
});
|
||||
24
pnpm-lock.yaml
generated
24
pnpm-lock.yaml
generated
@@ -131,7 +131,7 @@ importers:
|
||||
specifier: 4.1.10
|
||||
version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(typescript@5.9.3))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1))
|
||||
|
||||
packages-private/diagnostics-frontend-browser:
|
||||
packages-private/diagnostics-frontend:
|
||||
dependencies:
|
||||
diagnostics-shared:
|
||||
specifier: workspace:*
|
||||
@@ -156,28 +156,6 @@ importers:
|
||||
specifier: 4.1.10
|
||||
version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(typescript@5.9.3))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1))
|
||||
|
||||
packages-private/diagnostics-frontend-bundle:
|
||||
dependencies:
|
||||
diagnostics-shared:
|
||||
specifier: workspace:*
|
||||
version: link:../diagnostics-shared
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: 26.1.1
|
||||
version: 26.1.1
|
||||
tsx:
|
||||
specifier: 4.23.1
|
||||
version: 4.23.1
|
||||
typescript:
|
||||
specifier: 5.9.3
|
||||
version: 5.9.3
|
||||
vite:
|
||||
specifier: 8.1.4
|
||||
version: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)
|
||||
vitest:
|
||||
specifier: 4.1.10
|
||||
version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(typescript@5.9.3))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1))
|
||||
|
||||
packages-private/diagnostics-shared:
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
|
||||
Reference in New Issue
Block a user