Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 106 additions & 0 deletions .github/workflows/ci-perf-report.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
name: 'CI: Performance Report'

on:
push:
branches: [main, core/*]
paths-ignore: ['**/*.md']
pull_request:
branches-ignore: [wip/*, draft/*, temp/*]
paths-ignore: ['**/*.md']

concurrency:
group: perf-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read
pull-requests: write

jobs:
perf-tests:
if: github.repository == 'Comfy-Org/ComfyUI_frontend'
runs-on: ubuntu-latest
timeout-minutes: 30
container:
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.12
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
permissions:
contents: read
packages: read

steps:
- name: Checkout repository
uses: actions/checkout@v6

- name: Setup frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: true

- name: Start ComfyUI server
uses: ./.github/actions/start-comfyui-server

- name: Run performance tests
id: perf
continue-on-error: true
run: pnpm exec playwright test --project=performance --workers=1

- name: Upload perf metrics
uses: actions/upload-artifact@v6
with:
name: perf-metrics
path: test-results/perf-metrics.json
retention-days: 30
Comment thread
coderabbitai[bot] marked this conversation as resolved.

report:
needs: perf-tests
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
pull-requests: write

steps:
- name: Checkout repository
uses: actions/checkout@v6
Comment thread
christian-byrne marked this conversation as resolved.

- name: Setup Node
uses: actions/setup-node@v4

Check failure on line 69 in .github/workflows/ci-perf-report.yaml

View workflow job for this annotation

GitHub Actions / validate-pins

pinact error

uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
Comment thread
christian-byrne marked this conversation as resolved.
Outdated

- name: Download PR perf metrics
uses: actions/download-artifact@v7
with:
name: perf-metrics
path: test-results/

- name: Download baseline perf metrics
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
with:
branch: ${{ github.event.pull_request.base.ref }}
workflow: ci-perf-report.yaml
event: push
name: perf-metrics
path: temp/perf-baseline/
if_no_artifact_found: warn

- name: Generate perf report
run: node scripts/perf-report.js > perf-report.md

- name: Read perf report
id: perf-report
uses: juliangruber/read-file-action@b549046febe0fe86f8cb4f93c24e284433f9ab58 # v1.1.7
with:
path: ./perf-report.md

- name: Create or update PR comment
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
number: ${{ github.event.pull_request.number }}
body: |
${{ steps.perf-report.outputs.content }}
<!-- COMFYUI_FRONTEND_PERF -->
body-include: '<!-- COMFYUI_FRONTEND_PERF -->'
9 changes: 9 additions & 0 deletions browser_tests/fixtures/ComfyPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
} from './components/SidebarTab'
import { Topbar } from './components/Topbar'
import { CanvasHelper } from './helpers/CanvasHelper'
import { PerformanceHelper } from './helpers/PerformanceHelper'
import { ClipboardHelper } from './helpers/ClipboardHelper'
import { CommandHelper } from './helpers/CommandHelper'
import { DragDropHelper } from './helpers/DragDropHelper'
Expand Down Expand Up @@ -185,6 +186,7 @@ export class ComfyPage {
public readonly dragDrop: DragDropHelper
public readonly command: CommandHelper
public readonly bottomPanel: BottomPanel
public readonly perf: PerformanceHelper

/** Worker index to test user ID */
public readonly userIds: string[] = []
Expand Down Expand Up @@ -229,6 +231,7 @@ export class ComfyPage {
this.dragDrop = new DragDropHelper(page, this.assetPath.bind(this))
this.command = new CommandHelper(page)
this.bottomPanel = new BottomPanel(page)
this.perf = new PerformanceHelper(page)
}

get visibleToasts() {
Expand Down Expand Up @@ -436,7 +439,13 @@ export const comfyPageFixture = base.extend<{
}

await comfyPage.setup()

const isPerf = testInfo.tags.includes('@perf')
if (isPerf) await comfyPage.perf.init()

await use(comfyPage)

if (isPerf) await comfyPage.perf.dispose()
},
comfyMouse: async ({ comfyPage }, use) => {
const comfyMouse = new ComfyMouse(comfyPage)
Expand Down
81 changes: 81 additions & 0 deletions browser_tests/fixtures/helpers/PerformanceHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { CDPSession, Page } from '@playwright/test'

export interface PerfSnapshot {
RecalcStyleCount: number
RecalcStyleDuration: number
LayoutCount: number
LayoutDuration: number
TaskDuration: number
JSHeapUsedSize: number
Timestamp: number
}

export interface PerfMeasurement {
name: string
durationMs: number
styleRecalcs: number
styleRecalcDurationMs: number
layouts: number
layoutDurationMs: number
taskDurationMs: number
heapDeltaBytes: number
}

export class PerformanceHelper {
private cdp: CDPSession | null = null
private snapshot: PerfSnapshot | null = null

constructor(private readonly page: Page) {}

async init(): Promise<void> {
this.cdp = await this.page.context().newCDPSession(this.page)
await this.cdp.send('Performance.enable')
}

async dispose(): Promise<void> {
if (this.cdp) {
await this.cdp.send('Performance.disable')
await this.cdp.detach()
this.cdp = null
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

private async getSnapshot(): Promise<PerfSnapshot> {
if (!this.cdp) throw new Error('PerformanceHelper not initialized')
const { metrics } = await this.cdp.send('Performance.getMetrics')
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
const get = (name: string) =>
metrics.find((m) => m.name === name)?.value ?? 0
return {
RecalcStyleCount: get('RecalcStyleCount'),
RecalcStyleDuration: get('RecalcStyleDuration'),
LayoutCount: get('LayoutCount'),
LayoutDuration: get('LayoutDuration'),
TaskDuration: get('TaskDuration'),
JSHeapUsedSize: get('JSHeapUsedSize'),
Timestamp: get('Timestamp')
Comment thread
christian-byrne marked this conversation as resolved.
}
}

async startMeasuring(): Promise<void> {
this.snapshot = await this.getSnapshot()
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

async stopMeasuring(name: string): Promise<PerfMeasurement> {
if (!this.snapshot) throw new Error('Call startMeasuring() first')
const after = await this.getSnapshot()
const before = this.snapshot
this.snapshot = null
return {
name,
durationMs: (after.Timestamp - before.Timestamp) * 1000,
styleRecalcs: after.RecalcStyleCount - before.RecalcStyleCount,
styleRecalcDurationMs:
(after.RecalcStyleDuration - before.RecalcStyleDuration) * 1000,
layouts: after.LayoutCount - before.LayoutCount,
layoutDurationMs:
(after.LayoutDuration - before.LayoutDuration) * 1000,
taskDurationMs: (after.TaskDuration - before.TaskDuration) * 1000,
heapDeltaBytes: after.JSHeapUsedSize - before.JSHeapUsedSize
}
}
}
3 changes: 3 additions & 0 deletions browser_tests/globalTeardown.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import type { FullConfig } from '@playwright/test'
import dotenv from 'dotenv'

import { writePerfReport } from './helpers/perfReporter'
import { restorePath } from './utils/backupUtils'

dotenv.config()

export default function globalTeardown(_config: FullConfig) {
writePerfReport()

Comment thread
christian-byrne marked this conversation as resolved.
if (!process.env.CI && process.env.TEST_COMFYUI_DIR) {
restorePath([process.env.TEST_COMFYUI_DIR, 'user'])
restorePath([process.env.TEST_COMFYUI_DIR, 'models'])
Expand Down
33 changes: 33 additions & 0 deletions browser_tests/helpers/perfReporter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { mkdirSync, writeFileSync } from 'fs'
import { join } from 'path'

import type { PerfMeasurement } from '../fixtures/helpers/PerformanceHelper'

export interface PerfReport {
timestamp: string
gitSha: string
branch: string
measurements: PerfMeasurement[]
}

const measurements: PerfMeasurement[] = []

export function recordMeasurement(m: PerfMeasurement) {
measurements.push(m)
}

export function writePerfReport() {
if (measurements.length === 0) return

mkdirSync('test-results', { recursive: true })
const report: PerfReport = {
timestamp: new Date().toISOString(),
gitSha: process.env.GITHUB_SHA ?? 'local',
branch: process.env.GITHUB_HEAD_REF ?? 'local',
Comment thread
christian-byrne marked this conversation as resolved.
Outdated
measurements
}
writeFileSync(
join('test-results', 'perf-metrics.json'),
JSON.stringify(report, null, 2)
)
Comment thread
christian-byrne marked this conversation as resolved.
Outdated
}
91 changes: 91 additions & 0 deletions browser_tests/tests/performance.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { recordMeasurement } from '../helpers/perfReporter'

test.describe('Performance', { tag: ['@perf'] }, () => {
test('canvas idle style recalculations', async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('default')
await comfyPage.perf.startMeasuring()

// Let the canvas idle for 2 seconds — no user interaction.
// Measures baseline style recalcs from reactive state + render loop.
for (let i = 0; i < 120; i++) {
await comfyPage.nextFrame()
}

const m = await comfyPage.perf.stopMeasuring('canvas-idle')
recordMeasurement(m)
console.log(
`Canvas idle: ${m.styleRecalcs} style recalcs, ${m.layouts} layouts`
)
})

test('canvas mouse interaction style recalculations', 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 the 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
)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const m = await comfyPage.perf.stopMeasuring('canvas-mouse-sweep')
recordMeasurement(m)
console.log(
`Mouse sweep: ${m.styleRecalcs} style recalcs, ${m.layouts} layouts`
)
})

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`)
})

test('DOM widget clipping during node selection', async ({ comfyPage }) => {
// Load default workflow which has DOM widgets (text inputs, combos)
await comfyPage.workflow.loadWorkflow('default')
await comfyPage.perf.startMeasuring()

// Select and deselect nodes rapidly to trigger clipping recalculation
const canvas = comfyPage.canvas
const box = await canvas.boundingBox()
if (box) {
for (let i = 0; i < 20; i++) {
// Click on canvas area (nodes occupy various positions)
await comfyPage.page.mouse.click(
box.x + box.width / 3 + (i % 5) * 30,
box.y + box.height / 3 + (i % 4) * 30
)
await comfyPage.nextFrame()
}
}

const m = await comfyPage.perf.stopMeasuring('dom-widget-clipping')
recordMeasurement(m)
console.log(`Clipping: ${m.layouts} forced layouts`)
})
})
13 changes: 12 additions & 1 deletion playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,18 @@ export default defineConfig({
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
timeout: 15000,
grepInvert: /@mobile/ // Run all tests except those tagged with @mobile
grepInvert: /@mobile|@perf/ // Run all tests except those tagged with @mobile or @perf
},

{
name: 'performance',
use: {
...devices['Desktop Chrome'],
trace: 'retain-on-failure'
},
timeout: 60_000,
grep: /@perf/,
fullyParallel: false
},

{
Expand Down
Loading
Loading