-
-
Notifications
You must be signed in to change notification settings - Fork 2.6k
Alleviate song select post-filter update thread hitches by caching a model-to-carousel-item mapping #35628
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
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
…item mapping RFC. This started as a follow-up to ppy#35545 (comment), but increased in scope as I saw a bunch of other places that could benefit a similar optimisation. The villain of this PR is `CheckModelEquality()`. The reasons it is the villain are as follows: - On master, it gets called *a lot* in two particular usage sites which can be thought of as `O(n)` range where `n` is the number of the local user's beatmaps. The first usage site is the one added in the aforementioned PR. It will "only" linearly scan the current selection's group with `CheckModelEquality()`, but worst case scenario, the "current selection's group" could be *pretty much all of the user's beatmaps*. The second bad usage site is `Carousel.refreshAfterSelection()`, where every single carousel item is scanned with `CheckModelEquality()` to determine whether it is the current selection. This in profiling highlights some particularly nasty codegen, like the fact that auto-generated record equality isn't great when you start factoring in record inheritance, because you get stacks like ``` StarDifficultyGroupDefinition.Equals(StarDifficultyGroupDefinition) StarDifficultyGroupDefinition.Equals(object) StarDifficultyGroupDefinition.Equals(GroupDefinition) BeatmapCarousel.CheckModelEquality(object?, object?) ``` which is caused by the `GroupDefinition`-comparing branch of `CheckModelEquality()` being *above* the branches that compare its more-derived sub-records. But that's kind of beside the point because this PR just intends to circumvent *all* of the work. Noting the fact that both affected usage sites have a common goal of "I just want to find a carousel panel for this model", this PR proposes using a dictionary to facilitate retrieving just that. The caveats here are: - This heavily relies on `object.GetHashCode()`. How reliable that is going to be is a bit of a mystery inside of an enigma because `object.GetHashCode()` is, in the same vein as all virtual methods on `object`, one of the most unmaintainable things to use, because objects are free to not implement it, or worse, implement it *wrongly*, and you will *not* notice until something breaks. Tests don't, but it's up in the air as to how much that's worth. - The way this is packaged is ugly because the `Carousel.refreshAfterSelection()` *can't physically access* the mapping, because it's designed to be an abstract being, unless I invent the `FindCarouselItemsForSelection()`. It's OOP crap, sure, and I still continue to not understand the `Carousel`-`BeatmapCarousel` split because it's only made functional problems for me personally, but I'm also not about to merge them into one being over a potentially-throwaway performance PR. All that notwithstanding, I think the gains here are appealing enough to at least consider this, maybe not in precisely this package, but some other form that may be more appealing yet.
Mostly immaterial after the preceding commit, but possibly helpful nonetheless.
peppy
approved these changes
Nov 13, 2025
Member
peppy
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't have any better ideas. Looks about what I would have done to optimise the same point of contention so let's ship it.
This was referenced Nov 21, 2025
This was referenced Dec 8, 2025
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Labels
area:song-select
size/L
type/performance
Deals with performance regressions or fixes without changing functionality.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Fix post-filter update thread hitches by caching a model-to-carousel-item mapping
RFC.
This started as a follow-up to #35545 (comment), but increased in scope as I saw a bunch of other places that could benefit from a similar optimisation.
The villain of this PR is
CheckModelEquality(). The reasons it is the villain are as follows:On master, it gets called a lot in two particular usage sites which can be thought of as$O(n)$ range where $n$ is the number of the local user's beatmaps.
The first usage site is the one added in the aforementioned PR. It will "only" linearly scan the current selection's group with
CheckModelEquality(), but worst case scenario, the "current selection's group" could be pretty much all of the user's beatmaps.The second bad usage site is
Carousel.refreshAfterSelection(), where every single carousel item is scanned withCheckModelEquality()to determine whether it is the current selection.This in profiling highlights some particularly nasty codegen, like the fact that auto-generated record equality isn't great when you start factoring in record inheritance, because you get stacks like
which is caused by the
GroupDefinition-comparing branch ofCheckModelEquality()being above the branches that compare its more-derived sub-records.But that's kind of beside the point because this PR just intends to circumvent all of the work.
Noting the fact that both affected usage sites have a common goal of "I just want to find a carousel panel for this model", this PR proposes using a dictionary to facilitate retrieving just that.
The caveats here are:
This heavily relies on
object.GetHashCode(). How reliable that is going to be is a bit of a mystery inside of an enigma becauseobject.GetHashCode()is, in the same vein as all virtual methods onobject, one of the most unmaintainable things to use, because objects are free to not implement it, or worse, implement it wrongly, and you will not notice until something breaks. Tests don't, but it's up in the air as to how much that's worth. Removing the override of it onBeatmapInfoadded here does break a few tests, so the answer to that question is "it's worth something", I guess?The way this is packaged is ugly because the
Carousel.refreshAfterSelection()can't physically access the mapping, because it's designed to be an abstract being, unless I invent theFindCarouselItemsForSelection()wart. It's OOP crap, sure, and I still continue to not understand theCarousel-BeatmapCarouselsplit because it's only made functional problems for me personally, but I'm also not about to merge them into one being over a potentially-throwaway performance PR.All that notwithstanding, I think the gains here are appealing enough to at least consider this, maybe not in precisely this package, but some other form that may be more appealing yet.
Screenshots from profiling:
Adjust CheckModelEquality() comparison order to be more optimal
Mostly immaterial after the preceding commit, but possibly helpful nonetheless.