Skip to content

Commit 00cb8df

Browse files
⚡ Optimize N+1 queries in BackgroundDataStoreProcessor
Optimized `processScoresWithMissingStatistics` and `upgradeScoreRanks` by implementing chunking and batching write transactions. This significantly reduces database round-trips and transaction overhead when processing large numbers of scores at startup.
1 parent 5909cf1 commit 00cb8df

1 file changed

Lines changed: 110 additions & 35 deletions

File tree

osu.Game/Database/BackgroundDataStoreProcessor.cs

Lines changed: 110 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -418,7 +418,7 @@ private void processScoresWithMissingStatistics()
418418
int processedCount = 0;
419419
int failedCount = 0;
420420

421-
foreach (var id in scoreIds)
421+
foreach (var chunk in scoreIds.Chunk(100))
422422
{
423423
if (notification?.State == ProgressNotificationState.Cancelled)
424424
break;
@@ -427,33 +427,62 @@ private void processScoresWithMissingStatistics()
427427

428428
sleepIfRequired();
429429

430-
try
430+
var updates = new List<(Guid id, string json)>();
431+
var failedIds = new List<Guid>();
432+
433+
foreach (var id in chunk)
431434
{
432-
var score = scoreManager.Query(s => s.ID == id);
435+
try
436+
{
437+
var score = scoreManager.Query(s => s.ID == id);
433438

434-
if (score != null)
439+
if (score != null)
440+
{
441+
scoreManager.PopulateMaximumStatistics(score);
442+
updates.Add((id, JsonConvert.SerializeObject(score.MaximumStatistics)));
443+
}
444+
}
445+
catch (Exception e)
435446
{
436-
scoreManager.PopulateMaximumStatistics(score);
447+
Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}");
448+
failedIds.Add(id);
449+
}
450+
}
437451

452+
if (updates.Count > 0 || failedIds.Count > 0)
453+
{
454+
try
455+
{
438456
// Can't use async overload because we're not on the update thread.
439457
// ReSharper disable once MethodHasAsyncOverload
440458
realmAccess.Write(r =>
441459
{
442-
r.Find<ScoreInfo>(id)!.MaximumStatisticsJson = JsonConvert.SerializeObject(score.MaximumStatistics);
460+
foreach (var update in updates)
461+
{
462+
var s = r.Find<ScoreInfo>(update.id);
463+
if (s != null)
464+
s.MaximumStatisticsJson = update.json;
465+
}
466+
467+
foreach (var id in failedIds)
468+
{
469+
var s = r.Find<ScoreInfo>(id);
470+
if (s != null)
471+
s.BackgroundReprocessingFailed = true;
472+
}
443473
});
444-
}
445474

446-
++processedCount;
447-
}
448-
catch (ObjectDisposedException)
449-
{
450-
throw;
451-
}
452-
catch (Exception e)
453-
{
454-
Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}");
455-
realmAccess.Write(r => r.Find<ScoreInfo>(id)!.BackgroundReprocessingFailed = true);
456-
++failedCount;
475+
processedCount += updates.Count;
476+
failedCount += failedIds.Count;
477+
}
478+
catch (ObjectDisposedException)
479+
{
480+
throw;
481+
}
482+
catch (Exception e)
483+
{
484+
Logger.Log($"Fatal error writing batch in score statistics population: {e}");
485+
}
457486
}
458487
}
459488

@@ -592,7 +621,7 @@ private void upgradeScoreRanks()
592621
int processedCount = 0;
593622
int failedCount = 0;
594623

595-
foreach (var id in scoreIds)
624+
foreach (var chunk in scoreIds.Chunk(100))
596625
{
597626
if (notification?.State == ProgressNotificationState.Cancelled)
598627
break;
@@ -601,28 +630,74 @@ private void upgradeScoreRanks()
601630

602631
sleepIfRequired();
603632

604-
try
633+
var updates = new List<(Guid id, ScoreRank rank)>();
634+
var failedIds = new List<Guid>();
635+
636+
var detachedScores = realmAccess.Run(r =>
605637
{
606-
// Can't use async overload because we're not on the update thread.
607-
// ReSharper disable once MethodHasAsyncOverload
608-
realmAccess.Write(r =>
638+
var scores = new List<ScoreInfo>();
639+
640+
foreach (var id in chunk)
609641
{
610-
ScoreInfo s = r.Find<ScoreInfo>(id)!;
611-
s.Rank = StandardisedScoreMigrationTools.ComputeRank(s);
612-
s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION;
613-
});
642+
var s = r.Find<ScoreInfo>(id);
614643

615-
++processedCount;
616-
}
617-
catch (ObjectDisposedException)
644+
if (s != null)
645+
scores.Add(s.Detach());
646+
}
647+
648+
return scores;
649+
});
650+
651+
foreach (var detachedScore in detachedScores)
618652
{
619-
throw;
653+
try
654+
{
655+
updates.Add((detachedScore.ID, StandardisedScoreMigrationTools.ComputeRank(detachedScore)));
656+
}
657+
catch (Exception e)
658+
{
659+
Logger.Log($"Failed to update rank score {detachedScore.ID}: {e}");
660+
failedIds.Add(detachedScore.ID);
661+
}
620662
}
621-
catch (Exception e)
663+
664+
if (updates.Count > 0 || failedIds.Count > 0)
622665
{
623-
Logger.Log($"Failed to update rank score {id}: {e}");
624-
realmAccess.Write(r => r.Find<ScoreInfo>(id)!.BackgroundReprocessingFailed = true);
625-
++failedCount;
666+
try
667+
{
668+
// Can't use async overload because we're not on the update thread.
669+
// ReSharper disable once MethodHasAsyncOverload
670+
realmAccess.Write(r =>
671+
{
672+
foreach (var update in updates)
673+
{
674+
var s = r.Find<ScoreInfo>(update.id);
675+
if (s != null)
676+
{
677+
s.Rank = update.rank;
678+
s.TotalScoreVersion = LegacyScoreEncoder.LATEST_VERSION;
679+
}
680+
}
681+
682+
foreach (var id in failedIds)
683+
{
684+
var s = r.Find<ScoreInfo>(id);
685+
if (s != null)
686+
s.BackgroundReprocessingFailed = true;
687+
}
688+
});
689+
690+
processedCount += updates.Count;
691+
failedCount += failedIds.Count;
692+
}
693+
catch (ObjectDisposedException)
694+
{
695+
throw;
696+
}
697+
catch (Exception e)
698+
{
699+
Logger.Log($"Fatal error writing batch in score rank upgrade: {e}");
700+
}
626701
}
627702
}
628703

0 commit comments

Comments
 (0)