diff --git a/osu.Server.Queues.ScoreStatisticsProcessor/Stores/BeatmapStore.cs b/osu.Server.Queues.ScoreStatisticsProcessor/Stores/BeatmapStore.cs index 53cdb5b4..6a4a758d 100644 --- a/osu.Server.Queues.ScoreStatisticsProcessor/Stores/BeatmapStore.cs +++ b/osu.Server.Queues.ScoreStatisticsProcessor/Stores/BeatmapStore.cs @@ -2,13 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; using Dapper; +using Microsoft.Extensions.Caching.Memory; using MySqlConnector; using osu.Framework.IO.Network; using osu.Game.Beatmaps; @@ -29,9 +29,27 @@ public class BeatmapStore { private static readonly bool use_realtime_difficulty_calculation = Environment.GetEnvironmentVariable("REALTIME_DIFFICULTY") != "0"; private static readonly string beatmap_download_path = Environment.GetEnvironmentVariable("BEATMAP_DOWNLOAD_PATH") ?? "https://osu.ppy.sh/osu/{0}"; + private static readonly uint memory_cache_size_limit = uint.Parse(Environment.GetEnvironmentVariable("MEMORY_CACHE_SIZE_LIMIT") ?? "128000000"); + private static readonly TimeSpan memory_cache_sliding_expiration = TimeSpan.FromSeconds(uint.Parse(Environment.GetEnvironmentVariable("MEMORY_CACHE_SLIDING_EXPIRATION_SECONDS") ?? "3600")); - private readonly ConcurrentDictionary beatmapCache = new ConcurrentDictionary(); - private readonly ConcurrentDictionary attributeCache = new ConcurrentDictionary(); + /// + /// The size of a in bytes. Used for tracking memory usage. + /// + private const int beatmap_difficulty_attribute_size = 24; + + /// + /// The rough size of base class in bytes. + /// + private const int difficulty_attribute_size = 24; + + /// + /// The size of a in bytes. Used for tracking memory usage. + /// + private const int beatmap_size = 72; + + private readonly MemoryCache attributeMemoryCache; + + private readonly MemoryCache beatmapMemoryCache; private readonly IReadOnlyDictionary blacklist; private int beatmapCacheMiss; @@ -39,7 +57,7 @@ public class BeatmapStore public string GetCacheStats() { - string output = $"caches: [beatmap {beatmapCache.Count:N0} +{beatmapCacheMiss:N0}] [attrib {attributeCache.Count:N0} +{attribCacheMiss:N0}]"; + string output = $"caches: [beatmap {beatmapMemoryCache.Count:N0} +{beatmapCacheMiss:N0}] [attrib {attributeMemoryCache.Count:N0} +{attribCacheMiss:N0}]"; Interlocked.Exchange(ref beatmapCacheMiss, 0); Interlocked.Exchange(ref attribCacheMiss, 0); @@ -50,6 +68,16 @@ public string GetCacheStats() private BeatmapStore(IEnumerable> blacklist) { this.blacklist = new Dictionary(blacklist); + + attributeMemoryCache = new MemoryCache(new MemoryCacheOptions + { + SizeLimit = memory_cache_size_limit, + }); + + beatmapMemoryCache = new MemoryCache(new MemoryCacheOptions + { + SizeLimit = memory_cache_size_limit, + }); } /// @@ -98,35 +126,37 @@ public async Task GetDifficultyAttributesAsync(Beatmap bea return calculator.Calculate(mods); } - LegacyMods legacyModValue = getLegacyModsForAttributeLookup(beatmap, ruleset, mods); - - DifficultyAttributeKey key = new DifficultyAttributeKey(beatmap.beatmap_id, (uint)ruleset.RulesetInfo.OnlineID, (uint)legacyModValue); - - if (attributeCache.TryGetValue(key, out DifficultyAttributes? difficultyAttributes)) - return difficultyAttributes; + DifficultyAttributeKey key = new DifficultyAttributeKey(beatmap.beatmap_id, (uint)ruleset.RulesetInfo.OnlineID, (uint)getLegacyModsForAttributeLookup(beatmap, ruleset, mods)); - BeatmapDifficultyAttribute[] dbAttribs = (await connection.QueryAsync( - "SELECT * FROM osu_beatmap_difficulty_attribs WHERE `beatmap_id` = @BeatmapId AND `mode` = @RulesetId AND `mods` = @ModValue", new - { - key.BeatmapId, - key.RulesetId, - key.ModValue - }, transaction: transaction)).ToArray(); - - try - { - difficultyAttributes = LegacyRulesetHelper.CreateDifficultyAttributes(ruleset.RulesetInfo.OnlineID); - difficultyAttributes.FromDatabaseAttributes(dbAttribs.ToDictionary(a => (int)a.attrib_id, a => (double)a.value), beatmap); - return attributeCache[key] = difficultyAttributes; - } - catch (Exception ex) + return (await attributeMemoryCache.GetOrCreateAsync(key, async cacheEntry => { - throw new DifficultyAttributesMissingException(key, ex); - } - finally - { - Interlocked.Increment(ref attribCacheMiss); - } + try + { + BeatmapDifficultyAttribute[] dbAttributes = (await connection.QueryAsync( + "SELECT * FROM osu_beatmap_difficulty_attribs WHERE `beatmap_id` = @BeatmapId AND `mode` = @RulesetId AND `mods` = @ModValue", new + { + key.BeatmapId, + key.RulesetId, + key.ModValue + }, transaction: transaction)).ToArray(); + + // approximated + cacheEntry.SetSize(difficulty_attribute_size + beatmap_difficulty_attribute_size * dbAttributes.Length); + cacheEntry.SetSlidingExpiration(memory_cache_sliding_expiration); + + DifficultyAttributes attributes = LegacyRulesetHelper.CreateDifficultyAttributes(ruleset.RulesetInfo.OnlineID); + attributes.FromDatabaseAttributes(dbAttributes.ToDictionary(a => (int)a.attrib_id, a => (double)a.value), beatmap); + return attributes; + } + catch (Exception ex) + { + throw new DifficultyAttributesMissingException(key, ex); + } + finally + { + Interlocked.Increment(ref attribCacheMiss); + } + }))!; } /// @@ -154,17 +184,20 @@ private static LegacyMods getLegacyModsForAttributeLookup(Beatmap beatmap, Rules /// The . /// An existing transaction. /// The retrieved beatmap, or null if not existing. - public async Task GetBeatmapAsync(uint beatmapId, MySqlConnection connection, MySqlTransaction? transaction = null) - { - if (beatmapCache.TryGetValue(beatmapId, out var beatmap)) - return beatmap; - - Interlocked.Increment(ref beatmapCacheMiss); - return beatmapCache[beatmapId] = await connection.QuerySingleOrDefaultAsync("SELECT * FROM osu_beatmaps WHERE `beatmap_id` = @BeatmapId", new + public Task GetBeatmapAsync(uint beatmapId, MySqlConnection connection, MySqlTransaction? transaction = null) => beatmapMemoryCache.GetOrCreateAsync( + beatmapId, + cacheEntry => { - BeatmapId = beatmapId - }, transaction: transaction); - } + Interlocked.Increment(ref beatmapCacheMiss); + + cacheEntry.SetSlidingExpiration(memory_cache_sliding_expiration); + cacheEntry.SetSize(beatmap_size); + + return connection.QuerySingleOrDefaultAsync("SELECT * FROM osu_beatmaps WHERE `beatmap_id` = @BeatmapId", new + { + BeatmapId = beatmapId + }, transaction: transaction); + }); /// /// Whether performance points may be awarded for the given beatmap and ruleset combination.