Skip to content

PERF: Sticky edges speedup#31244

Open
scottshambaugh wants to merge 5 commits intomatplotlib:mainfrom
scottshambaugh:sticky_edges_speedup
Open

PERF: Sticky edges speedup#31244
scottshambaugh wants to merge 5 commits intomatplotlib:mainfrom
scottshambaugh:sticky_edges_speedup

Conversation

@scottshambaugh
Copy link
Contributor

@scottshambaugh scottshambaugh commented Mar 6, 2026

PR summary

This speeds up the sticky edges handling in autoscale_view. Most artists do not have sticky edges, so:

  • Sticky edges are now lazily constructed
  • Skip non-sticky children for checks, like ticks, spines, figure text, etc
  • We do not add empty sticky edges to x_sticky_lists and y_sticky_lists, which saves us from having to concatenate a large number of empty arrays.

This completely removes the time spent in the circled code paths here, about 60% of _unstale_view_lim and 15% of the total run time in this test script.
image

Profiling test script:

import numpy as np
import matplotlib.pyplot as plt

fig, ax = plt.subplots()

n = 1000
rng = np.random.default_rng(42)
x = rng.random(n)
y = rng.random(n)
for i in range(n):
    ax.scatter(x[i], y[i])

fig.canvas.draw()
plt.close(fig)

AI Disclosure

PR checklist

@scottshambaugh scottshambaugh marked this pull request as draft March 6, 2026 06:04
@scottshambaugh scottshambaugh marked this pull request as ready for review March 6, 2026 06:17
Comment on lines +3019 to +3020
# Use ._children and ._sticky_edges directly, because most extra
# artists in .get_children() (spines, titles, etc.) never have
Copy link
Member

Choose a reason for hiding this comment

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

We should formalize the concept of different categories of children. I would distinguish

  • data children: basically _children
  • auxiliary children: all supporing artists that are needed to draw the Axes itself.
    Names and exact definitions to be bikeshed.

For a start here, I would create Axes._get_data_children() as a private method, so that we can better document what that is and dont have to go into the private _childrenof the shared axes. That would also make the comment part on_children` unnecessary, because what you are writing there is expressed through the code concept.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I like that, updated

x_sticky_lists = []
for ax in x_shared:
for artist in (*ax._children, *ax._axis_map.values()):
sticky_edges = artist._sticky_edges
Copy link
Member

Choose a reason for hiding this comment

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

Slighty worried that this is unnecessary micro-optimization.

Image

Do 20ns per artist really make a difference? If not, I'd rather stay within the public API bundaries

Copy link
Contributor Author

@scottshambaugh scottshambaugh Mar 6, 2026

Choose a reason for hiding this comment

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

It does show up in the results - just the .sticky_edges property accesses are the circled bits in the baseline profiling here. Because autoscale_view is called for each scatter call in the script, this inner loop is O(n^2) with the number of artists. We could defer autoscaling or be smarter about which artists are on the boundary, but that would be a much bigger change.

image

@scottshambaugh
Copy link
Contributor Author

scottshambaugh commented Mar 6, 2026

Rebased, hadn't realized I already made a portion of these changes in #31004

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants