Skip to content

Audit BaseNode render isolation at 500-node scale - #1063

Draft
snuziale wants to merge 1 commit into
mainfrom
claude/basenode-audit-performance-td8ca5
Draft

Audit BaseNode render isolation at 500-node scale#1063
snuziale wants to merge 1 commit into
mainfrom
claude/basenode-audit-performance-td8ca5

Conversation

@snuziale

@snuziale snuziale commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator

Summary

Comprehensive performance audit of BaseNode and its render path at 500-node scale, with deterministic regression tests and benchmarks to prevent future regressions. All identified issues are fixed and pinned by tests.

Verdict: 500 nodes is comfortably supported. Mount costs ~370ms (one-time), and steady-state interactions are O(1): selecting/hovering a node re-renders exactly that node, position sweeps cost ~2.7ms.
image

Key Changes

New Test & Benchmark Files

  • BaseNode.perf.test.tsx (308 lines): Deterministic render-count regression guards at 500-node scale

    • Mounts N nodes with exactly one body render per node (no cascades)
    • Height write-back performs at most one store write per node on first mount
    • Absolute-position-only changes never re-render node bodies (memo fast path)
    • Single-node interactions (select/hover/edit) re-render only that node
    • Handle resolution occurs exactly once per node on mount
  • ConnectedHandlesContext.perf.test.tsx (133 lines): Subscription-isolation guards

    • Edge changes notify only touched nodes
    • New edges array with identical content notifies nobody (set reuse)
  • BaseNode.bench.tsx (113 lines): Timing benchmarks (non-CI-gating)

    • Mount 500 nodes, per-node cost floor, position-only sweeps, single-node interactions
  • canvas-scale.bench.ts (142 lines): Pure-function benchmarks for hot paths

    • resolveHandles, resolveDisplay, areNodePropsEqualIgnoringPosition, resolveCollisions
  • PERFORMANCE.md (221 lines): Audit findings, current numbers, and design decisions that scale well

  • node-height.test.ts (92 lines): Height computation tests with resolution-context parity

  • ExecutionStatusContext.test.tsx (88 lines): Render-efficiency regression tests for status hooks

Fixture & Utility Files

  • BaseNode.perf-fixtures.tsx: Shared test/bench data builders and provider stack
  • node-height.ts: Extracted height computation logic with public API for seeding nodes

Core Fixes (All Pinned by Tests)

F1: Handles resolved twice per node — FIXED

  • BaseNode now resolves handles once and passes preResolved to useButtonHandles
  • useButtonHandles skips internal resolution when preResolved is provided
  • Manifest path applies field whitelist (strips customPositionAndOffsets, boundary)
  • Override configs keep all runtime fields (onAction, etc.)
  • Added areResolvedHandleGroupsEqual comparator to avoid cascading invalidation

F2: Connect gestures re-resolved toolbars/adornments on all nodes — FIXED

  • statusContext now carries only nodeId, executionState, validationState, mode
  • Removed isConnecting, isSelected, isDragging (neither resolver reads them)
  • Interaction-dependent toolbar behavior remains in offsetToolbar and NodeToolbar props

F3: Execution/validation hooks double-rendered per update — FIXED

  • useNodeExecutionState and useElementValidationStatus now read state via useMemo on context identity
  • Removed useState-in-effect pattern; state available on first render
  • Each update costs one render per node instead of two

Modified Files

  • BaseNode.tsx: Removed isConnecting/isSelected/isDragging from statusContext, integrated height computation, handle resolution refactored
  • useButtonHandles.tsx: Added preResolved path with inert subscription sentinel
  • ExecutionStatusContext.tsx: Switched from useState-in-effect to useMemo getter pattern
  • ValidationStatusContext.tsx: Same pattern as ExecutionStatusContext

https://claude.ai/code/session_01UNiMAk1eE9HTe3N5RD1wHD

Copilot AI lite review requested due to automatic review settings August 18, 2026 00:52

Copilot AI left a comment

Copy link
Copy Markdown

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 18, 2026

Copy link
Copy Markdown
Contributor

Apollo Coded App preview deployments are ready.

Project Status Preview Updated (PT)
apollo-design Ready Preview · Logs Sep 04, 2026, 08:49:43 AM
apollo-docs Ready Preview · Logs Sep 04, 2026, 08:49:43 AM
apollo-landing Ready Preview · Logs Sep 04, 2026, 08:49:43 AM
apollo-vertex Ready Preview · Logs Sep 04, 2026, 08:49:43 AM

@github-actions github-actions Bot added the size:XXL 1,000+ changed lines. label Aug 18, 2026
@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Dependency License Review

  • 1951 package(s) scanned
  • ✅ No license issues found
  • ⚠️ 2 package(s) excluded (see details below)
License distribution
License Packages
MIT 1722
ISC 88
Apache-2.0 55
BSD-3-Clause 27
BSD-2-Clause 23
BlueOak-1.0.0 8
MPL-2.0 4
MIT-0 3
CC0-1.0 3
MIT OR Apache-2.0 2
(MIT OR Apache-2.0) 2
Unlicense 2
LGPL-3.0-or-later 1
Python-2.0 1
CC-BY-4.0 1
(MPL-2.0 OR Apache-2.0) 1
Unknown 1
Artistic-2.0 1
(WTFPL OR MIT) 1
(BSD-2-Clause OR MIT OR Apache-2.0) 1
CC-BY-3.0 1
0BSD 1
(MIT OR CC0-1.0) 1
MIT AND ISC 1
Excluded packages
Package Version License Reason
@img/sharp-libvips-linux-x64 1.3.2 LGPL-3.0-or-later LGPL pre-built binary, not linked
khroma 2.1.0 Unknown MIT per GitHub repo, missing license field in package.json

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

📊 Coverage + size by package

Per-package coverage and bundle size on this PR. New-line coverage = of the source lines this PR adds or changes, the % hit by tests.

Package Coverage New-line coverage Packed (gzip) Unpacked vs main
@uipath/apollo-core 42.01 MB 50.12 MB −1.83 MB
@uipath/apollo-react 40.0% 52.6% (70/133) 7.56 MB 29.14 MB +66.0 KB
@uipath/apollo-ui-icons 2.85 MB 6.91 MB 🆕 new
@uipath/apollo-wind 430.9 KB 2.76 MB +13.3 KB
@uipath/ap-chat 85.8% 43.88 MB 56.51 MB +439.5 KB

"Coverage" is each package's own coverage.include scope (e.g. apollo-core instruments only scripts/). "Packed"/"Unpacked" come from npm pack --dry-run and only cover built packages — "—" means not measured this run (package not affected / not built). "vs main" is the packed (gzipped) delta against the last successful main build (the package-sizes artifact from the Release workflow); "—" there means no main baseline was available this run. The baseline is main's latest build, not this PR's exact merge-base, so it includes any drift since the branch diverged. Packages with no vitest config are omitted.

@github-actions

github-actions Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Storybook visual diff

⚠️ Visual changes detected: 9 changed (of 342 compared, 333 unchanged). View report

Baseline is the deployed main Storybook, so changes merged to main after this branch was last updated can also appear here. Logs

Updated (PT): Sep 04, 2026, 09:06:26 AM

Copilot AI review requested due to automatic review settings August 18, 2026 02:09

Copilot AI left a comment

Copy link
Copy Markdown

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.

Copilot AI review requested due to automatic review settings August 20, 2026 19:57
@snuziale
snuziale force-pushed the claude/basenode-audit-performance-td8ca5 branch from a3c1fe2 to 735fd1e Compare August 20, 2026 19:57

Copilot AI left a comment

Copy link
Copy Markdown

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.

@snuziale
snuziale force-pushed the claude/basenode-audit-performance-td8ca5 branch from 735fd1e to b6bf27d Compare August 26, 2026 17:04
Audit BaseNode's render path at 500-node scale, fix the findings, and pin
the invariants that keep large canvases viable.

Audit and guards:

- PERFORMANCE.md: audit report with baseline numbers, ranked findings
  F1-F6, fix status, and a before/after improvements table
- BaseNode.perf.test.tsx: deterministic render-count guards at N=500 (one
  render per node on mount, position-only sweeps render zero bodies,
  select/hover/data edits re-render exactly one node, height write-back
  converges with at most one store write per node and zero when seeded)
- ConnectedHandlesContext.perf.test.tsx: granular notification guards (an
  edge change notifies only its endpoint nodes; identical edge arrays and
  untouched nodes keep stable Set snapshots)
- BaseNode.bench.tsx + canvas-scale.bench.ts: vitest benchmarks for the
  component pipeline and per-node hot paths (resolveHandles, resolveDisplay,
  memo comparator, resolveCollisions), run via the new `bench` script

Fixes:

- F1: resolve handle configurations once in BaseNode for every source
  (context override, data override, manifest) and pass preResolved to
  useButtonHandles, which skips its internal re-resolution and its
  node-data memo dependency on that path (1000 -> 500 resolution passes
  per 500-node mount). Other callers keep the previous behavior, and the
  manifest-path field whitelist is preserved so previously-ignored
  manifest group fields (customPositionAndOffsets, boundary) still do not
  reach the handle renderers.
- F2: drop interaction state (isConnecting/isSelected/isDragging) from
  statusContext in BaseNode and LoopNode; neither resolveToolbar nor
  resolveAdornments reads it, so connect gestures no longer re-resolve
  toolbars and adornments across all nodes (2000 -> 0 resolver runs per
  gesture at 500 nodes). Interaction-driven toolbar behavior stays in
  offsetToolbar and NodeToolbar props, and the now never-populated
  NodeStatusContext interaction fields are deprecated.
- F3: execution/validation status hooks now read the context getter
  during render (memoized) instead of setState-in-effect: state is
  available on the first render and each published update costs one
  render per node instead of two.
- F4: extract computeBaseNodeHeight to utils/node-height (exported) so
  consumers can seed node.height at creation and skip the mount
  write-back; BaseNode uses the same function so the rule cannot drift.
  It accepts a resolutionContext so heights seeded from raw manifests
  resolve repeat/string-visibility handles exactly like BaseNode.
- F5: add the missing useSmartHandles dependency to
  toolbarSideHandleAffordances (stale toolbar offset under context
  handle overrides).
- F6: keep the previous handleConfigurations identity when resolution
  output is value-identical (areResolvedHandleGroupsEqual), so
  label-only data edits no longer trigger updateNodeInternals DOM
  re-measures or handle element rebuilds.

Bench: 500-node mount improved ~15% (mean ~488ms -> ~412ms, happy-dom,
component code only).

Build hygiene: the rslib entry glob excluded test and stories files but
not bench or perf fixtures, so those compiled into dist and shipped in the
published package requiring vitest and @testing-library/react; Vitest 4
also dropped `**/dist/**` from its default exclude, so the bench glob
matched that build output. Exclude bench and perf fixtures from the rslib
entry and tsconfig (which also stops empty declaration stubs) and exclude
dist from both the test and benchmark runners. Bench files are therefore
no longer covered by the dts type check, matching the existing treatment
of test files.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings September 4, 2026 15:40
@snuziale
snuziale force-pushed the claude/basenode-audit-performance-td8ca5 branch from b6bf27d to b0acfe2 Compare September 4, 2026 15:40

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔵 Needs a closer look

It makes broad, performance-critical changes to BaseNode’s render/handle-resolution pipeline and adds a large new perf-test/bench harness that warrants final human verification.

Review details
  • Files reviewed: 22/22 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment on lines 18 to +55
@@ -22,6 +30,7 @@ export const useButtonHandles = ({
nodeWidth,
nodeHeight,
portalActions,
preResolved,
}: {
handleConfigurations: HandleGroupManifest[];
shouldShowHandles: boolean;
@@ -37,6 +46,14 @@ export const useButtonHandles = ({
nodeHeight?: number;
portalActions?: boolean;

/**
* Set when `handleConfigurations` is already the output of `resolveHandles`
* (templates replaced, repeats expanded, visibility booleans resolved).
* Skips the hook's internal resolution pass and its node-data dependency,
* so the same configuration is never resolved twice per render.
*/
preResolved?: boolean;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pkg:apollo-react size:XXL 1,000+ changed lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants