Files
houseplan-card/.github/workflows/performance.yml
T
Codex d515c42177 feat: LED strips — editor notes, docs, demo, smokes, golden scenes, profile (#780)
Stage 5 of #780.

- Import summary: «Strips left unbound after import: {n}» from the backend
  `unbound_led_strips` count; «Optimize plans» reports strips passing
  through walls per space and edits none (AC16).
- Linear field for long strips (ТЗ §13.2): pieces of at most the radius
  along the polyline, emitters thinned to r/4, each piece clipped to the
  visibility fans of its own emitters as separate clipPath children (no
  boolean pass per piece), one floor clip for the whole layer, no fan at
  all where nothing blocks within the radius; a grid index of body faces
  and boxed inside tests; unchanged fields skip re-diffing. 50×50 on the
  large house: first stable frame ~1.4 s, warm space ~1.1 s locally.
- led-strips-v1 profile: demo/benchmark_led_strips.mjs with the derived
  large-house fixture (10×5, 50×50, none), absolute limits of the ТЗ table
  in demo/performance/budgets-led-strips.json, exact counters (zero
  recomputes on HA ticks/camera/colour, ≤50 cache entries, no growth over
  20 cycles); added to the full performance workflow.
- Bundle: LAZY_LED_GZIP_CEILING 10 KiB, LAZY_LED_EDITOR_GZIP_CEILING 11 KiB
  (measured + 10 %, rounded up); overlaps with the initial and editor
  graphs refused; the lazy editor graph stays inside its ceiling.
- Smokes smoke_led_strip_draw/bind/glow, linked in smoke-links; 13 mutants
  in the registry (7 browser guards in the inventory); config field registry
  entry `spaces[].led_strips`.
- Golden: five new scenes on the `golden-led` space of the visual fixture
  (`ledStrips` option, the designer's four strips on #868D94), matrix v71.
- Docs: LIGHT, DEVICE-PRESENTATION, USER-GUIDE (en/ru), UX-MODES,
  ARCHITECTURE, ISOMETRIC, CONFIG-COMPATIBILITY, TOUCH-SUPPORT, demo/stand
  README, performance README; docs/design/led-strips with the unchanged
  designer archive, two paired frames and ACCEPTANCE.md; both changelogs.

Issue: #780
User-Visible: yes
2026-10-02 20:54:50 +03:00

240 lines
14 KiB
YAML

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
paths-ignore:
- ".github/workflows/**"
- "docs/**"
schedule:
# Минута не круглая (#658): доходит до main вместе со стабильным релизом.
- cron: "11 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-24.04
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
profile:
- large-house
- isometric
- isometric-backdrop
- isometric-stage3
- plan-snap
- interaction
- blend
- overlay
- space-default
- space-glow
- led-strips
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 }}
# #587: кандидат стабильного релиза судится о предыдущий стабильный
# тег, а не о прошлую вершину main. Признак кандидата — трейлер
# `Release:` в сообщении head-коммита, поэтому оно едет в скрипт.
HEAD_MESSAGE: ${{ github.event.head_commit.message }}
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
# Решение целиком в скрипте: его отрицательные случаи проверяются
# фикстурами, а shell-развилку прогнать тестом нельзя (#556, #587).
node scripts/performance-baseline.mjs
- 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-backdrop)
npm run benchmark:large-house-isometric-backdrop -- --target-root=../baseline --samples=7 --warmups=1 --output=../artifacts/performance/isometric-backdrop-baseline.json
npm run benchmark:large-house-isometric-backdrop -- --target-root=. --samples=7 --warmups=1 --output=../artifacts/performance/isometric-backdrop-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
;;
led-strips)
# #780 led-strips-v1: base predates LED strips, so the profile is
# judged by its absolute limits inside the runner; the zero-LED
# View stays covered by the relative interaction profile above.
npm run benchmark:led-strips -- --size=10x5 --samples=7 --warmups=1 --output=../artifacts/performance/led-strips-10x5.json
npm run benchmark:led-strips -- --size=50x50 --samples=7 --warmups=1 --output=../artifacts/performance/led-strips-50x50.json
npm run benchmark:led-strips -- --size=none --samples=7 --warmups=1 --output=../artifacts/performance/led-strips-none.json
;;
*)
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-backdrop)
npm run benchmark:compare -- --budgets=demo/performance/budgets-large-house-isometric-backdrop.json --baseline=../artifacts/performance/isometric-backdrop-baseline.json --candidate=../artifacts/performance/isometric-backdrop-candidate.json --baseline-sha="$(git -C ../baseline rev-parse HEAD)" --candidate-sha="$(git rev-parse HEAD)" --output=../artifacts/performance/isometric-backdrop-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
;;
led-strips)
echo "led-strips-v1: absolute limits and counters already enforced by the runner (budgets-led-strips.json)"
;;
esac
- name: Upload full performance report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: full-performance-${{ matrix.profile }}
path: artifacts/performance