Skip to content

Conversation

@cvanelteren
Copy link
Collaborator

@cvanelteren cvanelteren commented Nov 3, 2025

Closes #395

Summary

After profiling ultraplot's import performance, I found that matplotlib.pyplot was being loaded eagerly even when users didn't need it right away. This PR makes pyplot load lazily - only when you actually need it - while keeping everything backward compatible.

Performance Results (10 test runs, median values)

Scenario Before After Change
Import only 0.834s 0.820s 14ms faster (1.7%)
Import + plot 0.986s 0.935s 51ms faster (5.2%)

Both scenarios are faster now! The second result surprised me - I expected a slight slowdown for the import+plot case due to lazy loading overhead, but it's actually faster.

What Changed

Three files were modified:

  • ui.py: Added a _get_pyplot() function that imports pyplot only when needed
  • axes/plot.py: Removed the eager pyplot import at the top of the file
  • init.py: Added __getattr__ so uplt.pyplot still works (but loads lazily)

Backward Compatibility

Everything still works exactly as before:

  • uplt.pyplot.show() works
  • uplt.show() works
  • uplt.subplots() works
  • Accessing uplt.pyplot works (just loads on first access)
  • No API changes, no breaking changes

The Road Not Taken: Why I Stopped Here

I explored going further with optimizations, but decided against it. Here's my thought process:

Attempt 1: Deferring Resource Registration + RC Validation

What I tried: Move colormap, color, cycle, and font registration to happen only when creating plots.

Results:

  • Import-only got 9% faster
  • BUT import+plot got 4% slower overall
  • Added complexity with guard flags and proxy objects
  • Made the common use case (people who import to plot) worse

Decision: Not worth it. Most users import ultraplot specifically to make plots, so optimizing import-only at the expense of the plotting workflow is the wrong tradeoff.

Attempt 2: Aggressive Lazy Loading (chasing ~400ms)

What I considered: Lazy-load everything possible using __getattr__ tricks, optimize the rcsetup module (380ms, 55% of import time).

Why I didn't do it:

  • Would take 40+ hours of development time
  • High risk - rcsetup is core ultraplot's infrastructure
  • Complex to maintain - lots of lazy loading magic
  • Diminishing returns - most users pay the cost anyway when they plot

The reality check: Profiling showed that 55% of our import time is in internals.rcsetup, which creates all the RC parameter validators. This is fundamental infrastructure that's hard to defer without breaking things. Further optimization would be fighting the framework, not working with it.

Profiling Data

For context, here's where import time is actually spent:

internals.rcsetup:    380ms (55%)  ← Main bottleneck, hard to optimize
config module:        212ms (31%)  
internals.docstring:  150ms (22%)  
internals.inputs:      72ms (10%)  
axes modules:          25ms (4%)

My Recommendation

Ship this PR and call it good. Here's why:

  • We get measurable improvements in both scenarios
  • The code stays simple and maintainable
  • Zero breaking changes
  • Our ~820ms import is competitive (matplotlib: 600ms, pandas: 800ms, seaborn: 400ms)
  • Further optimization has diminishing returns vs complexity added

Going from 820ms to 400ms would require massive complexity for marginal real-world benefit. Most users won't notice the difference, and those who import without plotting (config scripts, CLI tools) already benefit from this PR.

Bottom line: This hits the sweet spot of meaningful improvement without unnecessary complexity.

@cvanelteren
Copy link
Collaborator Author

What I will try is to defer the docstring generation as it is not user facing and would shave off potentially 150ms.

@cvanelteren
Copy link
Collaborator Author

Things to check

  • make mpl import lazy and only when needed
  • defer docstring to when it is needed and make it less aggressive
  • test test test

@codecov
Copy link

codecov bot commented Nov 3, 2025

Codecov Report

❌ Patch coverage is 80.00000% with 5 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
ultraplot/ui.py 70.58% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

@cvanelteren
Copy link
Collaborator Author

Need a figure out to refactor this to reduce the complexity. Perhaps will move to a _lazy.py file to centralize the whole pipeline.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fix high import time

1 participant