diff --git a/samples/Samples.Mvc5/Controllers/HomeController.cs b/samples/Samples.Mvc5/Controllers/HomeController.cs
index a176a204a..bb6a45224 100644
--- a/samples/Samples.Mvc5/Controllers/HomeController.cs
+++ b/samples/Samples.Mvc5/Controllers/HomeController.cs
@@ -10,6 +10,8 @@
using Samples.Mvc5.EfModelFirst;
using Samples.Mvc5.EFCodeFirst;
using Samples.Mvc5.Helpers;
+using System.Threading.Tasks;
+using System.Collections.Generic;
namespace Samples.Mvc5.Controllers
{
@@ -131,6 +133,34 @@ public ActionResult About()
/// The .
public ActionResult ResultsAuthorization() => View();
+ public async Task CallAsync()
+ {
+ var profiler = MiniProfiler.Current;
+ var service = new FooService();
+ using (profiler.Step("action"))
+ {
+ var head = profiler.Head;
+ var tasks = new List();
+ for (int i = 0; i < 10; i++)
+ {
+ tasks.Add(service.FooAsync(head));
+ }
+ await Task.WhenAll(tasks);
+ return Content("All good");
+ }
+ }
+
+ class FooService
+ {
+ public async Task FooAsync(Timing head)
+ {
+ using (new Timing(MiniProfiler.Current, head, "foo async", minSaveMs: 100000))
+ {
+ await Task.Delay(100).ConfigureAwait(false);
+ }
+ }
+ }
+
///
/// fetch the route hits.
///
diff --git a/samples/Samples.Mvc5/Views/Home/Index.cshtml b/samples/Samples.Mvc5/Views/Home/Index.cshtml
index 3eb0a9b5e..b40fea6f6 100644
--- a/samples/Samples.Mvc5/Views/Home/Index.cshtml
+++ b/samples/Samples.Mvc5/Views/Home/Index.cshtml
@@ -65,6 +65,7 @@
Massive Nesting 2
Parameterized SQL with Enums
Test Min Save Ms
+ Async call + Concurrency
diff --git a/src/MiniProfiler.Shared/Timing.cs b/src/MiniProfiler.Shared/Timing.cs
index e3aa9839f..64d8c6a48 100644
--- a/src/MiniProfiler.Shared/Timing.cs
+++ b/src/MiniProfiler.Shared/Timing.cs
@@ -3,6 +3,8 @@
using System.Linq;
using System.Runtime.Serialization;
using StackExchange.Profiling.Helpers;
+using System.Threading;
+using System.Threading.Tasks;
#if NET45
using System.Web.Script.Serialization;
#endif
@@ -22,6 +24,9 @@ public class Timing : IDisposable
private readonly decimal? _minSaveMs;
private readonly bool _includeChildrenWithMinSave;
+ // used for concurrent access detection
+ private int _threadsAccessing = 0;
+
///
/// Initialises a new instance of the class.
/// Obsolete - used for serialization.
@@ -253,19 +258,43 @@ public void Stop()
///
public void AddChild(Timing timing)
{
- if (Children == null)
+ if (Interlocked.Increment(ref _threadsAccessing) > 1)
+ throw new Exception("Concurrent access");
+
+ try
+ {
+ if (Children == null)
Children = new List();
- Children.Add(timing);
- if(timing.Profiler == null)
- timing.Profiler = Profiler;
- timing.ParentTiming = this;
- timing.ParentTimingId = Id;
- if (Profiler != null)
- timing.MiniProfilerId = Profiler.Id;
+ Children.Add(timing);
+ if(timing.Profiler == null)
+ timing.Profiler = Profiler;
+ timing.ParentTiming = this;
+ timing.ParentTimingId = Id;
+ if (Profiler != null)
+ timing.MiniProfilerId = Profiler.Id;
+ }
+ finally
+ {
+ Interlocked.Decrement(ref _threadsAccessing);
+ }
}
- internal void RemoveChild(Timing timing) => Children?.Remove(timing);
+ internal void RemoveChild(Timing timing)
+ {
+ if (Interlocked.Increment(ref _threadsAccessing) > 1)
+ throw new Exception("Concurrent access");
+
+ Task.Delay(10).Wait();
+ try
+ {
+ Children?.Remove(timing);
+ }
+ finally
+ {
+ Interlocked.Decrement(ref _threadsAccessing);
+ }
+ }
///
/// Adds to this step's dictionary of