Skip to content

feat: add performance testing infrastructure with CDP metrics#9170

Merged
christian-byrne merged 10 commits intomainfrom
perf/testing-infrastructure
Feb 26, 2026
Merged

feat: add performance testing infrastructure with CDP metrics#9170
christian-byrne merged 10 commits intomainfrom
perf/testing-infrastructure

Conversation

@christian-byrne
Copy link
Contributor

@christian-byrne christian-byrne commented Feb 24, 2026

Summary

Add a permanent, non-failing performance regression detection system using Chrome DevTools Protocol metrics, with automatic PR commenting.

Changes

  • What: Performance testing infrastructure — PerformanceHelper fixture class using CDP Performance.getMetrics to collect RecalcStyleCount, LayoutCount, LayoutDuration, TaskDuration, JSHeapUsedSize. Adds @perf Playwright project (Chromium-only, single-threaded, 60s timeout), 4 baseline perf tests, CI workflow with sticky PR comment reporting, and perf-report.js script for generating markdown comparison tables.

Review Focus

  • PerformanceHelper uses page.context().newCDPSession(page) — CDP is Chromium-only, so perf metrics are not collected on Firefox. This is intentional since CDP gives us browser-level style recalc/layout counts that performance.mark/measure cannot capture.
  • The CI workflow uses continue-on-error: true so perf tests never block merging.
  • Baseline comparison uses dawidd6/action-download-artifact to download metrics from the target branch, following the same pattern as pr-size-report.yaml.

Stack

This is the foundation PR for the Firefox performance fix stack:

  1. → This PR: perf testing infrastructure
  2. perf/fix-cursor-cache — cursor style caching (depends on this)
  3. perf/fix-subgraph-svg — SVG pre-rasterization (depends on this)
  4. perf/fix-clippath-raf — RAF batching for clip-path (depends on this)

PRs 2-4 are independent of each other.

┆Issue is synchronized with this Notion page by Unito

Add a permanent, non-failing performance regression detection system:

- PerformanceHelper fixture class using Chrome DevTools Protocol
  (Performance.getMetrics) to collect RecalcStyleCount, LayoutCount,
  LayoutDuration, TaskDuration, and JSHeapUsedSize
- @Perf Playwright project (Chromium-only, single-threaded, 60s timeout)
- 4 baseline perf tests: canvas idle, mouse sweep, cursor mutations,
  DOM widget clipping
- perfReporter module that writes test-results/perf-metrics.json
- CI workflow (ci-perf-report.yaml) that runs perf tests, uploads
  artifacts, downloads baseline from target branch, and posts a
  sticky PR comment with color-coded deltas
- perf-report.js script for generating markdown comparison tables

The system never fails CI — it posts informational PR comments only.
Follows the same patterns as pr-size-report.yaml.

Amp-Thread-ID: https://ampcode.com/threads/T-019c8ed0-59ad-720b-bc4f-6f52dc452844
@github-actions
Copy link

github-actions bot commented Feb 24, 2026

🎨 Storybook: ✅ Built — View Storybook

Details

⏰ Completed at: 02/26/2026, 03:30:36 AM UTC

Links

@github-actions
Copy link

github-actions bot commented Feb 24, 2026

🎭 Playwright: ✅ 547 passed, 0 failed · 5 flaky

📊 Browser Reports
  • chromium: View Report (✅ 534 / ❌ 0 / ⚠️ 5 / ⏭️ 10)
  • chromium-2x: View Report (✅ 2 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • chromium-0.5x: View Report (✅ 1 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • mobile-chrome: View Report (✅ 10 / ❌ 0 / ⚠️ 0 / ⏭️ 0)

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 24, 2026

📝 Walkthrough

Walkthrough

Adds Playwright @perf tests and a PerformanceHelper that captures CDP metrics, a test-side reporter that writes per-measurement JSONs and aggregates them into test-results/perf-metrics.json, a CI workflow to run perf tests and post a Markdown report to PRs, and a script to compare current metrics against a baseline.

Changes

Cohort / File(s) Summary
CI Workflow
\.github/workflows/ci-perf-report.yaml
Adds "CI: Performance Report" workflow with perf-tests job (containerized Playwright performance project, runs Playwright perf tests, uploads perf-metrics artifact) and report job (downloads current & baseline artifacts, runs scripts/perf-report.ts, posts/updates PR comment using sentinel).
Playwright config
playwright.config.ts
Excludes @perf from regular chromium runs (`grepInvert: /@mobile
Fixtures & lifecycle
browser_tests/fixtures/ComfyPage.ts, browser_tests/globalTeardown.ts
Adds perf: PerformanceHelper to ComfyPage, conditionally calls init()/dispose() when tests include @perf; global teardown now invokes writePerfReport() to aggregate recorded measurements into test-results/perf-metrics.json.
Performance helper
browser_tests/fixtures/helpers/PerformanceHelper.ts
New PerformanceHelper using Playwright CDP: init(), dispose(), startMeasuring(), stopMeasuring() returning PerfMeasurement (duration, style recalcs/duration, layouts/duration, taskDuration, heap delta).
Perf aggregation reporter
browser_tests/helpers/perfReporter.ts
Adds recordMeasurement() (writes per-measurement JSONs to test-results/perf-temp) and writePerfReport() (aggregates them into test-results/perf-metrics.json with timestamp/git/branch metadata).
Performance tests
browser_tests/tests/performance.spec.ts
New @perf test suite exercising canvas idle, mouse sweep, cursor sweep, and DOM widget clipping; uses PerformanceHelper to measure, record, and log metrics.
Report generation script
scripts/perf-report.ts
New script that reads current and optional baseline perf-metrics.json, computes deltas for key metrics, renders a Markdown report (with delta indicators and collapsible raw JSON), and writes the report to stdout.
Manifest / package
package.json, manifest changes
Adds/updates scripts/dependencies to support perf reporting tools (e.g., tsx usage in CI).

Sequence Diagram

sequenceDiagram
    participant GH as GH Actions
    participant PT as Playwright Tests
    participant PM as PerformanceHelper
    participant PR as perfReporter
    participant Artifact as Artifact Storage
    participant Script as perf-report.ts
    participant Comment as PR Comment

    GH->>PT: start perf-tests job (container)
    PT->>PM: init() (open CDP session)
    PT->>PM: startMeasuring()
    Note over PT,PM: execute test interactions (frames, mouse, clicks)
    PT->>PM: stopMeasuring(label)
    PM-->>PR: recordMeasurement (write JSON to test-results/perf-temp)
    PT->>PR: writePerfReport() → create test-results/perf-metrics.json
    PT-->>Artifact: upload perf-metrics artifact

    GH->>GH: report job downloads current & baseline artifacts
    GH->>Script: run scripts/perf-report.ts (current, baseline)
    Script->>Script: compute deltas & render Markdown
    Script-->>GH: Markdown report
    GH->>Comment: create/update PR comment with sentinel and report
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 I hopped through frames and counted every flow,

I chased each layout where the style recalcs go.
Little JSON footprints tucked in perf-results' den,
CI reads my tally and posts it back to the PR then.
Hop, measure, report — a rabbit's performance ken.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.38% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: adding a performance testing infrastructure using CDP metrics.
Description check ✅ Passed The description covers the summary, changes, and review focus sections. It explains what was added, key design decisions (CDP Chromium-only), and the non-blocking workflow design.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch perf/testing-infrastructure

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

github-actions bot commented Feb 24, 2026

📦 Bundle: 4.43 MB gzip ⚪ 0 B

Details

Summary

  • Raw size: 20.8 MB baseline 20.8 MB — ⚪ 0 B
  • Gzip: 4.43 MB baseline 4.43 MB — ⚪ 0 B
  • Brotli: 3.42 MB baseline 3.42 MB — ⚪ 0 B
  • Bundles: 227 current • 227 baseline

Category Glance
Vendor & Third-Party ⚪ 0 B (8.84 MB) · Other ⚪ 0 B (7.74 MB) · Data & Services ⚪ 0 B (2.54 MB) · Graph Workspace ⚪ 0 B (986 kB) · Panels & Settings ⚪ 0 B (435 kB) · Views & Navigation ⚪ 0 B (72.1 kB) · + 5 more

App Entry Points — 17.9 kB (baseline 17.9 kB) • ⚪ 0 B

Main entry bundles and manifests

File Before After Δ Raw Δ Gzip Δ Brotli
assets/index-vN3IQZJM.js 17.9 kB 17.9 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
Graph Workspace — 986 kB (baseline 986 kB) • ⚪ 0 B

Graph editor runtime, canvas, workflow orchestration

File Before After Δ Raw Δ Gzip Δ Brotli
assets/GraphView-CHKSgoN-.js 986 kB 986 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
Views & Navigation — 72.1 kB (baseline 72.1 kB) • ⚪ 0 B

Top-level views, pages, and routed surfaces

File Before After Δ Raw Δ Gzip Δ Brotli
assets/CloudAuthTimeoutView-Xo8A130R.js 4.91 kB 4.91 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/CloudForgotPasswordView-DwyjPxGH.js 5.56 kB 5.56 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/CloudLayoutView-BjfkLmUz.js 6.43 kB 6.43 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/CloudLoginView-04FVJVcF.js 11.4 kB 11.4 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/CloudSignupView-D3v9Du-w.js 9.37 kB 9.37 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/CloudSorryContactSupportView-By_3y8r1.js 1.02 kB 1.02 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/CloudSubscriptionRedirectView-aUoXIHy4.js 4.75 kB 4.75 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/CloudSurveyView-DTxFfIG7.js 15.5 kB 15.5 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/layout-CnBYY9vj.js 296 B 296 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/UserCheckView-CMJBGvdw.js 8.41 kB 8.41 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/UserSelectView-BOgQKsjC.js 4.5 kB 4.5 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
Panels & Settings — 435 kB (baseline 435 kB) • ⚪ 0 B

Configuration panels, inspectors, and settings screens

File Before After Δ Raw Δ Gzip Δ Brotli
assets/AboutPanel-UsTZO3hp.js 9.79 kB 9.79 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/cloudRemoteConfig-D0V24PPf.js 1.44 kB 1.44 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/config-Tm3IpjoS.js 996 B 996 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/ExtensionPanel-BICWcQ1b.js 9.38 kB 9.38 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/KeybindingPanel-DsQF_4w1.js 12.3 kB 12.3 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/LegacyCreditsPanel-C5FeKDSg.js 20.6 kB 20.6 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/refreshRemoteConfig-DwrigCkG.js 1.14 kB 1.14 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/SecretsPanel-CzDNlIZm.js 21.5 kB 21.5 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/ServerConfigPanel-CvCjtX77.js 6.44 kB 6.44 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-axAeoUrZ.js 28.8 kB 28.8 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-B2VYgS-a.js 29.9 kB 29.9 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-BAN61mfQ.js 30.5 kB 30.5 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-BNn9zn5v.js 38.5 kB 38.5 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-CIIZ9BHS.js 28.7 kB 28.7 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-CRqB5n8m.js 34.2 kB 34.2 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-Cs_NVkbc.js 32.4 kB 32.4 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-CYbBf6ct.js 27.9 kB 27.9 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-DhTbqRj2.js 23.9 kB 23.9 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-DW8FtJHC.js 24.5 kB 24.5 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-FzjdIfFm.js 27.8 kB 27.8 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/SubscriptionPanel-Tbf5adyJ.js 18.1 kB 18.1 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/UserPanel-Cel9ycSv.js 6.16 kB 6.16 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
User & Accounts — 16 kB (baseline 16 kB) • ⚪ 0 B

Authentication, profile, and account management bundles

File Before After Δ Raw Δ Gzip Δ Brotli
assets/auth-CqQS_4lN.js 3.4 kB 3.4 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/auth-DcpRWmhe.js 357 B 357 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/firebaseAuthStore-CqDTroe5.js 788 B 788 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/PasswordFields-DLbVLg8O.js 4.51 kB 4.51 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/SignUpForm-DzJQBWxp.js 3.01 kB 3.01 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/UpdatePasswordContent-9V1A64wH.js 2.37 kB 2.37 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WorkspaceProfilePic-CK1MFS2B.js 1.57 kB 1.57 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
Editors & Dialogs — 736 B (baseline 736 B) • ⚪ 0 B

Modals, dialogs, drawers, and in-app editors

File Before After Δ Raw Δ Gzip Δ Brotli
assets/useSubscriptionDialog-CkWDQjBE.js 736 B 736 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
UI Components — 47.1 kB (baseline 47.1 kB) • ⚪ 0 B

Reusable component library chunks

File Before After Δ Raw Δ Gzip Δ Brotli
assets/Button-D1z3poyI.js 2.98 kB 2.98 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/CloudBadge-BrLRSAsL.js 1.24 kB 1.24 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/cloudFeedbackTopbarButton-CZ0suqLB.js 1.59 kB 1.59 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/ComfyQueueButton-B_W-sNLD.js 8.02 kB 8.02 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/ComfyQueueButton-LQwXuB5A.js 793 B 793 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/FormSearchInput--hCtYSQV.js 3.73 kB 3.73 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/ScrubableNumberInput-DecBFGbM.js 5.94 kB 5.94 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/SubscribeButton-BZgtOo2g.js 2.48 kB 2.48 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/TopbarBadge-Dd3SHM68.js 7.44 kB 7.44 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/UserAvatar-CnQQLXB-.js 1.17 kB 1.17 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/useTerminalTabs-CoLdDyV1.js 9.84 kB 9.84 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetButton-VJ452z4N.js 1.84 kB 1.84 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
Data & Services — 2.54 MB (baseline 2.54 MB) • ⚪ 0 B

Stores, services, APIs, and repositories

File Before After Δ Raw Δ Gzip Δ Brotli
assets/api-BUYukRxA.js 676 kB 676 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/audioService-D5jfSvoA.js 1.73 kB 1.73 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/bootstrapStore-jz0ILcOG.js 2.08 kB 2.08 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/dialogService-Diyqrb-Z.js 1.74 MB 1.74 MB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/dialogService-tEpUN0yI.js 725 B 725 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/extensionStore-BV2zsE9g.js 12.1 kB 12.1 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/keybindingService-DVGQJ-cn.js 6.52 kB 6.52 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/load3dService-BWe74-Gk.js 91 kB 91 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/releaseStore-a7jysDhF.js 760 B 760 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/releaseStore-D97HMhL0.js 7.96 kB 7.96 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/serverConfigStore-CkFMhmxK.js 2.32 kB 2.32 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settingStore-Cyq_Iclk.js 744 B 744 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/userStore-BO4t-5AZ.js 1.85 kB 1.85 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/workflowDraftStore-CCVPnjcx.js 736 B 736 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
Utilities & Hooks — 55.5 kB (baseline 55.5 kB) • ⚪ 0 B

Helpers, composables, and utility bundles

File Before After Δ Raw Δ Gzip Δ Brotli
assets/_plugin-vue_export-helper-ralzwvFM.js 315 B 315 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/audioUtils-G1Th5rWn.js 858 B 858 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/colorUtil-CY7QMUhQ.js 7 kB 7 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/envUtil-Clzmwvt4.js 466 B 466 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/markdownRendererUtil-Cddas8Zl.js 1.56 kB 1.56 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/SkeletonUtils-BputJAFn.js 133 B 133 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/subscriptionCheckoutUtil-BlWMG8mX.js 2.53 kB 2.53 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/useCurrentUser-C5HGmWPl.js 722 B 722 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/useErrorHandling-BOoJuLPx.js 1.5 kB 1.5 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/useExternalLink-D2JSE_FX.js 1.66 kB 1.66 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/useFeatureFlags-RyrV8Xv_.js 4.14 kB 4.14 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/useLoad3d-2MtIObFW.js 859 B 859 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/useLoad3d-DuKA1u8V.js 14.6 kB 14.6 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/useLoad3dViewer-B0dKACGq.js 14.1 kB 14.1 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/useLoad3dViewer-C0J1iLiw.js 838 B 838 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/useWorkspaceSwitch-1QBzMUJo.js 1.25 kB 1.25 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/useWorkspaceUI-BSmgkgVw.js 3 kB 3 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
Vendor & Third-Party — 8.84 MB (baseline 8.84 MB) • ⚪ 0 B

External libraries and shared vendor chunks

File Before After Δ Raw Δ Gzip Δ Brotli
assets/vendor-axios-Cp6hch1I.js 70.7 kB 70.7 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-chart-BxkFiWzp.js 399 kB 399 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-firebase-BvMr43CG.js 836 kB 836 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-i18n-DNX73mqE.js 133 kB 133 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-markdown-D5S6AC80.js 103 kB 103 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-other-DrYd4O-6.js 1.52 MB 1.52 MB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-primevue-BnCPTL0g.js 1.73 MB 1.73 MB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-reka-ui-DVmi2O2Z.js 388 kB 388 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-sentry-SQwstEKc.js 182 kB 182 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-three-LBLOE6BD.js 1.8 MB 1.8 MB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-tiptap-BnYkbQDM.js 634 kB 634 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-vue-core-DtiQ1dr9.js 311 kB 311 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-vueuse-D2jVNnmE.js 113 kB 113 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-xterm-DK6Ygydn.js 374 kB 374 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-yjs-CP_4YO8u.js 143 kB 143 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-zod-DcCUUPIi.js 109 kB 109 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
Other — 7.74 MB (baseline 7.74 MB) • ⚪ 0 B

Bundles that do not match a named category

File Before After Δ Raw Δ Gzip Δ Brotli
assets/AnimationControls-e1OB6oJR.js 4.61 kB 4.61 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/ApiNodesSignInContent-xPxNG93N.js 2.69 kB 2.69 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/AudioPreviewPlayer-BTVVmCZZ.js 10.9 kB 10.9 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/auto-BTnZwrs2.js 1.7 kB 1.7 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/BaseViewTemplate-C7TdQsa4.js 1.78 kB 1.78 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/CancelSubscriptionDialogContent-Cwd3J0Y3.js 4.79 kB 4.79 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/changeTracker-Bu1yKe_F.js 9.38 kB 9.38 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/changeTracker-C45Uialp.js 757 B 757 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/cloudBadges-C_YrGTkK.js 1.37 kB 1.37 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/CloudRunButtonWrapper-BKyw50BE.js 1.68 kB 1.68 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/cloudSessionCookie-CjejHRVF.js 3.1 kB 3.1 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/cloudSubscription-BYagZTGR.js 1.33 kB 1.33 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/comfy-logo-single-D9MrYETV.js 198 B 198 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/ComfyOrgHeader-CuEodz4y.js 910 B 910 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-Bdca9E4N.js 16.7 kB 16.7 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-Boqbp4FE.js 16.9 kB 16.9 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-ClFTZZY8.js 18.8 kB 18.8 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-CnE4PBoq.js 16.1 kB 16.1 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-CNvwc2P-.js 17.5 kB 17.5 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-CsOV8HcO.js 16.1 kB 16.1 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-DBggpNhX.js 15.2 kB 15.2 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-DC4TJj5_.js 15.1 kB 15.1 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-DPVHiFlP.js 16.1 kB 16.1 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-eBbhcNBX.js 17.5 kB 17.5 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-oGOH5erQ.js 15.9 kB 15.9 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/constants-htt0vt7m.js 579 B 579 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/core-hC2jixDY.js 72.7 kB 72.7 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/CreateWorkspaceDialogContent-CusEOU2w.js 5.53 kB 5.53 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/CurrentUserPopoverWorkspace-DDlKVYXc.js 19.9 kB 19.9 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/DeleteWorkspaceDialogContent-fzFM-fLR.js 4.23 kB 4.23 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/EditWorkspaceDialogContent-BwvToat1.js 5.33 kB 5.33 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/FreeTierDialogContent-Va2igSTW.js 5.39 kB 5.39 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/GlobalToast-CQ-ehHXO.js 2.91 kB 2.91 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/graphHasMissingNodes-TFq6H1VK.js 761 B 761 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/groupNode-CxRF1w7d.js 71.8 kB 71.8 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/i18n-BY6i52_i.js 526 kB 526 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/i18n-C3T8tTh4.js 199 B 199 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/InviteMemberDialogContent-C4CUTYgY.js 7.38 kB 7.38 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/InviteMemberUpsellDialogContent-J2gTiPm8.js 3.82 kB 3.82 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/LeaveWorkspaceDialogContent-BUjeFJFn.js 4.06 kB 4.06 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/Load3D-BZ8bQhYg.js 16.2 kB 16.2 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/Load3D-CFMSkxNv.js 1.07 kB 1.07 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/load3d-G_Lt6IR-.js 14.7 kB 14.7 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/Load3DConfiguration-DO3a8FsY.js 6.27 kB 6.27 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/Load3DControls-CN9E9act.js 30.9 kB 30.9 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/Load3dViewerContent-C_OMMOH3.js 23 kB 23 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/Load3dViewerContent-DHJpH17_.js 993 B 993 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-B_4-yaOE.js 154 kB 154 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-C1cLXKzG.js 151 kB 151 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-Cm0JA816.js 176 kB 176 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-CUFqlSSt.js 205 kB 205 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-DCM5-M4s.js 128 kB 128 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-DGdk9JD_.js 183 kB 183 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-DJEwO7w1.js 146 kB 146 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-Du9Mttqc.js 169 kB 169 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-nDJB1-zI.js 147 kB 147 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-ss5tT-9f.js 130 kB 130 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-ZrG2D_5c.js 149 kB 149 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/Media3DTop-CouvCIvd.js 1.82 kB 1.82 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/MediaAudioTop-CJNtJ1Vf.js 1.43 kB 1.43 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/MediaImageTop-CsrUK1I8.js 1.75 kB 1.75 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/MediaOtherTop-CGnATDyM.js 1.02 kB 1.02 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/MediaTextTop-DDsKiNmL.js 1.01 kB 1.01 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/MediaVideoTop-BvDxKWex.js 2.77 kB 2.77 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nightlyBadges-DDQcFnkC.js 1 kB 1 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs--tG2_kTy.js 390 kB 390 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-B1CcRnYm.js 443 kB 443 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-BOkmcPF-.js 397 kB 397 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-C9THNc4O.js 442 kB 442 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-CBfcBh4b.js 362 kB 362 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-CZlGuvdG.js 385 kB 385 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-DB-aHpeo.js 393 kB 393 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-DPlU1YTi.js 358 kB 358 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-mmU8eLDX.js 409 kB 409 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-rItghg3p.js 483 kB 483 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-s5_Usl0F.js 393 kB 393 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeTemplates-DDhV53Ia.js 9.29 kB 9.29 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/OBJLoader2WorkerModule-DTMpvldF.js 109 kB 109 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/onboardingCloudRoutes-B8XuUHMX.js 5.41 kB 5.41 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/Popover-BIYdg9E5.js 3.65 kB 3.65 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/Preview3d-BJu2qqxz.js 4.81 kB 4.81 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/previousFullPath-dOB52iw7.js 1.39 kB 1.39 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/RemoveMemberDialogContent-5MXUXXHc.js 4.04 kB 4.04 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/RevokeInviteDialogContent-DTh0AMjK.js 3.95 kB 3.95 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/rolldown-runtime-DLICfi3-.js 1.97 kB 1.97 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/saveMesh-BxVoOFYF.js 3.38 kB 3.38 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/SelectValue-C_7cycpB.js 8.94 kB 8.94 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/SignInContent-StOaxmKl.js 18.9 kB 18.9 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/signInSchema-DE78q_5I.js 1.53 kB 1.53 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/Slider-DVkw5nPu.js 3.52 kB 3.52 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/src-CbNGuSYA.js 251 B 251 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/SubscribeToRun-C1QGkyUW.js 2.2 kB 2.2 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/SubscriptionBenefits-BYIJyGB1.js 2.01 kB 2.01 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/SubscriptionPanelContentWorkspace-CxiwZVjP.js 21.5 kB 21.5 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/SubscriptionPanelContentWorkspace-e5Xnnjex.js 920 B 920 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/SubscriptionRequiredDialogContent-BuSP-6Tn.js 25.7 kB 25.7 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/SubscriptionRequiredDialogContentWorkspace-BT3uE5H-.js 46.3 kB 46.3 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/telemetry-zZf2dHJ2.js 226 B 226 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/tierBenefits-BFnC52kd.js 3.66 kB 3.66 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/types-DT3N7am7.js 204 B 204 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/ValueControlPopover-CQo9kQkk.js 4.92 kB 4.92 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/VideoPlayOverlay-D-ZhKuWc.js 1.35 kB 1.35 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/widget-NeEr3XWN.js 586 B 586 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetBoundingBox-BYbwNME9.js 283 B 283 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetBoundingBox-GzA4D-L-.js 3.19 kB 3.19 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetChart-DhNMsSAU.js 2.21 kB 2.21 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetColorPicker-ykkxGhLh.js 2.9 kB 2.9 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetCurve-CNAR8Elf.js 9.35 kB 9.35 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetGalleria-CmUesy36.js 3.61 kB 3.61 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetImageCompare-Dfo8GHhe.js 6.95 kB 6.95 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetImageCrop-CaoUgIXC.js 22.1 kB 22.1 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetInputNumber-C090TijP.js 469 B 469 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetInputNumber-D9yHpoqG.js 18.7 kB 18.7 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetInputText-DASYqR-h.js 1.86 kB 1.86 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetLayoutField-O1dUcz97.js 1.98 kB 1.98 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetLegacy-BfLAARSY.js 745 B 745 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetMarkdown-BryTE-VI.js 2.93 kB 2.93 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/widgetPropFilter-_HM7alVA.js 1.1 kB 1.1 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetRecordAudio-DRCfrXxb.js 17.3 kB 17.3 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetSelect-C3oRf5vU.js 58.1 kB 58.1 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetTextarea-C1btAxfw.js 3.96 kB 3.96 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetToggleSwitch-6Htnt0a8.js 6.8 kB 6.8 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/widgetTypes-Br_tbhcL.js 393 B 393 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetWithControl-DTXO0Y7G.js 4.1 kB 4.1 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WorkspacePanelContent-CwX5E_4c.js 29.3 kB 29.3 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (2)
browser_tests/tests/performance.spec.ts (1)

22-66: "canvas mouse interaction" and "cursor style mutations" tests have identical interaction loops — one is redundant.

Both tests perform the exact same 100-step mouse.move sweep with the same trajectory formula, producing identical measurements under different labels. If the intent is to measure distinct phenomena, the interaction pattern needs to differ (e.g., hover-only vs. move-with-clicks, or different trajectories).

♻️ Suggested approach
-  test('cursor style mutations during mouse sweep', async ({ comfyPage }) => {
-    await comfyPage.workflow.loadWorkflow('default')
-    await comfyPage.perf.startMeasuring()
-
-    const canvas = comfyPage.canvas
-    const box = await canvas.boundingBox()
-    if (box) {
-      // Sweep mouse across entire canvas — crosses nodes, empty space, slots
-      for (let i = 0; i < 100; i++) {
-        await comfyPage.page.mouse.move(
-          box.x + (box.width * i) / 100,
-          box.y + (box.height * (i % 3)) / 3
-        )
-      }
-    }
-
-    const m = await comfyPage.perf.stopMeasuring('cursor-sweep')
-    recordMeasurement(m)
-    console.log(`Cursor sweep: ${m.styleRecalcs} style recalcs`)
-  })

If this scenario is intentionally distinct, document what makes it different from canvas-mouse-sweep.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@browser_tests/tests/performance.spec.ts` around lines 22 - 66, Two tests
("canvas mouse interaction style recalculations" using
comfyPage.perf.stopMeasuring('canvas-mouse-sweep') and "cursor style mutations
during mouse sweep" using comfyPage.perf.stopMeasuring('cursor-sweep'))
currently run the exact same 100-step comfyPage.page.mouse.move trajectory,
making the second test redundant; either modify the second test's interaction
pattern (e.g., change comfyPage.page.mouse.move trajectory, add hover-only
checks, include clicks/drag events, or vary step count/timing) to measure a
different phenomenon, or explicitly document in the "cursor style mutations
during mouse sweep" test why it is intentionally identical to the
'canvas-mouse-sweep' case (update test name/comment) so reviewers understand the
distinction.
browser_tests/fixtures/helpers/PerformanceHelper.ts (1)

3-11: PerfSnapshot is exported but has no external consumers — consider keeping it internal.

Only PerfMeasurement is imported by perfReporter.ts. PerfSnapshot is only used within this file.

♻️ Proposed change
-export interface PerfSnapshot {
+interface PerfSnapshot {

Based on learnings: "Do not export declarations unless they are actually used elsewhere in the codebase. Keep the public API surface minimal."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@browser_tests/fixtures/helpers/PerformanceHelper.ts` around lines 3 - 11, The
PerfSnapshot interface is exported but not used outside this file (only
PerfMeasurement is imported by perfReporter.ts); remove the export from
PerfSnapshot to keep the public API minimal and internalize it. Edit the
declaration of PerfSnapshot in PerformanceHelper.ts (the interface named
PerfSnapshot) to be non-exported, ensure any internal references in this file
still compile, and keep exporting only PerfMeasurement for external use.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/ci-perf-report.yaml:
- Around line 50-55: The Upload perf metrics step (uses:
actions/upload-artifact@v6, name: "Upload perf metrics") should explicitly set
if-no-files-found: warn and run unconditionally by adding if: always(), and the
corresponding download step (uses: actions/download-artifact@v7, likely named
"Download PR perf metrics") should tolerate a missing artifact by adding
continue-on-error: true (or if-no-files-found: warn if supported); update those
two steps so a missing test-results/perf-metrics.json does not cause the report
job to fail with an unhelpful error.
- Around line 69-71: Replace the floating action reference "uses:
actions/setup-node@v4" with a pinned commit SHA for the setup-node action (i.e.,
change the uses line to the exact "actions/setup-node@<commit-sha>"
corresponding to the desired v4 release) so the workflow is immutably pinned;
keep the existing node-version: 22 input and update only the uses value to the
full commit SHA obtained from the actions/setup-node v4 repo tags/releases.

In `@browser_tests/globalTeardown.ts`:
- Around line 10-11: writePerfReport() in globalTeardown is a no-op because
worker processes recordMeasurement() into a different module instance; instead,
change the measurement pipeline so main process collects them: either implement
a Playwright custom reporter (use onTestEnd in a reporter to receive per-test
results and aggregate measurements from testInfo.attach) or have workers write
per-test temp files (from tests calling recordMeasurement or using
testInfo.attach) and modify globalTeardown to read and aggregate those temp
files before calling writePerfReport/aggregation logic; update references in
perfReporter.ts, globalTeardown, and test code so measurements are produced in
workers and consumed/aggregated in the main process.

In `@browser_tests/helpers/perfReporter.ts`:
- Around line 13-32: The module-level measurements array (measurements) is
populated in worker processes via recordMeasurement but writePerfReport runs in
the main process so measurements is always empty; replace this pattern with a
Playwright Reporter that collects per-test attachments: implement a PerfReporter
class implementing Reporter with a private measurements array, implement
onTestEnd to read JSON from result.attachments (name e.g. 'perf-measurement')
and push parsed PerfMeasurement into this.measurements, and implement onEnd to
mkdirSync and writeFileSync perf-metrics.json if this.measurements is non-empty;
update tests to call comfyPage.perf.stopMeasuring(...) and use
testInfo.attach('perf-measurement', { body: JSON.stringify(m), contentType:
'application/json' }) so measurements flow from workers to the reporter.

In `@browser_tests/tests/performance.spec.ts`:
- Around line 29-38: The loop that performs mouse moves is skipped when
canvas.boundingBox() returns null, causing stopMeasuring() to record a
misleading zero-interaction measurement; update each occurrence (where
canvas.boundingBox() is used before the mouse.move loop) to explicitly handle
null by either awaiting the canvas to be visible (e.g., retry/wait for selector
or bounding box) or throwing/asserting with a clear error so the test fails
instead of recording empty metrics; reference the canvas.boundingBox() call, the
comfyPage.page.mouse.move loop, and stopMeasuring() so you add the null-check
and an explicit fail/wait before attempting the interactions.

In `@scripts/perf-report.js`:
- Line 1: This new JS file violates the "no new JavaScript files" rule—rename
scripts/perf-report.js to scripts/perf-report.ts, remove the top-line "//
`@ts-check`" and convert any JSDoc-typed variables into proper TypeScript types
(update function signatures / variable declarations in this file), and ensure
the project's run/test scripts or README invoke it via tsx or ts-node (or your
project's TypeScript runner). Locate the file by name (scripts/perf-report.js →
scripts/perf-report.ts) and update imports/exports inside the file to use
TypeScript syntax so it compiles cleanly with the project's TypeScript
toolchain.
- Line 44: ESLint `no-console` is causing CI failures due to console.log usage
in scripts/perf-report.js; either add a file-level disable (/* eslint-disable
no-console */) at the top of scripts/perf-report.js or replace each console.log
call with process.stdout.write (ensuring you append "\n" where needed) — update
the console.log occurrences referenced in the diff and the other similar call
(the second console.log in the file) accordingly.
- Around line 78-93: The current percent calculations (recalcPct, layoutPct,
taskPct) silently return 0 when the baseline is zero, hiding new regressions;
update each calculation (e.g., recalcPct for base.styleRecalcs, layoutPct for
base.layouts) to detect base === 0 and m.* > 0 and set a sentinel (e.g., null or
Infinity) instead of 0; then update formatDelta to recognize that sentinel and
render a clear "new/regression" marker (or arrow) so lines.push uses
formatDelta(pct) and reports new non-zero measurements when baseline was zero.

---

Nitpick comments:
In `@browser_tests/fixtures/helpers/PerformanceHelper.ts`:
- Around line 3-11: The PerfSnapshot interface is exported but not used outside
this file (only PerfMeasurement is imported by perfReporter.ts); remove the
export from PerfSnapshot to keep the public API minimal and internalize it. Edit
the declaration of PerfSnapshot in PerformanceHelper.ts (the interface named
PerfSnapshot) to be non-exported, ensure any internal references in this file
still compile, and keep exporting only PerfMeasurement for external use.

In `@browser_tests/tests/performance.spec.ts`:
- Around line 22-66: Two tests ("canvas mouse interaction style recalculations"
using comfyPage.perf.stopMeasuring('canvas-mouse-sweep') and "cursor style
mutations during mouse sweep" using
comfyPage.perf.stopMeasuring('cursor-sweep')) currently run the exact same
100-step comfyPage.page.mouse.move trajectory, making the second test redundant;
either modify the second test's interaction pattern (e.g., change
comfyPage.page.mouse.move trajectory, add hover-only checks, include clicks/drag
events, or vary step count/timing) to measure a different phenomenon, or
explicitly document in the "cursor style mutations during mouse sweep" test why
it is intentionally identical to the 'canvas-mouse-sweep' case (update test
name/comment) so reviewers understand the distinction.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a94574d and a440e46.

📒 Files selected for processing (8)
  • .github/workflows/ci-perf-report.yaml
  • browser_tests/fixtures/ComfyPage.ts
  • browser_tests/fixtures/helpers/PerformanceHelper.ts
  • browser_tests/globalTeardown.ts
  • browser_tests/helpers/perfReporter.ts
  • browser_tests/tests/performance.spec.ts
  • playwright.config.ts
  • scripts/perf-report.js

christian-byrne and others added 2 commits February 24, 2026 01:31
- Replace console.log with process.stdout.write in perf-report.js (oxlint no-console)
- Pin actions/setup-node to v6 to match .pinact.yaml ignore list
- Add if: always() and if-no-files-found: warn to artifact upload
- Add continue-on-error: true to PR metrics download
- Fix measurement pipeline: write temp files per-test, aggregate in globalTeardown
- Remove duplicate cursor-sweep test (identical to canvas-mouse-sweep)
- Make PerfSnapshot interface non-exported (internal only)
- Throw on null boundingBox() instead of silently recording empty metrics

Amp-Thread-ID: https://ampcode.com/threads/T-019c8ef7-b4c9-70cc-b58f-99c036dee788
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
.github/workflows/ci-perf-report.yaml (1)

34-36: ⚠️ Potential issue | 🟠 Major

Pin actions/checkout and actions/setup-node to immutable SHAs.
Floating tags are mutable and can change without review; the rest of the workflow already uses pinned SHAs. Please pin both to the exact commit SHA for the chosen v6 release.

#!/bin/bash
set -euo pipefail

for repo in actions/checkout actions/setup-node; do
  echo "== $repo v6 tags =="
  curl -s "https://api.github.com/repos/$repo/tags" \
    | jq -r '.[] | select(.name | test("^v6")) | "\(.name) \(.commit.sha)"' \
    | head -n 5
done

Also applies to: 70-72

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci-perf-report.yaml around lines 34 - 36, Replace the
floating tags for both actions with immutable commit SHAs: change uses:
actions/checkout@v6 to uses: actions/checkout@<commit-sha-for-v6> and change
uses: actions/setup-node@v6 to uses: actions/setup-node@<commit-sha-for-v6>;
update both occurrences (the existing uses entries for actions/checkout and
actions/setup-node) to the exact commit SHAs you looked up so the workflow pins
the specific v6 commits rather than a mutable tag.
🧹 Nitpick comments (1)
browser_tests/fixtures/helpers/PerformanceHelper.ts (1)

46-47: Prefer a nested function declaration for get.
This aligns with the repo preference for pure function declarations and keeps helpers consistent.

♻️ Suggested tweak
-    const get = (name: string) =>
-      metrics.find((m) => m.name === name)?.value ?? 0
+    function get(name: string) {
+      return metrics.find((m) => m.name === name)?.value ?? 0
+    }

Based on learnings: Prefer pure function declarations over function expressions (e.g., use function foo() { ... } instead of const foo = () => { ... }) for pure functions in the repository. Function declarations are more functional-leaning, offer better hoisting clarity, and can improve readability and tooling consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@browser_tests/fixtures/helpers/PerformanceHelper.ts` around lines 46 - 47,
Replace the arrow-function helper with a nested function declaration: change the
const get = (name: string) => metrics.find(...)?.value ?? 0 into a function
get(name: string): number { ... } that returns metrics.find((m) => m.name ===
name)?.value ?? 0; keep it nested where metrics is in scope and preserve the
return type and behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In @.github/workflows/ci-perf-report.yaml:
- Around line 34-36: Replace the floating tags for both actions with immutable
commit SHAs: change uses: actions/checkout@v6 to uses:
actions/checkout@<commit-sha-for-v6> and change uses: actions/setup-node@v6 to
uses: actions/setup-node@<commit-sha-for-v6>; update both occurrences (the
existing uses entries for actions/checkout and actions/setup-node) to the exact
commit SHAs you looked up so the workflow pins the specific v6 commits rather
than a mutable tag.

---

Nitpick comments:
In `@browser_tests/fixtures/helpers/PerformanceHelper.ts`:
- Around line 46-47: Replace the arrow-function helper with a nested function
declaration: change the const get = (name: string) => metrics.find(...)?.value
?? 0 into a function get(name: string): number { ... } that returns
metrics.find((m) => m.name === name)?.value ?? 0; keep it nested where metrics
is in scope and preserve the return type and behavior.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a440e46 and 75a6981.

📒 Files selected for processing (5)
  • .github/workflows/ci-perf-report.yaml
  • browser_tests/fixtures/helpers/PerformanceHelper.ts
  • browser_tests/helpers/perfReporter.ts
  • browser_tests/tests/performance.spec.ts
  • scripts/perf-report.js
🚧 Files skipped from review as they are similar to previous changes (3)
  • browser_tests/tests/performance.spec.ts
  • browser_tests/helpers/perfReporter.ts
  • scripts/perf-report.js

@github-actions
Copy link

github-actions bot commented Feb 24, 2026

⚡ Performance Report

No baseline found — showing absolute values.

Metric Value
canvas-idle: style recalcs 122
canvas-idle: layouts 0
canvas-idle: task duration 403ms
canvas-idle: heap delta -3.6 MB
canvas-mouse-sweep: style recalcs 172
canvas-mouse-sweep: layouts 12
canvas-mouse-sweep: task duration 771ms
canvas-mouse-sweep: heap delta -2.7 MB
dom-widget-clipping: style recalcs 43
dom-widget-clipping: layouts 0
dom-widget-clipping: task duration 352ms
dom-widget-clipping: heap delta 6.4 MB
Raw data
{
  "timestamp": "2026-02-26T03:32:32.197Z",
  "gitSha": "d8b1d4ac8db129e261a5f35de9aea33b69ab0967",
  "branch": "perf/testing-infrastructure",
  "measurements": [
    {
      "name": "canvas-idle",
      "durationMs": 2034.9340000000211,
      "styleRecalcs": 122,
      "styleRecalcDurationMs": 16.698999999999998,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 403.1600000000001,
      "heapDeltaBytes": -3781536
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 1806.8290000000218,
      "styleRecalcs": 172,
      "styleRecalcDurationMs": 41.969,
      "layouts": 12,
      "layoutDurationMs": 3.207,
      "taskDurationMs": 771.304,
      "heapDeltaBytes": -2803328
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 577.5359999999807,
      "styleRecalcs": 43,
      "styleRecalcDurationMs": 13.268,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 351.864,
      "heapDeltaBytes": 6665424
    }
  ]
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
browser_tests/fixtures/helpers/PerformanceHelper.ts (1)

46-47: Use a function declaration for the pure helper.

This helper is pure and doesn’t require closure semantics, so prefer a function declaration.

♻️ Suggested refactor
-    const get = (name: string) =>
-      metrics.find((m) => m.name === name)?.value ?? 0
+    function get(name: string): number {
+      return metrics.find((m) => m.name === name)?.value ?? 0
+    }

Based on learnings: Prefer pure function declarations over function expressions (e.g., use function foo() { ... } instead of const foo = () => { ... }) for pure functions in the repository. Function declarations are more functional-leaning, offer better hoisting clarity, and can improve readability and tooling consistency. Apply this guideline across TypeScript files in Comfy-Org/ComfyUI_frontend, including story and UI component code, except where a function expression is semantically required (e.g., callbacks, higher-order functions with closures).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@browser_tests/fixtures/helpers/PerformanceHelper.ts` around lines 46 - 47,
Replace the const arrow helper with a pure function declaration: rename the
variable-style helper "get" (currently declared as const get = (name: string) =>
...) to a function declaration function get(name: string) { ... } that returns
metrics.find((m) => m.name === name)?.value ?? 0; update any local references if
needed so the semantics stay identical but use the function declaration form for
clarity and hoisting.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@browser_tests/fixtures/helpers/PerformanceHelper.ts`:
- Line 45: The call to this.cdp.send('Performance.getMetrics') is untyped so
metrics becomes implicit any; define a TypeScript response type for the CDP
Performance.getMetrics result (e.g., an interface with metrics: Array<{name:
string; value: number; [key: string]: unknown}>) and use that type as the
generic/return type on the CDPSession.send call in PerformanceHelper (the same
method where this.cdp.send(...) is invoked) so the result is strongly typed
instead of any.

---

Nitpick comments:
In `@browser_tests/fixtures/helpers/PerformanceHelper.ts`:
- Around line 46-47: Replace the const arrow helper with a pure function
declaration: rename the variable-style helper "get" (currently declared as const
get = (name: string) => ...) to a function declaration function get(name:
string) { ... } that returns metrics.find((m) => m.name === name)?.value ?? 0;
update any local references if needed so the semantics stay identical but use
the function declaration form for clarity and hoisting.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 75a6981 and 33d8b73.

📒 Files selected for processing (2)
  • browser_tests/fixtures/helpers/PerformanceHelper.ts
  • scripts/perf-report.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • scripts/perf-report.js

- Convert perf-report.js to TypeScript (no new JS files rule)
- Fix zero-baseline delta logic to flag regressions when baseline is 0
- Type CDP Performance.getMetrics response to avoid implicit any
- Convert arrow function to function declaration in PerformanceHelper

Amp-Thread-ID: https://ampcode.com/threads/T-019c9204-b28e-7739-83ab-e1737a1fd581
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/ci-perf-report.yaml:
- Around line 63-68: The job-level permissions for the report job only set
pull-requests: write and are missing contents: read required by the
actions/checkout@v6 step; update the report job's permissions block to include
contents: read alongside pull-requests: write so the checkout action has the
necessary read access.

In `@scripts/perf-report.ts`:
- Around line 76-79: The code path where base is falsy pushes only styleRecalcs
and layouts rows and omits the task-duration row, so update the block that
checks if (!base) to also push a task-duration line into lines (matching the
same markdown format used elsewhere) using the measurement's duration property
(e.g., m.taskDuration) so the new-measurement case emits rows for task duration,
styleRecalcs, and layouts; reference the variables base, m, and lines to locate
and modify the code.
- Around line 59-63: JSON.parse of the metrics files can throw and currently
crashes the script; wrap the file-read + JSON.parse for CURRENT_PATH and
BASELINE_PATH in try/catch (or extract a small safeParse function) so malformed
JSON is caught, log a warning including the file path and error, and set the
corresponding variable (current or baseline) to a safe default (e.g., an empty
PerfReport object or null for baseline) so the report step continues without
throwing; reference CURRENT_PATH, BASELINE_PATH, PerfReport, current, baseline,
readFileSync, existsSync, and JSON.parse when locating where to add the
try/catch.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 33d8b73 and fe24533.

📒 Files selected for processing (3)
  • .github/workflows/ci-perf-report.yaml
  • browser_tests/fixtures/helpers/PerformanceHelper.ts
  • scripts/perf-report.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
scripts/perf-report.ts (1)

59-63: ⚠️ Potential issue | 🟠 Major

JSON.parse on unvalidated file content can crash the report step.

Both parse calls (line 59 for current, lines 61-63 for baseline) throw on malformed JSON, which would fail the CI reporting step and make the pipeline non-silent.

This was flagged in a previous review and remains unaddressed in the current code.

🛡️ Proposed fix — wrap reads in a safe helper
+function readReport(path: string): PerfReport | null {
+  if (!existsSync(path)) return null
+  try {
+    return JSON.parse(readFileSync(path, 'utf-8')) as PerfReport
+  } catch {
+    return null
+  }
+}
+
 function main() {
-  if (!existsSync(CURRENT_PATH)) {
+  const current = readReport(CURRENT_PATH)
+  if (!current) {
     process.stdout.write(
       '## ⚡ Performance Report\n\nNo perf metrics found. Perf tests may not have run.\n'
     )
     process.exit(0)
   }
-
-  const current: PerfReport = JSON.parse(readFileSync(CURRENT_PATH, 'utf-8'))
-
-  const baseline: PerfReport | null = existsSync(BASELINE_PATH)
-    ? JSON.parse(readFileSync(BASELINE_PATH, 'utf-8'))
-    : null
+  const baseline = readReport(BASELINE_PATH)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/perf-report.ts` around lines 59 - 63, Wrap the raw file
reads/JSON.parse calls in a safe helper (e.g., parseJsonFile or safeReadJson)
and use it for the current and baseline reads so malformed JSON won't throw;
implement parseJsonFile(path: string): T | null that reads with readFileSync and
returns parsed JSON on success and null on failure (catching and optionally
logging the error), then replace the inline
JSON.parse(readFileSync(CURRENT_PATH, 'utf-8')) with
parseJsonFile<PerfReport>(CURRENT_PATH) and the baseline branch to
parseJsonFile(BASELINE_PATH) (keeping existsSync check) and ensure variables
current and baseline handle null results appropriately.
🧹 Nitpick comments (2)
scripts/perf-report.ts (2)

74-75: Measurements removed from PR are not surfaced in the report.

The loop iterates only current.measurements. A test renamed or deleted in the PR will disappear silently; the baseline entry that no longer has a PR counterpart is never reported.

♻️ Proposed addition — report missing (removed) measurements
     for (const m of current.measurements) {
       // ... existing per-measurement block ...
     }
+
+    // Surface measurements present in baseline but absent from current PR
+    for (const b of baseline.measurements) {
+      const found = current.measurements.find((m) => m.name === b.name)
+      if (!found) {
+        lines.push(`| ${b.name}: style recalcs | ${b.styleRecalcs} | — | removed |`)
+        lines.push(`| ${b.name}: layouts | ${b.layouts} | — | removed |`)
+        lines.push(`| ${b.name}: task duration | ${b.taskDurationMs.toFixed(0)}ms | — | removed |`)
+      }
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/perf-report.ts` around lines 74 - 75, The report currently iterates
only current.measurements (for const m of current.measurements) so
baseline.measurements entries that were renamed or removed in the PR are never
reported; add logic to detect and surface "removed" measurements by iterating
baseline.measurements (or computing the set difference between
baseline.measurements and current.measurements) and emit a report entry for each
baseline-only measurement (e.g., mark them as removed/missing). Update the
reporting/output code that consumes m/base to handle a removed flag or missing
base so removed tests appear in the final perf report.

3-19: Extract duplicated perf interfaces to a shared types file.

PerfMeasurement (lines 3-12) and PerfReport (lines 14-19) are exact copies of the interfaces defined in browser_tests/fixtures/helpers/PerformanceHelper.ts and browser_tests/helpers/perfReporter.ts respectively. This duplication creates a risk of type drift—future field additions to the browser_tests originals will silently diverge from this script unless manually replicated.

Move both interfaces to a shared location (e.g., types/perf.ts) and import from there in both scripts/perf-report.ts and the browser_tests modules.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/perf-report.ts` around lines 3 - 19, The PerfMeasurement and
PerfReport interfaces are duplicated; extract these interfaces into a shared
module (e.g., create and export them from types/perf.ts) and replace the local
definitions in scripts/perf-report.ts with imports from that shared file; then
update the browser_tests copies
(browser_tests/fixtures/helpers/PerformanceHelper.ts and
browser_tests/helpers/perfReporter.ts) to import the same types from
types/perf.ts so all three modules use the single exported PerfMeasurement and
PerfReport definitions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/perf-report.ts`:
- Around line 68-98: The baseline comparison loop in scripts/perf-report.ts is
missing the heap delta row for each measurement (you iterate
current.measurements and compare base via base =
baseline.measurements.find(...)), so add a heap-delta output just like the other
metrics: in the !base early-return block emit a row for `${m.name}: heap delta`
using `m.heapDeltaBytes`, and in the comparison branch compute `const heapDelta
= calcDelta(base.heapDeltaBytes, m.heapDeltaBytes)` and push a row using
`${base.heapDeltaBytes}` vs `${m.heapDeltaBytes}` with
`formatDeltaCell(heapDelta)` (format numbers similarly to taskDurationMs using
toFixed(0) or plain bytes as used elsewhere) so heap regressions are shown in
both new-measurement and baseline-comparison cases.

---

Duplicate comments:
In `@scripts/perf-report.ts`:
- Around line 59-63: Wrap the raw file reads/JSON.parse calls in a safe helper
(e.g., parseJsonFile or safeReadJson) and use it for the current and baseline
reads so malformed JSON won't throw; implement parseJsonFile(path: string): T |
null that reads with readFileSync and returns parsed JSON on success and null on
failure (catching and optionally logging the error), then replace the inline
JSON.parse(readFileSync(CURRENT_PATH, 'utf-8')) with
parseJsonFile<PerfReport>(CURRENT_PATH) and the baseline branch to
parseJsonFile(BASELINE_PATH) (keeping existsSync check) and ensure variables
current and baseline handle null results appropriately.

---

Nitpick comments:
In `@scripts/perf-report.ts`:
- Around line 74-75: The report currently iterates only current.measurements
(for const m of current.measurements) so baseline.measurements entries that were
renamed or removed in the PR are never reported; add logic to detect and surface
"removed" measurements by iterating baseline.measurements (or computing the set
difference between baseline.measurements and current.measurements) and emit a
report entry for each baseline-only measurement (e.g., mark them as
removed/missing). Update the reporting/output code that consumes m/base to
handle a removed flag or missing base so removed tests appear in the final perf
report.
- Around line 3-19: The PerfMeasurement and PerfReport interfaces are
duplicated; extract these interfaces into a shared module (e.g., create and
export them from types/perf.ts) and replace the local definitions in
scripts/perf-report.ts with imports from that shared file; then update the
browser_tests copies (browser_tests/fixtures/helpers/PerformanceHelper.ts and
browser_tests/helpers/perfReporter.ts) to import the same types from
types/perf.ts so all three modules use the single exported PerfMeasurement and
PerfReport definitions.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between fe24533 and 18ab1ee.

📒 Files selected for processing (2)
  • .github/workflows/ci-perf-report.yaml
  • scripts/perf-report.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/ci-perf-report.yaml

@christian-byrne christian-byrne marked this pull request as ready for review February 25, 2026 08:13
@christian-byrne christian-byrne requested a review from a team as a code owner February 25, 2026 08:13
@dosubot dosubot bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Feb 25, 2026
DrJKL
DrJKL previously approved these changes Feb 26, 2026
@DrJKL DrJKL assigned christian-byrne and unassigned DrJKL Feb 26, 2026
- Add gitSha/branch params to writePerfReport() for callable reuse
- Add delta() helper in stopMeasuring() to reduce repetition

Amp-Thread-ID: https://ampcode.com/threads/T-019c977c-d73c-70ec-81a9-cae4eb81465a
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
browser_tests/fixtures/helpers/PerformanceHelper.ts (1)

35-40: Make dispose idempotent and always clear internal state.

Consider clearing snapshot and nulling the session handle before cleanup so helper state is consistent even if cleanup throws.

Suggested refactor
  async dispose(): Promise<void> {
-   if (this.cdp) {
-     await this.cdp.send('Performance.disable')
-     await this.cdp.detach()
-     this.cdp = null
-   }
+   const cdp = this.cdp
+   this.cdp = null
+   this.snapshot = null
+   if (!cdp) return
+   try {
+     await cdp.send('Performance.disable')
+   } finally {
+     await cdp.detach()
+   }
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@browser_tests/fixtures/helpers/PerformanceHelper.ts` around lines 35 - 40,
Make dispose() idempotent by clearing internal state (e.g., this.snapshot and
the session handle this.cdp) before/independently of the cleanup RPCs and ensure
failing RPCs don't leave the helper in a dirty state: set this.snapshot = null
and capture the current this.cdp into a local variable then set this.cdp = null
early, and wrap the calls to localCdp.send('Performance.disable') and
localCdp.detach() in a try/catch so exceptions don't prevent state reset for
subsequent calls to dispose or other methods.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@browser_tests/fixtures/helpers/PerformanceHelper.ts`:
- Around line 62-64: startMeasuring currently overwrites an existing baseline
without warning; add a guard at the top of startMeasuring() that checks
this.snapshot and throws (or rejects) if a measurement is already in progress to
prevent nested measurements from silently replacing the baseline, e.g. check
this.snapshot !== undefined/null and raise an error advising to call
stopMeasuring() first; ensure stopMeasuring() clears this.snapshot so subsequent
measurements can start cleanly and keep the existing getSnapshot() usage
unchanged.

---

Nitpick comments:
In `@browser_tests/fixtures/helpers/PerformanceHelper.ts`:
- Around line 35-40: Make dispose() idempotent by clearing internal state (e.g.,
this.snapshot and the session handle this.cdp) before/independently of the
cleanup RPCs and ensure failing RPCs don't leave the helper in a dirty state:
set this.snapshot = null and capture the current this.cdp into a local variable
then set this.cdp = null early, and wrap the calls to
localCdp.send('Performance.disable') and localCdp.detach() in a try/catch so
exceptions don't prevent state reset for subsequent calls to dispose or other
methods.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 18ab1ee and 492eb08.

📒 Files selected for processing (2)
  • browser_tests/fixtures/helpers/PerformanceHelper.ts
  • browser_tests/helpers/perfReporter.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • browser_tests/helpers/perfReporter.ts

@socket-security
Copy link

socket-security bot commented Feb 26, 2026

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedvite@​8.0.0-beta.13941008299100

View full report

@socket-security
Copy link

socket-security bot commented Feb 26, 2026

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm vite is 91.0% likely obfuscated

Confidence: 0.91

Location: Package overview

From: apps/desktop-ui/package.jsonnpm/vite@8.0.0-beta.13

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/vite@8.0.0-beta.13. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@browser_tests/fixtures/helpers/PerformanceHelper.ts`:
- Around line 35-40: The dispose() method must always clear in-progress
measurement state to prevent startMeasuring() from failing due to a left-over
this.snapshot; update dispose() (in PerformanceHelper) to use a try/finally
around CDP calls so cleanup runs even if Performance.disable or detach throws,
and in the finally block set this.snapshot = null and this.cdp = null (and any
other measurement flags) to ensure stopMeasuring()/startMeasuring() see a clean
state.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 492eb08 and 02e014e.

📒 Files selected for processing (1)
  • browser_tests/fixtures/helpers/PerformanceHelper.ts

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
browser_tests/fixtures/helpers/PerformanceHelper.ts (1)

30-33: Guard init() against double-initialization.

Line 31 overwrites an existing CDPSession if init() is called twice, which can leave the prior session undisposed.

Suggested patch
  async init(): Promise<void> {
+   if (this.cdp) {
+     throw new Error('PerformanceHelper already initialized')
+   }
    this.cdp = await this.page.context().newCDPSession(this.page)
    await this.cdp.send('Performance.enable')
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@browser_tests/fixtures/helpers/PerformanceHelper.ts` around lines 30 - 33,
init() currently overwrites this.cdp on repeated calls; add a guard at the top
of the init method to no-op if this.cdp is already set (or alternatively
explicitly dispose/detach the existing session before creating a new one) so you
don't leak the previous CDPSession; locate init(), the this.cdp assignment
(newCDPSession(this.page)) and the this.cdp.send('Performance.enable') call and
either return early when this.cdp is truthy or call the appropriate
dispose/detach on the existing session before reassigning.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@browser_tests/fixtures/helpers/PerformanceHelper.ts`:
- Around line 38-43: In PerformanceHelper's cleanup block (the try/finally
around this.cdp.send('Performance.disable')), avoid clearing stale this.cdp only
after awaiting detach; instead capture the current CDP instance to a local
(e.g., const cdp = this.cdp), set this.cdp = null immediately, then if (cdp)
await cdp.detach() so the helper's state is reset even if detach() throws;
update the method that contains this.cdp.send / this.cdp.detach to follow this
pattern (use the local variable and null-assign this.cdp before awaiting
detach).

---

Nitpick comments:
In `@browser_tests/fixtures/helpers/PerformanceHelper.ts`:
- Around line 30-33: init() currently overwrites this.cdp on repeated calls; add
a guard at the top of the init method to no-op if this.cdp is already set (or
alternatively explicitly dispose/detach the existing session before creating a
new one) so you don't leak the previous CDPSession; locate init(), the this.cdp
assignment (newCDPSession(this.page)) and the
this.cdp.send('Performance.enable') call and either return early when this.cdp
is truthy or call the appropriate dispose/detach on the existing session before
reassigning.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b7cd12b and c5aefce.

📒 Files selected for processing (1)
  • browser_tests/fixtures/helpers/PerformanceHelper.ts

@christian-byrne christian-byrne merged commit 8e215b3 into main Feb 26, 2026
34 checks passed
@christian-byrne christian-byrne deleted the perf/testing-infrastructure branch February 26, 2026 04:10
christian-byrne added a commit that referenced this pull request Feb 27, 2026
## Summary

Batch `getBoundingClientRect()` calls in `updateClipPath` via
`requestAnimationFrame` to avoid forced synchronous layout.

## Changes

- **What**: Wrap the layout-reading portion of `updateClipPath` in
`requestAnimationFrame()` with cancellation. Multiple rapid calls within
the same frame are coalesced into a single layout read. Eliminates
~1,053 forced synchronous layouts per profiling session.

## Review Focus

- `getBoundingClientRect()` forces synchronous layout. When interleaved
with style mutations (from PrimeVue `useStyle`, cursor writes, Vue VDOM
patching), this creates layout thrashing — especially in Firefox where
Stylo aggressively invalidates the entire style cache.
- The RAF wrapper coalesces all calls within a frame into one, reading
layout only once per frame. The `cancelAnimationFrame` ensures only the
latest parameters are used.
- `willChange: 'clip-path'` is included to hint the browser to optimize
clip-path animations.

## Stack

4 of 4 in Firefox perf fix stack. Depends on #9170.

<!-- Fixes #ISSUE_NUMBER -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9173-fix-batch-updateClipPath-via-requestAnimationFrame-3116d73d3650810392f7fba7ea5ceb6f)
by [Unito](https://www.unito.io)
christian-byrne added a commit that referenced this pull request Feb 27, 2026
## Summary

Batch `getBoundingClientRect()` calls in `updateClipPath` via
`requestAnimationFrame` to avoid forced synchronous layout.

## Changes

- **What**: Wrap the layout-reading portion of `updateClipPath` in
`requestAnimationFrame()` with cancellation. Multiple rapid calls within
the same frame are coalesced into a single layout read. Eliminates
~1,053 forced synchronous layouts per profiling session.

## Review Focus

- `getBoundingClientRect()` forces synchronous layout. When interleaved
with style mutations (from PrimeVue `useStyle`, cursor writes, Vue VDOM
patching), this creates layout thrashing — especially in Firefox where
Stylo aggressively invalidates the entire style cache.
- The RAF wrapper coalesces all calls within a frame into one, reading
layout only once per frame. The `cancelAnimationFrame` ensures only the
latest parameters are used.
- `willChange: 'clip-path'` is included to hint the browser to optimize
clip-path animations.

## Stack

4 of 4 in Firefox perf fix stack. Depends on #9170.

<!-- Fixes #ISSUE_NUMBER -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9173-fix-batch-updateClipPath-via-requestAnimationFrame-3116d73d3650810392f7fba7ea5ceb6f)
by [Unito](https://www.unito.io)
christian-byrne added a commit that referenced this pull request Feb 28, 2026
## Summary

Pre-rasterize the SubgraphNode SVG icon to a bitmap canvas to eliminate
Firefox's per-frame SVG style processing.

## Changes

- **What**: Add `getWorkflowBitmap()` that lazily rasterizes the
`data:image/svg+xml` workflow icon to an `HTMLCanvasElement` (16×16) on
first use. `SubgraphNode.drawTitleBox()` draws the cached bitmap instead
of the raw SVG.

## Review Focus

- Firefox re-processes SVG internal stylesheets (`stroke`,
`stroke-linecap`, `stroke-width`) every time `ctx.drawImage(svgImage)`
is called. Chrome caches the rasterization. This happens on every frame
for every visible SubgraphNode.
- Reporter confirmed strong subgraph correlation: "it may be happening
in the default workflow with subgraph" / "didn't seem to happen just
using manually wired up diffusion loader, clip, sampler, etc."
- Falls back to the raw SVG Image if not yet loaded or if
`getContext('2d')` returns null.

## Stack

3 of 4 in Firefox perf fix stack. Depends on #9170.

<!-- Fixes #ISSUE_NUMBER -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9172-fix-pre-rasterize-SubgraphNode-SVG-icon-to-bitmap-canvas-3116d73d365081babf17cf0848d37269)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
DrJKL pushed a commit that referenced this pull request Feb 28, 2026
## Summary

Pre-rasterize the SubgraphNode SVG icon to a bitmap canvas to eliminate
Firefox's per-frame SVG style processing.

## Changes

- **What**: Add `getWorkflowBitmap()` that lazily rasterizes the
`data:image/svg+xml` workflow icon to an `HTMLCanvasElement` (16×16) on
first use. `SubgraphNode.drawTitleBox()` draws the cached bitmap instead
of the raw SVG.

## Review Focus

- Firefox re-processes SVG internal stylesheets (`stroke`,
`stroke-linecap`, `stroke-width`) every time `ctx.drawImage(svgImage)`
is called. Chrome caches the rasterization. This happens on every frame
for every visible SubgraphNode.
- Reporter confirmed strong subgraph correlation: "it may be happening
in the default workflow with subgraph" / "didn't seem to happen just
using manually wired up diffusion loader, clip, sampler, etc."
- Falls back to the raw SVG Image if not yet loaded or if
`getContext('2d')` returns null.

## Stack

3 of 4 in Firefox perf fix stack. Depends on #9170.

<!-- Fixes #ISSUE_NUMBER -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9172-fix-pre-rasterize-SubgraphNode-SVG-icon-to-bitmap-canvas-3116d73d365081babf17cf0848d37269)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
christian-byrne added a commit that referenced this pull request Mar 4, 2026
## Summary

Cache `canvas.style.cursor` to avoid redundant DOM writes that dirty
Firefox's style tree.

## Changes

- **What**: Add `_lastCursor` field to
`LGraphCanvas._updateCursorStyle()` — only writes `canvas.style.cursor`
when the value changes. Eliminates ~347 redundant style mutations per
profiling session.

## Review Focus

- The fix is 2 lines (cache field + comparison). The unit test validates
the caching pattern without requiring full LGraphCanvas instantiation.
- This is one of several contributors to Firefox's cascading style
recalculation freeze. Each `canvas.style.cursor` write dirties the style
tree, which is flushed during the next paint in the canvas render loop.

## Stack

2 of 4 in Firefox perf fix stack. Depends on #9170.

<!-- Fixes #ISSUE_NUMBER -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9171-fix-cache-canvas-cursor-style-to-avoid-redundant-DOM-writes-3116d73d36508139827fe1d644fa1bd0)
by [Unito](https://www.unito.io)
Myestery pushed a commit that referenced this pull request Mar 5, 2026
## Summary

Cache `canvas.style.cursor` to avoid redundant DOM writes that dirty
Firefox's style tree.

## Changes

- **What**: Add `_lastCursor` field to
`LGraphCanvas._updateCursorStyle()` — only writes `canvas.style.cursor`
when the value changes. Eliminates ~347 redundant style mutations per
profiling session.

## Review Focus

- The fix is 2 lines (cache field + comparison). The unit test validates
the caching pattern without requiring full LGraphCanvas instantiation.
- This is one of several contributors to Firefox's cascading style
recalculation freeze. Each `canvas.style.cursor` write dirties the style
tree, which is flushed during the next paint in the canvas render loop.

## Stack

2 of 4 in Firefox perf fix stack. Depends on #9170.

<!-- Fixes #ISSUE_NUMBER -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9171-fix-cache-canvas-cursor-style-to-avoid-redundant-DOM-writes-3116d73d36508139827fe1d644fa1bd0)
by [Unito](https://www.unito.io)
github-actions bot pushed a commit that referenced this pull request Mar 8, 2026
## Summary

Batch `getBoundingClientRect()` calls in `updateClipPath` via
`requestAnimationFrame` to avoid forced synchronous layout.

## Changes

- **What**: Wrap the layout-reading portion of `updateClipPath` in
`requestAnimationFrame()` with cancellation. Multiple rapid calls within
the same frame are coalesced into a single layout read. Eliminates
~1,053 forced synchronous layouts per profiling session.

## Review Focus

- `getBoundingClientRect()` forces synchronous layout. When interleaved
with style mutations (from PrimeVue `useStyle`, cursor writes, Vue VDOM
patching), this creates layout thrashing — especially in Firefox where
Stylo aggressively invalidates the entire style cache.
- The RAF wrapper coalesces all calls within a frame into one, reading
layout only once per frame. The `cancelAnimationFrame` ensures only the
latest parameters are used.
- `willChange: 'clip-path'` is included to hint the browser to optimize
clip-path animations.

## Stack

4 of 4 in Firefox perf fix stack. Depends on #9170.

<!-- Fixes #ISSUE_NUMBER -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9173-fix-batch-updateClipPath-via-requestAnimationFrame-3116d73d3650810392f7fba7ea5ceb6f)
by [Unito](https://www.unito.io)
github-actions bot pushed a commit that referenced this pull request Mar 8, 2026
## Summary

Pre-rasterize the SubgraphNode SVG icon to a bitmap canvas to eliminate
Firefox's per-frame SVG style processing.

## Changes

- **What**: Add `getWorkflowBitmap()` that lazily rasterizes the
`data:image/svg+xml` workflow icon to an `HTMLCanvasElement` (16×16) on
first use. `SubgraphNode.drawTitleBox()` draws the cached bitmap instead
of the raw SVG.

## Review Focus

- Firefox re-processes SVG internal stylesheets (`stroke`,
`stroke-linecap`, `stroke-width`) every time `ctx.drawImage(svgImage)`
is called. Chrome caches the rasterization. This happens on every frame
for every visible SubgraphNode.
- Reporter confirmed strong subgraph correlation: "it may be happening
in the default workflow with subgraph" / "didn't seem to happen just
using manually wired up diffusion loader, clip, sampler, etc."
- Falls back to the raw SVG Image if not yet loaded or if
`getContext('2d')` returns null.

## Stack

3 of 4 in Firefox perf fix stack. Depends on #9170.

<!-- Fixes #ISSUE_NUMBER -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9172-fix-pre-rasterize-SubgraphNode-SVG-icon-to-bitmap-canvas-3116d73d365081babf17cf0848d37269)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
github-actions bot pushed a commit that referenced this pull request Mar 8, 2026
## Summary

Cache `canvas.style.cursor` to avoid redundant DOM writes that dirty
Firefox's style tree.

## Changes

- **What**: Add `_lastCursor` field to
`LGraphCanvas._updateCursorStyle()` — only writes `canvas.style.cursor`
when the value changes. Eliminates ~347 redundant style mutations per
profiling session.

## Review Focus

- The fix is 2 lines (cache field + comparison). The unit test validates
the caching pattern without requiring full LGraphCanvas instantiation.
- This is one of several contributors to Firefox's cascading style
recalculation freeze. Each `canvas.style.cursor` write dirties the style
tree, which is flushed during the next paint in the canvas render loop.

## Stack

2 of 4 in Firefox perf fix stack. Depends on #9170.

<!-- Fixes #ISSUE_NUMBER -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9171-fix-cache-canvas-cursor-style-to-avoid-redundant-DOM-writes-3116d73d36508139827fe1d644fa1bd0)
by [Unito](https://www.unito.io)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

perf:speed size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants