每列单例 watcher, O(columns)订阅,降低峰值GC,但invalidation显著升高,整体愿意接受

This commit is contained in:
LA
2026-02-01 12:32:30 +08:00
parent 2292661bad
commit 79256ad077
8 changed files with 157 additions and 23 deletions

View File

@@ -11,7 +11,7 @@ namespace osu.Game.Benchmarks
{
public class BenchmarkHitObject : BenchmarkTest
{
[Params(1, 100, 1000)]
[Params(1, 1000, 5000)]
public int Count { get; set; }
[Params(false, true)]

View File

@@ -73,7 +73,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
accentColour = column.AccentColour.GetBoundCopy();
// Use shared column bindable to prevent per-instance allocation via GetBoundCopy().
accentColour = column.AccentColour;
accentColour.BindValueChanged(colour =>
{
largeFaint.Colour = Interpolation.ValueAt(0.8f, colour.NewValue, Color4.White, 0, 1);

View File

@@ -35,6 +35,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2
private Circle hitTargetLine = null!;
private CircularContainer? topIcon;
private Box? topIconBox;
private Bindable<Color4> accentColour = null!;
[Resolved]
@@ -123,7 +124,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
accentColour = column.AccentColour.GetBoundCopy();
// Use the column's shared bindable to avoid per-instance allocations from GetBoundCopy()
accentColour = column.AccentColour;
accentColour.BindValueChanged(colour =>
{
background.Colour = colour.NewValue.Darken(0.2f);
@@ -142,13 +144,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2
double bpm = beatmap.BeatmapInfo.BPM * gameplayClock.GetTrueGameplayRate();
beatInterval = 60000 / bpm;
// cache reference to inner box to avoid LINQ allocations during Update
topIconBox = topIcon?.Children.OfType<Box>().FirstOrDefault();
}
protected override void Update()
{
base.Update();
if (topIcon == null || !topIcon.Children.Any())
if (topIconBox == null)
return;
double progress = (gameplayClock.CurrentTime % beatInterval) / beatInterval;
@@ -156,7 +160,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2
if (progress < gameplayClock.ElapsedFrameTime / beatInterval)
{
double fadeTime = Math.Max(1, beatInterval / 2);
var box = topIcon.Children.OfType<Box>().FirstOrDefault();
var box = topIconBox;
box?.FadeTo(1, fadeTime)
.Then()

View File

@@ -34,6 +34,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2
private Container<Circle> bottomIcon = null!;
private CircularContainer topIcon = null!;
private Box? topIconBox;
private Bindable<Color4> accentColour = null!;
[Resolved]
@@ -162,12 +163,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2
// double bpm = beatmap.BeatmapInfo.BPM;
double bpm = beatmap.ControlPointInfo.TimingPointAt(gameplayClock.CurrentTime).BPM * gameplayClock.GetTrueGameplayRate();
applyBlinkingEffect(topIcon, bpm);
// cache inner box once to avoid repeated LINQ allocation inside scheduled callback
topIconBox = topIcon.Children.OfType<Box>().FirstOrDefault();
applyBlinkingEffect(topIconBox, bpm);
direction.BindTo(scrollingInfo.Direction);
direction.BindValueChanged(onDirectionChanged, true);
accentColour = column.AccentColour.GetBoundCopy();
// Use the column's shared bindable to avoid per-instance allocations from GetBoundCopy()
accentColour = column.AccentColour;
accentColour.BindValueChanged(colour =>
{
background.Colour = colour.NewValue.Darken(0.2f);
@@ -178,13 +182,15 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2
column.TopLevelContainer.Add(CreateProxy());
}
private void applyBlinkingEffect(CircularContainer container, double bpm)
private void applyBlinkingEffect(Box? box, double bpm)
{
if (box == null) return;
double interval = 60000 / bpm;
Scheduler.AddDelayed(() =>
{
container.Children.OfType<Box>().First().FadeTo(1, interval / 2).Then().FadeTo(0, interval / 2);
box.FadeTo(1, interval / 2).Then().FadeTo(0, interval / 2);
}, interval, true);
}

View File

@@ -27,6 +27,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2
private readonly IBindable<Color4> accentColour = new Bindable<Color4>();
private readonly Circle colouredBox;
private Drawable iconContainer = null!;
public Ez2NotePiece()
{
@@ -35,6 +36,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2
CornerRadius = CORNER_RADIUS;
// Masking = true;
iconContainer = CreateIcon();
InternalChildren = new[]
{
new Container
@@ -83,7 +86,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2
RelativeSizeAxes = Axes.X,
Height = 0,
},
CreateIcon(),
iconContainer,
};
}
@@ -99,7 +102,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Ez2
// NoteHeight = (float)config.Get<double>(ManiaRulesetSetting.ColumnWidth);
// specialFactor = (float)config.Get<double>(ManiaRulesetSetting.SpecialFactor);
CreateIcon().Size = new Vector2(DrawWidth / NoteHeight * 0.7f);
if (iconContainer != null)
iconContainer.Size = new Vector2(DrawWidth / NoteHeight * 0.7f);
}
protected virtual Drawable CreateIcon() => new Container

View File

@@ -87,7 +87,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
// 计算 drawSeparator 结果(基于不变的列数和列索引)
shouldDrawSeparator = drawSeparatorImpl(Column.Index, stageDefinition);
accentColour = new Bindable<Color4>(ezSkinConfig.GetColumnColor(stageDefinition.Columns, Column.Index));
// 使用 Column 提供的共享 bindable避免为每个列背景构建新的 Bindable 实例
accentColour = Column.AccentColour;
accentColour.BindValueChanged(colour =>
{
var baseCol = colour.NewValue;
@@ -104,7 +105,18 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
{
base.LoadComplete();
if (Column.BackgroundContainer.Children.OfType<Box>().All(b => b.Name != "Separator"))
bool hasSeparator = false;
foreach (var child in Column.BackgroundContainer.Children)
{
if (child is Box b && b.Name == "Separator")
{
hasSeparator = true;
break;
}
}
if (!hasSeparator)
Column.BackgroundContainer.Add(separator);
if (!Column.BackgroundContainer.Children.Contains(hitOverlay))

View File

@@ -1,6 +1,9 @@
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
@@ -101,9 +104,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
NoteSetName = Column.NoteSetBindable;
NoteSize = Column.NoteSizeBindable;
Column.NoteSetChanged += OnNoteSetChanged;
Column.NoteColourChanged += OnColourChanged;
Column.NoteSizeChanged += OnNoteSizeChanged;
// Use a per-column watcher to avoid creating an event handler delegate per-note.
ColumnWatcher.GetOrCreate(Column).Add(this);
UpdateSize();
}
@@ -153,7 +155,6 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
sideLine.UpdateGlowEffect(NoteColor);
}
}
}
private void OnColourChanged()
@@ -197,13 +198,122 @@ namespace osu.Game.Rulesets.Mania.Skinning.EzStylePro
{
if (isDisposing)
{
Column.NoteSetChanged -= OnNoteSetChanged;
Column.NoteColourChanged -= OnColourChanged;
Column.NoteSizeChanged -= OnNoteSizeChanged;
ColumnWatcher.Remove(Column, this);
}
base.Dispose(isDisposing);
}
// Forwarders used by ColumnWatcher when broadcasting per-column changes to instances.
internal void ForwardOnNoteSetChanged(ValueChangedEvent<string> e) => OnNoteSetChanged(e);
internal void ForwardOnColourChanged() => OnColourChanged();
internal void ForwardOnNoteSizeChanged() => OnNoteSizeChanged();
private class ColumnWatcher
{
private readonly Column column;
private readonly List<WeakReference<EzNoteBase>> notes = new List<WeakReference<EzNoteBase>>();
public ColumnWatcher(Column column)
{
this.column = column;
column.NoteSetChanged += onNoteSetChanged;
column.NoteColourChanged += onNoteColourChanged;
column.NoteSizeChanged += onNoteSizeChanged;
}
public void Add(EzNoteBase note)
{
// store weak reference to avoid preventing note GC if removed elsewhere
notes.Add(new WeakReference<EzNoteBase>(note));
}
public void Remove(EzNoteBase note)
{
notes.RemoveAll(wr => !wr.TryGetTarget(out var target) || ReferenceEquals(target, note));
}
private void onNoteSetChanged(ValueChangedEvent<string> e)
{
// iterate backwards and remove dead weak references in-place to avoid
// allocating a large temporary array when the column has many notes.
for (int i = notes.Count - 1; i >= 0; i--)
{
var wr = notes[i];
if (wr.TryGetTarget(out var target))
target.ForwardOnNoteSetChanged(e);
else
notes.RemoveAt(i);
}
}
private void onNoteColourChanged()
{
for (int i = notes.Count - 1; i >= 0; i--)
{
var wr = notes[i];
if (wr.TryGetTarget(out var target))
target.ForwardOnColourChanged();
else
notes.RemoveAt(i);
}
}
private void onNoteSizeChanged()
{
for (int i = notes.Count - 1; i >= 0; i--)
{
var wr = notes[i];
if (wr.TryGetTarget(out var target))
target.ForwardOnNoteSizeChanged();
else
notes.RemoveAt(i);
}
}
public void Unsubscribe()
{
column.NoteSetChanged -= onNoteSetChanged;
column.NoteColourChanged -= onNoteColourChanged;
column.NoteSizeChanged -= onNoteSizeChanged;
}
public bool IsEmpty => notes.All(wr => !wr.TryGetTarget(out _));
private static readonly Dictionary<Column, ColumnWatcher> watchers = new Dictionary<Column, ColumnWatcher>();
private static readonly object watcher_lock = new object();
public static ColumnWatcher GetOrCreate(Column c)
{
lock (watcher_lock)
{
if (!watchers.TryGetValue(c, out var w))
{
w = new ColumnWatcher(c);
watchers[c] = w;
}
return w;
}
}
public static void Remove(Column c, EzNoteBase note)
{
lock (watcher_lock)
{
if (!watchers.TryGetValue(c, out var w))
return;
w.Remove(note);
if (w.IsEmpty)
{
w.Unsubscribe();
watchers.Remove(c);
}
}
}
}
}
}

View File

@@ -128,7 +128,6 @@ Global
{F5037F74-E137-4940-8CE7-4D3E5DF0FB39}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F5037F74-E137-4940-8CE7-4D3E5DF0FB39}.Release|Any CPU.Build.0 = Release|Any CPU
{5672CA4D-1B37-425B-A118-A8DA26E78938}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5672CA4D-1B37-425B-A118-A8DA26E78938}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5672CA4D-1B37-425B-A118-A8DA26E78938}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5672CA4D-1B37-425B-A118-A8DA26E78938}.Release|Any CPU.Build.0 = Release|Any CPU
{5672CA4D-1B37-425B-A118-A8DA26E78938}.Release|Any CPU.Deploy.0 = Release|Any CPU
@@ -136,9 +135,7 @@ Global
{E8F3B2C4-9D7A-4E6B-8C2F-7A9B12345678}.Release|Any CPU.Build.0 = Release|Any CPU
{E8F3B2C4-9D7A-4E6B-8C2F-7A9B12345678}.Release|Any CPU.Deploy.0 = Release|Any CPU
{E8F3B2C4-9D7A-4E6B-8C2F-7A9B12345678}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E8F3B2C4-9D7A-4E6B-8C2F-7A9B12345678}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E8F3B2C4-9D7A-4E6B-8C2F-7A9B12345678}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E8F3B2C4-9D7A-4E6B-8C2F-7A9B12345678}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E8F3B2C4-9D7A-4E6B-8C2F-7A9B12345678}.Release|Any CPU.ActiveCfg = Release|Any CPU
{E8F3B2C4-9D7A-4E6B-8C2F-7A9B12345678}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection