Skip to content

Commit 03f745f

Browse files
author
Bart Koelman
committed
Added basic instrumentation framework
1 parent d555bb5 commit 03f745f

File tree

7 files changed

+573
-0
lines changed

7 files changed

+573
-0
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using System;
2+
using Microsoft.AspNetCore.Http;
3+
4+
namespace JsonApiDotNetCore.Diagnostics
5+
{
6+
/// <summary>
7+
/// Code timing session management intended for use in ASP.NET Web Applications. Uses <see cref="HttpContext.Items" /> to isolate concurrent requests.
8+
/// Can be used with async/wait, but it cannot distinguish between concurrently running threads within a single HTTP request, so you'll need to pass an
9+
/// <see cref="CascadingCodeTimer" /> instance through the entire call chain in that case.
10+
/// </summary>
11+
internal sealed class AspNetCodeTimerSession : ICodeTimerSession
12+
{
13+
private const string HttpContextItemKey = "CascadingCodeTimer:Session";
14+
15+
private readonly HttpContext _httpContext;
16+
private readonly IHttpContextAccessor _httpContextAccessor;
17+
18+
public ICodeTimer CodeTimer
19+
{
20+
get
21+
{
22+
HttpContext httpContext = GetHttpContext();
23+
var codeTimer = (ICodeTimer)httpContext.Items[HttpContextItemKey];
24+
25+
if (codeTimer == null)
26+
{
27+
codeTimer = new CascadingCodeTimer();
28+
httpContext.Items[HttpContextItemKey] = codeTimer;
29+
}
30+
31+
return codeTimer;
32+
}
33+
}
34+
35+
public event EventHandler Disposed;
36+
37+
public AspNetCodeTimerSession(IHttpContextAccessor httpContextAccessor)
38+
{
39+
ArgumentGuard.NotNull(httpContextAccessor, nameof(httpContextAccessor));
40+
41+
_httpContextAccessor = httpContextAccessor;
42+
}
43+
44+
public AspNetCodeTimerSession(HttpContext httpContext)
45+
{
46+
ArgumentGuard.NotNull(httpContext, nameof(httpContext));
47+
48+
_httpContext = httpContext;
49+
}
50+
51+
public void Dispose()
52+
{
53+
HttpContext httpContext = TryGetHttpContext();
54+
var codeTimer = (ICodeTimer)httpContext?.Items[HttpContextItemKey];
55+
56+
if (codeTimer != null)
57+
{
58+
codeTimer.Dispose();
59+
httpContext.Items[HttpContextItemKey] = null;
60+
}
61+
62+
OnDisposed();
63+
}
64+
65+
private void OnDisposed()
66+
{
67+
Disposed?.Invoke(this, EventArgs.Empty);
68+
}
69+
70+
private HttpContext GetHttpContext()
71+
{
72+
HttpContext httpContext = TryGetHttpContext();
73+
return httpContext ?? throw new InvalidOperationException("An active HTTP request is required.");
74+
}
75+
76+
private HttpContext TryGetHttpContext()
77+
{
78+
return _httpContext ?? _httpContextAccessor?.HttpContext;
79+
}
80+
}
81+
}
Lines changed: 282 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,282 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Globalization;
5+
using System.Linq;
6+
using System.Runtime.InteropServices;
7+
using System.Text;
8+
9+
namespace JsonApiDotNetCore.Diagnostics
10+
{
11+
/// <summary>
12+
/// Records execution times for nested code blocks.
13+
/// </summary>
14+
internal sealed class CascadingCodeTimer : ICodeTimer
15+
{
16+
private readonly Stopwatch _stopwatch = new();
17+
private readonly Stack<MeasureScope> _activeScopeStack = new();
18+
private readonly List<MeasureScope> _completedScopes = new();
19+
20+
static CascadingCodeTimer()
21+
{
22+
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
23+
{
24+
// Be default, measurements using Stopwatch can differ 25%-30% on the same function on the same computer.
25+
// The steps below ensure to get an accuracy of 0.1%-0.2%. With this accuracy, algorithms can be tested and compared.
26+
// https://www.codeproject.com/Articles/61964/Performance-Tests-Precise-Run-Time-Measurements-wi
27+
28+
// The most important thing is to prevent switching between CPU cores or processors. Switching dismisses the cache, etc. and has a huge performance impact on the test.
29+
Process.GetCurrentProcess().ProcessorAffinity = new IntPtr(2);
30+
31+
// To get the CPU core more exclusively, we must prevent that other threads can use this CPU core. We set our process and thread priority to achieve this.
32+
// Note we should NOT set the thread priority, because async/await usage makes the code jump between pooled threads (depending on Synchronization Context).
33+
Process.GetCurrentProcess().PriorityClass = ProcessPriorityClass.High;
34+
}
35+
}
36+
37+
/// <inheritdoc />
38+
public IDisposable Measure(string name, bool excludeInRelativeCost = false)
39+
{
40+
MeasureScope childScope = CreateChildScope(name, excludeInRelativeCost);
41+
_activeScopeStack.Push(childScope);
42+
43+
return childScope;
44+
}
45+
46+
private MeasureScope CreateChildScope(string name, bool excludeInRelativeCost)
47+
{
48+
if (_activeScopeStack.TryPeek(out MeasureScope topScope))
49+
{
50+
return topScope.SpawnChild(this, name, excludeInRelativeCost);
51+
}
52+
53+
return new MeasureScope(this, name, excludeInRelativeCost);
54+
}
55+
56+
private void Close(MeasureScope scope)
57+
{
58+
if (!_activeScopeStack.TryPeek(out MeasureScope topScope) || topScope != scope)
59+
{
60+
throw new InvalidOperationException($"Scope '{scope.Name}' cannot be disposed at this time, because it is not the currently active scope.");
61+
}
62+
63+
_activeScopeStack.Pop();
64+
65+
if (!_activeScopeStack.Any())
66+
{
67+
_completedScopes.Add(scope);
68+
}
69+
}
70+
71+
/// <inheritdoc />
72+
public string GetResult()
73+
{
74+
int paddingLength = GetPaddingLength();
75+
76+
var builder = new StringBuilder();
77+
WriteResult(builder, paddingLength);
78+
79+
return builder.ToString();
80+
}
81+
82+
private int GetPaddingLength()
83+
{
84+
int maxLength = 0;
85+
86+
foreach (MeasureScope scope in _completedScopes)
87+
{
88+
int nextLength = scope.GetPaddingLength();
89+
maxLength = Math.Max(maxLength, nextLength);
90+
}
91+
92+
if (_activeScopeStack.Any())
93+
{
94+
MeasureScope scope = _activeScopeStack.Peek();
95+
int nextLength = scope.GetPaddingLength();
96+
maxLength = Math.Max(maxLength, nextLength);
97+
}
98+
99+
return maxLength + 3;
100+
}
101+
102+
private void WriteResult(StringBuilder builder, int paddingLength)
103+
{
104+
foreach (MeasureScope scope in _completedScopes)
105+
{
106+
scope.WriteResult(builder, 0, paddingLength);
107+
}
108+
109+
if (_activeScopeStack.Any())
110+
{
111+
MeasureScope scope = _activeScopeStack.Peek();
112+
scope.WriteResult(builder, 0, paddingLength);
113+
}
114+
}
115+
116+
public void Dispose()
117+
{
118+
if (_stopwatch.IsRunning)
119+
{
120+
_stopwatch.Stop();
121+
}
122+
123+
_completedScopes.Clear();
124+
_activeScopeStack.Clear();
125+
}
126+
127+
private sealed class MeasureScope : IDisposable
128+
{
129+
private readonly CascadingCodeTimer _owner;
130+
private readonly IList<MeasureScope> _children = new List<MeasureScope>();
131+
private readonly bool _excludeInRelativeCost;
132+
private readonly TimeSpan _startedAt;
133+
private TimeSpan? _stoppedAt;
134+
135+
public string Name { get; }
136+
137+
public MeasureScope(CascadingCodeTimer owner, string name, bool excludeInRelativeCost)
138+
{
139+
_owner = owner;
140+
_excludeInRelativeCost = excludeInRelativeCost;
141+
Name = name;
142+
143+
EnsureRunning();
144+
_startedAt = owner._stopwatch.Elapsed;
145+
}
146+
147+
private void EnsureRunning()
148+
{
149+
if (!_owner._stopwatch.IsRunning)
150+
{
151+
_owner._stopwatch.Start();
152+
}
153+
}
154+
155+
public MeasureScope SpawnChild(CascadingCodeTimer owner, string name, bool excludeInRelativeCost)
156+
{
157+
var childScope = new MeasureScope(owner, name, excludeInRelativeCost);
158+
_children.Add(childScope);
159+
return childScope;
160+
}
161+
162+
public int GetPaddingLength()
163+
{
164+
return GetPaddingLength(0);
165+
}
166+
167+
private int GetPaddingLength(int indent)
168+
{
169+
int selfLength = indent * 2 + Name.Length;
170+
int maxChildrenLength = 0;
171+
172+
foreach (MeasureScope child in _children)
173+
{
174+
int nextLength = child.GetPaddingLength(indent + 1);
175+
maxChildrenLength = Math.Max(nextLength, maxChildrenLength);
176+
}
177+
178+
return Math.Max(selfLength, maxChildrenLength);
179+
}
180+
181+
private TimeSpan GetElapsedInSelf()
182+
{
183+
return GetElapsedInTotal() - GetElapsedInChildren();
184+
}
185+
186+
private TimeSpan GetElapsedInTotal()
187+
{
188+
TimeSpan stoppedAt = _stoppedAt ?? _owner._stopwatch.Elapsed;
189+
return stoppedAt - _startedAt;
190+
}
191+
192+
private TimeSpan GetElapsedInChildren()
193+
{
194+
TimeSpan elapsedInChildren = TimeSpan.Zero;
195+
196+
foreach (MeasureScope childScope in _children)
197+
{
198+
elapsedInChildren += childScope.GetElapsedInTotal();
199+
}
200+
201+
return elapsedInChildren;
202+
}
203+
204+
private TimeSpan GetSkippedInTotal()
205+
{
206+
TimeSpan skippedInSelf = _excludeInRelativeCost ? GetElapsedInSelf() : TimeSpan.Zero;
207+
TimeSpan skippedInChildren = GetSkippedInChildren();
208+
209+
return skippedInSelf + skippedInChildren;
210+
}
211+
212+
private TimeSpan GetSkippedInChildren()
213+
{
214+
TimeSpan skippedInChildren = TimeSpan.Zero;
215+
216+
foreach (MeasureScope childScope in _children)
217+
{
218+
skippedInChildren += childScope.GetSkippedInTotal();
219+
}
220+
221+
return skippedInChildren;
222+
}
223+
224+
public void WriteResult(StringBuilder builder, int indent, int paddingLength)
225+
{
226+
TimeSpan timeElapsedGlobal = GetElapsedInTotal() - GetSkippedInTotal();
227+
WriteResult(builder, indent, timeElapsedGlobal, paddingLength);
228+
}
229+
230+
private void WriteResult(StringBuilder builder, int indent, TimeSpan timeElapsedGlobal, int paddingLength)
231+
{
232+
TimeSpan timeElapsedInSelf = GetElapsedInSelf();
233+
double scaleElapsedInSelf = timeElapsedGlobal != TimeSpan.Zero ? timeElapsedInSelf / timeElapsedGlobal : 0;
234+
235+
WriteIndent(builder, indent);
236+
builder.Append(Name);
237+
WritePadding(builder, indent, paddingLength);
238+
builder.AppendFormat(CultureInfo.InvariantCulture, "{0,19:G}", timeElapsedInSelf);
239+
240+
if (!_excludeInRelativeCost)
241+
{
242+
builder.Append(" ... ");
243+
builder.AppendFormat(CultureInfo.InvariantCulture, "{0,7:#0.00%}", scaleElapsedInSelf);
244+
}
245+
246+
if (_stoppedAt == null)
247+
{
248+
builder.Append(" (active)");
249+
}
250+
251+
builder.AppendLine();
252+
253+
foreach (MeasureScope child in _children)
254+
{
255+
child.WriteResult(builder, indent + 1, timeElapsedGlobal, paddingLength);
256+
}
257+
}
258+
259+
private static void WriteIndent(StringBuilder builder, int indent)
260+
{
261+
builder.Append(new string(' ', indent * 2));
262+
}
263+
264+
private void WritePadding(StringBuilder builder, int indent, int paddingLength)
265+
{
266+
string padding = new('.', paddingLength - Name.Length - indent * 2);
267+
builder.Append(' ');
268+
builder.Append(padding);
269+
builder.Append(' ');
270+
}
271+
272+
public void Dispose()
273+
{
274+
if (_stoppedAt == null)
275+
{
276+
_stoppedAt = _owner._stopwatch.Elapsed;
277+
_owner.Close(this);
278+
}
279+
}
280+
}
281+
}
282+
}

0 commit comments

Comments
 (0)