diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs new file mode 100644 index 0000000000..f5c84c9edf --- /dev/null +++ b/osu.Game/Database/UserLookupCache.cs @@ -0,0 +1,118 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Users; + +namespace osu.Game.Database +{ + public class UserLookupCache : MemoryCachingComponent + { + private readonly ConcurrentBag nextTaskIDs = new ConcurrentBag(); + + [Resolved] + private IAPIProvider api { get; set; } + + private readonly object taskAssignmentLock = new object(); + + private Task> pendingRequest; + + /// + /// Whether has already grabbed its IDs. + /// + private bool pendingRequestConsumedIDs; + + public Task GetUser(int userId, CancellationToken token = default) => GetAsync(userId, token); + + protected override async Task ComputeValueAsync(int lookup, CancellationToken token = default) + { + var users = await getQueryTaskForUser(lookup); + return users.FirstOrDefault(u => u.Id == lookup); + } + + /// + /// Return the task responsible for fetching the provided user. + /// This may be part of a larger batch lookup to reduce web requests. + /// + /// The user to lookup. + /// The task responsible for the lookup. + private Task> getQueryTaskForUser(int userId) + { + lock (taskAssignmentLock) + { + nextTaskIDs.Add(userId); + + // if there's a pending request which hasn't been started yet (and is not yet full), we can wait on it. + if (pendingRequest != null && !pendingRequestConsumedIDs && nextTaskIDs.Count < 50) + return pendingRequest; + + return queueNextTask(nextLookup); + } + + List nextLookup() + { + int[] lookupItems; + + lock (taskAssignmentLock) + { + pendingRequestConsumedIDs = true; + lookupItems = nextTaskIDs.ToArray(); + nextTaskIDs.Clear(); + + if (lookupItems.Length == 0) + { + queueNextTask(null); + return new List(); + } + } + + var request = new GetUsersRequest(lookupItems); + + // rather than queueing, we maintain our own single-threaded request stream. + request.Perform(api); + + return request.Result.Users; + } + } + + /// + /// Queues new work at the end of the current work tasks. + /// Ensures the provided work is eventually run. + /// + /// The work to run. Can be null to signify the end of available work. + /// The task tracking this work. + private Task> queueNextTask(Func> work) + { + lock (taskAssignmentLock) + { + if (work == null) + { + pendingRequest = null; + pendingRequestConsumedIDs = false; + } + else if (pendingRequest == null) + { + // special case for the first request ever. + pendingRequest = Task.Run(work); + pendingRequestConsumedIDs = false; + } + else + { + // append the new request on to the last to be executed. + pendingRequest = pendingRequest.ContinueWith(_ => work()); + pendingRequestConsumedIDs = false; + } + + return pendingRequest; + } + } + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 3da692249d..193f6fe61b 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -61,6 +61,8 @@ namespace osu.Game protected BeatmapDifficultyCache DifficultyCache; + protected UserLookupCache UserCache; + protected SkinManager SkinManager; protected RulesetStore RulesetStore; @@ -229,6 +231,9 @@ namespace osu.Game dependencies.Cache(DifficultyCache = new BeatmapDifficultyCache()); AddInternal(DifficultyCache); + dependencies.Cache(UserCache = new UserLookupCache()); + AddInternal(UserCache); + var scorePerformanceManager = new ScorePerformanceCache(); dependencies.Cache(scorePerformanceManager); AddInternal(scorePerformanceManager); diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index e9915df801..a988381f29 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -8,8 +8,8 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; +using osu.Game.Database; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.Spectator; using osu.Game.Screens.Multi.Match.Components; using osu.Game.Screens.Play; @@ -45,6 +45,9 @@ namespace osu.Game.Overlays.Dashboard [Resolved] private IAPIProvider api { get; set; } + [Resolved] + private UserLookupCache users { get; set; } + protected override void LoadComplete() { base.LoadComplete(); @@ -55,18 +58,19 @@ namespace osu.Game.Overlays.Dashboard switch (e.Action) { case NotifyCollectionChangedAction.Add: - var request = new GetUsersRequest(e.NewItems.OfType().ToArray()); - request.Success += users => Schedule(() => + foreach (var id in e.NewItems.OfType().ToArray()) { - foreach (var user in users.Users) + users.GetUser(id).ContinueWith(u => { - if (playingUsers.Contains(user.Id)) - userFlow.Add(createUserPanel(user)); - } - }); + Schedule(() => + { + if (playingUsers.Contains(u.Result.Id)) + userFlow.Add(createUserPanel(u.Result)); + }); + }); + } - api.Queue(request); break; case NotifyCollectionChangedAction.Remove: