diff --git a/src/MiniProfiler.Providers.PostgreSql/PostgreSqlStorage.cs b/src/MiniProfiler.Providers.PostgreSql/PostgreSqlStorage.cs index d3b2bbd1..ce2a947d 100644 --- a/src/MiniProfiler.Providers.PostgreSql/PostgreSqlStorage.cs +++ b/src/MiniProfiler.Providers.PostgreSql/PostgreSqlStorage.cs @@ -12,7 +12,7 @@ namespace StackExchange.Profiling.Storage /// /// Understands how to store a to a PostgreSQL Server database. /// - public class PostgreSqlStorage : DatabaseStorageBase + public class PostgreSqlStorage : DatabaseStorageBase, IAdvancedAsyncStorage { /// /// Initializes a new instance of the class with the specified connection string. @@ -268,28 +268,40 @@ public override async Task LoadAsync(Guid id) /// The user to set this profiler ID as viewed for. /// The profiler ID to set viewed. public override Task SetViewedAsync(string user, Guid id) => ToggleViewedAsync(user, id, true); + + /// + /// Asynchronously sets the provided profiler sessions to "viewed" + /// + /// The user to set this profiler ID as viewed for. + /// The profiler IDs to set viewed. + public Task SetViewedAsync(string user, IEnumerable ids) => ToggleViewedAsync(user, ids, true); private string _toggleViewedSql; private string ToggleViewedSql => _toggleViewedSql ??= $@" Update {MiniProfilersTable} - Set HasUserViewed = @hasUserVeiwed - Where Id = @id + Set HasUserViewed = @hasUserViewed + Where Id = ANY(@ids) And ""User"" = @user"; - private void ToggleViewed(string user, Guid id, bool hasUserVeiwed) + private void ToggleViewed(string user, Guid id, bool hasUserViewed) { using (var conn = GetConnection()) { - conn.Execute(ToggleViewedSql, new { id, user, hasUserVeiwed }); + conn.Execute(ToggleViewedSql, new { ids = new [] { id }, user, hasUserViewed }); } } - private async Task ToggleViewedAsync(string user, Guid id, bool hasUserVeiwed) + private Task ToggleViewedAsync(string user, Guid id, bool hasUserViewed) + { + return ToggleViewedAsync(user, new [] { id }, hasUserViewed); + } + + private async Task ToggleViewedAsync(string user, IEnumerable ids, bool hasUserViewed) { using (var conn = GetConnection()) { - await conn.ExecuteAsync(ToggleViewedSql, new { id, user, hasUserVeiwed }).ConfigureAwait(false); + await conn.ExecuteAsync(ToggleViewedSql, new { ids = ids.ToArray(), user, hasUserViewed }).ConfigureAwait(false); } } diff --git a/src/MiniProfiler.Shared/Internal/MiniProfilerBaseOptionsExtensions.cs b/src/MiniProfiler.Shared/Internal/MiniProfilerBaseOptionsExtensions.cs index 1b2dabdd..6ae345c6 100644 --- a/src/MiniProfiler.Shared/Internal/MiniProfilerBaseOptionsExtensions.cs +++ b/src/MiniProfiler.Shared/Internal/MiniProfilerBaseOptionsExtensions.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; +using StackExchange.Profiling.Storage; namespace StackExchange.Profiling.Internal { @@ -23,7 +25,7 @@ public static List ExpireAndGetUnviewed(this MiniProfilerBaseOptions optio { for (var i = 0; i < ids.Count - options.MaxUnviewedProfiles; i++) { - options.Storage.SetViewedAsync(user, ids[i]); + options.Storage.SetViewed(user, ids[i]); } } return ids; @@ -42,12 +44,23 @@ public static async Task> ExpireAndGetUnviewedAsync(this MiniProfiler { return null; } + var ids = await options.Storage.GetUnviewedIdsAsync(user).ConfigureAwait(false); + if (ids?.Count > options.MaxUnviewedProfiles) { - for (var i = 0; i < ids.Count - options.MaxUnviewedProfiles; i++) + var idsToSetViewed = ids.Take(ids.Count - options.MaxUnviewedProfiles); + + if (options.Storage is IAdvancedAsyncStorage storage) + { + await storage.SetViewedAsync(user, idsToSetViewed).ConfigureAwait(false); + } + else { - await options.Storage.SetViewedAsync(user, ids[i]).ConfigureAwait(false); + foreach (var id in idsToSetViewed) + { + await options.Storage.SetViewedAsync(user, id).ConfigureAwait(false); + } } } return ids; diff --git a/src/MiniProfiler.Shared/Storage/IAdvancedAsyncStorage.cs b/src/MiniProfiler.Shared/Storage/IAdvancedAsyncStorage.cs new file mode 100644 index 00000000..d1508715 --- /dev/null +++ b/src/MiniProfiler.Shared/Storage/IAdvancedAsyncStorage.cs @@ -0,0 +1,19 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace StackExchange.Profiling.Storage +{ + /// + /// Provides saving and loading s to a storage medium with some advanced operations. + /// + public interface IAdvancedAsyncStorage : IAsyncStorage + { + /// + /// Asynchronously sets the provided profiler sessions to "viewed" + /// + /// The user to set this profiler ID as viewed for. + /// The profiler IDs to set viewed. + Task SetViewedAsync(string user, IEnumerable ids); + } +} diff --git a/src/MiniProfiler.Shared/Storage/MultiStorageProvider.cs b/src/MiniProfiler.Shared/Storage/MultiStorageProvider.cs index ebbb4a33..aca1181c 100644 --- a/src/MiniProfiler.Shared/Storage/MultiStorageProvider.cs +++ b/src/MiniProfiler.Shared/Storage/MultiStorageProvider.cs @@ -11,7 +11,7 @@ namespace StackExchange.Profiling.Storage /// When saving, will save in all Stores. /// /// Ideal usage scenario - you want to store requests in Cache and Sql Server, but only want to retrieve from Cache if it is available - public class MultiStorageProvider : IAsyncStorage + public class MultiStorageProvider : IAdvancedAsyncStorage { /// /// The stores that are exposed by this @@ -244,6 +244,31 @@ public Task SetViewedAsync(string user, Guid id) return Task.WhenAll(Stores.Select(s => s.SetViewedAsync(user, id))); } + /// + /// Asynchronously sets the provided profiler sessions to "viewed" + /// + /// The user to set this profiler ID as viewed for. + /// The profiler IDs to set viewed. + public Task SetViewedAsync(string user, IEnumerable ids) + { + if (Stores == null) return Task.CompletedTask; + + return Task.WhenAll(Stores.Select(async s => + { + if (s is IAdvancedAsyncStorage storage) + { + await storage.SetViewedAsync(user, ids).ConfigureAwait(false); + } + else + { + foreach (var id in ids) + { + await s.SetViewedAsync(user, id).ConfigureAwait(false); + } + } + })); + } + /// /// Runs on each object in and returns the Union of results. /// Will run on multiple stores in parallel if = true. diff --git a/src/MiniProfiler.Shared/Storage/NullStorage.cs b/src/MiniProfiler.Shared/Storage/NullStorage.cs index 0e6bafcc..5106fa23 100644 --- a/src/MiniProfiler.Shared/Storage/NullStorage.cs +++ b/src/MiniProfiler.Shared/Storage/NullStorage.cs @@ -8,7 +8,7 @@ namespace StackExchange.Profiling.Storage /// /// Empty storage no-nothing provider for doing nothing at all. Super efficient. /// - public class NullStorage : IAsyncStorage + public class NullStorage : IAdvancedAsyncStorage { /// /// Returns no profilers. @@ -88,6 +88,13 @@ public void SetViewed(string user, Guid id) { /* no-op */ } /// No one cares. public Task SetViewedAsync(string user, Guid id) => Task.CompletedTask; + /// + /// Sets nothing. + /// + /// No one cares. + /// No one cares. + public Task SetViewedAsync(string user, IEnumerable ids) => Task.CompletedTask; + /// /// Gets nothing. /// diff --git a/tests/MiniProfiler.Tests/Storage/StorageBaseTest.cs b/tests/MiniProfiler.Tests/Storage/StorageBaseTest.cs index 75884073..e386f6f2 100644 --- a/tests/MiniProfiler.Tests/Storage/StorageBaseTest.cs +++ b/tests/MiniProfiler.Tests/Storage/StorageBaseTest.cs @@ -1,8 +1,11 @@ using System; +using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Runtime.CompilerServices; using System.Threading.Tasks; using Dapper; +using StackExchange.Profiling.Internal; using StackExchange.Profiling.Storage; using Xunit; using Xunit.Abstractions; @@ -146,6 +149,35 @@ public async Task SetViewedAsync() var unviewedIds2 = await Storage.GetUnviewedIdsAsync(mp.User).ConfigureAwait(false); Assert.DoesNotContain(mp.Id, unviewedIds2); } + + [Fact] + public async Task ExpireAndGetUnviewedAsync() + { + Options.Storage = Storage; + var user = "TestUser"; + var mps = Enumerable.Range(0, 500) + .Select(i => GetMiniProfiler(user: user)) + .ToList(); + + foreach (var mp in mps) + { + Assert.False(mp.HasUserViewed); + await Storage.SaveAsync(mp).ConfigureAwait(false); + Assert.False(mp.HasUserViewed); + } + + var unviewedIds = await Storage.GetUnviewedIdsAsync(user).ConfigureAwait(false); + Assert.All(mps, mp => Assert.Contains(mp.Id, unviewedIds)); + + var sw = Stopwatch.StartNew(); + await Options.ExpireAndGetUnviewedAsync(user); + sw.Stop(); + Output.WriteLine($"{nameof(MiniProfilerBaseOptionsExtensions.ExpireAndGetUnviewedAsync)} completed in {sw.ElapsedMilliseconds}ms"); + + var unviewedIds2 = await Storage.GetUnviewedIdsAsync(user).ConfigureAwait(false); + Assert.InRange(unviewedIds2.Count, 0, Options.MaxUnviewedProfiles); + Assert.Subset(new HashSet(unviewedIds), new HashSet(unviewedIds2)); + } [Fact] public void SetUnviewed()