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

@Sevans711Sevans711 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)

Sevans711and 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 threadtest/test_dependencies.py Outdated
Co-authored-by: Rajeev Jain <rajeeja@gmail.com>
@Sevans711

Copy link
Copy Markdown
CollaboratorAuthor

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
CollaboratorAuthor

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.
@Sevans711Sevans711 added the improvement Improvements on existing features or infrastructure label Jul 8, 2026
@rajeejarajeeja moved this to 👀 In review in UXarray DevelopmentJul 8, 2026
@Sevans711
Sevans711 requested a review from rajeejaJuly 8, 2026 21:53
@github-actions

github-actionsBot commented Jul 8, 2026

Copy link
Copy Markdown

ASV Benchmarking

Benchmark Comparison Results

Benchmarks that have improved:

ChangeBefore [5154420]After [363e5cb]RatioBenchmark (Parameter)
-11.0±0.07μs9.81±0.2μs0.9bench_connectivity.Connectivity.time_node_edge('120km')
-439M389M0.89face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
-469M418M0.89face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
-630M391M0.62face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
-749M390M0.52face_bounds.FaceBounds.peakmem_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
-4.76±0.02s969±5ms0.2import.Imports.timeraw_import_uxarray
-454M404M0.89mpas_ocean.Gradient.peakmem_gradient('120km')
-552M384M0.7mpas_ocean.Gradient.peakmem_gradient('480km')
-403M354M0.88mpas_ocean.Integrate.peakmem_integrate('120km')
-385M331M0.86mpas_ocean.Integrate.peakmem_integrate('480km')
-377M328M0.87quad_hexagon.QuadHexagon.peakmem_open_dataset
-377M326M0.86quad_hexagon.QuadHexagon.peakmem_open_grid

Benchmarks that have stayed the same:

ChangeBefore [5154420]After [363e5cb]RatioBenchmark (Parameter)
10.6±0.06μs10.4±0.1μs0.98bench_connectivity.Connectivity.time_edge_face('120km')
11.1±0.1μs11.0±0.1μs0.99bench_connectivity.Connectivity.time_edge_face('480km')
10.6±0.2μs10.1±0.05μs0.95bench_connectivity.Connectivity.time_edge_node('120km')
11.0±0.1μs10.2±0.1μs0.92bench_connectivity.Connectivity.time_edge_node('480km')
10.5±0.2μs10.3±0.3μs0.99bench_connectivity.Connectivity.time_face_edge('120km')
10.9±0.2μs10.4±0.2μs0.96bench_connectivity.Connectivity.time_face_edge('480km')
10.6±0.09μs10.2±0.1μs0.96bench_connectivity.Connectivity.time_face_face('120km')
11.1±0.2μs10.5±0.3μs0.95bench_connectivity.Connectivity.time_face_face('480km')
21.8±0.3μs20.9±0.8μs0.96bench_connectivity.Connectivity.time_face_node('120km')
22.1±0.2μs21.0±0.5μs0.95bench_connectivity.Connectivity.time_face_node('480km')
10.9±0.2μs10.2±0.2μs0.94bench_connectivity.Connectivity.time_node_edge('480km')
10.5±0.2μs10.2±0.2μs0.97bench_connectivity.Connectivity.time_node_face('120km')
11.0±0.1μs10.7±0.2μs0.97bench_connectivity.Connectivity.time_node_face('480km')
14.5±0.2ms13.2±0.08ms0.92face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/mpas/QU/oQU480.231010.nc'))
3.65±0.05ms3.41±0.2ms0.94face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/scrip/outCSne8/outCSne8.nc'))
19.1±0.1ms17.7±0.07ms0.93face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/geoflow-small/grid.nc'))
2.17±0.03ms2.08±0.06ms0.96face_bounds.FaceBounds.time_face_bounds(PosixPath('/home/runner/work/uxarray/uxarray/test/meshfiles/ugrid/quad-hexagon/grid.nc'))
939±5ns890±30ns0.95mpas_ocean.CheckNorm.time_check_norm('120km')
896±5ns833±30ns0.93mpas_ocean.CheckNorm.time_check_norm('480km')
804±5ms774±7ms0.96mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('120km')
52.1±0.4ms50.1±2ms0.96mpas_ocean.ConnectivityConstruction.time_face_face_connectivity('480km')
656±8μs598±9μs0.91mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('120km')
593±9μs550±30μs0.93mpas_ocean.ConnectivityConstruction.time_n_nodes_per_face('480km')
5.37±0.04ms5.28±0.1ms0.98mpas_ocean.ConstructFaceLatLon.time_cartesian_averaging('120km')
3.84±0.04ms3.66±0.2ms0.95mpas_ocean.ConstructFaceLatLon.time_cartesian_averaging('480km')
3.45±0.01s3.27±0.04s0.95mpas_ocean.ConstructFaceLatLon.time_welzl('120km')
223±1ms209±3ms0.94mpas_ocean.ConstructFaceLatLon.time_welzl('480km')
18.1±0.01ms16.8±0.07ms0.93mpas_ocean.ConstructTreeStructures.time_ball_tree('120km')
990±20μs968±30μs0.98mpas_ocean.ConstructTreeStructures.time_ball_tree('480km')
10.5±0.02ms9.99±0.1ms0.95mpas_ocean.ConstructTreeStructures.time_kd_tree('120km')
663±10μs605±20μs0.91mpas_ocean.ConstructTreeStructures.time_kd_tree('480km')
688±5ms656±5ms0.95mpas_ocean.CrossSections.time_const_lat('120km', 1)
349±0.9ms333±4ms0.95mpas_ocean.CrossSections.time_const_lat('120km', 2)
180±1ms170±2ms0.95mpas_ocean.CrossSections.time_const_lat('120km', 4)
537±3ms511±5ms0.95mpas_ocean.CrossSections.time_const_lat('480km', 1)
273±0.7ms253±3ms0.93mpas_ocean.CrossSections.time_const_lat('480km', 2)
140±0.7ms133±1ms0.94mpas_ocean.CrossSections.time_const_lat('480km', 4)
24.3±0.1ms23.1±0.6ms0.95mpas_ocean.DualMesh.time_dual_mesh_construction('120km')
2.99±0.03ms2.86±0.1ms0.95mpas_ocean.DualMesh.time_dual_mesh_construction('480km')
958±20ms886±10ms0.93mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', False)
54.3±0.5ms50.2±1ms0.92mpas_ocean.GeoDataFrame.time_to_geodataframe('120km', True)
84.2±0.6ms78.8±2ms0.94mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', False)
5.67±0.1ms5.29±0.3ms0.93mpas_ocean.GeoDataFrame.time_to_geodataframe('480km', True)
173±0.4ms166±5ms0.96mpas_ocean.Gradient.time_gradient('120km')
12.1±0.05ms11.6±0.3ms0.96mpas_ocean.Gradient.time_gradient('480km')
225±1μs205±2μs0.91mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('120km')
132±1μs126±3μs0.96mpas_ocean.HoleEdgeIndices.time_construct_hole_edge_indices('480km')
218±0.8μs203±7μs0.93mpas_ocean.Integrate.time_integrate('120km')
200±2μs186±6μs0.93mpas_ocean.Integrate.time_integrate('480km')
133±4ms126±2ms0.95mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'exclude')
132±2ms126±3ms0.95mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'include')
132±2ms128±2ms0.97mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('120km', 'split')
10.3±0.2ms9.96±0.3ms0.96mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'exclude')
10.3±0.2ms10.2±0.09ms0.99mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'include')
10.7±0.3ms9.78±0.1ms0.92mpas_ocean.MatplotlibConversion.time_dataarray_to_polycollection('480km', 'split')
349±2μs325±4μs0.93mpas_ocean.PointInPolygon.time_face_search_lonlat('120km')
350±4μs346±10μs0.99mpas_ocean.PointInPolygon.time_face_search_lonlat('480km')
327±4μs311±8μs0.95mpas_ocean.PointInPolygon.time_face_search_xyz('120km')
331±2μs312±5μs0.94mpas_ocean.PointInPolygon.time_face_search_xyz('480km')
239±0.4ms239±1ms1.00mpas_ocean.RemapDownsample.time_bilinear_remapping
286±20ms282±5ms0.99mpas_ocean.RemapDownsample.time_inverse_distance_weighted_remapping
4.24±0.09ms4.05±0.1ms0.96mpas_ocean.RemapDownsample.time_nearest_neighbor_remapping
1.38±0.03s1.41±0.01s1.02mpas_ocean.RemapUpsample.time_bilinear_remapping
35.7±0.2ms35.2±0.2ms0.99mpas_ocean.RemapUpsample.time_inverse_distance_weighted_remapping
9.12±0.3ms9.29±0.2ms1.02mpas_ocean.RemapUpsample.time_nearest_neighbor_remapping
28.0±0.5ms26.8±1ms0.96mpas_ocean.ZonalAverage.time_zonal_average('120km')
9.55±2ms6.29±0.04ms~0.66mpas_ocean.ZonalAverage.time_zonal_average('480km')
6.79±0.2ms6.89±0.07ms1.02quad_hexagon.QuadHexagon.time_open_dataset
5.74±0.08ms5.79±0.09ms1.01quad_hexagon.QuadHexagon.time_open_grid

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

@cmdupuis3cmdupuis3 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:mainJul 9, 2026
13 checks passed
@github-project-automationgithub-project-automationBot moved this from 👀 In review to ✅ Done in UXarray DevelopmentJul 9, 2026
@Sevans711
Sevans711 deleted the speedup-import branch July 9, 2026 22:03
@Sevans711

Copy link
Copy Markdown
CollaboratorAuthor

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

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 freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improvementImprovements on existing features or infrastructurerun-benchmarkRun 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

@Sevans711@rajeeja@cmdupuis3@erogluorhan