--- name: perf-fix-with-proof description: 'Ships performance fixes with CI-proven improvement using stacked PRs. PR1 adds a @perf test (establishes baseline on main), PR2 adds the fix (CI shows delta). Use when implementing a perf optimization and wanting to prove it in CI.' --- # Performance Fix with Proof Ships perf fixes as two stacked PRs so CI automatically proves the improvement. ## Why Two PRs The `ci-perf-report.yaml` workflow compares PR metrics against the **base branch baseline**. If you add a new `@perf` test in the same PR as the fix, that test doesn't exist on main yet — no baseline, no delta, no proof. Stacking solves this: 1. **PR1 (test-only)** — adds the `@perf` test that exercises the bottleneck. Merges to main. CI runs it on main → baseline established. 2. **PR2 (fix)** — adds the optimization. CI runs the same test → compares against PR1's baseline → delta shows improvement. ## Workflow ### Step 1: Create the test branch ```bash git worktree add -b perf/test- origin/main ``` ### Step 2: Write the `@perf` test Add a test to `browser_tests/tests/performance.spec.ts` (or a new file with `@perf` tag). The test should stress the specific bottleneck. **Test structure:** ```typescript test('', async ({ comfyPage }) => { // 1. Load a workflow that exercises the bottleneck await comfyPage.workflow.loadWorkflow('') // 2. Start measuring await comfyPage.perf.startMeasuring() // 3. Perform the action that triggers the bottleneck (at scale) for (let i = 0; i < N; i++) { // ... stress the hot path ... await comfyPage.nextFrame() } // 4. Stop measuring and record const m = await comfyPage.perf.stopMeasuring('') recordMeasurement(m) console.log(`: ${m.styleRecalcs} recalcs, ${m.layouts} layouts`) }) ``` **Available metrics** (from `PerformanceHelper`): - `m.styleRecalcs` / `m.styleRecalcDurationMs` — style recalculation count and time - `m.layouts` / `m.layoutDurationMs` — forced layout count and time - `m.taskDurationMs` — total main-thread JS execution time - `m.heapDeltaBytes` — memory pressure delta **Key helpers** (from `ComfyPage`): - `comfyPage.perf.startMeasuring()` / `.stopMeasuring(name)` — CDP metrics capture - `comfyPage.nextFrame()` — wait one animation frame - `comfyPage.workflow.loadWorkflow(name)` — load a test workflow from `browser_tests/assets/` - `comfyPage.canvas` — the canvas locator - `comfyPage.page.mouse.move(x, y)` — mouse interaction ### Step 3: Add test workflow asset (if needed) If the bottleneck needs a specific workflow (e.g., 50+ nodes, many DOM widgets), add it to `browser_tests/assets/`. Keep it minimal — only the structure needed to trigger the bottleneck. ### Step 4: Verify locally ```bash pnpm exec playwright test --project=performance --grep "" ``` Confirm the test runs and produces reasonable metric values. ### Step 5: Create PR1 (test-only) ```bash pnpm typecheck:browser pnpm lint git add browser_tests/ git commit -m "test: add perf test for " git push -u origin perf/test- gh pr create --title "test: add perf test for " \ --body "Adds a @perf test to establish a baseline for . This is PR 1 of 2. The fix will follow in a separate PR once this baseline is established on main. ## What Adds \`\` to the performance test suite measuring during . ## Why Needed to prove the improvement from the upcoming fix for backlog item #." \ --base main ``` ### Step 6: Get PR1 merged Once PR1 merges, CI runs the test on main → baseline artifact saved. ### Step 7: Create PR2 (fix) on top of main ```bash git worktree add -b perf/fix- origin/main ``` Implement the fix. The `@perf` test from PR1 is now on main and will run automatically. CI will: 1. Run the test on the PR branch 2. Download the baseline from main (which includes PR1's test results) 3. Post a PR comment showing the delta ### Step 8: Verify the improvement shows in CI The `ci-perf-report.yaml` posts a comment like: ```markdown ## ⚡ Performance Report | Metric | Baseline | PR (n=3) | Δ | Sig | | --------------------- | -------- | -------- | ---- | --- | | : style recalcs | 450 | 12 | -97% | 🟢 | ``` If Δ is negative for the target metric, the fix is proven. ## Test Design Guidelines 1. **Stress the specific bottleneck** — don't measure everything, isolate the hot path 2. **Use enough iterations** — the test should run long enough that the metric difference is clear (100+ frames for idle tests, 50+ interactions for event tests) 3. **Keep it deterministic** — avoid timing-dependent assertions; measure counts not durations when possible 4. **Match the backlog entry** — reference the backlog item number in the test name or PR description ## Examples **Testing DOM widget reactive mutations (backlog #8):** ```typescript test('DOM widget positioning recalculations', async ({ comfyPage }) => { await comfyPage.workflow.loadWorkflow('default') await comfyPage.perf.startMeasuring() // Idle for 120 frames — DOM widgets update position every frame for (let i = 0; i < 120; i++) { await comfyPage.nextFrame() } const m = await comfyPage.perf.stopMeasuring('dom-widget-idle') recordMeasurement(m) }) ``` **Testing measureText caching (backlog #4):** ```typescript test('canvas text rendering with many nodes', async ({ comfyPage }) => { await comfyPage.workflow.loadWorkflow('large-workflow-50-nodes') await comfyPage.perf.startMeasuring() for (let i = 0; i < 60; i++) { await comfyPage.nextFrame() } const m = await comfyPage.perf.stopMeasuring('text-rendering-50-nodes') recordMeasurement(m) }) ``` ## Reference | Resource | Path | | ----------------- | ----------------------------------------------------- | | Perf test file | `browser_tests/tests/performance.spec.ts` | | PerformanceHelper | `browser_tests/fixtures/helpers/PerformanceHelper.ts` | | Perf reporter | `browser_tests/fixtures/utils/perfReporter.ts` | | CI workflow | `.github/workflows/ci-perf-report.yaml` | | Report generator | `scripts/perf-report.ts` | | Stats utilities | `scripts/perf-stats.ts` | | Backlog | `docs/perf/BACKLOG.md` (local only, not committed) | | Playbook | `docs/perf/PLAYBOOK.md` (local only, not committed) |