PERF: Defer tick materialization during Axes init/clear#31525
PERF: Defer tick materialization during Axes init/clear#31525eendebakpt wants to merge 7 commits intomatplotlib:mainfrom
Conversation
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
5708dc8 to
3ac8224
Compare
timhoffm
left a comment
There was a problem hiding this comment.
Thanks for the PR. The speedup is impressive, and the added complexity (rc caching) is bearable.
Strategically, I would like to move away from single-tick handling, but in the mean time this is a reasonable improvement.
| had_major = bool(self.__dict__.pop('majorTicks', None)) | ||
| had_minor = bool(self.__dict__.pop('minorTicks', None)) | ||
| if had_major or had_minor: | ||
| try: | ||
| self.set_clip_path(self.axes.patch) | ||
| except AttributeError: | ||
| pass |
There was a problem hiding this comment.
Is this a logical change or just rewriting? AFAICS we can impove the comment to something like
Delete the majorTicks / minorTicks instances so that the _LazyTickList descriptor is reactivated.
There was a problem hiding this comment.
This is a logical change as set_clip_path is no longer always executed. I updated the docstring
| try: | ||
| tick = instance._get_tick(major=self._major) | ||
| finally: | ||
| rc._update_raw(orig) |
There was a problem hiding this comment.
I suggest to still define a private _rc_context_raw() context manager to simplify the code. The rcParams handling is very much distracting from the non-trivial tick handling code.
Having tick collections is indeed the way to go. This change is orthogonal as it avoids some tick operations altogether. (but maybe if ticks are really fast that would not matter) |
| # Initialize here for testing; later add API | ||
| self._major_tick_kw = dict() | ||
| self._minor_tick_kw = dict() | ||
| # Snapshot of rcParams at the time of the last ``Axis.clear`` (or |
There was a problem hiding this comment.
Optional, but comments are never rendered, so ReST formatting is a bit of overkill here. Alternatives:
- No quotes but parentheses
[...] time of the last Axis.clear() (or
- Single ticks as used in CPython error messages
[...] time of the last 'Axis.clear'
| @@ -550,24 +550,48 @@ def __init__(self, major): | |||
| def __get__(self, instance, owner): | |||
| if instance is None: | |||
There was a problem hiding this comment.
Since the logic complexity grows, we should afford a description.
| if instance is None: | |
| """Materialize the desciptor to a list containing one properly configured tick.""" | |
| if instance is None: |
| # may call reset_ticks(). Therefore, the final tick list is | ||
| # created and assigned afterwards. | ||
| attr = 'majorTicks' if self._major else 'minorTicks' | ||
| setattr(instance, attr, []) |
There was a problem hiding this comment.
Can we keep one named instance around in the function?
- create an empty list
- assign it to the instance
- create and configure the tick
- add the tick to the list
- return the list
tick_list = []
setattr(instance, attr, tick_list) # must be defined before instance._get_tick()
# create and configure the tick
tick_list.append(tick)
return tick_list
IMHO this is logically clearer than an empty placeholder list that get's replaced later and going back and forward through the instance using multiple getattr/setattr` (which are needed to have one execution path for majorTicks/minorTicks, which in turn is needed because the logical complexity of tick configuring rises and we don't want to duplicate that in two branches for major/minor
| Copy Axis clip state onto a lazily-created Tick. | ||
|
|
||
| `Axis.set_clip_path` runs during ``Axes.__clear`` before any Tick | ||
| exists under the lazy-init refactor, so the clip stored on the |
There was a problem hiding this comment.
"lazy-init refactor" is a development activity/artifact, not an architectual concept. Also, I'm not exactly sure what "lazy-init refactor" is. Is it the introduction of _LazyTickList or this PR? Please rephrase.
| return [self.label, self.offsetText, | ||
| *self.get_major_ticks(), *self.get_minor_ticks()] | ||
|
|
||
| def _propagate_axis_state_to_tick(self, tick): |
There was a problem hiding this comment.
This could be moved to a method tick._configure_for_axis(axis). Conceptually the tick should know how it needs to be configured; e.g. the Axis should not have to know that the tick itself and its gridline need to be clipped.
| try: | ||
| tick = instance._get_tick(major=self._major) | ||
| finally: | ||
| rc._update_raw(orig) |
There was a problem hiding this comment.
I suggest to still define a private _rc_context_raw() context manager to simplify the code. The rcParams handling is very much distracting from the non-trivial tick handling code.
| for spine in self.spines.values(): | ||
| if spine._position is None and spine._transform is self.transData: | ||
| spine._position = ('outward', 0.0) | ||
| spine.set_transform(spine.get_spine_transform()) |
There was a problem hiding this comment.
If we're accessing private information like this, is there a reason we don't just call _ensure_position_is_set like in the comment?
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
a3f1be1 to
d0ed04c
Compare
PR summary
The performance if matplotlibs ticks is a bottleneck in various plots. See for example the discussions and references in #5665, #31012, #29594.
In this PR we prevent materialization of the
_LazyTickListwhen there are no ticks created yet. With the tick-materialization cascade gone from Axes.__clear, the spine transforms the cascade used to install as a side effect are installed explicitly at the end of __clear.Benchmark results (updated):
Benchmark script
Closes #23771.
AI Disclosure
Claude was used in identifying performance bottlenecks related to tick creation. Initially the goal was to create tick collections (as described in one of the references), but this approach seems to be a small change with large impact.
PR checklist