-
Notifications
You must be signed in to change notification settings - Fork 3.3k
Frontend Profiling CI #12938
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
dawoodkhan82
wants to merge
33
commits into
main
Choose a base branch
from
frontend-profiling
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+550
−0
Open
Frontend Profiling CI #12938
Changes from 18 commits
Commits
Show all changes
33 commits
Select commit
Hold shift + click to select a range
c1fed91
profiling first pass
dawoodkhan82 ca4ee69
Merge branch 'main' into frontend-profiling
dawoodkhan82 32d6053
Merge branch 'main' into frontend-profiling
dawoodkhan82 4baf7f1
Merge branch 'main' into frontend-profiling
dawoodkhan82 39e62d8
update workflow
dawoodkhan82 351f944
clean
dawoodkhan82 3ecd223
Merge branch 'main' into frontend-profiling
dawoodkhan82 08466aa
test fixes
dawoodkhan82 00f01d2
Merge branch 'main' into frontend-profiling
dawoodkhan82 55087b2
fix
dawoodkhan82 d018a6f
more updates
dawoodkhan82 1f9bb97
Merge branch 'main' into frontend-profiling
dawoodkhan82 41c2aff
update job
dawoodkhan82 411e37c
format
dawoodkhan82 2a64152
update workflow
dawoodkhan82 f198ff9
Merge branch 'main' into frontend-profiling
dawoodkhan82 0877ba6
cleanup
dawoodkhan82 dd7c7ce
print results
dawoodkhan82 68cba79
update workflow
dawoodkhan82 d315f90
new demo + test fixes
dawoodkhan82 586deaa
Merge branch 'main' into frontend-profiling
dawoodkhan82 4dd1d9a
format
dawoodkhan82 cdc53f7
workflow fix
dawoodkhan82 43ecb69
fix tests
dawoodkhan82 bd4f2ec
notebook fix
dawoodkhan82 2c59db9
workflow fix
dawoodkhan82 3a21339
nb fix
dawoodkhan82 6fc4c32
test
dawoodkhan82 ed3cb1f
Merge branch 'main' into frontend-profiling
dawoodkhan82 b7f9981
format
dawoodkhan82 f736ac7
Merge branch 'main' into frontend-profiling
freddyaboulton 03a9aaf
import
dawoodkhan82 3380f35
add warmup iteration
dawoodkhan82 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| name: "frontend-profiling" | ||
|
|
||
| on: | ||
| pull_request: | ||
| types: [opened, synchronize, reopened] | ||
|
|
||
| concurrency: | ||
| group: "frontend-profiling-${{ github.event.pull_request.number }}" | ||
| cancel-in-progress: true | ||
|
|
||
| permissions: {} | ||
|
|
||
| jobs: | ||
| benchmark: | ||
| permissions: | ||
| contents: read | ||
| name: "frontend-benchmark" | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 | ||
|
|
||
| - uses: "gradio-app/gradio/.github/actions/changes@main" | ||
| id: changes | ||
| with: | ||
| filter: "functional" | ||
| token: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: Find latest release tag | ||
| if: steps.changes.outputs.should_run == 'true' | ||
| id: latest_tag | ||
| run: echo "tag=$(git tag --list 'gradio@*' --sort=-v:refname | head -n 1)" >> "$GITHUB_OUTPUT" | ||
|
|
||
| - name: Checkout latest release | ||
| if: steps.changes.outputs.should_run == 'true' | ||
| run: git checkout ${{ steps.latest_tag.outputs.tag }} | ||
|
|
||
| - name: Install dependencies | ||
| if: steps.changes.outputs.should_run == 'true' | ||
| uses: "gradio-app/gradio/.github/actions/install-all-deps@main" | ||
| with: | ||
| python_version: "3.10" | ||
| os: "ubuntu-latest" | ||
|
|
||
| - name: Install Playwright | ||
| if: steps.changes.outputs.should_run == 'true' | ||
| run: pnpm exec playwright install chromium | ||
|
|
||
| - name: Checkout benchmark spec from PR | ||
| if: steps.changes.outputs.should_run == 'true' | ||
| run: git checkout ${{ github.event.pull_request.head.sha }} -- js/spa/test/tabs_performance.spec.ts | ||
|
|
||
| - name: Run benchmark (base) | ||
| if: steps.changes.outputs.should_run == 'true' | ||
| run: | | ||
| . venv/bin/activate | ||
| PERF_RESULTS_FILE=/tmp/bench_base.json pnpm exec playwright test \ | ||
| --config .config/playwright.config.js \ | ||
| js/spa/test/tabs_performance.spec.ts | ||
|
|
||
| - name: Checkout PR branch | ||
| if: steps.changes.outputs.should_run == 'true' | ||
| run: git checkout ${{ github.event.pull_request.head.sha }} | ||
|
|
||
| - name: Install and build PR | ||
| if: steps.changes.outputs.should_run == 'true' | ||
| run: | | ||
| . venv/bin/activate | ||
| uv pip install -e client/python | ||
| uv pip install -e ".[oauth,mcp]" | ||
| pnpm install --no-frozen-lockfile | ||
| pnpm build | ||
|
|
||
| - name: Run benchmark (PR) | ||
| if: steps.changes.outputs.should_run == 'true' | ||
| run: | | ||
| . venv/bin/activate | ||
| PERF_RESULTS_FILE=/tmp/bench_pr.json pnpm exec playwright test \ | ||
| --config .config/playwright.config.js \ | ||
| js/spa/test/tabs_performance.spec.ts | ||
|
|
||
| - name: Compare results | ||
| if: always() && steps.changes.outputs.should_run == 'true' | ||
| run: | | ||
| node -e " | ||
dawoodkhan82 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| const fs = require('fs'); | ||
|
|
||
| let pr, base; | ||
| try { | ||
| pr = JSON.parse(fs.readFileSync('/tmp/bench_pr.json', 'utf8')); | ||
| base = JSON.parse(fs.readFileSync('/tmp/bench_base.json', 'utf8')); | ||
| } catch (e) { | ||
| console.warn('Could not read benchmark results:', e.message); | ||
| process.exit(0); | ||
| } | ||
|
|
||
| const metrics = [ | ||
| { name: 'DOM Content Loaded', key: 'dom_content_loaded_ms', unit: 'ms', warn: 0.25, fail: 0.50 }, | ||
| { name: 'Page Load', key: 'page_load_ms', unit: 'ms', warn: 0.25, fail: 0.50 }, | ||
| { name: 'LCP', key: 'lcp_ms', unit: 'ms', warn: 0.25, fail: 0.50 }, | ||
| { name: 'Tab Navigation', key: 'tab_nav_ms', unit: 'ms', warn: 0.25, fail: 0.50 }, | ||
| { name: 'JS Size', key: 'total_js_kb', unit: 'KB', warn: 0.10, fail: 0.25 }, | ||
| { name: 'CSS Size', key: 'total_css_kb', unit: 'KB', warn: 0.10, fail: 0.25 }, | ||
| ]; | ||
|
|
||
| const pad = (s, n) => String(s).padEnd(n); | ||
| const rpad = (s, n) => String(s).padStart(n); | ||
|
|
||
| console.log(''); | ||
| console.log('='.repeat(78)); | ||
| console.log(' Frontend Performance Benchmark'); | ||
| console.log(' Base: ${{ steps.latest_tag.outputs.tag }}'); | ||
| console.log('='.repeat(78)); | ||
| console.log(''); | ||
| console.log(pad('Metric', 22) + rpad('Base', 12) + rpad('PR', 12) + rpad('Change', 12) + ' Status'); | ||
| console.log('-'.repeat(78)); | ||
|
|
||
| let shouldFail = false; | ||
| let md = '## Frontend Performance Benchmark\n\n'; | ||
| md += '| Metric | Base | PR | Change | Status |\n'; | ||
| md += '|--------|------|----|--------|--------|\n'; | ||
|
|
||
| for (const m of metrics) { | ||
| const bv = base[m.key]; | ||
| const pv = pr[m.key]; | ||
| if (bv === undefined || pv === undefined) continue; | ||
| const pct = bv === 0 ? 0 : (pv - bv) / bv; | ||
| const pctStr = (pct * 100).toFixed(1) + '%'; | ||
| const changeStr = (pct > 0 ? '+' : '') + pctStr; | ||
| const absDiff = pv - bv; | ||
| const minAbsDiff = m.unit === 'ms' ? 200 : 0; | ||
| let status = 'OK'; | ||
| let mdStatus = '✅'; | ||
| if (pct > m.fail && absDiff > minAbsDiff) { status = 'FAIL'; mdStatus = '❌ FAIL'; shouldFail = true; } | ||
| else if (pct > m.warn && absDiff > minAbsDiff) { status = 'WARNING'; mdStatus = '⚠️ WARNING'; } | ||
| console.log(pad(m.name, 22) + rpad(bv + ' ' + m.unit, 12) + rpad(pv + ' ' + m.unit, 12) + rpad(changeStr, 12) + ' ' + status); | ||
| md += '| ' + m.name + ' | ' + bv + ' ' + m.unit + ' | ' + pv + ' ' + m.unit + ' | ' + changeStr + ' | ' + mdStatus + ' |\n'; | ||
| } | ||
|
|
||
| console.log('-'.repeat(78)); | ||
| console.log(pad('JS Resources', 22) + rpad(base.js_resource_count || '-', 12) + rpad(pr.js_resource_count || '-', 12)); | ||
| console.log(pad('CSS Resources', 22) + rpad(base.css_resource_count || '-', 12) + rpad(pr.css_resource_count || '-', 12)); | ||
| console.log(''); | ||
|
|
||
| md += '\n| JS Resources | ' + (base.js_resource_count || '-') + ' | ' + (pr.js_resource_count || '-') + ' | | |\n'; | ||
| md += '| CSS Resources | ' + (base.css_resource_count || '-') + ' | ' + (pr.css_resource_count || '-') + ' | | |\n'; | ||
| md += '\n> Base: \`${{ steps.latest_tag.outputs.tag }}\` | Thresholds: timing warns at +25%/fails at +50%, size warns at +10%/fails at +25%.\n'; | ||
|
|
||
| fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, md); | ||
|
|
||
| if (shouldFail) { | ||
| console.log('❌ Frontend performance regression detected.'); | ||
| process.exit(1); | ||
| } else { | ||
| console.log('✅ No performance regressions detected.'); | ||
| } | ||
| " | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| import { test as base, expect } from "@playwright/test"; | ||
| import { launchGradioApp, killGradioApp } from "@self/tootils/app-launcher"; | ||
| import fs from "fs"; | ||
|
|
||
| const PERF_RESULTS_FILE = | ||
| process.env.PERF_RESULTS_FILE || "/tmp/perf_results.json"; | ||
|
|
||
| const ITERATIONS = 5; | ||
|
|
||
| function median(values: number[]): number { | ||
| const sorted = [...values].sort((a, b) => a - b); | ||
| const mid = Math.floor(sorted.length / 2); | ||
| return sorted.length % 2 !== 0 | ||
| ? sorted[mid] | ||
| : Math.round((sorted[mid - 1] + sorted[mid]) / 2); | ||
| } | ||
|
|
||
| const test = base.extend<{ perfPage: import("@playwright/test").Page }>({ | ||
| perfPage: async ({ page }, use, testInfo) => { | ||
| const { port, process: appProcess } = await launchGradioApp( | ||
| "tabs", | ||
| testInfo.workerIndex, | ||
| 60000 | ||
| ); | ||
|
|
||
| const url = `http://localhost:${port}`; | ||
|
|
||
| const resourceSizes = { js: 0, css: 0, jsCount: 0, cssCount: 0 }; | ||
| const responseHandler = (response: any): void => { | ||
| const rUrl = response.url(); | ||
| const headers = response.headers(); | ||
| const bytes = parseInt(headers["content-length"] || "0", 10); | ||
| if (rUrl.endsWith(".js") || rUrl.endsWith(".mjs")) { | ||
| resourceSizes.js += bytes; | ||
| resourceSizes.jsCount++; | ||
| } else if (rUrl.endsWith(".css")) { | ||
| resourceSizes.css += bytes; | ||
| resourceSizes.cssCount++; | ||
| } | ||
| }; | ||
|
|
||
| page.on("response", responseHandler); | ||
| await page.goto(url); | ||
| await page.waitForLoadState("networkidle"); | ||
| page.removeListener("response", responseHandler); | ||
|
|
||
| const domContentLoadedValues: number[] = []; | ||
| const pageLoadValues: number[] = []; | ||
| const lcpValues: number[] = []; | ||
| const tabNavValues: number[] = []; | ||
|
|
||
| for (let i = 0; i < ITERATIONS; i++) { | ||
| await page.goto(url); | ||
| await page.waitForLoadState("networkidle"); | ||
|
|
||
| await page.evaluate(() => { | ||
dawoodkhan82 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| (window as any).__lcpValue = 0; | ||
| new PerformanceObserver((list) => { | ||
| const entries = list.getEntries(); | ||
| if (entries.length > 0) { | ||
| (window as any).__lcpValue = entries[entries.length - 1].startTime; | ||
| } | ||
| }).observe({ type: "largest-contentful-paint", buffered: true }); | ||
| }); | ||
|
|
||
| await page.waitForTimeout(500); | ||
|
|
||
| const timings = await page.evaluate(() => { | ||
| const nav = performance.getEntriesByType( | ||
| "navigation" | ||
| )[0] as PerformanceNavigationTiming; | ||
| return { | ||
| domContentLoaded: Math.round(nav.domContentLoadedEventEnd), | ||
| pageLoad: Math.round(nav.loadEventEnd), | ||
| lcp: Math.round((window as any).__lcpValue || 0) | ||
| }; | ||
| }); | ||
|
|
||
| domContentLoadedValues.push(timings.domContentLoaded); | ||
| pageLoadValues.push(timings.pageLoad); | ||
| lcpValues.push(timings.lcp); | ||
|
|
||
| await page.evaluate(() => performance.mark("nav-start")); | ||
dawoodkhan82 marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| await page.click('button[data-tab-id="a2"]'); | ||
| await page.locator("text=Text 2!").waitFor({ state: "visible" }); | ||
| const navDuration = await page.evaluate(() => { | ||
| performance.mark("nav-end"); | ||
| const m = performance.measure("tab-nav", "nav-start", "nav-end"); | ||
| return Math.round(m.duration); | ||
| }); | ||
| tabNavValues.push(navDuration); | ||
| } | ||
|
|
||
| const perfMetrics = { | ||
| dom_content_loaded_ms: median(domContentLoadedValues), | ||
| page_load_ms: median(pageLoadValues), | ||
| lcp_ms: median(lcpValues), | ||
| tab_nav_ms: median(tabNavValues), | ||
| total_js_kb: Math.round(resourceSizes.js / 1024), | ||
| total_css_kb: Math.round(resourceSizes.css / 1024), | ||
| js_resource_count: resourceSizes.jsCount, | ||
| css_resource_count: resourceSizes.cssCount | ||
| }; | ||
|
|
||
| await page.evaluate( | ||
| (m) => ((window as any).__perfMetrics = m), | ||
| perfMetrics | ||
| ); | ||
|
|
||
| await use(page); | ||
| killGradioApp(appProcess); | ||
| } | ||
| }); | ||
|
|
||
| test("collect frontend performance metrics", async ({ perfPage: page }) => { | ||
| const metrics = await page.evaluate(() => (window as any).__perfMetrics); | ||
| fs.writeFileSync(PERF_RESULTS_FILE, JSON.stringify(metrics, null, 2)); | ||
| expect(metrics.dom_content_loaded_ms).toBeGreaterThan(0); | ||
| expect(metrics.page_load_ms).toBeGreaterThan(0); | ||
| expect(metrics.tab_nav_ms).toBeGreaterThan(0); | ||
| expect(metrics.total_js_kb).toBeGreaterThan(0); | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.