name: Полные бенчмарки производительности on: # Every main promotion is a stable-release candidate and must have an # exact-SHA full comparison before stable assets are published. push: branches: - main schedule: - cron: "0 4 * * 1" workflow_dispatch: inputs: comparison_ref: description: "Optional baseline tag, branch or SHA; empty uses the candidate parent" required: false type: string permissions: contents: read concurrency: group: full-performance-${{ github.ref }} cancel-in-progress: false jobs: performance: name: Бенчмарки рендера и геометрии # Every profile keeps base and candidate sequential on one hosted runner. # Independent profile pairs run in parallel: cross-profile timing is never # compared, while serialising all profile pairs cannot fit the job timeout. runs-on: ubuntu-latest timeout-minutes: 60 strategy: fail-fast: false matrix: profile: - large-house - isometric - isometric-stage3 - plan-snap - interaction - blend - overlay - space-default - space-glow steps: - name: Check out candidate uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: path: candidate fetch-depth: 2 - name: Resolve comparison SHA id: base working-directory: candidate env: EVENT_NAME: ${{ github.event_name }} PUSH_BEFORE_SHA: ${{ github.event.before }} MANUAL_BASE: ${{ inputs.comparison_ref }} run: | set -euo pipefail if [ "$(git rev-parse --is-shallow-repository)" = "true" ]; then git fetch --force --tags --prune --unshallow origin else git fetch --force --tags --prune origin fi if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ -n "$MANUAL_BASE" ]; then sha="$(git rev-parse "${MANUAL_BASE}^{commit}" 2>/dev/null || true)" source="manual comparison ref $MANUAL_BASE" elif [ "$EVENT_NAME" = "push" ] && [ -n "$PUSH_BEFORE_SHA" ] && ! printf '%s' "$PUSH_BEFORE_SHA" | grep -Eq '^0+$'; then sha="$PUSH_BEFORE_SHA" source="push before" else sha="$(git rev-parse HEAD^ 2>/dev/null || true)" source="candidate parent" fi requested_sha="$sha" usable=true reason="" if [ -z "$sha" ] || ! git cat-file -e "${sha}^{commit}" 2>/dev/null; then usable=false reason="commit is not present after fetching all remote refs" elif [ "$source" = "push before" ] && ! git merge-base --is-ancestor "$sha" HEAD; then usable=false reason="commit is no longer an ancestor of the pushed revision" fi if [ "$usable" != true ]; then parent_sha="$(git rev-parse HEAD^ 2>/dev/null || true)" if [ -n "$parent_sha" ] && [ "$parent_sha" != "$(git rev-parse HEAD)" ]; then sha="$parent_sha" source="candidate parent (unusable requested-base fallback)" echo "::warning::Comparison SHA ${requested_sha:-none} is unusable ($reason); using candidate parent $sha." usable=true fi fi if [ "$usable" != true ]; then fallback_tag="" fallback_sha="" head_sha="$(git rev-parse HEAD)" while IFS= read -r tag; do case "$tag" in v[0-9]*.[0-9]*.[0-9]*) ;; *) continue ;; esac tag_sha="$(git rev-list -n 1 "$tag")" if [ "$tag_sha" != "$head_sha" ]; then fallback_tag="$tag" fallback_sha="$tag_sha" break fi done < <(git tag --merged HEAD --sort=-version:refname) if [ -z "$fallback_sha" ]; then echo "::error::No usable comparison commit or previous release tag is reachable from HEAD." exit 1 fi sha="$fallback_sha" source="release tag $fallback_tag" echo "::warning::Using $fallback_tag ($sha) as the comparison base." fi if ! git cat-file -e "${sha}:demo/bundle-freshness.mjs" 2>/dev/null; then echo "::warning::Comparison $sha predates HP-PERF-01; using candidate parent HEAD^." sha="$(git rev-parse HEAD^)" source="candidate parent (HP-PERF-01 compatibility)" fi echo "sha=$sha" >> "$GITHUB_OUTPUT" echo "Comparison base: $sha ($source)" >> "$GITHUB_STEP_SUMMARY" - name: Check out base SHA uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: ref: ${{ steps.base.outputs.sha }} path: baseline - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7 with: node-version: 22 cache: npm cache-dependency-path: | candidate/package-lock.json baseline/package-lock.json - name: Install candidate and baseline dependencies run: npm ci --prefix candidate && npm ci --prefix baseline # То же, что в validate.yml: кэш браузеров, apt не трогаем (#206). - name: Кэш браузеров Playwright id: pw uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6 with: path: ~/.cache/ms-playwright key: playwright-${{ runner.os }}-${{ hashFiles('candidate/package-lock.json') }} - name: Install pinned Chromium if: steps.pw.outputs.cache-hit != 'true' working-directory: candidate run: npx playwright install chromium - name: Build both exact source trees run: | npm --prefix candidate run build (cd candidate && node scripts/bundle-sync.mjs) npm --prefix baseline run build if [ -f baseline/scripts/bundle-sync.mjs ]; then (cd baseline && node scripts/bundle-sync.mjs) else echo "::notice::The comparison base predates bundle-sync.mjs; materializing its freshly built bundle with the legacy copy path." mkdir -p baseline/custom_components/houseplan/frontend baseline/demo/srv/assets cp baseline/dist/houseplan-card.js baseline/custom_components/houseplan/frontend/houseplan-card.js cp baseline/dist/houseplan-card.js baseline/demo/srv/assets/houseplan-card.js fi - name: Capture base and candidate profile working-directory: candidate env: PROFILE: ${{ matrix.profile }} run: | mkdir -p ../artifacts/performance case "$PROFILE" in large-house) npm run benchmark:large-house -- --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/baseline.json npm run benchmark:large-house -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/candidate.json ;; isometric) npm run benchmark:large-house-isometric -- --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/isometric-baseline.json npm run benchmark:large-house-isometric -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/isometric-candidate.json ;; isometric-stage3) npm run benchmark:isometric-stage3-dense -- --allow-stage2-base --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/isometric-stage3-baseline.json npm run benchmark:isometric-stage3-dense -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/isometric-stage3-candidate.json ;; plan-snap) npm run benchmark:large-house-plan-snap -- --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/plan-snap-baseline.json npm run benchmark:large-house-plan-snap -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/plan-snap-candidate.json ;; interaction) npm run benchmark:large-house-interaction -- --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/interaction-baseline.json npm run benchmark:large-house-interaction -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/interaction-candidate.json ;; blend) npm run benchmark:glow -- --profile=large-light-blend-v1 --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/blend-baseline.json npm run benchmark:glow -- --profile=large-light-blend-v1 --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/blend-candidate.json ;; overlay) npm run benchmark:glow -- --profile=large-house-glow-overlay-v1 --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/overlay-baseline.json npm run benchmark:glow -- --profile=large-house-glow-overlay-v1 --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/overlay-candidate.json if ! grep -q "glow_enabled" ../baseline/src/logic.ts; then echo "Base predates independent Glow; bootstrap relative overlay baseline, keep absolute gate" cp ../artifacts/performance/overlay-candidate.json ../artifacts/performance/overlay-baseline.json fi ;; space-default) npm run benchmark:glow -- --profile=large-space-card-default-v1 --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/space-default-baseline.json npm run benchmark:glow -- --profile=large-space-card-default-v1 --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/space-default-candidate.json ;; space-glow) npm run benchmark:glow -- --profile=large-space-card-glow-v1 --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/space-glow-baseline.json npm run benchmark:glow -- --profile=large-space-card-glow-v1 --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/space-glow-candidate.json if ! grep -q "light_pools" ../baseline/src/space-card.ts; then echo "Base predates opt-in static Glow; bootstrap relative baseline, keep absolute gate" cp ../artifacts/performance/space-glow-candidate.json ../artifacts/performance/space-glow-baseline.json fi ;; *) echo "::error::Unknown performance profile: $PROFILE" exit 1 ;; esac - name: Enforce relative and absolute performance budget working-directory: candidate env: PROFILE: ${{ matrix.profile }} run: | case "$PROFILE" in large-house) npm run benchmark:compare -- --baseline=../artifacts/performance/baseline.json --candidate=../artifacts/performance/candidate.json --output=../artifacts/performance/comparison.json ;; isometric) npm run benchmark:compare -- --budgets=demo/performance/budgets-large-house-isometric.json --baseline=../artifacts/performance/isometric-baseline.json --candidate=../artifacts/performance/isometric-candidate.json --baseline-sha="$(git -C ../baseline rev-parse HEAD)" --candidate-sha="$(git rev-parse HEAD)" --output=../artifacts/performance/isometric-comparison.json ;; isometric-stage3) npm run benchmark:compare -- --budgets=demo/performance/budgets-isometric-stage3-dense.json --baseline=../artifacts/performance/isometric-stage3-baseline.json --candidate=../artifacts/performance/isometric-stage3-candidate.json --baseline-sha="$(git -C ../baseline rev-parse HEAD)" --candidate-sha="$(git rev-parse HEAD)" --output=../artifacts/performance/isometric-stage3-comparison.json ;; plan-snap) npm run benchmark:compare -- --budgets=demo/performance/budgets-large-house-plan-snap.json --baseline=../artifacts/performance/plan-snap-baseline.json --candidate=../artifacts/performance/plan-snap-candidate.json --output=../artifacts/performance/plan-snap-comparison.json ;; interaction) npm run benchmark:compare -- --budgets=demo/performance/budgets-large-house-interaction.json --baseline=../artifacts/performance/interaction-baseline.json --candidate=../artifacts/performance/interaction-candidate.json --output=../artifacts/performance/interaction-comparison.json ;; blend) npm run benchmark:compare -- --budgets=demo/performance/budgets-large-light-blend.json --baseline=../artifacts/performance/blend-baseline.json --candidate=../artifacts/performance/blend-candidate.json --output=../artifacts/performance/blend-comparison.json ;; overlay) npm run benchmark:compare -- --budgets=demo/performance/budgets-large-house-glow-overlay.json --baseline=../artifacts/performance/overlay-baseline.json --candidate=../artifacts/performance/overlay-candidate.json --output=../artifacts/performance/overlay-comparison.json ;; space-default) npm run benchmark:compare -- --budgets=demo/performance/budgets-large-space-card-default.json --baseline=../artifacts/performance/space-default-baseline.json --candidate=../artifacts/performance/space-default-candidate.json --output=../artifacts/performance/space-default-comparison.json ;; space-glow) npm run benchmark:compare -- --budgets=demo/performance/budgets-large-space-card-glow.json --baseline=../artifacts/performance/space-glow-baseline.json --candidate=../artifacts/performance/space-glow-candidate.json --output=../artifacts/performance/space-glow-comparison.json ;; esac - name: Upload full performance report if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: full-performance-${{ matrix.profile }} path: artifacts/performance