Cache file info access in viewengine

Move compilation and VirtualPathViewFactory to be singletons
And cache access to files.

The cache time is controlled by MVC options.
The cache is implemented in the ExpiringFileInfoCache.cs
This commit is contained in:
Yishai Galatzer 2014-07-14 09:32:37 -07:00
parent 87c430ae19
commit 472e500864
11 changed files with 628 additions and 13 deletions

View File

@ -0,0 +1,82 @@
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Concurrent;
using Microsoft.AspNet.FileSystems;
using Microsoft.Framework.OptionsModel;
using Microsoft.Framework.Runtime;
namespace Microsoft.AspNet.Mvc.Core
{
/// <summary>
/// A default implementation for the <see cref="IFileInfoCache" interface./>
/// </summary>
public class ExpiringFileInfoCache : IFileInfoCache
{
private readonly ConcurrentDictionary<string, ExpiringFileInfo> _fileInfoCache =
new ConcurrentDictionary<string, ExpiringFileInfo>(StringComparer.Ordinal);
private readonly PhysicalFileSystem _fileSystem;
private readonly TimeSpan _offset;
protected virtual IFileSystem FileSystem
{
get
{
return _fileSystem;
}
}
protected virtual DateTime UtcNow
{
get
{
return DateTime.UtcNow;
}
}
public ExpiringFileInfoCache(IApplicationEnvironment env,
IOptionsAccessor<MvcOptions> optionsAccessor)
{
// TODO: Inject the IFileSystem but only when we get it from the host
_fileSystem = new PhysicalFileSystem(env.ApplicationBasePath);
_offset = optionsAccessor.Options.ViewEngineOptions.ExpirationBeforeCheckingFilesOnDisk;
}
/// <inheritdoc />
public IFileInfo GetFileInfo([NotNull] string virtualPath)
{
IFileInfo fileInfo;
ExpiringFileInfo expiringFileInfo;
var utcNow = UtcNow;
if (_fileInfoCache.TryGetValue(virtualPath, out expiringFileInfo)
&& expiringFileInfo.ValidUntil > utcNow)
{
fileInfo = expiringFileInfo.FileInfo;
}
else
{
FileSystem.TryGetFileInfo(virtualPath, out fileInfo);
expiringFileInfo = new ExpiringFileInfo()
{
FileInfo = fileInfo,
ValidUntil = _offset == TimeSpan.MaxValue ? DateTime.MaxValue : utcNow.Add(_offset),
};
_fileInfoCache.AddOrUpdate(virtualPath, expiringFileInfo, (a, b) => expiringFileInfo);
}
return fileInfo;
}
private class ExpiringFileInfo
{
public IFileInfo FileInfo { get; set; }
public DateTime ValidUntil { get; set; }
}
}
}

View File

@ -0,0 +1,20 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNet.FileSystems;
namespace Microsoft.AspNet.Mvc.Core
{
/// <summary>
/// Provides cached access to file infos.
/// </summary>
public interface IFileInfoCache
{
/// <summary>
/// Returns a cached <see cref="IFileInfo" /> for a given path.
/// </summary>
/// <param name="virtualPath">The virtual path.</param>
/// <returns></returns>
IFileInfo GetFileInfo(string virtualPath);
}
}

View File

@ -27,7 +27,9 @@
<Compile Include="ActionDescriptor.cs" />
<Compile Include="ActionDescriptorProviderContext.cs" />
<Compile Include="ActionDescriptorsCollection.cs" />
<Compile Include="ExpiringFileInfoCache.cs" />
<Compile Include="Extensions\ViewEngineDescriptorExtensions.cs" />
<Compile Include="IExpiringFileInfoCache.cs" />
<Compile Include="ReflectedActionDescriptor.cs" />
<Compile Include="ReflectedActionDescriptorProvider.cs" />
<Compile Include="ReflectedModelBuilder\IReflectedApplicationModelConvention.cs" />
@ -207,6 +209,7 @@
<Compile Include="Rendering\SelectListItem.cs" />
<Compile Include="Rendering\UnobtrusiveValidationAttributesGenerator.cs" />
<Compile Include="Rendering\ViewEngineDescriptor.cs" />
<Compile Include="Rendering\RazorViewEngineOptions.cs" />
<Compile Include="Rendering\ViewEngineResult.cs" />
<Compile Include="RouteAttribute.cs" />
<Compile Include="RouteConstraintAttribute.cs" />

View File

@ -10,9 +10,13 @@ using Microsoft.AspNet.Mvc.Rendering;
namespace Microsoft.AspNet.Mvc
{
/// <summary>
/// Provides programmatic configuration for the MVC framework.
/// </summary>
public class MvcOptions
{
private AntiForgeryOptions _antiForgeryOptions = new AntiForgeryOptions();
private RazorViewEngineOptions _viewEngineOptions = new RazorViewEngineOptions();
public MvcOptions()
{
@ -21,6 +25,9 @@ namespace Microsoft.AspNet.Mvc
ViewEngines = new List<ViewEngineDescriptor>();
}
/// <summary>
/// Provides programmatic configuration for the anti-forgery token system.
/// </summary>
public AntiForgeryOptions AntiForgeryOptions
{
get
@ -41,6 +48,32 @@ namespace Microsoft.AspNet.Mvc
}
}
/// <summary>
/// Provides programmatic configuration for the default <see cref="IViewEngine" />.
/// </summary>
public RazorViewEngineOptions ViewEngineOptions
{
get
{
return _viewEngineOptions;
}
set
{
if (value == null)
{
throw new ArgumentNullException("value",
Resources.FormatPropertyOfTypeCannotBeNull("ViewEngineOptions",
typeof(MvcOptions)));
}
_viewEngineOptions = value;
}
}
/// <summary>
/// Get a list of the <see cref="ModelBinderDescriptor" /> used by the <see cref="CompositeModelBinder" />.
/// </summary>
public List<ModelBinderDescriptor> ModelBinders { get; private set; }
/// <summary>

View File

@ -0,0 +1,42 @@
// Copyright (c) Microsoft Open Technologies, Inc. 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.Mvc.Core
{
/// <summary>
/// Provides programmatic configuration for the default <see cref="Microsoft.AspNet.Mvc.Rendering.IViewEngine"/>.
/// </summary>
public class RazorViewEngineOptions
{
private TimeSpan _expirationBeforeCheckingFilesOnDisk = TimeSpan.FromSeconds(2);
/// <summary>
/// Controls the <see cref="ExpiringFileInfoCache" /> caching behavior.
/// </summary>
/// <remarks>
/// <see cref="TimeSpan"/> of <see cref="TimeSpan.Zero"/> or less, means no caching.
/// <see cref="TimeSpan"/> of <see cref="TimeSpan.MaxValue"/> means indefinite caching.
/// </remarks>
public TimeSpan ExpirationBeforeCheckingFilesOnDisk
{
get
{
return _expirationBeforeCheckingFilesOnDisk;
}
set
{
if (value.TotalMilliseconds < 0)
{
_expirationBeforeCheckingFilesOnDisk = TimeSpan.Zero;
}
else
{
_expirationBeforeCheckingFilesOnDisk = value;
}
}
}
}
}

View File

@ -4,6 +4,7 @@
"warningsAsErrors": true
},
"dependencies": {
"Microsoft.AspNet.FileSystems": "1.0.0-*",
"Microsoft.AspNet.Http": "1.0.0-*",
"Microsoft.AspNet.Mvc.Common": "",
"Microsoft.AspNet.Mvc.ModelBinding": "",

View File

@ -13,7 +13,8 @@ namespace Microsoft.AspNet.Mvc.Razor
{
public class RazorCompilationService : IRazorCompilationService
{
private static readonly CompilerCache _cache = new CompilerCache();
// This class must be registered as a singleton service for the caching to work.
private readonly CompilerCache _cache = new CompilerCache();
private readonly IApplicationEnvironment _environment;
private readonly ICompilationService _baseCompilationService;
private readonly IMvcRazorHost _razorHost;

View File

@ -2,36 +2,35 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNet.FileSystems;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Runtime;
namespace Microsoft.AspNet.Mvc.Razor
{
public class VirtualPathViewFactory : IVirtualPathViewFactory
{
private readonly PhysicalFileSystem _fileSystem;
private readonly IRazorCompilationService _compilationService;
private readonly ITypeActivator _activator;
private readonly IServiceProvider _serviceProvider;
private readonly IFileInfoCache _fileInfoCache;
public VirtualPathViewFactory(IApplicationEnvironment env,
IRazorCompilationService compilationService,
public VirtualPathViewFactory(IRazorCompilationService compilationService,
ITypeActivator typeActivator,
IServiceProvider serviceProvider)
IServiceProvider serviceProvider,
IFileInfoCache fileInfoCache)
{
// TODO: Continue to inject the IFileSystem but only when we get it from the host
_fileSystem = new PhysicalFileSystem(env.ApplicationBasePath);
_compilationService = compilationService;
_activator = typeActivator;
_serviceProvider = serviceProvider;
_fileInfoCache = fileInfoCache;
}
public IView CreateInstance([NotNull] string virtualPath)
{
IFileInfo fileInfo;
if (_fileSystem.TryGetFileInfo(virtualPath, out fileInfo))
var fileInfo = _fileInfoCache.GetFileInfo(virtualPath);
if (fileInfo != null)
{
var result = _compilationService.Compile(fileInfo);
return (IView)_activator.CreateInstance(_serviceProvider, result.CompiledType);

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNet.Mvc.Core;
using Microsoft.AspNet.Mvc.Filters;
using Microsoft.AspNet.Mvc.ModelBinding;
using Microsoft.AspNet.Mvc.Razor;
@ -41,9 +42,12 @@ namespace Microsoft.AspNet.Mvc
yield return describe.Singleton<IViewEngineProvider, DefaultViewEngineProvider>();
yield return describe.Scoped<ICompositeViewEngine, CompositeViewEngine>();
yield return describe.Transient<IRazorCompilationService, RazorCompilationService>();
yield return describe.Transient<IVirtualPathViewFactory, VirtualPathViewFactory>();
yield return describe.Singleton<IRazorCompilationService, RazorCompilationService>();
yield return describe.Singleton<IRazorViewActivator, RazorViewActivator>();
// Virtual path view factory needs to stay scoped so views can get get scoped services.
yield return describe.Scoped<IVirtualPathViewFactory, VirtualPathViewFactory>();
yield return describe.Singleton<IFileInfoCache, ExpiringFileInfoCache>();
yield return describe.Transient<INestedProvider<ActionDescriptorProviderContext>,
ReflectedActionDescriptorProvider>();

View File

@ -0,0 +1,429 @@
// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Threading;
using Microsoft.AspNet.FileSystems;
using Microsoft.Framework.OptionsModel;
using Microsoft.Framework.Runtime;
using Moq;
using Xunit;
namespace Microsoft.AspNet.Mvc.Core.Test
{
public class ExpiringFileInfoCacheTest
{
private const string FileName = "myView.cshtml";
public IApplicationEnvironment ApplicationEnvironment
{
get
{
var mock = new Mock<IApplicationEnvironment>(MockBehavior.Strict);
mock.Setup(ae => ae.ApplicationBasePath).Returns(Directory.GetCurrentDirectory());
return mock.Object;
}
}
public MvcOptions Options
{
get
{
return new MvcOptions();
}
}
public IOptionsAccessor<MvcOptions> OptionsAccessor
{
get
{
var options = Options;
var mock = new Mock<IOptionsAccessor<MvcOptions>>(MockBehavior.Strict);
mock.Setup(oa => oa.Options).Returns(options);
return mock.Object;
}
}
public ControllableExpiringFileInfoCache GetCache(IOptionsAccessor<MvcOptions> optionsAccessor)
{
return new ControllableExpiringFileInfoCache(ApplicationEnvironment, optionsAccessor);
}
public void CreateFile(string FileName, ControllableExpiringFileInfoCache cache)
{
var fileInfo = new DummyFileInfo()
{
Name = FileName,
LastModified = DateTime.Now,
};
cache.UnderlyingFileSystem.AddFile(fileInfo);
}
public void Sleep(ControllableExpiringFileInfoCache cache, int offsetMilliseconds)
{
cache.Sleep(offsetMilliseconds);
}
public void Sleep(IOptionsAccessor<MvcOptions> accessor, ControllableExpiringFileInfoCache cache, int offsetMilliSeconds)
{
var baseMilliSeconds = (int)accessor.Options.ViewEngineOptions.ExpirationBeforeCheckingFilesOnDisk.TotalMilliseconds;
cache.Sleep(baseMilliSeconds + offsetMilliSeconds);
}
public void SetExpiration(IOptionsAccessor<MvcOptions> accessor, TimeSpan expiration)
{
accessor.Options.ViewEngineOptions.ExpirationBeforeCheckingFilesOnDisk = expiration;
}
[Fact]
public void VerifyDefaultOptionsAreSetupCorrectly()
{
var optionsAccessor = OptionsAccessor;
// Assert
Assert.Equal(2000, optionsAccessor.Options.ViewEngineOptions.ExpirationBeforeCheckingFilesOnDisk.TotalMilliseconds);
}
[Fact]
public void GettingFileInfoReturnsTheSameDataWithDefaultOptions()
{
// Arrange
var cache = GetCache(OptionsAccessor);
CreateFile(FileName, cache);
// Act
var fileInfo1 = cache.GetFileInfo(FileName);
var fileInfo2 = cache.GetFileInfo(FileName);
// Assert
Assert.Same(fileInfo1, fileInfo2);
Assert.Equal(FileName, fileInfo1.Name);
}
[Fact]
public void GettingFileInfoReturnsTheSameDataWithDefaultOptionsEvenWhenFilesHaveChanged()
{
// Arrange
var cache = GetCache(OptionsAccessor);
CreateFile(FileName, cache);
// Act
var fileInfo1 = cache.GetFileInfo(FileName);
CreateFile(FileName, cache);
var fileInfo2 = cache.GetFileInfo(FileName);
// Assert
Assert.Same(fileInfo1, fileInfo2);
Assert.Equal(fileInfo1.LastModified, fileInfo2.LastModified);
Assert.Equal(FileName, fileInfo1.Name);
Assert.Equal(FileName, fileInfo2.Name);
}
[Fact]
public void GettingFileInfoReturnsNewDataWithDefaultOptionsAfterExpirationAndFileChange()
{
var optionsAccessor = OptionsAccessor;
// Arrange
var cache = GetCache(optionsAccessor);
CreateFile(FileName, cache);
// Act
var fileInfo1 = cache.GetFileInfo(FileName);
Sleep(optionsAccessor, cache, 500);
CreateFile(FileName, cache);
var fileInfo2 = cache.GetFileInfo(FileName);
// Assert
Assert.NotSame(fileInfo1, fileInfo2);
Assert.Equal(FileName, fileInfo1.Name);
Assert.Equal(FileName, fileInfo2.Name);
}
[Fact]
public void GettingFileInfoReturnsNewDataWithDefaultOptionsAfterExpiration()
{
// Arrange
var optionsAccessor = OptionsAccessor;
var cache = GetCache(optionsAccessor);
CreateFile(FileName, cache);
// Act
var fileInfo1 = cache.GetFileInfo(FileName);
Sleep(optionsAccessor, cache, 500);
var fileInfo2 = cache.GetFileInfo(FileName);
// Assert
Assert.NotSame(fileInfo1, fileInfo2);
Assert.Equal(fileInfo1.LastModified, fileInfo2.LastModified);
Assert.Equal(FileName, fileInfo1.Name);
Assert.Equal(FileName, fileInfo2.Name);
}
public static IEnumerable<object[]> ImmediateExpirationTimespans
{
get
{
yield return new object[]
{
TimeSpan.FromSeconds(0.0)
};
yield return new object[]
{
TimeSpan.FromSeconds(-1.0)
};
yield return new object[]
{
TimeSpan.MinValue
};
}
}
[Theory]
[MemberData("ImmediateExpirationTimespans")]
public void GettingFileInfoReturnsNewDataWithCustomImmediateExpiration(TimeSpan expiration)
{
// Arrange
var optionsAccessor = OptionsAccessor;
SetExpiration(optionsAccessor, expiration);
string FileName = "myfile4.cshtml";
var cache = GetCache(optionsAccessor);
CreateFile(FileName, cache);
// Act
var fileInfo1 = cache.GetFileInfo(FileName);
var fileInfo2 = cache.GetFileInfo(FileName);
// Assert
Assert.NotSame(fileInfo1, fileInfo2);
Assert.Equal(fileInfo1.LastModified, fileInfo2.LastModified);
Assert.Equal(FileName, fileInfo1.Name);
Assert.Equal(FileName, fileInfo2.Name);
}
public static IEnumerable<object[]> CustomExpirationTimespans
{
get
{
yield return new object[]
{
TimeSpan.FromSeconds(1.0)
};
yield return new object[]
{
TimeSpan.FromSeconds(3.0)
};
}
}
[Theory]
[MemberData("CustomExpirationTimespans")]
public void GettingFileInfoReturnsNewDataWithCustomExpiration(TimeSpan expiration)
{
// Arrange
var optionsAccessor = OptionsAccessor;
SetExpiration(optionsAccessor, expiration);
string FileName = "myfile5.cshtml";
var cache = GetCache(optionsAccessor);
CreateFile(FileName, cache);
// Act
var fileInfo1 = cache.GetFileInfo(FileName);
Sleep(optionsAccessor, cache, 500);
var fileInfo2 = cache.GetFileInfo(FileName);
// Assert
Assert.NotSame(fileInfo1, fileInfo2);
Assert.Equal(FileName, fileInfo1.Name);
}
[Theory]
[MemberData("CustomExpirationTimespans")]
public void GettingFileInfoReturnsSameDataWithCustomExpiration(TimeSpan expiration)
{
// Arrange
var optionsAccessor = OptionsAccessor;
SetExpiration(optionsAccessor, expiration);
string FileName = "myfile6.cshtml";
var cache = GetCache(optionsAccessor);
CreateFile(FileName, cache);
// Act
var fileInfo1 = cache.GetFileInfo(FileName);
Sleep(optionsAccessor, cache, -500);
var fileInfo2 = cache.GetFileInfo(FileName);
// Assert
Assert.Same(fileInfo1, fileInfo2);
Assert.Equal(FileName, fileInfo1.Name);
}
[Fact]
public void GettingFileInfoReturnsSameDataWithMaxExpiration()
{
// Arrange
var optionsAccessor = OptionsAccessor;
SetExpiration(optionsAccessor, TimeSpan.MaxValue);
string FileName = "myfile7.cshtml";
var cache = GetCache(optionsAccessor);
CreateFile(FileName, cache);
// Act
var fileInfo1 = cache.GetFileInfo(FileName);
Sleep(cache, 2500);
var fileInfo2 = cache.GetFileInfo(FileName);
// Assert
Assert.Same(fileInfo1, fileInfo2);
Assert.Equal(FileName, fileInfo1.Name);
}
public class ControllableExpiringFileInfoCache : ExpiringFileInfoCache
{
public ControllableExpiringFileInfoCache(IApplicationEnvironment env,
IOptionsAccessor<MvcOptions> optionsAccessor)
: base(env, optionsAccessor)
{
}
private DateTime? _internalUtcNow { get; set; }
private DummyFileSystem _underlyingFileSystem = new DummyFileSystem();
protected override DateTime UtcNow
{
get
{
if (_internalUtcNow == null)
{
_internalUtcNow = base.UtcNow;
}
return _internalUtcNow.Value.AddTicks(1);
}
}
protected override IFileSystem FileSystem
{
get
{
return UnderlyingFileSystem;
}
}
public void Sleep(int milliSeconds)
{
if (milliSeconds <= 0)
{
throw new InvalidOperationException();
}
_internalUtcNow = UtcNow.AddMilliseconds(milliSeconds);
}
public DummyFileSystem UnderlyingFileSystem
{
get
{
return _underlyingFileSystem;
}
}
}
public class DummyFileSystem : IFileSystem
{
private Dictionary<string, IFileInfo> _fileInfos = new Dictionary<string, IFileInfo>(StringComparer.OrdinalIgnoreCase);
public void AddFile(IFileInfo fileInfo)
{
if (_fileInfos.ContainsKey(fileInfo.Name))
{
_fileInfos[fileInfo.Name] = fileInfo;
}
else
{
_fileInfos.Add(fileInfo.Name, fileInfo);
}
}
public bool TryGetDirectoryContents(string subpath, out IEnumerable<IFileInfo> contents)
{
throw new NotImplementedException();
}
public bool TryGetFileInfo(string subpath, out IFileInfo fileInfo)
{
IFileInfo knownInfo;
if (_fileInfos.TryGetValue(subpath, out knownInfo))
{
fileInfo = new DummyFileInfo()
{
Name = knownInfo.Name,
LastModified = knownInfo.LastModified,
};
return true;
}
else
{
fileInfo = null;
return false;
}
}
}
public class DummyFileInfo : IFileInfo
{
public DateTime LastModified { get; set; }
public string Name { get; set; }
public long Length { get { throw new NotImplementedException(); } }
public bool IsDirectory { get { throw new NotImplementedException(); } }
public string PhysicalPath { get { throw new NotImplementedException(); } }
public Stream CreateReadStream() { throw new NotImplementedException(); }
}
}
}

View File

@ -30,6 +30,7 @@
<Compile Include="ActionResults\RedirectResultTest.cs" />
<Compile Include="ActionSelectionConventionTests.cs" />
<Compile Include="AntiXsrf\AntiForgeryOptionsTests.cs" />
<Compile Include="ExpiringFileInfoCacheTest.cs" />
<Compile Include="Extensions\ViewEngineDscriptorExtensionsTest.cs" />
<Compile Include="ReflectedModelBuilder\ReflectedParameterModelTests.cs" />
<Compile Include="ReflectedModelBuilder\ReflectedActionModelTests.cs" />