## What Add a per-frame text measurement cache for all hot-path ctx.measureText() calls. ## Why drawTruncatingText() in BaseWidget calls ctx.measureText() per widget per frame with zero caching. For a 50-node workflow at 60fps: ~78,000-243,000 measureText calls/sec. Text labels rarely change between frames. ## How Global Map<string, number> cache keyed by font+text, cleared once per frame at the start of drawFrontCanvas(). Replaces direct ctx.measureText() calls in BaseWidget.drawTruncatingText, draw.ts truncateTextToWidth/drawTextInArea, LGraphBadge.getWidth, LGraphButton.getWidth, and textUtils.truncateText. ## Perf Impact Expected: ~95% reduction in measureText calls (only cache misses on first frame and value changes). Firefox has slower measureText than Chrome, so this disproportionately benefits Firefox. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9404-fix-cache-ctx-measureText-results-to-avoid-redundant-calls-in-draw-loop-31a6d73d3650814e9cdac16949c55cb7) by [Unito](https://www.unito.io) --------- Co-authored-by: GitHub Action <action@github.com>
6.5 KiB
name, description
| name | description |
|---|---|
| perf-fix-with-proof | Ships performance fixes with CI-proven improvement using stacked PRs. PR1 adds a @perf test (establishes baseline on main), PR2 adds the fix (CI shows delta). Use when implementing a perf optimization and wanting to prove it in CI. |
Performance Fix with Proof
Ships perf fixes as two stacked PRs so CI automatically proves the improvement.
Why Two PRs
The ci-perf-report.yaml workflow compares PR metrics against the base branch baseline. If you add a new @perf test in the same PR as the fix, that test doesn't exist on main yet — no baseline, no delta, no proof. Stacking solves this:
- PR1 (test-only) — adds the
@perftest that exercises the bottleneck. Merges to main. CI runs it on main → baseline established. - PR2 (fix) — adds the optimization. CI runs the same test → compares against PR1's baseline → delta shows improvement.
Workflow
Step 1: Create the test branch
git worktree add <worktree-path> -b perf/test-<name> origin/main
Step 2: Write the @perf test
Add a test to browser_tests/tests/performance.spec.ts (or a new file with @perf tag). The test should stress the specific bottleneck.
Test structure:
test('<descriptive name>', async ({ comfyPage }) => {
// 1. Load a workflow that exercises the bottleneck
await comfyPage.workflow.loadWorkflow('<workflow>')
// 2. Start measuring
await comfyPage.perf.startMeasuring()
// 3. Perform the action that triggers the bottleneck (at scale)
for (let i = 0; i < N; i++) {
// ... stress the hot path ...
await comfyPage.nextFrame()
}
// 4. Stop measuring and record
const m = await comfyPage.perf.stopMeasuring('<metric-name>')
recordMeasurement(m)
console.log(`<name>: ${m.styleRecalcs} recalcs, ${m.layouts} layouts`)
})
Available metrics (from PerformanceHelper):
m.styleRecalcs/m.styleRecalcDurationMs— style recalculation count and timem.layouts/m.layoutDurationMs— forced layout count and timem.taskDurationMs— total main-thread JS execution timem.heapDeltaBytes— memory pressure delta
Key helpers (from ComfyPage):
comfyPage.perf.startMeasuring()/.stopMeasuring(name)— CDP metrics capturecomfyPage.nextFrame()— wait one animation framecomfyPage.workflow.loadWorkflow(name)— load a test workflow frombrowser_tests/assets/comfyPage.canvas— the canvas locatorcomfyPage.page.mouse.move(x, y)— mouse interaction
Step 3: Add test workflow asset (if needed)
If the bottleneck needs a specific workflow (e.g., 50+ nodes, many DOM widgets), add it to browser_tests/assets/. Keep it minimal — only the structure needed to trigger the bottleneck.
Step 4: Verify locally
pnpm exec playwright test --project=performance --grep "<test name>"
Confirm the test runs and produces reasonable metric values.
Step 5: Create PR1 (test-only)
pnpm typecheck:browser
pnpm lint
git add browser_tests/
git commit -m "test: add perf test for <bottleneck description>"
git push -u origin perf/test-<name>
gh pr create --title "test: add perf test for <bottleneck>" \
--body "Adds a @perf test to establish a baseline for <bottleneck>.
This is PR 1 of 2. The fix will follow in a separate PR once this baseline is established on main.
## What
Adds \`<test-name>\` to the performance test suite measuring <metric> during <action>.
## Why
Needed to prove the improvement from the upcoming fix for backlog item #<N>." \
--base main
Step 6: Get PR1 merged
Once PR1 merges, CI runs the test on main → baseline artifact saved.
Step 7: Create PR2 (fix) on top of main
git worktree add <worktree-path> -b perf/fix-<name> origin/main
Implement the fix. The @perf test from PR1 is now on main and will run automatically. CI will:
- Run the test on the PR branch
- Download the baseline from main (which includes PR1's test results)
- Post a PR comment showing the delta
Step 8: Verify the improvement shows in CI
The ci-perf-report.yaml posts a comment like:
## ⚡ Performance Report
| Metric | Baseline | PR (n=3) | Δ | Sig |
| --------------------- | -------- | -------- | ---- | --- |
| <name>: style recalcs | 450 | 12 | -97% | 🟢 |
If Δ is negative for the target metric, the fix is proven.
Test Design Guidelines
- Stress the specific bottleneck — don't measure everything, isolate the hot path
- Use enough iterations — the test should run long enough that the metric difference is clear (100+ frames for idle tests, 50+ interactions for event tests)
- Keep it deterministic — avoid timing-dependent assertions; measure counts not durations when possible
- Match the backlog entry — reference the backlog item number in the test name or PR description
Examples
Testing DOM widget reactive mutations (backlog #8):
test('DOM widget positioning recalculations', async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('default')
await comfyPage.perf.startMeasuring()
// Idle for 120 frames — DOM widgets update position every frame
for (let i = 0; i < 120; i++) {
await comfyPage.nextFrame()
}
const m = await comfyPage.perf.stopMeasuring('dom-widget-idle')
recordMeasurement(m)
})
Testing measureText caching (backlog #4):
test('canvas text rendering with many nodes', async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('large-workflow-50-nodes')
await comfyPage.perf.startMeasuring()
for (let i = 0; i < 60; i++) {
await comfyPage.nextFrame()
}
const m = await comfyPage.perf.stopMeasuring('text-rendering-50-nodes')
recordMeasurement(m)
})
Reference
| Resource | Path |
|---|---|
| Perf test file | browser_tests/tests/performance.spec.ts |
| PerformanceHelper | browser_tests/fixtures/helpers/PerformanceHelper.ts |
| Perf reporter | browser_tests/helpers/perfReporter.ts |
| CI workflow | .github/workflows/ci-perf-report.yaml |
| Report generator | scripts/perf-report.ts |
| Stats utilities | scripts/perf-stats.ts |
| Backlog | docs/perf/BACKLOG.md (local only, not committed) |
| Playbook | docs/perf/PLAYBOOK.md (local only, not committed) |