Skip to content

Commit e6ca829

Browse files
authored
Add IHttpSysRequestTimingFeature to support accessing http.sys timing info (#48218)
1 parent 23f18e2 commit e6ca829

File tree

8 files changed

+344
-1
lines changed

8 files changed

+344
-1
lines changed
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Server.HttpSys;
5+
6+
/// <summary>
7+
/// Defines the types of request processing timestamps exposed via the Http.Sys HTTP_REQUEST_TIMING_INFO extensibility point.
8+
/// </summary>
9+
/// <remarks>
10+
/// Use <see cref="IHttpSysRequestTimingFeature"/> to access these timestamps.
11+
/// </remarks>
12+
public enum HttpSysRequestTimingType
13+
{
14+
// IMPORTANT: Order matters and should match the order defined in HTTP_REQUEST_TIMING_TYPE
15+
16+
/// <summary>
17+
/// Time the connection started.
18+
/// </summary>
19+
ConnectionStart,
20+
21+
/// <summary>
22+
/// Time the first HTTP byte is received.
23+
/// </summary>
24+
DataStart,
25+
26+
/// <summary>
27+
/// Time TLS certificate loading starts.
28+
/// </summary>
29+
TlsCertificateLoadStart,
30+
31+
/// <summary>
32+
/// Time TLS certificate loading ends.
33+
/// </summary>
34+
TlsCertificateLoadEnd,
35+
36+
/// <summary>
37+
/// Time TLS leg one handshake starts.
38+
/// </summary>
39+
TlsHandshakeLeg1Start,
40+
41+
/// <summary>
42+
/// Time TLS leg one handshake ends.
43+
/// </summary>
44+
TlsHandshakeLeg1End,
45+
46+
/// <summary>
47+
/// Time TLS leg two handshake starts.
48+
/// </summary>
49+
TlsHandshakeLeg2Start,
50+
51+
/// <summary>
52+
/// Time TLS leg two handshake ends.
53+
/// </summary>
54+
TlsHandshakeLeg2End,
55+
56+
/// <summary>
57+
/// Time TLS attribute query starts.
58+
/// </summary>
59+
TlsAttributesQueryStart,
60+
61+
/// <summary>
62+
/// Time TLS attribute query ends.
63+
/// </summary>
64+
TlsAttributesQueryEnd,
65+
66+
/// <summary>
67+
/// Time TLS client cert query starts.
68+
/// </summary>
69+
TlsClientCertQueryStart,
70+
71+
/// <summary>
72+
/// Time TLS client cert query ends.
73+
/// </summary>
74+
TlsClientCertQueryEnd,
75+
76+
/// <summary>
77+
/// Time HTTP2 streaming starts.
78+
/// </summary>
79+
Http2StreamStart,
80+
81+
/// <summary>
82+
/// Time HTTP2 header decoding starts.
83+
/// </summary>
84+
Http2HeaderDecodeStart,
85+
86+
/// <summary>
87+
/// Time HTTP2 header decoding ends.
88+
/// </summary>
89+
Http2HeaderDecodeEnd,
90+
91+
/// <summary>
92+
/// Time HTTP header parsing starts.
93+
/// </summary>
94+
/// <remarks>
95+
/// For most requests, this is a good timestamp to use as a per request starting timestamp.
96+
/// </remarks>
97+
RequestHeaderParseStart,
98+
99+
/// <summary>
100+
/// Time HTTP header parsing ends.
101+
/// </summary>
102+
RequestHeaderParseEnd,
103+
104+
/// <summary>
105+
/// Time Http.Sys starts to determine which request queue to route the request to.
106+
/// </summary>
107+
RequestRoutingStart,
108+
109+
/// <summary>
110+
/// Time Http.Sys has determined which request queue to route the request to.
111+
/// </summary>
112+
RequestRoutingEnd,
113+
114+
/// <summary>
115+
/// Time the request is queued for inspection.
116+
/// </summary>
117+
RequestQueuedForInspection,
118+
119+
/// <summary>
120+
/// Time the request is delivered for inspection.
121+
/// </summary>
122+
RequestDeliveredForInspection,
123+
124+
/// <summary>
125+
/// Time the request has finished being inspected.
126+
/// </summary>
127+
RequestReturnedAfterInspection,
128+
129+
/// <summary>
130+
/// Time the request is queued for delegation.
131+
/// </summary>
132+
RequestQueuedForDelegation,
133+
134+
/// <summary>
135+
/// Time the request is delivered for delegation.
136+
/// </summary>
137+
RequestDeliveredForDelegation,
138+
139+
/// <summary>
140+
/// Time the request was delegated.
141+
/// </summary>
142+
RequestReturnedAfterDelegation,
143+
144+
/// <summary>
145+
/// Time the request was queued to the final request queue for processing.
146+
/// </summary>
147+
RequestQueuedForIO,
148+
149+
/// <summary>
150+
/// Time the request was delivered to the final request queue for processing.
151+
/// </summary>
152+
RequestDeliveredForIO,
153+
154+
/// <summary>
155+
/// Time HTTP3 streaming starts.
156+
/// </summary>
157+
Http3StreamStart,
158+
159+
/// <summary>
160+
/// Time HTTP3 header decoding starts.
161+
/// </summary>
162+
Http3HeaderDecodeStart,
163+
164+
/// <summary>
165+
/// Time HTTP3 header decoding ends.
166+
/// </summary>
167+
Http3HeaderDecodeEnd,
168+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Server.HttpSys;
5+
6+
/// <summary>
7+
/// This exposes the Http.Sys HTTP_REQUEST_TIMING_INFO extensibility point which contains request processing timestamp data from Http.Sys.
8+
/// </summary>
9+
public interface IHttpSysRequestTimingFeature
10+
{
11+
/// <summary>
12+
/// Gets all Http.Sys timing timestamps
13+
/// </summary>
14+
/// <remarks>
15+
/// These timestamps were obtained using QueryPerformanceCounter <see href="https://learn.microsoft.com/en-us/windows/win32/api/profileapi/nf-profileapi-queryperformancecounter"/> and the timestamp frequency can be obtained via QueryPerformanceFrequency <see href="https://learn.microsoft.com/en-us/windows/win32/api/profileapi/nf-profileapi-queryperformancefrequency"/>.
16+
/// The index of the timing can be cast to <see cref="HttpSysRequestTimingType"/> to know what the timing represents.
17+
/// The value may be 0 if the timing is not available for the current request.
18+
/// </remarks>
19+
ReadOnlySpan<long> Timestamps { get; }
20+
21+
/// <summary>
22+
/// Gets the timestamp for the given timing.
23+
/// </summary>
24+
/// <remarks>
25+
/// These timestamps were obtained using QueryPerformanceCounter <see href="https://learn.microsoft.com/windows/win32/api/profileapi/nf-profileapi-queryperformancecounter"/> and the timestamp frequency can be obtained via QueryPerformanceFrequency <see href="https://learn.microsoft.com/windows/win32/api/profileapi/nf-profileapi-queryperformancefrequency"/>.
26+
/// </remarks>
27+
/// <param name="timestampType">The timestamp type to get.</param>
28+
/// <param name="timestamp">The value of the timestamp if set.</param>
29+
/// <returns>True if the given timing was set (i.e., non-zero).</returns>
30+
bool TryGetTimestamp(HttpSysRequestTimingType timestampType, out long timestamp);
31+
32+
/// <summary>
33+
/// Gets the elapsed time between the two given timings.
34+
/// </summary>
35+
/// <param name="startingTimestampType">The timestamp type marking the beginning of the time period.</param>
36+
/// <param name="endingTimestampType">The timestamp type marking the end of the time period.</param>
37+
/// <param name="elapsed">A <see cref="TimeSpan"/> for the elapsed time between the starting and ending timestamps.</param>
38+
/// <returns>True if both given timings were set (i.e., non-zero).</returns>
39+
bool TryGetElapsedTime(HttpSysRequestTimingType startingTimestampType, HttpSysRequestTimingType endingTimestampType, out TimeSpan elapsed);
40+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,36 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
3+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.ConnectionStart = 0 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
4+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.DataStart = 1 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
5+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.Http2HeaderDecodeEnd = 14 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
6+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.Http2HeaderDecodeStart = 13 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
7+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.Http2StreamStart = 12 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
8+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.Http3HeaderDecodeEnd = 29 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
9+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.Http3HeaderDecodeStart = 28 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
10+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.Http3StreamStart = 27 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
11+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.RequestDeliveredForDelegation = 23 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
12+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.RequestDeliveredForInspection = 20 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
13+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.RequestDeliveredForIO = 26 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
14+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.RequestHeaderParseEnd = 16 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
15+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.RequestHeaderParseStart = 15 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
16+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.RequestQueuedForDelegation = 22 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
17+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.RequestQueuedForInspection = 19 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
18+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.RequestQueuedForIO = 25 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
19+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.RequestReturnedAfterDelegation = 24 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
20+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.RequestReturnedAfterInspection = 21 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
21+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.RequestRoutingEnd = 18 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
22+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.RequestRoutingStart = 17 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
23+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.TlsAttributesQueryEnd = 9 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
24+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.TlsAttributesQueryStart = 8 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
25+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.TlsCertificateLoadEnd = 3 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
26+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.TlsCertificateLoadStart = 2 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
27+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.TlsClientCertQueryEnd = 11 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
28+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.TlsClientCertQueryStart = 10 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
29+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.TlsHandshakeLeg1End = 5 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
30+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.TlsHandshakeLeg1Start = 4 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
31+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.TlsHandshakeLeg2End = 7 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
32+
Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType.TlsHandshakeLeg2Start = 6 -> Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType
33+
Microsoft.AspNetCore.Server.HttpSys.IHttpSysRequestTimingFeature
34+
Microsoft.AspNetCore.Server.HttpSys.IHttpSysRequestTimingFeature.Timestamps.get -> System.ReadOnlySpan<long>
35+
Microsoft.AspNetCore.Server.HttpSys.IHttpSysRequestTimingFeature.TryGetElapsedTime(Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType startingTimestampType, Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType endingTimestampType, out System.TimeSpan elapsed) -> bool
36+
Microsoft.AspNetCore.Server.HttpSys.IHttpSysRequestTimingFeature.TryGetTimestamp(Microsoft.AspNetCore.Server.HttpSys.HttpSysRequestTimingType timestampType, out long timestamp) -> bool

src/Servers/HttpSys/src/RequestProcessing/Request.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
using System.Globalization;
55
using System.Net;
6+
using System.Runtime.CompilerServices;
7+
using System.Runtime.InteropServices;
68
using System.Security;
79
using System.Security.Authentication;
810
using System.Security.Cryptography;
@@ -351,6 +353,37 @@ public IReadOnlyDictionary<int, ReadOnlyMemory<byte>> RequestInfo
351353
}
352354
}
353355

356+
public ReadOnlySpan<long> RequestTimestamps
357+
{
358+
get
359+
{
360+
/*
361+
Below is the definition of the timing info structure we are accessing the memory for.
362+
ULONG is 32-bit and maps to int. ULONGLONG is 64-bit and maps to long.
363+
364+
typedef struct _HTTP_REQUEST_TIMING_INFO
365+
{
366+
ULONG RequestTimingCount;
367+
ULONGLONG RequestTiming[HttpRequestTimingTypeMax];
368+
369+
} HTTP_REQUEST_TIMING_INFO, *PHTTP_REQUEST_TIMING_INFO;
370+
*/
371+
372+
if (!RequestInfo.TryGetValue((int)HttpApiTypes.HTTP_REQUEST_INFO_TYPE.HttpRequestInfoTypeRequestTiming, out var timingInfo))
373+
{
374+
return ReadOnlySpan<long>.Empty;
375+
}
376+
377+
var timingCount = MemoryMarshal.Read<int>(timingInfo.Span);
378+
379+
// Note that even though RequestTimingCount is an int, the compiler enforces alignment of data in the struct which causes 4 bytes
380+
// of padding to be added after RequestTimingCount, so we need to skip 64-bits before we get to the start of the RequestTiming array
381+
return MemoryMarshal.CreateReadOnlySpan(
382+
ref Unsafe.As<byte, long>(ref MemoryMarshal.GetReference(timingInfo.Span.Slice(sizeof(long)))),
383+
timingCount);
384+
}
385+
}
386+
354387
private void GetTlsHandshakeResults()
355388
{
356389
var handshake = RequestContext.GetTlsHandshake();

src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ internal partial class RequestContext :
3232
IHttpMaxRequestBodySizeFeature,
3333
IHttpBodyControlFeature,
3434
IHttpSysRequestInfoFeature,
35+
IHttpSysRequestTimingFeature,
3536
IHttpResponseTrailersFeature,
3637
IHttpResetFeature,
3738
IHttpSysRequestDelegationFeature,
@@ -588,6 +589,8 @@ bool IHttpBodyControlFeature.AllowSynchronousIO
588589

589590
IReadOnlyDictionary<int, ReadOnlyMemory<byte>> IHttpSysRequestInfoFeature.RequestInfo => Request.RequestInfo;
590591

592+
ReadOnlySpan<long> IHttpSysRequestTimingFeature.Timestamps => Request.RequestTimestamps;
593+
591594
IHeaderDictionary IHttpResponseTrailersFeature.Trailers
592595
{
593596
get => _responseTrailers ??= Response.Trailers;
@@ -598,6 +601,32 @@ IHeaderDictionary IHttpResponseTrailersFeature.Trailers
598601

599602
CancellationToken IConnectionLifetimeNotificationFeature.ConnectionClosedRequested { get; set; }
600603

604+
bool IHttpSysRequestTimingFeature.TryGetTimestamp(HttpSysRequestTimingType timestampType, out long timestamp)
605+
{
606+
int index = (int)timestampType;
607+
if (index < Request.RequestTimestamps.Length && Request.RequestTimestamps[index] > 0)
608+
{
609+
timestamp = Request.RequestTimestamps[index];
610+
return true;
611+
}
612+
613+
timestamp = default;
614+
return false;
615+
}
616+
617+
bool IHttpSysRequestTimingFeature.TryGetElapsedTime(HttpSysRequestTimingType startingTimestampType, HttpSysRequestTimingType endingTimestampType, out TimeSpan elapsed)
618+
{
619+
var timingFeature = (IHttpSysRequestTimingFeature)this;
620+
if (timingFeature.TryGetTimestamp(startingTimestampType, out long startTimestamp) && timingFeature.TryGetTimestamp(endingTimestampType, out long endTimestamp))
621+
{
622+
elapsed = Stopwatch.GetElapsedTime(startTimestamp, endTimestamp);
623+
return true;
624+
}
625+
626+
elapsed = default;
627+
return false;
628+
}
629+
601630
internal async Task OnResponseStart()
602631
{
603632
if (_responseStarted)

src/Servers/HttpSys/src/StandardFeatureCollection.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ internal sealed class StandardFeatureCollection : IFeatureCollection
2626
{ typeof(IHttpMaxRequestBodySizeFeature), _identityFunc },
2727
{ typeof(IHttpBodyControlFeature), _identityFunc },
2828
{ typeof(IHttpSysRequestInfoFeature), _identityFunc },
29+
{ typeof(IHttpSysRequestTimingFeature), _identityFunc },
2930
{ typeof(IHttpResponseTrailersFeature), ctx => ctx.GetResponseTrailersFeature() },
3031
{ typeof(IHttpResetFeature), ctx => ctx.GetResetFeature() },
3132
{ typeof(IConnectionLifetimeNotificationFeature), ctx => ctx.GetConnectionLifetimeNotificationFeature() },

src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System;
5+
using System.Diagnostics;
56
using System.IO;
67
using System.Net.Http;
78
using System.Runtime.InteropServices;
@@ -227,6 +228,36 @@ public async Task Https_ITlsHandshakeFeature_MatchesIHttpSysExtensionInfoFeature
227228
}
228229
}
229230

231+
[ConditionalFact]
232+
[MinimumOSVersion(OperatingSystems.Windows, WindowsVersions.Win10_20H2)]
233+
public async Task Https_SetsIHttpSysRequestTimingFeature()
234+
{
235+
using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext =>
236+
{
237+
try
238+
{
239+
var requestTimingFeature = httpContext.Features.Get<IHttpSysRequestTimingFeature>();
240+
Assert.NotNull(requestTimingFeature);
241+
Assert.True(requestTimingFeature.Timestamps.Length > (int)HttpSysRequestTimingType.Http3HeaderDecodeEnd);
242+
Assert.True(requestTimingFeature.TryGetTimestamp(HttpSysRequestTimingType.RequestHeaderParseStart, out var headerStart));
243+
Assert.True(requestTimingFeature.TryGetTimestamp(HttpSysRequestTimingType.RequestHeaderParseEnd, out var headerEnd));
244+
Assert.True(requestTimingFeature.TryGetElapsedTime(HttpSysRequestTimingType.RequestHeaderParseStart, HttpSysRequestTimingType.RequestHeaderParseEnd, out var elapsed));
245+
Assert.Equal(Stopwatch.GetElapsedTime(headerStart, headerEnd), elapsed);
246+
Assert.False(requestTimingFeature.TryGetTimestamp(HttpSysRequestTimingType.Http3StreamStart, out var streamStart));
247+
Assert.False(requestTimingFeature.TryGetTimestamp((HttpSysRequestTimingType)int.MaxValue, out var invalid));
248+
Assert.False(requestTimingFeature.TryGetElapsedTime(HttpSysRequestTimingType.Http3StreamStart, HttpSysRequestTimingType.RequestHeaderParseStart, out elapsed));
249+
}
250+
catch (Exception ex)
251+
{
252+
await httpContext.Response.WriteAsync(ex.ToString());
253+
}
254+
}))
255+
{
256+
string response = await SendRequestAsync(address);
257+
Assert.Equal(string.Empty, response);
258+
}
259+
}
260+
230261
private async Task<string> SendRequestAsync(string uri,
231262
X509Certificate cert = null)
232263
{

0 commit comments

Comments
 (0)