Add lazy difficulty attribute calculation#36608
Add lazy difficulty attribute calculation#36608minisbett wants to merge 5 commits intoppy:masterfrom
Conversation
peppy
left a comment
There was a problem hiding this comment.
Looks quite sane to my eye now.
|
@storycraft does this work for you? |
Looks somewhat okay to me. |
| cancellationToken.ThrowIfCancellationRequested(); | ||
| preProcess(mods, cancellationToken); | ||
|
|
||
| if (!Beatmap.HitObjects.Any()) | ||
| return Task.FromResult(Enumerable.Empty<TimedDifficultyAttributes>()); | ||
|
|
||
| var skills = CreateSkills(Beatmap, playableMods, clockRate); | ||
| var progressiveBeatmap = new ProgressiveCalculationBeatmap(Beatmap); | ||
| var difficultyObjects = getDifficultyHitObjects().ToArray(); | ||
|
|
||
| return Task.FromResult(enumerate()); |
There was a problem hiding this comment.
Not sure how to feel about this being written this way. All work here is synchronous and blocks the caller, meaning this method isn't usable in async/await flows unless you know to wrap it with Task.Run() so that the sync work is marshaled off-thread to TPL. @minisbett did you intend this?
There was a problem hiding this comment.
This was sort-of intended. I was not sure you would want to see Task.Run here, and maybe rather want Task as some kind of result pattern.
What would be preferred? Both would work for me as I never intend to use asynchronous processing.
There was a problem hiding this comment.
This was sort-of intended. I was not sure you would want to see
Task.Runhere, and maybe rather wantTaskas some kind of result pattern.What would be preferred? Both would work for me as I never intend to use asynchronous processing.
There's a reason why I said it's half ok to me.
As bdach already mentioned, the user would expect a task which is already started in parallel using Task.Run or isn't started at all.
But it blocks, which seems to defeat the purpose to me.
There was a problem hiding this comment.
Probably wrap the entire thing in a Task.Run() so the caller can decide whether they want to async/await or they're fine with full blocking via .Result.
There was a problem hiding this comment.
Would again, be useful to know the usages of this.
Using task wrapping is going to make things slower if not already in async-await code. And the whole idea here is performance I guess. As previously stated, without a usage this is all a lot of faffery. Not even a benchmark to be seen.
There was a problem hiding this comment.
Would again, be useful to know the usages of this.
I have mentioned the usage of it further up. I hadn't shown a specific code-example, because it would not really be helpful. But here you go:
[OsuNativeFunction]
[OsuNativeEnumerator<NativeTimedOsuDifficultyAttributes>]
public static ErrorCode CalculateTimedLazy(OsuDifficultyCalculatorHandle calcHandle, ModsCollectionHandle modsHandle, NativeOsuTimedDifficultyAttributesEnumeratorHandle* timedAttributesEnumeratorHandle)
{
DifficultyCalculatorContext<OsuDifficultyCalculator> context = calcHandle.Resolve();
Mod[] mods = modsHandle.IsNull ? [] : [.. modsHandle.Resolve().Select(x => x.ToMod(context.Ruleset))];
IEnumerator<NativeTimedOsuDifficultyAttributes> enumerator = LazyDifficultyCalculationHelper.CalculateTimedLazy(context.Calculator, mods)
.Select(x => new NativeTimedOsuDifficultyAttributes(x))
.GetEnumerator();
enumerator.MoveNext();
*timedAttributesEnumeratorHandle = ManagedObjectStore.Store(enumerator);
return ErrorCode.Success;
}I'm essentially storing an enumerator of it, and a caller via a C FFI can enumerate through the timed difficulty attributes by calling a specific function.
This is far from any async context, and while both the current implementation and one using Task.Run would work, I'd personally favour the current one. But that is up to you. What could be considered is whether this method could become used in Lazers' Performance Counter skin component, as it suffers from the same performance issues on beatmaps with a lot of objects (the counter appears grayed out and stays at 0pp until the whole calculation finished).
There was a problem hiding this comment.
Either we go with 8c5cb08 or I dunno. I'm kinda done with this discussion again. The whole method is just kooky behaviour start to end.
There was a problem hiding this comment.
I would be fine with that, changing it to use Task.Run could be done if the mentioned impact slow difficulty calculation has on the performance counter skin component is ever being addressed and changed to use this newly introduced method.
## Lazy Timed Difficulty Attributes
Adds the ability to calculate timed difficulty attributes lazily,
calculating the next attribute when calling a `Next` function.
This is what the API looks like:
```c
const ErrorCode OsuDifficultyCalculator_CalculateTimedLazy(ManagedObjectHandle calcHandle, ManagedObjectHandle modsHandle, ManagedObjectHandle* timedAttributesEnumeratorHandle);
const ErrorCode OsuDifficultyCalculator_CalculateTimedLazy_Next(ManagedObjectHandle enumeratorHandle, NativeTimedOsuDifficultyAttributes* obj);
const ErrorCode OsuDifficultyCalculator_CalculateTimedLazy_Destroy(ManagedObjectHandle enumeratorHandle);
const ErrorCode TaikoDifficultyCalculator_CalculateTimedLazy(ManagedObjectHandle calcHandle, ManagedObjectHandle modsHandle, ManagedObjectHandle* timedAttributesEnumeratorHandle);
const ErrorCode TaikoDifficultyCalculator_CalculateTimedLazy_Next(ManagedObjectHandle enumeratorHandle, NativeTimedTaikoDifficultyAttributes* obj);
const ErrorCode TaikoDifficultyCalculator_CalculateTimedLazy_Destroy(ManagedObjectHandle enumeratorHandle);
const ErrorCode CatchDifficultyCalculator_CalculateTimedLazy(ManagedObjectHandle calcHandle, ManagedObjectHandle modsHandle, ManagedObjectHandle* timedAttributesEnumeratorHandle);
const ErrorCode CatchDifficultyCalculator_CalculateTimedLazy_Next(ManagedObjectHandle enumeratorHandle, NativeTimedCatchDifficultyAttributes* obj);
const ErrorCode CatchDifficultyCalculator_CalculateTimedLazy_Destroy(ManagedObjectHandle enumeratorHandle);
const ErrorCode ManiaDifficultyCalculator_CalculateTimedLazy(ManagedObjectHandle calcHandle, ManagedObjectHandle modsHandle, ManagedObjectHandle* timedAttributesEnumeratorHandle);
const ErrorCode ManiaDifficultyCalculator_CalculateTimedLazy_Next(ManagedObjectHandle enumeratorHandle, NativeTimedManiaDifficultyAttributes* obj);
const ErrorCode ManiaDifficultyCalculator_CalculateTimedLazy_Destroy(ManagedObjectHandle enumeratorHandle);
```
## Native Enumeration
A common API for enumerating objects over the FFI, along a
source-generator was introduced. Methods marked with
`[OsuNativeFunction]` can additionally be marked with
`[OsuNativeEnumerator<T>]`, and for a method `Foo` a native function
`Foo_Next(TEnumeratorHandle, T*)`, returning the next object, and
`Foo_Destroy(THandle)`, destroying the managed enumerator, is generated.
Here is an example for an enumerator method, and the generated native
functions `Next` and `Destroy`:
*source*
```cs
[OsuNativeFunction]
[OsuNativeEnumerator<NativeFoo>]
public static ErrorCode GetFoos(FooEnumeratorHandle* handle)
{
IEnumerator<NativeFoo> enumerator = ...;
enumerator.MoveNext();
*handle = ManagedObjectStore.Store(enumerator);
return ErrorCode.Success;
}
```
*generated (error-handling omitted for simplicity)*
```cs
public static ErrorCode FooObject_GetFoos_Next(FooEnumeratorHandle* handle, NativeFoo* foo)
{
IEnumerator<NativeFoo> enumerator = enumeratorHandle.Resolve();
*obj = enumerator.Current;
return enumerator.MoveNext() ? ErrorCode.Success : ErrorCode.EndOfEnumeration;
}
public static ErrorCode FooObject_GetFoos_Destroy(FooEnumeratorHandle* handle)
{
ManagedObjectStore<IEnumerator<NativeFoo}>>.Remove(enumeratorHandle);
return ErrorCode.Success;
}
```
Additionally, a new special error code `EndOfEnumeration = -2` has been
introduced, and is returned when the call to `Next` just returned the
last object in the enumeration.
## Implementation of lazy calculation
Unfortunately, the osu!(lazer) codebase does not provide a way to
calculate timed difficulty attributes lazily,
`osu.Game.Rulesets.Difficulty.DifficultyCalculator.CalculateTimed`
calculates all timed difficulty attributes and then returns.
In order to work around that, a temporary class
`LazyDifficultyCalculationHelper` has been introduced, which,
unfortunately **using reflection**, copies the behavior of
`CalculateTimed`, but serving timed difficulty attributes via an
enumerable.
There is an open pull request, ppy/osu#36608,
aiming to implement lazy timed difficulty calculation the osu!(lazer)
codebase.
|
I would like to suggest not merging this (for now?), because there is something only now deemed necessary for this to work for us: being able to skip attributes, preventing the call of I'm not sure this can find it's way in here nicely? My current approach in my consumer library is heavily using reflection on the difficulty calculator, and therefore I'm curious if exposing required methods/properties would be okay? (instead of what this PR does, not additionally) That would probably save me from doing another PR in the future on something slightly related, and also make this thing in osu-tools less cheesy. The following things would be required to be exposed:
I do have doubts this is going to be acceptable, but wanted to ask since bdach had mentioned the possibility of it before. I personally feel like it should be okay to expose these, as that part of the osu!(lazer) code-base is pretty much the most likely to be a point of interest for third-parties to consume. If this is acceptable, I would create a new PR for that and close this one. Update: Another oversight is that unfortunately |
Second try at #36570 (continuing with their approval)
I've dissected the convoluted discussion that happened, and believe to have got the important things right:
CalculateTimedLazyCalculateTimedcallsCalculateTimedLazy, but continues to hold the 10 second timeout cancellation token, which is simply passed overCalculateTimedshould be identicalI have also added a remark to the two
CalculateTimedoverloads, in order to highlight the timeout behavior thatCalculateTimedLazydoes not have. Whether the lazy method should actually hold the timeout logic can be discussed, but I personally don't see a need for it.