[P1 performance] Renderer policy is frozen to the first graph opened in PR #124 #129

Open
opened 2026-07-28 22:56:55 +00:00 by lost-rob0t · 0 comments
lost-rob0t commented 2026-07-28 22:56:55 +00:00 (Migrated from github.com)

Finding

In perf/large-graph-responsiveness, GraphCanvas computes Cytoscape renderer options from initialGraphSize.current only when the component mounts:

const rendererOptions = rendererOptionsForGraph(
  initialGraphSize.current.nodeCount,
  initialGraphSize.current.edgeCount
);
const cy = createGraphAdapter({
  // ...
  ...rendererOptions
});

GraphCanvas is not keyed by workspaceId, so switching graphs reuses the same Cytoscape instance. The renderer policy therefore remains based on whichever graph happened to be open at mount.

Examples:

  • Start on a 100-node graph, then switch to 25,000 nodes: pixelRatio, hideEdgesOnViewport, and textureOnViewport remain in the small-graph configuration.
  • Start on a very large graph, then switch to a small graph: the small graph keeps reduced renderer quality.
  • Filtering a graph across the large/very-large thresholds does not update the policy either.

This undermines the main optimization branch and makes benchmark results dependent on scenario order.

Required fix

Use an explicit renderer-policy lifecycle. Since several Cytoscape renderer initialization options are not safely mutable after creation, remount/recreate only when the policy tier changes—not on every graph update.

const rendererTier = graphDetailLevel(
  visibleGraph.nodes.length,
  visibleGraph.edges.length
);

<GraphCanvas
  key={`renderer:${rendererTier}`}
  rendererTier={rendererTier}
  graph={visibleGraph}
  workspaceId={activeGraph?.id || "all-documents"}
  // ...
/>

Inside GraphCanvas, derive options from the passed tier rather than an initial ref:

const rendererOptions = rendererOptionsForTier(rendererTier);
const cy = createGraphAdapter({
  container,
  elements: [],
  style: themedGraphStyle(),
  ...rendererOptions
});

Before a tier-triggered remount, flush and restore:

const snapshot = {
  viewport: { pan: cy.pan(), zoom: cy.zoom() },
  selectedIds: cy.$("node:selected").map((node) => node.id())
};

Add hysteresis or stable tiers if filter changes near a threshold would cause repeated remounts.

Benchmark fix

The benchmark must randomize or explicitly test scenario order:

small -> very-large -> small
very-large -> small -> very-large

Assert the active renderer tier after each switch. Otherwise a fresh page per scenario can hide this production bug.

Acceptance criteria

  • Renderer settings match the currently visible graph tier.
  • Switching between small and very large graphs applies the correct policy.
  • Viewport and selection survive tier changes.
  • Filtering near thresholds does not create a remount loop.
  • Benchmark coverage includes mixed-size graph switching in one mounted application session.
## Finding In `perf/large-graph-responsiveness`, `GraphCanvas` computes Cytoscape renderer options from `initialGraphSize.current` only when the component mounts: ```js const rendererOptions = rendererOptionsForGraph( initialGraphSize.current.nodeCount, initialGraphSize.current.edgeCount ); const cy = createGraphAdapter({ // ... ...rendererOptions }); ``` `GraphCanvas` is not keyed by `workspaceId`, so switching graphs reuses the same Cytoscape instance. The renderer policy therefore remains based on whichever graph happened to be open at mount. Examples: - Start on a 100-node graph, then switch to 25,000 nodes: `pixelRatio`, `hideEdgesOnViewport`, and `textureOnViewport` remain in the small-graph configuration. - Start on a very large graph, then switch to a small graph: the small graph keeps reduced renderer quality. - Filtering a graph across the large/very-large thresholds does not update the policy either. This undermines the main optimization branch and makes benchmark results dependent on scenario order. ## Required fix Use an explicit renderer-policy lifecycle. Since several Cytoscape renderer initialization options are not safely mutable after creation, remount/recreate only when the policy tier changes—not on every graph update. ```js const rendererTier = graphDetailLevel( visibleGraph.nodes.length, visibleGraph.edges.length ); <GraphCanvas key={`renderer:${rendererTier}`} rendererTier={rendererTier} graph={visibleGraph} workspaceId={activeGraph?.id || "all-documents"} // ... /> ``` Inside `GraphCanvas`, derive options from the passed tier rather than an initial ref: ```js const rendererOptions = rendererOptionsForTier(rendererTier); const cy = createGraphAdapter({ container, elements: [], style: themedGraphStyle(), ...rendererOptions }); ``` Before a tier-triggered remount, flush and restore: ```js const snapshot = { viewport: { pan: cy.pan(), zoom: cy.zoom() }, selectedIds: cy.$("node:selected").map((node) => node.id()) }; ``` Add hysteresis or stable tiers if filter changes near a threshold would cause repeated remounts. ## Benchmark fix The benchmark must randomize or explicitly test scenario order: ```js small -> very-large -> small very-large -> small -> very-large ``` Assert the active renderer tier after each switch. Otherwise a fresh page per scenario can hide this production bug. ## Acceptance criteria - Renderer settings match the currently visible graph tier. - Switching between small and very large graphs applies the correct policy. - Viewport and selection survive tier changes. - Filtering near thresholds does not create a remount loop. - Benchmark coverage includes mixed-size graph switching in one mounted application session.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
nsaspy/quasar-ui#129
No description provided.