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

Turn difficulty & performance attributes into structs #30727

Open
wants to merge 17 commits into
base: master
Choose a base branch
from

Conversation

minisbett
Copy link
Contributor

@minisbett minisbett commented Nov 17, 2024

This turns all difficulty and performance attributes into structs, allowing them to be used via osu-native for marshalling.
The attributes should never contain anything that is not some basic numbers, but using them in osu-native highlights the necessity to keep an eye on that they always will.

Additionally, the mods were removed from the structs as they are managed objects. Because of that, the ShouldSerialize check for flashlight difficulty is handled via nullability instead.

@peppy
Copy link
Member

peppy commented Nov 18, 2024

@minisbett can you please do some performance tests (either via existing tests or adding new ones to cover diffcalc flow if it doesn't exist) so the core team doesn't get distracted by this?

We'd want to ensure this isn't losing any processing efficiency.

@minisbett
Copy link
Contributor Author

@minisbett can you please do some performance tests (either via existing tests or adding new ones to cover diffcalc flow if it doesn't exist) so the core team doesn't get distracted by this?

We'd want to ensure this isn't losing any processing efficiency.

Structs

| Method           | Mean           | Error        | StdDev       | Gen0     | Gen1     | Allocated  |
|----------------- |---------------:|-------------:|-------------:|---------:|---------:|-----------:|
| DifficultyOsu    | 5,032,112.9 ns | 45,103.61 ns | 42,189.94 ns | 328.1250 | 265.6250 | 5568.89 KB |
| DifficultyTaiko  | 2,166,386.8 ns | 43,218.13 ns | 44,381.84 ns | 250.0000 | 171.8750 | 4282.61 KB |
| DifficultyCatch  | 3,367,778.6 ns | 61,871.00 ns | 57,874.17 ns | 343.7500 | 265.6250 | 5634.84 KB |
| DifficultyMania  | 2,025,554.9 ns | 35,206.24 ns | 32,931.94 ns | 289.0625 | 160.1563 | 4744.16 KB |
| PerformanceOsu   |     1,615.5 ns |     28.06 ns |     31.18 ns |   0.2174 |        - |    3.57 KB |
| PerformanceTaiko |       922.9 ns |      7.55 ns |      5.89 ns |   0.1812 |        - |    2.96 KB |
| PerformanceCatch |       866.1 ns |     17.04 ns |     28.00 ns |   0.1659 |        - |    2.72 KB |
| PerformanceMania |       725.0 ns |     11.26 ns |     10.53 ns |   0.1631 |        - |    2.67 KB |

Classes

| Method           | Mean           | Error        | StdDev       | Gen0     | Gen1     | Allocated  |
|----------------- |---------------:|-------------:|-------------:|---------:|---------:|-----------:|
| DifficultyOsu    | 5,132,323.3 ns | 99,507.33 ns | 77,688.78 ns | 328.1250 | 265.6250 | 5568.86 KB |
| DifficultyTaiko  | 2,224,711.6 ns | 18,712.45 ns | 14,609.45 ns | 261.7188 | 183.5938 | 4282.55 KB |
| DifficultyCatch  | 3,385,101.3 ns | 62,399.92 ns | 55,315.91 ns | 343.7500 | 265.6250 | 5634.85 KB |
| DifficultyMania  | 2,000,673.8 ns | 25,826.86 ns | 24,158.46 ns | 289.0625 | 160.1563 | 4744.17 KB |
| PerformanceOsu   |     1,660.8 ns |     14.81 ns |     13.86 ns |   0.2174 |        - |    3.57 KB |
| PerformanceTaiko |       937.4 ns |     13.95 ns |     16.60 ns |   0.1812 |        - |    2.96 KB |
| PerformanceCatch |       855.7 ns |     13.41 ns |     11.88 ns |   0.1659 |        - |    2.72 KB |
| PerformanceMania |       757.7 ns |      7.36 ns |      6.89 ns |   0.1631 |        - |    2.67 KB |

Would've been surprised if structs are slower by any relevant amount. I've committed the tests.

@smoogipoo
Copy link
Contributor

Imo all the performance discussions above are unnecessary. Attributes aren't what diffcalc is allocating 2+GB/sec of. If me caching hitobjects barely improves performance then this change is not going to be felt.
Even if we generate 128B of attributes over 4000 hitobjects, that's still tiny and very acceptable whether as a class or a struct.

It's going to be boxed and effectively the same as a class at the end of the day. What's more important is making sure this is sanely usable in those other projects.

Finally, I had similar concerns with my diffcalc refactors - do I use structs + interfaces or figure out some jank format where everything is a struct and you can somehow provide arbitrary data. I came to the same conclusion there - allocations are not worth caring about here.

@peppy
Copy link
Member

peppy commented Nov 19, 2024

Imo all the performance discussions above are unnecessary.

It's a peace-of-mind thing. Maybe unnecessary for you, but necessary for me to move forward quickly. It just makes sense to place the onus of this on the PR author rather than us.

@minisbett
Copy link
Contributor Author

minisbett commented Nov 19, 2024

All performance concerns aside, there's some general consensus required that would come with implementing this PR with osu-native in mind. The discussion for that can be followed in #difficulty-osu on the devcord.

tl;dr: If we would want to care about native usage of the attributes in their implementation in the Lazer code, it'd mean that any managed fields are to be avoided, as they cause increased maintaining efforts on osu-native. Which is arguably a weird constraint put on Lazer development for a rather independent project.

Specifically, a nullable field in the taiko performance attributes is an issue. Removing it is not an option becaue despite having not really a use right now, with an upcoming PP rework it will turn into a difficulty attribute. So it either requires special handling for nullable values in osu-native, or there's a replacement for it's nullability (eg. double.PositiveInfinity), which is a whacky solution.

I might have found a solution for nullable value types that I still need to test properly, but even if that can be handled there still needs to be a consensus that the attributes should not contain non-(nullable-) value types.

The whole purpose of this PR was not having to 1:1 copy the attributes into osu-native, just as structs. But the extra handling mentioned above would likely mean that any attributes with managed fields require some kind of wrapping.

@minisbett
Copy link
Contributor Author

minisbett commented Nov 19, 2024

Update: I managed to easily handle nullables.

Therefore, the only concern there is is do we want to constrain attributes to not contain managed objects? That includes any C# classes, any arrays and strings.

I believe this contraint is sort-of given by nature, at least for difficulty attributes, as they also need to be serializable for the database. As for performance attributes, there's no contraints but on the other hand performance attributes are useless besides the total PP & the PP per skill for displaying in the result screen.

But to be able to confidentally progress on osu-native, I'd like to have this security and as a part of it get this PR merged.

@smoogipoo
Copy link
Contributor

smoogipoo commented Nov 20, 2024

Update: I managed to easily handle nullables.

Will osu-native handle it fine as-is or it will it need additional changes?

Also for other reviewers, keep in mind that o-n is not an official project (yet) and highly experimental. I suggest treating this as a code quality change first.

@smoogipoo
Copy link
Contributor

!diffcalc

RULESET=osu
DIFFICULTY_CALCULATOR_B=ppy/osu-difficulty-calculator#235
SCORE_PROCESSOR_B=ppy/osu-queue-score-statistics#301

@minisbett
Copy link
Contributor Author

minisbett commented Nov 20, 2024

Update: I managed to easily handle nullables.

Will osu-native handle it fine as-is or it will it need additional changes?

Also for other reviewers, keep in mind that o-n is not an official project (yet) and highly experimental. I suggest treating this as a code quality change first.

Yes, osu-native will be able to handle nullables.

The reason behind my trouble was that I've been creating a new project for C# bindings based on .NET 9, and therefore VS suggested to me to use the LibraryImport attribute instead of DllImort. Essentially, instead of runtime marshalling it uses a source generator for it, which can improve performance and allows more flexibility. This method of doing it is the issue, and I only later realized that it is a .NET 7+ feature and since the C# bindings should be netstandard2.0 compatible not an option.

Specifically, the issue was the usage of the bool type in Nullable<T>. C#'s nullables look like this:

public struct Nullable<T>
{
    public bool HasValue;
    public T Value;
}

With the new-ish source generated marshalling, the bool type is not implicitly considered blittable (and apparently it would work with a MashalAs attribute which I obviously can't add to a built-in type, so there would need to be a custom nullable implementation). I haven't fully understood the whole situation, but since I know I won't use this I decided to not waste time digging deeper.

If interested, more can be found here:
https://github.com/dotnet/runtime/blob/main/docs/design/libraries/LibraryImportGenerator/Compatibility.md#bool-marshalling
https://learn.microsoft.com/en-us/dotnet/standard/native-interop/pinvoke-source-generation

Copy link

@smoogipoo
Copy link
Contributor

!diffcalc

RULESET=taiko
DIFFICULTY_CALCULATOR_B=ppy/osu-difficulty-calculator#235
SCORE_PROCESSOR_B=ppy/osu-queue-score-statistics#301

@smoogipoo
Copy link
Contributor

!diffcalc

RULESET=catch
DIFFICULTY_CALCULATOR_B=ppy/osu-difficulty-calculator#235
SCORE_PROCESSOR_B=ppy/osu-queue-score-statistics#301

@smoogipoo
Copy link
Contributor

!diffcalc

RULESET=mania
DIFFICULTY_CALCULATOR_B=ppy/osu-difficulty-calculator#235
SCORE_PROCESSOR_B=ppy/osu-queue-score-statistics#301

Copy link

Copy link

Copy link

Difficulty calculation failed: https://github.com/ppy/osu/actions/runs/11948957279

@@ -8,6 +8,7 @@

namespace osu.Game.Rulesets.Catch.Difficulty
{
[JsonObject(MemberSerialization.OptIn)]
Copy link
Collaborator

Choose a reason for hiding this comment

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

what's that? why is it here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This attribute was previously defined on the class all attributes inherit from. Even though technically only the osu difficulty attributes need it right now, I decided to stick to how it was before.

Copy link

@bdach
Copy link
Collaborator

bdach commented Nov 25, 2024

I have a problem with this diff in that I don't... find it particularly compelling?

Like, if I'm to take @smoogipoo's proposed premise:

Also for other reviewers, keep in mind that o-n is not an official project (yet) and highly experimental. I suggest treating this as a code quality change first.

this arguably does not a code quality change make. I have a distinct feeling that outside of catch's aim-attribute-is-star-rating weirdness, inheritance made sense, and there need to be concessions made in removing it (for instance, the opt-in serialisation thing which is now plastered on 4 structs, although you could say you could just remove it from places where it's irrelevant). Like at best it's a sidegrade quality-wise, and at worst it's slightly worse than master. And as stated before, there are no relevant performance implications here either.

The only compelling reason I'd see to merge this is whatever the osu-native thing is trying to do, so I'm not sure how to proceed.

@minisbett
Copy link
Contributor Author

minisbett commented Nov 26, 2024

I have a problem with this diff in that I don't... find it particularly compelling?

Like, if I'm to take @smoogipoo's proposed premise:

Also for other reviewers, keep in mind that o-n is not an official project (yet) and highly experimental. I suggest treating this as a code quality change first.

this arguably does not a code quality change make. I have a distinct feeling that outside of catch's aim-attribute-is-star-rating weirdness, inheritance made sense, and there need to be concessions made in removing it (for instance, the opt-in serialisation thing which is now plastered on 4 structs, although you could say you could just remove it from places where it's irrelevant). Like at best it's a sidegrade quality-wise, and at worst it's slightly worse than master. And as stated before, there are no relevant performance implications here either.

The only compelling reason I'd see to merge this is whatever the osu-native thing is trying to do, so I'm not sure how to proceed.

Coming back to this; I've been a bit torn apart on whether this change makes sense on Lazers' end.

It might make more sense to have the attributes as POCOs. The whole reason for this change was to not have to maintain a 1:1 copy of the attributes as structs inside osu-native. The first idea was introducing interfaces in Lazer so that osu-native can contract them, but that comes with added OO complexity. But in the end, the consumer-side of osu-native still needs to implement them as a struct, so those structs could might as well be in some shared file between the two projects, making the trouble of this PR redundant.

@smoogipoo what do you think on how to handle this? Maybe it's best to return to my initial approach of having structs in osu-native along with helper methods going from the managed attributes <-> structs.

@smoogipoo
Copy link
Contributor

Maybe it's best to return to my initial approach of having structs in osu-native along with helper methods going from the managed attributes <-> structs

This is a valid path, and I suggest going with it for now.


While I'm going to deprioritise this PR, I'd still like some form of it to arrive at some point. I think there is refactoring validity in attributes not storing Mods which is being worked around in at least one other place, as well as having an empty attribute class.

@bdach
Copy link
Collaborator

bdach commented Dec 26, 2024

I think there is refactoring validity in attributes not storing Mods which is being worked around in at least one other place, as well as having an empty attribute class.

I can agree with these two elements being a net positive, but not with this PR as a whole. If those parts can be split out of this I think they'd be merged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants