Skip to content

refactor: harden ChangeTracker lifecycle with self-defending API#10816

Open
jaeone94 wants to merge 7 commits intomainfrom
refactor/changetracker-lifecycle-hardening
Open

refactor: harden ChangeTracker lifecycle with self-defending API#10816
jaeone94 wants to merge 7 commits intomainfrom
refactor/changetracker-lifecycle-hardening

Conversation

@jaeone94
Copy link
Copy Markdown
Collaborator

@jaeone94 jaeone94 commented Apr 2, 2026

Summary

Harden the ChangeTracker lifecycle to eliminate the class of bugs where an inactive workflow's tracker silently captures the wrong graph state. Renames checkState() to captureCanvasState() with a self-defending assertion, introduces deactivate() and prepareForSave() lifecycle methods, and closes a latent undo-history corruption bug discovered during code review.

Background

ComfyUI supports multiple workflows open as tabs, but only one canvas (app.rootGraph) exists at a time. When the user switches tabs, the old workflow's graph is unloaded and the new one is loaded into this shared canvas.

The old checkState() method serialized app.rootGraph into activeState to track changes for undo/redo. It had no awareness of which workflow it belonged to -- if called on an inactive tab's tracker, it would capture the active tab's graph data and silently overwrite the inactive workflow's state. This caused permanent data loss (fixed in PR #10745 with caller-side isActive guards).

The caller-side guards were fragile: every new call site had to remember to add the guard, and forgetting would reintroduce the same silent data corruption. Additionally, beforeLoadNewGraph only called store() (viewport/outputs) without checkState(), meaning canvas state could be stale if a tab switch happened without a preceding mouseup event.

Before (fragile)

saveWorkflow(workflow):
  if (isActive(workflow))              <-- caller must remember this guard
    workflow.changeTracker.checkState()      <-- name implies "read", actually writes
  ...

beforeLoadNewGraph():
  activeWorkflow.changeTracker.store()      <-- only saves viewport, NOT graph state

After (self-defending)

saveWorkflow(workflow):
  workflow.changeTracker.prepareForSave()   <-- handles active/inactive internally
  ...

beforeLoadNewGraph():
  activeWorkflow.changeTracker.deactivate() <-- captures graph + viewport together

Changes

  • What: Rename checkState to captureCanvasState with active-tracker assertion; add deactivate() and prepareForSave() lifecycle methods; fix undo-history corruption bug; add deprecated checkState() wrapper for extension compatibility; add 14 unit tests
  • Breaking: checkState() is renamed to captureCanvasState(). A deprecated checkState() wrapper is provided that delegates to captureCanvasState() and logs a DEV warning. Extensions calling checkState() will continue to work but should migrate. This is safe because the wrapper calls the exact same logic -- the only behavioral difference is the new active-tracker assertion, which improves safety for extensions (prevents silent data corruption instead of allowing it).
  • Dependencies: None

Commit-by-Commit Walkthrough

1. [517f7a3] refactor: rename checkState to captureCanvasState with active-tracker assertion

26 files changed -- mechanical rename across all production code, tests, and docs.

Core change in changeTracker.ts:

  • Renamed checkState() to captureCanvasState() to reflect that it is a write operation (serializes canvas into activeState + pushes undo), not a read
  • Added self-defending invariant: the method checks useWorkflowStore().activeWorkflow?.changeTracker === this and returns early (with a DEV error log) if called on an inactive tracker
  • Renamed the internal init() closure from checkState to captureState and updated all logger strings
  • Fixed ColorPickerButton.vue missing optional chaining (.checkState() to ?.captureCanvasState())
Before / After

Before -- no guard, ambiguous name:

// changeTracker.ts
checkState() {
  if (!app.graph || this.changeCount || ChangeTracker.isLoadingGraph) return
  const currentState = clone(app.rootGraph.serialize()) as ComfyWorkflowJSON
  // ... compare and push undo
}

// 20+ callers
workflowStore.activeWorkflow?.changeTracker?.checkState()

After -- self-defending, descriptive name:

// changeTracker.ts
captureCanvasState() {
  if (!app.graph || this.changeCount || ChangeTracker.isLoadingGraph) return

  const activeTracker = useWorkflowStore().activeWorkflow?.changeTracker
  if (activeTracker !== this) {
    if (import.meta.env.DEV) {
      logger.error('captureCanvasState called on inactive tracker for:', this.workflow.path)
    }
    return
  }

  const currentState = clone(app.rootGraph.serialize()) as ComfyWorkflowJSON
  // ... compare and push undo
}

// 20+ callers
workflowStore.activeWorkflow?.changeTracker?.captureCanvasState()

2. [fa16f3d] refactor: add deactivate() and prepareForSave() to ChangeTracker

Two new lifecycle methods:

  • deactivate() -- calls captureCanvasState() then store(). These two operations must always happen together when a workflow goes inactive, but were never combined. Previously store() was called alone in beforeLoadNewGraph, meaning canvas state could be stale.
  • prepareForSave() -- if this tracker is the active workflow's tracker, calls captureCanvasState(). Otherwise, no-op (the state was already frozen by deactivate()). This eliminates the need for caller-side isActive guards in save paths.
Before / After

Before -- store() and captureCanvasState() are separate, caller must coordinate:

// ChangeTracker only had:
store()   // saves viewport/outputs
restore() // restores viewport/outputs
reset()   // marks clean

After -- lifecycle transitions are explicit:

// ChangeTracker now also has:
deactivate() {
  this.captureCanvasState()  // freeze graph state
  this.store()               // freeze viewport/outputs
}

prepareForSave() {
  const isActive = useWorkflowStore().activeWorkflow?.changeTracker === this
  if (isActive) this.captureCanvasState()
  // inactive: no-op (already frozen by deactivate)
}

3. [bc8cecc] refactor: use deactivate/prepareForSave in workflowService

  • beforeLoadNewGraph: replaced store() with deactivate(), ensuring canvas state is always captured before a tab switch (closes the gap where activeState could be stale)
  • saveWorkflow and saveWorkflowAs (3 sites): replaced if (workflowStore.isActive(workflow)) captureCanvasState() with prepareForSave() -- no caller-side active checks needed
Before / After

Before -- caller-side guards, incomplete deactivation:

// beforeLoadNewGraph
activeWorkflow.changeTracker.store()  // only viewport, graph state may be stale

// saveWorkflow
if (workflowStore.isActive(workflow))
  workflow.changeTracker?.captureCanvasState()
await workflowStore.saveWorkflow(workflow)

// saveWorkflowAs (2 sites)
if (workflowStore.isActive(target))
  target.changeTracker?.captureCanvasState()

After -- self-contained, no guards needed:

// beforeLoadNewGraph
activeWorkflow.changeTracker?.deactivate()  // graph + viewport, guaranteed fresh

// saveWorkflow
workflow.changeTracker?.prepareForSave()
await workflowStore.saveWorkflow(workflow)

// saveWorkflowAs (2 sites)
target.changeTracker?.prepareForSave()

4. [3827ad6] fix: address code review findings for ChangeTracker refactor

Fixes discovered by 5 parallel code review agents:

  • Undo-history corruption bug (Bug Hunt): deactivate() in beforeLoadNewGraph introduced a new code path where captureCanvasState() could be called during an undo/redo operation (_restoringState = true). The undo flow is: undo() sets _restoringState = true, then calls app.loadGraphData(), which calls beforeLoadNewGraph(), which calls deactivate(), which calls captureCanvasState() -- pushing to the undo queue during undo. This corrupts undo history with duplicate entries. Fix: Added this._restoringState to the early-return guards in captureCanvasState().

  • Deprecated checkState() wrapper (API Contract): ChangeTracker is exposed to extensions via window.comfyAPI.changeTracker.ChangeTracker (through comfyAPIPlugin). Removing checkState() entirely would break extensions. The wrapper delegates to captureCanvasState() and logs a DEV deprecation warning. This is safe because: (1) the wrapper calls the exact same code path, (2) the new active-tracker assertion is strictly more protective than the old behavior (rejects invalid calls instead of corrupting data).

  • Null guard (Bug Hunt): activeWorkflow.changeTracker.deactivate() to ?.deactivate() for consistency with all other call sites

  • JSDoc precondition (Architecture): deactivate() must be called while the workflow is still active (before the activeWorkflow pointer moves). Documented this temporal coupling explicitly.

  • Mock utility (Test Quality): Added deactivate and prepareForSave to createMockChangeTracker in litegraphTestUtils.ts

  • Documentation: Added Lifecycle Methods table and _restoringState invariant to docs/architecture/change-tracker.md

Before / After

Before -- no undo guard, no deprecation wrapper:

// captureCanvasState guards (commit 1)
if (!app.graph || this.changeCount || ChangeTracker.isLoadingGraph) return

// no checkState() wrapper -- extensions break on rename

// beforeLoadNewGraph (commit 3)
activeWorkflow.changeTracker.deactivate()  // no optional chaining

After -- undo-safe, backward-compatible, null-safe:

// captureCanvasState guards
if (!app.graph || this.changeCount || this._restoringState || ChangeTracker.isLoadingGraph) return

// deprecated wrapper for extension compat
/** @deprecated Use captureCanvasState instead. */
checkState() {
  if (import.meta.env.DEV) {
    logger.warn('checkState() is deprecated -- use captureCanvasState() instead.')
  }
  this.captureCanvasState()
}

// beforeLoadNewGraph
activeWorkflow.changeTracker?.deactivate()  // null-safe

5. [244e214] test: add unit tests for ChangeTracker lifecycle methods

14 unit tests in new file src/scripts/changeTracker.test.ts:

Group Tests What they verify
captureCanvasState guards 5 No-op when: no graph, changeCount > 0, isLoadingGraph, _restoringState, inactive tracker
captureCanvasState capture 3 Pushes to undoQueue on diff, skips on identical state, caps at MAX_HISTORY
captureCanvasState redo 1 Clears redoQueue on new change
deactivate 2 Active: captures + stores. Inactive: skips capture, still stores viewport
prepareForSave 2 Active: captures. Inactive: no-op
checkState (deprecated) 1 Delegates to captureCanvasState

These tests specifically cover the bug scenarios this refactor defends against:

6. [d21294d] fix: address CodeRabbit review findings

Fixes from automated CodeRabbit review:

  • deactivate() inactive guard: store() reads global canvas/viewport/output state. If deactivate() were called after the active pointer moved, it would freeze the new workflow's viewport into the old tracker. Added the same isActive check as captureCanvasState() -- inactive calls are now a full no-op with a DEV error log.
  • Mock completeness: Added deprecated checkState() to createMockChangeTracker so extension-oriented tests don't hit checkState is not a function.
  • Documentation: Clarified that workflowService.ts calls captureCanvasState() indirectly via deactivate()/prepareForSave(), not directly.
  • Test update: deactivate() inactive path now asserts that store() is also not called (previously only checked serialize).
Before / After

Before -- deactivate() always calls store() even when inactive:

deactivate() {
  this.captureCanvasState()  // self-defending, no-ops if inactive
  this.store()               // NOT guarded -- freezes wrong viewport
}

After -- full no-op when inactive:

deactivate() {
  const isActive = useWorkflowStore().activeWorkflow?.changeTracker === this
  if (!isActive) {
    if (import.meta.env.DEV) {
      logger.error('deactivate() called on inactive tracker for:', this.workflow.path)
    }
    return
  }
  this.captureCanvasState()
  this.store()
}

Why No New E2E Tests

Existing E2E tests in browser_tests/tests/workflowPersistence.spec.ts already cover the observable behavior:

  • "Closing an inactive tab with save preserves its own content" (L326-405) -- verifies that saving an inactive tab preserves its content, not the active tab's. This exercises the prepareForSave() no-op path end-to-end.
  • "Closing an inactive unsaved tab with save preserves its own content" (L407-470) -- same scenario for saveWorkflowAs.

The changes in this PR are internal refactoring (method rename, extraction, guard addition) that do not change observable user behavior. The one behavioral fix (_restoringState guard preventing undo corruption) is an internal queue state issue best verified at the unit level -- E2E tests cannot directly inspect undo queue integrity, and the scenario (undo during tab switch) would produce a brittle, timing-dependent test.

Review Focus

  1. Commit 4: The _restoringState guard fix -- this is the only behavioral change that was not in the original design proposal. Verify the undo/redo flow analysis is correct.
  2. Deprecated checkState() wrapper: Is the deprecation strategy sufficient for the extension ecosystem, or should we also add the wrapper to the release notes?
  3. deactivate() temporal coupling: The method requires being called before the activeWorkflow pointer moves. The JSDoc documents this, but is that sufficient or should we add a runtime assertion?

New ChangeTracker Public API

Method Caller Purpose
captureCanvasState() Event handlers, UI interactions Snapshots canvas into activeState, pushes undo. Asserts active tracker.
deactivate() beforeLoadNewGraph only captureCanvasState() + store(). Freezes everything for tab switch.
prepareForSave() Save paths only Active: captureCanvasState(). Inactive: no-op.
checkState() Deprecated -- extensions only Wrapper that delegates to captureCanvasState() with DEV warning.
store() Internal to deactivate() Saves viewport, outputs, subgraph navigation.
restore() afterLoadNewGraph Restores viewport, outputs, subgraph navigation.
reset() afterLoadNewGraph, save Resets initial state (marks as "clean").

jaeone94 added 5 commits April 2, 2026 10:24
… assertion

Add self-defending invariant to captureCanvasState (formerly checkState):
only the active workflow's tracker may read from the canvas. Calling it
on an inactive tracker logs an error in DEV and returns early, preventing
silent cross-workflow data corruption.

Mechanical rename across all 26 production/test files and docs.
- deactivate(): unified freeze step combining captureCanvasState() +
  store(), called when a workflow goes inactive
- prepareForSave(): save-path API that flushes canvas state only for the
  active workflow (no-op for inactive, whose state was frozen by deactivate)
- beforeLoadNewGraph: replace store() with deactivate() so activeState
  is guaranteed fresh before tab switch
- saveWorkflow/saveWorkflowAs: replace isActive guards + captureCanvasState
  with prepareForSave() — no caller-side active checks needed
- Guard captureCanvasState against _restoringState to prevent undo
  history corruption when deactivate() is called during undo/redo
- Add deprecated checkState() wrapper for extension backward compat
- Use optional chaining for deactivate() call in beforeLoadNewGraph
- Add deactivate/prepareForSave to mock utility
- Document lifecycle methods and preconditions in JSDoc and arch docs
Cover captureCanvasState guards (no graph, changeCount, isLoadingGraph,
_restoringState, inactive tracker), state capture with undo queue
management, deactivate (active + inactive paths), prepareForSave
(active + inactive paths), and deprecated checkState wrapper.
@jaeone94 jaeone94 requested a review from a team April 2, 2026 03:38
@jaeone94 jaeone94 requested a review from Myestery as a code owner April 2, 2026 03:38
@dosubot dosubot bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label Apr 2, 2026
@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 2, 2026

🎨 Storybook: ✅ Built — View Storybook

Details

⏰ Completed at: 04/03/2026, 05:11:39 AM UTC

Links

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 2, 2026

🎭 Playwright: ✅ 931 passed, 0 failed · 7 flaky

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 2, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 473fe193-63cc-463e-8f89-1eae07dae627

📥 Commits

Reviewing files that changed from the base of the PR and between d21294d and 62d21de.

📒 Files selected for processing (1)
  • src/scripts/changeTracker.test.ts
✅ Files skipped from review due to trivial changes (1)
  • src/scripts/changeTracker.test.ts

📝 Walkthrough

Walkthrough

Renames the change-tracker snapshot API from checkState() to captureCanvasState(), adds lifecycle methods deactivate() and prepareForSave(), enforces guards against unsafe captures (inactive tracker, loading, restoring, pending changes, no graph), and updates call sites, tests, docs, and workflow save/deactivate flows accordingly.

Changes

Cohort / File(s) Summary
Browser E2E Tests
browser_tests/tests/actionbar.spec.ts, browser_tests/tests/browserTabTitle.spec.ts, browser_tests/tests/changeTrackerLoadGuard.spec.ts, browser_tests/tests/subgraph/subgraphNested.spec.ts, browser_tests/tests/workflowPersistence.spec.ts
Replaced checkState() invocations with captureCanvasState() in test harness/page.evaluate calls and updated related comments/descriptions.
Documentation
docs/architecture/change-tracker.md
Documented captureCanvasState() rename, added lifecycle table and cross-workflow/invocation guards, updated manual call-site guidance and invariants.
Component & Store Tests
src/components/builder/BuilderFooterToolbar.test.ts, src/components/builder/builderViewOptions.test.ts, src/components/graph/selectionToolbox/ColorPickerButton.test.ts, src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.test.ts, src/stores/appModeStore.test.ts, src/composables/useCoreCommands.test.ts
Updated mocks and expectations to use captureCanvasState and added mocks for new lifecycle methods where applicable.
Component Implementations
src/components/builder/builderViewOptions.ts, src/components/graph/selectionToolbox/ColorPickerButton.vue, src/components/searchbox/NodeSearchBoxPopover.vue, src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue, src/stores/appModeStore.ts, src/composables/useCoreCommands.ts
Replaced checkState() calls with captureCanvasState() after canvas mutations/state changes; preserved surrounding logic and optional chaining guards.
Composable Implementations
src/composables/graph/useCanvasRefresh.ts, src/composables/graph/useGroupMenuOptions.ts, src/composables/graph/useSelectedNodeActions.ts, src/composables/graph/useSelectionOperations.ts, src/composables/graph/useSubgraphOperations.ts
Switched change-tracker invocations to captureCanvasState() across graph, selection, and subgraph operations.
Core Change-Tracker Logic
src/scripts/changeTracker.ts
Introduced captureCanvasState() (guarded, serializes canvas, manages undo/redo/activeState), added deactivate() and prepareForSave(), deprecated checkState() (delegates with DEV-warn), and updated event hooks to use the new method.
Change-Tracker Test Suite
src/scripts/changeTracker.test.ts
New comprehensive tests covering captureCanvasState guards and state-change mechanics, undo/redo behavior, deactivate() and prepareForSave(), and checkState delegation.
Workflow Service
src/platform/workflow/core/services/workflowService.ts
Save flows now call prepareForSave(); teardown/deactivate uses deactivate(); removed redundant guards around prior checkState() usage.
Test Utilities
src/utils/__tests__/litegraphTestUtils.ts
createMockChangeTracker mock augmented with captureCanvasState, deactivate, and prepareForSave (retains checkState for backward-compat test coverage).

Sequence Diagram(s)

sequenceDiagram
    participant UI as UI/Component
    participant Store as workflowStore
    participant CT as ChangeTracker
    participant App as app/rootGraph
    participant Stores as output/subgraph stores

    UI->>Store: mutate canvas / mark dirty
    Store->>CT: activeWorkflow.changeTracker.captureCanvasState()
    CT->>CT: run guards (graph present, changeCount==0, not restoring, isActive)
    alt guards pass
        CT->>App: serialize rootGraph
        App-->>CT: serialized state
        CT->>CT: compare with activeState
        alt state changed
            CT->>CT: push previous to undoQueue
            CT->>CT: clear redoQueue
            CT->>CT: update activeState & updateModified()
        end
    else guards fail
        CT-->>Store: no-op (DEV-log)
    end

    Note right of CT: deactivate()/prepareForSave() call capture + store snapshots, then trigger Stores snapshot/export
    CT->>Stores: snapshot outputs / export subgraph (on deactivate)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped and peered across the pane,
I captured canvas, not checkState's name,
With guards and life‑cycle snug and neat,
Undo queues bounded, histories sweet,
A little rabbit cheers the change—hooray! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the main change: hardening the ChangeTracker lifecycle with a self-defending API through method renaming and new lifecycle methods.
Description check ✅ Passed The description is comprehensive and follows the template structure with Summary, Changes, and Review Focus sections. It provides detailed context, commit walkthrough, and clear explanation of the refactoring rationale.
End-To-End Regression Coverage For Fixes ✅ Passed PR title uses 'refactor' (not bug-fix language) and multiple browser_tests/ files are changed, satisfying both criteria.
Adr Compliance For Entity/Litegraph Changes ✅ Passed PR does not modify trigger paths (src/lib/litegraph/ or src/ecs/) or graph entity classes; all 28 changed files are in test, component, service, store, documentation, and utility directories outside ADR scope.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/changetracker-lifecycle-hardening

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions
Copy link
Copy Markdown

github-actions bot commented Apr 2, 2026

📦 Bundle: 5.11 MB gzip 🔴 +197 B

Details

Summary

  • Raw size: 23.5 MB baseline 23.5 MB — 🔴 +1.5 kB
  • Gzip: 5.11 MB baseline 5.11 MB — 🔴 +197 B
  • Brotli: 3.95 MB baseline 3.95 MB — 🔴 +51 B
  • Bundles: 250 current • 250 baseline • 114 added / 114 removed

Category Glance
Data & Services 🔴 +1.45 kB (2.97 MB) · Graph Workspace 🔴 +41 B (1.2 MB) · Other 🔴 +16 B (8.44 MB) · Vendor & Third-Party ⚪ 0 B (9.8 MB) · Panels & Settings ⚪ 0 B (484 kB) · Utilities & Hooks ⚪ 0 B (338 kB) · + 5 more

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

Main entry bundles and manifests

File Before After Δ Raw Δ Gzip Δ Brotli
assets/index-BQFKGOSV.js (removed) 22.3 kB 🟢 -22.3 kB 🟢 -7.95 kB 🟢 -6.79 kB
assets/index-jPZcJUjL.js (new) 22.3 kB 🔴 +22.3 kB 🔴 +7.95 kB 🔴 +6.79 kB

Status: 1 added / 1 removed

Graph Workspace — 1.2 MB (baseline 1.2 MB) • 🔴 +41 B

Graph editor runtime, canvas, workflow orchestration

File Before After Δ Raw Δ Gzip Δ Brotli
assets/GraphView-DSPHV0hB.js (new) 1.2 MB 🔴 +1.2 MB 🔴 +256 kB 🔴 +193 kB
assets/GraphView-uvJNeKIB.js (removed) 1.2 MB 🟢 -1.2 MB 🟢 -256 kB 🟢 -193 kB

Status: 1 added / 1 removed

Views & Navigation — 76.6 kB (baseline 76.6 kB) • ⚪ 0 B

Top-level views, pages, and routed surfaces

File Before After Δ Raw Δ Gzip Δ Brotli
assets/CloudSurveyView-CdeP6Vjo.js (new) 15.7 kB 🔴 +15.7 kB 🔴 +3.4 kB 🔴 +2.9 kB
assets/CloudSurveyView-YXF21fMV.js (removed) 15.7 kB 🟢 -15.7 kB 🟢 -3.4 kB 🟢 -2.89 kB
assets/CloudLoginView-DeVpiw6j.js (removed) 12 kB 🟢 -12 kB 🟢 -3.36 kB 🟢 -2.96 kB
assets/CloudLoginView-DTknAsuB.js (new) 12 kB 🔴 +12 kB 🔴 +3.35 kB 🔴 +2.97 kB
assets/CloudSignupView-CF2j63R0.js (removed) 9.78 kB 🟢 -9.78 kB 🟢 -2.86 kB 🟢 -2.5 kB
assets/CloudSignupView-DTEYTX0m.js (new) 9.78 kB 🔴 +9.78 kB 🔴 +2.85 kB 🔴 +2.5 kB
assets/UserCheckView-BAwIiBhw.js (new) 9.04 kB 🔴 +9.04 kB 🔴 +2.33 kB 🔴 +2.03 kB
assets/UserCheckView-C42KLgWV.js (removed) 9.04 kB 🟢 -9.04 kB 🟢 -2.33 kB 🟢 -2.03 kB
assets/CloudLayoutView-DgNsFpvL.js (removed) 7.54 kB 🟢 -7.54 kB 🟢 -2.36 kB 🟢 -2.06 kB
assets/CloudLayoutView-DOUjY-gZ.js (new) 7.54 kB 🔴 +7.54 kB 🔴 +2.36 kB 🔴 +2.04 kB
assets/CloudForgotPasswordView-CObJ9HnI.js (new) 5.94 kB 🔴 +5.94 kB 🔴 +2.09 kB 🔴 +1.84 kB
assets/CloudForgotPasswordView-D8x84dfQ.js (removed) 5.94 kB 🟢 -5.94 kB 🟢 -2.09 kB 🟢 -1.83 kB
assets/CloudAuthTimeoutView-Baorf2RK.js (new) 5.31 kB 🔴 +5.31 kB 🔴 +1.92 kB 🔴 +1.69 kB
assets/CloudAuthTimeoutView-DFnwyJDv.js (removed) 5.31 kB 🟢 -5.31 kB 🟢 -1.93 kB 🟢 -1.68 kB
assets/CloudSubscriptionRedirectView-DTdLEAPn.js (removed) 5.08 kB 🟢 -5.08 kB 🟢 -1.91 kB 🟢 -1.69 kB
assets/CloudSubscriptionRedirectView-DYDiXAAu.js (new) 5.08 kB 🔴 +5.08 kB 🔴 +1.9 kB 🔴 +1.68 kB
assets/UserSelectView-2hjP4ILh.js (new) 4.71 kB 🔴 +4.71 kB 🔴 +1.74 kB 🔴 +1.55 kB
assets/UserSelectView-BDBdkhno.js (removed) 4.71 kB 🟢 -4.71 kB 🟢 -1.74 kB 🟢 -1.54 kB

Status: 9 added / 9 removed / 2 unchanged

Panels & Settings — 484 kB (baseline 484 kB) • ⚪ 0 B

Configuration panels, inspectors, and settings screens

File Before After Δ Raw Δ Gzip Δ Brotli
assets/KeybindingPanel-1rCnJVyp.js (new) 46.6 kB 🔴 +46.6 kB 🔴 +9.52 kB 🔴 +8.46 kB
assets/KeybindingPanel-OOJsiP2n.js (removed) 46.6 kB 🟢 -46.6 kB 🟢 -9.52 kB 🟢 -8.47 kB
assets/SecretsPanel-C1Ms0qeT.js (new) 22.4 kB 🔴 +22.4 kB 🔴 +5.42 kB 🔴 +4.77 kB
assets/SecretsPanel-DsFrzhEL.js (removed) 22.4 kB 🟢 -22.4 kB 🟢 -5.42 kB 🟢 -4.78 kB
assets/LegacyCreditsPanel-CD2Y9_c-.js (new) 21.5 kB 🔴 +21.5 kB 🔴 +5.81 kB 🔴 +5.13 kB
assets/LegacyCreditsPanel-RU7odCVb.js (removed) 21.5 kB 🟢 -21.5 kB 🟢 -5.81 kB 🟢 -5.13 kB
assets/SubscriptionPanel-Bd3-KR2E.js (removed) 19.7 kB 🟢 -19.7 kB 🟢 -5.01 kB 🟢 -4.4 kB
assets/SubscriptionPanel-CA2bKaPe.js (new) 19.7 kB 🔴 +19.7 kB 🔴 +5.01 kB 🔴 +4.41 kB
assets/AboutPanel-BsCE8W7Q.js (new) 12 kB 🔴 +12 kB 🔴 +3.33 kB 🔴 +2.99 kB
assets/AboutPanel-CGui_hJV.js (removed) 12 kB 🟢 -12 kB 🟢 -3.33 kB 🟢 -2.98 kB
assets/ExtensionPanel-CuP2a8kL.js (new) 9.78 kB 🔴 +9.78 kB 🔴 +2.82 kB 🔴 +2.5 kB
assets/ExtensionPanel-g6TGAGkG.js (removed) 9.78 kB 🟢 -9.78 kB 🟢 -2.82 kB 🟢 -2.51 kB
assets/ServerConfigPanel-CCLA2DT-.js (new) 6.85 kB 🔴 +6.85 kB 🔴 +2.27 kB 🔴 +2.03 kB
assets/ServerConfigPanel-DY8jgiOW.js (removed) 6.85 kB 🟢 -6.85 kB 🟢 -2.27 kB 🟢 -2.03 kB
assets/UserPanel-CyQDmr1C.js (new) 6.56 kB 🔴 +6.56 kB 🔴 +2.15 kB 🔴 +1.89 kB
assets/UserPanel-ePxJEGZf.js (removed) 6.56 kB 🟢 -6.56 kB 🟢 -2.15 kB 🟢 -1.88 kB
assets/cloudRemoteConfig-BK93MxtA.js (removed) 1.85 kB 🟢 -1.85 kB 🟢 -903 B 🟢 -791 B
assets/cloudRemoteConfig-DQSyvV-S.js (new) 1.85 kB 🔴 +1.85 kB 🔴 +898 B 🔴 +791 B
assets/refreshRemoteConfig-3sXzzq4J.js (removed) 1.45 kB 🟢 -1.45 kB 🟢 -649 B 🟢 -548 B
assets/refreshRemoteConfig-cKOQDtWu.js (new) 1.45 kB 🔴 +1.45 kB 🔴 +649 B 🔴 +549 B

Status: 10 added / 10 removed / 12 unchanged

User & Accounts — 17.1 kB (baseline 17.1 kB) • ⚪ 0 B

Authentication, profile, and account management bundles

File Before After Δ Raw Δ Gzip Δ Brotli
assets/auth-Dhg1UQRU.js (removed) 3.57 kB 🟢 -3.57 kB 🟢 -1.26 kB 🟢 -1.07 kB
assets/auth-Y1xLij3u.js (new) 3.57 kB 🔴 +3.57 kB 🔴 +1.26 kB 🔴 +1.07 kB
assets/SignUpForm-Cj2NDE6B.js (removed) 3.16 kB 🟢 -3.16 kB 🟢 -1.29 kB 🟢 -1.15 kB
assets/SignUpForm-Ck1SOs4U.js (new) 3.16 kB 🔴 +3.16 kB 🔴 +1.29 kB 🔴 +1.14 kB
assets/UpdatePasswordContent-B5x7B0mt.js (removed) 2.66 kB 🟢 -2.66 kB 🟢 -1.19 kB 🟢 -1.07 kB
assets/UpdatePasswordContent-rtWn9La2.js (new) 2.66 kB 🔴 +2.66 kB 🔴 +1.19 kB 🔴 +1.05 kB
assets/authStore-B_NMw0CZ.js (removed) 989 B 🟢 -989 B 🟢 -484 B 🟢 -436 B
assets/authStore-CCZOVGoq.js (new) 989 B 🔴 +989 B 🔴 +481 B 🔴 +432 B
assets/auth-L9T3nftt.js (new) 348 B 🔴 +348 B 🔴 +216 B 🔴 +190 B
assets/auth-oyMq3yqm.js (removed) 348 B 🟢 -348 B 🟢 -218 B 🟢 -190 B

Status: 5 added / 5 removed / 2 unchanged

Editors & Dialogs — 109 kB (baseline 109 kB) • ⚪ 0 B

Modals, dialogs, drawers, and in-app editors

File Before After Δ Raw Δ Gzip Δ Brotli
assets/useShareDialog-BA_RlAHw.js (removed) 108 kB 🟢 -108 kB 🟢 -22.4 kB 🟢 -18.9 kB
assets/useShareDialog-Drky6G-g.js (new) 108 kB 🔴 +108 kB 🔴 +22.4 kB 🔴 +18.9 kB
assets/useSubscriptionDialog-B_e9qq4w.js (new) 969 B 🔴 +969 B 🔴 +472 B 🔴 +417 B
assets/useSubscriptionDialog-CMTg1gWK.js (removed) 969 B 🟢 -969 B 🟢 -475 B 🟢 -420 B

Status: 2 added / 2 removed

UI Components — 60.3 kB (baseline 60.3 kB) • ⚪ 0 B

Reusable component library chunks

File Before After Δ Raw Δ Gzip Δ Brotli
assets/ComfyQueueButton-oCYednBw.js (new) 13.5 kB 🔴 +13.5 kB 🔴 +3.79 kB 🔴 +3.37 kB
assets/ComfyQueueButton-RB_aFpSE.js (removed) 13.5 kB 🟢 -13.5 kB 🟢 -3.79 kB 🟢 -3.37 kB
assets/useTerminalTabs-BsoKJTif.js (new) 10.7 kB 🔴 +10.7 kB 🔴 +3.6 kB 🔴 +3.17 kB
assets/useTerminalTabs-CKn8frPW.js (removed) 10.7 kB 🟢 -10.7 kB 🟢 -3.6 kB 🟢 -3.16 kB
assets/SubscribeButton-DCD7mSE6.js (new) 2.42 kB 🔴 +2.42 kB 🔴 +1.04 kB 🔴 +914 B
assets/SubscribeButton-jWD2eDHw.js (removed) 2.42 kB 🟢 -2.42 kB 🟢 -1.05 kB 🟢 -914 B
assets/cloudFeedbackTopbarButton-BgA5VG9I.js (new) 1.66 kB 🔴 +1.66 kB 🔴 +841 B 🔴 +778 B
assets/cloudFeedbackTopbarButton-D247WnGJ.js (removed) 1.66 kB 🟢 -1.66 kB 🟢 -844 B 🟢 -750 B
assets/ComfyQueueButton-CG4AMD2y.js (removed) 1.03 kB 🟢 -1.03 kB 🟢 -489 B 🟢 -442 B
assets/ComfyQueueButton-JEgs8h4U.js (new) 1.03 kB 🔴 +1.03 kB 🔴 +486 B 🔴 +437 B

Status: 5 added / 5 removed / 8 unchanged

Data & Services — 2.97 MB (baseline 2.97 MB) • 🔴 +1.45 kB

Stores, services, APIs, and repositories

File Before After Δ Raw Δ Gzip Δ Brotli
assets/dialogService-CkbyVB_v.js (new) 1.94 MB 🔴 +1.94 MB 🔴 +446 kB 🔴 +338 kB
assets/dialogService-CaW4TQiA.js (removed) 1.94 MB 🟢 -1.94 MB 🟢 -445 kB 🟢 -338 kB
assets/api-BCWs3Jcr.js (new) 885 kB 🔴 +885 kB 🔴 +211 kB 🔴 +167 kB
assets/api-BzQeKjsg.js (removed) 885 kB 🟢 -885 kB 🟢 -211 kB 🟢 -167 kB
assets/load3dService-C9ZR-ISJ.js (removed) 92.5 kB 🟢 -92.5 kB 🟢 -19.7 kB 🟢 -16.9 kB
assets/load3dService-LLvGK-NE.js (new) 92.5 kB 🔴 +92.5 kB 🔴 +19.7 kB 🔴 +17 kB
assets/workflowShareService-C72JmUzp.js (new) 16.6 kB 🔴 +16.6 kB 🔴 +4.88 kB 🔴 +4.32 kB
assets/workflowShareService-CKcycI85.js (removed) 16.6 kB 🟢 -16.6 kB 🟢 -4.88 kB 🟢 -4.32 kB
assets/keybindingService-C__Yt75C.js (removed) 13.8 kB 🟢 -13.8 kB 🟢 -3.67 kB 🟢 -3.21 kB
assets/keybindingService-CRujPHQl.js (new) 13.8 kB 🔴 +13.8 kB 🔴 +3.66 kB 🔴 +3.22 kB
assets/releaseStore-B_8YuLcD.js (removed) 8.12 kB 🟢 -8.12 kB 🟢 -2.28 kB 🟢 -2 kB
assets/releaseStore-DTnvWGBq.js (new) 8.12 kB 🔴 +8.12 kB 🔴 +2.28 kB 🔴 +2 kB
assets/userStore-BrNhxOWq.js (removed) 2.24 kB 🟢 -2.24 kB 🟢 -869 B 🟢 -766 B
assets/userStore-BTHkC1D-.js (new) 2.24 kB 🔴 +2.24 kB 🔴 +869 B 🔴 +764 B
assets/audioService-BjUxxfL5.js (new) 1.8 kB 🔴 +1.8 kB 🔴 +877 B 🔴 +758 B
assets/audioService-CM5KuzAn.js (removed) 1.8 kB 🟢 -1.8 kB 🟢 -877 B 🟢 -758 B
assets/releaseStore-BLZ8d_Un.js (removed) 993 B 🟢 -993 B 🟢 -482 B 🟢 -430 B
assets/releaseStore-DEodkbqe.js (new) 993 B 🔴 +993 B 🔴 +479 B 🔴 +426 B
assets/workflowDraftStore-BNuNE4GJ.js (new) 969 B 🔴 +969 B 🔴 +472 B 🔴 +424 B
assets/workflowDraftStore-BO40oXEh.js (removed) 969 B 🟢 -969 B 🟢 -473 B 🟢 -426 B
assets/dialogService-DvQDJTAr.js (removed) 958 B 🟢 -958 B 🟢 -467 B 🟢 -417 B
assets/dialogService-SEBj9c9j.js (new) 958 B 🔴 +958 B 🔴 +464 B 🔴 +414 B
assets/settingStore-CHAty-Ih.js (removed) 956 B 🟢 -956 B 🟢 -470 B 🟢 -419 B
assets/settingStore-CHYU4wk_.js (new) 956 B 🔴 +956 B 🔴 +466 B 🔴 +417 B
assets/assetsStore-DAUqwqx-.js (new) 955 B 🔴 +955 B 🔴 +466 B 🔴 +417 B
assets/assetsStore-DOBjeWas.js (removed) 955 B 🟢 -955 B 🟢 -469 B 🟢 -421 B

Status: 13 added / 13 removed / 4 unchanged

Utilities & Hooks — 338 kB (baseline 338 kB) • ⚪ 0 B

Helpers, composables, and utility bundles

File Before After Δ Raw Δ Gzip Δ Brotli
assets/useConflictDetection-Crbi5nHC.js (new) 232 kB 🔴 +232 kB 🔴 +51.3 kB 🔴 +41.8 kB
assets/useConflictDetection-CVtIDolT.js (removed) 232 kB 🟢 -232 kB 🟢 -51.3 kB 🟢 -41.7 kB
assets/useLoad3dViewer-B_0Ikfsb.js (new) 18.7 kB 🔴 +18.7 kB 🔴 +4.44 kB 🔴 +3.91 kB
assets/useLoad3dViewer-CC1LaBue.js (removed) 18.7 kB 🟢 -18.7 kB 🟢 -4.44 kB 🟢 -3.88 kB
assets/useLoad3d-BqeVn-4U.js (new) 15 kB 🔴 +15 kB 🔴 +3.79 kB 🔴 +3.36 kB
assets/useLoad3d-K5cZszIo.js (removed) 15 kB 🟢 -15 kB 🟢 -3.79 kB 🟢 -3.35 kB
assets/useFeatureFlags-9aZ5L7az.js (new) 5.78 kB 🔴 +5.78 kB 🔴 +1.75 kB 🔴 +1.48 kB
assets/useFeatureFlags-DjYwCzEG.js (removed) 5.78 kB 🟢 -5.78 kB 🟢 -1.75 kB 🟢 -1.48 kB
assets/useCopyToClipboard-BdFO5ast.js (removed) 5.29 kB 🟢 -5.29 kB 🟢 -1.86 kB 🟢 -1.57 kB
assets/useCopyToClipboard-DBfswXxn.js (new) 5.29 kB 🔴 +5.29 kB 🔴 +1.86 kB 🔴 +1.57 kB
assets/useWorkspaceUI-Bgl5NYuC.js (removed) 3.34 kB 🟢 -3.34 kB 🟢 -979 B 🟢 -813 B
assets/useWorkspaceUI-C2rFXLoE.js (new) 3.34 kB 🔴 +3.34 kB 🔴 +980 B 🔴 +814 B
assets/subscriptionCheckoutUtil-1BCtyJfV.js (removed) 2.97 kB 🟢 -2.97 kB 🟢 -1.31 kB 🟢 -1.14 kB
assets/subscriptionCheckoutUtil-CegIEWH7.js (new) 2.97 kB 🔴 +2.97 kB 🔴 +1.31 kB 🔴 +1.14 kB
assets/assetPreviewUtil-CRM3UXaC.js (new) 2.27 kB 🔴 +2.27 kB 🔴 +959 B 🔴 +837 B
assets/assetPreviewUtil-EaQ588si.js (removed) 2.27 kB 🟢 -2.27 kB 🟢 -958 B 🟢 -832 B
assets/useUpstreamValue-CH98pguo.js (removed) 2.08 kB 🟢 -2.08 kB 🟢 -805 B 🟢 -713 B
assets/useUpstreamValue-DOD11pDM.js (new) 2.08 kB 🔴 +2.08 kB 🔴 +804 B 🔴 +718 B
assets/useLoad3d-B7q1omQe.js (removed) 1.13 kB 🟢 -1.13 kB 🟢 -540 B 🟢 -484 B
assets/useLoad3d-S6NAqw_4.js (new) 1.13 kB 🔴 +1.13 kB 🔴 +535 B 🔴 +478 B
assets/useLoad3dViewer-DT9BBmYK.js (removed) 1.07 kB 🟢 -1.07 kB 🟢 -505 B 🟢 -455 B
assets/useLoad3dViewer-KlomxeR5.js (new) 1.07 kB 🔴 +1.07 kB 🔴 +504 B 🔴 +452 B
assets/useCurrentUser-BZmQjFiX.js (removed) 955 B 🟢 -955 B 🟢 -470 B 🟢 -421 B
assets/useCurrentUser-CXwWnHpU.js (new) 955 B 🔴 +955 B 🔴 +466 B 🔴 +417 B
assets/useWorkspaceSwitch-B-sD6nes.js (new) 747 B 🔴 +747 B 🔴 +381 B 🔴 +332 B
assets/useWorkspaceSwitch-CrmBR2te.js (removed) 747 B 🟢 -747 B 🟢 -384 B 🟢 -330 B

Status: 13 added / 13 removed / 13 unchanged

Vendor & Third-Party — 9.8 MB (baseline 9.8 MB) • ⚪ 0 B

External libraries and shared vendor chunks

Status: 16 unchanged

Other — 8.44 MB (baseline 8.44 MB) • 🔴 +16 B

Bundles that do not match a named category

File Before After Δ Raw Δ Gzip Δ Brotli
assets/core-CmhufUOl.js (removed) 76.8 kB 🟢 -76.8 kB 🟢 -19.9 kB 🟢 -16.9 kB
assets/core-Yb4ehLbq.js (new) 76.8 kB 🔴 +76.8 kB 🔴 +19.9 kB 🔴 +16.9 kB
assets/groupNode-CJUzZrFX.js (removed) 74 kB 🟢 -74 kB 🟢 -18.5 kB 🟢 -16.3 kB
assets/groupNode-owiEjnQO.js (new) 74 kB 🔴 +74 kB 🔴 +18.5 kB 🔴 +16.3 kB
assets/WidgetSelect-BFiThgSl.js (new) 64.6 kB 🔴 +64.6 kB 🔴 +14.1 kB 🔴 +12.2 kB
assets/WidgetSelect-BmX1IdnY.js (removed) 64.6 kB 🟢 -64.6 kB 🟢 -14.1 kB 🟢 -12.2 kB
assets/SubscriptionRequiredDialogContentWorkspace-BI1sMvEI.js (removed) 48.9 kB 🟢 -48.9 kB 🟢 -9.29 kB 🟢 -7.95 kB
assets/SubscriptionRequiredDialogContentWorkspace-BtYxib7a.js (new) 48.9 kB 🔴 +48.9 kB 🔴 +9.29 kB 🔴 +7.94 kB
assets/WidgetPainter-4prHWy6D.js (new) 33.3 kB 🔴 +33.3 kB 🔴 +8.11 kB 🔴 +7.19 kB
assets/WidgetPainter-DpbzYR6N.js (removed) 33.3 kB 🟢 -33.3 kB 🟢 -8.12 kB 🟢 -7.19 kB
assets/Load3DControls-BJ4G1OlM.js (removed) 32.1 kB 🟢 -32.1 kB 🟢 -5.47 kB 🟢 -4.76 kB
assets/Load3DControls-BzaDgCZv.js (new) 32.1 kB 🔴 +32.1 kB 🔴 +5.47 kB 🔴 +4.75 kB
assets/WorkspacePanelContent-CvstlHMA.js (new) 29.9 kB 🔴 +29.9 kB 🔴 +6.33 kB 🔴 +5.54 kB
assets/WorkspacePanelContent-jc_Je5qX.js (removed) 29.9 kB 🟢 -29.9 kB 🟢 -6.33 kB 🟢 -5.55 kB
assets/SubscriptionRequiredDialogContent-BBoDF4TA.js (new) 28.2 kB 🔴 +28.2 kB 🔴 +7.17 kB 🔴 +6.29 kB
assets/SubscriptionRequiredDialogContent-DLaMFntU.js (removed) 28.2 kB 🟢 -28.2 kB 🟢 -7.17 kB 🟢 -6.3 kB
assets/Load3dViewerContent-cfKeohJN.js (new) 24.5 kB 🔴 +24.5 kB 🔴 +5.33 kB 🔴 +4.63 kB
assets/Load3dViewerContent-DlcxV4NQ.js (removed) 24.5 kB 🟢 -24.5 kB 🟢 -5.33 kB 🟢 -4.64 kB
assets/WidgetImageCrop-Ba_-JKNi.js (new) 23.3 kB 🔴 +23.3 kB 🔴 +5.82 kB 🔴 +5.14 kB
assets/WidgetImageCrop-CyeWwhwR.js (removed) 23.3 kB 🟢 -23.3 kB 🟢 -5.83 kB 🟢 -5.13 kB
assets/SubscriptionPanelContentWorkspace-BO-QsEEl.js (removed) 22.2 kB 🟢 -22.2 kB 🟢 -5.18 kB 🟢 -4.55 kB
assets/SubscriptionPanelContentWorkspace-BVRtINFJ.js (new) 22.2 kB 🔴 +22.2 kB 🔴 +5.18 kB 🔴 +4.56 kB
assets/SignInContent-2crlqyio.js (removed) 20.4 kB 🟢 -20.4 kB 🟢 -5.29 kB 🟢 -4.64 kB
assets/SignInContent-D8QBejQn.js (new) 20.4 kB 🔴 +20.4 kB 🔴 +5.29 kB 🔴 +4.63 kB
assets/CurrentUserPopoverWorkspace-CUrxlqTi.js (removed) 20.4 kB 🟢 -20.4 kB 🟢 -4.83 kB 🟢 -4.33 kB
assets/CurrentUserPopoverWorkspace-CyeFl_82.js (new) 20.4 kB 🔴 +20.4 kB 🔴 +4.83 kB 🔴 +4.32 kB
assets/WidgetInputNumber-BvSsd-8q.js (new) 19.1 kB 🔴 +19.1 kB 🔴 +4.84 kB 🔴 +4.29 kB
assets/WidgetInputNumber-CThSBFOy.js (removed) 19.1 kB 🟢 -19.1 kB 🟢 -4.84 kB 🟢 -4.3 kB
assets/WidgetRecordAudio-BNgrHpxO.js (new) 18.1 kB 🔴 +18.1 kB 🔴 +5.18 kB 🔴 +4.63 kB
assets/WidgetRecordAudio-DbiOw4aS.js (removed) 18.1 kB 🟢 -18.1 kB 🟢 -5.18 kB 🟢 -4.64 kB
assets/Load3D-BAqEzQD6.js (new) 16.9 kB 🔴 +16.9 kB 🔴 +4.12 kB 🔴 +3.59 kB
assets/Load3D-DaoaT4Qg.js (removed) 16.9 kB 🟢 -16.9 kB 🟢 -4.12 kB 🟢 -3.59 kB
assets/load3d-0zKfloic.js (new) 15 kB 🔴 +15 kB 🔴 +4.31 kB 🔴 +3.74 kB
assets/load3d-DG7xgSgw.js (removed) 15 kB 🟢 -15 kB 🟢 -4.32 kB 🟢 -3.73 kB
assets/WaveAudioPlayer-CCKMo0Hj.js (removed) 13.4 kB 🟢 -13.4 kB 🟢 -3.69 kB 🟢 -3.22 kB
assets/WaveAudioPlayer-DaYMx2kf.js (new) 13.4 kB 🔴 +13.4 kB 🔴 +3.69 kB 🔴 +3.22 kB
assets/WidgetCurve-BwSKwdKs.js (new) 12 kB 🔴 +12 kB 🔴 +3.85 kB 🔴 +3.48 kB
assets/WidgetCurve-CJKyPzZZ.js (removed) 12 kB 🟢 -12 kB 🟢 -3.85 kB 🟢 -3.48 kB
assets/TeamWorkspacesDialogContent-CwJ0ecBn.js (new) 11.1 kB 🔴 +11.1 kB 🔴 +3.33 kB 🔴 +2.96 kB
assets/TeamWorkspacesDialogContent-DvhvDNTi.js (removed) 11.1 kB 🟢 -11.1 kB 🟢 -3.33 kB 🟢 -2.98 kB
assets/nodeTemplates-DEkASurP.js (new) 9.58 kB 🔴 +9.58 kB 🔴 +3.37 kB 🔴 +2.97 kB
assets/nodeTemplates-DyFUneAJ.js (removed) 9.58 kB 🟢 -9.58 kB 🟢 -3.37 kB 🟢 -2.97 kB
assets/InviteMemberDialogContent-CfZJqkNW.js (removed) 7.77 kB 🟢 -7.77 kB 🟢 -2.45 kB 🟢 -2.15 kB
assets/InviteMemberDialogContent-UbzHcPjm.js (new) 7.77 kB 🔴 +7.77 kB 🔴 +2.45 kB 🔴 +2.13 kB
assets/Load3DConfiguration-Caj3ePe_.js (removed) 6.6 kB 🟢 -6.6 kB 🟢 -2.04 kB 🟢 -1.78 kB
assets/Load3DConfiguration-D2HILjib.js (new) 6.6 kB 🔴 +6.6 kB 🔴 +2.04 kB 🔴 +1.78 kB
assets/onboardingCloudRoutes-C0BKfjJg.js (removed) 6.53 kB 🟢 -6.53 kB 🟢 -2.04 kB 🟢 -1.74 kB
assets/onboardingCloudRoutes-Ch98Pv9d.js (new) 6.53 kB 🔴 +6.53 kB 🔴 +2.03 kB 🔴 +1.75 kB
assets/WidgetWithControl-DTVlhJZp.js (new) 5.99 kB 🔴 +5.99 kB 🔴 +2.38 kB 🔴 +2.14 kB
assets/WidgetWithControl-zQzZrRGA.js (removed) 5.99 kB 🟢 -5.99 kB 🟢 -2.38 kB 🟢 -2.11 kB
assets/CreateWorkspaceDialogContent-G_YP3B6g.js (new) 5.95 kB 🔴 +5.95 kB 🔴 +2.15 kB 🔴 +1.87 kB
assets/CreateWorkspaceDialogContent-OncOa_4A.js (removed) 5.95 kB 🟢 -5.95 kB 🟢 -2.15 kB 🟢 -1.88 kB
assets/FreeTierDialogContent-BjRQxuv0.js (new) 5.82 kB 🔴 +5.82 kB 🔴 +2.04 kB 🔴 +1.8 kB
assets/FreeTierDialogContent-DxCZpQ0d.js (removed) 5.82 kB 🟢 -5.82 kB 🟢 -2.05 kB 🟢 -1.81 kB
assets/EditWorkspaceDialogContent-BXdZE0-0.js (new) 5.75 kB 🔴 +5.75 kB 🔴 +2.11 kB 🔴 +1.84 kB
assets/EditWorkspaceDialogContent-DBO9zu4Z.js (removed) 5.75 kB 🟢 -5.75 kB 🟢 -2.11 kB 🟢 -1.84 kB
assets/WidgetTextarea-QltrRGkZ.js (new) 5.53 kB 🔴 +5.53 kB 🔴 +2.17 kB 🔴 +1.91 kB
assets/WidgetTextarea-wSbzdwB0.js (removed) 5.53 kB 🟢 -5.53 kB 🟢 -2.17 kB 🟢 -1.92 kB
assets/Preview3d-BEVDSjfw.js (new) 5.36 kB 🔴 +5.36 kB 🔴 +1.79 kB 🔴 +1.56 kB
assets/Preview3d-DTw058QB.js (removed) 5.36 kB 🟢 -5.36 kB 🟢 -1.79 kB 🟢 -1.56 kB
assets/ValueControlPopover-C-L2RzXq.js (removed) 5.33 kB 🟢 -5.33 kB 🟢 -1.93 kB 🟢 -1.73 kB
assets/ValueControlPopover-CSaHG69U.js (new) 5.33 kB 🔴 +5.33 kB 🔴 +1.92 kB 🔴 +1.72 kB
assets/CancelSubscriptionDialogContent-Bp1UNeeG.js (removed) 5.22 kB 🟢 -5.22 kB 🟢 -1.95 kB 🟢 -1.7 kB
assets/CancelSubscriptionDialogContent-nJ8oxOa7.js (new) 5.22 kB 🔴 +5.22 kB 🔴 +1.94 kB 🔴 +1.7 kB
assets/DeleteWorkspaceDialogContent-CDpy1d0a.js (new) 4.65 kB 🔴 +4.65 kB 🔴 +1.78 kB 🔴 +1.55 kB
assets/DeleteWorkspaceDialogContent-DCJ84ssM.js (removed) 4.65 kB 🟢 -4.65 kB 🟢 -1.79 kB 🟢 -1.55 kB
assets/LeaveWorkspaceDialogContent-CZ8hRGcx.js (removed) 4.48 kB 🟢 -4.48 kB 🟢 -1.74 kB 🟢 -1.51 kB
assets/LeaveWorkspaceDialogContent-W5IyvmmA.js (new) 4.48 kB 🔴 +4.48 kB 🔴 +1.73 kB 🔴 +1.5 kB
assets/RemoveMemberDialogContent-BmcwhCXC.js (removed) 4.46 kB 🟢 -4.46 kB 🟢 -1.69 kB 🟢 -1.47 kB
assets/RemoveMemberDialogContent-DnyFxzx4.js (new) 4.46 kB 🔴 +4.46 kB 🔴 +1.69 kB 🔴 +1.47 kB
assets/tierBenefits-BOggn_Qq.js (new) 4.45 kB 🔴 +4.45 kB 🔴 +1.58 kB 🔴 +1.36 kB
assets/tierBenefits-CYjfq_7r.js (removed) 4.45 kB 🟢 -4.45 kB 🟢 -1.58 kB 🟢 -1.36 kB
assets/RevokeInviteDialogContent-1VE36NIF.js (removed) 4.37 kB 🟢 -4.37 kB 🟢 -1.7 kB 🟢 -1.48 kB
assets/RevokeInviteDialogContent-DMfD8sQ-.js (new) 4.37 kB 🔴 +4.37 kB 🔴 +1.7 kB 🔴 +1.48 kB
assets/InviteMemberUpsellDialogContent-Bn97rBtB.js (removed) 4.27 kB 🟢 -4.27 kB 🟢 -1.56 kB 🟢 -1.37 kB
assets/InviteMemberUpsellDialogContent-C73_Qai0.js (new) 4.27 kB 🔴 +4.27 kB 🔴 +1.56 kB 🔴 +1.36 kB
assets/cloudSessionCookie-BoMEk9_I.js (new) 4.12 kB 🔴 +4.12 kB 🔴 +1.48 kB 🔴 +1.29 kB
assets/cloudSessionCookie-JUUJgAQZ.js (removed) 4.12 kB 🟢 -4.12 kB 🟢 -1.49 kB 🟢 -1.3 kB
assets/saveMesh-fK9AkNbt.js (removed) 3.92 kB 🟢 -3.92 kB 🟢 -1.68 kB 🟢 -1.48 kB
assets/saveMesh-H3cCbPue.js (new) 3.92 kB 🔴 +3.92 kB 🔴 +1.67 kB 🔴 +1.48 kB
assets/Media3DTop-BcPUfJsz.js (removed) 3.85 kB 🟢 -3.85 kB 🟢 -1.62 kB 🟢 -1.43 kB
assets/Media3DTop-D0Kmaf9v.js (new) 3.85 kB 🔴 +3.85 kB 🔴 +1.62 kB 🔴 +1.43 kB
assets/GlobalToast-BjilTU8D.js (new) 3.05 kB 🔴 +3.05 kB 🔴 +1.26 kB 🔴 +1.07 kB
assets/GlobalToast-L0qD3dTO.js (removed) 3.05 kB 🟢 -3.05 kB 🟢 -1.26 kB 🟢 -1.1 kB
assets/SubscribeToRun--ruiPnUF.js (new) 2.13 kB 🔴 +2.13 kB 🔴 +982 B 🔴 +879 B
assets/SubscribeToRun-tWskb2oE.js (removed) 2.13 kB 🟢 -2.13 kB 🟢 -983 B 🟢 -880 B
assets/MediaAudioTop-BoNVu8hI.js (new) 2.02 kB 🔴 +2.02 kB 🔴 +981 B 🔴 +834 B
assets/MediaAudioTop-DK0qHiik.js (removed) 2.02 kB 🟢 -2.02 kB 🟢 -983 B 🟢 -829 B
assets/CloudRunButtonWrapper-CbHTTPh_.js (new) 1.99 kB 🔴 +1.99 kB 🔴 +905 B 🔴 +800 B
assets/CloudRunButtonWrapper-wI670lOZ.js (removed) 1.99 kB 🟢 -1.99 kB 🟢 -908 B 🟢 -801 B
assets/graphHasMissingNodes-D90nwQPm.js (new) 1.83 kB 🔴 +1.83 kB 🔴 +862 B 🔴 +751 B
assets/graphHasMissingNodes-Dum-aLUN.js (removed) 1.83 kB 🟢 -1.83 kB 🟢 -861 B 🟢 -750 B
assets/cloudBadges-8InANwP5.js (new) 1.77 kB 🔴 +1.77 kB 🔴 +888 B 🔴 +770 B
assets/cloudBadges-lz6UBuij.js (removed) 1.77 kB 🟢 -1.77 kB 🟢 -889 B 🟢 -792 B
assets/cloudSubscription-BNfs8Qnm.js (removed) 1.68 kB 🟢 -1.68 kB 🟢 -812 B 🟢 -710 B
assets/cloudSubscription-CgSmFjgD.js (new) 1.68 kB 🔴 +1.68 kB 🔴 +811 B 🔴 +699 B
assets/previousFullPath-BEVIYNi9.js (removed) 1.53 kB 🟢 -1.53 kB 🟢 -695 B 🟢 -601 B
assets/previousFullPath-wf0J_zjm.js (new) 1.53 kB 🔴 +1.53 kB 🔴 +696 B 🔴 +601 B
assets/Load3D-B_rypPzS.js (new) 1.34 kB 🔴 +1.34 kB 🔴 +611 B 🔴 +540 B
assets/Load3D-EtnbyXQT.js (removed) 1.34 kB 🟢 -1.34 kB 🟢 -612 B 🟢 -546 B
assets/nightlyBadges-BRaiJ3Am.js (removed) 1.29 kB 🟢 -1.29 kB 🟢 -657 B 🟢 -585 B
assets/nightlyBadges-DuVIXIEA.js (new) 1.29 kB 🔴 +1.29 kB 🔴 +656 B 🔴 +578 B
assets/Load3dViewerContent-C53k28P3.js (removed) 1.23 kB 🟢 -1.23 kB 🟢 -564 B 🟢 -496 B
assets/Load3dViewerContent-D5DKjxv8.js (new) 1.23 kB 🔴 +1.23 kB 🔴 +563 B 🔴 +499 B
assets/SubscriptionPanelContentWorkspace-CZ39jzsj.js (new) 1.15 kB 🔴 +1.15 kB 🔴 +533 B 🔴 +465 B
assets/SubscriptionPanelContentWorkspace-DSw2csls.js (removed) 1.15 kB 🟢 -1.15 kB 🟢 -535 B 🟢 -467 B
assets/WidgetLegacy-CKHHvyxl.js (removed) 978 B 🟢 -978 B 🟢 -481 B 🟢 -427 B
assets/WidgetLegacy-Vb1KDYgC.js (new) 978 B 🔴 +978 B 🔴 +479 B 🔴 +424 B
assets/changeTracker-B3TsF5bD.js (removed) 952 B 🟢 -952 B 🟢 -470 B 🟢 -418 B
assets/changeTracker-DPkmQcDu.js (new) 952 B 🔴 +952 B 🔴 +466 B 🔴 +415 B

Status: 55 added / 55 removed / 79 unchanged

⚡ Performance Report

canvas-idle: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 60.3 MB heap
canvas-mouse-sweep: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 56.4 MB heap
canvas-zoom-sweep: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 64.1 MB heap
dom-widget-clipping: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 47.1 MB heap
large-graph-idle: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 52.8 MB heap
large-graph-pan: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 67.8 MB heap
large-graph-zoom: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 59.4 MB heap
minimap-idle: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 54.2 MB heap
subgraph-dom-widget-clipping: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 47.3 MB heap
subgraph-idle: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 60.1 MB heap
subgraph-mouse-sweep: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 52.2 MB heap
viewport-pan-sweep: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 65.8 MB heap
vue-large-graph-idle: · 58.1 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 153.6 MB heap
vue-large-graph-pan: · 58.1 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 142.3 MB heap
workflow-execution: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 47.7 MB heap

⚠️ 6 regressions detected

Metric Baseline PR (median) Δ Sig
canvas-mouse-sweep: task duration 983ms 1019ms +4% ⚠️ z=2.6
large-graph-idle: task duration 545ms 665ms +22% ⚠️ z=2.3
large-graph-pan: task duration 1073ms 1175ms +10% ⚠️ z=2.2
minimap-idle: task duration 536ms 657ms +23% ⚠️ z=2.7
subgraph-idle: script duration 18ms 26ms +43% ⚠️ z=2.1
subgraph-mouse-sweep: task duration 910ms 938ms +3% ⚠️ z=2.5
All metrics
Metric Baseline PR (median) Δ Sig
canvas-idle: avg frame time 17ms 17ms -0% z=-0.9
canvas-idle: p95 frame time 17ms 17ms +0%
canvas-idle: layout duration 0ms 0ms +0%
canvas-idle: style recalc duration 9ms 9ms -2% z=-1.8
canvas-idle: layout count 0 0 +0%
canvas-idle: style recalc count 11 9 -18% z=-3.8
canvas-idle: task duration 348ms 414ms +19% z=0.6
canvas-idle: script duration 22ms 24ms +8% z=-0.7
canvas-idle: TBT 0ms 0ms +0%
canvas-idle: heap used 61.2 MB 60.3 MB -2%
canvas-idle: DOM nodes 21 18 -14% z=-3.6
canvas-idle: event listeners 6 6 +0% z=-1.2
canvas-mouse-sweep: avg frame time 17ms 17ms +0% z=-0.4
canvas-mouse-sweep: p95 frame time 17ms 17ms -1%
canvas-mouse-sweep: layout duration 4ms 4ms -2% z=-0.6
canvas-mouse-sweep: style recalc duration 43ms 45ms +5% z=0.8
canvas-mouse-sweep: layout count 12 12 +0%
canvas-mouse-sweep: style recalc count 81 84 +4% z=2.3
canvas-mouse-sweep: task duration 983ms 1019ms +4% ⚠️ z=2.6
canvas-mouse-sweep: script duration 137ms 141ms +3% z=0.9
canvas-mouse-sweep: TBT 0ms 0ms +0%
canvas-mouse-sweep: heap used 56.3 MB 56.4 MB +0%
canvas-mouse-sweep: DOM nodes 65 65 +0% z=1.0
canvas-mouse-sweep: event listeners 6 6 +0% z=-0.6
canvas-zoom-sweep: avg frame time 17ms 17ms -0% z=-1.1
canvas-zoom-sweep: p95 frame time 17ms 17ms +0%
canvas-zoom-sweep: layout duration 1ms 1ms +12% z=0.6
canvas-zoom-sweep: style recalc duration 18ms 20ms +12% z=0.4
canvas-zoom-sweep: layout count 6 6 +0%
canvas-zoom-sweep: style recalc count 32 32 +0% z=1.6
canvas-zoom-sweep: task duration 310ms 366ms +18% z=1.7
canvas-zoom-sweep: script duration 27ms 30ms +12% z=1.0
canvas-zoom-sweep: TBT 0ms 0ms +0%
canvas-zoom-sweep: heap used 64.2 MB 64.1 MB -0%
canvas-zoom-sweep: DOM nodes 80 81 +1% z=2.2
canvas-zoom-sweep: event listeners 19 19 +0% z=-0.9
dom-widget-clipping: avg frame time 17ms 17ms +0% z=0.1
dom-widget-clipping: p95 frame time 17ms 17ms +1%
dom-widget-clipping: layout duration 0ms 0ms +0%
dom-widget-clipping: style recalc duration 10ms 10ms +1% z=-0.2
dom-widget-clipping: layout count 0 0 +0%
dom-widget-clipping: style recalc count 13 13 +0% z=-0.2
dom-widget-clipping: task duration 354ms 357ms +1% z=-0.5
dom-widget-clipping: script duration 64ms 69ms +7% z=0.3
dom-widget-clipping: TBT 0ms 0ms +0%
dom-widget-clipping: heap used 47.0 MB 47.1 MB +0%
dom-widget-clipping: DOM nodes 22 22 +0% z=-0.1
dom-widget-clipping: event listeners 2 2 +0% variance too high
large-graph-idle: avg frame time 17ms 17ms +0% z=-0.2
large-graph-idle: p95 frame time 17ms 17ms +0%
large-graph-idle: layout duration 0ms 0ms +0%
large-graph-idle: style recalc duration 10ms 9ms -3% z=-2.7
large-graph-idle: layout count 0 0 +0%
large-graph-idle: style recalc count 10 10 +0% z=-5.1
large-graph-idle: task duration 545ms 665ms +22% ⚠️ z=2.3
large-graph-idle: script duration 101ms 116ms +15% z=1.3
large-graph-idle: TBT 0ms 0ms +0%
large-graph-idle: heap used 52.8 MB 52.8 MB +0%
large-graph-idle: DOM nodes -255 -258 +1% z=-312.8
large-graph-idle: event listeners -123 -127 +3% z=-25.1
large-graph-pan: avg frame time 17ms 17ms +0% z=0.3
large-graph-pan: p95 frame time 17ms 17ms +1%
large-graph-pan: layout duration 0ms 0ms +0%
large-graph-pan: style recalc duration 16ms 16ms -3% z=-1.9
large-graph-pan: layout count 0 0 +0%
large-graph-pan: style recalc count 68 69 +1% z=-0.9
large-graph-pan: task duration 1073ms 1175ms +10% ⚠️ z=2.2
large-graph-pan: script duration 400ms 413ms +3% z=0.3
large-graph-pan: TBT 0ms 0ms +0%
large-graph-pan: heap used 65.1 MB 67.8 MB +4%
large-graph-pan: DOM nodes -261 -261 +0% z=-169.7
large-graph-pan: event listeners -127 -127 +0% z=-159.3
large-graph-zoom: avg frame time 17ms 17ms +0%
large-graph-zoom: p95 frame time 17ms 17ms +0%
large-graph-zoom: layout duration 7ms 8ms +5%
large-graph-zoom: style recalc duration 16ms 16ms +0%
large-graph-zoom: layout count 60 60 +0%
large-graph-zoom: style recalc count 66 66 +0%
large-graph-zoom: task duration 1385ms 1422ms +3%
large-graph-zoom: script duration 518ms 513ms -1%
large-graph-zoom: TBT 0ms 0ms +0%
large-graph-zoom: heap used 61.0 MB 59.4 MB -3%
large-graph-zoom: DOM nodes -264 -265 +0%
large-graph-zoom: event listeners -123 -123 +0%
minimap-idle: avg frame time 17ms 17ms +0% z=0.1
minimap-idle: p95 frame time 17ms 17ms -1%
minimap-idle: layout duration 0ms 0ms +0%
minimap-idle: style recalc duration 9ms 10ms +8% z=0.4
minimap-idle: layout count 0 0 +0%
minimap-idle: style recalc count 9 10 +11% z=0.6
minimap-idle: task duration 536ms 657ms +23% ⚠️ z=2.7
minimap-idle: script duration 97ms 111ms +15% z=1.3
minimap-idle: TBT 0ms 0ms +0%
minimap-idle: heap used 53.9 MB 54.2 MB +1%
minimap-idle: DOM nodes -261 -260 -0% z=-204.1
minimap-idle: event listeners -127 -127 +0% z=-199.3
subgraph-dom-widget-clipping: avg frame time 17ms 17ms +0% z=0.1
subgraph-dom-widget-clipping: p95 frame time 17ms 17ms +0%
subgraph-dom-widget-clipping: layout duration 0ms 0ms +0%
subgraph-dom-widget-clipping: style recalc duration 14ms 12ms -13% z=-0.8
subgraph-dom-widget-clipping: layout count 0 0 +0%
subgraph-dom-widget-clipping: style recalc count 49 48 -2% z=0.1
subgraph-dom-widget-clipping: task duration 370ms 371ms +0% z=-0.4
subgraph-dom-widget-clipping: script duration 132ms 121ms -8% z=-1.1
subgraph-dom-widget-clipping: TBT 0ms 0ms +0%
subgraph-dom-widget-clipping: heap used 47.3 MB 47.3 MB +0%
subgraph-dom-widget-clipping: DOM nodes 23 22 -4% z=-0.2
subgraph-dom-widget-clipping: event listeners 8 8 +0% z=-1.4
subgraph-idle: avg frame time 17ms 17ms +0% z=0.4
subgraph-idle: p95 frame time 17ms 17ms +1%
subgraph-idle: layout duration 0ms 0ms +0%
subgraph-idle: style recalc duration 10ms 11ms +5% z=0.7
subgraph-idle: layout count 0 0 +0%
subgraph-idle: style recalc count 11 11 +0% z=0.2
subgraph-idle: task duration 370ms 428ms +16% z=1.9
subgraph-idle: script duration 18ms 26ms +43% ⚠️ z=2.1
subgraph-idle: TBT 0ms 0ms +0%
subgraph-idle: heap used 60.1 MB 60.1 MB +0%
subgraph-idle: DOM nodes 22 22 +0% z=0.1
subgraph-idle: event listeners 6 6 +0% variance too high
subgraph-mouse-sweep: avg frame time 17ms 17ms +0% z=0.4
subgraph-mouse-sweep: p95 frame time 17ms 17ms +0%
subgraph-mouse-sweep: layout duration 5ms 4ms -9% z=-1.0
subgraph-mouse-sweep: style recalc duration 48ms 48ms -0% z=1.8
subgraph-mouse-sweep: layout count 16 16 +0%
subgraph-mouse-sweep: style recalc count 85 85 +0% z=2.5
subgraph-mouse-sweep: task duration 910ms 938ms +3% ⚠️ z=2.5
subgraph-mouse-sweep: script duration 109ms 109ms +1% z=1.3
subgraph-mouse-sweep: TBT 0ms 0ms +0%
subgraph-mouse-sweep: heap used 52.5 MB 52.2 MB -1%
subgraph-mouse-sweep: DOM nodes 73 72 -1% z=2.3
subgraph-mouse-sweep: event listeners 6 6 +0% variance too high
viewport-pan-sweep: avg frame time 17ms 17ms +0%
viewport-pan-sweep: p95 frame time 17ms 17ms +0%
viewport-pan-sweep: layout duration 0ms 0ms +0%
viewport-pan-sweep: style recalc duration 46ms 45ms -1%
viewport-pan-sweep: layout count 0 0 +0%
viewport-pan-sweep: style recalc count 252 252 +0%
viewport-pan-sweep: task duration 3713ms 4002ms +8%
viewport-pan-sweep: script duration 1263ms 1302ms +3%
viewport-pan-sweep: TBT 0ms 0ms +0%
viewport-pan-sweep: heap used 73.1 MB 65.8 MB -10%
viewport-pan-sweep: DOM nodes -254 -255 +0%
viewport-pan-sweep: event listeners -111 -107 -4%
vue-large-graph-idle: avg frame time 17ms 17ms +0%
vue-large-graph-idle: p95 frame time 17ms 17ms -1%
vue-large-graph-idle: layout duration 0ms 0ms +0%
vue-large-graph-idle: style recalc duration 0ms 0ms +0%
vue-large-graph-idle: layout count 0 0 +0%
vue-large-graph-idle: style recalc count 0 0 +0%
vue-large-graph-idle: task duration 12296ms 12523ms +2%
vue-large-graph-idle: script duration 626ms 609ms -3%
vue-large-graph-idle: TBT 0ms 0ms +0%
vue-large-graph-idle: heap used 158.3 MB 153.6 MB -3%
vue-large-graph-idle: DOM nodes -8331 -8331 +0%
vue-large-graph-idle: event listeners -16462 -16467 +0%
vue-large-graph-pan: avg frame time 17ms 17ms -0%
vue-large-graph-pan: p95 frame time 17ms 17ms +1%
vue-large-graph-pan: layout duration 0ms 0ms +0%
vue-large-graph-pan: style recalc duration 14ms 15ms +2%
vue-large-graph-pan: layout count 0 0 +0%
vue-large-graph-pan: style recalc count 70 68 -3%
vue-large-graph-pan: task duration 14487ms 14625ms +1%
vue-large-graph-pan: script duration 844ms 866ms +3%
vue-large-graph-pan: TBT 0ms 0ms +0%
vue-large-graph-pan: heap used 152.4 MB 142.3 MB -7%
vue-large-graph-pan: DOM nodes -8331 -8331 +0%
vue-large-graph-pan: event listeners -16460 -16486 +0%
workflow-execution: avg frame time 17ms 17ms +0% z=0.6
workflow-execution: p95 frame time 17ms 17ms -1%
workflow-execution: layout duration 1ms 1ms +0% z=-1.2
workflow-execution: style recalc duration 24ms 24ms +0% z=-0.3
workflow-execution: layout count 5 5 +0% z=0.1
workflow-execution: style recalc count 16 18 +13% z=0.1
workflow-execution: task duration 123ms 129ms +5% z=0.6
workflow-execution: script duration 27ms 30ms +10% z=0.4
workflow-execution: TBT 0ms 0ms +0%
workflow-execution: heap used 47.7 MB 47.7 MB +0%
workflow-execution: DOM nodes 156 158 +1% z=-0.4
workflow-execution: event listeners 71 71 +0% z=4.4
Historical variance (last 15 runs)
Metric μ σ CV
canvas-idle: avg frame time 17ms 0ms 0.0%
canvas-idle: layout duration 0ms 0ms 0.0%
canvas-idle: style recalc duration 11ms 1ms 8.2%
canvas-idle: layout count 0 0 0.0%
canvas-idle: style recalc count 11 1 5.0%
canvas-idle: task duration 395ms 31ms 7.9%
canvas-idle: script duration 25ms 2ms 8.8%
canvas-idle: TBT 0ms 0ms 0.0%
canvas-idle: DOM nodes 23 1 5.6%
canvas-idle: event listeners 12 5 40.9%
canvas-mouse-sweep: avg frame time 17ms 0ms 0.0%
canvas-mouse-sweep: layout duration 4ms 0ms 5.4%
canvas-mouse-sweep: style recalc duration 43ms 3ms 7.4%
canvas-mouse-sweep: layout count 12 0 0.0%
canvas-mouse-sweep: style recalc count 79 2 3.0%
canvas-mouse-sweep: task duration 865ms 58ms 6.7%
canvas-mouse-sweep: script duration 136ms 6ms 4.8%
canvas-mouse-sweep: TBT 0ms 0ms 0.0%
canvas-mouse-sweep: DOM nodes 62 3 4.2%
canvas-mouse-sweep: event listeners 8 4 49.4%
canvas-zoom-sweep: avg frame time 17ms 0ms 0.0%
canvas-zoom-sweep: layout duration 1ms 0ms 7.0%
canvas-zoom-sweep: style recalc duration 19ms 2ms 8.0%
canvas-zoom-sweep: layout count 6 0 0.0%
canvas-zoom-sweep: style recalc count 31 0 1.5%
canvas-zoom-sweep: task duration 327ms 23ms 7.1%
canvas-zoom-sweep: script duration 27ms 3ms 11.1%
canvas-zoom-sweep: TBT 0ms 0ms 0.0%
canvas-zoom-sweep: DOM nodes 79 1 1.0%
canvas-zoom-sweep: event listeners 24 5 21.8%
dom-widget-clipping: avg frame time 17ms 0ms 0.0%
dom-widget-clipping: layout duration 0ms 0ms 0.0%
dom-widget-clipping: style recalc duration 10ms 1ms 8.0%
dom-widget-clipping: layout count 0 0 0.0%
dom-widget-clipping: style recalc count 13 0 3.8%
dom-widget-clipping: task duration 365ms 16ms 4.5%
dom-widget-clipping: script duration 68ms 3ms 4.8%
dom-widget-clipping: TBT 0ms 0ms 0.0%
dom-widget-clipping: DOM nodes 22 1 6.4%
dom-widget-clipping: event listeners 8 6 81.2%
large-graph-idle: avg frame time 17ms 0ms 0.0%
large-graph-idle: layout duration 0ms 0ms 0.0%
large-graph-idle: style recalc duration 12ms 1ms 8.6%
large-graph-idle: layout count 0 0 0.0%
large-graph-idle: style recalc count 12 0 2.7%
large-graph-idle: task duration 542ms 54ms 10.0%
large-graph-idle: script duration 102ms 11ms 10.3%
large-graph-idle: TBT 0ms 0ms 0.0%
large-graph-idle: DOM nodes 25 1 3.7%
large-graph-idle: event listeners 26 6 23.2%
large-graph-pan: avg frame time 17ms 0ms 0.0%
large-graph-pan: layout duration 0ms 0ms 0.0%
large-graph-pan: style recalc duration 17ms 1ms 4.6%
large-graph-pan: layout count 0 0 0.0%
large-graph-pan: style recalc count 70 1 0.9%
large-graph-pan: task duration 1082ms 43ms 4.0%
large-graph-pan: script duration 408ms 20ms 4.8%
large-graph-pan: TBT 0ms 0ms 0.0%
large-graph-pan: DOM nodes 19 2 8.7%
large-graph-pan: event listeners 5 1 16.8%
minimap-idle: avg frame time 17ms 0ms 0.0%
minimap-idle: layout duration 0ms 0ms 0.0%
minimap-idle: style recalc duration 10ms 1ms 8.6%
minimap-idle: layout count 0 0 0.0%
minimap-idle: style recalc count 10 1 7.1%
minimap-idle: task duration 527ms 47ms 9.0%
minimap-idle: script duration 98ms 10ms 10.1%
minimap-idle: TBT 0ms 0ms 0.0%
minimap-idle: DOM nodes 19 1 7.1%
minimap-idle: event listeners 5 1 14.4%
subgraph-dom-widget-clipping: avg frame time 17ms 0ms 0.0%
subgraph-dom-widget-clipping: layout duration 0ms 0ms 0.0%
subgraph-dom-widget-clipping: style recalc duration 13ms 1ms 7.4%
subgraph-dom-widget-clipping: layout count 0 0 0.0%
subgraph-dom-widget-clipping: style recalc count 48 1 1.2%
subgraph-dom-widget-clipping: task duration 378ms 18ms 4.9%
subgraph-dom-widget-clipping: script duration 128ms 6ms 4.9%
subgraph-dom-widget-clipping: TBT 0ms 0ms 0.0%
subgraph-dom-widget-clipping: DOM nodes 22 1 5.0%
subgraph-dom-widget-clipping: event listeners 16 6 36.0%
subgraph-idle: avg frame time 17ms 0ms 0.0%
subgraph-idle: layout duration 0ms 0ms 0.0%
subgraph-idle: style recalc duration 10ms 1ms 7.5%
subgraph-idle: layout count 0 0 0.0%
subgraph-idle: style recalc count 11 1 6.0%
subgraph-idle: task duration 370ms 31ms 8.5%
subgraph-idle: script duration 20ms 3ms 13.2%
subgraph-idle: TBT 0ms 0ms 0.0%
subgraph-idle: DOM nodes 22 1 6.9%
subgraph-idle: event listeners 10 7 64.5%
subgraph-mouse-sweep: avg frame time 17ms 0ms 0.0%
subgraph-mouse-sweep: layout duration 5ms 0ms 6.8%
subgraph-mouse-sweep: style recalc duration 42ms 3ms 7.8%
subgraph-mouse-sweep: layout count 16 0 0.0%
subgraph-mouse-sweep: style recalc count 80 2 2.4%
subgraph-mouse-sweep: task duration 766ms 69ms 9.0%
subgraph-mouse-sweep: script duration 101ms 7ms 6.5%
subgraph-mouse-sweep: TBT 0ms 0ms 0.0%
subgraph-mouse-sweep: DOM nodes 67 2 3.3%
subgraph-mouse-sweep: event listeners 8 4 52.6%
workflow-execution: avg frame time 17ms 0ms 0.0%
workflow-execution: layout duration 2ms 0ms 9.4%
workflow-execution: style recalc duration 24ms 2ms 9.1%
workflow-execution: layout count 5 1 11.0%
workflow-execution: style recalc count 18 2 11.5%
workflow-execution: task duration 123ms 11ms 8.8%
workflow-execution: script duration 29ms 3ms 10.2%
workflow-execution: TBT 0ms 0ms 0.0%
workflow-execution: DOM nodes 161 7 4.4%
workflow-execution: event listeners 52 4 8.4%
Trend (last 15 commits on main)
Metric Trend Dir Latest
canvas-idle: avg frame time ▆▃▆▁▆▃▆█▆▆▄▃▃▄▃ ➡️ 17ms
canvas-idle: p95 frame time ➡️ NaNms
canvas-idle: layout duration ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
canvas-idle: style recalc duration ▇▇▆▆▃█▄▃▄▃▇▄▁▆▇ ➡️ 11ms
canvas-idle: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0
canvas-idle: style recalc count █▃▅▂▅▆▃▁▂▁▂▅▆▅▆ ➡️ 12
canvas-idle: task duration ▃▃▃▆▂▃▃▅▆▂█▃▁▃▃ ➡️ 391ms
canvas-idle: script duration ▄▃▅▇▂▅▃▆▇▅█▄▁▅▆ ➡️ 27ms
canvas-idle: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
canvas-idle: heap used ➡️ NaN MB
canvas-idle: DOM nodes █▇▆▅▃▇▃▁▂▂▅▆▆▆▇ ➡️ 24
canvas-idle: event listeners ▅█▅▄▁▅▁▁▁▄▅▅▁▅▄ 📉 11
canvas-mouse-sweep: avg frame time ▆█▆▃▁▃▁▆▆▁▃▆▆▃▃ ➡️ 17ms
canvas-mouse-sweep: p95 frame time ➡️ NaNms
canvas-mouse-sweep: layout duration ▁▃▂▄▁▂▁▃▆▂█▇▆▄▃ ➡️ 4ms
canvas-mouse-sweep: style recalc duration ▄▄▂▄▁▂▃▃▅▄█▆▂▄▄ ➡️ 43ms
canvas-mouse-sweep: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 12
canvas-mouse-sweep: style recalc count █▅▄▃▂▂▁▄▄▅▆▅▂▇▄ ➡️ 79
canvas-mouse-sweep: task duration █▆▄▂▂▃▂▄▄▅█▆▁▆▄ ➡️ 868ms
canvas-mouse-sweep: script duration ▄▅▄▆▄▆▆▆▅▅█▆▁▅▆ ➡️ 139ms
canvas-mouse-sweep: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
canvas-mouse-sweep: heap used ➡️ NaN MB
canvas-mouse-sweep: DOM nodes █▅▃▃▁▂▂▃▂▄▆▅▃▅▅ ➡️ 64
canvas-mouse-sweep: event listeners █▁▁▁▁▁▇▁▁▁██▇▁█ 📈 13
canvas-zoom-sweep: avg frame time ▅▅█▄▅▁▁▁▅▁▁▅▄▅▁ ➡️ 17ms
canvas-zoom-sweep: p95 frame time ➡️ NaNms
canvas-zoom-sweep: layout duration ▆▅▅▄▁▁█▅▃▅▇▆▁▂▆ ➡️ 1ms
canvas-zoom-sweep: style recalc duration ▆▅▄▆▅▃█▆▇▅▇▄▁▃▅ ➡️ 20ms
canvas-zoom-sweep: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 6
canvas-zoom-sweep: style recalc count ▁▁▃▄▆▃▆█▄▄▆▁▆▁▆ ➡️ 32
canvas-zoom-sweep: task duration ▄▂▁▇▂▂▄▅▆▃█▄▁▁▅ ➡️ 338ms
canvas-zoom-sweep: script duration ▃▃▂▇▂▂▅▇▆▅█▄▁▂▆ ➡️ 30ms
canvas-zoom-sweep: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
canvas-zoom-sweep: heap used ➡️ NaN MB
canvas-zoom-sweep: DOM nodes ▄▃▁▅█▁▃▆▄▅▅▃▃▄▃ ➡️ 79
canvas-zoom-sweep: event listeners ▁▁▂▅█▂▁▅▁▅▅▄▁▅▁ ➡️ 19
dom-widget-clipping: avg frame time ▂▄▅▅▂▄█▇▅▇▇▅▅▁▇ ➡️ 17ms
dom-widget-clipping: p95 frame time ➡️ NaNms
dom-widget-clipping: layout duration ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
dom-widget-clipping: style recalc duration ▆▆▂▆▄▃██▄▁▆▇▆▃▅ ➡️ 10ms
dom-widget-clipping: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0
dom-widget-clipping: style recalc count ▇█▅█▅▄█▇▇▁▇▄▇▂▅ ➡️ 13
dom-widget-clipping: task duration ▃▃▁▅▄▃▅▆▅▂▇█▁▅▅ ➡️ 371ms
dom-widget-clipping: script duration ▅▄▄▆▆▅▇▇▆▃█▇▁▇▇ ➡️ 71ms
dom-widget-clipping: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
dom-widget-clipping: heap used ➡️ NaN MB
dom-widget-clipping: DOM nodes ▇▇▄▇▅▄█▇▅▁▅▄▇▃▄ ➡️ 21
dom-widget-clipping: event listeners ▅▅▅▅▁▅██▁▁▁▁█▁▁ 📉 2
large-graph-idle: avg frame time ▅▅▅▅▅▂▁▂▄▅▄▂▂▅█ ➡️ 17ms
large-graph-idle: p95 frame time ➡️ NaNms
large-graph-idle: layout duration ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
large-graph-idle: style recalc duration ▅▅▅▆▄▅▃▄▅▅▆█▁▄▆ ➡️ 13ms
large-graph-idle: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0
large-graph-idle: style recalc count █▆█▃▃▁▃▆▃▆▆▃▆██ ➡️ 12
large-graph-idle: task duration ▂▃▂▆▂▃▃▇▅▃██▁▂▅ ➡️ 569ms
large-graph-idle: script duration ▄▅▄▆▄▅▅▇▆▅█▆▁▃▆ ➡️ 110ms
large-graph-idle: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
large-graph-idle: heap used ➡️ NaN MB
large-graph-idle: DOM nodes ▆█▅▂▅▃▁▂▃▅▅▆▂▆▅ ➡️ 25
large-graph-idle: event listeners ███▇██▄▁▄▇▇█▂█▇ ➡️ 29
large-graph-pan: avg frame time ▆▃▃▆█▃▁█▆▆▆▆█▁▆ ➡️ 17ms
large-graph-pan: p95 frame time ➡️ NaNms
large-graph-pan: layout duration ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
large-graph-pan: style recalc duration ▃▂▄▄▁▅▂▂▁▄▄█▃▁▂ ➡️ 17ms
large-graph-pan: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0
large-graph-pan: style recalc count ▆▃█▂▃▂▂▂▁▇▅▃█▆▃ ➡️ 69
large-graph-pan: task duration ▄▃▄▆▄▄▄▆▄▄█▆▁▂▅ ➡️ 1100ms
large-graph-pan: script duration ▅▄▅▆▆▅▄▆▄▅█▄▁▄▅ ➡️ 413ms
large-graph-pan: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
large-graph-pan: heap used ➡️ NaN MB
large-graph-pan: DOM nodes ▅▃▆▂▄▁▃▁▁▅▁▂█▅▂ ➡️ 18
large-graph-pan: event listeners █▆█▁▁▆▁▁▃▆▁▃██▃ ➡️ 5
minimap-idle: avg frame time ▃▆▆▃█▁█▆▆▃▃▆█▆█ ➡️ 17ms
minimap-idle: p95 frame time ➡️ NaNms
minimap-idle: layout duration ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
minimap-idle: style recalc duration ▄█▁█▅▅█▅▅▃▅▁▁▄▆ ➡️ 10ms
minimap-idle: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0
minimap-idle: style recalc count ▃▅▂▄█▃▆▁▂▅▂▁▅▆▃ ➡️ 9
minimap-idle: task duration ▃▄▁▅▁▃▄▅▇▃█▅▁▁▅ ➡️ 547ms
minimap-idle: script duration ▄▆▃▇▃▅▆▆▇▅█▅▁▃▆ ➡️ 106ms
minimap-idle: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
minimap-idle: heap used ➡️ NaN MB
minimap-idle: DOM nodes ▃▅▂▄█▃▆▁▂▅▂▁▅▆▃ ➡️ 19
minimap-idle: event listeners ▃▃▆▁▁▁▃▁▁▆▁▃█▆▁ ➡️ 4
subgraph-dom-widget-clipping: avg frame time ▅▄▄▄▄▄█▄▄▄▃▁▆▃▃ ➡️ 17ms
subgraph-dom-widget-clipping: p95 frame time ➡️ NaNms
subgraph-dom-widget-clipping: layout duration ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
subgraph-dom-widget-clipping: style recalc duration ▂▄▃▅▅▃▂▅▇▃▄█▁▄▆ ➡️ 14ms
subgraph-dom-widget-clipping: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0
subgraph-dom-widget-clipping: style recalc count ▇█▆▃▆▃▁▆█▇▃▆▇█▅ ➡️ 48
subgraph-dom-widget-clipping: task duration ▂▃▃▆▅▅▂▅█▂▆█▁▂▇ ➡️ 398ms
subgraph-dom-widget-clipping: script duration ▃▃▃▄▅▅▂▄█▂▅▇▁▂▅ ➡️ 131ms
subgraph-dom-widget-clipping: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
subgraph-dom-widget-clipping: heap used ➡️ NaN MB
subgraph-dom-widget-clipping: DOM nodes ▅▇▅▂▅▂▁▅▅▅▁▇▅█▄ ➡️ 22
subgraph-dom-widget-clipping: event listeners ▅▅▅▂▅▁▅██▁▁█▅█▅ 📈 16
subgraph-idle: avg frame time ▆▆█▁▆▃▆▆▆▃▆▁▃▆█ ➡️ 17ms
subgraph-idle: p95 frame time ➡️ NaNms
subgraph-idle: layout duration ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
subgraph-idle: style recalc duration ▁▇▃▆▂▄▂▃▃▆▆▄▃▇█ ➡️ 12ms
subgraph-idle: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0
subgraph-idle: style recalc count ▃▆▃▃▂▅▁▂▁▆▃▃██▇ ➡️ 12
subgraph-idle: task duration ▁▃▁▇▁▁▃▆▅▂█▅▁▁▄ ➡️ 378ms
subgraph-idle: script duration ▁▃▂▇▁▂▃▇▆▂█▅▂▁▅ ➡️ 22ms
subgraph-idle: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
subgraph-idle: heap used ➡️ NaN MB
subgraph-idle: DOM nodes ▃▅▃▂▁▄▁▂▁▅▃▂▇█▇ ➡️ 24
subgraph-idle: event listeners ▁▅▁▁▁▁▁▁▁▅▄▁███ 📈 21
subgraph-mouse-sweep: avg frame time ▅▄▁▃▃▄▆▄▆▃▃█▁▃▃ ➡️ 17ms
subgraph-mouse-sweep: p95 frame time ➡️ NaNms
subgraph-mouse-sweep: layout duration ▁▄▄▄▃▃▅▅▅▂█▇▂▃▆ ➡️ 5ms
subgraph-mouse-sweep: style recalc duration ▃▂▄▅▂▃▄▅█▃█▆▁▂▅ ➡️ 43ms
subgraph-mouse-sweep: layout count ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 16
subgraph-mouse-sweep: style recalc count ▅▂▅▅▁▄▃▅█▅▆▄▂▄▅ ➡️ 81
subgraph-mouse-sweep: task duration ▃▂▄▅▂▄▄▅▇▄█▆▁▃▅ ➡️ 785ms
subgraph-mouse-sweep: script duration ▄▅▄▇▅▅▆▇▆▅██▁▄▆ ➡️ 105ms
subgraph-mouse-sweep: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
subgraph-mouse-sweep: heap used ➡️ NaN MB
subgraph-mouse-sweep: DOM nodes ▅▁▄▅▁▄▃▃█▅▅▄▂▅▃ ➡️ 66
subgraph-mouse-sweep: event listeners ▇▁▂▇▁▂▂▂█▇▂▂▇▇▂ 📈 5
workflow-execution: avg frame time ▆▆▆▄▆▆▃▄▁▄█▆▅▄▆ ➡️ 17ms
workflow-execution: p95 frame time ➡️ NaNms
workflow-execution: layout duration ▁▆▁▃▂▄▃▂▃▃▅█▄▂▅ ➡️ 2ms
workflow-execution: style recalc duration ▃▇▅▇▁▅▆▇█▁██▂▄▆ ➡️ 25ms
workflow-execution: layout count ▁█▂▃▂▃▃▁▃▃▄▃▂▃▂ ➡️ 5
workflow-execution: style recalc count ▃█▅▇▁▄▅▆▅▅▅▅▄▄▂ ➡️ 15
workflow-execution: task duration ▂▅▄▅▁▄▆▆▆▁▇█▁▃▃ ➡️ 120ms
workflow-execution: script duration ▄▃▄▄▃▅▄▅▆▂▇█▁▃▄ ➡️ 29ms
workflow-execution: TBT ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ➡️ 0ms
workflow-execution: heap used ➡️ NaN MB
workflow-execution: DOM nodes ▂█▃▆▁▄▃▅▃█▃▃▄▃▁ ➡️ 152
workflow-execution: event listeners ▅███▁▅███▁██▅█▅ ➡️ 49
Raw data
{
  "timestamp": "2026-04-03T05:22:32.136Z",
  "gitSha": "2cdda4ab9385f9f546563c9d2254f70c8faa7281",
  "branch": "refactor/changetracker-lifecycle-hardening",
  "measurements": [
    {
      "name": "canvas-idle",
      "durationMs": 2013.8279999999895,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 9.187000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 375.48100000000005,
      "heapDeltaBytes": -5107496,
      "heapUsedBytes": 44269556,
      "domNodes": 16,
      "jsHeapTotalBytes": 24641536,
      "scriptDurationMs": 16.832,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-idle",
      "durationMs": 2023.1390000000147,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 9.321,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 413.73,
      "heapDeltaBytes": 21051624,
      "heapUsedBytes": 65192544,
      "domNodes": 20,
      "jsHeapTotalBytes": 22806528,
      "scriptDurationMs": 29.239999999999995,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-idle",
      "durationMs": 2021.255999999994,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 8.419999999999998,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 430.246,
      "heapDeltaBytes": 20056916,
      "heapUsedBytes": 63205888,
      "domNodes": 18,
      "jsHeapTotalBytes": 23068672,
      "scriptDurationMs": 23.810000000000002,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 2060.5860000000007,
      "styleRecalcs": 85,
      "styleRecalcDurationMs": 45.245,
      "layouts": 12,
      "layoutDurationMs": 3.3869999999999996,
      "taskDurationMs": 939.3979999999999,
      "heapDeltaBytes": 16256108,
      "heapUsedBytes": 59155640,
      "domNodes": 67,
      "jsHeapTotalBytes": 22806528,
      "scriptDurationMs": 130.898,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 1999.4689999999764,
      "styleRecalcs": 81,
      "styleRecalcDurationMs": 45.371,
      "layouts": 12,
      "layoutDurationMs": 3.5020000000000002,
      "taskDurationMs": 1048.146,
      "heapDeltaBytes": 16136608,
      "heapUsedBytes": 59163900,
      "domNodes": 65,
      "jsHeapTotalBytes": 23330816,
      "scriptDurationMs": 141.46,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 2032.8980000000456,
      "styleRecalcs": 84,
      "styleRecalcDurationMs": 43.839,
      "layouts": 12,
      "layoutDurationMs": 3.5829999999999993,
      "taskDurationMs": 1018.7539999999999,
      "heapDeltaBytes": 16224340,
      "heapUsedBytes": 59086276,
      "domNodes": 65,
      "jsHeapTotalBytes": 23068672,
      "scriptDurationMs": 145.063,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1752.049999999997,
      "styleRecalcs": 32,
      "styleRecalcDurationMs": 17.827,
      "layouts": 6,
      "layoutDurationMs": 0.6679999999999999,
      "taskDurationMs": 295.91999999999996,
      "heapDeltaBytes": 24701180,
      "heapUsedBytes": 67202584,
      "domNodes": 79,
      "jsHeapTotalBytes": 20447232,
      "scriptDurationMs": 22.989999999999995,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1748.0720000000929,
      "styleRecalcs": 32,
      "styleRecalcDurationMs": 20.412,
      "layouts": 6,
      "layoutDurationMs": 0.7020000000000002,
      "taskDurationMs": 366.09899999999993,
      "heapDeltaBytes": 24511608,
      "heapUsedBytes": 67508524,
      "domNodes": 81,
      "jsHeapTotalBytes": 20185088,
      "scriptDurationMs": 30.11,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1729.0239999999812,
      "styleRecalcs": 33,
      "styleRecalcDurationMs": 19.791999999999998,
      "layouts": 6,
      "layoutDurationMs": 0.58,
      "taskDurationMs": 371.21,
      "heapDeltaBytes": 24719364,
      "heapUsedBytes": 67254380,
      "domNodes": 81,
      "jsHeapTotalBytes": 20447232,
      "scriptDurationMs": 33.292,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 588.1939999999872,
      "styleRecalcs": 13,
      "styleRecalcDurationMs": 10.626000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 356.69700000000006,
      "heapDeltaBytes": 6477780,
      "heapUsedBytes": 49311548,
      "domNodes": 22,
      "jsHeapTotalBytes": 13107200,
      "scriptDurationMs": 69.56099999999999,
      "eventListeners": 2,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999727
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 557.4390000000449,
      "styleRecalcs": 13,
      "styleRecalcDurationMs": 9.312,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 357.024,
      "heapDeltaBytes": 6513372,
      "heapUsedBytes": 50781640,
      "domNodes": 22,
      "jsHeapTotalBytes": 13631488,
      "scriptDurationMs": 68.048,
      "eventListeners": 2,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.669999999999998,
      "p95FrameDurationMs": 16.799999999999727
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 582.2220000000016,
      "styleRecalcs": 12,
      "styleRecalcDurationMs": 9.746,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 374.035,
      "heapDeltaBytes": 6526540,
      "heapUsedBytes": 49345276,
      "domNodes": 20,
      "jsHeapTotalBytes": 13107200,
      "scriptDurationMs": 68.679,
      "eventListeners": 2,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.663333333333338,
      "p95FrameDurationMs": 16.700000000000273
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2025.2729999999701,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 9.366000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 522.596,
      "heapDeltaBytes": 3988512,
      "heapUsedBytes": 54924284,
      "domNodes": -257,
      "jsHeapTotalBytes": 15921152,
      "scriptDurationMs": 95.18499999999999,
      "eventListeners": -125,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.670000000000012,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2043.3249999999816,
      "styleRecalcs": 11,
      "styleRecalcDurationMs": 11.7,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 674.7850000000001,
      "heapDeltaBytes": 5095320,
      "heapUsedBytes": 55952708,
      "domNodes": -258,
      "jsHeapTotalBytes": 15921152,
      "scriptDurationMs": 116.72099999999999,
      "eventListeners": -127,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2025.166000000013,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 8.674999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 664.8739999999999,
      "heapDeltaBytes": 4273192,
      "heapUsedBytes": 55379768,
      "domNodes": -259,
      "jsHeapTotalBytes": 15659008,
      "scriptDurationMs": 115.98800000000001,
      "eventListeners": -127,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2146.4520000000107,
      "styleRecalcs": 69,
      "styleRecalcDurationMs": 15.801000000000002,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1074.4399999999998,
      "heapDeltaBytes": 20000908,
      "heapUsedBytes": 72233540,
      "domNodes": -260,
      "jsHeapTotalBytes": 18223104,
      "scriptDurationMs": 412.89500000000004,
      "eventListeners": -127,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2111.9300000000294,
      "styleRecalcs": 68,
      "styleRecalcDurationMs": 15.201999999999996,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1176.29,
      "heapDeltaBytes": 15317752,
      "heapUsedBytes": 68549516,
      "domNodes": -261,
      "jsHeapTotalBytes": 19271680,
      "scriptDurationMs": 424.543,
      "eventListeners": -127,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2153.015000000096,
      "styleRecalcs": 69,
      "styleRecalcDurationMs": 16.304999999999996,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1175.471,
      "heapDeltaBytes": 17472420,
      "heapUsedBytes": 71055592,
      "domNodes": -262,
      "jsHeapTotalBytes": 18223104,
      "scriptDurationMs": 413.17699999999996,
      "eventListeners": -127,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3163.558999999964,
      "styleRecalcs": 66,
      "styleRecalcDurationMs": 15.831999999999999,
      "layouts": 60,
      "layoutDurationMs": 7.158000000000001,
      "taskDurationMs": 1308.665,
      "heapDeltaBytes": 7690788,
      "heapUsedBytes": 62460168,
      "domNodes": -264,
      "jsHeapTotalBytes": 16969728,
      "scriptDurationMs": 494.59100000000007,
      "eventListeners": -125,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3166.3049999999657,
      "styleRecalcs": 66,
      "styleRecalcDurationMs": 17.956,
      "layouts": 60,
      "layoutDurationMs": 7.747000000000001,
      "taskDurationMs": 1421.98,
      "heapDeltaBytes": 7588912,
      "heapUsedBytes": 62336388,
      "domNodes": -265,
      "jsHeapTotalBytes": 17756160,
      "scriptDurationMs": 512.6469999999999,
      "eventListeners": -123,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3177.4480000000267,
      "styleRecalcs": 65,
      "styleRecalcDurationMs": 16.131000000000004,
      "layouts": 60,
      "layoutDurationMs": 7.652,
      "taskDurationMs": 1436.106,
      "heapDeltaBytes": 569336,
      "heapUsedBytes": 55801676,
      "domNodes": -266,
      "jsHeapTotalBytes": 18280448,
      "scriptDurationMs": 514.729,
      "eventListeners": -123,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "minimap-idle",
      "durationMs": 2012.0130000000245,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 9.926,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 517.9860000000001,
      "heapDeltaBytes": 3803316,
      "heapUsedBytes": 56487344,
      "domNodes": -260,
      "jsHeapTotalBytes": 16445440,
      "scriptDurationMs": 92.30499999999999,
      "eventListeners": -127,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "minimap-idle",
      "durationMs": 2029.137999999989,
      "styleRecalcs": 5,
      "styleRecalcDurationMs": 5.5329999999999995,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 657.246,
      "heapDeltaBytes": 1951152,
      "heapUsedBytes": 56848340,
      "domNodes": -267,
      "jsHeapTotalBytes": 15921152,
      "scriptDurationMs": 110.91699999999999,
      "eventListeners": -127,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "minimap-idle",
      "durationMs": 2048.674999999889,
      "styleRecalcs": 11,
      "styleRecalcDurationMs": 10.274999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 671.195,
      "heapDeltaBytes": 5148560,
      "heapUsedBytes": 57811120,
      "domNodes": -256,
      "jsHeapTotalBytes": 16445440,
      "scriptDurationMs": 119.222,
      "eventListeners": -127,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 546.970999999985,
      "styleRecalcs": 48,
      "styleRecalcDurationMs": 11.712000000000002,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 350.332,
      "heapDeltaBytes": 6642572,
      "heapUsedBytes": 49706112,
      "domNodes": 22,
      "jsHeapTotalBytes": 13893632,
      "scriptDurationMs": 120.485,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000273
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 561.2449999999853,
      "styleRecalcs": 48,
      "styleRecalcDurationMs": 12.893,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 371.023,
      "heapDeltaBytes": 6514608,
      "heapUsedBytes": 49240368,
      "domNodes": 22,
      "jsHeapTotalBytes": 13369344,
      "scriptDurationMs": 121.34899999999999,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000273
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 604.684999999904,
      "styleRecalcs": 48,
      "styleRecalcDurationMs": 11.947000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 390.72499999999997,
      "heapDeltaBytes": 6522760,
      "heapUsedBytes": 49590576,
      "domNodes": 22,
      "jsHeapTotalBytes": 13893632,
      "scriptDurationMs": 139.764,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999727
    },
    {
      "name": "subgraph-idle",
      "durationMs": 2028.347999999994,
      "styleRecalcs": 12,
      "styleRecalcDurationMs": 10.999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 340.96999999999997,
      "heapDeltaBytes": 20241288,
      "heapUsedBytes": 63342468,
      "domNodes": 24,
      "jsHeapTotalBytes": 22544384,
      "scriptDurationMs": 18.298000000000002,
      "eventListeners": 10,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-idle",
      "durationMs": 2015.2500000000373,
      "styleRecalcs": 11,
      "styleRecalcDurationMs": 10.528999999999998,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 428.44199999999995,
      "heapDeltaBytes": 19938884,
      "heapUsedBytes": 62744876,
      "domNodes": 22,
      "jsHeapTotalBytes": 23068672,
      "scriptDurationMs": 25.738000000000003,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "subgraph-idle",
      "durationMs": 1992.8519999999708,
      "styleRecalcs": 11,
      "styleRecalcDurationMs": 11.314000000000002,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 452.383,
      "heapDeltaBytes": 19902316,
      "heapUsedBytes": 63053280,
      "domNodes": 22,
      "jsHeapTotalBytes": 23068672,
      "scriptDurationMs": 25.836000000000006,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1724.2200000000025,
      "styleRecalcs": 76,
      "styleRecalcDurationMs": 38.258,
      "layouts": 16,
      "layoutDurationMs": 4.548,
      "taskDurationMs": 679.795,
      "heapDeltaBytes": 2636524,
      "heapUsedBytes": 54034960,
      "domNodes": 63,
      "jsHeapTotalBytes": 25690112,
      "scriptDurationMs": 100.965,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 2041.3220000000365,
      "styleRecalcs": 87,
      "styleRecalcDurationMs": 48.300999999999995,
      "layouts": 16,
      "layoutDurationMs": 4.311,
      "taskDurationMs": 987.74,
      "heapDeltaBytes": 11919248,
      "heapUsedBytes": 55017088,
      "domNodes": 72,
      "jsHeapTotalBytes": 22806528,
      "scriptDurationMs": 110.61000000000001,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1984.1159999999718,
      "styleRecalcs": 85,
      "styleRecalcDurationMs": 48.367,
      "layouts": 16,
      "layoutDurationMs": 4.391,
      "taskDurationMs": 938.2850000000001,
      "heapDeltaBytes": 11854740,
      "heapUsedBytes": 54765012,
      "domNodes": 74,
      "jsHeapTotalBytes": 22544384,
      "scriptDurationMs": 109.16999999999999,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 8153.112999999962,
      "styleRecalcs": 252,
      "styleRecalcDurationMs": 42.854000000000006,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 3619.65,
      "heapDeltaBytes": 18448828,
      "heapUsedBytes": 68944432,
      "domNodes": -253,
      "jsHeapTotalBytes": 20582400,
      "scriptDurationMs": 1226.4219999999998,
      "eventListeners": -105,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 8250.720999999998,
      "styleRecalcs": 251,
      "styleRecalcDurationMs": 45.406,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 4119.263,
      "heapDeltaBytes": 2491888,
      "heapUsedBytes": 53182564,
      "domNodes": -255,
      "jsHeapTotalBytes": 19591168,
      "scriptDurationMs": 1381.5819999999999,
      "eventListeners": -107,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 8222.616000000016,
      "styleRecalcs": 252,
      "styleRecalcDurationMs": 45.233000000000004,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 4001.8949999999995,
      "heapDeltaBytes": 25766656,
      "heapUsedBytes": 76333208,
      "domNodes": -256,
      "jsHeapTotalBytes": 20582400,
      "scriptDurationMs": 1301.763,
      "eventListeners": -111,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 11759.192999999981,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 11737.689999999999,
      "heapDeltaBytes": -50073940,
      "heapUsedBytes": 161087860,
      "domNodes": -8331,
      "jsHeapTotalBytes": 19984384,
      "scriptDurationMs": 599.453,
      "eventListeners": -16467,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333326,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 12711.825999999974,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 12696.391000000001,
      "heapDeltaBytes": -45841200,
      "heapUsedBytes": 157504340,
      "domNodes": -8333,
      "jsHeapTotalBytes": 17100800,
      "scriptDurationMs": 609.448,
      "eventListeners": -16468,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.77333333333336,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 12541.823000000022,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 12522.945,
      "heapDeltaBytes": -30268208,
      "heapUsedBytes": 165958992,
      "domNodes": -8331,
      "jsHeapTotalBytes": 27848704,
      "scriptDurationMs": 641.0089999999999,
      "eventListeners": -16466,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.219999999999953,
      "p95FrameDurationMs": 16.80000000000291
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 14091.229,
      "styleRecalcs": 64,
      "styleRecalcDurationMs": 12.600000000000028,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 14055.667,
      "heapDeltaBytes": -29908584,
      "heapUsedBytes": 164355660,
      "domNodes": -8331,
      "jsHeapTotalBytes": 25227264,
      "scriptDurationMs": 833.6170000000001,
      "eventListeners": -16460,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.219999999999953,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 14760.340999999926,
      "styleRecalcs": 68,
      "styleRecalcDurationMs": 14.77700000000004,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 14735.603000000001,
      "heapDeltaBytes": -49319304,
      "heapUsedBytes": 148317252,
      "domNodes": -8331,
      "jsHeapTotalBytes": -5181440,
      "scriptDurationMs": 866.361,
      "eventListeners": -16486,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.219999999999953,
      "p95FrameDurationMs": 16.80000000000291
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 14652.584000000046,
      "styleRecalcs": 68,
      "styleRecalcDurationMs": 14.783999999999963,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 14625.259999999998,
      "heapDeltaBytes": -61797728,
      "heapUsedBytes": 149216460,
      "domNodes": -8333,
      "jsHeapTotalBytes": -462848,
      "scriptDurationMs": 923.769,
      "eventListeners": -16486,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.223333333333237,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "workflow-execution",
      "durationMs": 445.3560000000607,
      "styleRecalcs": 21,
      "styleRecalcDurationMs": 30.841,
      "layouts": 6,
      "layoutDurationMs": 1.8359999999999999,
      "taskDurationMs": 146.146,
      "heapDeltaBytes": 4771780,
      "heapUsedBytes": 50035340,
      "domNodes": 170,
      "jsHeapTotalBytes": 262144,
      "scriptDurationMs": 35.001000000000005,
      "eventListeners": 71,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.669999999999998,
      "p95FrameDurationMs": 16.700000000000273
    },
    {
      "name": "workflow-execution",
      "durationMs": 453.58599999997296,
      "styleRecalcs": 18,
      "styleRecalcDurationMs": 23.671,
      "layouts": 5,
      "layoutDurationMs": 1.374,
      "taskDurationMs": 128.97099999999998,
      "heapDeltaBytes": 4461364,
      "heapUsedBytes": 54924040,
      "domNodes": 158,
      "jsHeapTotalBytes": 262144,
      "scriptDurationMs": 29.602000000000004,
      "eventListeners": 71,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000273
    },
    {
      "name": "workflow-execution",
      "durationMs": 464.32900000002064,
      "styleRecalcs": 17,
      "styleRecalcDurationMs": 22.223999999999997,
      "layouts": 5,
      "layoutDurationMs": 1.362,
      "taskDurationMs": 124.783,
      "heapDeltaBytes": 4467384,
      "heapUsedBytes": 49107960,
      "domNodes": 157,
      "jsHeapTotalBytes": 0,
      "scriptDurationMs": 30.230000000000004,
      "eventListeners": 71,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999727
    }
  ]
}

Copy link
Copy Markdown
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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/scripts/changeTracker.ts (1)

83-92: ⚠️ Potential issue | 🟠 Major

Return early before store() when deactivate() is called on an inactive tracker.

captureCanvasState() is self-defending now, but store() still reads global canvas/output/navigation state. If the active pointer has already moved, this freezes the new workflow's viewport/output state into the old tracker, and restore() will later bring back the wrong UI state.

Proposed fix
  deactivate() {
+    const isActive = useWorkflowStore().activeWorkflow?.changeTracker === this
+    if (!isActive) {
+      if (import.meta.env.DEV) {
+        logger.error('deactivate() called on inactive tracker for:', this.workflow.path)
+      }
+      return
+    }
+
     this.captureCanvasState()
     this.store()
   }

Also applies to: 103-105

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

In `@src/scripts/changeTracker.ts` around lines 83 - 92, The store() method reads
global canvas/output/navigation state even after the tracker has been
deactivated; add a defensive early return at the top of store() that checks the
tracker's active/deactivated flag (e.g., this.active or this.deactivated) and
returns immediately if the tracker is not active so it doesn't capture the new
workflow's UI state; apply the same guard to the other similar block mentioned
(lines around 103-105) so deactivate() prevents storing stale state and
restore() will not reapply incorrect UI state.
docs/architecture/change-tracker.md (1)

60-75: ⚠️ Potential issue | 🟡 Minor

Clarify that workflowService.ts is no longer a direct caller.

This section now says the listed files call captureCanvasState() explicitly, but workflowService.ts goes through deactivate() / prepareForSave(). Either drop that entry here or reword the heading to include indirect lifecycle helpers.

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

In `@docs/architecture/change-tracker.md` around lines 60 - 75, The list
incorrectly claims workflowService.ts explicitly calls captureCanvasState();
instead clarify that workflowService.ts triggers capture via lifecycle helpers
(deactivate() / prepareForSave()) or remove it from the explicit-call list. Edit
the heading or the list entry so it either reads "Existing Manual or Indirect
Call Sites" or delete the workflowService.ts bullet and add a parenthetical note
referencing deactivate() and prepareForSave() to indicate indirect invocation of
captureCanvasState().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/platform/workflow/core/services/workflowService.ts`:
- Around line 141-143: The isSelfOverwrite branch is re-entering the save
lifecycle by calling workflow.changeTracker?.prepareForSave() and await
saveWorkflow(workflow) which duplicates work and emits conflicting telemetry;
remove the await saveWorkflow(workflow) call from the isSelfOverwrite branch
(and also remove the prepareForSave() call here so prepareForSave is only
executed once inside saveWorkflow) so the outer is_new: true telemetry path
performs the actual prepare/save and emits a single save event.

In `@src/utils/__tests__/litegraphTestUtils.ts`:
- Around line 259-261: The shared mock used by createMockChangeTracker() omits
the deprecated checkState() alias; re-add checkState to the mock (e.g. as a
vi.fn() or as an alias to captureCanvasState) so tests that call
tracker.checkState() continue to work and the mock stays compatible with the
real tracker API; update the mock object that currently defines
captureCanvasState, deactivate, and prepareForSave to also include checkState
(or forward to captureCanvasState).

---

Outside diff comments:
In `@docs/architecture/change-tracker.md`:
- Around line 60-75: The list incorrectly claims workflowService.ts explicitly
calls captureCanvasState(); instead clarify that workflowService.ts triggers
capture via lifecycle helpers (deactivate() / prepareForSave()) or remove it
from the explicit-call list. Edit the heading or the list entry so it either
reads "Existing Manual or Indirect Call Sites" or delete the workflowService.ts
bullet and add a parenthetical note referencing deactivate() and
prepareForSave() to indicate indirect invocation of captureCanvasState().

In `@src/scripts/changeTracker.ts`:
- Around line 83-92: The store() method reads global canvas/output/navigation
state even after the tracker has been deactivated; add a defensive early return
at the top of store() that checks the tracker's active/deactivated flag (e.g.,
this.active or this.deactivated) and returns immediately if the tracker is not
active so it doesn't capture the new workflow's UI state; apply the same guard
to the other similar block mentioned (lines around 103-105) so deactivate()
prevents storing stale state and restore() will not reapply incorrect UI state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9c6546ba-5b87-4c48-b1b0-0dce771d20d9

📥 Commits

Reviewing files that changed from the base of the PR and between d946694 and 244e214.

📒 Files selected for processing (27)
  • browser_tests/tests/actionbar.spec.ts
  • browser_tests/tests/browserTabTitle.spec.ts
  • browser_tests/tests/changeTrackerLoadGuard.spec.ts
  • browser_tests/tests/subgraph/subgraphNested.spec.ts
  • browser_tests/tests/workflowPersistence.spec.ts
  • docs/architecture/change-tracker.md
  • src/components/builder/BuilderFooterToolbar.test.ts
  • src/components/builder/builderViewOptions.test.ts
  • src/components/builder/builderViewOptions.ts
  • src/components/graph/selectionToolbox/ColorPickerButton.test.ts
  • src/components/graph/selectionToolbox/ColorPickerButton.vue
  • src/components/searchbox/NodeSearchBoxPopover.vue
  • src/composables/graph/useCanvasRefresh.ts
  • src/composables/graph/useGroupMenuOptions.ts
  • src/composables/graph/useSelectedNodeActions.ts
  • src/composables/graph/useSelectionOperations.ts
  • src/composables/graph/useSubgraphOperations.ts
  • src/composables/useCoreCommands.test.ts
  • src/composables/useCoreCommands.ts
  • src/platform/workflow/core/services/workflowService.ts
  • src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.test.ts
  • src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue
  • src/scripts/changeTracker.test.ts
  • src/scripts/changeTracker.ts
  • src/stores/appModeStore.test.ts
  • src/stores/appModeStore.ts
  • src/utils/__tests__/litegraphTestUtils.ts

Comment on lines 141 to 143
if (isSelfOverwrite) {
if (workflowStore.isActive(workflow)) workflow.changeTracker?.checkState()
workflow.changeTracker?.prepareForSave()
await saveWorkflow(workflow)
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Apr 2, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid re-entering saveWorkflow() from the self-overwrite branch.

This branch already ends in the outer is_new: true telemetry path, but saveWorkflow() also runs prepareForSave() and emits the normal is_new: false save event. A same-path “Save As” therefore double-enters the save lifecycle and records two conflicting save events.

♻️ Proposed fix
     if (isSelfOverwrite) {
       workflow.changeTracker?.prepareForSave()
-      await saveWorkflow(workflow)
+      await workflowStore.saveWorkflow(workflow)
     } else {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/platform/workflow/core/services/workflowService.ts` around lines 141 -
143, The isSelfOverwrite branch is re-entering the save lifecycle by calling
workflow.changeTracker?.prepareForSave() and await saveWorkflow(workflow) which
duplicates work and emits conflicting telemetry; remove the await
saveWorkflow(workflow) call from the isSelfOverwrite branch (and also remove the
prepareForSave() call here so prepareForSave is only executed once inside
saveWorkflow) so the outer is_new: true telemetry path performs the actual
prepare/save and emits a single save event.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is pre-existing behavior -- the self-overwrite branch called await saveWorkflow(workflow) before this PR as well. This refactor only changed checkState() to prepareForSave() at the call site; the saveWorkflow() delegation and telemetry flow were not introduced here. prepareForSave() is idempotent (either a no-op or re-captures the same canvas state), so the double call is harmless. Fixing the double-enter / double-telemetry is a valid improvement but belongs in a separate PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@jaeone94, understood — the await saveWorkflow(workflow) delegation in the self-overwrite branch is indeed pre-existing, and this PR's only change there is the checkState()prepareForSave() substitution. Since prepareForSave() is idempotent, the double-call is harmless.

The double-enter / double-telemetry concern is still valid as a standalone improvement. Would you like me to open a follow-up issue to track it?


🧠 Learnings used
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 9345
File: src/platform/workflow/management/stores/workflowStore.ts:488-491
Timestamp: 2026-03-03T01:32:58.349Z
Learning: In `src/platform/workflow/management/stores/workflowStore.ts`, `openWorkflowPaths` maintains unique workflow paths by design. Operations like `openWorkflowsInBackground` use `_.union` to enforce uniqueness. When updating paths (e.g., during rename), replacing the first occurrence with `indexOf`/`splice` is sufficient because duplicates should not exist.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8951
File: src/platform/workflow/management/stores/workflowStore.ts:301-301
Timestamp: 2026-03-05T23:21:47.113Z
Learning: In src/platform/workflow/management/stores/workflowStore.ts, `createTemporary()` and `createNewTemporary()` intentionally inject a UUID `id` into workflow data via `ensureWorkflowId()` to give workflows stable identity for sharing. This is a known behavioral change introduced in PR `#8951`. Extensions relying on serialized content matching their input may be silently affected.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 10689
File: src/stores/executionStore.ts:637-645
Timestamp: 2026-03-29T00:40:33.555Z
Learning: In `src/stores/executionStore.ts` (Comfy-Org/ComfyUI_frontend PR `#10689`), `isJobForActiveWorkflow()` intentionally returns `true` (permissive) when `jobIdToSessionWorkflowPath` does not contain a mapping for the given `promptId`. This is deliberate graceful degradation: returning `false` for unknown jobs would break legitimate pre-session jobs and legacy WS messages that lack a `prompt_id`. The narrow race window where a WS message arrives before the HTTP response from `queuePrompt` self-resolves once `storeJob` populates the map. Do not suggest changing the unknown-job fallback to `false` or adding a pre-response association to close this race — the added complexity has no real benefit given the narrow window.

Learnt from: jaeone94
Repo: Comfy-Org/ComfyUI_frontend PR: 9921
File: src/platform/workflow/core/services/workflowService.ts:540-541
Timestamp: 2026-03-14T17:10:20.194Z
Learning: In `src/platform/workflow/core/services/workflowService.ts`, `showPendingWarnings()` intentionally only handles `missingNodeTypes` from `wf.pendingWarnings`. `missingModels` is no longer set in `pendingWarnings` because the legacy missing-models dialog has been removed. Missing models are now surfaced directly via `surfaceMissingModels()` inside `runMissingModelPipeline` in `src/scripts/app.ts` for both Cloud and OSS environments. Do not suggest re-adding `missingModels` handling to `showPendingWarnings()`.

Learnt from: xmarre
Repo: Comfy-Org/ComfyUI_frontend PR: 10367
File: src/platform/workflow/persistence/stores/workflowDraftStoreV2.ts:274-274
Timestamp: 2026-03-21T16:32:23.142Z
Learning: In `src/platform/workflow/persistence/stores/workflowDraftStoreV2.ts`, `updatedAt` in `getDraft()` intentionally returns `payload.updatedAt` (content freshness timestamp), NOT the index/MRU recency timestamp. These are two separate concepts: `payload.updatedAt` tracks when draft content last changed and is used by `ComfyWorkflow.load()` for remote-vs-draft staleness comparisons; MRU recency lives in the index order and is refreshed by `markDraftUsed()` without mutating the stored payload. Do NOT suggest collapsing these into a single timestamp — doing so would make a restored/accessed draft appear newer than the remote workflow even if the content hadn't changed.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 10547
File: browser_tests/tests/workflowPersistence.spec.ts:459-471
Timestamp: 2026-03-27T05:12:16.400Z
Learning: In `browser_tests/tests/workflowPersistence.spec.ts` (Comfy-Org/ComfyUI_frontend PR `#10547`), the G10 test for PR `#8715` (transient blob:/api-view URL serialization) intentionally validates only the static/default workflow export — not an executed/transient widget state. Putting an image widget into its executed state requires actual model inference (image output generation), which is not available in the current E2E test infrastructure. The test still covers the regression if the `widget.serialize` disable path breaks. Do not flag the absence of transient-state setup in this test; full integration coverage is tracked separately.

Learnt from: xmarre
Repo: Comfy-Org/ComfyUI_frontend PR: 10367
File: browser_tests/tests/workflowPersistence.spec.ts:68-77
Timestamp: 2026-03-21T16:57:48.590Z
Learning: In `browser_tests/tests/workflowPersistence.spec.ts`, the `waitForTabStatePersistence` helper intentionally does NOT validate `activeIndex` on the persisted `OpenPaths` session storage entry. The test immediately overwrites `openPointer.activeIndex = 1` after the wait resolves, so the previously persisted `activeIndex` value is irrelevant. The only invariant being guarded is that `openPointer.paths.length >= expectedMinPaths` (i.e., all workflow paths have been flushed into storage before the test proceeds). Do not suggest adding `activeIndex` validation to this wait predicate in future reviews.

Learnt from: Myestery
Repo: Comfy-Org/ComfyUI_frontend PR: 7422
File: .github/workflows/pr-update-playwright-expectations.yaml:131-135
Timestamp: 2025-12-12T23:02:37.473Z
Learning: In the `.github/workflows/pr-update-playwright-expectations.yaml` workflow in the Comfy-Org/ComfyUI_frontend repository, the snapshot update process is intentionally scoped to only add and update snapshot images. Deletions of snapshot files are handled explicitly outside this workflow and should not be suggested as part of this automation.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 9901
File: src/platform/workspace/composables/useCreateWorkspaceUrlLoader.ts:48-64
Timestamp: 2026-03-17T20:38:03.421Z
Learning: In `src/platform/workspace/composables/useCreateWorkspaceUrlLoader.ts`, the URL trigger (`?create_workspace=1`) and preserved query are cleaned up **before** `dialogService.showCreateWorkspaceDialog()` is awaited. This is intentional: it prevents an infinite reload loop if the lazy-loaded dialog component has a persistent failure. The console.error on failure is acceptable for now; do not flag this ordering as a bug. A follow-up issue exists to add a user-visible toast/retry without restoring the URL.

Learnt from: jaeone94
Repo: Comfy-Org/ComfyUI_frontend PR: 10309
File: src/platform/missingMedia/composables/useMissingMediaInteractions.ts:56-71
Timestamp: 2026-03-25T15:00:58.823Z
Learning: In `src/platform/missingMedia/composables/useMissingMediaInteractions.ts` (Comfy-Org/ComfyUI_frontend), the `applyValueToNodes` function intentionally omits `node.onWidgetChanged()` and `graph._version++` after setting the combo widget value. This mirrors the identical pattern in `src/platform/missingModel/composables/useMissingModelInteractions.ts` (lines 184-186). Undo/redo has been verified working in both pipelines. Do not flag the absence of `onWidgetChanged` or `_version++` as a bug in either pipeline; any change to add them must be applied consistently to both pipelines together in a dedicated follow-up PR.

Learnt from: jaeone94
Repo: Comfy-Org/ComfyUI_frontend PR: 10309
File: src/platform/missingMedia/composables/useMissingMediaInteractions.ts:153-195
Timestamp: 2026-03-25T15:31:50.781Z
Learning: In `src/platform/missingMedia/composables/useMissingMediaInteractions.ts` (Comfy-Org/ComfyUI_frontend PR `#10309`), `cancelSelection()` does not abort in-flight `api.fetchApi()` uploads. A follow-up issue (`#10511`) exists to add a per-upload `AbortController` map (keyed by `name`), pass its signal into `api.fetchApi`, guard post-await mutations with `signal.aborted` checks, and have `cancelSelection()` call `abort()` before clearing state. The same fix must be applied to the `missingModel` pipeline counterpart composable in the same PR to keep both pipelines consistent. Do not re-raise this as a blocking issue in PR `#10309`; reference issue `#10511` instead.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7415
File: browser_tests/tests/mobileBaseline.spec.ts:17-22
Timestamp: 2025-12-13T05:54:35.779Z
Learning: In browser_tests tests for the Comfy-Org/ComfyUI_frontend repository, the `comfyPage.loadWorkflow()` method already handles all necessary synchronization and waiting. No additional `await comfyPage.nextFrame()` call is needed before taking screenshots after loading a workflow.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 10140
File: apps/website/package.json:1-79
Timestamp: 2026-03-17T07:28:27.677Z
Learning: In Comfy-Org/ComfyUI_frontend, PRs managed via Graphite stacked sets (using `gt split`) may show browser_tests/ files in the diff as artifacts of the split process. These files contain no actual changes relative to origin/main and should not be flagged as unrelated scope changes. Verify against the PR title/description stacking context before raising scope concerns.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 9965
File: src/platform/cloud/onboarding/CloudSubscriptionRedirectView.vue:77-79
Timestamp: 2026-03-15T15:24:20.611Z
Learning: Comfy-Org/ComfyUI_frontend maintainers prefer PRs to stay narrowly scoped to the stated fix. Cross-file refactors (e.g., adding concurrency guards in composables like useBillingContext) should be proposed as separate follow-up issues rather than requested within a targeted bug-fix PR.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: :0-0
Timestamp: 2026-03-20T19:58:37.779Z
Learning: In Comfy-Org/ComfyUI_frontend PR `#10348`, the PR titled "fix: clean up knip config, upgrade to v6, remove unused exports" is a dependency upgrade (knip v5 → v6) + config cleanup, not a bug fix. The appropriate conventional commit type is `chore:` or `build:`, not `fix:`.

Learnt from: benceruleanlu
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: :0-0
Timestamp: 2026-03-27T05:26:01.429Z
Learning: In Comfy-Org/ComfyUI_frontend PR `#10526`, the async scope-disposal race in `src/platform/cloud/notification/components/DesktopCloudNotificationController.vue` (desktop/mac-only cloud promo flow) is intentionally covered only at the unit level via `DesktopCloudNotificationController.test.ts`, using a deferred `settingStore.load()` promise and fake timers to deterministically verify that the dialog is not scheduled or shown after the owning scope is disposed. A Playwright E2E test is not appropriate here because: (1) the non-deterministic ordering cannot be reliably reproduced in Playwright, (2) the feature requires a real macOS Electron environment not available in CI, and (3) a timing-driven pseudo-E2E would be flaky and weaker than the unit test. If broader coverage is needed later, the right approach is a separate desktop promo smoke test, not a race-specific E2E regression. Do not request a Playwright/E2E test for this specific race condition scenario.

Learnt from: CR
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-03-24T17:16:12.386Z
Learning: For bug-fix PRs, ensure end-to-end regression coverage by either: (1) changing at least one file under `browser_tests/`, or (2) providing a concrete, non-placeholder explanation in the PR description of why an end-to-end regression test was not added. Use only PR metadata available in the review context (PR title, commit subjects, files changed relative to base, PR description). Do not rely on shell commands or inspect files outside the PR.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7169
File: src/platform/remote/comfyui/jobs/jobTypes.ts:1-107
Timestamp: 2025-12-09T03:39:54.501Z
Learning: In the ComfyUI_frontend project, Zod is on v3.x. Do not suggest Zod v4 standalone validators (z.uuid, z.ulid, z.cuid2, z.nanoid) until an upgrade to Zod 4 is performed. When reviewing TypeScript files (e.g., src/platform/remote/comfyui/jobs/jobTypes.ts) validate against Zod 3 capabilities and avoid introducing v4-specific features; flag any proposal to upgrade or incorporate v4-only validators and propose staying with compatible 3.x patterns.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7358
File: src/components/dialog/content/signin/SignUpForm.vue:45-54
Timestamp: 2025-12-11T12:25:15.470Z
Learning: This repository uses CI automation to format code (pnpm format). Do not include manual formatting suggestions in code reviews for Comfy-Org/ComfyUI_frontend. If formatting issues are detected, rely on the CI formatter or re-run pnpm format. Focus reviews on correctness, readability, performance, accessibility, and maintainability rather than style formatting.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7416
File: src/stores/imagePreviewStore.ts:5-7
Timestamp: 2025-12-13T11:03:11.264Z
Learning: In the ComfyUI_frontend repository, lint rules require keeping 'import type' statements separate from non-type imports, even if importing from the same module. Do not suggest consolidating them into a single import statement. Ensure type imports remain on their own line (import type { ... } from 'module') and regular imports stay on separate lines.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7537
File: src/components/ui/button/Button.stories.ts:45-55
Timestamp: 2025-12-17T00:40:09.635Z
Learning: 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).

Learnt from: kaili-yang
Repo: Comfy-Org/ComfyUI_frontend PR: 7805
File: src/composables/useCoreCommands.ts:439-439
Timestamp: 2025-12-30T22:22:33.836Z
Learning: When accessing reactive properties from Pinia stores in TypeScript files, avoid using .value on direct property access (e.g., useStore().isOverlayExpanded). Pinia auto-wraps refs when accessed directly, returning the primitive value. The .value accessor is only needed when destructuring store properties or when using storeToRefs().

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7906
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:545-552
Timestamp: 2026-01-12T17:39:27.738Z
Learning: In Vue/TypeScript files (src/**/*.{ts,tsx,vue}), prefer if/else statements over ternary operators when performing side effects or actions (e.g., mutating state, calling methods with side effects). Ternaries should be reserved for computing and returning values.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8248
File: src/platform/assets/schemas/assetSchema.ts:109-118
Timestamp: 2026-01-22T23:44:59.888Z
Learning: Do not export declarations (e.g., schemas, constants, types) unless they are actually used elsewhere in the codebase. Keep the public API surface minimal by exporting only items with concrete usage. This reduces coupling and keeps imports explicit. In src/platform/assets/schemas/assetSchema.ts, ensure only necessary exports are exposed; remove unused exports or switch to non-exported/internal where appropriate.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8722
File: src/composables/maskeditor/useBrushDrawing.ts:5-5
Timestamp: 2026-02-08T03:19:12.009Z
Learning: Treat unresolved import warnings caused by pnpm workspace catalog mappings (pnpm-workspace.yaml) and unplugin-typegpu in vite.config.mts as false positives. Do not flag these in PR reviews; they are expected in sandboxes without node_modules and are not indicative of actual missing dependencies in the TS codebase.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8966
File: src/extensions/core/load3d.ts:427-430
Timestamp: 2026-02-19T02:06:23.468Z
Learning: In TypeScript, you can use typeof SomeClass in type annotation positions (e.g., param: typeof LGraphNode) even when SomeClass is imported with import type. This is a type query that only exists at compile time and is erased in runtime, so it is safe to combine with type-only imports. Apply this pattern to TS files broadly when you need a type that references the shape of a class or constructor function without importing the value at runtime.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8966
File: src/extensions/core/uploadAudio.ts:91-94
Timestamp: 2026-02-19T02:06:38.395Z
Learning: In TypeScript files, you can use a type annotation like 'nodeType: typeof MyClass' even if MyClass is imported via 'import type'. Both the type-only import and 'typeof' operate at the type level and are erased at compile time. This pattern is commonly used for constructor types (e.g., 'nodeType: typeof LGraphNode'). Apply this pattern across TypeScript files in the repository (src/**/*.ts) as appropriate, ensuring the imported symbol is a type-only import when possible for clarity and to avoid runtime imports.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8992
File: src/lib/litegraph/src/widgets/GradientSliderWidget.ts:18-18
Timestamp: 2026-02-20T21:08:19.814Z
Learning: When drawing with CanvasRenderingContext2D in TypeScript/JavaScript, wrap the drawing logic with ctx.save() at the start and ctx.restore() at the end to preserve and restore the canvas state. Do not manually destructure and restore individual properties (e.g., fillStyle, strokeStyle); rely on save/restore to manage state changes in a scoped manner. This should be applied to all TS files that perform canvas drawing.

Learnt from: dante01yoon
Repo: Comfy-Org/ComfyUI_frontend PR: 9075
File: src/scripts/api.featureFlags.test.ts:237-268
Timestamp: 2026-02-22T04:27:33.379Z
Learning: In Vite/Vitest, import.meta.env.DEV is true for any mode that is not 'production' (i.e., DEV is the opposite of PROD, and can be true in 'test', 'development', etc.). Do not assume DEV implies only 'development' mode. When reviewing code and tests, treat DEV as a non-production flag and verify environment-specific logic accordingly. Reference: https://vite.dev/guide/env-and-mode#modes

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 9427
File: src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuFilter.vue:33-33
Timestamp: 2026-03-06T00:53:28.835Z
Learning: When reviewing code, note that the enforce-canonical-classes (better-tailwindcss) rule may auto-fix Tailwind v3 !class-name syntax by converting it to v4 class-name! syntax. Do not treat these auto-fixed class-name! instances as newly introduced issues; the perceived change is in syntax placement, not in usage or intent. This guidance applies across all .vue and .ts files in the repository.

Learnt from: sonnybox
Repo: Comfy-Org/ComfyUI_frontend PR: 9446
File: src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue:45-45
Timestamp: 2026-03-06T01:55:00.013Z
Learning: Treat wrap-break-word as a valid Tailwind CSS utility for overflow-wrap: break-word in Tailwind v4+ projects. Do not flag this class as invalid in any Vue (.vue) or TypeScript (.ts/.tsx) files within the repository (e.g., Comfy-Org/ComfyUI_frontend) or other Tailwind v4+ projects. When reviewing, verify that the class is used to enable word breaking in long text content and reference the Tailwind docs: https://tailwindcss.com/docs/overflow-wrap.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

yes and assign me please

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
gh issue create \
  --repo Comfy-Org/ComfyUI_frontend \
  --title "refactor: fix double save lifecycle and conflicting telemetry in saveWorkflowAs self-overwrite branch" \
  --body "## Summary

In \`src/platform/workflow/core/services/workflowService.ts\`, the \`saveWorkflowAs\` self-overwrite branch (\`isSelfOverwrite === true\`) currently calls:

\`\`\`ts
workflow.changeTracker?.prepareForSave()
await saveWorkflow(workflow)
\`\`\`

\`saveWorkflow()\` itself calls \`prepareForSave()\` again and emits the normal \`is_new: false\` telemetry event, while the outer \`saveWorkflowAs\` function unconditionally emits \`is_new: true\` after the branch resolves. This results in:

1. **Double \`prepareForSave()\` invocation** — harmless today because \`prepareForSave()\` is idempotent, but wasteful and misleading.
2. **Conflicting save telemetry** — both \`is_new: false\` (from \`saveWorkflow\`) and \`is_new: true\` (from the outer \`saveWorkflowAs\` path) are emitted for a single user action.

## Proposed fix

Replace the \`saveWorkflow(workflow)\` delegation with a direct \`workflowStore.saveWorkflow(workflow)\` call (bypassing the wrapper's extra \`prepareForSave\` + telemetry), so the lifecycle runs exactly once and the outer \`is_new: true\` event is the single authoritative record.

\`\`\`diff
     if (isSelfOverwrite) {
       workflow.changeTracker?.prepareForSave()
-      await saveWorkflow(workflow)
+      await workflowStore.saveWorkflow(workflow)
     } else {
\`\`\`

## Context

- Identified during review of PR `#10816` (comment: https://github.com/Comfy-Org/ComfyUI_frontend/pull/10816#discussion_r3025739180)
- The \`saveWorkflow()\` delegation is pre-existing behaviour; this PR only changed \`checkState()\` → \`prepareForSave()\` at the call site.
- Fix is intentionally deferred to a separate PR to keep `#10816` narrowly scoped.

Reported by: `@jaeone94`" \
  --assignee jaeone94

Length of output: 58


@jaeone94, done! Follow-up issue opened and assigned to you: #10819

It tracks the double prepareForSave() invocation and the conflicting is_new: false / is_new: true telemetry in the saveWorkflowAs self-overwrite branch, with the proposed fix included.


🧠 Learnings used
Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 9345
File: src/platform/workflow/management/stores/workflowStore.ts:488-491
Timestamp: 2026-03-03T01:32:58.349Z
Learning: In `src/platform/workflow/management/stores/workflowStore.ts`, `openWorkflowPaths` maintains unique workflow paths by design. Operations like `openWorkflowsInBackground` use `_.union` to enforce uniqueness. When updating paths (e.g., during rename), replacing the first occurrence with `indexOf`/`splice` is sufficient because duplicates should not exist.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8951
File: src/platform/workflow/management/stores/workflowStore.ts:301-301
Timestamp: 2026-03-05T23:21:47.113Z
Learning: In src/platform/workflow/management/stores/workflowStore.ts, `createTemporary()` and `createNewTemporary()` intentionally inject a UUID `id` into workflow data via `ensureWorkflowId()` to give workflows stable identity for sharing. This is a known behavioral change introduced in PR `#8951`. Extensions relying on serialized content matching their input may be silently affected.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 10689
File: src/stores/executionStore.ts:637-645
Timestamp: 2026-03-29T00:40:33.555Z
Learning: In `src/stores/executionStore.ts` (Comfy-Org/ComfyUI_frontend PR `#10689`), `isJobForActiveWorkflow()` intentionally returns `true` (permissive) when `jobIdToSessionWorkflowPath` does not contain a mapping for the given `promptId`. This is deliberate graceful degradation: returning `false` for unknown jobs would break legitimate pre-session jobs and legacy WS messages that lack a `prompt_id`. The narrow race window where a WS message arrives before the HTTP response from `queuePrompt` self-resolves once `storeJob` populates the map. Do not suggest changing the unknown-job fallback to `false` or adding a pre-response association to close this race — the added complexity has no real benefit given the narrow window.

Learnt from: jaeone94
Repo: Comfy-Org/ComfyUI_frontend PR: 9921
File: src/platform/workflow/core/services/workflowService.ts:540-541
Timestamp: 2026-03-14T17:10:20.194Z
Learning: In `src/platform/workflow/core/services/workflowService.ts`, `showPendingWarnings()` intentionally only handles `missingNodeTypes` from `wf.pendingWarnings`. `missingModels` is no longer set in `pendingWarnings` because the legacy missing-models dialog has been removed. Missing models are now surfaced directly via `surfaceMissingModels()` inside `runMissingModelPipeline` in `src/scripts/app.ts` for both Cloud and OSS environments. Do not suggest re-adding `missingModels` handling to `showPendingWarnings()`.

Learnt from: xmarre
Repo: Comfy-Org/ComfyUI_frontend PR: 10367
File: src/platform/workflow/persistence/stores/workflowDraftStoreV2.ts:274-274
Timestamp: 2026-03-21T16:32:23.142Z
Learning: In `src/platform/workflow/persistence/stores/workflowDraftStoreV2.ts`, `updatedAt` in `getDraft()` intentionally returns `payload.updatedAt` (content freshness timestamp), NOT the index/MRU recency timestamp. These are two separate concepts: `payload.updatedAt` tracks when draft content last changed and is used by `ComfyWorkflow.load()` for remote-vs-draft staleness comparisons; MRU recency lives in the index order and is refreshed by `markDraftUsed()` without mutating the stored payload. Do NOT suggest collapsing these into a single timestamp — doing so would make a restored/accessed draft appear newer than the remote workflow even if the content hadn't changed.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 9427
File: src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuFilter.vue:33-33
Timestamp: 2026-03-06T00:53:28.835Z
Learning: When reviewing code, note that the enforce-canonical-classes (better-tailwindcss) rule may auto-fix Tailwind v3 !class-name syntax by converting it to v4 class-name! syntax. Do not treat these auto-fixed class-name! instances as newly introduced issues; the perceived change is in syntax placement, not in usage or intent. This guidance applies across all .vue and .ts files in the repository.

Learnt from: sonnybox
Repo: Comfy-Org/ComfyUI_frontend PR: 9446
File: src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue:45-45
Timestamp: 2026-03-06T01:55:00.013Z
Learning: Treat wrap-break-word as a valid Tailwind CSS utility for overflow-wrap: break-word in Tailwind v4+ projects. Do not flag this class as invalid in any Vue (.vue) or TypeScript (.ts/.tsx) files within the repository (e.g., Comfy-Org/ComfyUI_frontend) or other Tailwind v4+ projects. When reviewing, verify that the class is used to enable word breaking in long text content and reference the Tailwind docs: https://tailwindcss.com/docs/overflow-wrap.

Learnt from: benceruleanlu
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: :0-0
Timestamp: 2026-03-27T05:26:01.429Z
Learning: In Comfy-Org/ComfyUI_frontend PR `#10526`, the async scope-disposal race in `src/platform/cloud/notification/components/DesktopCloudNotificationController.vue` (desktop/mac-only cloud promo flow) is intentionally covered only at the unit level via `DesktopCloudNotificationController.test.ts`, using a deferred `settingStore.load()` promise and fake timers to deterministically verify that the dialog is not scheduled or shown after the owning scope is disposed. A Playwright E2E test is not appropriate here because: (1) the non-deterministic ordering cannot be reliably reproduced in Playwright, (2) the feature requires a real macOS Electron environment not available in CI, and (3) a timing-driven pseudo-E2E would be flaky and weaker than the unit test. If broader coverage is needed later, the right approach is a separate desktop promo smoke test, not a race-specific E2E regression. Do not request a Playwright/E2E test for this specific race condition scenario.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 10548
File: browser_tests/tests/propertiesPanel/propertiesPanel.spec.ts:147-153
Timestamp: 2026-03-27T05:12:33.040Z
Learning: In Comfy-Org/ComfyUI_frontend browser_tests, `PropertiesPanelHelper.ts` intentionally uses `i[class*="lucide--pencil"]` as the selector for `titleEditIcon` as a temporary measure. Adding a `data-testid` to the pencil icon requires a source change to the PropertiesPanel Vue component and is deferred to a separate PR tracked as FIX-03 (raw CSS selector → TestId migration) in the test coverage overhaul. Do not flag this CSS class selector as a violation in future reviews of this file.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 10140
File: apps/website/package.json:1-79
Timestamp: 2026-03-17T07:28:27.677Z
Learning: In Comfy-Org/ComfyUI_frontend, PRs managed via Graphite stacked sets (using `gt split`) may show browser_tests/ files in the diff as artifacts of the split process. These files contain no actual changes relative to origin/main and should not be flagged as unrelated scope changes. Verify against the PR title/description stacking context before raising scope concerns.

Learnt from: jaeone94
Repo: Comfy-Org/ComfyUI_frontend PR: 10309
File: src/platform/missingMedia/composables/useMissingMediaInteractions.ts:56-71
Timestamp: 2026-03-25T15:00:58.823Z
Learning: In `src/platform/missingMedia/composables/useMissingMediaInteractions.ts` (Comfy-Org/ComfyUI_frontend), the `applyValueToNodes` function intentionally omits `node.onWidgetChanged()` and `graph._version++` after setting the combo widget value. This mirrors the identical pattern in `src/platform/missingModel/composables/useMissingModelInteractions.ts` (lines 184-186). Undo/redo has been verified working in both pipelines. Do not flag the absence of `onWidgetChanged` or `_version++` as a bug in either pipeline; any change to add them must be applied consistently to both pipelines together in a dedicated follow-up PR.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 9965
File: src/platform/cloud/onboarding/CloudSubscriptionRedirectView.vue:77-79
Timestamp: 2026-03-15T15:24:20.611Z
Learning: Comfy-Org/ComfyUI_frontend maintainers prefer PRs to stay narrowly scoped to the stated fix. Cross-file refactors (e.g., adding concurrency guards in composables like useBillingContext) should be proposed as separate follow-up issues rather than requested within a targeted bug-fix PR.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7598
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:131-131
Timestamp: 2025-12-18T02:07:44.374Z
Learning: Comfy-Org/ComfyUI_frontend uses Tailwind CSS v4 utilities, including the new “safe” overflow-alignment classes. Do not flag items-center-safe, justify-*-safe, or place-*-safe utilities as invalid in src/**/*.vue or stories.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 10140
File: apps/website/package.json:1-79
Timestamp: 2026-03-17T07:28:27.677Z
Learning: In Comfy-Org/ComfyUI_frontend, the narrow-scope learning (PRs should stay focused on the stated fix) applies to bug-fix PRs. It does not apply to intentional feature scaffold PRs that are part of a stacked PR series (e.g., [1/3], [2/3], [3/3] in the PR title managed via Graphite).

Learnt from: pythongosssss
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: :0-0
Timestamp: 2026-03-18T15:38:16.169Z
Learning: In Comfy-Org/ComfyUI_frontend, race conditions between WebSocket events and HTTP responses (e.g., the WebSocket/HTTP race in job tracking in `src/renderer/extensions/linearMode/linearOutputStore.ts`) are not suitable candidates for E2E testing because the non-deterministic ordering cannot be reliably reproduced in Playwright. Unit tests (e.g., in `linearOutputStore.test.ts`) that deterministically simulate event ordering are the correct approach. Do not request E2E tests for such race condition scenarios.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 10547
File: browser_tests/tests/workflowPersistence.spec.ts:459-471
Timestamp: 2026-03-27T05:12:16.400Z
Learning: In `browser_tests/tests/workflowPersistence.spec.ts` (Comfy-Org/ComfyUI_frontend PR `#10547`), the G10 test for PR `#8715` (transient blob:/api-view URL serialization) intentionally validates only the static/default workflow export — not an executed/transient widget state. Putting an image widget into its executed state requires actual model inference (image output generation), which is not available in the current E2E test infrastructure. The test still covers the regression if the `widget.serialize` disable path breaks. Do not flag the absence of transient-state setup in this test; full integration coverage is tracked separately.

Learnt from: jaeone94
Repo: Comfy-Org/ComfyUI_frontend PR: 10309
File: src/platform/missingMedia/composables/useMissingMediaInteractions.ts:153-195
Timestamp: 2026-03-25T15:31:50.781Z
Learning: In `src/platform/missingMedia/composables/useMissingMediaInteractions.ts` (Comfy-Org/ComfyUI_frontend PR `#10309`), `cancelSelection()` does not abort in-flight `api.fetchApi()` uploads. A follow-up issue (`#10511`) exists to add a per-upload `AbortController` map (keyed by `name`), pass its signal into `api.fetchApi`, guard post-await mutations with `signal.aborted` checks, and have `cancelSelection()` call `abort()` before clearing state. The same fix must be applied to the `missingModel` pipeline counterpart composable in the same PR to keep both pipelines consistent. Do not re-raise this as a blocking issue in PR `#10309`; reference issue `#10511` instead.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: :0-0
Timestamp: 2026-03-20T19:58:37.779Z
Learning: In Comfy-Org/ComfyUI_frontend PR `#10348`, the PR titled "fix: clean up knip config, upgrade to v6, remove unused exports" is a dependency upgrade (knip v5 → v6) + config cleanup, not a bug fix. The appropriate conventional commit type is `chore:` or `build:`, not `fix:`.

Learnt from: Myestery
Repo: Comfy-Org/ComfyUI_frontend PR: 7422
File: .github/workflows/pr-update-playwright-expectations.yaml:131-135
Timestamp: 2025-12-12T23:02:37.473Z
Learning: In the `.github/workflows/pr-update-playwright-expectations.yaml` workflow in the Comfy-Org/ComfyUI_frontend repository, the snapshot update process is intentionally scoped to only add and update snapshot images. Deletions of snapshot files are handled explicitly outside this workflow and should not be suggested as part of this automation.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 9680
File: src/renderer/core/layout/store/layoutStore.ts:920-922
Timestamp: 2026-03-10T00:27:24.529Z
Learning: In `src/renderer/core/layout/store/layoutStore.ts` (`LayoutStoreImpl`), the `LayoutChange` object is constructed inside `applyOperation` with an immutable `source` captured from `operation.source` and `actor` from `operation.actor`. `notifyChange(change)` passes only this frozen `LayoutChange` to listeners — no listener reads the shared mutable `this.currentSource` or `this.currentActor`. Therefore, making `notifyChange` synchronous (removing the `setTimeout`) does NOT expose the temporarily-mutated `this.currentSource` (set in `batchUpdateNodeBounds`) to listeners. Do not flag the synchronous `notifyChange` call in `finalizeOperation` as a context-pollution risk; the immutable-context pattern is already in place.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7169
File: src/platform/remote/comfyui/jobs/jobTypes.ts:1-107
Timestamp: 2025-12-09T03:39:54.501Z
Learning: In the ComfyUI_frontend project, Zod is on v3.x. Do not suggest Zod v4 standalone validators (z.uuid, z.ulid, z.cuid2, z.nanoid) until an upgrade to Zod 4 is performed. When reviewing TypeScript files (e.g., src/platform/remote/comfyui/jobs/jobTypes.ts) validate against Zod 3 capabilities and avoid introducing v4-specific features; flag any proposal to upgrade or incorporate v4-only validators and propose staying with compatible 3.x patterns.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7358
File: src/components/dialog/content/signin/SignUpForm.vue:45-54
Timestamp: 2025-12-11T12:25:15.470Z
Learning: This repository uses CI automation to format code (pnpm format). Do not include manual formatting suggestions in code reviews for Comfy-Org/ComfyUI_frontend. If formatting issues are detected, rely on the CI formatter or re-run pnpm format. Focus reviews on correctness, readability, performance, accessibility, and maintainability rather than style formatting.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 7416
File: src/stores/imagePreviewStore.ts:5-7
Timestamp: 2025-12-13T11:03:11.264Z
Learning: In the ComfyUI_frontend repository, lint rules require keeping 'import type' statements separate from non-type imports, even if importing from the same module. Do not suggest consolidating them into a single import statement. Ensure type imports remain on their own line (import type { ... } from 'module') and regular imports stay on separate lines.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7537
File: src/components/ui/button/Button.stories.ts:45-55
Timestamp: 2025-12-17T00:40:09.635Z
Learning: 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).

Learnt from: kaili-yang
Repo: Comfy-Org/ComfyUI_frontend PR: 7805
File: src/composables/useCoreCommands.ts:439-439
Timestamp: 2025-12-30T22:22:33.836Z
Learning: When accessing reactive properties from Pinia stores in TypeScript files, avoid using .value on direct property access (e.g., useStore().isOverlayExpanded). Pinia auto-wraps refs when accessed directly, returning the primitive value. The .value accessor is only needed when destructuring store properties or when using storeToRefs().

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 7906
File: src/components/sidebar/tabs/AssetsSidebarTab.vue:545-552
Timestamp: 2026-01-12T17:39:27.738Z
Learning: In Vue/TypeScript files (src/**/*.{ts,tsx,vue}), prefer if/else statements over ternary operators when performing side effects or actions (e.g., mutating state, calling methods with side effects). Ternaries should be reserved for computing and returning values.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8248
File: src/platform/assets/schemas/assetSchema.ts:109-118
Timestamp: 2026-01-22T23:44:59.888Z
Learning: Do not export declarations (e.g., schemas, constants, types) unless they are actually used elsewhere in the codebase. Keep the public API surface minimal by exporting only items with concrete usage. This reduces coupling and keeps imports explicit. In src/platform/assets/schemas/assetSchema.ts, ensure only necessary exports are exposed; remove unused exports or switch to non-exported/internal where appropriate.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8722
File: src/composables/maskeditor/useBrushDrawing.ts:5-5
Timestamp: 2026-02-08T03:19:12.009Z
Learning: Treat unresolved import warnings caused by pnpm workspace catalog mappings (pnpm-workspace.yaml) and unplugin-typegpu in vite.config.mts as false positives. Do not flag these in PR reviews; they are expected in sandboxes without node_modules and are not indicative of actual missing dependencies in the TS codebase.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8966
File: src/extensions/core/load3d.ts:427-430
Timestamp: 2026-02-19T02:06:23.468Z
Learning: In TypeScript, you can use typeof SomeClass in type annotation positions (e.g., param: typeof LGraphNode) even when SomeClass is imported with import type. This is a type query that only exists at compile time and is erased in runtime, so it is safe to combine with type-only imports. Apply this pattern to TS files broadly when you need a type that references the shape of a class or constructor function without importing the value at runtime.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8966
File: src/extensions/core/uploadAudio.ts:91-94
Timestamp: 2026-02-19T02:06:38.395Z
Learning: In TypeScript files, you can use a type annotation like 'nodeType: typeof MyClass' even if MyClass is imported via 'import type'. Both the type-only import and 'typeof' operate at the type level and are erased at compile time. This pattern is commonly used for constructor types (e.g., 'nodeType: typeof LGraphNode'). Apply this pattern across TypeScript files in the repository (src/**/*.ts) as appropriate, ensuring the imported symbol is a type-only import when possible for clarity and to avoid runtime imports.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 8992
File: src/lib/litegraph/src/widgets/GradientSliderWidget.ts:18-18
Timestamp: 2026-02-20T21:08:19.814Z
Learning: When drawing with CanvasRenderingContext2D in TypeScript/JavaScript, wrap the drawing logic with ctx.save() at the start and ctx.restore() at the end to preserve and restore the canvas state. Do not manually destructure and restore individual properties (e.g., fillStyle, strokeStyle); rely on save/restore to manage state changes in a scoped manner. This should be applied to all TS files that perform canvas drawing.

Learnt from: dante01yoon
Repo: Comfy-Org/ComfyUI_frontend PR: 9075
File: src/scripts/api.featureFlags.test.ts:237-268
Timestamp: 2026-02-22T04:27:33.379Z
Learning: In Vite/Vitest, import.meta.env.DEV is true for any mode that is not 'production' (i.e., DEV is the opposite of PROD, and can be true in 'test', 'development', etc.). Do not assume DEV implies only 'development' mode. When reviewing code and tests, treat DEV as a non-production flag and verify environment-specific logic accordingly. Reference: https://vite.dev/guide/env-and-mode#modes

- Guard deactivate() against inactive tracker to prevent store() from
  freezing wrong viewport/output data into the old tracker
- Update docs: workflowService calls captureCanvasState indirectly via
  deactivate/prepareForSave, not directly
- Add deprecated checkState() to mock utility for extension compat
- Update deactivate test: inactive path is now a full no-op
Copy link
Copy Markdown
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 `@src/scripts/changeTracker.ts`:
- Around line 103-126: deactivate() can leave inactive workflows with stale
activeState because captureCanvasState() early-returns when
ChangeTracker.isLoadingGraph is true; extract the serialize + push-undo + store
logic currently inside captureCanvasState()/store() into a new private helper
(e.g., finalizeSnapshot() or serializeAndPushUndo()) and have deactivate() call
that helper after confirming this tracker is active (the same isActive check it
already does) instead of routing through captureCanvasState(); keep
prepareForSave() behavior unchanged and ensure the new helper bypasses the
isLoadingGraph guard so the outgoing workflow always freezes and persists its
final graph snapshot.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 15814261-63ec-4e4b-9934-a0c53616dad2

📥 Commits

Reviewing files that changed from the base of the PR and between 244e214 and d21294d.

📒 Files selected for processing (4)
  • docs/architecture/change-tracker.md
  • src/scripts/changeTracker.test.ts
  • src/scripts/changeTracker.ts
  • src/utils/__tests__/litegraphTestUtils.ts
✅ Files skipped from review due to trivial changes (1)
  • src/scripts/changeTracker.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/utils/tests/litegraphTestUtils.ts
  • docs/architecture/change-tracker.md

@jaeone94
Copy link
Copy Markdown
Collaborator Author

jaeone94 commented Apr 2, 2026

@coderabbitai approve

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 2, 2026

Tip

For best results, initiate chat on the files or code changes.

@jaeone94 I'll approve the PR now.

[approve]


🧠 Learnings used
Learnt from: jaeone94
Repo: Comfy-Org/ComfyUI_frontend PR: 10816
File: src/scripts/changeTracker.ts:103-126
Timestamp: 2026-04-02T04:11:52.063Z
Learning: In `src/scripts/app.ts` (Comfy-Org/ComfyUI_frontend), within `loadGraphData()`, `useWorkflowService().beforeLoadNewGraph()` is called at line ~1142 while `ChangeTracker.isLoadingGraph` is still `false`. The flag `ChangeTracker.isLoadingGraph = true` is only set at line ~1295, approximately 150 lines later. Therefore, `ChangeTracker.deactivate()` (triggered via `beforeLoadNewGraph`) runs before the `isLoadingGraph` guard is raised, so `captureCanvasState()` inside `deactivate()` is NOT blocked by that guard. Do not flag `deactivate()` as susceptible to the `isLoadingGraph` early-return.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 10140
File: apps/website/package.json:1-79
Timestamp: 2026-03-17T07:28:27.677Z
Learning: In Comfy-Org/ComfyUI_frontend, PRs managed via Graphite stacked sets (using `gt split`) may show browser_tests/ files in the diff as artifacts of the split process. These files contain no actual changes relative to origin/main and should not be flagged as unrelated scope changes. Verify against the PR title/description stacking context before raising scope concerns.

Learnt from: Yourz
Repo: Comfy-Org/ComfyUI_frontend PR: 9965
File: src/platform/cloud/onboarding/CloudSubscriptionRedirectView.vue:77-79
Timestamp: 2026-03-15T15:24:20.611Z
Learning: Comfy-Org/ComfyUI_frontend maintainers prefer PRs to stay narrowly scoped to the stated fix. Cross-file refactors (e.g., adding concurrency guards in composables like useBillingContext) should be proposed as separate follow-up issues rather than requested within a targeted bug-fix PR.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 10547
File: browser_tests/tests/workflowPersistence.spec.ts:459-471
Timestamp: 2026-03-27T05:12:16.400Z
Learning: In `browser_tests/tests/workflowPersistence.spec.ts` (Comfy-Org/ComfyUI_frontend PR `#10547`), the G10 test for PR `#8715` (transient blob:/api-view URL serialization) intentionally validates only the static/default workflow export — not an executed/transient widget state. Putting an image widget into its executed state requires actual model inference (image output generation), which is not available in the current E2E test infrastructure. The test still covers the regression if the `widget.serialize` disable path breaks. Do not flag the absence of transient-state setup in this test; full integration coverage is tracked separately.

Learnt from: christian-byrne
Repo: Comfy-Org/ComfyUI_frontend PR: 8951
File: src/platform/workflow/management/stores/workflowStore.ts:301-301
Timestamp: 2026-03-05T23:21:47.113Z
Learning: In src/platform/workflow/management/stores/workflowStore.ts, `createTemporary()` and `createNewTemporary()` intentionally inject a UUID `id` into workflow data via `ensureWorkflowId()` to give workflows stable identity for sharing. This is a known behavioral change introduced in PR `#8951`. Extensions relying on serialized content matching their input may be silently affected.

Learnt from: jaeone94
Repo: Comfy-Org/ComfyUI_frontend PR: 10309
File: src/platform/missingMedia/composables/useMissingMediaInteractions.ts:153-195
Timestamp: 2026-03-25T15:31:50.781Z
Learning: In `src/platform/missingMedia/composables/useMissingMediaInteractions.ts` (Comfy-Org/ComfyUI_frontend PR `#10309`), `cancelSelection()` does not abort in-flight `api.fetchApi()` uploads. A follow-up issue (`#10511`) exists to add a per-upload `AbortController` map (keyed by `name`), pass its signal into `api.fetchApi`, guard post-await mutations with `signal.aborted` checks, and have `cancelSelection()` call `abort()` before clearing state. The same fix must be applied to the `missingModel` pipeline counterpart composable in the same PR to keep both pipelines consistent. Do not re-raise this as a blocking issue in PR `#10309`; reference issue `#10511` instead.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 0
File: :0-0
Timestamp: 2026-03-20T19:58:37.779Z
Learning: In Comfy-Org/ComfyUI_frontend PR `#10348`, the PR titled "fix: clean up knip config, upgrade to v6, remove unused exports" is a dependency upgrade (knip v5 → v6) + config cleanup, not a bug fix. The appropriate conventional commit type is `chore:` or `build:`, not `fix:`.

Learnt from: jaeone94
Repo: Comfy-Org/ComfyUI_frontend PR: 10309
File: src/platform/missingMedia/composables/useMissingMediaInteractions.ts:56-71
Timestamp: 2026-03-25T15:00:58.823Z
Learning: In `src/platform/missingMedia/composables/useMissingMediaInteractions.ts` (Comfy-Org/ComfyUI_frontend), the `applyValueToNodes` function intentionally omits `node.onWidgetChanged()` and `graph._version++` after setting the combo widget value. This mirrors the identical pattern in `src/platform/missingModel/composables/useMissingModelInteractions.ts` (lines 184-186). Undo/redo has been verified working in both pipelines. Do not flag the absence of `onWidgetChanged` or `_version++` as a bug in either pipeline; any change to add them must be applied consistently to both pipelines together in a dedicated follow-up PR.

Learnt from: Myestery
Repo: Comfy-Org/ComfyUI_frontend PR: 7422
File: .github/workflows/pr-update-playwright-expectations.yaml:131-135
Timestamp: 2025-12-12T23:02:37.473Z
Learning: In the `.github/workflows/pr-update-playwright-expectations.yaml` workflow in the Comfy-Org/ComfyUI_frontend repository, the snapshot update process is intentionally scoped to only add and update snapshot images. Deletions of snapshot files are handled explicitly outside this workflow and should not be suggested as part of this automation.

Learnt from: DrJKL
Repo: Comfy-Org/ComfyUI_frontend PR: 9680
File: src/renderer/core/layout/store/layoutStore.ts:920-922
Timestamp: 2026-03-10T00:27:24.529Z
Learning: In `src/renderer/core/layout/store/layoutStore.ts` (`LayoutStoreImpl`), the `LayoutChange` object is constructed inside `applyOperation` with an immutable `source` captured from `operation.source` and `actor` from `operation.actor`. `notifyChange(change)` passes only this frozen `LayoutChange` to listeners — no listener reads the shared mutable `this.currentSource` or `this.currentActor`. Therefore, making `notifyChange` synchronous (removing the `setTimeout`) does NOT expose the temporarily-mutated `this.currentSource` (set in `batchUpdateNodeBounds`) to listeners. Do not flag the synchronous `notifyChange` call in `finalizeOperation` as a context-pollution risk; the immutable-context pattern is already in place.

} as unknown as ComfyWorkflowJSON
}

function createTracker(initialState?: ComfyWorkflowJSON): ChangeTracker {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we use

export function createMockChangeTracker(
instead?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This test file tests the ChangeTracker class itself — the actual guards, undo queue logic, and lifecycle method behavior. createMockChangeTracker creates a stub with vi.fn() for all methods, which is designed for testing consumers of ChangeTracker (e.g., workflowService). Using the mock here would mean we're testing the mock, not the real class — which is explicitly discouraged in AGENTS.md:

Do not write tests that just test the mocks — Ensure that the tests fail when the code itself would behave in a way that was not expected or desired

The real ChangeTracker constructor + real method logic is needed to verify these invariants.

* INVARIANT: only the active workflow's tracker may read from the canvas.
* Calling this on an inactive tracker would capture the wrong graph.
*/
captureCanvasState() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

How about the old undo/redo queue functionality ?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Undo/redo functionality is fully preserved. captureCanvasState() is the old checkState() with the same internal logic — serialize, compare via graphEqual, push to undoQueue, clear redoQueue, cap at MAX_HISTORY. The undo(), redo(), and undoRedo() methods are completely untouched.

The only behavioral difference: calling on an inactive tracker now returns early (no-op) instead of silently capturing the wrong graph into the undo queue. This is a bug fix, not a feature removal — the old behavior corrupted undo history with the active tab's data.

The deprecated checkState() wrapper delegates to captureCanvasState() directly, so extensions get identical undo/redo behavior.

const original = tracker.activeState

const savedGraph = app.graph
;(app as { graph: unknown }).graph = null
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

can we use mock instead?

Suggested change
;(app as { graph: unknown }).graph = null
vi.spyOn(app, 'graph', 'get').mockReturnValue(null)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Done !

Replace manual property assignment with vi.spyOn(app, 'graph', 'get')
for cleaner mocking.
@jaeone94 jaeone94 requested a review from Myestery April 3, 2026 10:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants