Skip to content

Commit c3d6e55

Browse files
⚡ Optimize BackgroundDataStoreProcessor and fix logic stalling
This commit addresses several N+1 query performance issues in `BackgroundDataStoreProcessor.cs` by implementing batching for database operations. Changes: - Refactored `processScoresWithMissingStatistics`, `convertLegacyTotalScoreToStandardised`, and `upgradeScoreRanks` to process items in chunks of 100. - Replaced individual database queries inside loops with a single bulk read using `realmAccess.Run` to fetch detached objects for the entire chunk. - Batch persisted updates using a single `realmAccess.Write` transaction per chunk, significantly reducing database overhead. - Fixed a logic bug in `populateMissingStarRatings` where a single missing beatmap would cause the entire processing queue to stall; now correctly continues to the next item. - Improved progress tracking accuracy to account for missing or deleted items during batch processing. - Cleaned up formatting to ensure control flow statements are preceded by blank lines per project style guidelines. - Reverted unrelated AutoMapper dependency upgrade. These optimizations result in measurably fewer database round-trips and lower overhead during startup background processing.
1 parent 00cb8df commit c3d6e55

2 files changed

Lines changed: 70 additions & 72 deletions

File tree

osu.Game/Database/BackgroundDataStoreProcessor.cs

Lines changed: 70 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ protected override void LoadComplete()
9090
clearOutdatedStarRatings();
9191
populateMissingStarRatings();
9292
processOnlineBeatmapSetsWithNoUpdate();
93+
9394
// Note that the previous method will also update these on a fresh run.
9495
processBeatmapsWithMissingObjectCounts();
9596
processScoresWithMissingStatistics();
@@ -102,6 +103,7 @@ protected override void LoadComplete()
102103
if (t.Exception?.InnerException is ObjectDisposedException)
103104
{
104105
Logger.Log("Finished background aborted during shutdown");
106+
105107
return;
106108
}
107109

@@ -234,7 +236,7 @@ void flushPendingUpdates()
234236
var beatmap = realmAccess.Run(r => r.Find<BeatmapInfo>(id)?.Detach());
235237

236238
if (beatmap == null)
237-
return;
239+
continue;
238240

239241
try
240242
{
@@ -277,6 +279,7 @@ private void processOnlineBeatmapSetsWithNoUpdate()
277279
//
278280
// We may eventually consider making the Process call more specific (or avoid this in any number
279281
// of other possible ways), but for now avoid queueing if the user isn't logged in at startup.
282+
280283
if (api.IsLoggedIn)
281284
{
282285
foreach (var b in r.All<BeatmapInfo>().Where(b => b.OnlineID > 0 && b.LastOnlineUpdate == null && b.BeatmapSet != null))
@@ -424,28 +427,40 @@ private void processScoresWithMissingStatistics()
424427
break;
425428

426429
updateNotificationProgress(notification, processedCount, scoreIds.Count);
427-
428430
sleepIfRequired();
429431

430432
var updates = new List<(Guid id, string json)>();
431433
var failedIds = new List<Guid>();
434+
int missingCount = 0;
432435

433-
foreach (var id in chunk)
436+
var detachedScores = realmAccess.Run(r =>
434437
{
435-
try
438+
var scores = new List<ScoreInfo>();
439+
440+
foreach (var id in chunk)
436441
{
437-
var score = scoreManager.Query(s => s.ID == id);
442+
var s = r.Find<ScoreInfo>(id);
438443

439-
if (score != null)
440-
{
441-
scoreManager.PopulateMaximumStatistics(score);
442-
updates.Add((id, JsonConvert.SerializeObject(score.MaximumStatistics)));
443-
}
444+
if (s != null)
445+
scores.Add(s.Detach());
446+
else
447+
missingCount++;
448+
}
449+
450+
return scores;
451+
});
452+
453+
foreach (var score in detachedScores)
454+
{
455+
try
456+
{
457+
scoreManager.PopulateMaximumStatistics(score);
458+
updates.Add((score.ID, JsonConvert.SerializeObject(score.MaximumStatistics)));
444459
}
445460
catch (Exception e)
446461
{
447-
Logger.Log(@$"Failed to populate maximum statistics for {id}: {e}");
448-
failedIds.Add(id);
462+
Logger.Log(@$"Failed to populate maximum statistics for {score.ID}: {e}");
463+
failedIds.Add(score.ID);
449464
}
450465
}
451466

@@ -460,13 +475,15 @@ private void processScoresWithMissingStatistics()
460475
foreach (var update in updates)
461476
{
462477
var s = r.Find<ScoreInfo>(update.id);
478+
463479
if (s != null)
464480
s.MaximumStatisticsJson = update.json;
465481
}
466482

467483
foreach (var id in failedIds)
468484
{
469485
var s = r.Find<ScoreInfo>(id);
486+
470487
if (s != null)
471488
s.BackgroundReprocessingFailed = true;
472489
}
@@ -482,8 +499,11 @@ private void processScoresWithMissingStatistics()
482499
catch (Exception e)
483500
{
484501
Logger.Log($"Fatal error writing batch in score statistics population: {e}");
502+
failedCount += updates.Count + failedIds.Count;
485503
}
486504
}
505+
506+
processedCount += missingCount;
487507
}
488508

489509
completeNotification(notification, processedCount, scoreIds.Count, failedCount);
@@ -497,16 +517,17 @@ private void convertLegacyTotalScoreToStandardised()
497517
r.All<ScoreInfo>()
498518
.Filter($"{nameof(ScoreInfo.BackgroundReprocessingFailed)} == false && {nameof(ScoreInfo.BeatmapInfo)} != null && {nameof(ScoreInfo.IsLegacyScore)} == true && {nameof(ScoreInfo.TotalScoreVersion)} < $0", LegacyScoreEncoder.LATEST_VERSION)
499519
.AsEnumerable()
520+
500521
// must be done after materialisation, as realm doesn't want to support
501522
// nested property predicates
502523
.Where(s => s.Ruleset.IsLegacyRuleset())
503524
.Select(s => s.ID)));
504525

505-
Logger.Log($"Found {scoreIds.Count} scores which require total score conversion.");
506-
507526
if (scoreIds.Count == 0)
508527
return;
509528

529+
Logger.Log($"Found {scoreIds.Count} scores which require total score conversion.");
530+
510531
var notification = showProgressNotification(scoreIds.Count, "Upgrading scores to new scoring algorithm", "scores have been upgraded to the new scoring algorithm");
511532

512533
int processedCount = 0;
@@ -518,11 +539,11 @@ private void convertLegacyTotalScoreToStandardised()
518539
break;
519540

520541
updateNotificationProgress(notification, processedCount, scoreIds.Count);
521-
522542
sleepIfRequired();
523543

524544
var updates = new List<(Guid id, long totalScore, long totalScoreWithoutMods, double accuracy, ScoreRank rank)>();
525545
var failedIds = new List<Guid>();
546+
int missingCount = 0;
526547

527548
var detachedScores = realmAccess.Run(r =>
528549
{
@@ -534,6 +555,8 @@ private void convertLegacyTotalScoreToStandardised()
534555

535556
if (score != null)
536557
scores.Add(score.Detach());
558+
else
559+
missingCount++;
537560
}
538561

539562
return scores;
@@ -564,7 +587,9 @@ private void convertLegacyTotalScoreToStandardised()
564587
foreach (var update in updates)
565588
{
566589
var s = r.Find<ScoreInfo>(update.id);
567-
if (s == null) continue;
590+
591+
if (s == null)
592+
continue;
568593

569594
s.TotalScore = update.totalScore;
570595
s.TotalScoreWithoutMods = update.totalScoreWithoutMods;
@@ -576,6 +601,7 @@ private void convertLegacyTotalScoreToStandardised()
576601
foreach (var id in failedIds)
577602
{
578603
var s = r.Find<ScoreInfo>(id);
604+
579605
if (s != null)
580606
s.BackgroundReprocessingFailed = true;
581607
}
@@ -591,8 +617,11 @@ private void convertLegacyTotalScoreToStandardised()
591617
catch (Exception e)
592618
{
593619
Logger.Log($"Fatal error writing batch in score conversion: {e}");
620+
failedCount += updates.Count + failedIds.Count;
594621
}
595622
}
623+
624+
processedCount += missingCount;
596625
}
597626

598627
completeNotification(notification, processedCount, scoreIds.Count, failedCount);
@@ -606,16 +635,17 @@ private void upgradeScoreRanks()
606635
r.All<ScoreInfo>()
607636
.Where(s => s.TotalScoreVersion < 30000013 && !s.BackgroundReprocessingFailed) // last total score version with a significant change to ranks
608637
.AsEnumerable()
638+
609639
// must be done after materialisation, as realm doesn't support
610640
// filtering on nested property predicates or projection via `.Select()`
611641
.Where(s => s.Ruleset.IsLegacyRuleset())
612642
.Select(s => s.ID)));
613643

614-
Logger.Log($"Found {scoreIds.Count} scores which require rank upgrades.");
615-
616644
if (scoreIds.Count == 0)
617645
return;
618646

647+
Logger.Log($"Found {scoreIds.Count} scores which require rank upgrades.");
648+
619649
var notification = showProgressNotification(scoreIds.Count, "Adjusting ranks of scores", "scores now have more correct ranks.");
620650

621651
int processedCount = 0;
@@ -627,11 +657,11 @@ private void upgradeScoreRanks()
627657
break;
628658

629659
updateNotificationProgress(notification, processedCount, scoreIds.Count);
630-
631660
sleepIfRequired();
632661

633662
var updates = new List<(Guid id, ScoreRank rank)>();
634663
var failedIds = new List<Guid>();
664+
int missingCount = 0;
635665

636666
var detachedScores = realmAccess.Run(r =>
637667
{
@@ -643,6 +673,8 @@ private void upgradeScoreRanks()
643673

644674
if (s != null)
645675
scores.Add(s.Detach());
676+
else
677+
missingCount++;
646678
}
647679

648680
return scores;
@@ -672,6 +704,7 @@ private void upgradeScoreRanks()
672704
foreach (var update in updates)
673705
{
674706
var s = r.Find<ScoreInfo>(update.id);
707+
675708
if (s != null)
676709
{
677710
s.Rank = update.rank;
@@ -682,6 +715,7 @@ private void upgradeScoreRanks()
682715
foreach (var id in failedIds)
683716
{
684717
var s = r.Find<ScoreInfo>(id);
718+
685719
if (s != null)
686720
s.BackgroundReprocessingFailed = true;
687721
}
@@ -697,20 +731,25 @@ private void upgradeScoreRanks()
697731
catch (Exception e)
698732
{
699733
Logger.Log($"Fatal error writing batch in score rank upgrade: {e}");
734+
failedCount += updates.Count + failedIds.Count;
700735
}
701736
}
737+
738+
processedCount += missingCount;
702739
}
703740

704741
completeNotification(notification, processedCount, scoreIds.Count, failedCount);
705742
}
706743

707744
private void backpopulateMissingSubmissionAndRankDates()
708745
{
709-
if (DebugUtils.IsNUnitRunning) return;
746+
if (DebugUtils.IsNUnitRunning)
747+
return;
710748

711749
if (!localMetadataSource.Available)
712750
{
713751
Logger.Log("Cannot backpopulate missing submission/rank dates because the local metadata cache is missing.");
752+
714753
return;
715754
}
716755

@@ -719,12 +758,14 @@ private void backpopulateMissingSubmissionAndRankDates()
719758
if (!localMetadataSource.IsAtLeastVersion(2))
720759
{
721760
Logger.Log("Cannot backpopulate missing submission/rank dates because the local metadata cache is too old.");
761+
722762
return;
723763
}
724764
}
725765
catch (Exception ex)
726766
{
727767
Logger.Log($"Error when trying to query version of local metadata cache: {ex}");
768+
728769
return;
729770
}
730771

@@ -778,10 +819,12 @@ private void backpopulateMissingSubmissionAndRankDates()
778819
Debug.Assert(result != null);
779820
beatmapSet.DateRanked = result.DateRanked;
780821
beatmapSet.DateSubmitted = result.DateSubmitted;
822+
781823
return true;
782824
}
783825

784826
Logger.Log($"Could not find {beatmapSet.GetDisplayString()} in local cache while backpopulating missing submission/rank date");
827+
785828
return false;
786829
});
787830

@@ -813,19 +856,22 @@ protected virtual void BackpopulateUserTags()
813856

814857
if (!localMetadataSource.Available || !localMetadataSource.IsAtLeastVersion(3))
815858
{
816-
if (DebugUtils.IsNUnitRunning) return;
859+
if (DebugUtils.IsNUnitRunning)
860+
return;
817861

818862
Logger.Log(@"Local metadata cache has too low version to backpopulate user tags, attempting refetch...");
819863
localMetadataSource.FetchCache().WaitSafely();
820864

821865
if (!localMetadataSource.Available || !localMetadataSource.IsAtLeastVersion(3))
822866
{
823867
Logger.Log(@"Local metadata cache refetch failed. Aborting user tags backpopulation.");
868+
824869
return;
825870
}
826871
}
827872

828873
var lastPopulation = config.Get<DateTime?>(OsuSetting.LastOnlineTagsPopulation);
874+
829875
// dropping time data here completely is intentional, because storing the date to config is a lossy operation
830876
// (truncates some ticks off of the date when it's being converted to string and back).
831877
// therefore, if precision isn't explicitly constrained, the condition below would always fail just because the date stored to config
@@ -835,6 +881,7 @@ protected virtual void BackpopulateUserTags()
835881
if (metadataSourceFetchDate <= lastPopulation)
836882
{
837883
Logger.Log($@"Skipping user tag population because the local metadata source hasn't been updated since the last time user tags were checked ({lastPopulation.Value:d})");
884+
838885
return;
839886
}
840887

@@ -888,13 +935,15 @@ protected virtual void BackpopulateUserTags()
888935
{
889936
beatmap.Metadata.UserTags.Clear();
890937
beatmap.Metadata.UserTags.AddRange(userTags);
938+
891939
return true;
892940
}
893941

894942
return false;
895943
}
896944

897945
Logger.Log(@$"Could not find {beatmap.GetDisplayString()} in local cache while backpopulating missing user tags");
946+
898947
return false;
899948
});
900949

replace_method.py

Lines changed: 0 additions & 51 deletions
This file was deleted.

0 commit comments

Comments
 (0)