Refactor SearchContainer filtering operation and fix potential flaws#5154
Refactor SearchContainer filtering operation and fix potential flaws#5154frenzibyte wants to merge 13 commits intoppy:masterfrom
SearchContainer filtering operation and fix potential flaws#5154Conversation
db0cd71 to
7f19ea8
Compare
|
So the source looks sane more or less, but what worries me a touch is the fact that - as far as I can tell - the search container's filtering process will now always traverse its full subtree when looking for children to filter. Previously So in general I'd like to see how much slower (if any slower at all?) it makes filtering work in the game-side settings sidebar. Yes the results are cached and invalidated rarely, but I still want to see what I'm dealing with here exactly in that respect, as the settings sidebar has quite a few things in it already. |
|
Not sure how valid this is, but I've measured the time spent in master diffdiff --git a/osu.Framework/Graphics/Containers/SearchContainer.cs b/osu.Framework/Graphics/Containers/SearchContainer.cs
index 789d92e88..ba5bbd877 100644
--- a/osu.Framework/Graphics/Containers/SearchContainer.cs
+++ b/osu.Framework/Graphics/Containers/SearchContainer.cs
@@ -7,6 +7,7 @@
using System.Linq;
using osu.Framework.Caching;
using osu.Framework.Extensions.IEnumerableExtensions;
+using osu.Framework.Timing;
namespace osu.Framework.Graphics.Containers
{
@@ -84,10 +85,22 @@ protected override void Update()
}
}
+ private static readonly StopwatchClock perf_clock = new StopwatchClock(true);
+ private static int container_count = 0;
+ private static int count = 0;
+
private void performFilter()
{
+ Logging.Logger.Log("Performing layout");
+ double timeBefore = perf_clock.CurrentTime;
+
+ container_count = 0;
+ count = 0;
+
string[] terms = (searchTerm ?? string.Empty).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
Children.OfType<IFilterable>().ForEach(child => match(child, terms, terms.Length > 0, allowNonContiguousMatching));
+
+ Logging.Logger.Log($"Finished layout: {perf_clock.CurrentTime - timeBefore}ms, processed {container_count} containers, and {count} drawables in total.");
}
private static bool match(IFilterable filterable, IEnumerable<string> searchTerms, bool searchActive, bool nonContiguousMatching)
@@ -97,11 +110,14 @@ private static bool match(IFilterable filterable, IEnumerable<string> searchTerm
!filterable.FilterTerms.Any(filterTerm =>
checkTerm(filterTerm, term, nonContiguousMatching))).ToArray();
+ count++;
bool matching = childTerms.Length == 0;
//We need to check the children and should any child match this matches as well
if (filterable is IHasFilterableChildren hasFilterableChildren)
{
+ container_count++;
+
foreach (IFilterable child in hasFilterableChildren.FilterableChildren)
matching |= match(child, childTerms, searchActive, nonContiguousMatching);
}PR diffdiff --git a/osu.Framework/Graphics/Containers/SearchContainer.cs b/osu.Framework/Graphics/Containers/SearchContainer.cs
index 74018aca2..8353e61b2 100644
--- a/osu.Framework/Graphics/Containers/SearchContainer.cs
+++ b/osu.Framework/Graphics/Containers/SearchContainer.cs
@@ -6,6 +6,7 @@
using System.Globalization;
using System.Linq;
using osu.Framework.Caching;
+using osu.Framework.Timing;
namespace osu.Framework.Graphics.Containers
{
@@ -83,10 +84,22 @@ protected override void Update()
}
}
+ private static readonly StopwatchClock perf_clock = new StopwatchClock(true);
+ private static int container_count = 0;
+ private static int count = 0;
+
private void performFilter()
{
+ Logging.Logger.Log("Performing layout");
+ double timeBefore = perf_clock.CurrentTime;
+
+ container_count = 0;
+ count = 0;
+
string[] terms = (searchTerm ?? string.Empty).Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
matchSubTree(this, terms, terms.Length > 0, allowNonContiguousMatching);
+
+ Logging.Logger.Log($"Finished layout: {perf_clock.CurrentTime - timeBefore}ms, processed {container_count} containers, and {count} drawables in total.");
}
/// <summary>
@@ -102,6 +115,8 @@ private static bool matchSubTree(Drawable drawable, IReadOnlyList<string> search
if (drawable.IsPresent && drawable is IContainerEnumerable<Drawable> container)
{
+ container_count++;
+
foreach (var child in container.Children)
matching |= matchSubTree(child, nonMatchingTerms, searchActive, nonContiguousMatching);
}
@@ -124,6 +139,7 @@ private static bool matchSubTree(Drawable drawable, IReadOnlyList<string> search
/// <param name="nonMatchingTerms">The search terms which do not match the specified drawable.</param>
private static bool match(Drawable drawable, IReadOnlyList<string> searchTerms, bool nonContiguousMatching, out IReadOnlyList<string> nonMatchingTerms)
{
+ count++;
nonMatchingTerms = searchTerms;
if (drawable is IFilterable filterable)Unsure how to write a proper benchmark that shows the speed of this new implementation though, since the prior relies on |
|
Just a thought: Would making ie. for any containers osu-side which are hiding groups of settings, they would need to implement |
|
Another alternative would be to require the use of |
Not sure how to feel about implementing
Hmm... maybe that could work, but I'm not entirely sure how valid that is, since it means we'll have to enforce using Note that the |
|
I'd be okay with wrapping |
Was relying on `VisibilityContainer`, which is a very wrong class to use for expanding/contracting buttons.
|
Looks to be feasible after trying that. I've gone and refactored the "hide-able" components in game settings to use I did notice one issue regarding the layout invalidation change I pushed, which is that the search container is actually be invalidated per-frame, destroying the point of a I’m not entirely sure what’s going on, but unless someone beats me to it, I’ll try investigating tomorrow. |
| /// Hiding <see cref="IFilterable"/>s for purposes other than filtering require being wrapped in a | ||
| /// <see cref="VisibilityContainer"/> with its visibility state set to <see cref="Visibility.Hidden"/>. |
There was a problem hiding this comment.
I think this documentation should be on the SearchContainer class, mentioning something like:
Summary:
A container which handles filtering of the child hierarchy.
Remarks:
- Children which are searchable should be marked with the `IFilterable` interface. They do not need to be direct children.
- Containers above `IFilterable`s which should be hidden only when all children are filtered away should be marked with the `IHasFilterableChildren` interface.
- Containers which have the potential to hide children via external means should be `VisibilityContainer`s, in order to correctly exclude hidden child `IFilterable`s from search.
There was a problem hiding this comment.
As mentioned, containers above IFilterables which should hide based on their children IFilterables will still have to be marked with an IFilterable interface, to allow for cases where some child IFilterables may be nested below others:
- Root
- IFilterable Header
- IFilterable 1
- IFilterable 2
- IFilterable 3
- (VisibilityContainer)
- IFilterable 4
Which, with an IHasFilterableChildren interface, would require explicitly specifying that in the property (FilterableChildren => filterables.Append(filterableInVisibility)), and that can get worst if the FilterableChildren impleemntation is inherited by a base class (i.e. how we have the settings overlay currently structured).
In addition, with an IHasFilterableChildren interface, it won't be simple to tell whether a filterable is hidden by a parent VisibilityContainer, unless we mark said container with an IFilterable interface and append it to the children list.
There was a problem hiding this comment.
Do you not find it weird that you may have containers that define as IFilterable and specify zero filter terms? It feels very counter intuitive.
There was a problem hiding this comment.
Is there an existing case of that? I’m going by the concept that no container hide on child filterables without having its own search terms that group them (i.e. a header container).
There was a problem hiding this comment.
I guess it's maybe quite uncommon?
Co-authored-by: Dean Herbert <pe@ppy.sh>
Turns out this is happening due to the osu-framework/osu.Framework/Graphics/Containers/CompositeDrawable.cs Lines 1844 to 1852 in a379f77 The Have pushed one that ignores the |
…in applied" This reverts commit 6da4478.
|
Actually I'm just going to revert that because it only ended up breaking tests, and I'm not entirely sure what can be done here... going to block for now at least. |
|
What needs to happen for this to be unblocked again (besides resolving the merge conflicts)? I am interested in getting this in as I'm trying and failing to work around multiple |
|
If I recall correctly, the I was quite lost in the invalidation flow and kept this blocked for now since it makes the cache pointless and potentially result in performance regression (since the settings overlay seems to have autosize + padding/margin). |
|
OK, but are you planning to get back to this at some point or would I need to go try and fix myself? That's what I'm most interested in. |
|
You might know the invalidation flow better than me, but I plan to give this a second attempt this week. So feel free if you want to beat me to it. |
|
After looking at this pull again after a while I think it has a fatal internal contradiction in the special handling of osu-framework/osu.Framework/Graphics/Containers/SearchContainer.cs Lines 139 to 140 in 26c722e which I'm pretty sure is the ultimate cause of the issue described above wherein the search container will invalidate every frame if no item is matching (because it's flapping between the "reset everything to unfiltered" state and the "everything is filtered" state). I don't know what the fix is yet, but I'd say there is too much in-band data passing to my liking here. Likely we need a separate API to exclude |
|
Superseded by #5530. |
As it turns out, 99915f9 introduced a performance regression in `SearchContainer`. Due to borrowing from ppy#5154 too much, the search logic was changed in order to hook into `InvalidateLayout()` to perform search, rather than just do it when a new internal child was added, as was the case previously. For the sake of completeness, let's review the possible causes for `InvalidateLayout()` firing with respect to filtering: - `AddInternal()`: Valid, as the new child needs to be examined against the current filter, and preserved by reverting to the old code. - `RemoveInternal()`: Mostly irrelevant, as long as the presence of the removed drawable in the container doesn't impact the filter state of any of its siblings. Let's not support that case for now. - `ClearInternal()`: Totally irrelevant, as anything that could be filtered is now gone. - `SetLayoutPosition()`: Hopefully irrelevant. - `UpdateChildrenLife()`: Not relevant. Nothing in `SearchContainer` considers lifetime explicitly. - `FlowContainer.childLayout` becoming invalid: The invalidation flags there are `RequiredParentSizeToFit | Presence`. Neither should be relevant for filtering, given the direction taken with `IConditionalFilterable`. - `Direction` and `Spacing`: 100% irrelevant.
I've initially intended to look at just adding guards against non-present drawables, but the current logic for filtering out drawables was quite convoluted that I looked at first refactoring it with flexibility in mind, then adding the necessary guards to solve the issues pointed out in ppy/osu#18003.