Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@bdach
Copy link
Collaborator

@bdach bdach commented Nov 6, 2025

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 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. Removing the override of it on BeatmapInfo added 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 the FindCarouselItemsForSelection() wart. 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.

Screenshots from profiling:

master this PR
Screenshot 2025-11-06 at 12 03 35 Screenshot 2025-11-06 at 12 06 50
Screenshot 2025-11-06 at 12 04 06 Screenshot 2025-11-06 at 12 07 21

Adjust CheckModelEquality() comparison order to be more optimal

Mostly immaterial after the preceding commit, but possibly helpful nonetheless.

bdach added 2 commits November 6, 2025 12:10
…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.
@bdach bdach requested a review from peppy November 6, 2025 11:52
@bdach bdach self-assigned this Nov 6, 2025
@bdach bdach added type/performance Deals with performance regressions or fixes without changing functionality. area:song-select labels Nov 6, 2025
@bdach bdach moved this from Next up to Pending Review in osu! untitled project Nov 6, 2025
Copy link
Member

@peppy peppy left a 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.

@peppy peppy merged commit b64abbf into ppy:master Nov 13, 2025
7 of 9 checks passed
@github-project-automation github-project-automation bot moved this from Pending Review to Done in osu! untitled project Nov 13, 2025
@bdach bdach deleted the optimise-lookups branch November 14, 2025 06:19
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.

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

2 participants