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()