Skip to content
This repository was archived by the owner on Dec 18, 2018. It is now read-only.

[DESIGN] Damianedwards/date header perf #167

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions KestrelHttpServer.sln
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
build.cmd = build.cmd
global.json = global.json
makefile.shade = makefile.shade
NuGet.Config = NuGet.Config
EndProjectSection
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "SampleApp", "samples\SampleApp\SampleApp.xproj", "{2C3CB3DC-EEBF-4F52-9E1C-4F2F972E76C3}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public ConnectionContext(ConnectionContext context) : base(context)
SocketInput = context.SocketInput;
SocketOutput = context.SocketOutput;
ConnectionControl = context.ConnectionControl;
DateHeaderValueManager = context.DateHeaderValueManager;
}

public SocketInput SocketInput { get; set; }
Expand Down
149 changes: 149 additions & 0 deletions src/Microsoft.AspNet.Server.Kestrel/Http/DateHeaderValueManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;
using System.Threading;
using Microsoft.AspNet.Server.Kestrel.Infrastructure;

namespace Microsoft.AspNet.Server.Kestrel.Http
{
/// <summary>
/// Manages the generation of the date header value.
/// </summary>
public class DateHeaderValueManager : IDisposable
{
private readonly ISystemClock _systemClock;
private readonly TimeSpan _timeWithoutRequestsUntilIdle;
private readonly TimeSpan _timerInterval;
private readonly uint _timerTicksWithoutRequestsUntilIdle;

private volatile string _dateValue;
private bool _isDisposed = false;
private bool _hadRequestsSinceLastTimerTick = false;
private Timer _dateValueTimer;
private object _timerLocker = new object();
private int _timerTicksSinceLastRequest;

/// <summary>
/// Initializes a new instance of the <see cref="DateHeaderValueManager"/> class.
/// </summary>
public DateHeaderValueManager()
: this(
systemClock: new SystemClock(),
timeWithoutRequestsUntilIdle: TimeSpan.FromSeconds(10),
timerInterval: TimeSpan.FromSeconds(1))
{

}

// Internal for testing
internal DateHeaderValueManager(
ISystemClock systemClock,
TimeSpan timeWithoutRequestsUntilIdle,
TimeSpan timerInterval)
{
_systemClock = systemClock;
_timeWithoutRequestsUntilIdle = timeWithoutRequestsUntilIdle;
_timerInterval = timerInterval;

// Calculate the number of timer ticks where no requests are seen before we're considered to be idle.
// Once we're idle, the timer is shutdown to prevent code from running while there are no requests.
// The timer is started again on the next request.
_timerTicksWithoutRequestsUntilIdle = (uint)(_timeWithoutRequestsUntilIdle.TotalMilliseconds / _timerInterval.TotalMilliseconds);
}

/// <summary>
/// Returns a value representing the current server date/time for use in the HTTP "Date" response header
/// in accordance with http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.18
/// </summary>
/// <returns>The value.</returns>
public string GetDateHeaderValue()
{
PumpTimer();

// See https://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.110).aspx#RFC1123 for info on the format
// string used here.
// The null-coalesce here is to protect against returning null after Dispose() is called, at which
// point _dateValue will be null forever after.
return _dateValue ?? _systemClock.UtcNow.ToString("r");
}

/// <summary>
/// Releases all resources used by the current instance of <see cref="DateHeaderValueManager"/>.
/// </summary>
public void Dispose()
{
lock (_timerLocker)
{
if (_dateValueTimer != null)
{
DisposeTimer();
}

_isDisposed = true;
}
}

private void PumpTimer()
{
_hadRequestsSinceLastTimerTick = true;

// If we're already disposed we don't care about starting the timer again. This avoids us having to worry
// about requests in flight during dispose (not that that should actually happen) as those will just get
// SystemClock.UtcNow (aka "the slow way").
if (!_isDisposed && _dateValueTimer == null)
{
lock (_timerLocker)
{
if (!_isDisposed && _dateValueTimer == null)
{
// Immediately assign the date value and start the timer again. We assign the value immediately
// here as the timer won't fire until the timer interval has passed and we want a value assigned
// inline now to serve requests that occur in the meantime.
_dateValue = _systemClock.UtcNow.ToString("r");
_dateValueTimer = new Timer(UpdateDateValue, state: null, dueTime: _timerInterval, period: _timerInterval);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not create the timer in the constructor and just do Change() here? Then, _dateValueTimer != null is an invariant and wouldn't need to be checked all the time.

Also: Why not use dueTime: TimeSpan.Zero to run the handler immediately, which sets the _dateValue immediately.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not create the timer in the constructor and just do Change() here? Then, _dateValueTimer != null is an invariant and wouldn't need to be checked all the time.

_dateValueTimer == null is the thing determining whether it needs to be switched on again. Could use boolean with _dateValueTimer.Change() but that may make the logic more complicated?

Why not use dueTime: TimeSpan.Zero to run the handler immediately

That would queue the callback to run immediately; but doesn't call it as part of the Timer constructor so the initialisation of the value would be in a race with the function return; which it would probably always loose. Though that would be fixed by the null coalescing in GetDateHeaderValue as it would create its own value.

However, not sure switching it off during low request periods vs creating a string every second with a single timer has much benefit as the timer isn't necessarily a real "individual" timer? http://referencesource.microsoft.com/#mscorlib/system/threading/timer.cs,29

We use a single native timer, supplied by the VM, to schedule all managed timers in the AppDomain.

So in the spirit of

Why not create the timer in the constructor

Just switch the timer on in constructor and leave it ticking away until shut down? The side effect would be the GC has to clean up a string every second when the server isn't doing anything; but its happy doing that when the server is fully loaded; and the logic becomes much simpler in UpdateDateValue and you can drop PumpTimer and the locks altogether?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is a different thought for the entire implementation that completely removes any need for synchronization:

http://docs.libuv.org/en/latest/timer.html

There would be one timer per Kestrel thread, but on the other hand the DateTime string wouldn't be shared between processors and every update is already in the "correct" processor's cache.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The libuv timers are implemented in more or less the same way on linux and win as the .net timer; but you'd pay the added overhead of interop'ing the callback and the code for it would be less clear.

As Kestrel uses multiple (maybe?) libuv threads for socket IO then as you say, this would update in different cpu caches. However, as its a write once per second, read many times the processor cache would only have to refresh once per second (or once every 100k? executions) so it wouldn't be that significant; and it would put extra book keeping on the socket IO threads.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was concern over having any code running when the process is idle as it may interfere with mechanisms that perform operations on or in relation to idle processes. This design allows us to run the timer only when there are requests.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Process suspension type things? If is a requirement LGTM

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that was one of the scenarios.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That requirement makes this solution rather elegant 👍

}
}
}
}

// Called by the Timer (background) thread
private void UpdateDateValue(object state)
{
// See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.18 for required format of Date header
_dateValue = _systemClock.UtcNow.ToString("r");

if (_hadRequestsSinceLastTimerTick)
{
// We served requests since the last tick, reset the flag and return as we're still active
_hadRequestsSinceLastTimerTick = false;
_timerTicksSinceLastRequest = 0;
return;
}

// No requests since the last timer tick, we need to check if we're beyond the idle threshold
_timerTicksSinceLastRequest++;
if (_timerTicksSinceLastRequest == _timerTicksWithoutRequestsUntilIdle)
{
// No requests since idle threshold so stop the timer if it's still running
if (_dateValueTimer != null)
{
lock (_timerLocker)
{
if (_dateValueTimer != null)
{
DisposeTimer();
}
}
}
}
}

private void DisposeTimer()
{
_dateValueTimer.Dispose();
_dateValueTimer = null;
_dateValue = null;
}
}
}
7 changes: 4 additions & 3 deletions src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public class Frame : FrameContext, IFrameControl
private bool _keepAlive;
private bool _autoChunk;
private readonly FrameRequestHeaders _requestHeaders = new FrameRequestHeaders();
private readonly FrameResponseHeaders _responseHeaders = new FrameResponseHeaders();
private readonly FrameResponseHeaders _responseHeaders;

private List<KeyValuePair<Func<object, Task>, object>> _onStarting;
private List<KeyValuePair<Func<object, Task>, object>> _onCompleted;
Expand All @@ -38,6 +38,7 @@ public Frame(ConnectionContext context) : base(context)
FrameControl = this;
StatusCode = 200;
RequestHeaders = _requestHeaders;
_responseHeaders = new FrameResponseHeaders(DateHeaderValueManager.GetDateHeaderValue());
ResponseHeaders = _responseHeaders;
}

Expand Down Expand Up @@ -388,8 +389,8 @@ public void ProduceEnd(Exception ex)
// the app func has failed. https://github.com/aspnet/KestrelHttpServer/issues/43
_onStarting = null;

ResponseHeaders = new FrameResponseHeaders();
ResponseHeaders["Content-Length"] = new[] { "0" };
ResponseHeaders = new FrameResponseHeaders(DateHeaderValueManager.GetDateHeaderValue());
ResponseHeaders["Content-Length"] = "0";
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
{
public partial class FrameResponseHeaders
{
public FrameResponseHeaders()
public FrameResponseHeaders(string dateHeaderValue)
{
_Server = "Kestrel";
_Date = DateTime.UtcNow.ToString("r");
_Date = dateHeaderValue;
_bits = 67108868L;
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.AspNet.Server.Kestrel/Http/FrameHeaders.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ StringValues IDictionary<string, StringValues>.this[string key]
get
{
return GetValueFast(key);
}
}

set
{
Expand Down
3 changes: 2 additions & 1 deletion src/Microsoft.AspNet.Server.Kestrel/Http/Listener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public abstract class Listener : ListenerContext, IDisposable
{
protected Listener(ServiceContext serviceContext) : base(serviceContext)
{

}

protected UvStreamHandle ListenSocket { get; private set; }
Expand Down Expand Up @@ -56,7 +57,7 @@ protected static void ConnectionCallback(UvStreamHandle stream, int status, Exce
if (error != null)
{
listener.Log.LogError("Listener.ConnectionCallback ", error);
}
}
else
{
listener.OnConnection(stream, status);
Expand Down
4 changes: 4 additions & 0 deletions src/Microsoft.AspNet.Server.Kestrel/Http/ListenerContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public ListenerContext() { }
public ListenerContext(ServiceContext serviceContext)
{
Memory = serviceContext.Memory;
DateHeaderValueManager = serviceContext.DateHeaderValueManager;
Log = serviceContext.Log;
}

Expand All @@ -22,6 +23,7 @@ public ListenerContext(ListenerContext listenerContext)
Thread = listenerContext.Thread;
Application = listenerContext.Application;
Memory = listenerContext.Memory;
DateHeaderValueManager = listenerContext.DateHeaderValueManager;
Log = listenerContext.Log;
}

Expand All @@ -31,6 +33,8 @@ public ListenerContext(ListenerContext listenerContext)

public IMemoryPool Memory { get; set; }

public DateHeaderValueManager DateHeaderValueManager { get; set; }

public IKestrelTrace Log { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public abstract class ListenerSecondary : ListenerContext, IDisposable
{
protected ListenerSecondary(ServiceContext serviceContext) : base(serviceContext)
{

}

UvPipeHandle DispatchPipe { get; set; }
Expand Down
18 changes: 18 additions & 0 deletions src/Microsoft.AspNet.Server.Kestrel/Infrastructure/ISystemClock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;

namespace Microsoft.AspNet.Server.Kestrel.Infrastructure
{
/// <summary>
/// Abstracts the system clock to facilitate testing.
/// </summary>
public interface ISystemClock
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

internal

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Closed this one, look at #220 instead

{
/// <summary>
/// Retrieves the current system time in UTC.
/// </summary>
DateTimeOffset UtcNow { get; }
}
}
24 changes: 24 additions & 0 deletions src/Microsoft.AspNet.Server.Kestrel/Infrastructure/SystemClock.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System;

namespace Microsoft.AspNet.Server.Kestrel.Infrastructure
{
/// <summary>
/// Provides access to the normal system clock.
/// </summary>
public class SystemClock : ISystemClock
{
/// <summary>
/// Retrieves the current system time in UTC.
/// </summary>
public DateTimeOffset UtcNow
{
get
{
return DateTimeOffset.UtcNow;
}
}
}
}
4 changes: 4 additions & 0 deletions src/Microsoft.AspNet.Server.Kestrel/KestrelEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,15 @@ private KestrelEngine(IApplicationShutdown appShutdownService, ILogger logger)
{
AppShutdown = appShutdownService,
Memory = new MemoryPool(),
DateHeaderValueManager = new DateHeaderValueManager(),
Log = new KestrelTrace(logger)
};

Threads = new List<KestrelThread>();
}

public Libuv Libuv { get; private set; }

public List<KestrelThread> Threads { get; private set; }

public void Start(int count)
Expand All @@ -104,6 +106,8 @@ public void Dispose()
thread.Stop(TimeSpan.FromSeconds(2.5));
}
Threads.Clear();

_serviceContext.DateHeaderValueManager.Dispose();
}

public IDisposable CreateServer(string scheme, string host, int port, Func<Frame, Task> application)
Expand Down
2 changes: 2 additions & 0 deletions src/Microsoft.AspNet.Server.Kestrel/ServiceContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public class ServiceContext

public IMemoryPool Memory { get; set; }

public DateHeaderValueManager DateHeaderValueManager { get; set; }

public IKestrelTrace Log { get; set; }
}
}
3 changes: 2 additions & 1 deletion src/Microsoft.AspNet.Server.Kestrel/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@
"System.Threading": "4.0.11-beta-*",
"System.Threading.Tasks": "4.0.11-beta-*",
"System.Threading.Thread": "4.0.0-beta-*",
"System.Threading.ThreadPool": "4.0.10-beta-*"
"System.Threading.ThreadPool": "4.0.10-beta-*",
"System.Threading.Timer": "4.0.0"
}
}
},
Expand Down
Loading