Overview
Edit session performance on large meshes has several bottlenecks across session begin, per-pick interactions, and commit. This issue tracks a progressive improvement plan — each step is independent and leaves the existing test suite green.
The performance work can be done directly in paraview_backend.py without first extracting backend/selection.py. The selection methods have too many cross-cutting dependencies on backend state (self.source, self.view, self.simple, self.render(), etc.) to benefit from a preventive extraction. The backend refactor can proceed in parallel starting from more self-contained modules (display, coloring, pipeline); selection follows after those have clean interfaces.
See docs/logic_flows/ for detailed flow diagrams.
Data flow and bottlenecks at a glance
─── SESSION BEGIN ───────────────────────────────────────────────────────────
ParaView pipeline (server-side)
source proxy → UpdatePipeline()
│
│ servermanager.Fetch(source) ⚠ B1: O(mesh), collapses MPI ranks
▼
vtkUnstructuredGrid (Python)
│
│ DeepCopy() ⚠ B2: O(mesh), second full copy
▼
edit_session.working_dataset
─── DURING SESSION (every pick) ────────────────────────────────────────────
ParaView side (proxy) Edit session side (working_dataset)
───────────────────── ────────────────────────────────────
renders source in viewport holds all field mutations
SelectSurfaceCells() fires tracks selected cell/surface/point IDs
cell IDs from pick
│
│ Fetch(source) ⚠ B3: O(mesh), every pick
│ + rebuild coord index ⚠ B4: O(n_cells) Python, every pick
│ + coordinate remap
▼
IDs on working_dataset ──────────► edit_session.add/replace_selection()
│
overlay rebuild ⚠ B6: every pick
(surface mode only)
GeometryFilter pick
│
│ coordinate lookup ⚠ B5: O(n_source_points), every pick
│ + boundary map rebuild ⚠ B5b: O(n_cells×faces), every pick
▼
boundary face keys ───────────────► edit_session
─── COMMIT ──────────────────────────────────────────────────────────────────
working_dataset
│
│ vtkXMLUnstructuredGridWriter ⚠ B7: O(mesh) I/O, full file always
▼
output .vtu → load as new pipeline node
Current bottlenecks
Reference mesh for timing: test_data/hyper_sphere-3D-6ref_clipped.vtu (~5.6 M cells). AMD Ryzen 7 8845HS, ~25 GB RAM, Ubuntu 24.04.4 LTS, single MPI rank. See benchmark comment for full before/after tables.
| ID |
Where |
Cost |
Frequency |
~Time (s) |
Status |
| B1 |
Fetch(source) at session begin |
O(mesh), collapses MPI ranks |
once |
~0.029 s (localhost; >> on MPI) |
open |
| B2 |
DeepCopy at session begin |
O(mesh), second full in-memory copy |
once |
~1.4 s |
open |
| B3 |
Fetch(source) inside remap |
O(mesh) transfer |
every pick |
~0.025 s → eliminated |
✅ PR #35 |
| B4 |
Coordinate index rebuild inside remap |
O(n_source_cells) Python loop |
every pick |
~5 s (surface) / ~35 s (volume) → eliminated |
✅ PR #35 |
| B5 |
Coordinate lookup in _surface_keys_from_selected_dataset |
O(n_source_points) |
every pick, surface mode only |
~27–32 s → ~0.07 s (−99.7%) |
✅ PR #37 |
| B5b |
_boundary_codim_elements rebuild in _normalize_surface_keys_to_source_boundary |
O(n_source_cells × faces_per_cell) |
every pick, surface mode only |
included in B5 baseline → session-once (per-pick cost eliminated) |
✅ PR #37 |
| B6 |
Overlay rebuild |
O(selected_cells) |
every pick |
TBD |
open |
| B7 |
Full mesh write + reload at commit |
O(mesh) I/O |
once |
TBD |
open |
| B8 |
Adjacency graph build in _ensure_volume_adjacency / _ensure_surface_adjacency |
O(n_cells × faces) Python→C++ calls |
first grow per session (cached after) |
TBD |
open |
| B9 |
_surface_boundary_map_for_top_cells() lazy build + uncached _top_dimension() in EditSession |
O(n_cells × faces) Python→C++ calls |
first surface pick per session (boundary map); every surface pick (_top_dimension) |
~20 s (1st pick) / ~2 s (subsequent) |
open |
| B10 |
top_dimension scan in _pick_surface_keys_native (backend) |
O(n_cells) Python→C++ loop |
every surface pick |
~2 s |
open |
| B11 |
Re-entry overhead after commit — full session begin (Fetch + DeepCopy + cache builds) paid again on every subsequent field assignment |
O(mesh) |
once per field assignment beyond the first |
~2.5 s (baseline) / ~31.6 s (after PR #37) |
open (#39) |
B3 and B4 are the most impactful because they repeat on every user interaction during a session. The eager build of the B4 cache at session begin (PR #35) moves that cost to session start — measurably slower on large meshes. Lazy initialization (build on first pick instead) would restore fast session open.
Step 1 — Quick wins, no architectural change
Fixes: B3, B4 + pipeline lock correctness bug; B5 investigated — both PassThrough variants ruled out in PV 6.1
✅ Cache the remap (B3, B4) — done in PR #35
_remap_cell_ids_to_edit_target_dataset called Fetch(source) and rebuilt the coordinate index on every single pick. Both are now computed once at begin() and reused for the entire session — world-space coordinates do not change when the user rotates the camera (view transform, not mesh transform).
Known trade-off: the cache is built eagerly at begin(), which makes session start measurably slower on large meshes (confirmed in issue #34 comments). Lazy initialization (defer to first pick) would recover that cost without changing the per-pick result.
❌ PassThroughCellIds on GeometryFilter (B5)
PR #31 ruled out PassThroughPointIds (vtkOriginalPointIds produces an identity mapping through the proxy/Fetch round-trip in PV 6.1).
❌ Ruled out — same failure mode as PassThroughPointIds. PassThroughCellIds was the natural next candidate: for a 3D mesh with N source cells and M boundary faces, if the array carried the source 3-D cell ID (not the output face index 0..M-1), the surface-key mapping could become O(selected_cells) with no source-point index build.
On cube.vtk (64 hex cells, ~96 boundary quads), vtkOriginalCellIds reported value 68 for a selected face, but n_source_cells = 64 (68 ≥ 64). In PV 6.1 the array carries the face's sequential index in the GeometryFilter's PolyData output (0..M-1), not the source 3-D cell ID. Both PassThrough variants produce the wrong reference space through the proxy/Fetch round-trip. All code changes were reverted.
Remaining options for B5 (lower priority — see Step 1 status below):
vtkStaticPointLocator: drop-in C++ replacement for the Python loop in _point_coordinate_indexes. Limited benefit: _edit_source_point_indexes is already cached at session begin (B4), so the uncached path only runs outside edit sessions or on the very first pick of a session.
SelectCellsThrough bypass: skip GeometryFilter entirely; run SelectCellsThrough directly on the source (works on unstructured grids), get cell IDs via vtkOriginalCellIds (already reliable in the click-pick path, §6a), filter to boundary cells using the edit session boundary map. Needs a visibility/depth check to discard interior cells — reuse _surface_element_is_visible from the §6c fallback path. More promising than vtkStaticPointLocator but also made obsolete by Step 2.
✅ Boundary map caching (B5b) — fixed in PR #37
_normalize_surface_keys_to_source_boundary called _boundary_codim_elements(source_dataset) on every pick in surface mode. This rebuilt the full boundary map (O(n_source_cells × faces_per_cell)) each time — the same per-pick rebuild pattern that B4 eliminated for the coordinate index. Fixed in PR #37 (Option 1): set_edit_target_dataset builds _edit_boundary_elements once at session begin and passes it through the surface-pick chain.
✅ Pipeline lock during edit (correctness, not performance) — done in PR #35 / #33
Clicking a different pipeline node while an edit session is active silently broke the pick target. Node-switch events are now blocked (with a warning) when edit_session.active.
Step 1 status — paused in favor of Step 2
The main per-pick wins (B3, B4) are done. Remaining Step 1 work:
| Item |
Decision |
| Deferred B4 initialization (first pick) |
(Optional) Low effort. Moves the cache build from session open to the first pick — session opens faster, but the first pick blocks for the same duration. No reduction in total cost; just different distribution. |
| B5b boundary map caching |
✅ Fixed in PR #37 alongside B5. |
B5 SelectCellsThrough bypass |
Deferred — Step 2 Option 2 (tracked in PR #37 comment). |
B5 vtkStaticPointLocator |
Minimal impact given B4 cache; not worth pursuing. |
Step 2 (file-based edit node) eliminates B3, B4, B5, and B5b entirely by making pick IDs land directly on the edit dataset with no remap. It is the next priority, on a new branch.
✅ Step 2 — Edit pipeline node (PR #37)
Fixes: B3, B4 (remap eliminated); B5 and B5b (session-scoped geometry caches)
Core idea: create a real pipeline node (kind="edit") as the active source during the session. The edit node reads the same backing data as the original node, so pick cell IDs directly index working_dataset with no coordinate-based remapping. A dedicated reader proxy approach was explored first but caused regressions in coloring, LUT, and representation handling (those code paths assume a regular pipeline node); using a real node fits the existing architecture with zero special-casing.
Session begin — two cases
Root reader (no parent node) — open a second Reader proxy on the same backing file. No write needed.
Filter active — write the filter output to a temp .vtu server-side (XMLUnstructuredGridWriter), then open it. Avoids Fetch(source) MPI-rank collapse for the filter case.
In both cases: Fetch(edit_source) → DeepCopy → working_dataset (B2 remains; working_dataset must be a mutable Python object for field assignment and adjacency growth).
Discard: delete edit node, restore pre-edit node visibility, remove temp file.
Commit: write working_dataset to disk, delete edit node, load output as new pipeline node.
What Step 2 fixes
Volume picks: B3, B4 eliminated. Cell IDs from the edit node directly index working_dataset with no remap or coordinate scan. Confirmed fast by comparison with 6101876 (pre-Step-1).
Surface picks: B5 and B5b fixed via session-scoped caches. set_edit_target_dataset builds _edit_source_point_indexes (coordinate index, O(n_points)) and _edit_boundary_elements (boundary face map, O(n_cells×faces)) once at session begin. Both are passed through the surface-pick chain. B5 and B5b were initially marked ✅ after Step 2 but were not actually fixed — _edit_source_point_indexes was removed alongside the remap caches, causing silent per-pick rebuilds. See PR #37 comment for full investigation.
PassThroughPointIds on GeometryFilter is definitively closed: servermanager.Fetch() returns an identity mapping for vtkOriginalPointIds even on meshes with interior points, producing silently wrong face keys (see MIGRATION_LOG.md).
Option 2 (SelectSurfaceCells on volume mesh) — deferred. Tracked in PR #37 comment. Would eliminate the coordinate scan entirely; medium effort; deferred as a future improvement.
Remaining bottleneck — B8 (adjacency growth): for grow selection, _ensure_volume_adjacency / _ensure_surface_adjacency iterate all cells via individual Python→C++ calls — O(n_cells × faces). Cached after first grow per session. Potential fix: vtkStaticCellLinks (C++ build) or numpy connectivity arrays.
Step 3 — Server-side mutations
Fixes: B1 completely
Move field-value mutations server-side (e.g. a ProgrammableFilter driven by a selection mask) to eliminate the last Fetch. This is a significant architectural change and should only be pursued if Step 2 is not sufficient for the target cluster workloads.
Overview
Edit session performance on large meshes has several bottlenecks across session begin, per-pick interactions, and commit. This issue tracks a progressive improvement plan — each step is independent and leaves the existing test suite green.
The performance work can be done directly in
paraview_backend.pywithout first extractingbackend/selection.py. The selection methods have too many cross-cutting dependencies on backend state (self.source,self.view,self.simple,self.render(), etc.) to benefit from a preventive extraction. The backend refactor can proceed in parallel starting from more self-contained modules (display,coloring,pipeline); selection follows after those have clean interfaces.See
docs/logic_flows/for detailed flow diagrams.Data flow and bottlenecks at a glance
Current bottlenecks
Fetch(source)at session beginDeepCopyat session beginFetch(source)inside remap_surface_keys_from_selected_dataset_boundary_codim_elementsrebuild in_normalize_surface_keys_to_source_boundary_ensure_volume_adjacency/_ensure_surface_adjacency_surface_boundary_map_for_top_cells()lazy build + uncached_top_dimension()inEditSession_top_dimension)top_dimensionscan in_pick_surface_keys_native(backend)B3 and B4 are the most impactful because they repeat on every user interaction during a session. The eager build of the B4 cache at session begin (PR #35) moves that cost to session start — measurably slower on large meshes. Lazy initialization (build on first pick instead) would restore fast session open.
Step 1 — Quick wins, no architectural change
Fixes: B3, B4 + pipeline lock correctness bug; B5 investigated — both PassThrough variants ruled out in PV 6.1
✅ Cache the remap (B3, B4) — done in PR #35
_remap_cell_ids_to_edit_target_datasetcalledFetch(source)and rebuilt the coordinate index on every single pick. Both are now computed once atbegin()and reused for the entire session — world-space coordinates do not change when the user rotates the camera (view transform, not mesh transform).Known trade-off: the cache is built eagerly at
begin(), which makes session start measurably slower on large meshes (confirmed in issue #34 comments). Lazy initialization (defer to first pick) would recover that cost without changing the per-pick result.❌ PassThroughCellIds on GeometryFilter (B5)
PR #31 ruled out
PassThroughPointIds(vtkOriginalPointIdsproduces an identity mapping through the proxy/Fetch round-trip in PV 6.1).❌ Ruled out — same failure mode as
PassThroughPointIds.PassThroughCellIdswas the natural next candidate: for a 3D mesh with N source cells and M boundary faces, if the array carried the source 3-D cell ID (not the output face index 0..M-1), the surface-key mapping could become O(selected_cells) with no source-point index build.On
cube.vtk(64 hex cells, ~96 boundary quads),vtkOriginalCellIdsreported value68for a selected face, butn_source_cells = 64(68 ≥ 64). In PV 6.1 the array carries the face's sequential index in the GeometryFilter's PolyData output (0..M-1), not the source 3-D cell ID. Both PassThrough variants produce the wrong reference space through the proxy/Fetch round-trip. All code changes were reverted.Remaining options for B5 (lower priority — see Step 1 status below):
vtkStaticPointLocator: drop-in C++ replacement for the Python loop in_point_coordinate_indexes. Limited benefit:_edit_source_point_indexesis already cached at session begin (B4), so the uncached path only runs outside edit sessions or on the very first pick of a session.SelectCellsThroughbypass: skipGeometryFilterentirely; runSelectCellsThroughdirectly on the source (works on unstructured grids), get cell IDs viavtkOriginalCellIds(already reliable in the click-pick path, §6a), filter to boundary cells using the edit session boundary map. Needs a visibility/depth check to discard interior cells — reuse_surface_element_is_visiblefrom the §6c fallback path. More promising thanvtkStaticPointLocatorbut also made obsolete by Step 2.✅ Boundary map caching (B5b) — fixed in PR #37
_normalize_surface_keys_to_source_boundarycalled_boundary_codim_elements(source_dataset)on every pick in surface mode. This rebuilt the full boundary map (O(n_source_cells × faces_per_cell)) each time — the same per-pick rebuild pattern that B4 eliminated for the coordinate index. Fixed in PR #37 (Option 1):set_edit_target_datasetbuilds_edit_boundary_elementsonce at session begin and passes it through the surface-pick chain.✅ Pipeline lock during edit (correctness, not performance) — done in PR #35 / #33
Clicking a different pipeline node while an edit session is active silently broke the pick target. Node-switch events are now blocked (with a warning) when
edit_session.active.Step 1 status — paused in favor of Step 2
The main per-pick wins (B3, B4) are done. Remaining Step 1 work:
SelectCellsThroughbypassvtkStaticPointLocatorStep 2 (file-based edit node) eliminates B3, B4, B5, and B5b entirely by making pick IDs land directly on the edit dataset with no remap. It is the next priority, on a new branch.
✅ Step 2 — Edit pipeline node (PR #37)
Fixes: B3, B4 (remap eliminated); B5 and B5b (session-scoped geometry caches)
Core idea: create a real pipeline node (
kind="edit") as the active source during the session. The edit node reads the same backing data as the original node, so pick cell IDs directly indexworking_datasetwith no coordinate-based remapping. A dedicated reader proxy approach was explored first but caused regressions in coloring, LUT, and representation handling (those code paths assume a regular pipeline node); using a real node fits the existing architecture with zero special-casing.Session begin — two cases
Root reader (no parent node) — open a second Reader proxy on the same backing file. No write needed.
Filter active — write the filter output to a temp
.vtuserver-side (XMLUnstructuredGridWriter), then open it. AvoidsFetch(source)MPI-rank collapse for the filter case.In both cases:
Fetch(edit_source)→DeepCopy→working_dataset(B2 remains;working_datasetmust be a mutable Python object for field assignment and adjacency growth).Discard: delete edit node, restore pre-edit node visibility, remove temp file.
Commit: write
working_datasetto disk, delete edit node, load output as new pipeline node.What Step 2 fixes
Volume picks: B3, B4 eliminated. Cell IDs from the edit node directly index
working_datasetwith no remap or coordinate scan. Confirmed fast by comparison with6101876(pre-Step-1).Surface picks: B5 and B5b fixed via session-scoped caches.
set_edit_target_datasetbuilds_edit_source_point_indexes(coordinate index, O(n_points)) and_edit_boundary_elements(boundary face map, O(n_cells×faces)) once at session begin. Both are passed through the surface-pick chain. B5 and B5b were initially marked ✅ after Step 2 but were not actually fixed —_edit_source_point_indexeswas removed alongside the remap caches, causing silent per-pick rebuilds. See PR #37 comment for full investigation.PassThroughPointIdsonGeometryFilteris definitively closed:servermanager.Fetch()returns an identity mapping forvtkOriginalPointIdseven on meshes with interior points, producing silently wrong face keys (seeMIGRATION_LOG.md).Option 2 (SelectSurfaceCells on volume mesh) — deferred. Tracked in PR #37 comment. Would eliminate the coordinate scan entirely; medium effort; deferred as a future improvement.
Remaining bottleneck — B8 (adjacency growth): for grow selection,
_ensure_volume_adjacency/_ensure_surface_adjacencyiterate all cells via individual Python→C++ calls — O(n_cells × faces). Cached after first grow per session. Potential fix:vtkStaticCellLinks(C++ build) or numpy connectivity arrays.Step 3 — Server-side mutations
Fixes: B1 completely
Move field-value mutations server-side (e.g. a
ProgrammableFilterdriven by a selection mask) to eliminate the lastFetch. This is a significant architectural change and should only be pursued if Step 2 is not sufficient for the target cluster workloads.