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

Cancel difficulty calculation after 10 seconds by default #32208

Merged
merged 1 commit into from
Mar 4, 2025

Conversation

smoogipoo
Copy link
Contributor

@smoogipoo smoogipoo commented Mar 4, 2025

The following (when run in osu-tools) will die in the new taiko code during diffcalc:

dotnet run -- difficulty 4328583 -r:1

This adds the same 10s timeout as WorkingBeatmap has for loading beatmaps, to now additionally govern the diffcalc process itself. It will apply to both osu-tools and osu-difficulty-calculator.

@@ -62,6 +62,11 @@ public DifficultyAttributes Calculate(CancellationToken cancellationToken = defa
/// <returns>A structure describing the difficulty of the beatmap.</returns>
public DifficultyAttributes Calculate([NotNull] IEnumerable<Mod> mods, CancellationToken cancellationToken = default)
{
using var timedCancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(10));
Copy link
Member

Choose a reason for hiding this comment

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

I get this is 100% a nitpick but we might want to avoid creating this when it's not being used. Just to set a good example for the rest of diffcalc (which is definitely where any allocation issue will lie 🤷).

diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
index add24f7866..7d01e678b1 100644
--- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
+++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
@@ -62,13 +62,18 @@ public DifficultyAttributes Calculate(CancellationToken cancellationToken = defa
         /// <returns>A structure describing the difficulty of the beatmap.</returns>
         public DifficultyAttributes Calculate([NotNull] IEnumerable<Mod> mods, CancellationToken cancellationToken = default)
         {
-            using var timedCancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(10));
-
-            if (!cancellationToken.CanBeCanceled)
-                cancellationToken = timedCancellationSource.Token;
-
-            cancellationToken.ThrowIfCancellationRequested();
-            preProcess(mods, cancellationToken);
+            if (cancellationToken.CanBeCanceled)
+            {
+                cancellationToken.ThrowIfCancellationRequested();
+                preProcess(mods, cancellationToken);
+            }
+            else
+            {
+                // Difficulty cancellation can potentially get stuck in weird beatmaps, so force a timeout if no cancellation
+                // mechanism was provided.
+                using (var timedCancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
+                    preProcess(mods, timedCancellationSource.Token);
+            }
 
             var skills = CreateSkills(Beatmap, playableMods, clockRate);
 
@@ -103,19 +108,24 @@ public List<TimedDifficultyAttributes> CalculateTimed(CancellationToken cancella
         /// <returns>The set of <see cref="TimedDifficultyAttributes"/>.</returns>
         public List<TimedDifficultyAttributes> CalculateTimed([NotNull] IEnumerable<Mod> mods, CancellationToken cancellationToken = default)
         {
-            using var timedCancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(10));
-
-            if (!cancellationToken.CanBeCanceled)
-                cancellationToken = timedCancellationSource.Token;
-
-            cancellationToken.ThrowIfCancellationRequested();
-            preProcess(mods, cancellationToken);
-
             var attribs = new List<TimedDifficultyAttributes>();
 
             if (!Beatmap.HitObjects.Any())
                 return attribs;
 
+            if (cancellationToken.CanBeCanceled)
+            {
+                cancellationToken.ThrowIfCancellationRequested();
+                preProcess(mods, cancellationToken);
+            }
+            else
+            {
+                // Difficulty cancellation can potentially get stuck in weird beatmaps, so force a timeout if no cancellation
+                // mechanism was provided.
+                using (var timedCancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
+                    preProcess(mods, timedCancellationSource.Token);
+            }
+
             var skills = CreateSkills(Beatmap, playableMods, clockRate);
             var progressiveBeatmap = new ProgressiveCalculationBeatmap(Beatmap);
             var difficultyObjects = getDifficultyHitObjects().ToArray();

Copy link
Contributor Author

@smoogipoo smoogipoo Mar 4, 2025

Choose a reason for hiding this comment

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

The important part isn't the preProcess(), it's the usage in the loop below. In other words, the lifetime of the CTS has to be for the entire method.

One way of structuring it would be a nested method, like so:

diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
index add24f7866..624a8d72bf 100644
--- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
+++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs
@@ -62,29 +62,33 @@ public DifficultyAttributes Calculate(CancellationToken cancellationToken = defa
         /// <returns>A structure describing the difficulty of the beatmap.</returns>
         public DifficultyAttributes Calculate([NotNull] IEnumerable<Mod> mods, CancellationToken cancellationToken = default)
         {
-            using var timedCancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(10));
+            if (cancellationToken.CanBeCanceled)
+                return doTheThing(cancellationToken);
 
-            if (!cancellationToken.CanBeCanceled)
-                cancellationToken = timedCancellationSource.Token;
+            using (var timedCancellationSource = new CancellationTokenSource(TimeSpan.FromSeconds(10)))
+                return doTheThing(timedCancellationSource.Token);
 
-            cancellationToken.ThrowIfCancellationRequested();
-            preProcess(mods, cancellationToken);
+            DifficultyAttributes doTheThing(CancellationToken token)
+            {
+                token.ThrowIfCancellationRequested();
+                preProcess(mods, token);
 
-            var skills = CreateSkills(Beatmap, playableMods, clockRate);
+                var skills = CreateSkills(Beatmap, playableMods, clockRate);
 
-            if (!Beatmap.HitObjects.Any())
-                return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate);
+                if (!Beatmap.HitObjects.Any())
+                    return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate);
 
-            foreach (var hitObject in getDifficultyHitObjects())
-            {
-                foreach (var skill in skills)
+                foreach (var hitObject in getDifficultyHitObjects())
                 {
-                    cancellationToken.ThrowIfCancellationRequested();
-                    skill.Process(hitObject);
+                    foreach (var skill in skills)
+                    {
+                        token.ThrowIfCancellationRequested();
+                        skill.Process(hitObject);
+                    }
                 }
-            }
 
-            return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate);
+                return CreateDifficultyAttributes(Beatmap, playableMods, skills, clockRate);
+            }
         }
 
         /// <summary>

But imo this isn't really worth it? If you disagree and feel that it is then I can push it.

Copy link
Member

Choose a reason for hiding this comment

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

ah right, i missed that.

let's just leave it

@peppy peppy merged commit 63cd93a into ppy:master Mar 4, 2025
8 of 10 checks passed
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.

None yet

2 participants