Skip to content

fix: detect painter canvas emptiness from pixel data, not stroke flags#12009

Open
christian-byrne wants to merge 2 commits intomainfrom
fix/painter-serialize-preserves-modelvalue-on-remount
Open

fix: detect painter canvas emptiness from pixel data, not stroke flags#12009
christian-byrne wants to merge 2 commits intomainfrom
fix/painter-serialize-preserves-modelvalue-on-remount

Conversation

@christian-byrne
Copy link
Copy Markdown
Contributor

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

Summary

Painter widget no longer fires POST /upload/image on queue: serializeValue returns '' even when the canvas has visible strokes. Backend then receives mask="" (locally) or surfaces ImageDownloadError: Failed to validate images on cloud (cloud rejects empty mask paths since cloud PR #2946 / commit d06857ba0).

Root cause

isCanvasEmpty() returned !hasStrokes, where hasStrokes is a closure-local let inside usePainter(). That flag desyncs from the actual canvas content under at least two confirmed conditions:

  1. WidgetPainter remount. usePainter() re-runs in setup, producing a brand-new closure with hasStrokes = false. registerWidgetSerialization() in onMounted overwrites litegraphNode.widgets[mask].serializeValue with the new closure's version. The next serializeValue call sees hasStrokes=false even though the user already painted, so it returns ''.
  2. Non-primary pointer events. handlePointerDown early-returns on e.button !== 0. For some touchpad/pen pointer-event variants observed in the wild, pointerdown fires with e.button !== 0, so startStroke never runs and hasStrokes never flips. Pixels can still reach the canvas through other paths, leaving the flag desynced.

Confirmed via Matt Miller's console diagnostic on prod 2026-05-05: he painted strokes (~9% non-zero alpha pixels), clicked Run, and serializeValue returned "" while mask.value stayed "" throughout.

Fix

Replace the flag-based isCanvasEmpty() check with a pixel-data scan:

function isCanvasPixelEmpty(el: HTMLCanvasElement): boolean {
  const ctx = el.getContext('2d')
  if (!ctx) return true
  const { data } = ctx.getImageData(0, 0, el.width, el.height)
  for (let i = 3; i < data.length; i += 4) {
    if (data[i] !== 0) return false
  }
  return true
}

async function serializeValue(): Promise<string> {
  const el = canvasEl.value
  if (!el) return modelValue.value

  if (isCanvasPixelEmpty(el)) return modelValue.value

  if (!isDirty.value && modelValue.value) return modelValue.value
  // …upload
}

Behaviour matrix:

Scenario canvas pixels isDirty modelValue Returned
Fresh painter, never painted, queued empty false '' ''
Painted, queued (happy path) non-empty true anything upload result
Painted, queued, but hasStrokes=false due to remount or non-primary pointerdown non-empty false '' upload result (the regression)
Workflow loaded, queued before image-restore empty false painter/x.png [temp] painter/x.png [temp]
Painted, then cleared, then queued empty true '' (cleared by handleClear) ''

handleClear now also resets modelValue.value = '' so a user-initiated clear still resolves to an empty mask even though the pixel-data check would otherwise defer to the cached upload reference.

The hasStrokes flag and the isCanvasEmpty helper that read it are now dead code and have been removed.

Why this is the right fix (not just defense in depth)

The earlier iteration of this PR reordered the short-circuits to return modelValue.value when !isDirty.value. That fix handles scenario 4 (workflow-restored mask reference) but fails the exact case the user actually reported: fresh node, painted strokes, hasStrokes=false because of the desync above, modelValue.value=''. The pixel-data scan addresses the real failure mode.

Investigation: which PR introduced the regression?

I ran a parallel-subagent investigation across the rendering and reactivity paths between v1.42.15 and main. None of the suspected PRs (#10966, #10741, #10302/#10309, #11423, #11613, #11691, #11541) showed a verifiable change that increases WidgetPainter remount frequency. Specifically:

A synthetic Vitest reproducer that mutates nodeData.widgets reactively does not remount WidgetPainter on current main, so the trigger is empirical and would surface only in a real browser session. Given that the second condition (non-primary pointerdown) was added in #8521 and has been latent forever, the user-visible regression is most plausibly the touchpad/pen pointer-event class of bug, not a remount-frequency change. Either way, the pixel-data fix in this PR addresses both failure modes simultaneously.

Verification

pnpm exec vitest run src/composables/painter/usePainter.test.ts → 29 passed
pnpm typecheck → 0 errors
pnpm exec eslint src/composables/painter/ → 0 errors
pnpm knip → 0 unused (1 unrelated tag hint)
pnpm format → applied

Three new tests added under describe('serializeValue'):

  • uploads canvas content even when the isDirty flag is false (regression: stroke-tracking flag can desync from real canvas pixel data on remount or non-primary pointerdown)
  • returns empty string when canvas has no pixels and modelValue is empty
  • returns empty string after handleClear even when modelValue previously held an upload reference

…ount

The painter's serializeValue closure captured hasStrokes and isDirty as
local variables that reset on every WidgetPainter.vue mount. After a
remount (e.g. NodeWidgets re-render driven by useProcessedWidgets, added
in #10966), serializeValue saw isCanvasEmpty()=true and returned '' even
when modelValue still held a valid mask reference from a workflow load
or a prior upload — backend then received an empty mask, and on cloud
this surfaced as ImageDownloadError.

Reorder the short-circuits so a non-dirty painter returns its existing
modelValue.value before the canvas-empty guard, and fall back to the
prior modelValue.value if the canvas element is no longer mounted.
@christian-byrne christian-byrne requested a review from a team May 6, 2026 03:38
@christian-byrne christian-byrne requested a review from jtydhr88 as a code owner May 6, 2026 03:38
@dosubot dosubot Bot added the size:XS This PR changes 0-9 lines, ignoring generated files. label May 6, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 6, 2026

📝 Walkthrough

Walkthrough

serializeValue now uses a pixel-based emptiness check and short-circuits to the existing modelValue when the canvas is missing or empty; handleClear clears modelValue; tests add a regression ensuring remounted painters preserve restored mask references.

Changes

Painter serialization & emptiness detection

Layer / File(s) Summary
New helper (emptiness)
src/composables/painter/usePainter.ts
Adds isCanvasPixelEmpty(el: HTMLCanvasElement): boolean which inspects canvas pixel data to determine visual emptiness.
Core serialization logic
src/composables/painter/usePainter.ts
serializeValue() reordered: returns modelValue if canvas element missing or pixel-empty; retains not-dirty short-circuit to avoid blanking restored values.
Clear behavior
src/composables/painter/usePainter.ts
handleClear() now clears modelValue.value = '' (replacing previous hasStrokes = false reset) to avoid resurrecting uploaded masks.
Tests / Regression
src/composables/painter/usePainter.test.ts
Adds makeFakeCanvas test helper and a regression test that mounts with an existing modelValue and nodeId and asserts serializeValue returns the existing modelValue; removes prior test that expected an empty string when modelValue existed.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

I nibble lines and pixels small,
A canvas hush, then not at all—
Restored masks kept, no blanking fright,
Brushes safe through remount night. 🐰🎨


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:" in title (bug-fix signal), doesn't change browser_tests/, and lacks explanation for missing e2e tests. Add or update Playwright regression test under browser_tests/ for mask preservation on remount, or add concrete explanation in PR description of why end-to-end testing isn't practical.
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% 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 describes the main change: replacing stroke-flag-based detection with pixel-data-based emptiness detection for the painter canvas.
Description check ✅ Passed The description comprehensively covers the summary, root causes, fix with code samples, behavior matrix, and verification steps against the required template sections.
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 modifies src/composables/painter/ only. ADR compliance checks apply only to src/lib/litegraph/, src/ecs/, or graph entity-related files. This PR does not touch any ADR-applicable paths.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/painter-serialize-preserves-modelvalue-on-remount

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

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

🎨 Storybook: ✅ Built — View Storybook

Details

⏰ Completed at: 05/06/2026, 07:21:34 AM UTC

Links

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 6, 2026

🎭 Playwright: ✅ 1529 passed, 0 failed · 3 flaky

📊 Browser Reports
  • chromium: View Report (✅ 1510 / ❌ 0 / ⚠️ 3 / ⏭️ 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 6, 2026

📦 Bundle: 5.26 MB gzip 🔴 +292 B

Details

Summary

  • Raw size: 24.2 MB baseline 24.2 MB — 🔴 +463 B
  • Gzip: 5.26 MB baseline 5.26 MB — 🔴 +292 B
  • Brotli: 4.07 MB baseline 4.07 MB — 🔴 +360 B
  • Bundles: 259 current • 259 baseline • 118 added / 118 removed

Category Glance
Other 🔴 +463 B (8.86 MB) · Vendor & Third-Party ⚪ 0 B (9.94 MB) · Data & Services ⚪ 0 B (3.05 MB) · Graph Workspace ⚪ 0 B (1.24 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-DIdK7yRD.js (new) 22.6 kB 🔴 +22.6 kB 🔴 +8.01 kB 🔴 +6.88 kB
assets/index-DxhLU2ri.js (removed) 22.6 kB 🟢 -22.6 kB 🟢 -8.01 kB 🟢 -6.88 kB

Status: 1 added / 1 removed

Graph Workspace — 1.24 MB (baseline 1.24 MB) • ⚪ 0 B

Graph editor runtime, canvas, workflow orchestration

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

Status: 1 added / 1 removed

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

Top-level views, pages, and routed surfaces

File Before After Δ Raw Δ Gzip Δ Brotli
assets/CloudSurveyView-CC2c5G_c.js (new) 19.6 kB 🔴 +19.6 kB 🔴 +5.14 kB 🔴 +4.57 kB
assets/CloudSurveyView-DoQ_6knU.js (removed) 19.6 kB 🟢 -19.6 kB 🟢 -5.14 kB 🟢 -4.57 kB
assets/CloudLoginView-BCf5XQoE.js (removed) 12.4 kB 🟢 -12.4 kB 🟢 -3.51 kB 🟢 -3.1 kB
assets/CloudLoginView-BNSTaVPx.js (new) 12.4 kB 🔴 +12.4 kB 🔴 +3.51 kB 🔴 +3.09 kB
assets/CloudSignupView-BOloNUxP.js (removed) 10.2 kB 🟢 -10.2 kB 🟢 -3 kB 🟢 -2.65 kB
assets/CloudSignupView-OwF3TCmE.js (new) 10.2 kB 🔴 +10.2 kB 🔴 +3.01 kB 🔴 +2.65 kB
assets/UserCheckView-BcWl713Z.js (new) 9.07 kB 🔴 +9.07 kB 🔴 +2.33 kB 🔴 +2.04 kB
assets/UserCheckView-lbIjmbWR.js (removed) 9.07 kB 🟢 -9.07 kB 🟢 -2.33 kB 🟢 -2.05 kB
assets/CloudLayoutView-Copa-kSQ.js (new) 7.73 kB 🔴 +7.73 kB 🔴 +2.45 kB 🔴 +2.14 kB
assets/CloudLayoutView-Okg5m98E.js (removed) 7.73 kB 🟢 -7.73 kB 🟢 -2.45 kB 🟢 -2.13 kB
assets/CloudForgotPasswordView-BU_4eLKz.js (new) 6.14 kB 🔴 +6.14 kB 🔴 +2.18 kB 🔴 +1.95 kB
assets/CloudForgotPasswordView-CvXFa1_6.js (removed) 6.14 kB 🟢 -6.14 kB 🟢 -2.18 kB 🟢 -1.94 kB
assets/CloudAuthTimeoutView-93MuH2Op.js (removed) 5.5 kB 🟢 -5.5 kB 🟢 -2.01 kB 🟢 -1.78 kB
assets/CloudAuthTimeoutView-B84h6oKm.js (new) 5.5 kB 🔴 +5.5 kB 🔴 +2.01 kB 🔴 +1.76 kB
assets/CloudSubscriptionRedirectView-C5TSPzD1.js (removed) 5.28 kB 🟢 -5.28 kB 🟢 -2 kB 🟢 -1.77 kB
assets/CloudSubscriptionRedirectView-Cp8-hYyC.js (new) 5.28 kB 🔴 +5.28 kB 🔴 +2 kB 🔴 +1.78 kB
assets/UserSelectView-BMGtuyhI.js (new) 4.73 kB 🔴 +4.73 kB 🔴 +1.75 kB 🔴 +1.55 kB
assets/UserSelectView-BPtVpA8K.js (removed) 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-BQFCsXo3.js (new) 46.8 kB 🔴 +46.8 kB 🔴 +9.65 kB 🔴 +8.58 kB
assets/KeybindingPanel-DoCO6h-5.js (removed) 46.8 kB 🟢 -46.8 kB 🟢 -9.65 kB 🟢 -8.58 kB
assets/SecretsPanel-AeVlq7CI.js (removed) 22.9 kB 🟢 -22.9 kB 🟢 -5.54 kB 🟢 -4.88 kB
assets/SecretsPanel-B3xq6QcH.js (new) 22.9 kB 🔴 +22.9 kB 🔴 +5.55 kB 🔴 +4.88 kB
assets/LegacyCreditsPanel-B8IsXZRC.js (new) 21.7 kB 🔴 +21.7 kB 🔴 +5.91 kB 🔴 +5.21 kB
assets/LegacyCreditsPanel-CJRGf5iI.js (removed) 21.7 kB 🟢 -21.7 kB 🟢 -5.91 kB 🟢 -5.21 kB
assets/SubscriptionPanel-CDM3-wYC.js (new) 20 kB 🔴 +20 kB 🔴 +5.11 kB 🔴 +4.51 kB
assets/SubscriptionPanel-DFLw5cSs.js (removed) 20 kB 🟢 -20 kB 🟢 -5.11 kB 🟢 -4.5 kB
assets/AboutPanel-CGXoYR5R.js (new) 12 kB 🔴 +12 kB 🔴 +3.32 kB 🔴 +2.98 kB
assets/AboutPanel-o4keE4hW.js (removed) 12 kB 🟢 -12 kB 🟢 -3.32 kB 🟢 -2.98 kB
assets/ExtensionPanel-CRiweJo8.js (new) 9.97 kB 🔴 +9.97 kB 🔴 +2.91 kB 🔴 +2.6 kB
assets/ExtensionPanel-D-OlswId.js (removed) 9.97 kB 🟢 -9.97 kB 🟢 -2.91 kB 🟢 -2.59 kB
assets/ServerConfigPanel-AKY3Og-M.js (removed) 7.05 kB 🟢 -7.05 kB 🟢 -2.36 kB 🟢 -2.12 kB
assets/ServerConfigPanel-M3j_dK9s.js (new) 7.05 kB 🔴 +7.05 kB 🔴 +2.36 kB 🔴 +2.12 kB
assets/UserPanel-DkNwZkdA.js (removed) 6.75 kB 🟢 -6.75 kB 🟢 -2.24 kB 🟢 -1.97 kB
assets/UserPanel-TxR0EZEC.js (new) 6.75 kB 🔴 +6.75 kB 🔴 +2.24 kB 🔴 +1.96 kB
assets/cloudRemoteConfig-BLW3xd-T.js (removed) 2.05 kB 🟢 -2.05 kB 🟢 -986 B 🟢 -849 B
assets/cloudRemoteConfig-D61T-EvY.js (new) 2.05 kB 🔴 +2.05 kB 🔴 +986 B 🔴 +852 B
assets/refreshRemoteConfig--LostMKw.js (new) 1.45 kB 🔴 +1.45 kB 🔴 +647 B 🔴 +550 B
assets/refreshRemoteConfig-BCsA4F5g.js (removed) 1.45 kB 🟢 -1.45 kB 🟢 -646 B 🟢 -548 B

Status: 10 added / 10 removed / 11 unchanged

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

Authentication, profile, and account management bundles

File Before After Δ Raw Δ Gzip Δ Brotli
assets/auth-B5XkBNqt.js (new) 3.65 kB 🔴 +3.65 kB 🔴 +1.29 kB 🔴 +1.11 kB
assets/auth-fBRF0ZR3.js (removed) 3.65 kB 🟢 -3.65 kB 🟢 -1.29 kB 🟢 -1.11 kB
assets/SignUpForm-Bh0UEm3Q.js (new) 3.19 kB 🔴 +3.19 kB 🔴 +1.29 kB 🔴 +1.17 kB
assets/SignUpForm-Bip4OvZo.js (removed) 3.19 kB 🟢 -3.19 kB 🟢 -1.29 kB 🟢 -1.16 kB
assets/UpdatePasswordContent-DHWdeI7p.js (new) 2.9 kB 🔴 +2.9 kB 🔴 +1.3 kB 🔴 +1.16 kB
assets/UpdatePasswordContent-DVVSLHDR.js (removed) 2.9 kB 🟢 -2.9 kB 🟢 -1.3 kB 🟢 -1.16 kB
assets/authStore-A3m-MUtG.js (new) 1.19 kB 🔴 +1.19 kB 🔴 +568 B 🔴 +506 B
assets/authStore-CXTkoRA4.js (removed) 1.19 kB 🟢 -1.19 kB 🟢 -565 B 🟢 -506 B
assets/auth-Cp5WDXk0.js (new) 348 B 🔴 +348 B 🔴 +212 B 🔴 +210 B
assets/auth-DXxC3ezY.js (removed) 348 B 🟢 -348 B 🟢 -210 B 🟢 -186 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-BAATAWDQ.js (removed) 85.8 kB 🟢 -85.8 kB 🟢 -18.6 kB 🟢 -15.8 kB
assets/ComfyHubPublishDialog-BprGUuZK.js (new) 85.8 kB 🔴 +85.8 kB 🔴 +18.6 kB 🔴 +15.9 kB
assets/useShareDialog-BlHsVfaM.js (removed) 23.8 kB 🟢 -23.8 kB 🟢 -5.78 kB 🟢 -5.12 kB
assets/useShareDialog-Co5iMiwW.js (new) 23.8 kB 🔴 +23.8 kB 🔴 +5.78 kB 🔴 +5.12 kB
assets/ComfyHubPublishDialog-BFuGYoPX.js (new) 1.35 kB 🔴 +1.35 kB 🔴 +623 B 🔴 +550 B
assets/ComfyHubPublishDialog-Cp9ojy0B.js (removed) 1.35 kB 🟢 -1.35 kB 🟢 -624 B 🟢 -551 B
assets/useSubscriptionDialog-BQa0n7DN.js (removed) 1.17 kB 🟢 -1.17 kB 🟢 -554 B 🟢 -489 B
assets/useSubscriptionDialog-Bzwu10tQ.js (new) 1.17 kB 🔴 +1.17 kB 🔴 +557 B 🔴 +488 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-Bp1mG8D9.js (new) 13.5 kB 🔴 +13.5 kB 🔴 +3.79 kB 🔴 +3.38 kB
assets/ComfyQueueButton-gDlHKLU-.js (removed) 13.5 kB 🟢 -13.5 kB 🟢 -3.79 kB 🟢 -3.39 kB
assets/useTerminalTabs-BIJ00fdz.js (removed) 11 kB 🟢 -11 kB 🟢 -3.73 kB 🟢 -3.28 kB
assets/useTerminalTabs-BlLaYlRN.js (new) 11 kB 🔴 +11 kB 🔴 +3.73 kB 🔴 +3.28 kB
assets/SubscribeButton-C2UkGHOs.js (new) 2.42 kB 🔴 +2.42 kB 🔴 +1.05 kB 🔴 +944 B
assets/SubscribeButton-Dbppnh3Z.js (removed) 2.42 kB 🟢 -2.42 kB 🟢 -1.04 kB 🟢 -926 B
assets/cloudFeedbackTopbarButton-9ffPKkWA.js (removed) 1.83 kB 🟢 -1.83 kB 🟢 -943 B 🟢 -832 B
assets/cloudFeedbackTopbarButton-jiqB1H85.js (new) 1.83 kB 🔴 +1.83 kB 🔴 +943 B 🔴 +828 B
assets/ComfyQueueButton-i1DlqNRA.js (removed) 1.27 kB 🟢 -1.27 kB 🟢 -591 B 🟢 -526 B
assets/ComfyQueueButton-wwWQHC3T.js (new) 1.27 kB 🔴 +1.27 kB 🔴 +592 B 🔴 +526 B

Status: 5 added / 5 removed / 9 unchanged

Data & Services — 3.05 MB (baseline 3.05 MB) • ⚪ 0 B

Stores, services, APIs, and repositories

File Before After Δ Raw Δ Gzip Δ Brotli
assets/dialogService-BLaDfhkI.js (removed) 1.99 MB 🟢 -1.99 MB 🟢 -459 kB 🟢 -348 kB
assets/dialogService-BLJE5anK.js (new) 1.99 MB 🔴 +1.99 MB 🔴 +459 kB 🔴 +348 kB
assets/api-BsmF5XC1.js (removed) 887 kB 🟢 -887 kB 🟢 -212 kB 🟢 -167 kB
assets/api-CSGk5s5L.js (new) 887 kB 🔴 +887 kB 🔴 +212 kB 🔴 +167 kB
assets/load3dService-B6Pf9qv6.js (removed) 116 kB 🟢 -116 kB 🟢 -25.3 kB 🟢 -21.6 kB
assets/load3dService-BnrfRb96.js (new) 116 kB 🔴 +116 kB 🔴 +25.3 kB 🔴 +21.5 kB
assets/workflowShareService-Blkcsl2q.js (removed) 16.6 kB 🟢 -16.6 kB 🟢 -4.89 kB 🟢 -4.33 kB
assets/workflowShareService-gfaqU0zm.js (new) 16.6 kB 🔴 +16.6 kB 🔴 +4.89 kB 🔴 +4.34 kB
assets/keybindingService-CRqiGKgt.js (new) 13.8 kB 🔴 +13.8 kB 🔴 +3.67 kB 🔴 +3.21 kB
assets/keybindingService-tS927KOB.js (removed) 13.8 kB 🟢 -13.8 kB 🟢 -3.67 kB 🟢 -3.22 kB
assets/releaseStore-BL1wRiRD.js (removed) 8.12 kB 🟢 -8.12 kB 🟢 -2.28 kB 🟢 -2 kB
assets/releaseStore-CfRuowgk.js (new) 8.12 kB 🔴 +8.12 kB 🔴 +2.28 kB 🔴 +2 kB
assets/userStore-Bt3iuHwc.js (removed) 2.24 kB 🟢 -2.24 kB 🟢 -870 B 🟢 -761 B
assets/userStore-RymCVpLP.js (new) 2.24 kB 🔴 +2.24 kB 🔴 +870 B 🔴 +762 B
assets/audioService-BvbD4rS0.js (new) 1.8 kB 🔴 +1.8 kB 🔴 +877 B 🔴 +767 B
assets/audioService-BXqNkHcn.js (removed) 1.8 kB 🟢 -1.8 kB 🟢 -879 B 🟢 -756 B
assets/releaseStore-DEFa94LJ.js (removed) 1.19 kB 🟢 -1.19 kB 🟢 -560 B 🟢 -499 B
assets/releaseStore-JQMDlKEp.js (new) 1.19 kB 🔴 +1.19 kB 🔴 +560 B 🔴 +502 B
assets/workflowDraftStore-5yEg3yc9.js (new) 1.17 kB 🔴 +1.17 kB 🔴 +555 B 🔴 +491 B
assets/workflowDraftStore-CCfmBoQ2.js (removed) 1.17 kB 🟢 -1.17 kB 🟢 -555 B 🟢 -491 B
assets/dialogService-CBJm7Anu.js (removed) 1.16 kB 🟢 -1.16 kB 🟢 -548 B 🟢 -489 B
assets/dialogService-jfcRdCRB.js (new) 1.16 kB 🔴 +1.16 kB 🔴 +548 B 🔴 +490 B
assets/settingStore-Br462IDz.js (removed) 1.15 kB 🟢 -1.15 kB 🟢 -550 B 🟢 -492 B
assets/settingStore-rRHR676h.js (new) 1.15 kB 🔴 +1.15 kB 🔴 +551 B 🔴 +492 B
assets/assetsStore-BXDOF-Ul.js (removed) 1.15 kB 🟢 -1.15 kB 🟢 -551 B 🟢 -489 B
assets/assetsStore-CdSbrIF_.js (new) 1.15 kB 🔴 +1.15 kB 🔴 +551 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-COES3hsL.js (removed) 233 kB 🟢 -233 kB 🟢 -51.8 kB 🟢 -42.2 kB
assets/useConflictDetection-hWjx0GVq.js (new) 233 kB 🔴 +233 kB 🔴 +51.8 kB 🔴 +42.2 kB
assets/useLoad3d-BsiTc8xj.js (removed) 22.5 kB 🟢 -22.5 kB 🟢 -5.17 kB 🟢 -4.57 kB
assets/useLoad3d-DejwjERO.js (new) 22.5 kB 🔴 +22.5 kB 🔴 +5.18 kB 🔴 +4.57 kB
assets/useLoad3dViewer-BMHXEgAo.js (removed) 21 kB 🟢 -21 kB 🟢 -4.92 kB 🟢 -4.3 kB
assets/useLoad3dViewer-TtYYdWAb.js (new) 21 kB 🔴 +21 kB 🔴 +4.92 kB 🔴 +4.31 kB
assets/useFeatureFlags-DT5R3F11.js (new) 5.95 kB 🔴 +5.95 kB 🔴 +1.8 kB 🔴 +1.53 kB
assets/useFeatureFlags-ooSQ4wJg.js (removed) 5.95 kB 🟢 -5.95 kB 🟢 -1.79 kB 🟢 -1.53 kB
assets/useCopyToClipboard-BFH4PANj.js (removed) 5.29 kB 🟢 -5.29 kB 🟢 -1.86 kB 🟢 -1.57 kB
assets/useCopyToClipboard-DcrAVeQH.js (new) 5.29 kB 🔴 +5.29 kB 🔴 +1.86 kB 🔴 +1.57 kB
assets/useWorkspaceUI-D042axgP.js (removed) 3.34 kB 🟢 -3.34 kB 🟢 -981 B 🟢 -810 B
assets/useWorkspaceUI-eaYJuFPC.js (new) 3.34 kB 🔴 +3.34 kB 🔴 +982 B 🔴 +809 B
assets/subscriptionCheckoutUtil-D8Ue2OfR.js (removed) 3.31 kB 🟢 -3.31 kB 🟢 -1.36 kB 🟢 -1.19 kB
assets/subscriptionCheckoutUtil-fU2JySSG.js (new) 3.31 kB 🔴 +3.31 kB 🔴 +1.36 kB 🔴 +1.19 kB
assets/assetPreviewUtil-DDqK_Twy.js (new) 2.27 kB 🔴 +2.27 kB 🔴 +958 B 🔴 +833 B
assets/assetPreviewUtil-pm8LlwfD.js (removed) 2.27 kB 🟢 -2.27 kB 🟢 -957 B 🟢 -837 B
assets/useUpstreamValue-dpRCOXQd.js (removed) 2.08 kB 🟢 -2.08 kB 🟢 -805 B 🟢 -712 B
assets/useUpstreamValue-OPbLQMDc.js (new) 2.08 kB 🔴 +2.08 kB 🔴 +805 B 🔴 +714 B
assets/useLoad3d-B8aaKcV2.js (removed) 1.33 kB 🟢 -1.33 kB 🟢 -617 B 🟢 -567 B
assets/useLoad3d-DoihVKjy.js (new) 1.33 kB 🔴 +1.33 kB 🔴 +616 B 🔴 +565 B
assets/useLoad3dViewer-CXt8cY6d.js (new) 1.27 kB 🔴 +1.27 kB 🔴 +583 B 🔴 +525 B
assets/useLoad3dViewer-zzkEh6CX.js (removed) 1.27 kB 🟢 -1.27 kB 🟢 -584 B 🟢 -527 B
assets/useCurrentUser-Cfbp8Qzr.js (new) 1.15 kB 🔴 +1.15 kB 🔴 +552 B 🔴 +486 B
assets/useCurrentUser-CFJcDt7g.js (removed) 1.15 kB 🟢 -1.15 kB 🟢 -550 B 🟢 -488 B
assets/useWorkspaceSwitch-BA3VGBFe.js (new) 747 B 🔴 +747 B 🔴 +383 B 🔴 +330 B
assets/useWorkspaceSwitch-Bvy8z2se.js (removed) 747 B 🟢 -747 B 🟢 -382 B 🟢 -325 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.86 MB (baseline 8.86 MB) • 🔴 +463 B

Bundles that do not match a named category

File Before After Δ Raw Δ Gzip Δ Brotli
assets/core-CKB3WLkX.js (removed) 77.3 kB 🟢 -77.3 kB 🟢 -20 kB 🟢 -17.1 kB
assets/core-jjvc23Kd.js (new) 77.3 kB 🔴 +77.3 kB 🔴 +20 kB 🔴 +17 kB
assets/groupNode-DAfczx2m.js (removed) 74.9 kB 🟢 -74.9 kB 🟢 -18.7 kB 🟢 -16.5 kB
assets/groupNode-DnZr64hc.js (new) 74.9 kB 🔴 +74.9 kB 🔴 +18.7 kB 🔴 +16.5 kB
assets/WidgetSelect-NKL6p7cc.js (removed) 67.4 kB 🟢 -67.4 kB 🟢 -14.6 kB 🟢 -12.7 kB
assets/WidgetSelect-VSyyay0K.js (new) 67.4 kB 🔴 +67.4 kB 🔴 +14.6 kB 🔴 +12.7 kB
assets/SubscriptionRequiredDialogContentWorkspace-DQ69VRnT.js (removed) 48.8 kB 🟢 -48.8 kB 🟢 -9.52 kB 🟢 -8.21 kB
assets/SubscriptionRequiredDialogContentWorkspace-PCOW9x0r.js (new) 48.8 kB 🔴 +48.8 kB 🔴 +9.52 kB 🔴 +8.21 kB
assets/Load3DControls-BuVkVFCS.js (removed) 46.1 kB 🟢 -46.1 kB 🟢 -7.51 kB 🟢 -6.55 kB
assets/Load3DControls-DKtPisBu.js (new) 46.1 kB 🔴 +46.1 kB 🔴 +7.51 kB 🔴 +6.55 kB
assets/WidgetPainter-8kAavHKT.js (new) 34.5 kB 🔴 +34.5 kB 🔴 +8.51 kB 🔴 +7.51 kB
assets/WorkspacePanelContent-BGmvdRF-.js (removed) 34.2 kB 🟢 -34.2 kB 🟢 -7.41 kB 🟢 -6.58 kB
assets/WorkspacePanelContent-DvQSfluJ.js (new) 34.2 kB 🔴 +34.2 kB 🔴 +7.41 kB 🔴 +6.57 kB
assets/WidgetPainter-C7zbqHss.js (removed) 34 kB 🟢 -34 kB 🟢 -8.3 kB 🟢 -7.34 kB
assets/Load3dViewerContent-C-Hd46q2.js (new) 28 kB 🔴 +28 kB 🔴 +5.85 kB 🔴 +5.07 kB
assets/Load3dViewerContent-D95KTp0f.js (removed) 28 kB 🟢 -28 kB 🟢 -5.85 kB 🟢 -5.07 kB
assets/SubscriptionRequiredDialogContent-B40K_MSK.js (new) 27.5 kB 🔴 +27.5 kB 🔴 +6.99 kB 🔴 +6.16 kB
assets/SubscriptionRequiredDialogContent-D1Rla2hu.js (removed) 27.5 kB 🟢 -27.5 kB 🟢 -6.98 kB 🟢 -6.16 kB
assets/WidgetImageCrop-Du5h00En.js (removed) 24.3 kB 🟢 -24.3 kB 🟢 -6.2 kB 🟢 -5.44 kB
assets/WidgetImageCrop-mZLrMwHN.js (new) 24.3 kB 🔴 +24.3 kB 🔴 +6.2 kB 🔴 +5.46 kB
assets/SubscriptionPanelContentWorkspace-B59ojvLo.js (removed) 22.2 kB 🟢 -22.2 kB 🟢 -5.18 kB 🟢 -4.56 kB
assets/SubscriptionPanelContentWorkspace-C2uKkf4J.js (new) 22.2 kB 🔴 +22.2 kB 🔴 +5.18 kB 🔴 +4.56 kB
assets/SignInContent-CMxKv4mg.js (new) 20.8 kB 🔴 +20.8 kB 🔴 +5.44 kB 🔴 +4.76 kB
assets/SignInContent-Qu6tzUQj.js (removed) 20.8 kB 🟢 -20.8 kB 🟢 -5.44 kB 🟢 -4.75 kB
assets/CurrentUserPopoverWorkspace-CEtRxwpW.js (removed) 20.6 kB 🟢 -20.6 kB 🟢 -4.91 kB 🟢 -4.39 kB
assets/CurrentUserPopoverWorkspace-oBnxG_AF.js (new) 20.6 kB 🔴 +20.6 kB 🔴 +4.91 kB 🔴 +4.39 kB
assets/WidgetInputNumber-BtGtM-7v.js (removed) 19.1 kB 🟢 -19.1 kB 🟢 -4.84 kB 🟢 -4.29 kB
assets/WidgetInputNumber-DsH6C6G0.js (new) 19.1 kB 🔴 +19.1 kB 🔴 +4.83 kB 🔴 +4.29 kB
assets/Load3D-C-rUoYwF.js (new) 18.5 kB 🔴 +18.5 kB 🔴 +4.39 kB 🔴 +3.83 kB
assets/Load3D-Cg6QYnuX.js (removed) 18.5 kB 🟢 -18.5 kB 🟢 -4.39 kB 🟢 -3.85 kB
assets/WidgetRecordAudio-Cr5XgRXM.js (new) 17.5 kB 🔴 +17.5 kB 🔴 +5.04 kB 🔴 +4.51 kB
assets/WidgetRecordAudio-MCZBv4IB.js (removed) 17.5 kB 🟢 -17.5 kB 🟢 -5.04 kB 🟢 -4.51 kB
assets/WidgetRange-BL8u_93e.js (new) 17.1 kB 🔴 +17.1 kB 🔴 +4.61 kB 🔴 +4.11 kB
assets/WidgetRange-Drv2tUHA.js (removed) 17.1 kB 🟢 -17.1 kB 🟢 -4.61 kB 🟢 -4.13 kB
assets/load3d-Cue43QB3.js (new) 15.8 kB 🔴 +15.8 kB 🔴 +4.59 kB 🔴 +3.98 kB
assets/load3d-DQyZy57y.js (removed) 15.8 kB 🟢 -15.8 kB 🟢 -4.59 kB 🟢 -3.98 kB
assets/WaveAudioPlayer-Cy81p9VG.js (removed) 13.4 kB 🟢 -13.4 kB 🟢 -3.69 kB 🟢 -3.23 kB
assets/WaveAudioPlayer-DXmfZxmd.js (new) 13.4 kB 🔴 +13.4 kB 🔴 +3.69 kB 🔴 +3.23 kB
assets/WidgetCurve-Bx6goK7p.js (new) 12.2 kB 🔴 +12.2 kB 🔴 +3.93 kB 🔴 +3.56 kB
assets/WidgetCurve-CL_00LY1.js (removed) 12.2 kB 🟢 -12.2 kB 🟢 -3.93 kB 🟢 -3.56 kB
assets/TeamWorkspacesDialogContent-BGpUduPa.js (new) 11.3 kB 🔴 +11.3 kB 🔴 +3.42 kB 🔴 +3.05 kB
assets/TeamWorkspacesDialogContent-DfFjQ-C7.js (removed) 11.3 kB 🟢 -11.3 kB 🟢 -3.42 kB 🟢 -3.04 kB
assets/nodeTemplates-BLWSSNSt.js (removed) 9.84 kB 🟢 -9.84 kB 🟢 -3.48 kB 🟢 -3.08 kB
assets/nodeTemplates-BMEt08lS.js (new) 9.84 kB 🔴 +9.84 kB 🔴 +3.48 kB 🔴 +3.08 kB
assets/NightlySurveyController-D_hHOTVL.js (removed) 8.97 kB 🟢 -8.97 kB 🟢 -3.15 kB 🟢 -2.79 kB
assets/NightlySurveyController-uFaQPsbz.js (new) 8.97 kB 🔴 +8.97 kB 🔴 +3.15 kB 🔴 +2.78 kB
assets/Load3DConfiguration-Ie86-Gex.js (new) 8.61 kB 🔴 +8.61 kB 🔴 +2.54 kB 🔴 +2.23 kB
assets/Load3DConfiguration-w46e-jbl.js (removed) 8.61 kB 🟢 -8.61 kB 🟢 -2.54 kB 🟢 -2.23 kB
assets/InviteMemberDialogContent-BzAOzf20.js (removed) 7.94 kB 🟢 -7.94 kB 🟢 -2.53 kB 🟢 -2.22 kB
assets/InviteMemberDialogContent-DcnO4MSe.js (new) 7.94 kB 🔴 +7.94 kB 🔴 +2.53 kB 🔴 +2.22 kB
assets/onboardingCloudRoutes-OYnqYtOQ.js (removed) 6.73 kB 🟢 -6.73 kB 🟢 -2.12 kB 🟢 -1.83 kB
assets/onboardingCloudRoutes-sq3h2ILB.js (new) 6.73 kB 🔴 +6.73 kB 🔴 +2.12 kB 🔴 +1.82 kB
assets/CreateWorkspaceDialogContent-CU7LRZ6T.js (removed) 6.15 kB 🟢 -6.15 kB 🟢 -2.24 kB 🟢 -1.96 kB
assets/CreateWorkspaceDialogContent-zNlgMaor.js (new) 6.15 kB 🔴 +6.15 kB 🔴 +2.24 kB 🔴 +1.96 kB
assets/WidgetWithControl-CpNmmaKp.js (new) 6.09 kB 🔴 +6.09 kB 🔴 +2.44 kB 🔴 +2.19 kB
assets/WidgetWithControl-ZSKD-Jno.js (removed) 6.09 kB 🟢 -6.09 kB 🟢 -2.44 kB 🟢 -2.17 kB
assets/FreeTierDialogContent-Cj0FSUjz.js (removed) 6.01 kB 🟢 -6.01 kB 🟢 -2.13 kB 🟢 -1.88 kB
assets/FreeTierDialogContent-gfhal2Vn.js (new) 6.01 kB 🔴 +6.01 kB 🔴 +2.13 kB 🔴 +1.89 kB
assets/EditWorkspaceDialogContent-CmdOJ_Mw.js (new) 5.95 kB 🔴 +5.95 kB 🔴 +2.2 kB 🔴 +1.93 kB
assets/EditWorkspaceDialogContent-DoxW829c.js (removed) 5.95 kB 🟢 -5.95 kB 🟢 -2.2 kB 🟢 -1.94 kB
assets/WidgetTextarea-BuEIW4YA.js (removed) 5.76 kB 🟢 -5.76 kB 🟢 -2.27 kB 🟢 -2.01 kB
assets/WidgetTextarea-BvLmot9y.js (new) 5.76 kB 🔴 +5.76 kB 🔴 +2.27 kB 🔴 +2.03 kB
assets/Preview3d-CnOVOkEi.js (new) 5.73 kB 🔴 +5.73 kB 🔴 +1.92 kB 🔴 +1.68 kB
assets/Preview3d-jDl6QT1y.js (removed) 5.73 kB 🟢 -5.73 kB 🟢 -1.92 kB 🟢 -1.69 kB
assets/ValueControlPopover-Bd1D7hB6.js (new) 5.53 kB 🔴 +5.53 kB 🔴 +2.01 kB 🔴 +1.79 kB
assets/ValueControlPopover-UpQCdfHN.js (removed) 5.53 kB 🟢 -5.53 kB 🟢 -2.01 kB 🟢 -1.79 kB
assets/CancelSubscriptionDialogContent-BmxNO_mK.js (removed) 5.49 kB 🟢 -5.49 kB 🟢 -2.06 kB 🟢 -1.81 kB
assets/CancelSubscriptionDialogContent-eyf_e4CA.js (new) 5.49 kB 🔴 +5.49 kB 🔴 +2.06 kB 🔴 +1.81 kB
assets/DeleteWorkspaceDialogContent-BR3yzdWV.js (new) 4.85 kB 🔴 +4.85 kB 🔴 +1.88 kB 🔴 +1.63 kB
assets/DeleteWorkspaceDialogContent-DSpKniKS.js (removed) 4.85 kB 🟢 -4.85 kB 🟢 -1.88 kB 🟢 -1.63 kB
assets/LeaveWorkspaceDialogContent-C5Ys5pTg.js (removed) 4.68 kB 🟢 -4.68 kB 🟢 -1.82 kB 🟢 -1.59 kB
assets/LeaveWorkspaceDialogContent-CaZViD7J.js (new) 4.68 kB 🔴 +4.68 kB 🔴 +1.82 kB 🔴 +1.59 kB
assets/RemoveMemberDialogContent-Cy49BTrA.js (removed) 4.66 kB 🟢 -4.66 kB 🟢 -1.77 kB 🟢 -1.56 kB
assets/RemoveMemberDialogContent-D7ExwXp9.js (new) 4.66 kB 🔴 +4.66 kB 🔴 +1.77 kB 🔴 +1.56 kB
assets/RevokeInviteDialogContent-DIjPRSi3.js (removed) 4.57 kB 🟢 -4.57 kB 🟢 -1.78 kB 🟢 -1.57 kB
assets/RevokeInviteDialogContent-j_SfYWWC.js (new) 4.57 kB 🔴 +4.57 kB 🔴 +1.79 kB 🔴 +1.57 kB
assets/InviteMemberUpsellDialogContent-Cg-8DFiF.js (new) 4.47 kB 🔴 +4.47 kB 🔴 +1.65 kB 🔴 +1.46 kB
assets/InviteMemberUpsellDialogContent-PwjF_uE4.js (removed) 4.47 kB 🟢 -4.47 kB 🟢 -1.65 kB 🟢 -1.45 kB
assets/tierBenefits-2cBRl4u0.js (removed) 4.45 kB 🟢 -4.45 kB 🟢 -1.57 kB 🟢 -1.36 kB
assets/tierBenefits-Deuah1fc.js (new) 4.45 kB 🔴 +4.45 kB 🔴 +1.57 kB 🔴 +1.36 kB
assets/cloudSessionCookie-CJMGDYkD.js (removed) 4.31 kB 🟢 -4.31 kB 🟢 -1.58 kB 🟢 -1.38 kB
assets/cloudSessionCookie-DL1yi1xf.js (new) 4.31 kB 🔴 +4.31 kB 🔴 +1.58 kB 🔴 +1.37 kB
assets/Media3DTop-DHWWL9K7.js (new) 4.04 kB 🔴 +4.04 kB 🔴 +1.71 kB 🔴 +1.5 kB
assets/Media3DTop-DseiwRsJ.js (removed) 4.04 kB 🟢 -4.04 kB 🟢 -1.71 kB 🟢 -1.51 kB
assets/saveMesh--bvQBffS.js (removed) 4.03 kB 🟢 -4.03 kB 🟢 -1.76 kB 🟢 -1.56 kB
assets/saveMesh-DNIizqLB.js (new) 4.03 kB 🔴 +4.03 kB 🔴 +1.76 kB 🔴 +1.56 kB
assets/GlobalToast-Bw8IYlZF.js (new) 3.05 kB 🔴 +3.05 kB 🔴 +1.26 kB 🔴 +1.07 kB
assets/GlobalToast-C_zKQ-so.js (removed) 3.05 kB 🟢 -3.05 kB 🟢 -1.26 kB 🟢 -1.07 kB
assets/CloudRunButtonWrapper-BMvORlHj.js (new) 2.23 kB 🔴 +2.23 kB 🔴 +1.02 kB 🔴 +913 B
assets/CloudRunButtonWrapper-Bviae6rF.js (removed) 2.23 kB 🟢 -2.23 kB 🟢 -1.01 kB 🟢 -908 B
assets/SubscribeToRun-CPqhhn__.js (removed) 2.13 kB 🟢 -2.13 kB 🟢 -981 B 🟢 -887 B
assets/SubscribeToRun-CS4NFweY.js (new) 2.13 kB 🔴 +2.13 kB 🔴 +984 B 🔴 +888 B
assets/MediaAudioTop-Cv1QJhvx.js (removed) 2.08 kB 🟢 -2.08 kB 🟢 -1.01 kB 🟢 -865 B
assets/MediaAudioTop-dc6KE9gz.js (new) 2.08 kB 🔴 +2.08 kB 🔴 +1.01 kB 🔴 +865 B
assets/cloudBadges-D_RDNXY8.js (new) 1.96 kB 🔴 +1.96 kB 🔴 +978 B 🔴 +850 B
assets/cloudBadges-DfBzgSwH.js (removed) 1.96 kB 🟢 -1.96 kB 🟢 -976 B 🟢 -847 B
assets/cloudSubscription-C8FqQBii.js (removed) 1.88 kB 🟢 -1.88 kB 🟢 -898 B 🟢 -781 B
assets/cloudSubscription-Cqhsp54O.js (new) 1.88 kB 🔴 +1.88 kB 🔴 +897 B 🔴 +782 B
assets/graphHasMissingNodes-Bqt8laKd.js (new) 1.83 kB 🔴 +1.83 kB 🔴 +860 B 🔴 +759 B
assets/graphHasMissingNodes-CHCzfHD9.js (removed) 1.83 kB 🟢 -1.83 kB 🟢 -862 B 🟢 -776 B
assets/Load3D-BVt2e7gI.js (removed) 1.58 kB 🟢 -1.58 kB 🟢 -712 B 🟢 -628 B
assets/Load3D-plkqUs41.js (new) 1.58 kB 🔴 +1.58 kB 🔴 +709 B 🔴 +630 B
assets/previousFullPath-Clu9t87f.js (new) 1.53 kB 🔴 +1.53 kB 🔴 +695 B 🔴 +594 B
assets/previousFullPath-DhJdxlKw.js (removed) 1.53 kB 🟢 -1.53 kB 🟢 -693 B 🟢 -597 B
assets/nightlyBadges-9s7QtmFn.js (new) 1.49 kB 🔴 +1.49 kB 🔴 +742 B 🔴 +651 B
assets/nightlyBadges-CpLzpbUZ.js (removed) 1.49 kB 🟢 -1.49 kB 🟢 -743 B 🟢 -664 B
assets/Load3dViewerContent-Dy-NwDtT.js (new) 1.46 kB 🔴 +1.46 kB 🔴 +662 B 🔴 +590 B
assets/Load3dViewerContent-yMeExyIB.js (removed) 1.46 kB 🟢 -1.46 kB 🟢 -661 B 🟢 -610 B
assets/SubscriptionPanelContentWorkspace-B4ukPSCR.js (new) 1.35 kB 🔴 +1.35 kB 🔴 +614 B 🔴 +536 B
assets/SubscriptionPanelContentWorkspace-D3zmGJRn.js (removed) 1.35 kB 🟢 -1.35 kB 🟢 -614 B 🟢 -541 B
assets/WidgetLegacy-5bsSgO3S.js (new) 1.18 kB 🔴 +1.18 kB 🔴 +562 B 🔴 +497 B
assets/WidgetLegacy-C3pgXKxQ.js (removed) 1.18 kB 🟢 -1.18 kB 🟢 -562 B 🟢 -502 B
assets/changeTracker-B3xJU1xW.js (removed) 1.15 kB 🟢 -1.15 kB 🟢 -549 B 🟢 -485 B
assets/changeTracker-BlZOkJ-t.js (new) 1.15 kB 🔴 +1.15 kB 🔴 +551 B 🔴 +482 B

Status: 57 added / 57 removed / 79 unchanged

⚡ Performance Report

canvas-idle: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 68.4 MB heap
canvas-mouse-sweep: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 51.0 MB heap
canvas-zoom-sweep: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 45.8 MB heap
dom-widget-clipping: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 54.7 MB heap
large-graph-idle: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 59.8 MB heap
large-graph-pan: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 58.9 MB heap
large-graph-zoom: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 66.2 MB heap
minimap-idle: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 63.7 MB heap
subgraph-dom-widget-clipping: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 54.9 MB heap
subgraph-idle: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 67.6 MB heap
subgraph-mouse-sweep: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 60.0 MB heap
viewport-pan-sweep: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 69.9 MB heap
vue-large-graph-idle: · 58.1 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 163.6 MB heap
vue-large-graph-pan: · 58.1 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 59ms TBT · 146.2 MB heap
workflow-execution: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 52.4 MB heap

No regressions detected.

All metrics
Metric Baseline PR (median) Δ Sig
canvas-idle: avg frame time 17ms 17ms -0% z=-0.9
canvas-idle: p95 frame time 17ms 17ms +1%
canvas-idle: layout duration 0ms 0ms +0%
canvas-idle: style recalc duration 9ms 7ms -14% z=-3.9
canvas-idle: layout count 0 0 +0%
canvas-idle: style recalc count 8 8 +0% z=-5.6
canvas-idle: task duration 376ms 330ms -12% z=-2.1
canvas-idle: script duration 20ms 15ms -27% z=-4.7
canvas-idle: TBT 0ms 0ms +0%
canvas-idle: heap used 68.1 MB 68.4 MB +0%
canvas-idle: DOM nodes 16 16 +0% z=-5.2
canvas-idle: event listeners 4 4 +0% z=-1.6
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 3ms -15% z=-1.7
canvas-mouse-sweep: style recalc duration 39ms 40ms +2% z=-0.8
canvas-mouse-sweep: layout count 12 12 +0%
canvas-mouse-sweep: style recalc count 73 79 +8% z=0.1
canvas-mouse-sweep: task duration 854ms 948ms +11% z=1.4
canvas-mouse-sweep: script duration 141ms 116ms -18% z=-3.0
canvas-mouse-sweep: TBT 0ms 0ms +0%
canvas-mouse-sweep: heap used 56.5 MB 51.0 MB -10%
canvas-mouse-sweep: DOM nodes -261 -263 +1% z=-125.8
canvas-mouse-sweep: event listeners -131 -131 +0% z=-33.5
canvas-zoom-sweep: avg frame time 17ms 17ms +0% z=0.5
canvas-zoom-sweep: p95 frame time 17ms 17ms +1%
canvas-zoom-sweep: layout duration 1ms 1ms -17% z=-2.0
canvas-zoom-sweep: style recalc duration 19ms 16ms -15% z=-1.9
canvas-zoom-sweep: layout count 6 6 +0%
canvas-zoom-sweep: style recalc count 31 32 +3% z=1.6
canvas-zoom-sweep: task duration 339ms 289ms -15% z=-1.7
canvas-zoom-sweep: script duration 25ms 23ms -5% z=-1.2
canvas-zoom-sweep: TBT 0ms 0ms +0%
canvas-zoom-sweep: heap used 46.6 MB 45.8 MB -2%
canvas-zoom-sweep: DOM nodes 75 79 +5% z=-0.3
canvas-zoom-sweep: event listeners 19 21 +11% z=-0.5
dom-widget-clipping: avg frame time 17ms 17ms -0% z=-0.6
dom-widget-clipping: p95 frame time 17ms 17ms +0%
dom-widget-clipping: layout duration 0ms 0ms +0%
dom-widget-clipping: style recalc duration 7ms 7ms +0% z=-3.4
dom-widget-clipping: layout count 0 0 +0%
dom-widget-clipping: style recalc count 10 11 +10% z=-4.2
dom-widget-clipping: task duration 339ms 320ms -6% z=-2.7
dom-widget-clipping: script duration 61ms 54ms -11% z=-4.3
dom-widget-clipping: TBT 0ms 0ms +0%
dom-widget-clipping: heap used 54.7 MB 54.7 MB -0%
dom-widget-clipping: DOM nodes 16 18 +13% z=-2.9
dom-widget-clipping: event listeners 0 0 +0% variance too high
large-graph-idle: avg frame time 17ms 17ms +0% z=-0.2
large-graph-idle: p95 frame time 17ms 17ms -1%
large-graph-idle: layout duration 0ms 0ms +0%
large-graph-idle: style recalc duration 10ms 9ms -10% z=-3.2
large-graph-idle: layout count 0 0 +0%
large-graph-idle: style recalc count 9 10 +11% z=-5.1
large-graph-idle: task duration 591ms 530ms -10% z=-0.2
large-graph-idle: script duration 102ms 84ms -17% z=-1.7
large-graph-idle: TBT 0ms 0ms +0%
large-graph-idle: heap used 63.1 MB 59.8 MB -5%
large-graph-idle: DOM nodes -261 -260 -0% z=-315.0
large-graph-idle: event listeners -129 -129 +0% z=-25.4
large-graph-pan: avg frame time 17ms 17ms -0% z=-0.8
large-graph-pan: p95 frame time 17ms 17ms -1%
large-graph-pan: layout duration 0ms 0ms +0%
large-graph-pan: style recalc duration 18ms 17ms -5% z=0.2
large-graph-pan: layout count 0 0 +0%
large-graph-pan: style recalc count 68 67 -1% z=-4.0
large-graph-pan: task duration 1139ms 1068ms -6% z=-0.3
large-graph-pan: script duration 408ms 395ms -3% z=-0.6
large-graph-pan: TBT 0ms 0ms +0%
large-graph-pan: heap used 66.1 MB 58.9 MB -11%
large-graph-pan: DOM nodes -266 -266 +0% z=-172.7
large-graph-pan: event listeners -127 -127 +0% z=-159.3
large-graph-zoom: avg frame time 17ms 17ms +0%
large-graph-zoom: p95 frame time 17ms 17ms +1%
large-graph-zoom: layout duration 7ms 8ms +4%
large-graph-zoom: style recalc duration 19ms 18ms -5%
large-graph-zoom: layout count 60 60 +0%
large-graph-zoom: style recalc count 65 66 +2%
large-graph-zoom: task duration 1419ms 1266ms -11%
large-graph-zoom: script duration 509ms 466ms -8%
large-graph-zoom: TBT 0ms 0ms +0%
large-graph-zoom: heap used 66.3 MB 66.2 MB -0%
large-graph-zoom: DOM nodes -269 -265 -1%
large-graph-zoom: event listeners -127 -125 -2%
minimap-idle: avg frame time 17ms 17ms +0% z=0.1
minimap-idle: p95 frame time 17ms 17ms -1%
minimap-idle: layout duration 0ms 0ms +0%
minimap-idle: style recalc duration 9ms 7ms -15% z=-2.7
minimap-idle: layout count 0 0 +0%
minimap-idle: style recalc count 8 9 +13% z=-0.8
minimap-idle: task duration 603ms 510ms -15% z=-0.4
minimap-idle: script duration 107ms 75ms -30% z=-2.3
minimap-idle: TBT 0ms 0ms +0%
minimap-idle: heap used 60.6 MB 63.7 MB +5%
minimap-idle: DOM nodes -264 -264 +0% z=-207.0
minimap-idle: event listeners -127 -129 +2% 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 -1%
subgraph-dom-widget-clipping: layout duration 0ms 0ms +0%
subgraph-dom-widget-clipping: style recalc duration 12ms 10ms -14% z=-2.4
subgraph-dom-widget-clipping: layout count 0 0 +0%
subgraph-dom-widget-clipping: style recalc count 47 47 +0% z=-1.6
subgraph-dom-widget-clipping: task duration 370ms 347ms -6% z=-1.7
subgraph-dom-widget-clipping: script duration 127ms 121ms -4% z=-1.1
subgraph-dom-widget-clipping: TBT 0ms 0ms +0%
subgraph-dom-widget-clipping: heap used 54.9 MB 54.9 MB +0%
subgraph-dom-widget-clipping: DOM nodes 20 19 -5% z=-2.8
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 9ms 7ms -20% z=-3.9
subgraph-idle: layout count 0 0 +0%
subgraph-idle: style recalc count 8 9 +13% z=-2.9
subgraph-idle: task duration 384ms 332ms -14% z=-1.2
subgraph-idle: script duration 19ms 13ms -35% z=-2.8
subgraph-idle: TBT 0ms 0ms +0%
subgraph-idle: heap used 43.3 MB 67.6 MB +56%
subgraph-idle: DOM nodes -261 18 -107% z=-2.5
subgraph-idle: event listeners -133 4 -103% variance too high
subgraph-mouse-sweep: avg frame time 17ms 17ms +0% z=0.4
subgraph-mouse-sweep: p95 frame time 17ms 17ms +0%
subgraph-mouse-sweep: layout duration 5ms 4ms -12% z=-1.7
subgraph-mouse-sweep: style recalc duration 38ms 35ms -8% z=-2.2
subgraph-mouse-sweep: layout count 16 16 +0%
subgraph-mouse-sweep: style recalc count 75 75 +0% z=-2.7
subgraph-mouse-sweep: task duration 735ms 618ms -16% z=-2.1
subgraph-mouse-sweep: script duration 100ms 83ms -17% z=-2.8
subgraph-mouse-sweep: TBT 0ms 0ms +0%
subgraph-mouse-sweep: heap used 60.1 MB 60.0 MB -0%
subgraph-mouse-sweep: DOM nodes 60 60 +0% z=-3.1
subgraph-mouse-sweep: event listeners 4 6 +50% variance too high
viewport-pan-sweep: avg frame time 17ms 17ms +0%
viewport-pan-sweep: p95 frame time 17ms 17ms +1%
viewport-pan-sweep: layout duration 0ms 0ms +0%
viewport-pan-sweep: style recalc duration 55ms 55ms +1%
viewport-pan-sweep: layout count 0 0 +0%
viewport-pan-sweep: style recalc count 250 250 +0%
viewport-pan-sweep: task duration 3937ms 3508ms -11%
viewport-pan-sweep: script duration 1308ms 1166ms -11%
viewport-pan-sweep: TBT 0ms 0ms +0%
viewport-pan-sweep: heap used 67.2 MB 69.9 MB +4%
viewport-pan-sweep: DOM nodes -263 -259 -2%
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 12635ms 12438ms -2%
vue-large-graph-idle: script duration 621ms 598ms -4%
vue-large-graph-idle: TBT 0ms 0ms +0%
vue-large-graph-idle: heap used 163.8 MB 163.6 MB -0%
vue-large-graph-idle: DOM nodes -8331 -8331 +0%
vue-large-graph-idle: event listeners -16464 -16462 -0%
vue-large-graph-pan: avg frame time 18ms 17ms -3%
vue-large-graph-pan: p95 frame time 17ms 17ms +1%
vue-large-graph-pan: layout duration 0ms 0ms +0%
vue-large-graph-pan: style recalc duration 18ms 19ms +8%
vue-large-graph-pan: layout count 0 0 +0%
vue-large-graph-pan: style recalc count 67 68 +1%
vue-large-graph-pan: task duration 14511ms 14732ms +2%
vue-large-graph-pan: script duration 888ms 848ms -5%
vue-large-graph-pan: TBT 0ms 59ms
vue-large-graph-pan: heap used 159.0 MB 146.2 MB -8%
vue-large-graph-pan: DOM nodes -8331 -8331 +0%
vue-large-graph-pan: event listeners -16460 -16488 +0%
workflow-execution: avg frame time 17ms 17ms +0% z=0.6
workflow-execution: p95 frame time 17ms 17ms +0%
workflow-execution: layout duration 1ms 1ms -6% z=-1.0
workflow-execution: style recalc duration 27ms 23ms -16% z=-0.6
workflow-execution: layout count 5 5 +0% z=0.1
workflow-execution: style recalc count 18 17 -6% z=-0.4
workflow-execution: task duration 136ms 123ms -10% z=-0.0
workflow-execution: script duration 30ms 29ms -1% z=0.1
workflow-execution: TBT 0ms 0ms +0%
workflow-execution: heap used 52.8 MB 52.4 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-06T07:33:52.122Z",
  "gitSha": "6e42253cc32ee8bb291be938e03c27f5a830e2d9",
  "branch": "fix/painter-serialize-preserves-modelvalue-on-remount",
  "measurements": [
    {
      "name": "canvas-idle",
      "durationMs": 2065.4069999999933,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 7.356000000000002,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 340.775,
      "heapDeltaBytes": 23460152,
      "heapUsedBytes": 72789820,
      "domNodes": 16,
      "jsHeapTotalBytes": 14942208,
      "scriptDurationMs": 15.462000000000002,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-idle",
      "durationMs": 2013.417000000004,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 6.256000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 329.92999999999995,
      "heapDeltaBytes": 22955056,
      "heapUsedBytes": 71726960,
      "domNodes": 16,
      "jsHeapTotalBytes": 14680064,
      "scriptDurationMs": 14.775999999999998,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "canvas-idle",
      "durationMs": 1999.1710000000467,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 7.793999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 329.94800000000004,
      "heapDeltaBytes": 22956780,
      "heapUsedBytes": 71768012,
      "domNodes": 20,
      "jsHeapTotalBytes": 14942208,
      "scriptDurationMs": 14.264000000000001,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 1902.1459999999877,
      "styleRecalcs": 74,
      "styleRecalcDurationMs": 40.06700000000001,
      "layouts": 12,
      "layoutDurationMs": 3.5060000000000002,
      "taskDurationMs": 845.3510000000001,
      "heapDeltaBytes": -89664,
      "heapUsedBytes": 48606284,
      "domNodes": -265,
      "jsHeapTotalBytes": 16117760,
      "scriptDurationMs": 117.43500000000002,
      "eventListeners": -131,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 2023.8630000000057,
      "styleRecalcs": 83,
      "styleRecalcDurationMs": 40.485,
      "layouts": 12,
      "layoutDurationMs": 3.278,
      "taskDurationMs": 965.813,
      "heapDeltaBytes": 4778760,
      "heapUsedBytes": 53453380,
      "domNodes": -262,
      "jsHeapTotalBytes": 15855616,
      "scriptDurationMs": 111.96300000000001,
      "eventListeners": -131,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 2013.6009999999942,
      "styleRecalcs": 79,
      "styleRecalcDurationMs": 37.463,
      "layouts": 12,
      "layoutDurationMs": 3.28,
      "taskDurationMs": 947.5539999999999,
      "heapDeltaBytes": 6797132,
      "heapUsedBytes": 55507000,
      "domNodes": -263,
      "jsHeapTotalBytes": 16117760,
      "scriptDurationMs": 115.837,
      "eventListeners": -129,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1727.6350000000207,
      "styleRecalcs": 32,
      "styleRecalcDurationMs": 16.174999999999997,
      "layouts": 6,
      "layoutDurationMs": 0.5519999999999999,
      "taskDurationMs": 288.863,
      "heapDeltaBytes": 259612,
      "heapUsedBytes": 47961176,
      "domNodes": 79,
      "jsHeapTotalBytes": 14942208,
      "scriptDurationMs": 24.67799999999999,
      "eventListeners": 21,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1716.8019999999728,
      "styleRecalcs": 30,
      "styleRecalcDurationMs": 14.498999999999999,
      "layouts": 6,
      "layoutDurationMs": 0.6329999999999999,
      "taskDurationMs": 271.154,
      "heapDeltaBytes": 229500,
      "heapUsedBytes": 48606608,
      "domNodes": 74,
      "jsHeapTotalBytes": 15204352,
      "scriptDurationMs": 17.201,
      "eventListeners": 21,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.660000000000007,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1745.8710000000792,
      "styleRecalcs": 33,
      "styleRecalcDurationMs": 16.312,
      "layouts": 6,
      "layoutDurationMs": 0.554,
      "taskDurationMs": 292.71899999999994,
      "heapDeltaBytes": 207684,
      "heapUsedBytes": 48003296,
      "domNodes": 80,
      "jsHeapTotalBytes": 14680064,
      "scriptDurationMs": 23.37,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 522.5700000000018,
      "styleRecalcs": 11,
      "styleRecalcDurationMs": 7.183,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 314.959,
      "heapDeltaBytes": 9394948,
      "heapUsedBytes": 57709904,
      "domNodes": 18,
      "jsHeapTotalBytes": 15990784,
      "scriptDurationMs": 53.275999999999996,
      "eventListeners": 0,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.669999999999998,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 550.2629999999726,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 6.879999999999997,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 320.35999999999996,
      "heapDeltaBytes": 8744788,
      "heapUsedBytes": 57392656,
      "domNodes": 16,
      "jsHeapTotalBytes": 16515072,
      "scriptDurationMs": 53.895,
      "eventListeners": 0,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.663333333333338,
      "p95FrameDurationMs": 16.700000000000273
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 603.7900000000036,
      "styleRecalcs": 12,
      "styleRecalcDurationMs": 8.181000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 342.525,
      "heapDeltaBytes": 9354900,
      "heapUsedBytes": 57318460,
      "domNodes": 20,
      "jsHeapTotalBytes": 14942208,
      "scriptDurationMs": 59.88299999999999,
      "eventListeners": 0,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.663333333333338,
      "p95FrameDurationMs": 16.799999999999727
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2033.996000000002,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 10.984999999999998,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 531.015,
      "heapDeltaBytes": 14849984,
      "heapUsedBytes": 72671648,
      "domNodes": -255,
      "jsHeapTotalBytes": -233472,
      "scriptDurationMs": 84.10000000000001,
      "eventListeners": -129,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2033.9889999999627,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 6.768,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 520.9970000000001,
      "heapDeltaBytes": 2365696,
      "heapUsedBytes": 60886752,
      "domNodes": -265,
      "jsHeapTotalBytes": 4485120,
      "scriptDurationMs": 77.64999999999998,
      "eventListeners": -129,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2020.8800000000338,
      "styleRecalcs": 11,
      "styleRecalcDurationMs": 8.839999999999996,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 530.3549999999999,
      "heapDeltaBytes": 5611836,
      "heapUsedBytes": 62723384,
      "domNodes": -260,
      "jsHeapTotalBytes": 1077248,
      "scriptDurationMs": 84.77699999999999,
      "eventListeners": -129,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2108.5559999999646,
      "styleRecalcs": 67,
      "styleRecalcDurationMs": 16.703000000000003,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1067.557,
      "heapDeltaBytes": 2482996,
      "heapUsedBytes": 61780788,
      "domNodes": -267,
      "jsHeapTotalBytes": 1282048,
      "scriptDurationMs": 396.61,
      "eventListeners": -129,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2105.8340000000157,
      "styleRecalcs": 69,
      "styleRecalcDurationMs": 18.316000000000003,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 994.605,
      "heapDeltaBytes": 7080024,
      "heapUsedBytes": 66178460,
      "domNodes": -261,
      "jsHeapTotalBytes": 1282048,
      "scriptDurationMs": 353.781,
      "eventListeners": -127,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2115.4089999999997,
      "styleRecalcs": 67,
      "styleRecalcDurationMs": 17.453,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1075.4830000000002,
      "heapDeltaBytes": -5302848,
      "heapUsedBytes": 53909216,
      "domNodes": -266,
      "jsHeapTotalBytes": 7892992,
      "scriptDurationMs": 395.464,
      "eventListeners": -127,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3168.332000000021,
      "styleRecalcs": 66,
      "styleRecalcDurationMs": 18.464,
      "layouts": 60,
      "layoutDurationMs": 7.726,
      "taskDurationMs": 1266.3719999999998,
      "heapDeltaBytes": 8827428,
      "heapUsedBytes": 69627424,
      "domNodes": -266,
      "jsHeapTotalBytes": 552960,
      "scriptDurationMs": 467.823,
      "eventListeners": -125,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66999999999998,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3135.6040000000007,
      "styleRecalcs": 65,
      "styleRecalcDurationMs": 18.291,
      "layouts": 60,
      "layoutDurationMs": 7.7219999999999995,
      "taskDurationMs": 1262.0529999999999,
      "heapDeltaBytes": 7245956,
      "heapUsedBytes": 69382884,
      "domNodes": -265,
      "jsHeapTotalBytes": 3756032,
      "scriptDurationMs": 462.569,
      "eventListeners": -125,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3151.0119999999233,
      "styleRecalcs": 66,
      "styleRecalcDurationMs": 19.03,
      "layouts": 60,
      "layoutDurationMs": 7.844,
      "taskDurationMs": 1270.2369999999999,
      "heapDeltaBytes": -8895176,
      "heapUsedBytes": 53074960,
      "domNodes": -263,
      "jsHeapTotalBytes": 4542464,
      "scriptDurationMs": 465.78200000000004,
      "eventListeners": -125,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "minimap-idle",
      "durationMs": 2016.0640000000285,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 7.361,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 510.07199999999995,
      "heapDeltaBytes": 5792284,
      "heapUsedBytes": 66829300,
      "domNodes": -264,
      "jsHeapTotalBytes": 4018176,
      "scriptDurationMs": 75.053,
      "eventListeners": -131,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "minimap-idle",
      "durationMs": 2031.5380000000118,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 6.610999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 505.046,
      "heapDeltaBytes": 2314768,
      "heapUsedBytes": 62654276,
      "domNodes": -267,
      "jsHeapTotalBytes": 5271552,
      "scriptDurationMs": 75.145,
      "eventListeners": -129,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "minimap-idle",
      "durationMs": 2037.113999999974,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 8.146,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 520.821,
      "heapDeltaBytes": 16303252,
      "heapUsedBytes": 76638608,
      "domNodes": -254,
      "jsHeapTotalBytes": -233472,
      "scriptDurationMs": 77.903,
      "eventListeners": -129,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 554.9589999999966,
      "styleRecalcs": 48,
      "styleRecalcDurationMs": 11.755,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 352.03499999999997,
      "heapDeltaBytes": 9144332,
      "heapUsedBytes": 57782388,
      "domNodes": 21,
      "jsHeapTotalBytes": 16252928,
      "scriptDurationMs": 121.209,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000273
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 513.4869999999978,
      "styleRecalcs": 46,
      "styleRecalcDurationMs": 9.565,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 333.04699999999997,
      "heapDeltaBytes": 8843040,
      "heapUsedBytes": 57519624,
      "domNodes": 18,
      "jsHeapTotalBytes": 16515072,
      "scriptDurationMs": 117.71699999999998,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 527.733000000012,
      "styleRecalcs": 47,
      "styleRecalcDurationMs": 10.406,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 346.52099999999996,
      "heapDeltaBytes": 8891360,
      "heapUsedBytes": 57584980,
      "domNodes": 19,
      "jsHeapTotalBytes": 15990784,
      "scriptDurationMs": 121.518,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000273
    },
    {
      "name": "subgraph-idle",
      "durationMs": 2007.4989999999957,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 7.366,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 374.373,
      "heapDeltaBytes": -4856276,
      "heapUsedBytes": 43719908,
      "domNodes": -260,
      "jsHeapTotalBytes": 14282752,
      "scriptDurationMs": 13.33,
      "eventListeners": -131,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "subgraph-idle",
      "durationMs": 1992.6980000000185,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 7.346999999999998,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 325.86499999999995,
      "heapDeltaBytes": 22383156,
      "heapUsedBytes": 70912072,
      "domNodes": 18,
      "jsHeapTotalBytes": 14942208,
      "scriptDurationMs": 12.645,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-idle",
      "durationMs": 1989.6659999999429,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 7.150000000000002,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 331.507,
      "heapDeltaBytes": 23279880,
      "heapUsedBytes": 71958964,
      "domNodes": 18,
      "jsHeapTotalBytes": 14942208,
      "scriptDurationMs": 12.615,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1673.827000000017,
      "styleRecalcs": 75,
      "styleRecalcDurationMs": 35.032000000000004,
      "layouts": 16,
      "layoutDurationMs": 4.56,
      "taskDurationMs": 598.402,
      "heapDeltaBytes": 14248900,
      "heapUsedBytes": 62960268,
      "domNodes": 61,
      "jsHeapTotalBytes": 14942208,
      "scriptDurationMs": 80.365,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1688.211000000024,
      "styleRecalcs": 76,
      "styleRecalcDurationMs": 37.18000000000001,
      "layouts": 16,
      "layoutDurationMs": 4.155,
      "taskDurationMs": 658.477,
      "heapDeltaBytes": -4874532,
      "heapUsedBytes": 43781496,
      "domNodes": -260,
      "jsHeapTotalBytes": 15331328,
      "scriptDurationMs": 82.63199999999998,
      "eventListeners": -133,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1689.8730000000342,
      "styleRecalcs": 75,
      "styleRecalcDurationMs": 33.770999999999994,
      "layouts": 16,
      "layoutDurationMs": 4.08,
      "taskDurationMs": 618.276,
      "heapDeltaBytes": 14723528,
      "heapUsedBytes": 63403192,
      "domNodes": 60,
      "jsHeapTotalBytes": 14680064,
      "scriptDurationMs": 87.168,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 8247.281999999985,
      "styleRecalcs": 250,
      "styleRecalcDurationMs": 55.481,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 3508.245,
      "heapDeltaBytes": 10047620,
      "heapUsedBytes": 68512252,
      "domNodes": -263,
      "jsHeapTotalBytes": 6787072,
      "scriptDurationMs": 1166.1889999999999,
      "eventListeners": -113,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.670000000000012,
      "p95FrameDurationMs": 16.80000000000109
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 8129.867999999988,
      "styleRecalcs": 250,
      "styleRecalcDurationMs": 54.026999999999994,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 3448.613,
      "heapDeltaBytes": 21189804,
      "heapUsedBytes": 78928996,
      "domNodes": -258,
      "jsHeapTotalBytes": 1544192,
      "scriptDurationMs": 1148.2930000000001,
      "eventListeners": -113,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.669999999999952,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 8161.3700000000335,
      "styleRecalcs": 250,
      "styleRecalcDurationMs": 55.246,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 3684.1539999999995,
      "heapDeltaBytes": 13447908,
      "heapUsedBytes": 73270144,
      "domNodes": -259,
      "jsHeapTotalBytes": 10223616,
      "scriptDurationMs": 1302.355,
      "eventListeners": -113,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 12468.16799999999,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 12438.267000000002,
      "heapDeltaBytes": -25630656,
      "heapUsedBytes": 170874644,
      "domNodes": -8331,
      "jsHeapTotalBytes": 23130112,
      "scriptDurationMs": 598.047,
      "eventListeners": -16466,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.220000000000073,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 12221.897000000012,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 12210.808,
      "heapDeltaBytes": -18512192,
      "heapUsedBytes": 186351848,
      "domNodes": -8331,
      "jsHeapTotalBytes": 24702976,
      "scriptDurationMs": 539.5020000000001,
      "eventListeners": -16462,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.779999999999927,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 12593.76800000007,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 12582.813000000002,
      "heapDeltaBytes": -35697308,
      "heapUsedBytes": 171572396,
      "domNodes": -8331,
      "jsHeapTotalBytes": 23654400,
      "scriptDurationMs": 598.617,
      "eventListeners": -16462,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.223333333333358,
      "p95FrameDurationMs": 16.80000000000291
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 15192.836999999998,
      "styleRecalcs": 72,
      "styleRecalcDurationMs": 19.038,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 15143.384000000002,
      "heapDeltaBytes": -47470940,
      "heapUsedBytes": 165176840,
      "domNodes": -8331,
      "jsHeapTotalBytes": 847872,
      "scriptDurationMs": 974.1610000000001,
      "eventListeners": -16458,
      "totalBlockingTimeMs": 1,
      "frameDurationMs": 17.219999999999953,
      "p95FrameDurationMs": 16.80000000000291
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 14596.01100000009,
      "styleRecalcs": 66,
      "styleRecalcDurationMs": 19.453,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 14570.079,
      "heapDeltaBytes": -56335104,
      "heapUsedBytes": 153250984,
      "domNodes": -8331,
      "jsHeapTotalBytes": -724992,
      "scriptDurationMs": 848.3159999999999,
      "eventListeners": -16488,
      "totalBlockingTimeMs": 62,
      "frameDurationMs": 17.220000000000073,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 14753.652999999986,
      "styleRecalcs": 68,
      "styleRecalcDurationMs": 18.20900000000003,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 14731.618,
      "heapDeltaBytes": -45072612,
      "heapUsedBytes": 152905628,
      "domNodes": -8329,
      "jsHeapTotalBytes": -724992,
      "scriptDurationMs": 839.6859999999999,
      "eventListeners": -16488,
      "totalBlockingTimeMs": 59,
      "frameDurationMs": 17.219999999999953,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "workflow-execution",
      "durationMs": 459.31999999999107,
      "styleRecalcs": 19,
      "styleRecalcDurationMs": 27.566999999999997,
      "layouts": 5,
      "layoutDurationMs": 1.495,
      "taskDurationMs": 130.91599999999997,
      "heapDeltaBytes": 5406332,
      "heapUsedBytes": 55148316,
      "domNodes": 170,
      "jsHeapTotalBytes": 524288,
      "scriptDurationMs": 31.049999999999994,
      "eventListeners": 69,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000273
    },
    {
      "name": "workflow-execution",
      "durationMs": 467.78299999994033,
      "styleRecalcs": 17,
      "styleRecalcDurationMs": 22.899999999999995,
      "layouts": 5,
      "layoutDurationMs": 1.399,
      "taskDurationMs": 122.74399999999999,
      "heapDeltaBytes": 5173952,
      "heapUsedBytes": 54933968,
      "domNodes": 157,
      "jsHeapTotalBytes": 0,
      "scriptDurationMs": 29.275,
      "eventListeners": 69,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000273
    },
    {
      "name": "workflow-execution",
      "durationMs": 455.09100000003855,
      "styleRecalcs": 16,
      "styleRecalcDurationMs": 19.993999999999996,
      "layouts": 4,
      "layoutDurationMs": 1.1440000000000003,
      "taskDurationMs": 108.96600000000001,
      "heapDeltaBytes": 5049396,
      "heapUsedBytes": 54925572,
      "domNodes": 155,
      "jsHeapTotalBytes": 262144,
      "scriptDurationMs": 22.626,
      "eventListeners": 69,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000273
    }
  ]
}

@codecov
Copy link
Copy Markdown

codecov Bot commented May 6, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

@@             Coverage Diff             @@
##             main   #12009       +/-   ##
===========================================
- Coverage   72.82%   57.64%   -15.18%     
===========================================
  Files        1499     1390      -109     
  Lines       83910    70816    -13094     
  Branches    23151    19764     -3387     
===========================================
- Hits        61108    40823    -20285     
- Misses      21970    29491     +7521     
+ Partials      832      502      -330     
Flag Coverage Δ
e2e ?
unit 57.64% <100.00%> (+0.06%) ⬆️

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

Files with missing lines Coverage Δ
src/composables/painter/usePainter.ts 50.63% <100.00%> (-37.71%) ⬇️

... and 990 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.

@christian-byrne christian-byrne changed the title fix: painter widget preserves mask reference across WidgetPainter remount (regression from #10966) fix: painter widget preserves mask reference across WidgetPainter remount May 6, 2026
Read actual canvas pixel data in serializeValue instead of trusting the
closure-local hasStrokes/isDirty flags. Per Slack thread evidence (Matt
Miller, 2026-05-05), the canvas can hold visible strokes (~9% non-zero
alpha pixels) while hasStrokes stays false. Two known triggers:

1. WidgetPainter remount swaps usePainter() for a fresh closure with
   hasStrokes=false; the registered serializeValue on the mask widget
   then disagrees with what the user sees on the canvas.
2. handlePointerDown early-returns on e.button !== 0, which can fire
   for some touchpad / pen pointer-event variants. startStroke never
   runs and hasStrokes never flips, even though pixels reach the
   canvas via other paths.

The new isCanvasPixelEmpty(el) reads ImageData and scans the alpha
channel. When canvas is empty, serializeValue defers to modelValue so
workflow-restored references survive a pre-image-load remount;
handleClear now also resets modelValue so a user-initiated clear still
resolves to ''.

Removes the now-unused hasStrokes flag and isCanvasEmpty helper.

Adds unit tests covering: pixel-driven upload despite isDirty=false,
empty canvas + empty modelValue → '', and post-handleClear → ''.
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:XS This PR changes 0-9 lines, ignoring generated files. labels May 6, 2026
@christian-byrne christian-byrne changed the title fix: painter widget preserves mask reference across WidgetPainter remount fix: detect painter canvas emptiness from pixel data, not stroke flags May 6, 2026
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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/composables/painter/usePainter.ts`:
- Around line 643-655: When detecting an empty canvas in usePainter (the
isCanvasPixelEmpty(el) check), do not return the old modelValue if the canvas
was modified (isDirty.value === true); instead clear the cached reference and
return '' so erased/restored masks do not reappear. Concretely: in the
empty-canvas branch, if isDirty.value is true set modelValue.value = '' (or
otherwise clear the cached upload ref) and return ''; only fall back to
modelValue when the canvas is empty AND isDirty is false. Update the logic
around isCanvasPixelEmpty, isDirty, and modelValue and add a regression test
"erase restored mask to fully transparent pixels" that simulates restoring a
mask then erasing it to fully transparent to assert serialize/upload yields ''
(handleClear remains unchanged).
🪄 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: 57732b4b-5441-4ce1-af08-da5825ca6129

📥 Commits

Reviewing files that changed from the base of the PR and between 1521e69 and b32cd53.

📒 Files selected for processing (2)
  • src/composables/painter/usePainter.test.ts
  • src/composables/painter/usePainter.ts

Comment on lines +643 to +655
// Authoritative emptiness check: read actual pixel data instead of
// relying on the `isDirty` flag, which can desync from canvas content
// on WidgetPainter remount or on non-primary pointerdown variants where
// the closure-local stroke bookkeeping was bypassed.
// When the canvas is empty, defer to `modelValue` so a workflow-restored
// mask reference (or a pending image-restore) survives. `handleClear`
// explicitly resets `modelValue` so a user-initiated clear still yields ''.
if (isCanvasPixelEmpty(el)) return modelValue.value

// Canvas has visible content. If we already uploaded this exact content
// (no new strokes since last successful upload) and the cached value is
// valid, reuse it to avoid redundant uploads.
if (!isDirty.value && modelValue.value) return modelValue.value
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Do not reuse modelValue after a dirty erase-to-empty path.

If a previously restored mask is fully erased with the eraser, isDirty.value is true but isCanvasPixelEmpty(el) is also true, so this returns the old upload reference instead of serializing an empty mask. That brings deleted content back on the next save. Only fall back to modelValue for an empty canvas when nothing changed since the last successful serialize; otherwise clear the cached reference and return ''. Please also add a regression test for “erase restored mask to fully transparent pixels” since handleClear() does not cover that path.

Suggested fix
-    if (isCanvasPixelEmpty(el)) return modelValue.value
+    if (isCanvasPixelEmpty(el)) {
+      if (isDirty.value) {
+        modelValue.value = ''
+        return ''
+      }
+      return modelValue.value
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Authoritative emptiness check: read actual pixel data instead of
// relying on the `isDirty` flag, which can desync from canvas content
// on WidgetPainter remount or on non-primary pointerdown variants where
// the closure-local stroke bookkeeping was bypassed.
// When the canvas is empty, defer to `modelValue` so a workflow-restored
// mask reference (or a pending image-restore) survives. `handleClear`
// explicitly resets `modelValue` so a user-initiated clear still yields ''.
if (isCanvasPixelEmpty(el)) return modelValue.value
// Canvas has visible content. If we already uploaded this exact content
// (no new strokes since last successful upload) and the cached value is
// valid, reuse it to avoid redundant uploads.
if (!isDirty.value && modelValue.value) return modelValue.value
// Authoritative emptiness check: read actual pixel data instead of
// relying on the `isDirty` flag, which can desync from canvas content
// on WidgetPainter remount or on non-primary pointerdown variants where
// the closure-local stroke bookkeeping was bypassed.
// When the canvas is empty, defer to `modelValue` so a workflow-restored
// mask reference (or a pending image-restore) survives. `handleClear`
// explicitly resets `modelValue` so a user-initiated clear still yields ''.
if (isCanvasPixelEmpty(el)) {
if (isDirty.value) {
modelValue.value = ''
return ''
}
return modelValue.value
}
// Canvas has visible content. If we already uploaded this exact content
// (no new strokes since last successful upload) and the cached value is
// valid, reuse it to avoid redundant uploads.
if (!isDirty.value && modelValue.value) return modelValue.value
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/composables/painter/usePainter.ts` around lines 643 - 655, When detecting
an empty canvas in usePainter (the isCanvasPixelEmpty(el) check), do not return
the old modelValue if the canvas was modified (isDirty.value === true); instead
clear the cached reference and return '' so erased/restored masks do not
reappear. Concretely: in the empty-canvas branch, if isDirty.value is true set
modelValue.value = '' (or otherwise clear the cached upload ref) and return '';
only fall back to modelValue when the canvas is empty AND isDirty is false.
Update the logic around isCanvasPixelEmpty, isDirty, and modelValue and add a
regression test "erase restored mask to fully transparent pixels" that simulates
restoring a mask then erasing it to fully transparent to assert serialize/upload
yields '' (handleClear remains unchanged).

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

Labels

size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant