Skip to content

fix: recover stuck node progress and scope mirror to active workflow#11866

Open
christian-byrne wants to merge 7 commits intomainfrom
glary/fix-stuck-progress-eviction
Open

fix: recover stuck node progress and scope mirror to active workflow#11866
christian-byrne wants to merge 7 commits intomainfrom
glary/fix-stuck-progress-eviction

Conversation

@christian-byrne
Copy link
Copy Markdown
Contributor

@christian-byrne christian-byrne commented May 3, 2026

PR Created by the Glary-Bot Agent


Summary

Fixes the bug where node-level progress UI stays stuck on a node after the job has finished (Partner Nodes, observed multiple times in #bug-dump), and prevents cross-workflow progress leakage where a job from one open workflow tab updates the progress UI of another.

Problem

Two independent issues both contribute to the user-visible stuck-progress bug:

  1. Dropped terminal WS message → node progress never clears. nodeProgressStates and nodeProgressStatesByJob[jobId] are only cleared from handleExecutionSuccess / handleExecutionError / handleExecutionInterrupted. The Cloud backend broadcasts these once and does not retry, so any drop leaves the per-node progress bar / time-elapsed counter pinned. queueStore.update() polling sees the job move Running → History (Job Queue and Assets panel update correctly) but never reconciles node-level state.
  2. Cross-workflow leakage. handleProgressState and handleProgress overwrite the global nodeProgressStates mirror and _executingNodeProgress unconditionally, regardless of which open workflow initiated the job. Switching tabs while a job is in flight bleeds progress into the wrong workflow.

Changes

  • executionStore.reconcileTerminalJobs(active, terminal) — new polling-driven reconciliation. Walks tracked jobs, evicts any job that is in the terminal set and not in the active set. Internally uses evictTerminalJob, which always removes per-job state but only clears the global mirror / activeJobId / _executingNodeProgress when the evicted job actually owns them (mirror ownership identified via prompt_id on NodeProgressState since the mirror is replaced wholesale per message). Idempotent with the WS terminal handlers.
  • queueStore.update() — calls reconcileTerminalJobs whenever history is non-empty, regardless of whether queue is empty. The "last active job finishes" case legitimately produces empty Running/Pending plus the job in history, and that is the most common path we need to recover from.
  • useQueuePolling — activates while activeJobsCount > 0 instead of === 1, so recovery still happens when one job's terminal is dropped while another is pending/running.
  • handleProgressState and handleProgress — gate writes to the global mirror / _executingNodeProgress by active workflow. Resolution order: workflow_id on the WS message → jobIdToWorkflowIdjobIdToSessionWorkflowPath → fall through to legacy behaviour. Per-job state (nodeProgressStatesByJob) is unconditionally written either way.
  • apiSchema.ts — adds optional workflow_id to progress, progress_state, executing, executed, progress_text, execution_start, execution_success, execution_cached, execution_interrupted, execution_error, and to NodeProgressState. Optional so the schema is correct both before and after the Cloud deploy that adds the field; gating uses it the moment it arrives.

Why this approach

resetExecutionState is unsafe to call for non-active jobs — it unconditionally clears the global mirror and activeJobId, which would clobber a different running job. evictTerminalJob is the polling-safe variant.

Active-workflow gating is implemented with the existing jobIdToWorkflowId / jobIdToSessionWorkflowPath mappings rather than a UI-wide refactor of all 8 consumers of the global mirror. The gate keeps the mirror surface stable while eliminating the leakage at the only places it's written.

Edge cases

  • WS terminal arrives before the next poll → poll is a no-op (job already cleared).
  • Poll arrives before WS terminal → WS terminal handler is a no-op (activeJobId already null, delete of missing key, etc.).
  • Job appears in both Running and History in the same snapshot (race window between getQueue and getHistory) → guarded by if (activeJobIds.has(jobId)) continue.
  • Last active job finishes, queue empty + job in history → terminal reconciliation runs because it is gated on history non-empty, not queue non-empty.
  • History truncation — terminal recovery uses historyTasks from the same fetch, so jobs rotated out of the bounded window still get LRU-evicted from nodeProgressStatesByJob at 1000 entries.
  • No active workflow / unresolvable workflow context → mirror gate falls through to legacy update so the existing single-tab UX is preserved.

Tests

  • useExecutionStore - reconcileTerminalJobs: 6 cases covering active vs non-active eviction, stale-mirror cleanup, race guard, missing-from-terminal skip, idempotency.
  • useExecutionStore - active workflow gating of progress mirror: 9 cases covering workflow_id match/mismatch on both progress_state and progress events, both fallback resolution paths, default-to-legacy when unresolvable, no-active-workflow short-circuit, and per-job state always updating.
  • useQueueStore - history reconciliation: 2 cases asserting reconcileTerminalJobs runs when queue is empty + history non-empty, and is skipped when history is empty.
  • useQueuePolling: existing > 1 test inverted to assert polling now runs.

pnpm test:unit on the changed stores → 139 passed, 1 skipped. pnpm typecheck, pnpm lint, pnpm format, pnpm knip → clean.

Manual verification

The bug is a race in store reconciliation triggered by dropped WebSocket terminal messages from the Cloud backend. There is no UI control to trigger this locally — reproducing it requires intercepting execution_success / execution_error / execution_interrupted at the network/realtime-broadcast layer, which is exactly the failure mode the unit tests simulate directly via mocked progress_state events without a corresponding terminal event. The 17 new unit tests directly exercise the dropped-message scenario, the multi-workflow leakage scenario, and the recovery path.

Out of scope

  • Backend workflow_id propagation in core ComfyUI — tracked in BE-672.
  • Migrating the 8 global-mirror consumers to read directly from nodeProgressStatesByJob[activeJobId]. The gate already prevents leakage; that refactor can land separately if desired.

┆Issue is synchronized with this Notion page by Unito

Glary-Bot added 2 commits May 2, 2026 02:09
When WebSocket terminal messages (execution_success / execution_error /
execution_interrupted) are dropped, node-level progress UI gets stuck on
the affected node even though the job has reached a terminal state on
the backend. Concurrently, progress messages from a job initiated in
another open workflow can leak into the active one because the global
nodeProgressStates mirror is overwritten unconditionally on every
progress_state event.

Changes:

- Add reconcileTerminalJobs(active, terminal) to executionStore. Used
  by polling to detect jobs that moved Running -> History via the
  backend snapshot and clear their per-job state without disturbing a
  different running job (evictTerminalJob clears the global mirror only
  when the evicted job actually owns it, identified via prompt_id on
  NodeProgressState).
- Wire reconcileTerminalJobs into queueStore.update() alongside the
  existing reconcileInitializingJobs call, so the polling fallback now
  recovers stuck progress without requiring backend retries or
  WebSocket sequence numbers.
- Broaden useQueuePolling activation from activeJobsCount === 1 to
  activeJobsCount > 0 so the recovery path also covers the case where
  one job's terminal message is dropped while another job is still
  pending or running.
- Gate writes to the global nodeProgressStates mirror in
  handleProgressState by active workflow. Resolution order:
  workflow_id on the WS message, jobIdToWorkflowId mapping,
  jobIdToSessionWorkflowPath fallback, defaulting to the legacy
  behaviour when nothing is resolvable.
- Add optional workflow_id field to all execution-related WS message
  schemas so the gating starts using the backend-provided value as
  soon as it ships.

Tests:

- Active-workflow gating: per-job state always updates; mirror only
  updates on workflow_id match (or via mapping fallbacks); preserves
  legacy behaviour when no resolution is available.
- reconcileTerminalJobs: evicts non-active terminal jobs without
  disturbing the active job, evicts the active job and clears the
  mirror, clears a stale mirror still owned by a job that just became
  terminal, skips still-active jobs, skips jobs absent from terminal,
  idempotent.
- queueStore.update(): always reconcile terminal jobs when history is
  non-empty, not just when queue has data. Previously, the common case
  of the last active job finishing (queue empty, job in history) was
  skipped, leaving the very stuck-progress bug we are trying to fix.
- handleProgress: gate _executingNodeProgress by the same active-
  workflow check as handleProgressState, so per-node current progress
  no longer leaks across workflows.
- Add tests for both fixes.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 3, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR implements workflow-aware execution state management for multi-job polling and cross-tab synchronization. Schema changes add workflow_id to WebSocket messages; polling now supports multiple concurrent active jobs; execution handlers gate progress updates to the active workflow and reconcile terminal jobs; and the queue store invokes terminal reconciliation during updates.

Changes

Workflow-Aware Job Execution and Multi-Job Polling

Layer / File(s) Summary
Schema Extensions
src/schemas/apiSchema.ts
Introduces zWorkflowId and adds optional workflow_id to ProgressWsMessage, NodeProgressState, ProgressStateWsMessage, ExecutingWsMessage, zExecutionWsMessageBase, and ProgressTextWsMessage. NodeProgressState also gains optional node relationship fields (display_node_id, parent_node_id, real_node_id).
Multi-Job Polling
src/platform/remote/comfyui/useQueuePolling.ts, src/platform/remote/comfyui/useQueuePolling.test.ts
Polling now triggers when activeJobsCount >= 1 (instead of only === 1), enabling concurrent job polling. Tests updated to assert single-call polling after 8s timer advance when two jobs are active.
Workflow Gating & Terminal Reconciliation
src/stores/executionStore.ts
Adds messageMatchesActiveWorkflow() to resolve message ownership via workflow_id or job-to-workflow mappings. progress_state always updates per-job progress but gates global mirror/preview revocation to active-workflow messages only. New evictTerminalJob(), mirrorOwnerJobId(), and reconcileTerminalJobs() utilities handle safe cleanup and recovery. reconcileMirrorForActiveWorkflow() rebuilds the global progress mirror on active-workflow changes. Exports reconcileTerminalJobs and reconcileMirrorForActiveWorkflow.
Queue Integration
src/stores/queueStore.ts
update() now computes activeJobIds from Running/Pending jobs and calls reconcileTerminalJobs(activeJobIds, terminalJobIds) when history is non-empty, enabling polling-based job eviction.
Comprehensive Tests
src/stores/executionStore.test.ts, src/stores/queueStore.test.ts
Added 661 lines covering workflow-matched progress updates, active-workflow mirror rebuilding, terminal job eviction semantics (clearing activeJobId, handling non-active vs active terminal jobs), stale mirror cleanup on workflow changes, and fallback mapping logic. Queue store tests validate reconcileTerminalJobs invocation on history availability.

Sequence Diagram

sequenceDiagram
    participant WS as WebSocket
    participant Handler as Progress Handler
    participant Resolver as messageMatchesActiveWorkflow
    participant Mirror as Global nodeProgressStates
    participant Store as workflowStore
    participant Output as nodeOutputStore

    WS->>Handler: progress_state (workflow_id, job_id)
    Handler->>Resolver: Check workflow ownership
    Resolver->>Store: Is workflow_id active workflow?
    Store-->>Resolver: activeWorkflow match result
    Resolver-->>Handler: isActiveWorkflowMessage
    
    Handler->>Handler: Update nodeProgressStatesByJob[job_id]
    
    alt Workflow matches active
        Handler->>Output: Revoke previews (running transition)
        Handler->>Mirror: Update nodeProgressStates
        Handler->>Handler: Update _executingNodeProgress
    else Workflow mismatch or unresolved
        Handler->>Handler: Skip global updates
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~90 minutes

Poem

🐰 A swift rabbit hops through the workflows true,
Each job now knows which tab it grew!
Polling two at once, mirrors align,
Reconciliation keeps the logs so fine. 🌿✨


Caution

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

  • Ignore (reviewers only)

❌ Failed checks (1 error, 1 warning)

Check name Status Explanation Resolution
End-To-End Regression Coverage For Fixes ❌ Error PR uses 'fix' language but lacks browser_tests/ changes or concrete explanation for missing E2E regression tests. Add Playwright regression test under browser_tests/ or provide concrete explanation in PR description for why E2E testing is impractical.
Docstring Coverage ⚠️ Warning Docstring coverage is 20.59% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: recovering stuck node progress (via terminal job reconciliation) and scoping the progress mirror to the active workflow (via workflow-gating logic).
Description check ✅ Passed The description is comprehensive, covering the problem statement, solution approach, edge cases, test coverage, and out-of-scope items. While it exceeds the template structure, all required information (what changed, why, and focus areas) is clearly documented.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Adr Compliance For Entity/Litegraph Changes ✅ Passed PR changes are confined to src/platform/remote/comfyui/, src/schemas/, and src/stores/ directories, which fall entirely outside the scope of ADR compliance check for litegraph/entity modifications.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch glary/fix-stuck-progress-eviction

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

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 3, 2026

🎭 Playwright: ✅ 1465 passed, 0 failed · 1 flaky

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

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 3, 2026

🎨 Storybook: ✅ Built — View Storybook

Details

⏰ Completed at: 05/03/2026, 07:57:21 PM UTC

Links

@glary-bot
Copy link
Copy Markdown

glary-bot Bot commented May 3, 2026

Acknowledged — waiting for the full CodeRabbit review pass to come through. Will address concrete feedback once it arrives.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 3, 2026

📦 Bundle: 5.26 MB gzip 🔴 +1.74 kB

Details

Summary

  • Raw size: 24.2 MB baseline 24.2 MB — 🔴 +7.71 kB
  • Gzip: 5.26 MB baseline 5.26 MB — 🔴 +1.74 kB
  • Brotli: 4.07 MB baseline 4.07 MB — 🔴 +999 B
  • Bundles: 258 current • 258 baseline • 118 added / 118 removed

Category Glance
Data & Services 🔴 +7.71 kB (3.05 MB) · Graph Workspace 🟢 -3 B (1.24 MB) · Vendor & Third-Party ⚪ 0 B (9.94 MB) · Other ⚪ 0 B (8.84 MB) · Panels & Settings ⚪ 0 B (489 kB) · Utilities & Hooks ⚪ 0 B (365 kB) · + 5 more

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

Main entry bundles and manifests

File Before After Δ Raw Δ Gzip Δ Brotli
assets/index-Bd0ODNEB.js (new) 22.6 kB 🔴 +22.6 kB 🔴 +8 kB 🔴 +6.89 kB
assets/index-DY_ACs88.js (removed) 22.6 kB 🟢 -22.6 kB 🟢 -8.01 kB 🟢 -6.89 kB

Status: 1 added / 1 removed

Graph Workspace — 1.24 MB (baseline 1.24 MB) • 🟢 -3 B

Graph editor runtime, canvas, workflow orchestration

File Before After Δ Raw Δ Gzip Δ Brotli
assets/GraphView-htPHp318.js (removed) 1.24 MB 🟢 -1.24 MB 🟢 -265 kB 🟢 -199 kB
assets/GraphView-m9xiJX6H.js (new) 1.24 MB 🔴 +1.24 MB 🔴 +265 kB 🔴 +199 kB

Status: 1 added / 1 removed

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

Top-level views, pages, and routed surfaces

File Before After Δ Raw Δ Gzip Δ Brotli
assets/CloudSurveyView-BHt7J_M6.js (removed) 19.6 kB 🟢 -19.6 kB 🟢 -5.14 kB 🟢 -4.57 kB
assets/CloudSurveyView-DVX6JOHP.js (new) 19.6 kB 🔴 +19.6 kB 🔴 +5.14 kB 🔴 +4.59 kB
assets/CloudLoginView-C2AruHOH.js (new) 12.2 kB 🔴 +12.2 kB 🔴 +3.45 kB 🔴 +3.04 kB
assets/CloudLoginView-hEtsKKvW.js (removed) 12.2 kB 🟢 -12.2 kB 🟢 -3.45 kB 🟢 -3.04 kB
assets/CloudSignupView-5dzD-Rmj.js (removed) 9.97 kB 🟢 -9.97 kB 🟢 -2.95 kB 🟢 -2.59 kB
assets/CloudSignupView-B6vEF6v-.js (new) 9.97 kB 🔴 +9.97 kB 🔴 +2.94 kB 🔴 +2.6 kB
assets/UserCheckView-B_ji1FG8.js (new) 9.07 kB 🔴 +9.07 kB 🔴 +2.34 kB 🔴 +2.04 kB
assets/UserCheckView-Bsz-hf_I.js (removed) 9.07 kB 🟢 -9.07 kB 🟢 -2.33 kB 🟢 -2.06 kB
assets/CloudLayoutView-C__Fkz-C.js (removed) 7.73 kB 🟢 -7.73 kB 🟢 -2.45 kB 🟢 -2.14 kB
assets/CloudLayoutView-Cud_9MEy.js (new) 7.73 kB 🔴 +7.73 kB 🔴 +2.45 kB 🔴 +2.15 kB
assets/CloudForgotPasswordView-C-PEwgUW.js (new) 6.14 kB 🔴 +6.14 kB 🔴 +2.19 kB 🔴 +1.91 kB
assets/CloudForgotPasswordView-Cde_S1xU.js (removed) 6.14 kB 🟢 -6.14 kB 🟢 -2.19 kB 🟢 -1.94 kB
assets/CloudAuthTimeoutView-Cjk_hxj_.js (removed) 5.5 kB 🟢 -5.5 kB 🟢 -2.02 kB 🟢 -1.78 kB
assets/CloudAuthTimeoutView-DzeZemDf.js (new) 5.5 kB 🔴 +5.5 kB 🔴 +2.02 kB 🔴 +1.76 kB
assets/CloudSubscriptionRedirectView-B2Ndw2SO.js (removed) 5.28 kB 🟢 -5.28 kB 🟢 -2 kB 🟢 -1.79 kB
assets/CloudSubscriptionRedirectView-DD2V3JAp.js (new) 5.28 kB 🔴 +5.28 kB 🔴 +2 kB 🔴 +1.78 kB
assets/UserSelectView-BlkLF0N2.js (removed) 4.73 kB 🟢 -4.73 kB 🟢 -1.76 kB 🟢 -1.56 kB
assets/UserSelectView-Dsu5CQlL.js (new) 4.73 kB 🔴 +4.73 kB 🔴 +1.75 kB 🔴 +1.55 kB

Status: 9 added / 9 removed / 2 unchanged

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

Configuration panels, inspectors, and settings screens

File Before After Δ Raw Δ Gzip Δ Brotli
assets/KeybindingPanel-Bb8x_qVT.js (new) 46.7 kB 🔴 +46.7 kB 🔴 +9.62 kB 🔴 +8.54 kB
assets/KeybindingPanel-DfbtL9qX.js (removed) 46.7 kB 🟢 -46.7 kB 🟢 -9.62 kB 🟢 -8.54 kB
assets/SecretsPanel-BF9NsLCw.js (removed) 22.9 kB 🟢 -22.9 kB 🟢 -5.55 kB 🟢 -4.87 kB
assets/SecretsPanel-Dmuvqar3.js (new) 22.9 kB 🔴 +22.9 kB 🔴 +5.55 kB 🔴 +4.87 kB
assets/LegacyCreditsPanel-BAtcf7oZ.js (new) 21.7 kB 🔴 +21.7 kB 🔴 +5.91 kB 🔴 +5.2 kB
assets/LegacyCreditsPanel-CXjoj07n.js (removed) 21.7 kB 🟢 -21.7 kB 🟢 -5.91 kB 🟢 -5.2 kB
assets/SubscriptionPanel-BqZmlJs7.js (new) 19.9 kB 🔴 +19.9 kB 🔴 +5.09 kB 🔴 +4.48 kB
assets/SubscriptionPanel-Dh9xd-j4.js (removed) 19.9 kB 🟢 -19.9 kB 🟢 -5.09 kB 🟢 -4.48 kB
assets/AboutPanel-DKlQ629J.js (removed) 12 kB 🟢 -12 kB 🟢 -3.33 kB 🟢 -2.98 kB
assets/AboutPanel-DMZmrbO4.js (new) 12 kB 🔴 +12 kB 🔴 +3.33 kB 🔴 +2.99 kB
assets/ExtensionPanel-7z4PARJP.js (removed) 9.97 kB 🟢 -9.97 kB 🟢 -2.91 kB 🟢 -2.59 kB
assets/ExtensionPanel-Cp8nPWAi.js (new) 9.97 kB 🔴 +9.97 kB 🔴 +2.91 kB 🔴 +2.58 kB
assets/ServerConfigPanel-7QO14baC.js (new) 7.05 kB 🔴 +7.05 kB 🔴 +2.36 kB 🔴 +2.1 kB
assets/ServerConfigPanel-BkzuHEIo.js (removed) 7.05 kB 🟢 -7.05 kB 🟢 -2.36 kB 🟢 -2.12 kB
assets/UserPanel-BwFuUNeF.js (new) 6.75 kB 🔴 +6.75 kB 🔴 +2.24 kB 🔴 +1.97 kB
assets/UserPanel-BxI_cKbb.js (removed) 6.75 kB 🟢 -6.75 kB 🟢 -2.25 kB 🟢 -1.98 kB
assets/cloudRemoteConfig-BvQhVzq-.js (new) 2.05 kB 🔴 +2.05 kB 🔴 +989 B 🔴 +849 B
assets/cloudRemoteConfig-DEphCx_Q.js (removed) 2.05 kB 🟢 -2.05 kB 🟢 -990 B 🟢 -852 B
assets/refreshRemoteConfig-BU7cKKwy.js (new) 1.45 kB 🔴 +1.45 kB 🔴 +648 B 🔴 +548 B
assets/refreshRemoteConfig-C5icyDg2.js (removed) 1.45 kB 🟢 -1.45 kB 🟢 -648 B 🟢 -550 B

Status: 10 added / 10 removed / 11 unchanged

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

Authentication, profile, and account management bundles

File Before After Δ Raw Δ Gzip Δ Brotli
assets/auth-CxSUUCS9.js (new) 3.57 kB 🔴 +3.57 kB 🔴 +1.26 kB 🔴 +1.08 kB
assets/auth-D-Fup9mf.js (removed) 3.57 kB 🟢 -3.57 kB 🟢 -1.26 kB 🟢 -1.08 kB
assets/SignUpForm-BGQMAcDg.js (new) 3.19 kB 🔴 +3.19 kB 🔴 +1.29 kB 🔴 +1.15 kB
assets/SignUpForm-DS9vVsP4.js (removed) 3.19 kB 🟢 -3.19 kB 🟢 -1.29 kB 🟢 -1.15 kB
assets/UpdatePasswordContent-DFnykmCv.js (new) 2.9 kB 🔴 +2.9 kB 🔴 +1.3 kB 🔴 +1.16 kB
assets/UpdatePasswordContent-WQHUynU-.js (removed) 2.9 kB 🟢 -2.9 kB 🟢 -1.3 kB 🟢 -1.16 kB
assets/authStore-CqdRcA6G.js (removed) 1.19 kB 🟢 -1.19 kB 🟢 -569 B 🟢 -509 B
assets/authStore-CWVij02f.js (new) 1.19 kB 🔴 +1.19 kB 🔴 +568 B 🔴 +505 B
assets/auth-CF7G-5FB.js (removed) 348 B 🟢 -348 B 🟢 -216 B 🟢 -189 B
assets/auth-EBvfp6Yh.js (new) 348 B 🔴 +348 B 🔴 +216 B 🔴 +207 B

Status: 5 added / 5 removed / 2 unchanged

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

Modals, dialogs, drawers, and in-app editors

File Before After Δ Raw Δ Gzip Δ Brotli
assets/ComfyHubPublishDialog-C6X7ZRyM.js (removed) 85.8 kB 🟢 -85.8 kB 🟢 -18.6 kB 🟢 -15.9 kB
assets/ComfyHubPublishDialog-DkjcmhcH.js (new) 85.8 kB 🔴 +85.8 kB 🔴 +18.6 kB 🔴 +15.9 kB
assets/useShareDialog-0ru1HKM3.js (removed) 23.8 kB 🟢 -23.8 kB 🟢 -5.78 kB 🟢 -5.12 kB
assets/useShareDialog-4b4rK5o3.js (new) 23.8 kB 🔴 +23.8 kB 🔴 +5.78 kB 🔴 +5.12 kB
assets/ComfyHubPublishDialog-DMzgjtEp.js (removed) 1.35 kB 🟢 -1.35 kB 🟢 -630 B 🟢 -555 B
assets/ComfyHubPublishDialog-DVimo3vM.js (new) 1.35 kB 🔴 +1.35 kB 🔴 +629 B 🔴 +555 B
assets/useSubscriptionDialog-C9A9pFQw.js (new) 1.17 kB 🔴 +1.17 kB 🔴 +560 B 🔴 +484 B
assets/useSubscriptionDialog-CNAlV8d9.js (removed) 1.17 kB 🟢 -1.17 kB 🟢 -560 B 🟢 -487 B

Status: 4 added / 4 removed

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

Reusable component library chunks

File Before After Δ Raw Δ Gzip Δ Brotli
assets/ComfyQueueButton-B3wVI9JH.js (removed) 13.5 kB 🟢 -13.5 kB 🟢 -3.79 kB 🟢 -3.38 kB
assets/ComfyQueueButton-DBlrcKqH.js (new) 13.5 kB 🔴 +13.5 kB 🔴 +3.79 kB 🔴 +3.38 kB
assets/useTerminalTabs-BE9hheWA.js (removed) 11 kB 🟢 -11 kB 🟢 -3.73 kB 🟢 -3.28 kB
assets/useTerminalTabs-BM1eAY69.js (new) 11 kB 🔴 +11 kB 🔴 +3.73 kB 🔴 +3.28 kB
assets/SubscribeButton-aiXGvDg4.js (new) 2.42 kB 🔴 +2.42 kB 🔴 +1.05 kB 🔴 +938 B
assets/SubscribeButton-CFrx0pNN.js (removed) 2.42 kB 🟢 -2.42 kB 🟢 -1.05 kB 🟢 -949 B
assets/cloudFeedbackTopbarButton-C6z3j4Qw.js (removed) 1.83 kB 🟢 -1.83 kB 🟢 -948 B 🟢 -831 B
assets/cloudFeedbackTopbarButton-DZNdy_nh.js (new) 1.83 kB 🔴 +1.83 kB 🔴 +947 B 🔴 +828 B
assets/ComfyQueueButton-Ctu6wgUq.js (removed) 1.27 kB 🟢 -1.27 kB 🟢 -598 B 🟢 -531 B
assets/ComfyQueueButton-DvZ68Sk1.js (new) 1.27 kB 🔴 +1.27 kB 🔴 +594 B 🔴 +530 B

Status: 5 added / 5 removed / 9 unchanged

Data & Services — 3.05 MB (baseline 3.04 MB) • 🔴 +7.71 kB

Stores, services, APIs, and repositories

File Before After Δ Raw Δ Gzip Δ Brotli
assets/dialogService-CGVTsjEw.js (new) 1.99 MB 🔴 +1.99 MB 🔴 +457 kB 🔴 +347 kB
assets/dialogService-CSPOx4oU.js (removed) 1.98 MB 🟢 -1.98 MB 🟢 -456 kB 🟢 -345 kB
assets/api-CRNYddnm.js (new) 888 kB 🔴 +888 kB 🔴 +212 kB 🔴 +167 kB
assets/api-SJ_f8Fca.js (removed) 887 kB 🟢 -887 kB 🟢 -212 kB 🟢 -167 kB
assets/load3dService-D4pMMNww.js (removed) 115 kB 🟢 -115 kB 🟢 -25.1 kB 🟢 -21.3 kB
assets/load3dService-m8iquOwx.js (new) 115 kB 🔴 +115 kB 🔴 +25 kB 🔴 +21.3 kB
assets/workflowShareService-BmlX5LXj.js (removed) 16.6 kB 🟢 -16.6 kB 🟢 -4.89 kB 🟢 -4.33 kB
assets/workflowShareService-DY41H5UL.js (new) 16.6 kB 🔴 +16.6 kB 🔴 +4.89 kB 🔴 +4.33 kB
assets/keybindingService-7Vb-bgAj.js (removed) 13.8 kB 🟢 -13.8 kB 🟢 -3.67 kB 🟢 -3.22 kB
assets/keybindingService-Dx8z0ovg.js (new) 13.8 kB 🔴 +13.8 kB 🔴 +3.67 kB 🔴 +3.22 kB
assets/releaseStore-BIzrHHFj.js (removed) 8.12 kB 🟢 -8.12 kB 🟢 -2.28 kB 🟢 -2 kB
assets/releaseStore-JVsA37Fd.js (new) 8.12 kB 🔴 +8.12 kB 🔴 +2.28 kB 🔴 +2 kB
assets/userStore-B9bOksA8.js (new) 2.24 kB 🔴 +2.24 kB 🔴 +869 B 🔴 +762 B
assets/userStore-j66UfMED.js (removed) 2.24 kB 🟢 -2.24 kB 🟢 -871 B 🟢 -760 B
assets/audioService-BD9PLeZp.js (new) 1.8 kB 🔴 +1.8 kB 🔴 +876 B 🔴 +759 B
assets/audioService-BzNKGfGP.js (removed) 1.8 kB 🟢 -1.8 kB 🟢 -880 B 🟢 -762 B
assets/releaseStore-ByZruO-U.js (new) 1.19 kB 🔴 +1.19 kB 🔴 +563 B 🔴 +496 B
assets/releaseStore-CyylxVxU.js (removed) 1.19 kB 🟢 -1.19 kB 🟢 -563 B 🟢 -499 B
assets/workflowDraftStore-jjm7K97B.js (new) 1.17 kB 🔴 +1.17 kB 🔴 +559 B 🔴 +490 B
assets/workflowDraftStore-MMFzVCpp.js (removed) 1.17 kB 🟢 -1.17 kB 🟢 -559 B 🟢 -495 B
assets/dialogService-7OTfWy6c.js (removed) 1.16 kB 🟢 -1.16 kB 🟢 -551 B 🟢 -490 B
assets/dialogService-Do7xBhzX.js (new) 1.16 kB 🔴 +1.16 kB 🔴 +550 B 🔴 +488 B
assets/settingStore-CcIhJdQS.js (removed) 1.15 kB 🟢 -1.15 kB 🟢 -555 B 🟢 -490 B
assets/settingStore-DLYRFLOG.js (new) 1.15 kB 🔴 +1.15 kB 🔴 +554 B 🔴 +486 B
assets/assetsStore-BNSrb6hm.js (new) 1.15 kB 🔴 +1.15 kB 🔴 +554 B 🔴 +486 B
assets/assetsStore-DKSzl_a9.js (removed) 1.15 kB 🟢 -1.15 kB 🟢 -555 B 🟢 -489 B

Status: 13 added / 13 removed / 4 unchanged

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

Helpers, composables, and utility bundles

File Before After Δ Raw Δ Gzip Δ Brotli
assets/useConflictDetection-BSZUSCD1.js (removed) 233 kB 🟢 -233 kB 🟢 -51.8 kB 🟢 -42.2 kB
assets/useConflictDetection-CjFuJyyu.js (new) 233 kB 🔴 +233 kB 🔴 +51.8 kB 🔴 +42.2 kB
assets/useLoad3d-Bo_S428k.js (removed) 22.3 kB 🟢 -22.3 kB 🟢 -5.09 kB 🟢 -4.52 kB
assets/useLoad3d-ChzALx4_.js (new) 22.3 kB 🔴 +22.3 kB 🔴 +5.09 kB 🔴 +4.49 kB
assets/useLoad3dViewer-dU_DKJ5m.js (removed) 20.8 kB 🟢 -20.8 kB 🟢 -4.91 kB 🟢 -4.29 kB
assets/useLoad3dViewer-jzTiJ9pU.js (new) 20.8 kB 🔴 +20.8 kB 🔴 +4.91 kB 🔴 +4.3 kB
assets/useFeatureFlags-BANIWljL.js (removed) 5.95 kB 🟢 -5.95 kB 🟢 -1.79 kB 🟢 -1.52 kB
assets/useFeatureFlags-Dr1rYOfS.js (new) 5.95 kB 🔴 +5.95 kB 🔴 +1.79 kB 🔴 +1.52 kB
assets/useCopyToClipboard-9BhcdABM.js (new) 5.29 kB 🔴 +5.29 kB 🔴 +1.86 kB 🔴 +1.57 kB
assets/useCopyToClipboard-vytMrERG.js (removed) 5.29 kB 🟢 -5.29 kB 🟢 -1.86 kB 🟢 -1.57 kB
assets/useWorkspaceUI-BdKf1BUr.js (new) 3.34 kB 🔴 +3.34 kB 🔴 +981 B 🔴 +807 B
assets/useWorkspaceUI-BUEtImwc.js (removed) 3.34 kB 🟢 -3.34 kB 🟢 -980 B 🟢 -811 B
assets/subscriptionCheckoutUtil-n4GTBjEo.js (removed) 3.31 kB 🟢 -3.31 kB 🟢 -1.36 kB 🟢 -1.18 kB
assets/subscriptionCheckoutUtil-ogEkVRVI.js (new) 3.31 kB 🔴 +3.31 kB 🔴 +1.36 kB 🔴 +1.19 kB
assets/assetPreviewUtil-IAMJOPFM.js (removed) 2.27 kB 🟢 -2.27 kB 🟢 -957 B 🟢 -838 B
assets/assetPreviewUtil-UeTAUbtH.js (new) 2.27 kB 🔴 +2.27 kB 🔴 +957 B 🔴 +833 B
assets/useUpstreamValue-BOtrrcu2.js (removed) 2.08 kB 🟢 -2.08 kB 🟢 -804 B 🟢 -721 B
assets/useUpstreamValue-xLGGLdTj.js (new) 2.08 kB 🔴 +2.08 kB 🔴 +804 B 🔴 +721 B
assets/useLoad3d-BxyWyHED.js (new) 1.33 kB 🔴 +1.33 kB 🔴 +621 B 🔴 +560 B
assets/useLoad3d-CN8wFBHx.js (removed) 1.33 kB 🟢 -1.33 kB 🟢 -623 B 🟢 -571 B
assets/useLoad3dViewer-CsE7N8Uo.js (new) 1.27 kB 🔴 +1.27 kB 🔴 +588 B 🔴 +528 B
assets/useLoad3dViewer-Dr0IMvko.js (removed) 1.27 kB 🟢 -1.27 kB 🟢 -588 B 🟢 -529 B
assets/useCurrentUser-CK5alWge.js (removed) 1.15 kB 🟢 -1.15 kB 🟢 -555 B 🟢 -489 B
assets/useCurrentUser-SZRsawM6.js (new) 1.15 kB 🔴 +1.15 kB 🔴 +555 B 🔴 +486 B
assets/useWorkspaceSwitch-B0bUY6HQ.js (new) 747 B 🔴 +747 B 🔴 +384 B 🔴 +330 B
assets/useWorkspaceSwitch-CombYlBz.js (removed) 747 B 🟢 -747 B 🟢 -383 B 🟢 -329 B

Status: 13 added / 13 removed / 18 unchanged

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

External libraries and shared vendor chunks

Status: 16 unchanged

Other — 8.84 MB (baseline 8.84 MB) • ⚪ 0 B

Bundles that do not match a named category

File Before After Δ Raw Δ Gzip Δ Brotli
assets/core-BbZYB-qM.js (new) 76.6 kB 🔴 +76.6 kB 🔴 +19.9 kB 🔴 +16.9 kB
assets/core-CkgSNfrr.js (removed) 76.6 kB 🟢 -76.6 kB 🟢 -19.9 kB 🟢 -16.9 kB
assets/groupNode-APnm71Lp.js (new) 74.9 kB 🔴 +74.9 kB 🔴 +18.7 kB 🔴 +16.5 kB
assets/groupNode-DagmeMW4.js (removed) 74.9 kB 🟢 -74.9 kB 🟢 -18.7 kB 🟢 -16.5 kB
assets/WidgetSelect-C3bUAZZd.js (new) 67.2 kB 🔴 +67.2 kB 🔴 +14.6 kB 🔴 +12.6 kB
assets/WidgetSelect-vcdnR-_I.js (removed) 67.2 kB 🟢 -67.2 kB 🟢 -14.6 kB 🟢 -12.7 kB
assets/SubscriptionRequiredDialogContentWorkspace-DacqURM1.js (new) 48.8 kB 🔴 +48.8 kB 🔴 +9.52 kB 🔴 +8.22 kB
assets/SubscriptionRequiredDialogContentWorkspace-DTxdI9SY.js (removed) 48.8 kB 🟢 -48.8 kB 🟢 -9.52 kB 🟢 -8.2 kB
assets/Load3DControls-8yacjl-z.js (new) 46.1 kB 🔴 +46.1 kB 🔴 +7.51 kB 🔴 +6.54 kB
assets/Load3DControls-jipAZyVs.js (removed) 46.1 kB 🟢 -46.1 kB 🟢 -7.51 kB 🟢 -6.56 kB
assets/WidgetPainter-B-90uIn9.js (new) 34 kB 🔴 +34 kB 🔴 +8.3 kB 🔴 +7.36 kB
assets/WidgetPainter-pJlAkfbg.js (removed) 34 kB 🟢 -34 kB 🟢 -8.3 kB 🟢 -7.37 kB
assets/WorkspacePanelContent-D833nv_n.js (new) 32.8 kB 🔴 +32.8 kB 🔴 +7.01 kB 🔴 +6.18 kB
assets/WorkspacePanelContent-DGv67Scf.js (removed) 32.8 kB 🟢 -32.8 kB 🟢 -7.01 kB 🟢 -6.19 kB
assets/Load3dViewerContent-H0hKUA_C.js (new) 28 kB 🔴 +28 kB 🔴 +5.85 kB 🔴 +5.07 kB
assets/Load3dViewerContent-JaAxKQoA.js (removed) 28 kB 🟢 -28 kB 🟢 -5.85 kB 🟢 -5.07 kB
assets/SubscriptionRequiredDialogContent-ClRhezjb.js (new) 27.5 kB 🔴 +27.5 kB 🔴 +6.98 kB 🔴 +6.16 kB
assets/SubscriptionRequiredDialogContent-D7odbgC0.js (removed) 27.5 kB 🟢 -27.5 kB 🟢 -6.98 kB 🟢 -6.19 kB
assets/WidgetImageCrop-Bk8Ck4lj.js (new) 24.3 kB 🔴 +24.3 kB 🔴 +6.2 kB 🔴 +5.46 kB
assets/WidgetImageCrop-CIfqTHbR.js (removed) 24.3 kB 🟢 -24.3 kB 🟢 -6.2 kB 🟢 -5.46 kB
assets/SubscriptionPanelContentWorkspace-D1UILfrD.js (removed) 22.2 kB 🟢 -22.2 kB 🟢 -5.17 kB 🟢 -4.57 kB
assets/SubscriptionPanelContentWorkspace-wLpoF_8D.js (new) 22.2 kB 🔴 +22.2 kB 🔴 +5.17 kB 🔴 +4.55 kB
assets/SignInContent-Bz7R388i.js (new) 20.6 kB 🔴 +20.6 kB 🔴 +5.37 kB 🔴 +4.68 kB
assets/SignInContent-YFO3p-E4.js (removed) 20.6 kB 🟢 -20.6 kB 🟢 -5.37 kB 🟢 -4.69 kB
assets/CurrentUserPopoverWorkspace-BGMSfDfR.js (new) 20.6 kB 🔴 +20.6 kB 🔴 +4.91 kB 🔴 +4.4 kB
assets/CurrentUserPopoverWorkspace-CPwIO2Sq.js (removed) 20.6 kB 🟢 -20.6 kB 🟢 -4.91 kB 🟢 -4.4 kB
assets/WidgetInputNumber-CpnsMkux.js (removed) 19.1 kB 🟢 -19.1 kB 🟢 -4.84 kB 🟢 -4.29 kB
assets/WidgetInputNumber-DkflL3pP.js (new) 19.1 kB 🔴 +19.1 kB 🔴 +4.84 kB 🔴 +4.29 kB
assets/Load3D-Bn632LM5.js (new) 18.5 kB 🔴 +18.5 kB 🔴 +4.39 kB 🔴 +3.83 kB
assets/Load3D-BvunICO-.js (removed) 18.5 kB 🟢 -18.5 kB 🟢 -4.39 kB 🟢 -3.84 kB
assets/WidgetRecordAudio-D9hxWeAd.js (new) 17.4 kB 🔴 +17.4 kB 🔴 +5.02 kB 🔴 +4.48 kB
assets/WidgetRecordAudio-DPdQtVFp.js (removed) 17.4 kB 🟢 -17.4 kB 🟢 -5.02 kB 🟢 -4.48 kB
assets/WidgetRange-CBG23LMv.js (removed) 17.1 kB 🟢 -17.1 kB 🟢 -4.61 kB 🟢 -4.14 kB
assets/WidgetRange-CvDQs7FO.js (new) 17.1 kB 🔴 +17.1 kB 🔴 +4.61 kB 🔴 +4.12 kB
assets/load3d-BCUCvqJY.js (removed) 15.8 kB 🟢 -15.8 kB 🟢 -4.6 kB 🟢 -3.98 kB
assets/load3d-CjKz6en0.js (new) 15.8 kB 🔴 +15.8 kB 🔴 +4.6 kB 🔴 +3.97 kB
assets/WaveAudioPlayer-73fNyePD.js (new) 13.4 kB 🔴 +13.4 kB 🔴 +3.69 kB 🔴 +3.23 kB
assets/WaveAudioPlayer-CvhL5Iwc.js (removed) 13.4 kB 🟢 -13.4 kB 🟢 -3.69 kB 🟢 -3.23 kB
assets/WidgetCurve--PEIs-Ux.js (new) 12.2 kB 🔴 +12.2 kB 🔴 +3.93 kB 🔴 +3.56 kB
assets/WidgetCurve-C1rmZCSn.js (removed) 12.2 kB 🟢 -12.2 kB 🟢 -3.93 kB 🟢 -3.56 kB
assets/TeamWorkspacesDialogContent-BsHEQuAF.js (removed) 11.3 kB 🟢 -11.3 kB 🟢 -3.43 kB 🟢 -3.05 kB
assets/TeamWorkspacesDialogContent-CzKWIA5y.js (new) 11.3 kB 🔴 +11.3 kB 🔴 +3.42 kB 🔴 +3.05 kB
assets/nodeTemplates-A625RbyI.js (new) 9.84 kB 🔴 +9.84 kB 🔴 +3.48 kB 🔴 +3.08 kB
assets/nodeTemplates-B15Zg-CE.js (removed) 9.84 kB 🟢 -9.84 kB 🟢 -3.48 kB 🟢 -3.07 kB
assets/NightlySurveyController-3qn27ayB.js (new) 9.02 kB 🔴 +9.02 kB 🔴 +3.16 kB 🔴 +2.79 kB
assets/NightlySurveyController-DzTUzg9O.js (removed) 9.02 kB 🟢 -9.02 kB 🟢 -3.16 kB 🟢 -2.79 kB
assets/Load3DConfiguration-Bhx-R3Km.js (new) 8.03 kB 🔴 +8.03 kB 🔴 +2.36 kB 🔴 +2.06 kB
assets/Load3DConfiguration-CGXjFwQY.js (removed) 8.03 kB 🟢 -8.03 kB 🟢 -2.37 kB 🟢 -2.07 kB
assets/InviteMemberDialogContent-BnUB_ZHA.js (removed) 7.94 kB 🟢 -7.94 kB 🟢 -2.53 kB 🟢 -2.22 kB
assets/InviteMemberDialogContent-Bt-ji0t_.js (new) 7.94 kB 🔴 +7.94 kB 🔴 +2.53 kB 🔴 +2.22 kB
assets/onboardingCloudRoutes-C_YguXj7.js (removed) 6.59 kB 🟢 -6.59 kB 🟢 -2.06 kB 🟢 -1.76 kB
assets/onboardingCloudRoutes-YYYmn_dm.js (new) 6.59 kB 🔴 +6.59 kB 🔴 +2.06 kB 🔴 +1.76 kB
assets/CreateWorkspaceDialogContent-C1Y4_2Dz.js (new) 6.15 kB 🔴 +6.15 kB 🔴 +2.24 kB 🔴 +1.97 kB
assets/CreateWorkspaceDialogContent-D8MU98Tk.js (removed) 6.15 kB 🟢 -6.15 kB 🟢 -2.24 kB 🟢 -1.96 kB
assets/WidgetWithControl-BbF78UZb.js (new) 6.05 kB 🔴 +6.05 kB 🔴 +2.42 kB 🔴 +2.14 kB
assets/WidgetWithControl-DJW_UvaE.js (removed) 6.05 kB 🟢 -6.05 kB 🟢 -2.42 kB 🟢 -2.18 kB
assets/FreeTierDialogContent-CzQEKcJM.js (new) 6.01 kB 🔴 +6.01 kB 🔴 +2.14 kB 🔴 +1.9 kB
assets/FreeTierDialogContent-D-stVJWB.js (removed) 6.01 kB 🟢 -6.01 kB 🟢 -2.14 kB 🟢 -1.91 kB
assets/EditWorkspaceDialogContent-CFQuRaFU.js (removed) 5.95 kB 🟢 -5.95 kB 🟢 -2.2 kB 🟢 -1.93 kB
assets/EditWorkspaceDialogContent-CJmQSisQ.js (new) 5.95 kB 🔴 +5.95 kB 🔴 +2.2 kB 🔴 +1.93 kB
assets/WidgetTextarea-BREzyJes.js (removed) 5.76 kB 🟢 -5.76 kB 🟢 -2.27 kB 🟢 -2.02 kB
assets/WidgetTextarea-DIXLaseY.js (new) 5.76 kB 🔴 +5.76 kB 🔴 +2.27 kB 🔴 +2.02 kB
assets/Preview3d-CwDPeOtv.js (removed) 5.73 kB 🟢 -5.73 kB 🟢 -1.93 kB 🟢 -1.69 kB
assets/Preview3d-Rk4VoMob.js (new) 5.73 kB 🔴 +5.73 kB 🔴 +1.93 kB 🔴 +1.69 kB
assets/ValueControlPopover-Byu_xdya.js (removed) 5.53 kB 🟢 -5.53 kB 🟢 -2.02 kB 🟢 -1.81 kB
assets/ValueControlPopover-DIVlVcz9.js (new) 5.53 kB 🔴 +5.53 kB 🔴 +2.02 kB 🔴 +1.81 kB
assets/CancelSubscriptionDialogContent-CzqX664J.js (removed) 5.49 kB 🟢 -5.49 kB 🟢 -2.06 kB 🟢 -1.81 kB
assets/CancelSubscriptionDialogContent-D65B17Ou.js (new) 5.49 kB 🔴 +5.49 kB 🔴 +2.06 kB 🔴 +1.8 kB
assets/DeleteWorkspaceDialogContent-B9UumP3T.js (removed) 4.85 kB 🟢 -4.85 kB 🟢 -1.88 kB 🟢 -1.63 kB
assets/DeleteWorkspaceDialogContent-Bbgxa_qC.js (new) 4.85 kB 🔴 +4.85 kB 🔴 +1.88 kB 🔴 +1.62 kB
assets/LeaveWorkspaceDialogContent-CAsa6RNw.js (new) 4.68 kB 🔴 +4.68 kB 🔴 +1.82 kB 🔴 +1.59 kB
assets/LeaveWorkspaceDialogContent-RFWYV-PN.js (removed) 4.68 kB 🟢 -4.68 kB 🟢 -1.83 kB 🟢 -1.58 kB
assets/RemoveMemberDialogContent-BtRTshd0.js (removed) 4.66 kB 🟢 -4.66 kB 🟢 -1.78 kB 🟢 -1.55 kB
assets/RemoveMemberDialogContent-Buxgxc9Q.js (new) 4.66 kB 🔴 +4.66 kB 🔴 +1.78 kB 🔴 +1.56 kB
assets/RevokeInviteDialogContent-C42Hb4hM.js (new) 4.57 kB 🔴 +4.57 kB 🔴 +1.79 kB 🔴 +1.57 kB
assets/RevokeInviteDialogContent-C4L42VBo.js (removed) 4.57 kB 🟢 -4.57 kB 🟢 -1.79 kB 🟢 -1.58 kB
assets/InviteMemberUpsellDialogContent-DbdYAK5k.js (removed) 4.47 kB 🟢 -4.47 kB 🟢 -1.65 kB 🟢 -1.44 kB
assets/InviteMemberUpsellDialogContent-DXmdYJSx.js (new) 4.47 kB 🔴 +4.47 kB 🔴 +1.65 kB 🔴 +1.46 kB
assets/tierBenefits-BuaIDJ2o.js (removed) 4.45 kB 🟢 -4.45 kB 🟢 -1.57 kB 🟢 -1.36 kB
assets/tierBenefits-Ds9UAT7G.js (new) 4.45 kB 🔴 +4.45 kB 🔴 +1.57 kB 🔴 +1.36 kB
assets/cloudSessionCookie-CVZ72kBd.js (removed) 4.31 kB 🟢 -4.31 kB 🟢 -1.58 kB 🟢 -1.38 kB
assets/cloudSessionCookie-DEc8CsmY.js (new) 4.31 kB 🔴 +4.31 kB 🔴 +1.58 kB 🔴 +1.37 kB
assets/Media3DTop-Dr1sSAOu.js (new) 4.04 kB 🔴 +4.04 kB 🔴 +1.71 kB 🔴 +1.5 kB
assets/Media3DTop-vpYNykTo.js (removed) 4.04 kB 🟢 -4.04 kB 🟢 -1.71 kB 🟢 -1.51 kB
assets/saveMesh-CUOK0fp2.js (removed) 4.03 kB 🟢 -4.03 kB 🟢 -1.77 kB 🟢 -1.57 kB
assets/saveMesh-PbbO4JaQ.js (new) 4.03 kB 🔴 +4.03 kB 🔴 +1.77 kB 🔴 +1.56 kB
assets/GlobalToast-1PN38Xzq.js (removed) 3.05 kB 🟢 -3.05 kB 🟢 -1.26 kB 🟢 -1.12 kB
assets/GlobalToast-3Ucltl1W.js (new) 3.05 kB 🔴 +3.05 kB 🔴 +1.26 kB 🔴 +1.07 kB
assets/CloudRunButtonWrapper-BISNk_0j.js (removed) 2.23 kB 🟢 -2.23 kB 🟢 -1.02 kB 🟢 -907 B
assets/CloudRunButtonWrapper-fuRCpJCf.js (new) 2.23 kB 🔴 +2.23 kB 🔴 +1.02 kB 🔴 +905 B
assets/SubscribeToRun-TZk_q7TV.js (removed) 2.13 kB 🟢 -2.13 kB 🟢 -982 B 🟢 -880 B
assets/SubscribeToRun-UIZy7gMA.js (new) 2.13 kB 🔴 +2.13 kB 🔴 +982 B 🔴 +880 B
assets/MediaAudioTop-Bc1ECSVm.js (new) 2.08 kB 🔴 +2.08 kB 🔴 +1 kB 🔴 +864 B
assets/MediaAudioTop-Bw-rbIH6.js (removed) 2.08 kB 🟢 -2.08 kB 🟢 -1.01 kB 🟢 -864 B
assets/cloudBadges-Bri-Ya3N.js (new) 1.96 kB 🔴 +1.96 kB 🔴 +978 B 🔴 +849 B
assets/cloudBadges-Dmw73zyV.js (removed) 1.96 kB 🟢 -1.96 kB 🟢 -979 B 🟢 -850 B
assets/cloudSubscription-51rwOue9.js (new) 1.88 kB 🔴 +1.88 kB 🔴 +898 B 🔴 +776 B
assets/cloudSubscription-CK2peurr.js (removed) 1.88 kB 🟢 -1.88 kB 🟢 -900 B 🟢 -778 B
assets/graphHasMissingNodes-C8z8xgkf.js (new) 1.83 kB 🔴 +1.83 kB 🔴 +860 B 🔴 +771 B
assets/graphHasMissingNodes-CyU1n7bf.js (removed) 1.83 kB 🟢 -1.83 kB 🟢 -861 B 🟢 -759 B
assets/Load3D-CV9DyVoC.js (removed) 1.58 kB 🟢 -1.58 kB 🟢 -716 B 🟢 -635 B
assets/Load3D-DNUPmNJe.js (new) 1.58 kB 🔴 +1.58 kB 🔴 +715 B 🔴 +633 B
assets/previousFullPath-DZIPjG_L.js (removed) 1.53 kB 🟢 -1.53 kB 🟢 -694 B 🟢 -599 B
assets/previousFullPath-PCXPxTyK.js (new) 1.53 kB 🔴 +1.53 kB 🔴 +694 B 🔴 +593 B
assets/nightlyBadges-BSCp_hLi.js (new) 1.49 kB 🔴 +1.49 kB 🔴 +748 B 🔴 +684 B
assets/nightlyBadges-Dk0Pn3ou.js (removed) 1.49 kB 🟢 -1.49 kB 🟢 -747 B 🟢 -663 B
assets/Load3dViewerContent-CkgJRisY.js (removed) 1.46 kB 🟢 -1.46 kB 🟢 -666 B 🟢 -589 B
assets/Load3dViewerContent-DEoPv_Ig.js (new) 1.46 kB 🔴 +1.46 kB 🔴 +664 B 🔴 +607 B
assets/SubscriptionPanelContentWorkspace-BlTOP0ML.js (new) 1.35 kB 🔴 +1.35 kB 🔴 +618 B 🔴 +540 B
assets/SubscriptionPanelContentWorkspace-LlYeI-Ez.js (removed) 1.35 kB 🟢 -1.35 kB 🟢 -615 B 🟢 -540 B
assets/WidgetLegacy-CRdvdPsP.js (new) 1.18 kB 🔴 +1.18 kB 🔴 +566 B 🔴 +492 B
assets/WidgetLegacy-D3YcBk3A.js (removed) 1.18 kB 🟢 -1.18 kB 🟢 -566 B 🟢 -495 B
assets/changeTracker-CC4KWvFh.js (removed) 1.15 kB 🟢 -1.15 kB 🟢 -556 B 🟢 -488 B
assets/changeTracker-DBFSloOl.js (new) 1.15 kB 🔴 +1.15 kB 🔴 +555 B 🔴 +482 B

Status: 57 added / 57 removed / 78 unchanged

⚡ Performance Report

canvas-idle: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 68.9 MB heap
canvas-mouse-sweep: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 64.3 MB heap
canvas-zoom-sweep: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 46.3 MB heap
dom-widget-clipping: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 54.9 MB heap
large-graph-idle: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 57.1 MB heap
large-graph-pan: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 64.2 MB heap
large-graph-zoom: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 66.8 MB heap
minimap-idle: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 66.9 MB heap
subgraph-dom-widget-clipping: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 55.1 MB heap
subgraph-idle: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 68.2 MB heap
subgraph-mouse-sweep: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 48.8 MB heap
viewport-pan-sweep: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 68.9 MB heap
vue-large-graph-idle: · 58.1 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 164.0 MB heap
vue-large-graph-pan: · 56.3 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 157.7 MB heap
workflow-execution: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 53.5 MB heap

⚠️ 3 regressions detected

Metric Baseline PR (median) Δ Sig
large-graph-pan: style recalc duration 19ms 19ms +3% ⚠️ z=2.3
large-graph-pan: task duration 1034ms 1176ms +14% ⚠️ z=2.2
subgraph-mouse-sweep: layout duration 5ms 5ms +13% ⚠️ z=2.1
All metrics
Metric Baseline PR (median) Δ Sig
canvas-idle: avg frame time 17ms 17ms +0% z=-0.1
canvas-idle: p95 frame time 17ms 17ms +1%
canvas-idle: layout duration 0ms 0ms +0%
canvas-idle: style recalc duration 7ms 8ms +8% z=-3.3
canvas-idle: layout count 0 0 +0%
canvas-idle: style recalc count 8 8 +0% z=-5.6
canvas-idle: task duration 361ms 399ms +11% z=0.1
canvas-idle: script duration 16ms 22ms +40% z=-1.5
canvas-idle: TBT 0ms 0ms +0%
canvas-idle: heap used 68.1 MB 68.9 MB +1%
canvas-idle: DOM nodes 16 16 +0% z=-5.2
canvas-idle: event listeners 4 6 +50% z=-1.2
canvas-mouse-sweep: avg frame time 17ms 17ms +0% z=-0.4
canvas-mouse-sweep: p95 frame time 17ms 17ms +0%
canvas-mouse-sweep: layout duration 4ms 4ms -1% z=-0.6
canvas-mouse-sweep: style recalc duration 37ms 40ms +10% z=-0.7
canvas-mouse-sweep: layout count 12 12 +0%
canvas-mouse-sweep: style recalc count 72 75 +4% z=-1.6
canvas-mouse-sweep: task duration 790ms 826ms +5% z=-0.7
canvas-mouse-sweep: script duration 123ms 126ms +3% z=-1.4
canvas-mouse-sweep: TBT 0ms 0ms +0%
canvas-mouse-sweep: heap used 50.9 MB 64.3 MB +26%
canvas-mouse-sweep: DOM nodes -263 58 -122% z=-1.7
canvas-mouse-sweep: event listeners -131 6 -105% z=-0.6
canvas-zoom-sweep: avg frame time 17ms 17ms +0% z=0.5
canvas-zoom-sweep: p95 frame time 17ms 17ms +0%
canvas-zoom-sweep: layout duration 1ms 1ms +22% z=1.7
canvas-zoom-sweep: style recalc duration 16ms 18ms +8% z=-0.9
canvas-zoom-sweep: layout count 6 6 +0%
canvas-zoom-sweep: style recalc count 31 31 +0% z=-0.6
canvas-zoom-sweep: task duration 289ms 340ms +18% z=0.6
canvas-zoom-sweep: script duration 17ms 24ms +41% z=-0.9
canvas-zoom-sweep: TBT 0ms 0ms +0%
canvas-zoom-sweep: heap used 46.6 MB 46.3 MB -1%
canvas-zoom-sweep: DOM nodes 76 75 -1% z=-5.4
canvas-zoom-sweep: event listeners 19 21 +11% z=-0.5
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 7ms 8ms +12% z=-2.9
dom-widget-clipping: layout count 0 0 +0%
dom-widget-clipping: style recalc count 10 10 +0% z=-6.2
dom-widget-clipping: task duration 328ms 349ms +7% z=-0.9
dom-widget-clipping: script duration 58ms 56ms -3% z=-3.6
dom-widget-clipping: TBT 0ms 0ms +0%
dom-widget-clipping: heap used 55.9 MB 54.9 MB -2%
dom-widget-clipping: DOM nodes 16 16 +0% z=-4.3
dom-widget-clipping: event listeners 0 2 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 8ms 10ms +16% z=-2.6
large-graph-idle: layout count 0 0 +0%
large-graph-idle: style recalc count 9 9 +0% z=-8.3
large-graph-idle: task duration 546ms 600ms +10% z=1.1
large-graph-idle: script duration 90ms 107ms +19% z=0.4
large-graph-idle: TBT 0ms 0ms +0%
large-graph-idle: heap used 59.5 MB 57.1 MB -4%
large-graph-idle: DOM nodes -263 -261 -1% z=-316.1
large-graph-idle: event listeners -129 -129 +0% z=-25.4
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 19ms 19ms +3% ⚠️ z=2.3
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 1034ms 1176ms +14% ⚠️ z=2.2
large-graph-pan: script duration 362ms 399ms +10% z=-0.5
large-graph-pan: TBT 0ms 0ms +0%
large-graph-pan: heap used 62.9 MB 64.2 MB +2%
large-graph-pan: DOM nodes -261 -262 +0% z=-170.3
large-graph-pan: event listeners -127 -129 +2% z=-161.7
large-graph-zoom: avg frame time 17ms 17ms +0%
large-graph-zoom: p95 frame time 17ms 17ms +0%
large-graph-zoom: layout duration 8ms 8ms -7%
large-graph-zoom: style recalc duration 20ms 19ms -1%
large-graph-zoom: layout count 60 60 +0%
large-graph-zoom: style recalc count 65 66 +2%
large-graph-zoom: task duration 1359ms 1385ms +2%
large-graph-zoom: script duration 498ms 488ms -2%
large-graph-zoom: TBT 0ms 0ms +0%
large-graph-zoom: heap used 66.5 MB 66.8 MB +0%
large-graph-zoom: DOM nodes -267 -270 +1%
large-graph-zoom: event listeners -125 -125 +0%
minimap-idle: avg frame time 17ms 17ms +0% z=-0.9
minimap-idle: p95 frame time 17ms 17ms +0%
minimap-idle: layout duration 0ms 0ms +0%
minimap-idle: style recalc duration 7ms 7ms +5% z=-2.8
minimap-idle: layout count 0 0 +0%
minimap-idle: style recalc count 8 7 -13% z=-3.8
minimap-idle: task duration 529ms 607ms +15% z=1.7
minimap-idle: script duration 83ms 103ms +24% z=0.5
minimap-idle: TBT 0ms 0ms +0%
minimap-idle: heap used 61.6 MB 66.9 MB +9%
minimap-idle: DOM nodes -266 -265 -0% z=-207.7
minimap-idle: event listeners -129 -129 +0% z=-202.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 10ms 10ms +7% z=-2.4
subgraph-dom-widget-clipping: layout count 0 0 +0%
subgraph-dom-widget-clipping: style recalc count 45 46 +2% z=-3.3
subgraph-dom-widget-clipping: task duration 341ms 368ms +8% z=-0.6
subgraph-dom-widget-clipping: script duration 118ms 118ms +0% z=-1.6
subgraph-dom-widget-clipping: TBT 0ms 0ms +0%
subgraph-dom-widget-clipping: heap used 54.9 MB 55.1 MB +0%
subgraph-dom-widget-clipping: DOM nodes 16 17 +6% z=-4.6
subgraph-dom-widget-clipping: event listeners 6 8 +33% z=-1.4
subgraph-idle: avg frame time 17ms 17ms +0% z=0.4
subgraph-idle: p95 frame time 17ms 17ms +0%
subgraph-idle: layout duration 0ms 0ms +0%
subgraph-idle: style recalc duration 7ms 9ms +22% z=-1.8
subgraph-idle: layout count 0 0 +0%
subgraph-idle: style recalc count 9 9 +0% z=-2.9
subgraph-idle: task duration 335ms 392ms +17% z=0.7
subgraph-idle: script duration 13ms 19ms +40% z=-0.5
subgraph-idle: TBT 0ms 0ms +0%
subgraph-idle: heap used 68.0 MB 68.2 MB +0%
subgraph-idle: DOM nodes 18 18 +0% z=-2.5
subgraph-idle: event listeners 4 6 +50% variance too high
subgraph-mouse-sweep: avg frame time 17ms 17ms -0% z=-0.5
subgraph-mouse-sweep: p95 frame time 17ms 17ms -1%
subgraph-mouse-sweep: layout duration 5ms 5ms +13% ⚠️ z=2.1
subgraph-mouse-sweep: style recalc duration 37ms 40ms +8% z=-0.8
subgraph-mouse-sweep: layout count 16 16 +0%
subgraph-mouse-sweep: style recalc count 76 75 -1% z=-2.7
subgraph-mouse-sweep: task duration 684ms 777ms +14% z=0.2
subgraph-mouse-sweep: script duration 86ms 92ms +6% z=-1.4
subgraph-mouse-sweep: TBT 0ms 0ms +0%
subgraph-mouse-sweep: heap used 43.7 MB 48.8 MB +12%
subgraph-mouse-sweep: DOM nodes -263 -262 -0% z=-147.2
subgraph-mouse-sweep: event listeners -133 -129 -3% 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 55ms 56ms +2%
viewport-pan-sweep: layout count 0 0 +0%
viewport-pan-sweep: style recalc count 249 249 +0%
viewport-pan-sweep: task duration 3514ms 4116ms +17%
viewport-pan-sweep: script duration 1156ms 1334ms +15%
viewport-pan-sweep: TBT 0ms 0ms +0%
viewport-pan-sweep: heap used 72.4 MB 68.9 MB -5%
viewport-pan-sweep: DOM nodes -264 -262 -1%
viewport-pan-sweep: event listeners -113 -113 +0%
vue-large-graph-idle: avg frame time 17ms 17ms +0%
vue-large-graph-idle: p95 frame time 17ms 17ms +0%
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 12762ms 10835ms -15%
vue-large-graph-idle: script duration 596ms 599ms +0%
vue-large-graph-idle: TBT 0ms 0ms +0%
vue-large-graph-idle: heap used 162.8 MB 164.0 MB +1%
vue-large-graph-idle: DOM nodes -8331 -8331 +0%
vue-large-graph-idle: event listeners -16466 -16466 +0%
vue-large-graph-pan: avg frame time 17ms 18ms +3%
vue-large-graph-pan: p95 frame time 17ms 17ms +0%
vue-large-graph-pan: layout duration 0ms 0ms +0%
vue-large-graph-pan: style recalc duration 18ms 18ms -1%
vue-large-graph-pan: layout count 0 0 +0%
vue-large-graph-pan: style recalc count 70 66 -6%
vue-large-graph-pan: task duration 15121ms 13187ms -13%
vue-large-graph-pan: script duration 867ms 922ms +6%
vue-large-graph-pan: TBT 0ms 0ms +0%
vue-large-graph-pan: heap used 153.0 MB 157.7 MB +3%
vue-large-graph-pan: DOM nodes -8329 -8331 +0%
vue-large-graph-pan: event listeners -16488 -16462 -0%
workflow-execution: avg frame time 17ms 17ms +0% z=0.6
workflow-execution: p95 frame time 17ms 17ms +0%
workflow-execution: layout duration 2ms 2ms -13% z=0.1
workflow-execution: style recalc duration 23ms 24ms +5% z=-0.0
workflow-execution: layout count 4 5 +25% z=0.1
workflow-execution: style recalc count 18 17 -6% z=-0.4
workflow-execution: task duration 116ms 127ms +10% z=0.4
workflow-execution: script duration 23ms 24ms +4% z=-1.8
workflow-execution: TBT 0ms 0ms +0%
workflow-execution: heap used 52.9 MB 53.5 MB +1%
workflow-execution: DOM nodes 157 157 +0% z=-0.5
workflow-execution: event listeners 69 69 +0% z=3.9
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-05-03T20:16:27.645Z",
  "gitSha": "592cac8bc2810e5f310444959d699f52fd009116",
  "branch": "glary/fix-stuck-progress-eviction",
  "measurements": [
    {
      "name": "canvas-idle",
      "durationMs": 2032.2979999999973,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 7.93,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 398.837,
      "heapDeltaBytes": 22832364,
      "heapUsedBytes": 72231484,
      "domNodes": 16,
      "jsHeapTotalBytes": 15204352,
      "scriptDurationMs": 21.863999999999997,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-idle",
      "durationMs": 1995.5209999999965,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 7.061000000000002,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 393.78400000000005,
      "heapDeltaBytes": 22805944,
      "heapUsedBytes": 72410760,
      "domNodes": 16,
      "jsHeapTotalBytes": 14942208,
      "scriptDurationMs": 20.722000000000005,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-idle",
      "durationMs": 2039.2150000000129,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 9.768000000000002,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 412.25199999999995,
      "heapDeltaBytes": 2063928,
      "heapUsedBytes": 67663672,
      "domNodes": 20,
      "jsHeapTotalBytes": 20017152,
      "scriptDurationMs": 29.823999999999998,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 1902.5660000000073,
      "styleRecalcs": 75,
      "styleRecalcDurationMs": 41.201,
      "layouts": 12,
      "layoutDurationMs": 4.228,
      "taskDurationMs": 847.9599999999999,
      "heapDeltaBytes": 18290244,
      "heapUsedBytes": 67467336,
      "domNodes": 58,
      "jsHeapTotalBytes": 15990784,
      "scriptDurationMs": 130.43500000000003,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 1797.6430000000505,
      "styleRecalcs": 76,
      "styleRecalcDurationMs": 40.336,
      "layouts": 12,
      "layoutDurationMs": 3.5010000000000003,
      "taskDurationMs": 792.6469999999999,
      "heapDeltaBytes": 19335184,
      "heapUsedBytes": 68217168,
      "domNodes": 59,
      "jsHeapTotalBytes": 15466496,
      "scriptDurationMs": 126.442,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 1851.4979999999923,
      "styleRecalcs": 74,
      "styleRecalcDurationMs": 39.162,
      "layouts": 12,
      "layoutDurationMs": 3.47,
      "taskDurationMs": 825.932,
      "heapDeltaBytes": 18235020,
      "heapUsedBytes": 67017068,
      "domNodes": 56,
      "jsHeapTotalBytes": 16777216,
      "scriptDurationMs": 121.99100000000001,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1718.6049999999682,
      "styleRecalcs": 31,
      "styleRecalcDurationMs": 17.762999999999998,
      "layouts": 6,
      "layoutDurationMs": 0.6789999999999999,
      "taskDurationMs": 329.878,
      "heapDeltaBytes": 151496,
      "heapUsedBytes": 48777904,
      "domNodes": 76,
      "jsHeapTotalBytes": 15204352,
      "scriptDurationMs": 23.387,
      "eventListeners": 21,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1734.929999999963,
      "styleRecalcs": 31,
      "styleRecalcDurationMs": 19.031,
      "layouts": 6,
      "layoutDurationMs": 0.831,
      "taskDurationMs": 340.14599999999996,
      "heapDeltaBytes": 214208,
      "heapUsedBytes": 48532068,
      "domNodes": 75,
      "jsHeapTotalBytes": 14680064,
      "scriptDurationMs": 24.474999999999998,
      "eventListeners": 21,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1721.6190000000324,
      "styleRecalcs": 31,
      "styleRecalcDurationMs": 17.706,
      "layouts": 6,
      "layoutDurationMs": 0.719,
      "taskDurationMs": 344.6480000000001,
      "heapDeltaBytes": -165404,
      "heapUsedBytes": 47896872,
      "domNodes": 75,
      "jsHeapTotalBytes": 14942208,
      "scriptDurationMs": 24.87600000000001,
      "eventListeners": 21,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 641.3840000000164,
      "styleRecalcs": 11,
      "styleRecalcDurationMs": 7.700000000000002,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 373.36400000000003,
      "heapDeltaBytes": 8801996,
      "heapUsedBytes": 57546416,
      "domNodes": 18,
      "jsHeapTotalBytes": 14942208,
      "scriptDurationMs": 60.575,
      "eventListeners": 2,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000273
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 555.0240000000031,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 7.603999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 348.179,
      "heapDeltaBytes": 9436408,
      "heapUsedBytes": 58660040,
      "domNodes": 16,
      "jsHeapTotalBytes": 14417920,
      "scriptDurationMs": 55.478,
      "eventListeners": 0,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 551.1010000000169,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 7.129,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 349.15299999999996,
      "heapDeltaBytes": 8689696,
      "heapUsedBytes": 57472596,
      "domNodes": 16,
      "jsHeapTotalBytes": 15204352,
      "scriptDurationMs": 56.133,
      "eventListeners": 2,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999727
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2026.4449999999954,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 9.544,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 617.3249999999999,
      "heapDeltaBytes": 3667876,
      "heapUsedBytes": 60837872,
      "domNodes": -261,
      "jsHeapTotalBytes": 4222976,
      "scriptDurationMs": 106.89599999999999,
      "eventListeners": -129,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2009.5180000000141,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 7.806,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 599.679,
      "heapDeltaBytes": 1049032,
      "heapUsedBytes": 59181588,
      "domNodes": -264,
      "jsHeapTotalBytes": 4485120,
      "scriptDurationMs": 100.741,
      "eventListeners": -159,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2018.2810000000018,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 9.794,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 600.214,
      "heapDeltaBytes": 2227212,
      "heapUsedBytes": 59868432,
      "domNodes": -261,
      "jsHeapTotalBytes": 5533696,
      "scriptDurationMs": 107.46499999999999,
      "eventListeners": -129,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2139.3489999999815,
      "styleRecalcs": 69,
      "styleRecalcDurationMs": 18.697000000000003,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1175.934,
      "heapDeltaBytes": 20412040,
      "heapUsedBytes": 79506164,
      "domNodes": -267,
      "jsHeapTotalBytes": 4689920,
      "scriptDurationMs": 399.12499999999994,
      "eventListeners": -159,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2131.920999999977,
      "styleRecalcs": 70,
      "styleRecalcDurationMs": 24.865,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1189.391,
      "heapDeltaBytes": 8351080,
      "heapUsedBytes": 67273808,
      "domNodes": -259,
      "jsHeapTotalBytes": 1544192,
      "scriptDurationMs": 404.031,
      "eventListeners": -127,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2109.1709999999466,
      "styleRecalcs": 69,
      "styleRecalcDurationMs": 19.116999999999997,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1125.4679999999998,
      "heapDeltaBytes": -2734108,
      "heapUsedBytes": 57686880,
      "domNodes": -262,
      "jsHeapTotalBytes": 5242880,
      "scriptDurationMs": 389.93,
      "eventListeners": -129,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3215.6689999999912,
      "styleRecalcs": 66,
      "styleRecalcDurationMs": 20.078000000000003,
      "layouts": 60,
      "layoutDurationMs": 7.755999999999999,
      "taskDurationMs": 1383.2559999999999,
      "heapDeltaBytes": 9534072,
      "heapUsedBytes": 70091124,
      "domNodes": -268,
      "jsHeapTotalBytes": 5533696,
      "scriptDurationMs": 488.289,
      "eventListeners": -125,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.670000000000012,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3169.433999999967,
      "styleRecalcs": 64,
      "styleRecalcDurationMs": 18.319,
      "layouts": 60,
      "layoutDurationMs": 7.774999999999999,
      "taskDurationMs": 1388.645,
      "heapDeltaBytes": 10428000,
      "heapUsedBytes": 71514328,
      "domNodes": -271,
      "jsHeapTotalBytes": 1077248,
      "scriptDurationMs": 487.637,
      "eventListeners": -125,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3195.842999999968,
      "styleRecalcs": 66,
      "styleRecalcDurationMs": 19.311000000000003,
      "layouts": 60,
      "layoutDurationMs": 7.648999999999999,
      "taskDurationMs": 1385.426,
      "heapDeltaBytes": 9179076,
      "heapUsedBytes": 69941136,
      "domNodes": -270,
      "jsHeapTotalBytes": 5533696,
      "scriptDurationMs": 487.22,
      "eventListeners": -125,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "minimap-idle",
      "durationMs": 2018.1050000000482,
      "styleRecalcs": 7,
      "styleRecalcDurationMs": 7.226999999999998,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 605.664,
      "heapDeltaBytes": 7235596,
      "heapUsedBytes": 67882996,
      "domNodes": -264,
      "jsHeapTotalBytes": 552960,
      "scriptDurationMs": 104.477,
      "eventListeners": -129,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "minimap-idle",
      "durationMs": 2014.7939999999949,
      "styleRecalcs": 7,
      "styleRecalcDurationMs": 7.140000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 617.7479999999999,
      "heapDeltaBytes": 9490276,
      "heapUsedBytes": 70174064,
      "domNodes": -266,
      "jsHeapTotalBytes": -233472,
      "scriptDurationMs": 101.43999999999998,
      "eventListeners": -129,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "minimap-idle",
      "durationMs": 2010.1329999999962,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 7.796000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 606.722,
      "heapDeltaBytes": 10207116,
      "heapUsedBytes": 70256552,
      "domNodes": -265,
      "jsHeapTotalBytes": 28672,
      "scriptDurationMs": 102.885,
      "eventListeners": -129,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 540.5640000000176,
      "styleRecalcs": 46,
      "styleRecalcDurationMs": 10.454,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 355.476,
      "heapDeltaBytes": 9776868,
      "heapUsedBytes": 59478716,
      "domNodes": 17,
      "jsHeapTotalBytes": 15728640,
      "scriptDurationMs": 116.88099999999999,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000273
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 575.3450000000271,
      "styleRecalcs": 45,
      "styleRecalcDurationMs": 10.369,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 367.62600000000003,
      "heapDeltaBytes": 9068992,
      "heapUsedBytes": 57804868,
      "domNodes": 15,
      "jsHeapTotalBytes": 15204352,
      "scriptDurationMs": 118.229,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.663333333333338,
      "p95FrameDurationMs": 16.700000000000273
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 587.9740000000311,
      "styleRecalcs": 46,
      "styleRecalcDurationMs": 11.041000000000002,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 381.11599999999993,
      "heapDeltaBytes": 9147444,
      "heapUsedBytes": 57800412,
      "domNodes": 18,
      "jsHeapTotalBytes": 15466496,
      "scriptDurationMs": 122.169,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-idle",
      "durationMs": 1997.8710000000035,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 9.791,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 391.72900000000004,
      "heapDeltaBytes": 23209512,
      "heapUsedBytes": 71538352,
      "domNodes": 20,
      "jsHeapTotalBytes": 14942208,
      "scriptDurationMs": 20.394999999999996,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-idle",
      "durationMs": 2001.0199999999827,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 8.431999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 387.96400000000006,
      "heapDeltaBytes": 23263280,
      "heapUsedBytes": 72922952,
      "domNodes": 18,
      "jsHeapTotalBytes": 15204352,
      "scriptDurationMs": 18.701999999999998,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-idle",
      "durationMs": 1995.1800000000048,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 9.028999999999998,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 394.78000000000003,
      "heapDeltaBytes": 22531444,
      "heapUsedBytes": 71209752,
      "domNodes": 18,
      "jsHeapTotalBytes": 14680064,
      "scriptDurationMs": 18.914,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1715.079000000003,
      "styleRecalcs": 75,
      "styleRecalcDurationMs": 39.879,
      "layouts": 16,
      "layoutDurationMs": 5.435,
      "taskDurationMs": 724.4549999999999,
      "heapDeltaBytes": 14750684,
      "heapUsedBytes": 63330936,
      "domNodes": 60,
      "jsHeapTotalBytes": 15466496,
      "scriptDurationMs": 90.86799999999998,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1707.9709999999864,
      "styleRecalcs": 75,
      "styleRecalcDurationMs": 40.319,
      "layouts": 16,
      "layoutDurationMs": 5.383,
      "taskDurationMs": 776.895,
      "heapDeltaBytes": 2819340,
      "heapUsedBytes": 51130992,
      "domNodes": -263,
      "jsHeapTotalBytes": 14807040,
      "scriptDurationMs": 91.75500000000001,
      "eventListeners": -129,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1696.5549999999894,
      "styleRecalcs": 74,
      "styleRecalcDurationMs": 37.672,
      "layouts": 16,
      "layoutDurationMs": 4.521,
      "taskDurationMs": 781.313,
      "heapDeltaBytes": -1728432,
      "heapUsedBytes": 46903696,
      "domNodes": -262,
      "jsHeapTotalBytes": 15069184,
      "scriptDurationMs": 93.454,
      "eventListeners": -129,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 8176.755999999955,
      "styleRecalcs": 251,
      "styleRecalcDurationMs": 56.061,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 3991.099,
      "heapDeltaBytes": 15228816,
      "heapUsedBytes": 72283908,
      "domNodes": -262,
      "jsHeapTotalBytes": 3379200,
      "scriptDurationMs": 1317.651,
      "eventListeners": -113,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 8156.655999999998,
      "styleRecalcs": 249,
      "styleRecalcDurationMs": 55.157,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 4310.275,
      "heapDeltaBytes": 19462860,
      "heapUsedBytes": 77968320,
      "domNodes": -263,
      "jsHeapTotalBytes": 8622080,
      "scriptDurationMs": 1431.025,
      "eventListeners": -113,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 8153.9470000000165,
      "styleRecalcs": 249,
      "styleRecalcDurationMs": 55.980000000000004,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 4116.269,
      "heapDeltaBytes": 6125156,
      "heapUsedBytes": 65676020,
      "domNodes": -261,
      "jsHeapTotalBytes": 5767168,
      "scriptDurationMs": 1333.8129999999999,
      "eventListeners": -113,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 10870.069000000001,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 10835.408,
      "heapDeltaBytes": -34856612,
      "heapUsedBytes": 171916100,
      "domNodes": -8331,
      "jsHeapTotalBytes": 25227264,
      "scriptDurationMs": 601.039,
      "eventListeners": -16466,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333326,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 10925.185,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 10910.184000000001,
      "heapDeltaBytes": -25429316,
      "heapUsedBytes": 171758956,
      "domNodes": -8331,
      "jsHeapTotalBytes": 24178688,
      "scriptDurationMs": 574.9459999999999,
      "eventListeners": -16466,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.223333333333358,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 10846.221000000014,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 10831.283,
      "heapDeltaBytes": -43585892,
      "heapUsedBytes": 172058260,
      "domNodes": -8331,
      "jsHeapTotalBytes": 25227264,
      "scriptDurationMs": 599.408,
      "eventListeners": -16466,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 18.886666666666738,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 13245.985999999959,
      "styleRecalcs": 66,
      "styleRecalcDurationMs": 18.500000000000018,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 13211.814,
      "heapDeltaBytes": -30575716,
      "heapUsedBytes": 164922040,
      "domNodes": -8331,
      "jsHeapTotalBytes": -1773568,
      "scriptDurationMs": 959.067,
      "eventListeners": -16460,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.77333333333336,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 13225.94799999996,
      "styleRecalcs": 67,
      "styleRecalcDurationMs": 17.91000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 13187.295999999998,
      "heapDeltaBytes": -40406644,
      "heapUsedBytes": 175840184,
      "domNodes": -8331,
      "jsHeapTotalBytes": 237568,
      "scriptDurationMs": 922.4700000000001,
      "eventListeners": -16488,
      "totalBlockingTimeMs": 52,
      "frameDurationMs": 17.220000000000073,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 12867.91100000005,
      "styleRecalcs": 66,
      "styleRecalcDurationMs": 18.074000000000005,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 12846.504999999997,
      "heapDeltaBytes": -29326892,
      "heapUsedBytes": 165331992,
      "domNodes": -8331,
      "jsHeapTotalBytes": -3346432,
      "scriptDurationMs": 891.1719999999998,
      "eventListeners": -16462,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.776666666666642,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "workflow-execution",
      "durationMs": 476.1010000000283,
      "styleRecalcs": 20,
      "styleRecalcDurationMs": 25.002000000000002,
      "layouts": 5,
      "layoutDurationMs": 1.909,
      "taskDurationMs": 132.54599999999996,
      "heapDeltaBytes": 5336876,
      "heapUsedBytes": 56728376,
      "domNodes": 189,
      "jsHeapTotalBytes": 262144,
      "scriptDurationMs": 23.806999999999995,
      "eventListeners": 69,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000273
    },
    {
      "name": "workflow-execution",
      "durationMs": 465.1259999999411,
      "styleRecalcs": 16,
      "styleRecalcDurationMs": 24.176999999999996,
      "layouts": 5,
      "layoutDurationMs": 1.565,
      "taskDurationMs": 127.29499999999999,
      "heapDeltaBytes": 5033220,
      "heapUsedBytes": 56126976,
      "domNodes": 155,
      "jsHeapTotalBytes": 262144,
      "scriptDurationMs": 25.081000000000003,
      "eventListeners": 71,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000273
    },
    {
      "name": "workflow-execution",
      "durationMs": 459.25499999998465,
      "styleRecalcs": 17,
      "styleRecalcDurationMs": 21.356999999999996,
      "layouts": 4,
      "layoutDurationMs": 1.098,
      "taskDurationMs": 120.04200000000002,
      "heapDeltaBytes": 5060712,
      "heapUsedBytes": 54972736,
      "domNodes": 157,
      "jsHeapTotalBytes": 262144,
      "scriptDurationMs": 22.128999999999994,
      "eventListeners": 69,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.663333333333338,
      "p95FrameDurationMs": 16.700000000000273
    }
  ]
}

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

🤖 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/schemas/apiSchema.ts`:
- Around line 121-125: handleProgressText in src/stores/executionStore.ts
currently only gates progress_text by prompt_id, allowing execution_start from
another tab to leak previews when nodeIds overlap; update handleProgressText to
also check workflow_id from the incoming zProgressTextWsMessage against the
store's active workflow (the same gating used for progress and progress_state),
i.e., read workflow_id from the payload (zProgressTextWsMessage) and
short-circuit if it doesn't match the active workflow before applying text
updates, keeping existing prompt_id checks intact.

In `@src/stores/executionStore.ts`:
- Around line 608-619: reconcileTerminalJobs currently builds tracked from
nodeProgressStatesByJob and activeJobId, so jobs only present in
initializingJobIds never get checked; update reconcileTerminalJobs to also
include entries from initializingJobIds into the tracked Set before iterating
(use initializingJobIds.forEach to add each JobId), then proceed with the same
active/terminal checks and call evictTerminalJob(jobId) for matches; reference
reconcileTerminalJobs, initializingJobIds, nodeProgressStatesByJob, activeJobId,
and evictTerminalJob.
🪄 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: 68623f2b-0192-4fe2-a994-d5171333ad72

📥 Commits

Reviewing files that changed from the base of the PR and between 7abd9d1 and 82f1f91.

📒 Files selected for processing (7)
  • src/platform/remote/comfyui/useQueuePolling.test.ts
  • src/platform/remote/comfyui/useQueuePolling.ts
  • src/schemas/apiSchema.ts
  • src/stores/executionStore.test.ts
  • src/stores/executionStore.ts
  • src/stores/queueStore.test.ts
  • src/stores/queueStore.ts

Comment thread src/schemas/apiSchema.ts
Comment thread src/stores/executionStore.ts
- handleProgressText: gate by active workflow using workflow_id from
  the message, matching the gating already on progress and
  progress_state. Prevents text previews from a different tab leaking
  onto the active workflow when nodeIds overlap.
- reconcileTerminalJobs: union initializingJobIds into the tracked set
  so a cloud job that lands in history while still in the 'Waiting for
  a machine' state gets evicted via evictTerminalJob, clearing the
  stuck initializing indicator.
- Add tests for both: workflow_id mismatch/match on progress_text, and
  initializing-only job eviction.
@codecov
Copy link
Copy Markdown

codecov Bot commented May 3, 2026

Codecov Report

❌ Patch coverage is 93.75000% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/stores/executionStore.ts 93.27% 8 Missing ⚠️
@@             Coverage Diff             @@
##             main   #11866       +/-   ##
===========================================
- Coverage   71.21%   56.00%   -15.22%     
===========================================
  Files        1483     1383      -100     
  Lines       85606    70728    -14878     
  Branches    23055    19712     -3343     
===========================================
- Hits        60965    39610    -21355     
- Misses      23731    30591     +6860     
+ Partials      910      527      -383     
Flag Coverage Δ
e2e ?
unit 56.00% <93.75%> (+1.47%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/platform/remote/comfyui/useQueuePolling.ts 100.00% <100.00%> (ø)
src/schemas/apiSchema.ts 100.00% <100.00%> (ø)
src/stores/queueStore.ts 75.15% <100.00%> (-7.39%) ⬇️
src/stores/executionStore.ts 87.59% <93.27%> (-3.21%) ⬇️

... and 1012 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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/stores/executionStore.ts`:
- Around line 355-367: When workflowStore.activeWorkflow changes we must
reconcile the mirror so stale progress from a previous workflow doesn't persist;
update the logic around messageMatchesActiveWorkflow to, on active workflow
change, retrieve the canonical state from nodeProgressStatesByJob for the
current job/workflow and either clear nodeProgressStates and
_executingNodeProgress or rebuild them from that map. Specifically: in the
blocks using messageMatchesActiveWorkflow, add handling that looks up
nodeProgressStatesByJob[jobId] (or its workflow-specific entry) and sets
nodeProgressStates.value and _executingNodeProgress.value to the corresponding
canonical entries (or null/empty if none) whenever workflowStore.activeWorkflow
changes; apply the same reconciliation fix to the other block referenced (around
the 387-410 logic) so both incoming-event gating and manual tab switches produce
a correct mirror.
🪄 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: 6bb3bd03-a8ff-4ca7-94b8-891266cc817d

📥 Commits

Reviewing files that changed from the base of the PR and between 82f1f91 and 34b556c.

📒 Files selected for processing (2)
  • src/stores/executionStore.test.ts
  • src/stores/executionStore.ts

Comment thread src/stores/executionStore.ts Outdated
The incoming-event gate prevents new progress messages from leaking into
the wrong workflow, but if workflow A had already populated the mirror
and the user switches to B, the stale A progress would still render
until another event arrived. Add a watcher on workflowStore.activeWorkflow
that rebuilds the mirror from nodeProgressStatesByJob using the existing
workflow-id / session-path mappings, picking the most recently inserted
job for the new active workflow, or clears the mirror entirely when no
job belongs to it. Also clears _executingNodeProgress when its job no
longer matches the active workflow.
@christian-byrne christian-byrne requested a review from jaeone94 May 3, 2026 06:40
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 (1)
src/scripts/app.ts (1)

748-763: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't suppress node.onExecuted for background workflows without a replay path.

This early return prevents callback-driven UI state from ever being rebuilt for jobs started in another workflow tab. src/extensions/core/load3d.ts:477-510 is one concrete example: it wraps node.onExecuted to update the load3d preview, so those previews stay stale once the callback is skipped here. Persisting raw outputs is good, but this needs an activation-time replay/projector instead of dropping the callback entirely.

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

In `@src/scripts/app.ts` around lines 748 - 763, The early return using
useExecutionStore.messageMatchesActiveWorkflow(...) causes node.onExecuted
callbacks to be skipped for background workflows; instead, always invoke
getNodeByExecutionId(this.rootGraph, executionId) and, if node.onExecuted
exists, call node.onExecuted(detail.output) so callback-driven UI (e.g., load3d
preview wrappers) still update; if you need to avoid immediate UI updates for
non-active workflows, push the output and callback into a replay/projector queue
keyed by the workflow (implement a replay mechanism that replays queued
node.onExecuted calls on activation) rather than suppressing the callback in
this function.
🧹 Nitpick comments (1)
src/stores/executionStore.ts (1)

630-632: ⚡ Quick win

Clean job-to-workflow mappings during terminal eviction

evictTerminalJob() is now the central terminal cleanup path, but it leaves jobIdToWorkflowId (and optionally session path mapping) behind. Over long sessions, this causes unnecessary map growth.

Proposed fix
   function evictTerminalJob(jobId: JobId) {
     if (!jobId) return
     ...
     useJobPreviewStore().clearPreview(jobId)
     clearInitializationByJobId(jobId)
+    jobIdToWorkflowId.value.delete(jobId)
+    if (jobIdToSessionWorkflowPath.value.has(jobId)) {
+      const next = new Map(jobIdToSessionWorkflowPath.value)
+      next.delete(jobId)
+      jobIdToSessionWorkflowPath.value = next
+    }

     const isActive = activeJobId.value === jobId
     const mirrorBelongsToEvicted = mirrorOwnerJobId() === jobId
     ...
   }

Based on learnings: prompt IDs are UUIDs and never reused, so pruning terminal-job mappings is safe and prevents monotonic growth.

Also applies to: 648-652

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

In `@src/stores/executionStore.ts` around lines 630 - 632, evictTerminalJob()
currently calls useJobPreviewStore().clearPreview(jobId) and
clearInitializationByJobId(jobId) but does not remove the jobId from the
jobIdToWorkflowId map (and related session-path mappings), causing unbounded map
growth; update evictTerminalJob() to also remove the job from jobIdToWorkflowId
(jobIdToWorkflowId.delete(jobId)) and remove any associated session/path mapping
(e.g., delete from jobId->sessionPath or sessionPath->jobId maps) in the same
cleanup path (also apply the same deletion logic in the other cleanup site
referenced around the other eviction block) so terminal evictions fully prune
job-related mappings.
🤖 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/app.ts`:
- Around line 801-819: When a workflow becomes active we need to rehydrate
cached previews from the job preview store into the node-output store; update
reconcileMirrorForActiveWorkflow() (or the activation path that checks
useExecutionStore().messageMatchesActiveWorkflow(jobId, workflowId)) to, after
confirming the workflow is active, read cached previews from
useJobPreviewStore().previewsByPromptId (or the job preview lookup used by
setPreviewUrl(jobId, blobUrl, displayNodeId)) and call
setNodePreviewsByExecutionId(...) for the appropriate displayNodeId/node parent
chain (and call revokePreviewsByExecutionId if needed) so the canvas-visible
previews are restored when switching back to a workflow that previously received
previews.

In `@src/stores/executionStore.ts`:
- Around line 351-356: The loop that calls revokePreviewsByExecutionId for nodes
in 'running' state can revoke previews from executions that belong to a
different active workflow; fetch the current active workflow id from the
workflow store (e.g. via useWorkflowStore()/getActiveWorkflowId() or the
existing activeWorkflow variable) and add a guard before calling
revokePreviewsByExecutionId(nodeId) so it only runs when nodeState.workflowId
(or the appropriate ownership field on nodeState) matches the active workflow
id; apply the same ownership check to the other similar block (the one covering
lines ~365-377) to prevent cross-workflow revocations.

---

Outside diff comments:
In `@src/scripts/app.ts`:
- Around line 748-763: The early return using
useExecutionStore.messageMatchesActiveWorkflow(...) causes node.onExecuted
callbacks to be skipped for background workflows; instead, always invoke
getNodeByExecutionId(this.rootGraph, executionId) and, if node.onExecuted
exists, call node.onExecuted(detail.output) so callback-driven UI (e.g., load3d
preview wrappers) still update; if you need to avoid immediate UI updates for
non-active workflows, push the output and callback into a replay/projector queue
keyed by the workflow (implement a replay mechanism that replays queued
node.onExecuted calls on activation) rather than suppressing the callback in
this function.

---

Nitpick comments:
In `@src/stores/executionStore.ts`:
- Around line 630-632: evictTerminalJob() currently calls
useJobPreviewStore().clearPreview(jobId) and clearInitializationByJobId(jobId)
but does not remove the jobId from the jobIdToWorkflowId map (and related
session-path mappings), causing unbounded map growth; update evictTerminalJob()
to also remove the job from jobIdToWorkflowId (jobIdToWorkflowId.delete(jobId))
and remove any associated session/path mapping (e.g., delete from
jobId->sessionPath or sessionPath->jobId maps) in the same cleanup path (also
apply the same deletion logic in the other cleanup site referenced around the
other eviction block) so terminal evictions fully prune job-related mappings.
🪄 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: 2f44898b-c399-4d51-b904-778d003b8690

📥 Commits

Reviewing files that changed from the base of the PR and between 34b556c and 9ebab86.

📒 Files selected for processing (4)
  • src/scripts/api.ts
  • src/scripts/app.ts
  • src/stores/executionStore.test.ts
  • src/stores/executionStore.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/stores/executionStore.test.ts

Comment thread src/scripts/app.ts Outdated
Comment thread src/stores/executionStore.ts Outdated
handleProgressState was calling revokePreviewsByExecutionId before the
active-workflow gate, so a progress_state from a non-active workflow
could still revoke previews on the active canvas when nodeIds collided.
Hoist the active-workflow check above the revocation loop so previews
are only touched when the message belongs to the active tab.
@glary-bot glary-bot Bot force-pushed the glary/fix-stuck-progress-eviction branch from 9ebab86 to 7f48b33 Compare May 3, 2026 06:44
@christian-byrne christian-byrne marked this pull request as ready for review May 3, 2026 06:51
@christian-byrne christian-byrne requested a review from a team May 3, 2026 06:51
@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label May 3, 2026
Copy link
Copy Markdown
Collaborator

@jaeone94 jaeone94 left a comment

Choose a reason for hiding this comment

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

Thanks for the thorough write-up and the test coverage here. I think the overall direction is right: scoping execution UI state by the active workflow and adding polling-based terminal reconciliation should address the stuck-progress class of bugs.

That said, a few execution event paths still appear to bypass the new workflow ownership checks or still use the older global reset behavior. Those gaps seem likely to preserve some of the cross-workflow leakage this PR is trying to remove, especially for executed, terminal WS events, executing, and progress_text.
One adjacent path worth checking as well: src/scripts/app.ts:766 still calls onExecutionStart on the currently visible graph without workflow gating. it may still allow background workflow starts to trigger callbacks on the wrong canvas.

Comment thread src/scripts/app.ts Outdated
const nodeOutputStore = useNodeOutputStore()
const executionId = String(detail.display_node || detail.node)

// Always persist outputs into the (job-scoped) node-output store so
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Issue: this write still happens before the active-workflow gate, but nodeOutputStore.setNodeOutputsByExecutionId() resolves the execution id against the current app.rootGraph. If a background workflow emits executed for a colliding node id, it can still overwrite the visible workflow's outputs even though node.onExecuted is gated below. Could this write either be gated as well, or routed through a genuinely job/workflow-scoped output store?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Agreed this is real. The executed write into nodeOutputStore resolving against app.rootGraph is a parallel cross-workflow leak, alongside other consumers of the global mirror (group nodes, minimap, GraphCanvas, useNodeExecutionState, browser tab title, litegraph stroke styles).

Out of scope for this PR — app.ts is not modified here, and properly fixing it requires either (a) gating writes in app.ts and rehydrating on tab switch, or (b) routing all node-output access through a job-keyed store and changing all 8 consumers. Both are sizable refactors that I deliberately scoped out per the original design discussion (where we picked the focused gate-in-executionStore approach over the UI-wide refactor).

I'll file a follow-up issue covering: executed output gating, canvas preview rehydration on tab switch, and consumer migration to per-job state. Happy to take that as a separate PR.

}

/**
* Safely evict per-job execution artifacts for a job that has reached a
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Issue: this new safe eviction primitive is exactly the behavior needed for non-active terminal jobs, but the WS terminal handlers still call resetExecutionState(prompt_id). That older path clears the global mirror, _executingNodeProgress, activeJobId, and queuedJobs[activeJobId] even when the terminal prompt_id is not the active job. If job A finishes after job B has become active, A's success event can wipe B's execution state.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Excellent catch — this was a real cross-workflow clobbering bug. Fixed in 452ef58.

Added a small terminateJob(jobId) helper that routes to evictTerminalJob for non-active jobs (designed to be safe for any jobId, never touches another running job's mirror) and to resetExecutionState only when jobId === activeJobId. Applied at every WS terminal call site: handleExecutionSuccess, handleExecutionInterrupted, handleExecutionError, handleServiceLevelError, handleCloudValidationError.

Three regression tests added in terminal WS handlers do not clobber active job: success on a non-active job preserves the active job's mirror/activeJobId, interrupted on a non-active job same, success on the active job clears as before.

Comment thread src/stores/queueStore.ts
executionStore.reconcileInitializingJobs(activeJobIds)
}

// Reconcile terminal jobs whenever history is non-empty. The last
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Issue: fetching queue and history concurrently can still miss the recovery case this PR is targeting. If getHistory() returns before the finishing job appears in history while getQueue() returns after it disappeared from Running/Pending, reconciliation sees the job in neither set; then activeJobsCount becomes 0 and polling stops, leaving dropped-terminal progress stuck. A follow-up history fetch or continued polling when tracked progress exists but no terminal id was seen would close this race.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Real edge case — appreciated. The race window is bounded (backend writes ordering between queue and history are short) but not zero. The robust fix is what you suggested: keep polling while there's tracked progress that hasn't been seen in either set, regardless of activeJobsCount.

I'll address this in a small follow-up rather than expanding this PR further: change useQueuePolling activation to activeJobsCount > 0 || executionStore.nodeProgressStatesByJob has untracked entries, plus an explicit "saw queue empty + history empty + still tracked" continued-poll path. That keeps this PR focused on the WS schema + eviction + workflow gating bundle that's already grown larger than originally planned.

In the meantime, the practical mitigation is that the next real WS event (heartbeat, status, etc.) re-triggers polling via GraphView.vue, so the recovery latency in the worst case is bounded by the time to the next heartbeat rather than indefinite.

Comment thread src/schemas/apiSchema.ts
const zExecutingWsMessage = z.object({
node: zNodeId,
display_node: zNodeId,
prompt_id: zJobId
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggestion: since executing now carries workflow_id in the schema, the API dispatch path should preserve that metadata too. It currently dispatches only display_node || node, so downstream listeners cannot apply the same active-workflow gate as the other execution events and background executions can still clear active node progress or trigger redraws for the visible canvas.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Right — the schema now describes a richer executing payload but api.ts still strips it down to display_node || node at line 731-734, and the type system at line 218-220 hard-codes executing to NodeId. Plumbing the full payload through requires changes in api.ts (dispatch + ApiToEventType), the handleExecuting handler signature, and existing test mocks. That's a wider structural change than I want to bundle into this PR (which is already covering schema + eviction + active-workflow gating).

Out of scope here, but I'll include this in the follow-up issue alongside the executed write gating (3178588877) and the queue/history race (3178588879). The current handleExecuting is partially safe today because it only mutates activeJob.value.nodes (which is gated through activeJobId indirection) — but you're correct that listeners can't apply the new workflow gate downstream until the dispatch carries the metadata. Tracked for the follow-up.

Comment thread src/stores/executionStore.ts Outdated
const { nodeId, text, prompt_id, workflow_id } = e.detail
if (!text || !nodeId) return

// Filter: only accept progress for the active prompt
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Issue: this workflow ownership check runs after the legacy activeJobId prompt guard. Since activeJobId is still global and can point at another workflow's latest job, progress_text for the currently visible workflow can be dropped even when workflow_id matches the active workflow. Suggestion: prefer the workflow gate when metadata is available, and only fall back to the legacy activeJobId guard when ownership cannot be resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fixed in 452ef58. handleProgressText now prefers the workflow-ownership gate when ownership can be resolved (any of: workflow_id on the message, jobIdToWorkflowId mapping, jobIdToSessionWorkflowPath mapping). Only falls back to the legacy activeJobId !== prompt_id guard when ownership is genuinely unresolvable, so progress_text for the visible workflow won't be dropped just because activeJobId happens to point at another workflow's job.

@jaeone94 jaeone94 assigned christian-byrne and unassigned jaeone94 May 3, 2026
…jobs

The WS terminal handlers (execution_success, execution_error,
execution_interrupted, plus the cloud validation and service-level
error paths) all called resetExecutionState(prompt_id) directly. That
function unconditionally clears the global mirror, _executingNodeProgress,
activeJobId, and queuedJobs[activeJobId.value], which means a terminal
event for a non-active job (e.g. job A finishing after job B becomes
active) wipes the active job's UI state.

Add a small terminateJob(jobId) helper that routes to evictTerminalJob
for non-active jobs (already designed to be safe for any jobId) and to
resetExecutionState only when jobId === activeJobId. Apply at every WS
terminal call site.

Also reorder the gating in handleProgressText: prefer the active-workflow
gate when ownership can be resolved (workflow_id on the message or a
registered mapping), and only fall back to the legacy activeJobId guard
when ownership is unresolvable. The previous order would drop legitimate
text previews for the visible workflow when activeJobId pointed at a
different workflow's job.
@glary-bot glary-bot Bot force-pushed the glary/fix-stuck-progress-eviction branch from 17c6807 to 452ef58 Compare May 3, 2026 19:49
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.

🧹 Nitpick comments (2)
src/stores/executionStore.ts (1)

353-360: 💤 Low value

Consider hoisting useNodeOutputStore() outside the loop.

While functionally correct (Pinia stores are singletons), calling useNodeOutputStore() and destructuring inside the loop on every iteration is slightly wasteful.

♻️ Suggested refactor
   if (isActiveWorkflowMessage) {
+    const { revokePreviewsByExecutionId } = useNodeOutputStore()
     for (const nodeId in nodes) {
       const nodeState = nodes[nodeId]
       if (nodeState.state === 'running' && !previousForJob[nodeId]) {
-        const { revokePreviewsByExecutionId } = useNodeOutputStore()
         revokePreviewsByExecutionId(nodeId)
       }
     }
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stores/executionStore.ts` around lines 353 - 360, The loop calls
useNodeOutputStore() and destructures revokePreviewsByExecutionId on every
iteration which is wasteful; hoist the store lookup and destructuring outside
the for-loop: obtain const { revokePreviewsByExecutionId } =
useNodeOutputStore() once before iterating over nodes (while keeping the same
guard isActiveWorkflowMessage and checks for nodeState.state === 'running' &&
!previousForJob[nodeId]) and then call revokePreviewsByExecutionId(nodeId)
inside the loop.
src/stores/executionStore.test.ts (1)

86-96: 💤 Low value

Remove dead mock for @/stores/imagePreviewStore.

Lines 86-90 mock @/stores/imagePreviewStore exporting useNodeOutputStore, but executionStore.ts imports useNodeOutputStore from @/stores/nodeOutputStore (line 32). The correct mock at lines 92-96 supersedes this one. The dead mock is confusing and could mask future import changes.

♻️ Suggested removal
-vi.mock('@/stores/imagePreviewStore', () => ({
-  useNodeOutputStore: () => ({
-    revokePreviewsByExecutionId: vi.fn()
-  })
-}))
-
 vi.mock('@/stores/nodeOutputStore', () => ({
   useNodeOutputStore: () => ({
     revokePreviewsByExecutionId: mockRevokePreviewsByExecutionId
   })
 }))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stores/executionStore.test.ts` around lines 86 - 96, Remove the dead mock
block that defines useNodeOutputStore returning a revokePreviewsByExecutionId
vi.fn(); it is superseded by the working mock that uses
mockRevokePreviewsByExecutionId and is confusing. Locate the mock that returns {
useNodeOutputStore: () => ({ revokePreviewsByExecutionId: vi.fn() }) } and
delete that entire mock declaration so only the correct mock referencing
mockRevokePreviewsByExecutionId remains.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/stores/executionStore.test.ts`:
- Around line 86-96: Remove the dead mock block that defines useNodeOutputStore
returning a revokePreviewsByExecutionId vi.fn(); it is superseded by the working
mock that uses mockRevokePreviewsByExecutionId and is confusing. Locate the mock
that returns { useNodeOutputStore: () => ({ revokePreviewsByExecutionId: vi.fn()
}) } and delete that entire mock declaration so only the correct mock
referencing mockRevokePreviewsByExecutionId remains.

In `@src/stores/executionStore.ts`:
- Around line 353-360: The loop calls useNodeOutputStore() and destructures
revokePreviewsByExecutionId on every iteration which is wasteful; hoist the
store lookup and destructuring outside the for-loop: obtain const {
revokePreviewsByExecutionId } = useNodeOutputStore() once before iterating over
nodes (while keeping the same guard isActiveWorkflowMessage and checks for
nodeState.state === 'running' && !previousForJob[nodeId]) and then call
revokePreviewsByExecutionId(nodeId) inside the loop.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5cc5a8d3-b84c-4f2b-946f-4d756c1bf8c0

📥 Commits

Reviewing files that changed from the base of the PR and between 17c6807 and 452ef58.

📒 Files selected for processing (2)
  • src/stores/executionStore.test.ts
  • src/stores/executionStore.ts

- Hoist useNodeOutputStore() destructure outside the per-node loop in
  handleProgressState so the store lookup happens once per message
  instead of once per running node.
- Remove the dead vi.mock for @/stores/imagePreviewStore (executionStore
  imports useNodeOutputStore from @/stores/nodeOutputStore; the second
  mock is the live one).
@glary-bot
Copy link
Copy Markdown

glary-bot Bot commented May 3, 2026

Both nitpicks addressed in 4d9011b:

  • Hoisted const { revokePreviewsByExecutionId } = useNodeOutputStore() outside the per-node loop in handleProgressState.
  • Removed the dead vi.mock('@/stores/imagePreviewStore', ...) block; only the correct @/stores/nodeOutputStore mock remains.

Copy link
Copy Markdown
Contributor Author

@christian-byrne christian-byrne left a comment

Choose a reason for hiding this comment

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

executionStore.reconcileTerminalJobs(active, terminal) — new polling-driven reconciliation. Walks tracked jobs, evicts any job that is in the terminal set and not in the active set. Internally uses evictTerminalJob, which always removes per-job state but only clears the global mirror / activeJobId / _executingNodeProgress when the evicted job actually owns them (mirror ownership identified via prompt_id on NodeProgressState since the mirror is replaced wholesale per message). Idempotent with the WS terminal handlers.

There is 0% chance that a polling based approach is the move here. We need to have singular state and have the eviction be a watcher or watchEffect.

* {@link activeJobId}) only when those still belong to the evicted job.
* - Idempotent: calling for an already-cleared job is a no-op.
*/
function evictTerminalJob(jobId: JobId) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

How about this for design:

  • when we store progress state, keep the job id (promptId) in the ref/local data.
  • expose a finished jobs ref from the queue or history store, or wherever
  • watch the finished jobs ref in the execution store or in respective useNodeProgressState

This current set up obvioulsy is not good.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Fair — the imperative evictTerminalJob / reconcileTerminalJobs path I added is fundamentally an external "go scrub stale state" pattern, which is the opposite of how the rest of executionStore works (events come in, state mutates locally). Reactive design is cleaner.

Outline of the redesign as I read your comment:

  1. Tag progress data with promptId at write time. nodeProgressStatesByJob already does this implicitly via the outer key, and NodeProgressState.prompt_id is on every entry. So the data is already promptId-aware; we don't need to add storage.

  2. Expose finishedJobs reactively from queueStore (or a new derived store). Computed from historyTasks (the source of truth for terminal state), keyed by jobId, so consumers can subscribe to "this jobId just became terminal" without writing imperative reconciliation loops.

  3. Watch from executionStore (or useNodeProgressState). A single watcher on finishedJobs — when a tracked promptId enters that set, run the same per-job cleanup that evictTerminalJob does today (delete from nodeProgressStatesByJob, conditionally clear mirror, clear preview, clear initializing). Same eviction primitive, reactive trigger.

This collapses two of the things people complained about in the current PR:

  • No more "polling fires reconcileTerminalJobs which iterates all tracked jobs" — just a watch on the derived diff
  • No more queueStore.update() knowing about executionStore's internals

I'll move this work to a new PR with that shape (per your split suggestion). Will keep the evictTerminalJob logic in spirit but expose it as the watcher's reaction rather than as a polling-callable export. Tests will cover: terminal arrival via WS (current happy path), terminal arrival only via history (the dropped-WS recovery case), and the multi-job-cross-workflow permutations.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I sort of want to separate this into two PRs:

  1. First PR just for adding the workflow_id in the schema and adding the logic to filter progress events and UI side effects by workflow_id. Add tests e2e and unit and permutations
  2. Second PR for the eventual consistency fallback handling (basically the queue polling eventual consistency backup but applied to porgress state messages). Also needs e2e, unit, and permutation tests. Of course, apply my review comments when you move that code to a new PR.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Agreed — splitting cleanly along that line makes sense, and would have been the right call from the start. Plan:

PR 1 (this branch, rescope): schema-level workflow_id addition + active-workflow gating in executionStore + tab-switch mirror reconciliation. Drop the polling/eviction commits. Tests stay focused on gating semantics.

PR 2 (new branch): queue-polling eventual-consistency fallback for stuck progress, redesigned per your other comment (#3183431959 — store promptId with progress state, expose finishedJobs reactively from queue/history store, watch from execution store). E2E + permutation tests included.

Going to wait for confirmation from the team before rewriting branch history (this PR has been through several review rounds and has people referencing specific commits). If you'd prefer a clean redo, happy to close this and open the two new PRs from scratch.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants