8000 Fix invalidate of extent cache in Layers by Czaki · Pull Request #7972 · napari/napari · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Fix invalidate of extent cache in Layers #7972

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 13, 2025

Conversation

Czaki
Copy link
Collaborator
@Czaki Czaki commented May 27, 2025

References and relevant issues

Description

In #7015 the change from cleaning extent to refresh it miss that _block_refresh prevents from invalidating cache when a callback connected to event expects it.

It also updates pasting spacing information to reduce layer refresh.

@Czaki Czaki added this to the 0.6.2 milestone May 27, 2025
@Czaki Czaki requested a review from a team as a code owner May 27, 2025 16:30
@Czaki Czaki added the bugfix PR with bugfix label May 27, 2025
@github-actions github-actions bot added the tests Something related to our tests label May 27, 2025
Comment on lines +146 to +147
for layer in ll.selection:
layer.refresh(data_displayed=False)
Copy link
Contributor

Choose a reason for hiding this comment

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

would it be possible/good to only fire this for layers where something actually changed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

hm. I think it may require more refactor. And current change already reduce number of refresh call.

self.events.extent()

def _clear_extent_augmented(self) -> None:
"""Clear extent_augmented cache and emit extent_augmented event."""
if '_extent_augmented' in self.__dict__:
Copy link
Contributor

Choose a reason for hiding this comment

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

Why this change? The separation is useful for cases like Points where changing the size does not actually modify the extent, only the extent augmented.

Copy link
Collaborator Author
@Czaki Czaki May 28, 2025

Choose a reason for hiding this comment

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

To not call 2 functions in each update place.
I do not spot the case of Points. Will update it later.

But I just remove function, and no test crash so it is not called in any place except refresh, that it is called next to just _clear_extent

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Should we restore it? But then where to use it?

Copy link
Contributor

Choose a reason for hiding this comment

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

As discussed, these probably can be chained together, or left as one function. Currently this optimization of only calling one was unused.

Copy link
codecov bot commented May 28, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 92.95%. Comparing base (e845801) to head (fa3ef32).
Report is 4 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #7972      +/-   ##
==========================================
+ Coverage   92.91%   92.95%   +0.04%     
==========================================
  Files         647      647              
  Lines       60875    60917      +42     
==========================================
+ Hits        56560    56625      +65     
+ Misses       4315     4292      -23     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@brisvag
Copy link
Contributor
brisvag commented May 29, 2025

The more I look around the more I'm confused... How did you notice this problem, since _block_refresh is not used anywhere in the codebase except for tests? Why is block refresh even a thing in fact, should we just delete that method? We're solving a fake problem I think...

Also, I would think force=True should do what your trying to do (either by removing the block refresh or by adding or if force to that if check), rather than calling a small part of the refresh method manually like you're doing here.

]
elif isinstance(loaded_attr_value, np.ndarray):
if loaded_attr_value.ndim == 1:
with layer._block_refresh():
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@brisvag You may see here where I want to use block of refresh until all properties are updated as calling refresh triggers multiple heavy computation.

I intend to avoid refresh, so force is exactly this that I do not want to use.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, I looked at this PR in 3 different moments and lost track 😅

But you can do force and pass only extent as true, and this keeps everything tidy inside the refresh method. I would remove the whole refresh_blocked machinery, and then do

layer.refresh(extent=True, thumbnail=False, data_displayed=False, highlight=False)

Copy link
Contributor

Choose a reason for hiding this comment

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

We already do that ina number of places, like when swapping shapes mode to refresh highlights only

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No. We cannot do this. In general scenario, the change of scale or units impact rendering and may require reload of data. However, in this specific case, such reload are just obsolete CPU cycles that only slowdown code execution as there are other attributes that may be modified too. So we want to have only one data reload, not more.

Copy link
Contributor
@brisvag brisvag May 29, 2025

Choose a reason for hiding this comment

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

It's literally identical though:

layer.refresh(extent=True, thumbnail=False, data_displayed=False, highlight=False)

Is exactly equivalent to (and ONLY to):

            self._clear_extent()
            self._clear_extent_augmented()

and we'd be using public api and a single source for refreshing isntead of having cache invalidation scattered all over the place. Also, it would allows us to remove this refresh_blocked part.

Unless you refer to if extent: ... as "obsolete CPU cycles", but there's no way that's even close to being relevant here O.o

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No it is not equivalent.
Please see this line:

if self._refresh_blocked:
logger.debug('Layer.refresh blocked: %s', self)
return

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We need to always invalidate extent, and sometimes refresh view of data.

Copy link
Contributor

Choose a reason for hiding this comment

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

I thinkwe lost track of my point here xD Waht I meant was to remove the _refresh_blocked machinery (both the guard and the context manager, and instead only use the line I suggested. I f refresh blocked is removed, that becomes equivalent.

The reason I'm insisiting on this is that the whole reason why #7015 was made was to centralize the spot where refresh happens so that it's easier to reason about, test, and maintain. Having cache invalidation scattered around makes bugs harder to hunt down. Having extra machinery to block refreshing, also. All the parameters in refresh() were added specifically to solve this problem of "I want to refresh but only the important bits".

If you still think my solution is not equivalent, maybe let's hop onto a call so you can explain why, cause I don't see the problem at the moment :P

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If you still think my solution is not equivalent, maybe let's hop onto a call so you can explain why, cause I don't see the problem at the moment :P

Simple example.
You have multiscale data, where only visible part of data is loaded to GPU. When you change scale, you need to select chunks sended to GPU.

Hoverer when you want to update multiple parameters (ex both scale and transform) you want to wait with calculation and fetch of new chunk until both parameters are set.

In the ideal world, such updates such changes should not trigger any data fetch until painting next frame, but it requires refactor, that is already planned for napari-lite.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok we had a call, and I understand the issue now. To summarize my understanding, the issue is that setting scale, translate etc in a layer uses property setters which will fire refresh() (all args as True). This results in many recalculations of not only the extents, but also the slicing and lots of potentially expensive calculation.

Ultimately, the right way to solve this is to have some nice system for delaying/queuing/deduplicating events, but most importantly to remove slicing state from layers.

Since those are both big undertakings, this PR is the best for now :)

@Czaki Czaki requested a review from brisvag June 9, 2025 17:26
Copy link
Contributor
@brisvag brisvag left a comment

Choose a reason for hiding this comment

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

Sorry, forgot to mark as approved last time!

@brisvag brisvag added the ready to merge Last chance for comments! Will be merged in ~24h label Jun 10, 2025
@TimMonko TimMonko merged commit 987a8f4 into napari:main Jun 13, 2025
36 checks passed
@github-actions github-actions bot removed the ready to merge Last chance for comments! Will be merged in ~24h label Jun 13, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bugfix PR with bugfix tests Something related to our tests
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants
0