Skip to content
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

Provides generic version of LifetimeEntryManager #6212

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 38 additions & 15 deletions osu.Framework/Graphics/Performance/LifetimeEntry.cs
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

#nullable disable

using System;

namespace osu.Framework.Graphics.Performance
{
/// <summary>
/// An object for a <see cref="LifetimeEntryManager"/> to consume, which provides a <see cref="LifetimeStart"/> and <see cref="LifetimeEnd"/>.
/// An object for a <see cref="LifetimeEntryManager"/> to consume, which provides a <see cref="LifetimeEntry{T}.LifetimeStart"/> and <see cref="LifetimeEntry{T}.LifetimeEnd"/>.
/// </summary>
/// <remarks>
/// Management of the object which the <see cref="LifetimeEntry"/> refers to is left up to the consumer.
/// </remarks>
public class LifetimeEntry
public class LifetimeEntry : LifetimeEntry<LifetimeEntry>
{
}

/// <summary>
/// The required base type for the <see cref="LifetimeEntryManager{T}"/> to consume, which provides a <see cref="LifetimeEntry{T}.LifetimeStart"/> and <see cref="LifetimeEntry{T}.LifetimeEnd"/>.
/// </summary>
/// <typeparam name="TDerived">The implemented class itself. Used to provide derived category information for base categories using the Curiously Recurring Template Pattern(CRTP).</typeparam>
public abstract class LifetimeEntry<TDerived> where TDerived : LifetimeEntry<TDerived>
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nooooooooooooooooooooooooope. Nope nope nope nope. I'm not having anything that the cpp people invented. Especially not with a name like "curiously recurring". I don't even follow what this is supposed to be doing.

I will not accept this sort of arcane construction here. If this makes-or-breaks the entire series then it might as well be closed right now.

Copy link
Author

Choose a reason for hiding this comment

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

I will not accept this sort of arcane construction here.

Sadly, this is necessary. Manager needs to use this type argument to get the actual type of the field.

I even thought this was a fairly common way of writing modern C#. I just want to clarify here what the real name of this mode is.😕

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm honestly having a difficult time accepting that linked article if it's giving IEquatable<T> as an example of this. IEquatable<T> is nowhere near as difficult to parse! It's not doing this IEquatable<T> where T : IEquatable<T> brain twister thing in any class signature. Its methods accept T.

Copy link
Author

Choose a reason for hiding this comment

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

I just spent some time searching for something I myself learned about this many years ago. Luckily, I found it and I thought this article might be a good explanation.

{
private double lifetimeStart = double.MinValue;

/// <summary>
/// The time at which this <see cref="LifetimeEntry"/> becomes alive in a <see cref="LifetimeEntryManager"/>.
/// The time at which this <see cref="LifetimeEntry{T}"/> becomes alive in a <see cref="LifetimeEntryManager{T}"/>.
/// </summary>
public double LifetimeStart
{
Expand All @@ -30,7 +36,7 @@ public double LifetimeStart
private double lifetimeEnd = double.MaxValue;

/// <summary>
/// The time at which this <see cref="LifetimeEntry"/> becomes dead in a <see cref="LifetimeEntryManager"/>.
/// The time at which this <see cref="LifetimeEntry{T}"/> becomes dead in a <see cref="LifetimeEntryManager{T}"/>.
/// </summary>
public double LifetimeEnd
{
Expand All @@ -42,15 +48,15 @@ public double LifetimeEnd
/// Invoked before <see cref="LifetimeStart"/> or <see cref="LifetimeEnd"/> changes.
/// It is used because <see cref="LifetimeChanged"/> cannot be used to ensure comparator stability.
/// </summary>
internal event Action<LifetimeEntry> RequestLifetimeUpdate;
internal event Action<TDerived>? RequestLifetimeUpdate;

/// <summary>
/// Invoked after <see cref="LifetimeStart"/> or <see cref="LifetimeEnd"/> changes.
/// </summary>
public event Action<LifetimeEntry> LifetimeChanged;
public event Action<TDerived>? LifetimeChanged;

/// <summary>
/// Update <see cref="LifetimeStart"/> of this <see cref="LifetimeEntry"/>.
/// Update <see cref="LifetimeStart"/> of this <see cref="LifetimeEntry{T}"/>.
/// </summary>
protected virtual void SetLifetimeStart(double start)
{
Expand All @@ -59,7 +65,7 @@ protected virtual void SetLifetimeStart(double start)
}

/// <summary>
/// Update <see cref="LifetimeEnd"/> of this <see cref="LifetimeEntry"/>.
/// Update <see cref="LifetimeEnd"/> of this <see cref="LifetimeEntry{T}"/>.
/// </summary>
protected virtual void SetLifetimeEnd(double end)
{
Expand All @@ -68,27 +74,44 @@ protected virtual void SetLifetimeEnd(double end)
}

/// <summary>
/// Updates the stored lifetimes of this <see cref="LifetimeEntry"/>.
/// Updates the stored lifetimes of this <see cref="LifetimeEntry{T}"/>.
/// </summary>
/// <param name="start">The new <see cref="LifetimeStart"/> value.</param>
/// <param name="end">The new <see cref="LifetimeEnd"/> value.</param>
protected void SetLifetime(double start, double end)
{
RequestLifetimeUpdate?.Invoke(this);
// Due to the type constraints of C#, we cannot declare `LifetimeEntry<T> where T = LifetimeEntry<T>` to limit the type of `this` to be `T`. But when used correctly, this will always be the case.
// aka. We can't stop anyone from writing code like this:
// ```csharp
// public class MyEntry : LifetimeEntry<MyEntry> { }
// LifetimeEntry<MyEntry> foo = new();
// ```
// We prevent users from inadvertently writing such code by declaring `LifetimeEntry<T>` as `abstract`, but we cannot prevent it completely.
// ```csharp
// public class NewEntry<T> : LifetimeEntry<T> where T : LifetimeEntry<T> { }
// NewEntry<MyEntry> a = new();
// ```
// Happily, however, the compiler correctly prevents code like this from compiling. This is also enough to deter users from writing incorrect code:
// ```csharp
// public class ErrorEntry : NewEntry<MyEntry> { }
// LifetimeEntryManager<NewEntry<MyEntry>> error1 = new(); // Compiler error.
// LifetimeEntryManager<ErrorEntry> error2 = new(); // Compiler error.
// ```
RequestLifetimeUpdate?.Invoke((TDerived)this);
Comment on lines +83 to +100
Copy link
Collaborator

Choose a reason for hiding this comment

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

Case in point for the above diatribe. If API correctness of a method requires this sort of preamble to convince readers that the API surface is safe, the whole thing is overdone.

Copy link
Author

Choose a reason for hiding this comment

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

Basically, it's the same thing, I'm just trying to illustrate why we still can't get away from typecasting. I simply think that a more detailed explanation will help myself or other users understand the design logic clearly.

These comments are not actually necessary, since I don't really need any additional constraints or commitments. These are written within the design scope of the compiler and I'm not trying to break anything.

I tried to clarify my thinking through comments, but now it seems like this is overkill?

Copy link
Collaborator

Choose a reason for hiding this comment

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

I'm not even sure what that is attempting to explain. Why is the first snippet "unwanted"? It looks like perfectly reasonable code. What is the second snippet supposed to be? What are NewEntry and MyEntry?

Copy link
Author

@Frederisk Frederisk Mar 26, 2024

Choose a reason for hiding this comment

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

I'm not even sure what that is attempting to explain. Why is the first snippet "unwanted"?

Because if the user writes code like that, the cast here will throw an exception.

LifetimeEntry<MyEntry>
// TDerived is "MyEntry" here.
// this.GetType() == LifetimeEntry
...
(MyEntry)this // throw an Exception: A base type cannot be cast to a derived type.

Also notice that I declared this class as an abstract class to prevent users from doing something wrong like this.

Copy link
Author

Choose a reason for hiding this comment

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

However, this subtle error will be exposed through the following compilation errors when writing. I just want to make this clear. After all, this recursive form of generics can be a bit confusing to understand how the compiler will handle it.

Copy link
Author

@Frederisk Frederisk Mar 26, 2024

Choose a reason for hiding this comment

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

What is the second snippet supposed to be? What are NewEntry and MyEntry?

Assumptions about an extreme situation. Although I declared this class as abstract class. But like all abstract classes there are problems that may be encountered. You can't prevent users from implementing an abstract class without taking any meaningful action. This is basically like:

abstract class A {
  // some thing here.
}
class B : A { } // class B is the same as A, but it's no longer abstract!

This makes it possible for users to write illogical code.

Then in the third code block, I actually illustrate that even if this happens, the compiler will correctly prevent the user from doing this.


lifetimeStart = start;
lifetimeEnd = Math.Max(start, end); // Negative intervals are undesired.

LifetimeChanged?.Invoke(this);
LifetimeChanged?.Invoke((TDerived)this);
}

/// <summary>
/// The current state of this <see cref="LifetimeEntry"/>.
/// The current state of this <see cref="LifetimeEntry{T}"/>.
/// </summary>
internal LifetimeEntryState State { get; set; }

/// <summary>
/// Uniquely identifies this <see cref="LifetimeEntry"/> in a <see cref="LifetimeEntryManager"/>.
/// Uniquely identifies this <see cref="LifetimeEntry{T}"/> in a <see cref="LifetimeEntryManager{T}"/>.
/// </summary>
internal ulong ChildId { get; set; }
}
Expand Down
77 changes: 43 additions & 34 deletions osu.Framework/Graphics/Performance/LifetimeEntryManager.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.

#nullable disable

using System;
using System.Collections.Generic;
using System.Diagnostics;

using JetBrains.Annotations;

using osu.Framework.Extensions.ObjectExtensions;
using osu.Framework.Graphics.Containers;
using osu.Framework.Statistics;
Expand All @@ -16,49 +16,58 @@ namespace osu.Framework.Graphics.Performance
/// <summary>
/// Provides time-optimised updates for lifetime change notifications.
/// This is used in specialised <see cref="CompositeDrawable"/>s to optimise lifetime changes (see: <see cref="LifetimeManagementContainer"/>).
/// If you wish to implement your own <see cref="LifetimeEntry{T}"/>, please use the generic version <see cref="LifetimeEntryManager{T}"/>.
/// </summary>
/// <remarks>
/// The time complexity of updating lifetimes is O(number of alive items).
/// </remarks>
public class LifetimeEntryManager
public class LifetimeEntryManager : LifetimeEntryManager<LifetimeEntry>
{
}

/// <summary>
/// Provides time-optimised updates for lifetime change notifications.
/// </summary>
/// <typeparam name="T">Implementation of <see cref="LifetimeEntry{T}"/>. Note that your own class must inherit from the generic version <see cref="LifetimeEntry{T}"/> rather than <see cref="LifetimeEntry"/>.</typeparam>
public class LifetimeEntryManager<T> where T : LifetimeEntry<T>
{
/// <summary>
/// Invoked immediately when a <see cref="LifetimeEntry"/> becomes alive.
/// Invoked immediately when a <see cref="LifetimeEntry{T}"/> becomes alive.
/// </summary>
public event Action<LifetimeEntry> EntryBecameAlive;
public event Action<T>? EntryBecameAlive;

/// <summary>
/// Invoked immediately when a <see cref="LifetimeEntry"/> becomes dead.
/// Invoked immediately when a <see cref="LifetimeEntry{T}"/> becomes dead.
/// </summary>
public event Action<LifetimeEntry> EntryBecameDead;
public event Action<T>? EntryBecameDead;

/// <summary>
/// Invoked when a <see cref="LifetimeEntry"/> crosses a lifetime boundary.
/// Invoked when a <see cref="LifetimeEntry{T}"/> crosses a lifetime boundary.
/// </summary>
public event Action<LifetimeEntry, LifetimeBoundaryKind, LifetimeBoundaryCrossingDirection> EntryCrossedBoundary;
public event Action<T, LifetimeBoundaryKind, LifetimeBoundaryCrossingDirection>? EntryCrossedBoundary;

/// <summary>
/// Contains all the newly-added (but not yet processed) entries.
/// </summary>
private readonly List<LifetimeEntry> newEntries = new List<LifetimeEntry>();
private readonly List<T> newEntries = [];

/// <summary>
/// Contains all the currently-alive entries.
/// </summary>
private readonly List<LifetimeEntry> activeEntries = new List<LifetimeEntry>();
private readonly List<T> activeEntries = [];

/// <summary>
/// Contains all entries that should come alive in the future.
/// </summary>
private readonly SortedSet<LifetimeEntry> futureEntries = new SortedSet<LifetimeEntry>(new LifetimeStartComparator());
private readonly SortedSet<T> futureEntries = new(new LifetimeStartComparator());

/// <summary>
/// Contains all entries that were alive in the past.
/// </summary>
private readonly SortedSet<LifetimeEntry> pastEntries = new SortedSet<LifetimeEntry>(new LifetimeEndComparator());
private readonly SortedSet<T> pastEntries = new(new LifetimeEndComparator());

private readonly Queue<(LifetimeEntry, LifetimeBoundaryKind, LifetimeBoundaryCrossingDirection)> eventQueue =
new Queue<(LifetimeEntry, LifetimeBoundaryKind, LifetimeBoundaryCrossingDirection)>();
private readonly Queue<(T, LifetimeBoundaryKind, LifetimeBoundaryCrossingDirection)> eventQueue =
new();

/// <summary>
/// Used to ensure a stable sort if multiple entries with the same lifetime are added.
Expand All @@ -68,8 +77,8 @@ public class LifetimeEntryManager
/// <summary>
/// Adds an entry.
/// </summary>
/// <param name="entry">The <see cref="LifetimeEntry"/> to add.</param>
public void AddEntry(LifetimeEntry entry)
/// <param name="entry">The <see cref="LifetimeEntry{T}"/> to add.</param>
public void AddEntry(T entry)
{
entry.RequestLifetimeUpdate += requestLifetimeUpdate;
entry.ChildId = ++currentChildId;
Expand All @@ -81,9 +90,9 @@ public void AddEntry(LifetimeEntry entry)
/// <summary>
/// Removes an entry.
/// </summary>
/// <param name="entry">The <see cref="LifetimeEntry"/> to remove.</param>
/// <returns>Whether <paramref name="entry"/> was contained by this <see cref="LifetimeEntryManager"/>.</returns>
public bool RemoveEntry(LifetimeEntry entry)
/// <param name="entry">The <see cref="LifetimeEntry{T}"/> to remove.</param>
/// <returns>Whether <paramref name="entry"/> was contained by this <see cref="LifetimeEntryManager{T}"/>.</returns>
public bool RemoveEntry(T entry)
{
entry.RequestLifetimeUpdate -= requestLifetimeUpdate;

Expand Down Expand Up @@ -160,7 +169,7 @@ public void ClearEntries()
/// <summary>
/// Invoked when the lifetime of an entry is going to changed.
/// </summary>
private void requestLifetimeUpdate(LifetimeEntry entry)
private void requestLifetimeUpdate(T entry)
{
// Entries in the past/future sets need to be re-sorted to prevent the comparer from becoming unstable.
// To prevent, e.g. CompositeDrawable alive children changing during enumeration, the entry's state must not be updated immediately.
Expand All @@ -185,7 +194,7 @@ private void requestLifetimeUpdate(LifetimeEntry entry)
/// <param name="state">The <see cref="LifetimeEntryState"/>.</param>
/// <returns>Either <see cref="futureEntries"/>, <see cref="pastEntries"/>, or null.</returns>
[CanBeNull]
private SortedSet<LifetimeEntry> futureOrPastEntries(LifetimeEntryState state)
private SortedSet<T>? futureOrPastEntries(LifetimeEntryState state)
{
switch (state)
{
Expand Down Expand Up @@ -274,17 +283,17 @@ public bool Update(double startTime, double endTime)
}

/// <summary>
/// Updates the state of a single <see cref="LifetimeEntry"/>.
/// Updates the state of a single <see cref="LifetimeEntry{T}"/>.
/// </summary>
/// <param name="entry">The <see cref="LifetimeEntry"/> to update.</param>
/// <param name="entry">The <see cref="LifetimeEntry{T}"/> to update.</param>
/// <param name="startTime">The start of the time range.</param>
/// <param name="endTime">The end of the time range.</param>
/// <param name="isNewEntry">Whether <paramref name="entry"/> is part of the new entries set.
/// The state may be "new" or "past"/"future", in which case it will undergo further processing to return it to the correct set.</param>
/// <param name="mutateActiveEntries">Whether <see cref="activeEntries"/> should be mutated by this invocation.
/// If <c>false</c>, the caller is expected to handle mutation of <see cref="activeEntries"/> based on any changes to the entry's state.</param>
/// <returns>Whether the state of <paramref name="entry"/> has changed.</returns>
private bool updateChildEntry(LifetimeEntry entry, double startTime, double endTime, bool isNewEntry, bool mutateActiveEntries)
private bool updateChildEntry(T entry, double startTime, double endTime, bool isNewEntry, bool mutateActiveEntries)
{
LifetimeEntryState oldState = entry.State;

Expand Down Expand Up @@ -343,11 +352,11 @@ private bool updateChildEntry(LifetimeEntry entry, double startTime, double endT
/// <summary>
/// Retrieves the new state for an entry.
/// </summary>
/// <param name="entry">The <see cref="LifetimeEntry"/>.</param>
/// <param name="entry">The <see cref="LifetimeEntry{T}"/>.</param>
/// <param name="startTime">The start of the time range.</param>
/// <param name="endTime">The end of the time range.</param>
/// <returns>The state of <paramref name="entry"/>. Can be either <see cref="LifetimeEntryState.Past"/>, <see cref="LifetimeEntryState.Current"/>, or <see cref="LifetimeEntryState.Future"/>.</returns>
private LifetimeEntryState getState(LifetimeEntry entry, double startTime, double endTime)
private LifetimeEntryState getState(T entry, double startTime, double endTime)
{
// Consider a static entry and a moving time range:
// [-----------Entry-----------]
Expand All @@ -367,7 +376,7 @@ private LifetimeEntryState getState(LifetimeEntry entry, double startTime, doubl
return LifetimeEntryState.Current;
}

private void enqueueEvents(LifetimeEntry entry, LifetimeEntryState oldState, LifetimeEntryState newState)
private void enqueueEvents(T entry, LifetimeEntryState oldState, LifetimeEntryState newState)
{
Debug.Assert(oldState != newState);

Expand All @@ -394,11 +403,11 @@ private void enqueueEvents(LifetimeEntry entry, LifetimeEntryState oldState, Lif
}

/// <summary>
/// Compares by <see cref="LifetimeEntry.LifetimeStart"/>.
/// Compares by <see cref="LifetimeEntry{T}.LifetimeStart"/>.
/// </summary>
private sealed class LifetimeStartComparator : IComparer<LifetimeEntry>
private sealed class LifetimeStartComparator : IComparer<T>
{
public int Compare(LifetimeEntry x, LifetimeEntry y)
public int Compare(T? x, T? y)
{
ArgumentNullException.ThrowIfNull(x);
ArgumentNullException.ThrowIfNull(y);
Expand All @@ -409,11 +418,11 @@ public int Compare(LifetimeEntry x, LifetimeEntry y)
}

/// <summary>
/// Compares by <see cref="LifetimeEntry.LifetimeEnd"/>.
/// Compares by <see cref="LifetimeEntry{T}.LifetimeEnd"/>.
/// </summary>
private sealed class LifetimeEndComparator : IComparer<LifetimeEntry>
private sealed class LifetimeEndComparator : IComparer<T>
{
public int Compare(LifetimeEntry x, LifetimeEntry y)
public int Compare(T? x, T? y)
{
ArgumentNullException.ThrowIfNull(x);
ArgumentNullException.ThrowIfNull(y);
Expand Down
10 changes: 5 additions & 5 deletions osu.Framework/Graphics/Performance/LifetimeEntryState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,27 @@
namespace osu.Framework.Graphics.Performance
{
/// <summary>
/// The state of a <see cref="LifetimeEntry"/>.
/// The state of a <see cref="LifetimeEntry{T}"/>.
/// </summary>
public enum LifetimeEntryState
{
/// <summary>
/// The <see cref="LifetimeEntry"/> hasn't been processed within the <see cref="LifetimeEntryManager"/> yet.
/// The <see cref="LifetimeEntry{T}"/> hasn't been processed within the <see cref="LifetimeEntryManager{T}"/> yet.
/// </summary>
New,

/// <summary>
/// The <see cref="LifetimeEntry"/> is currently dead and becomes alive when current time &gt;= <see cref="LifetimeEntry.LifetimeStart"/>.
/// The <see cref="LifetimeEntry{T}"/> is currently dead and becomes alive when current time &gt;= <see cref="LifetimeEntry{T}.LifetimeStart"/>.
/// </summary>
Future,

/// <summary>
/// The <see cref="LifetimeEntry"/> is currently alive.
/// The <see cref="LifetimeEntry{T}"/> is currently alive.
/// </summary>
Current,

/// <summary>
/// The <see cref="LifetimeEntry"/> is currently dead and becomes alive when current time &lt; <see cref="LifetimeEntry.LifetimeEnd"/>.
/// The <see cref="LifetimeEntry{T}"/> is currently dead and becomes alive when current time &lt; <see cref="LifetimeEntry{T}.LifetimeEnd"/>.
/// </summary>
Past,
}
Expand Down