mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-19 22:09:37 +00:00
## Summary Enhances the CI performance report with explicit FPS metrics, percentile frame times, and milestone target thresholds. ### Changes **PerformanceHelper** (data collection): - `measureFrameDurations()` now returns individual frame durations instead of just the average, enabling percentile computation - Computes `p95FrameDurationMs` from sorted frame durations - Strips `allFrameDurationsMs` from serialized JSON to avoid bloating artifacts **perf-report.ts** (report rendering): - **Headline summary** at top of report with key metrics per test scenario - **FPS display**: derives avg FPS and P5 FPS from frame duration metrics - **Target thresholds**: shows P5 FPS ≥ 52 target with ✅/❌ pass/fail indicator - **p95 frame time**: added as a tracked metric in the comparison table - Metrics reordered to show frame time/FPS first (what people look for) ### Target From the Nodes 2.0 Perf milestone: **P5 ≥ 52 FPS** on 245-node workflow (equivalent to P95 frame time ≤ 19.2ms). ### Example headline output ``` > **vue-large-graph-pan**: 60 avg FPS · 58 P5 FPS ✅ (target: ≥52) · 12ms TBT · 45.2 MB heap > **canvas-zoom-sweep**: 45 avg FPS · 38 P5 FPS ❌ (target: ≥52) · 85ms TBT · 52.1 MB heap ``` Follow-up to #10477 (merged). ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10516-perf-add-FPS-p95-frame-time-and-target-thresholds-to-CI-perf-report-32e6d73d365081a2a2a6ceae7d6e9be5) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action <action@github.com>
221 lines
6.8 KiB
TypeScript
221 lines
6.8 KiB
TypeScript
import type { CDPSession, Page } from '@playwright/test'
|
|
|
|
interface PerfSnapshot {
|
|
RecalcStyleCount: number
|
|
RecalcStyleDuration: number
|
|
LayoutCount: number
|
|
LayoutDuration: number
|
|
TaskDuration: number
|
|
JSHeapUsedSize: number
|
|
Timestamp: number
|
|
Nodes: number
|
|
JSHeapTotalSize: number
|
|
ScriptDuration: number
|
|
JSEventListeners: number
|
|
}
|
|
|
|
export interface PerfMeasurement {
|
|
name: string
|
|
durationMs: number
|
|
styleRecalcs: number
|
|
styleRecalcDurationMs: number
|
|
layouts: number
|
|
layoutDurationMs: number
|
|
taskDurationMs: number
|
|
heapDeltaBytes: number
|
|
heapUsedBytes: number
|
|
domNodes: number
|
|
jsHeapTotalBytes: number
|
|
scriptDurationMs: number
|
|
eventListeners: number
|
|
totalBlockingTimeMs: number
|
|
frameDurationMs: number
|
|
p95FrameDurationMs: number
|
|
allFrameDurationsMs: 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> {
|
|
this.snapshot = null
|
|
if (this.cdp) {
|
|
try {
|
|
await this.cdp.send('Performance.disable')
|
|
} finally {
|
|
await this.cdp.detach()
|
|
this.cdp = null
|
|
}
|
|
}
|
|
}
|
|
|
|
private async getSnapshot(): Promise<PerfSnapshot> {
|
|
if (!this.cdp) throw new Error('PerformanceHelper not initialized')
|
|
const { metrics } = (await this.cdp.send('Performance.getMetrics')) as {
|
|
metrics: { name: string; value: number }[]
|
|
}
|
|
function get(name: string): number {
|
|
return 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'),
|
|
Nodes: get('Nodes'),
|
|
JSHeapTotalSize: get('JSHeapTotalSize'),
|
|
ScriptDuration: get('ScriptDuration'),
|
|
JSEventListeners: get('JSEventListeners')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Collect longtask entries from PerformanceObserver and compute TBT.
|
|
* TBT = sum of (duration - 50ms) for every task longer than 50ms.
|
|
*/
|
|
private async collectTBT(): Promise<number> {
|
|
return this.page.evaluate(() => {
|
|
const state = (window as unknown as Record<string, unknown>)
|
|
.__perfLongtaskState as
|
|
| { observer: PerformanceObserver; tbtMs: number }
|
|
| undefined
|
|
if (!state) return 0
|
|
|
|
// Flush any queued-but-undelivered entries into our accumulator
|
|
for (const entry of state.observer.takeRecords()) {
|
|
if (entry.duration > 50) state.tbtMs += entry.duration - 50
|
|
}
|
|
const result = state.tbtMs
|
|
state.tbtMs = 0
|
|
return result
|
|
})
|
|
}
|
|
|
|
/**
|
|
* Measure individual frame durations via rAF timing over a sample window.
|
|
* Returns all per-frame durations so callers can compute avg, p95, etc.
|
|
*/
|
|
private async measureFrameDurations(sampleFrames = 30): Promise<number[]> {
|
|
return this.page.evaluate((frames) => {
|
|
return new Promise<number[]>((resolve) => {
|
|
const timeout = setTimeout(() => resolve([]), 5000)
|
|
const timestamps: number[] = []
|
|
let count = 0
|
|
function tick(ts: number) {
|
|
timestamps.push(ts)
|
|
count++
|
|
if (count <= frames) {
|
|
requestAnimationFrame(tick)
|
|
} else {
|
|
clearTimeout(timeout)
|
|
if (timestamps.length < 2) {
|
|
resolve([])
|
|
return
|
|
}
|
|
const durations: number[] = []
|
|
for (let i = 1; i < timestamps.length; i++) {
|
|
durations.push(timestamps[i] - timestamps[i - 1])
|
|
}
|
|
resolve(durations)
|
|
}
|
|
}
|
|
requestAnimationFrame(tick)
|
|
})
|
|
}, sampleFrames)
|
|
}
|
|
|
|
async startMeasuring(): Promise<void> {
|
|
if (this.snapshot) {
|
|
throw new Error(
|
|
'Measurement already in progress — call stopMeasuring() first'
|
|
)
|
|
}
|
|
// Install longtask observer if not already present, then reset the
|
|
// accumulator so old longtasks don't bleed into the new measurement window.
|
|
await this.page.evaluate(() => {
|
|
const win = window as unknown as Record<string, unknown>
|
|
if (!win.__perfLongtaskState) {
|
|
const state: { observer: PerformanceObserver; tbtMs: number } = {
|
|
observer: new PerformanceObserver((list) => {
|
|
const self = (window as unknown as Record<string, unknown>)
|
|
.__perfLongtaskState as {
|
|
observer: PerformanceObserver
|
|
tbtMs: number
|
|
}
|
|
for (const entry of list.getEntries()) {
|
|
if (entry.duration > 50) self.tbtMs += entry.duration - 50
|
|
}
|
|
}),
|
|
tbtMs: 0
|
|
}
|
|
state.observer.observe({ type: 'longtask', buffered: true })
|
|
win.__perfLongtaskState = state
|
|
}
|
|
const state = win.__perfLongtaskState as {
|
|
observer: PerformanceObserver
|
|
tbtMs: number
|
|
}
|
|
state.tbtMs = 0
|
|
state.observer.takeRecords()
|
|
})
|
|
this.snapshot = await this.getSnapshot()
|
|
}
|
|
|
|
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
|
|
|
|
function delta(key: keyof PerfSnapshot): number {
|
|
return after[key] - before[key]
|
|
}
|
|
|
|
const [totalBlockingTimeMs, allFrameDurationsMs] = await Promise.all([
|
|
this.collectTBT(),
|
|
this.measureFrameDurations()
|
|
])
|
|
|
|
const frameDurationMs =
|
|
allFrameDurationsMs.length > 0
|
|
? allFrameDurationsMs.reduce((a, b) => a + b, 0) /
|
|
allFrameDurationsMs.length
|
|
: 0
|
|
|
|
const sorted = [...allFrameDurationsMs].sort((a, b) => a - b)
|
|
const p95FrameDurationMs =
|
|
sorted.length > 0 ? sorted[Math.ceil(sorted.length * 0.95) - 1] : 0
|
|
|
|
return {
|
|
name,
|
|
durationMs: delta('Timestamp') * 1000,
|
|
styleRecalcs: delta('RecalcStyleCount'),
|
|
styleRecalcDurationMs: delta('RecalcStyleDuration') * 1000,
|
|
layouts: delta('LayoutCount'),
|
|
layoutDurationMs: delta('LayoutDuration') * 1000,
|
|
taskDurationMs: delta('TaskDuration') * 1000,
|
|
heapDeltaBytes: delta('JSHeapUsedSize'),
|
|
heapUsedBytes: after.JSHeapUsedSize,
|
|
domNodes: delta('Nodes'),
|
|
jsHeapTotalBytes: delta('JSHeapTotalSize'),
|
|
scriptDurationMs: delta('ScriptDuration') * 1000,
|
|
eventListeners: delta('JSEventListeners'),
|
|
totalBlockingTimeMs,
|
|
frameDurationMs,
|
|
p95FrameDurationMs,
|
|
allFrameDurationsMs
|
|
}
|
|
}
|
|
}
|