Skip to content

Resolve per-type graph styles via data() mappers, not per-type selectors - #2112

Draft
kmcginnes wants to merge 2 commits into
mainfrom
schema-view-style-perf
Draft

Resolve per-type graph styles via data() mappers, not per-type selectors#2112
kmcginnes wants to merge 2 commits into
mainfrom
schema-view-style-perf

Conversation

@kmcginnes

@kmcginnes kmcginnes commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Description

The Schema View locked up and never rendered on databases with very large label sets (~10k node types + ~10k edge types). Root cause: createGraphStyles emitted one Cytoscape selector per type (node[type="…"], edge[type="…"]), so Cytoscape resolved every element's style by scanning ~20k selectors — O(elements × contexts) ≈ 4×10⁸, pinning the main thread. A trace attributed ~88% of self-time to the Cytoscape style engine; layout was never reached.

This moves per-type styling off the stylesheet and onto element data: each element's resolved style values are precomputed onto ele.data() as ge_* fields, and a single static node rule + single edge rule read them back through data(…) mappers (plus two gated selectors for the optional cases, node[__iconUrl] and edge[ge_lineDashPattern]). Style-context count drops from ~20k to O(1) in the number of types. The type→style mapping now happens once per element at build time instead of per element × per selector at style time — the same pattern the pre-existing node[__iconUrl] mapper already used, generalized.

Both surfaces that share useGraphStyles are covered: the explorer graph (renderedEntities.ts) and the Schema View (useSchemaGraphData.ts). The Schema View merges its label into the base node/edge rules rather than replacing them, so the ge_* mappers survive. Supporting change: useBackgroundImageMap moves into core/icons so the element-enrichment seam consumes it without a core → modules import.

How to read

  1. graphElementStyleData.ts — start here; the new pure vertexStyleData / edgeStyleData producers that own the per-element precompute (isDark label color, dotted→dashed remap + dash pattern, border-opacity)
  2. useGraphStyles.ts — the collapsed static stylesheet: one node rule + one edge rule of data(ge_*) mappers, replacing createGraphStyles
  3. renderedEntities.ts & useSchemaGraphData.ts — the two element-enrichment seams that write ge_* onto element data
  4. useSchemaGraphStyles.ts — merges the schema label into the base rules (must not clobber the ge_* mappers)
  5. useBackgroundImageMap.ts — relocated to core/icons (file move + import repoint)

docs/adr/20260813-element-data-style-mappers.md records the decision, the O(1) rationale, and the two lockstep constraints (producer/consumer must move together; a stylesheet consumer must merge into — not replace — the base rules).

Validation

  • pnpm checks passes (types + lint + format); pnpm test passes (222 files, 2672 tests).
  • New coverage: pure producer tests for graphElementStyleData, O(1) style-context-count guards on both the explorer and schema stylesheets, a guard that the schema label-merge retains the ge_* mappers, and an empty-labelColor crash guard.
  • Verified live against a real 10k-label Neptune sync: the Schema View renders (previously locked up), no console errors, and Cytoscape style self-time drops from ~88% to ~0% in a fresh trace.

Related Issues

Check List

  • I confirm that my contribution is made under the terms of the Apache 2.0 license.
  • I have verified pnpm checks passes with no errors.
  • I have verified pnpm test passes with no failures.
  • I have covered new added functionality with unit tests if necessary.
  • I have updated documentation if necessary.

The schema view emitted one Cytoscape selector per vertex/edge type
(~20k contexts at 10k labels), making style application O(elements ×
contexts) and locking the main thread so the view never rendered.

Precompute each element's resolved style values onto ele.data() (ge_*
fields) at the element-construction seams and read them back through a
single node rule + single edge rule using data() mappers, plus two
gated rules (node[__iconUrl], edge[ge_lineDashPattern]). Context count
is now O(1) in the number of types. A live 10k sync confirms the schema
view renders with cytoscape style self-time dropping from 87.6% to ~0%.

The schema view merges its label into the base node/edge rules rather
than replacing them, so the ge_* mappers survive; label-text-color
resolution falls back to the default color instead of throwing on an
empty labelColor. useBackgroundImageMap moves to core/icons so the
element-enrichment seam consumes it without a core-to-modules import.
See docs/adr/20260813-element-data-style-mappers.md.

Refs #2104.
…#2118)

`LabelPreview` inlined its own `new Color(...).isDark()` with no guard for the
empty `labelColor` an imported style file can carry, so the preview could throw
where the canvas does not. Both now call `labelTextColorFor`, memoized because
parsing a color is the only non-trivial work here.

Also reads the fallback from `appDefaultEdgeStyle.labelColor` rather than
repeating the hex, and looks up line dash patterns through a `Map` so a
`lineStyle` colliding with `Object.prototype` cannot resolve to a function.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Schema view lockup from per-type Cytoscape style selectors in useGraphStyles

1 participant