Skip to content

Fixes 31156: release TipTap editors on unmount and load the ELK layout engine on demand - #31157

Open
siddhant1 wants to merge 2 commits into
mainfrom
long-animation-frame-block
Open

Fixes 31156: release TipTap editors on unmount and load the ELK layout engine on demand#31157
siddhant1 wants to merge 2 commits into
mainfrom
long-animation-frame-block

Conversation

@siddhant1

@siddhant1 siddhant1 commented Aug 7, 2026

Copy link
Copy Markdown
Member

Fixes #31156

Two independent main-thread fixes found while investigating Sentry ui.long-animation-frame spans ("Main UI thread blocked") — 826 spans over two weeks, p99 1461ms, max 4304ms, 153.4 s of total blocking.


1. TipTap editors were never destroyed

useCustomEditor creates a ProseMirror Editor in an effect, but cleanup only flipped a local isMounted flag — instance.destroy() was never called (a grep across src/ matches only test files). The view, plugin stack, contenteditable DOM and MutationObserver outlived the component.

It compounds because the editor mounts once per table row — the shared description column (utils/TableColumn.util.tsx) renders RichTextEditorPreviewerNew, which mounts a BlockEditor.

The isDestroyed guard mirrors @tiptap/react's own useEditor cleanup (node_modules/@tiptap/react/dist/index.js:1013, :1039) — <StrictMode> double-invokes effects, so cleanup can run twice.

Verified at runtime

Production build served against a live backend, driving SPA navigation (pushState + popstate) so components unmount without a page reload — a full reload resets the count regardless and proves nothing:

round 0  tags: 16
round 1  my-data: 9    tags: 16
round 2  my-data: 9    tags: 16
round 3  my-data: 9    tags: 16
round 4  my-data: 9    tags: 16

.ProseMirror count returns to exactly the row count every round and drops 16 → 9 on leaving. No compounding.


2. ELK layout engine loaded on every authenticated page

EntityLineageLayoutUtils imported ELKUtil at module scope. That module also exports five pure geometry helpers, so importing any one of them dragged elkjs — 1.37 MB built.

LineageControlButtons imports only centerNodePosition, and is reached statically from Lineage.component.tsx. That was enough to pull elkjs into the shared chunk that 476 route chunks statically import. Every authenticated page downloaded and parsed the lineage layout engine — including glossary, which uses G6/antv-dagre and never calls ELK (OntologyExplorer chunk: 0 occurrences of org.eclipse.elk).

ELKLayout is referenced in exactly one place, inside getELKLayoutedElements, which is already async and already awaited at every call site. Moving the import inside it is behaviour-preserving; the ELK engine itself is untouched.

Measured on a production build

before after
ServicesPage must-load closure 13.13 MB 11.75 MB
glossary terms tab (runtime) 12.57 MB / 203 chunks 11.20 MB / 202 chunks
ELKUtil chunk importers 1 static 0 static, 1 dynamic

−1.37 MB on every authenticated route.

Verified against a live backend

  • Glossary terms tab: 0 elk-related requests, page still renders (7 rows, 7 editors)
  • /lineage: fetches ELKUtil + vendor-elk on demand, renders 83 nodes, 0 console errors

Type of change

  • Bug fix (non-breaking)
  • Performance improvement (no functional change)

Tests

312 tests pass across the affected areas (EntityLineageLayoutUtils, Lineage, BlockEditor, EntityLineage, LineageProvider, Lineage components).

14 suites fail to run with "Jest encountered an unexpected token" — a pre-existing environment issue with the ui-core-components prebuilt dist. Control run on pristine main shows 15 such failures, i.e. this branch has strictly fewer. I also A/B'd the three lineage suites with and without the ELK change: identical failures both ways, so none are caused by this PR.

tsc --noEmit: 0 errors in both changed files. prettier --check: clean.

Not verified: ESLint could not run locally — this branch's eslint.config.mjs requires eslint-plugin-sonarjs, absent from the checkouts available here. Relying on CI.

Manual test steps

Editor leak: open a table of descriptions (/tags/General, a glossary terms tab), run document.querySelectorAll('.ProseMirror').length, navigate away and back within the app (don't reload), re-run. Before: grows. After: returns to the row count.

ELK: open a glossary page with DevTools Network filtered to elk — no requests. Then open /lineageELKUtil and vendor-elk load, and the graph renders.

Checklist

  • Self-reviewed
  • Verified at runtime against a live backend, not just unit tests
  • Confirmed no test regressions via control run and per-change A/B
  • UI screen recording — N/A, no visual change

🤖 Generated with Claude Code


Summary by Gitar

  • Performance:
    • Dynamically import ELKUtil in EntityLineageLayoutUtils to prevent bundling elkjs (~1.37MB) across all authenticated pages

This will update automatically on new commits.

…ulating

`useCustomEditor` creates a ProseMirror `Editor` in an effect but its cleanup
only flipped an `isMounted` flag — `instance.destroy()` was never called
(grep finds it only in test files). The editor's view, plugin state,
contenteditable DOM and MutationObserver therefore outlive the component.

This compounds because the editor is mounted per table row: the description
column renders `RichTextEditorPreviewerNew` for every row, so a page of
descriptions is a page of editors. Measured live on release-2-0: 16 live
`.ProseMirror` instances for 16 rows on `/tags/General`, 7 for 7 on a glossary
terms table. Every pagination click, search keystroke or tab switch abandoned
that many editors for the rest of the session.

The `isDestroyed` guard mirrors `@tiptap/react`'s own `useEditor` cleanup
(node_modules/@tiptap/react/dist/index.js:1013 and :1039) — StrictMode
double-invokes effects, so the cleanup can run twice.

Co-Authored-By: Claude <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 7, 2026 06:16
@siddhant1
siddhant1 requested a review from a team as a code owner August 7, 2026 06:16

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

❌ PR checklist incomplete

This PR cannot be merged until the following are addressed on its linked issue:

The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically.

Maintainers can bypass this check by adding the skip-pr-checks label.

@github-actions github-actions Bot added the UI UI specific issues label Aug 7, 2026
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

🔴 Playwright Results — workflow failed

Validated commit 1def9009f9358cddad405a3feeeb8bf1b75de09d in Playwright run 31160251982, attempt 1.

✅ 0 passed · ❌ 0 failed · 🟡 0 flaky · ⏭️ 0 skipped · 🧰 0 lifecycle flaky

Pipeline and setup failures (6)

  • The build job finished with status failure.
  • Duration-aware shard planning finished with status skipped.
  • Fixture cache restoration finished with status skipped.
  • Seeded fixture preparation finished with status skipped.
  • The Playwright shard matrix was unexpectedly skipped.
  • No expected Playwright shards were declared.

Performance

⚪ Performance metrics unavailable; see the CI and reporting failures above.

Shard Passed Failed Flaky Skipped Lifecycle failed Lifecycle flaky

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

Copilot AI review requested due to automatic review settings August 7, 2026 08:05
@siddhant1 siddhant1 changed the title Fixes 31156: destroy the TipTap editor on unmount so instances stop accumulating Fixes 31156: release TipTap editors on unmount and load the ELK layout engine on demand Aug 7, 2026

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Hi there 👋 Thanks for your contribution!

The OpenMetadata team will review the PR shortly! Once it has been labeled as safe to test, the CI workflows
will start executing and we'll be able to make sure everything is working as expected.

Let us know if you need any help!

@siddhant1 siddhant1 added safe to test Add this label to run secure Github workflows on PRs To release Will cherry-pick this PR into the release branch labels Aug 7, 2026
…demand

`EntityLineageLayoutUtils` mixed two concerns: five pure geometry helpers
(`centerNodePosition`, `getNodesBoundsReactFlow`, `getViewportForBoundsReactFlow`,
...) and two ELK-backed layout functions. Because `ELKUtil` was imported at
module scope, importing any of the pure helpers dragged elkjs — 1.37MB built.

`LineageControlButtons` imports only `centerNodePosition`, and `LineageProvider`
is statically imported by eight lineage components. That was enough to land
elkjs in the shared chunk which 476 route chunks statically import, so every
authenticated page downloaded and parsed the lineage layout engine. Glossary
pages paid for it while rendering with G6/antv-dagre, never calling ELK at all.

`getELKLayoutedElements` and `positionNodesUsingElk` now live in
`utils/Lineage/Layout/ElkLayoutUtils.ts`, alongside a named `loadElkLayout()`
lazy boundary. `EntityLineageLayoutUtils` no longer references ELK in any form,
so the pure helpers cannot re-acquire the dependency by accident.

A plain file split would not have been enough on its own: `LineageProvider` is
the sole caller of the ELK functions and is itself reachable from the shared
cluster, so the engine needs the dynamic boundary regardless. Making it a named
function in an ELK-specific module keeps that explicit rather than burying an
`await import()` in the middle of layout logic.

Measured on a production build:

  ELKUtil chunk importers   1 static -> 0 static, 1 dynamic
  glossary terms tab        12.57 MB -> 11.20 MB   (-1.37 MB, runtime)

Verified against a live backend: the glossary terms tab makes zero elk-related
requests and still renders, and /lineage fetches ELKUtil + vendor-elk on demand
and draws 83 nodes with no console errors.

Co-Authored-By: Claude <noreply@anthropic.com>
@siddhant1
siddhant1 force-pushed the long-animation-frame-block branch from 1def900 to 9a9143e Compare August 7, 2026 10:28
Copilot AI review requested due to automatic review settings August 7, 2026 10:28

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@gitar-bot

gitar-bot Bot commented Aug 7, 2026

Copy link
Copy Markdown
Code Review ✅ Approved

Releases TipTap editors on component unmount and lazy-loads the ELK layout engine to reduce bundle size and prevent memory leaks. No issues found.

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

❌ UI Checkstyle Failed

❌ ESLint + Prettier + Organise Imports (src)

One or more source files have linting or formatting issues.

Affected files
  • openmetadata-ui/src/main/resources/ui/src/context/LineageProvider/LineageProvider.tsx

❌ Core Components - I18n Sync

Core-components t() keys, locale files, or language-set are out of sync. Run yarn check-i18n-all locally.

🔍 ESLint findings in this PR's files — 0 error(s), 48 warning(s)

Errors block the build. Warnings do not yet — they are rules whose backlog is still
being worked down, listed so this PR does not add to it. See docs/ui-code-quality-gate.md.

0 error(s), 48 warning(s) across 2 changed file(s).

Count Rule
35 react-hooks/exhaustive-deps
5 sonarjs/cyclomatic-complexity
3 sonarjs/no-duplicate-string
2 sonarjs/no-nested-functions
2 sonarjs/cognitive-complexity
1 sonarjs/no-nested-conditional
All findings
Location Rule Message
🟡 src/components/BlockEditor/hooks/useCustomEditor.ts:128:34 sonarjs/no-nested-functions Refactor this code to not nest functions more than 4 levels deep.
🟡 src/components/BlockEditor/hooks/useCustomEditor.ts:148:6 react-hooks/exhaustive-deps React Hook useEffect was passed a dependency list that is not an array literal. This means we can't statically verify whether you've passed the correct dependen
🟡 src/components/BlockEditor/hooks/useCustomEditor.ts:148:6 react-hooks/exhaustive-deps React Hook useEffect has missing dependencies: 'forceUpdate' and 'options'. Either include them or remove the dependency array.
🟡 src/context/LineageProvider/LineageProvider.tsx:205:62 sonarjs/cyclomatic-complexity {"message":"Function has a complexity of 13 which is greater than 10 authorized.","cost":3,"secondaryLocations":[{"line":205,"column":61,"endLine":205,"endColum
🟡 src/context/LineageProvider/LineageProvider.tsx:336:9 sonarjs/no-nested-conditional Extract this nested ternary operation into an independent statement.
🟡 src/context/LineageProvider/LineageProvider.tsx:470:6 react-hooks/exhaustive-deps React Hook useMemo has an unnecessary dependency: 'isEditMode'. Either exclude it or remove the dependency array.
🟡 src/context/LineageProvider/LineageProvider.tsx:503:7 sonarjs/cognitive-complexity Refactor this function to reduce its Cognitive Complexity from 20 to the 15 allowed.
🟡 src/context/LineageProvider/LineageProvider.tsx:503:7 sonarjs/cyclomatic-complexity {"message":"Function has a complexity of 19 which is greater than 10 authorized.","cost":9,"secondaryLocations":[{"line":503,"column":6,"endLine":503,"endColumn
🟡 src/context/LineageProvider/LineageProvider.tsx:622:5 react-hooks/exhaustive-deps React Hook useCallback has missing dependencies: 'setColumnsHavingLineage', 'setEdges', 'setIsRepositioning', and 'setNodes'. Either include them or remove the
🟡 src/context/LineageProvider/LineageProvider.tsx:681:13 sonarjs/no-duplicate-string Define a constant instead of duplicating this literal 4 times.
🟡 src/context/LineageProvider/LineageProvider.tsx:682:23 sonarjs/no-duplicate-string Define a constant instead of duplicating this literal 4 times.
🟡 src/context/LineageProvider/LineageProvider.tsx:690:5 react-hooks/exhaustive-deps React Hook useCallback has a missing dependency: 't'. Either include it or remove the dependency array.
🟡 src/context/LineageProvider/LineageProvider.tsx:749:5 react-hooks/exhaustive-deps React Hook useCallback has missing dependencies: 'isTourOpen', 'setEdges', 'setNodes', 't', and 'timeFilter'. Either include them or remove the dependency array
🟡 src/context/LineageProvider/LineageProvider.tsx:778:5 react-hooks/exhaustive-deps React Hook useCallback has missing dependencies: 'navigate', 'setActiveLayer', and 'setPlatformView'. Either include them or remove the dependency array.
🟡 src/context/LineageProvider/LineageProvider.tsx:819:26 sonarjs/no-duplicate-string Define a constant instead of duplicating this literal 4 times.
🟡 src/context/LineageProvider/LineageProvider.tsx:850:5 react-hooks/exhaustive-deps React Hook useCallback has missing dependencies: 'exportLineageData', 'showModal', and 't'. Either include them or remove the dependency array.
🟡 src/context/LineageProvider/LineageProvider.tsx:862:75 sonarjs/cyclomatic-complexity {"message":"Function has a complexity of 20 which is greater than 10 authorized.","cost":10,"secondaryLocations":[{"line":862,"column":74,"endLine":862,"endColu
🟡 src/context/LineageProvider/LineageProvider.tsx:984:5 react-hooks/exhaustive-deps React Hook useCallback has missing dependencies: 'lineageData?.downstreamEdges', 'lineageData?.upstreamEdges', 't', and 'updateLineageData'. Either include them
🟡 src/context/LineageProvider/LineageProvider.tsx:1036:5 react-hooks/exhaustive-deps React Hook useCallback has missing dependencies: 'setTracedColumns' and 'setTracedNodes'. Either include them or remove the dependency array.
🟡 src/context/LineageProvider/LineageProvider.tsx:1057:5 react-hooks/exhaustive-deps React Hook useCallback has missing dependencies: 'onPlatformViewChange' and 'setIsPlatformLineage'. Either include them or remove the dependency array.
🟡 src/context/LineageProvider/LineageProvider.tsx:1073:6 react-hooks/exhaustive-deps React Hook useEffect has missing dependencies: 'setSelectedEdge', 'setTracedColumns', and 'setTracedNodes'. Either include them or remove the dependency array.
🟡 src/context/LineageProvider/LineageProvider.tsx:1083:5 react-hooks/exhaustive-deps React Hook useCallback has a missing dependency: 'setTracedColumns'. Either include it or remove the dependency array.
🟡 src/context/LineageProvider/LineageProvider.tsx:1241:5 react-hooks/exhaustive-deps React Hook useCallback has missing dependencies: 'edges', 'removeEdgesBySourceTarget', and 'removeNodeById'. Either include them or remove the dependency array.
🟡 src/context/LineageProvider/LineageProvider.tsx:1433:5 react-hooks/exhaustive-deps React Hook useCallback has a missing dependency: 'setSelectedNode'. Either include it or remove the dependency array.
🟡 src/context/LineageProvider/LineageProvider.tsx:1456:5 react-hooks/exhaustive-deps React Hook useCallback has missing dependencies: 'selectLoadMoreNode', 'setActiveNode', 'setSelectedEdge', and 'setSelectedNode'. Either include them or remove
🟡 src/context/LineageProvider/LineageProvider.tsx:1466:6 react-hooks/exhaustive-deps React Hook useCallback has missing dependencies: 'setActiveNode', 'setSelectedColumn', 'setSelectedNode', 'setTracedColumns', and 'setTracedNodes'. Either inclu
🟡 src/context/LineageProvider/LineageProvider.tsx:1478:6 react-hooks/exhaustive-deps React Hook useCallback has missing dependencies: 'setActiveNode', 'setSelectedEdge', 'setSelectedNode', 'setTracedColumns', and 'setTracedNodes'. Either include
🟡 src/context/LineageProvider/LineageProvider.tsx:1526:6 react-hooks/exhaustive-deps React Hook useCallback has a missing dependency: 'setSelectedEdge'. Either include it or remove the dependency array.
🟡 src/context/LineageProvider/LineageProvider.tsx:1529:33 sonarjs/cyclomatic-complexity {"message":"Function has a complexity of 14 which is greater than 10 authorized.","cost":4,"secondaryLocations":[{"line":1529,"column":32,"endLine":1529,"endCol
🟡 src/context/LineageProvider/LineageProvider.tsx:1626:31 sonarjs/no-nested-functions Refactor this code to not nest functions more than 4 levels deep.
🟡 src/context/LineageProvider/LineageProvider.tsx:1653:5 react-hooks/exhaustive-deps React Hook useCallback has missing dependencies: 'addTracedColumns', 'setColumnsHavingLineage', and 'setTracedNodes'. Either include them or remove the dependen
🟡 src/context/LineageProvider/LineageProvider.tsx:1672:6 react-hooks/exhaustive-deps React Hook useCallback has a missing dependency: 'setSelectedEdge'. Either include it or remove the dependency array.
🟡 src/context/LineageProvider/LineageProvider.tsx:1752:5 react-hooks/exhaustive-deps React Hook useCallback has missing dependencies: 'handleModalCancel' and 'updateEdge'. Either include them or remove the dependency array.
🟡 src/context/LineageProvider/LineageProvider.tsx:1815:5 react-hooks/exhaustive-deps React Hook useCallback has missing dependencies: 'setSelectedEdge' and 'updateEdge'. Either include them or remove the dependency array.
🟡 src/context/LineageProvider/LineageProvider.tsx:1878:5 react-hooks/exhaustive-deps React Hook useCallback has missing dependencies: 'setActiveNode' and 'updateLineageData'. Either include them or remove the dependency array.
🟡 src/context/LineageProvider/LineageProvider.tsx:1883:6 react-hooks/exhaustive-deps React Hook useEffect has a missing dependency: 'redraw'. Either include it or remove the dependency array.
🟡 src/context/LineageProvider/LineageProvider.tsx:1885:47 sonarjs/cognitive-complexity Refactor this function to reduce its Cognitive Complexity from 16 to the 15 allowed.
🟡 src/context/LineageProvider/LineageProvider.tsx:1885:47 sonarjs/cyclomatic-complexity {"message":"Function has a complexity of 21 which is greater than 10 authorized.","cost":11,"secondaryLocations":[{"line":1885,"column":46,"endLine":1885,"endCo
🟡 src/context/LineageProvider/LineageProvider.tsx:1938:6 react-hooks/exhaustive-deps React Hook useCallback has missing dependencies: 'fetchLineageData', 'fetchPlatformLineage', and 'timeFilter'. Either include them or remove the dependency arra
🟡 src/context/LineageProvider/LineageProvider.tsx:1965:6 react-hooks/exhaustive-deps React Hook useEffect has missing dependencies: 'setActiveLayer' and 'setLineageConfig'. Either include them or remove the dependency array.
🟡 src/context/LineageProvider/LineageProvider.tsx:2004:6 react-hooks/exhaustive-deps React Hook useEffect has missing dependencies: 'removeEdgeHandler' and 'removeNodeHandler'. Either include them or remove the dependency array.
🟡 src/context/LineageProvider/LineageProvider.tsx:2010:6 react-hooks/exhaustive-deps React Hook useEffect has a missing dependency: 'redraw'. Either include it or remove the dependency array.
🟡 src/context/LineageProvider/LineageProvider.tsx:2014:6 react-hooks/exhaustive-deps React Hook useEffect has a missing dependency: 'onPlatformViewUpdate'. Either include it or remove the dependency array.
🟡 src/context/LineageProvider/LineageProvider.tsx:2062:6 react-hooks/exhaustive-deps React Hook useMemo has a missing dependency: 'queryFilter'. Either include it or remove the dependency array.
🟡 src/context/LineageProvider/LineageProvider.tsx:2116:6 react-hooks/exhaustive-deps React Hook useEffect has a missing dependency: 'updateActiveLayer'. Either include it or remove the dependency array.
🟡 src/context/LineageProvider/LineageProvider.tsx:2122:6 react-hooks/exhaustive-deps React Hook useEffect has a missing dependency: 'fetchDataQualityLineage'. Either include it or remove the dependency array.
🟡 src/context/LineageProvider/LineageProvider.tsx:2164:6 react-hooks/exhaustive-deps React Hook useMemo has missing dependencies: 'platformView' and 't'. Either include them or remove the dependency array.
🟡 src/context/LineageProvider/LineageProvider.tsx:2169:6 react-hooks/exhaustive-deps React Hook useEffect has a missing dependency: 'reset'. Either include it or remove the dependency array.

Fix locally (fast - only checks files changed in this branch):

make ui-checkstyle-changed

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

Labels

safe to test Add this label to run secure Github workflows on PRs To release Will cherry-pick this PR into the release branch UI UI specific issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Release TipTap editor instances on unmount so long sessions stay responsive

2 participants