From 283108f6eef1489ce406f8a077e7c0940c27b58b Mon Sep 17 00:00:00 2001 From: Benjamin Grabkowitz Date: Mon, 8 Aug 2022 16:37:31 -0400 Subject: [PATCH 1/2] Refactoring MiniProfilerBaseOptionsExtensions.ExpireAndGetUnviewedAsync to utilize a new overload of IAsyncStorage.SetViewedAsync that accepts multiple ids. Adding PostgreSqlStorage implementation that can set multiple profilers as viewed in a single query. Code cleanup. Adding implementation for RemoteAppendExample.cs Proposal for an IAdvancedAsyncStorage interface to facilitate some optional advanced operations. --- .../PostgreSqlStorage.cs | 26 +++++++++++---- .../MiniProfilerBaseOptionsExtensions.cs | 19 +++++++++-- .../Storage/IAdvancedAsyncStorage.cs | 18 +++++++++++ .../Storage/MultiStorageProvider.cs | 27 +++++++++++++++- .../Storage/NullStorage.cs | 9 +++++- .../Storage/StorageBaseTest.cs | 32 +++++++++++++++++++ 6 files changed, 119 insertions(+), 12 deletions(-) create mode 100644 src/MiniProfiler.Shared/Storage/IAdvancedAsyncStorage.cs diff --git a/src/MiniProfiler.Providers.PostgreSql/PostgreSqlStorage.cs b/src/MiniProfiler.Providers.PostgreSql/PostgreSqlStorage.cs index d3b2bbd1d..ce2a947d7 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 1b2dabddd..6ae345c69 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 000000000..cf33d9986 --- /dev/null +++ b/src/MiniProfiler.Shared/Storage/IAdvancedAsyncStorage.cs @@ -0,0 +1,18 @@ +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 ebbb4a33a..aca1181cd 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 0e6bafccf..5106fa234 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 758840730..e386f6f21 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() From 830aee2efca9d0a1e92ceb8120856f513bfe12ab Mon Sep 17 00:00:00 2001 From: Benjamin Grabkowitz Date: Mon, 8 Aug 2022 22:25:21 -0400 Subject: [PATCH 2/2] Correcting wrong C# version. --- .../Storage/IAdvancedAsyncStorage.cs | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/MiniProfiler.Shared/Storage/IAdvancedAsyncStorage.cs b/src/MiniProfiler.Shared/Storage/IAdvancedAsyncStorage.cs index cf33d9986..d15087158 100644 --- a/src/MiniProfiler.Shared/Storage/IAdvancedAsyncStorage.cs +++ b/src/MiniProfiler.Shared/Storage/IAdvancedAsyncStorage.cs @@ -2,17 +2,18 @@ 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 +namespace StackExchange.Profiling.Storage { /// - /// Asynchronously sets the provided profiler sessions to "viewed" + /// Provides saving and loading s to a storage medium with some advanced operations. /// - /// The user to set this profiler ID as viewed for. - /// The profiler IDs to set viewed. - Task SetViewedAsync(string user, IEnumerable ids); + 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); + } }