Skip to content

Speed up import and load hvplot lazily#1541

Merged
cmdupuis3 merged 11 commits into
UXARRAY:mainfrom
Sevans711:speedup-import
Jul 9, 2026
Merged

Speed up import and load hvplot lazily#1541
cmdupuis3 merged 11 commits into
UXARRAY:mainfrom
Sevans711:speedup-import

Conversation

@Sevans711

@Sevans711 Sevans711 commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Closes #1539

Overview

Speeds up import uxarray from ~1.7 seconds to ~0.7 seconds, by making hvplot.pandas and hvplot.xarray imports lazy, i.e. only import them when actually initializing a PlotAccessor object for the first time.

PR Checklist

General

  • An issue is linked created and linked
  • Add appropriate labels
  • Filled out Overview and Expected Usage (if applicable) sections

Testing

  • Adequate tests are created if there is new functionality
  • Tests cover all possible logical paths in your function
  • Tests are not too basic (such as simply calling a function and nothing else)

Sevans711 and others added 3 commits June 30, 2026 11:34
Small change which reduces time for "import uxarray" on my machine from 1.7 seconds to 0.7 seconds. Also avoids the strange gray bars in Jupyter until actually making a plot for the first time, instead of immediately when importing uxarray. (The bars apparently came from "import hvplot.pandas" and "import hvplot.xarray"). See also: UXARRAY#1260

This also means hvplot import is lazy, which means we should be able to move hvplot to be an optional dependency instead of a requirement. See also: UXARRAY#1224
Comment thread test/test_dependencies.py Outdated
Co-authored-by: Rajeev Jain <rajeeja@gmail.com>
@Sevans711

Copy link
Copy Markdown
Collaborator Author

We might want to delay merging; I just discovered that import hvplot.pandas and import hvplot.xarray always change the backend to bokeh. Thus, with these changes, lazily importing hvplot means the first plot you make from uxarray will be forced into having bokeh backend, regardless of the backend you tried to set originally. I am creating a bug report on hvplot repository now about this.

@Sevans711

Copy link
Copy Markdown
Collaborator Author

I'm very confused about why my latest changes would cause those crashes… I only added an additional import hvplot and an additional call to hvplot.extension. But, the crashes seem to be like AttributeError: 'UxDataArray' object has no attribute 'plot'. Did you mean: 'hvplot'?

Any guesses on how my changes might have led to this? A quick attempt to reproduce it didn't lead me anywhere either, e.g.:

uxds = ux.tutorial.open_dataset("outCSne30-vortex")
hasattr(uxds, 'plot')

gives True. I can chase this down more if I should prioritize it, but wanted to check in first in case you have a guess already. Seems like it might be related to the __getattr__ architecture?

hvplot has no Store attribute, so the previous guard always skipped the
save/restore and the active backend was still forced to bokeh. Read/write the
backend on holoviews.Store, and only restore backends that were actually loaded
(Store.registry) to avoid breaking rendering on a fresh import.
@Sevans711 Sevans711 added the improvement Improvements on existing features or infrastructure label Jul 8, 2026
@rajeeja rajeeja moved this to 👀 In review in UXarray Development Jul 8, 2026
@Sevans711 Sevans711 requested a review from rajeeja July 8, 2026 21:53
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

ASV Benchmarking

Benchmark Comparison Results

Benchmarks that have improved:

Change Before [5154420] After [363e5cb] Ratio Benchmark (Parameter)
- 11.0±0.07μs 9.81±0.2μs 0.9 bench_connectivity.Connectivity.time_node_edge('120km')
- 439M 389M 0.89 face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
- 469M 418M 0.89 face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
- 630M 391M 0.62 face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
- 749M 390M 0.52 face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
- 4.76±0.02s 969±5ms 0.2 import.Imports.timeraw_import_uxarray
- 454M 404M 0.89 mpas_ocean.Gradient.peakmem_gradient('120km')
- 552M 384M 0.7 mpas_ocean.Gradient.peakmem_gradient('480km')
- 403M 354M 0.88 mpas_ocean.Integrate.peakmem_integrate('120km')
- 385M 331M 0.86 mpas_ocean.Integrate.peakmem_integrate('480km')
- 377M 328M 0.87 quad_hexagon.QuadHexagon.peakmem_open_dataset
- 377M 326M 0.86 quad_hexagon.QuadHexagon.peakmem_open_grid

Benchmarks that have stayed the same:

Change Before [5154420] After [363e5cb] Ratio Benchmark (Parameter)
10.6±0.06μs 10.4±0.1μs 0.98 bench_connectivity.Connectivity.time_edge_face('120km')
11.1±0.1μs 11.0±0.1μs 0.99 bench_connectivity.Connectivity.time_edge_face('480km')
10.6±0.2μs 10.1±0.05μs 0.95 bench_connectivity.Connectivity.time_edge_node('120km')
11.0±0.1μs 10.2±0.1μs 0.92 bench_connectivity.Connectivity.time_edge_node('480km')
10.5±0.2μs 10.3±0.3μs 0.99 bench_connectivity.Connectivity.time_face_edge('120km')
10.9±0.2μs 10.4±0.2μs 0.96 bench_connectivity.Connectivity.time_face_edge('480km')
10.6±0.09μs 10.2±0.1μs 0.96 bench_connectivity.Connectivity.time_face_face('120km')
11.1±0.2μs 10.5±0.3μs 0.95 bench_connectivity.Connectivity.time_face_face('480km')
21.8±0.3μs 20.9±0.8μs 0.96 bench_connectivity.Connectivity.time_face_node('120km')
22.1±0.2μs 21.0±0.5μs 0.95 bench_connectivity.Connectivity.time_face_node('480km')
10.9±0.2μs 10.2±0.2μs 0.94 bench_connectivity.Connectivity.time_node_edge('480km')
10.5±0.2μs 10.2±0.2μs 0.97 bench_connectivity.Connectivity.time_node_face('120km')
11.0±0.1μs 10.7±0.2μs 0.97 bench_connectivity.Connectivity.time_node_face('480km')
14.5±0.2ms 13.2±0.08ms 0.92 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
3.65±0.05ms 3.41±0.2ms 0.94 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
19.1±0.1ms 17.7±0.07ms 0.93 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
2.17±0.03ms 2.08±0.06ms 0.96 face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
939±5ns 890±30ns 0.95 mpas_ocean.CheckNorm.time_check_norm('120km')
896±5ns 833±30ns 0.93 mpas_ocean.CheckNorm.time_check_norm('480km')
804±5ms 774±7ms 0.96 mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('120km')
52.1±0.4ms 50.1±2ms 0.96 mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('480km')
656±8μs 598±9μs 0.91 mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('120km')
593±9μs 550±30μs 0.93 mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('480km')
5.37±0.04ms 5.28±0.1ms 0.98 mpas_ocean.ConstructFaceLatLon.time_cartesian_averaging('120km')
3.84±0.04ms 3.66±0.2ms 0.95 mpas_ocean.ConstructFaceLatLon.time_cartesian_averaging('480km')
3.45±0.01s 3.27±0.04s 0.95 mpas_ocean.ConstructFaceLatLon.time_welzl('120km')
223±1ms 209±3ms 0.94 mpas_ocean.ConstructFaceLatLon.time_welzl('480km')
18.1±0.01ms 16.8±0.07ms 0.93 mpas_ocean.ConstructTreeStructures.time_ball_tree('120km')
990±20μs 968±30μs 0.98 mpas_ocean.ConstructTreeStructures.time_ball_tree('480km')
10.5±0.02ms 9.99±0.1ms 0.95 mpas_ocean.ConstructTreeStructures.time_kd_tree('120km')
663±10μs 605±20μs 0.91 mpas_ocean.ConstructTreeStructures.time_kd_tree('480km')
688±5ms 656±5ms 0.95 mpas_ocean.CrossSections.time_const_lat('120km', 1)
349±0.9ms 333±4ms 0.95 mpas_ocean.CrossSections.time_const_lat('120km', 2)
180±1ms 170±2ms 0.95 mpas_ocean.CrossSections.time_const_lat('120km', 4)
537±3ms 511±5ms 0.95 mpas_ocean.CrossSections.time_const_lat('480km', 1)
273±0.7ms 253±3ms 0.93 mpas_ocean.CrossSections.time_const_lat('480km', 2)
140±0.7ms 133±1ms 0.94 mpas_ocean.CrossSections.time_const_lat('480km', 4)
24.3±0.1ms 23.1±0.6ms 0.95 mpas_ocean.DualMesh.time_dual_mesh_construction('120km')
2.99±0.03ms 2.86±0.1ms 0.95 mpas_ocean.DualMesh.time_dual_mesh_construction('480km')
958±20ms 886±10ms 0.93 mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', False)
54.3±0.5ms 50.2±1ms 0.92 mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', True)
84.2±0.6ms 78.8±2ms 0.94 mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', False)
5.67±0.1ms 5.29±0.3ms 0.93 mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', True)
173±0.4ms 166±5ms 0.96 mpas_ocean.Gradient.time_gradient('120km')
12.1±0.05ms 11.6±0.3ms 0.96 mpas_ocean.Gradient.time_gradient('480km')
225±1μs 205±2μs 0.91 mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('120km')
132±1μs 126±3μs 0.96 mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('480km')
218±0.8μs 203±7μs 0.93 mpas_ocean.Integrate.time_integrate('120km')
200±2μs 186±6μs 0.93 mpas_ocean.Integrate.time_integrate('480km')
133±4ms 126±2ms 0.95 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'exclude')
132±2ms 126±3ms 0.95 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'include')
132±2ms 128±2ms 0.97 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'split')
10.3±0.2ms 9.96±0.3ms 0.96 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'exclude')
10.3±0.2ms 10.2±0.09ms 0.99 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'include')
10.7±0.3ms 9.78±0.1ms 0.92 mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'split')
349±2μs 325±4μs 0.93 mpas_ocean.PointInPolygon.time_face_search_lonlat('120km')
350±4μs 346±10μs 0.99 mpas_ocean.PointInPolygon.time_face_search_lonlat('480km')
327±4μs 311±8μs 0.95 mpas_ocean.PointInPolygon.time_face_search_xyz('120km')
331±2μs 312±5μs 0.94 mpas_ocean.PointInPolygon.time_face_search_xyz('480km')
239±0.4ms 239±1ms 1.00 mpas_ocean.RemapDownsample.time_bilinear_remapping
286±20ms 282±5ms 0.99 mpas_ocean.RemapDownsample.time_inverse_distance_weighted_remapping
4.24±0.09ms 4.05±0.1ms 0.96 mpas_ocean.RemapDownsample.time_nearest_neighbor_remapping
1.38±0.03s 1.41±0.01s 1.02 mpas_ocean.RemapUpsample.time_bilinear_remapping
35.7±0.2ms 35.2±0.2ms 0.99 mpas_ocean.RemapUpsample.time_inverse_distance_weighted_remapping
9.12±0.3ms 9.29±0.2ms 1.02 mpas_ocean.RemapUpsample.time_nearest_neighbor_remapping
28.0±0.5ms 26.8±1ms 0.96 mpas_ocean.ZonalAverage.time_zonal_average('120km')
9.55±2ms 6.29±0.04ms ~0.66 mpas_ocean.ZonalAverage.time_zonal_average('480km')
6.79±0.2ms 6.89±0.07ms 1.02 quad_hexagon.QuadHexagon.time_open_dataset
5.74±0.08ms 5.79±0.09ms 1.01 quad_hexagon.QuadHexagon.time_open_grid

@Sevans711 Sevans711 requested a review from cmdupuis3 July 8, 2026 22:04

@cmdupuis3 cmdupuis3 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Wondering if there's a better way to collect all of the possible optional dependencies in one place, and import the accessors when need.

Doesn't seem necessary atm but would probably be good to use, like, lru_cache or something to force one-time loads across the whole package.

@cmdupuis3 cmdupuis3 merged commit a7509c3 into UXARRAY:main Jul 9, 2026
13 checks passed
@github-project-automation github-project-automation Bot moved this from 👀 In review to ✅ Done in UXarray Development Jul 9, 2026
@Sevans711 Sevans711 deleted the speedup-import branch July 9, 2026 22:03
@Sevans711

Copy link
Copy Markdown
Collaborator Author

Thank you for your review and comments, and merging to main! I think we shouldn't need to worry about making our own cache, because Python already handles import caching nicely. We just have to make sure optional dependencies and any "heavy"/"takes a long time to import" dependencies are only imported when we actually need them, rather than immediately upon import uxarray.

Will continue optional dependency updates / discussion here: #1548

@rajeeja

rajeeja commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Thanks for looking into this.

Sevans711 added a commit that referenced this pull request Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improvement Improvements on existing features or infrastructure run-benchmark Run ASV benchmark workflow

Projects

Status: ✅ Done

Development

Successfully merging this pull request may close these issues.

speed up import and load hvplot lazily

4 participants