* Modifying RazorPrecompiler to use IMemoryCache to cache results when

precompilation is invoked via Design Time Host.
* Change compilation to occur in parallel
This commit is contained in:
Pranav K 2015-01-08 10:07:13 -08:00
parent eda4b16cc5
commit 12243e7d97
5 changed files with 183 additions and 58 deletions

View File

@ -0,0 +1,71 @@
// 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.Collections.Generic;
using Microsoft.CodeAnalysis;
namespace Microsoft.AspNet.Mvc.Razor
{
/// <summary>
/// An entry in the cache used by <see cref="RazorPreCompiler"/>.
/// </summary>
public class PrecompilationCacheEntry
{
/// <summary>
/// Initializes a new instance of <see cref="PrecompilationCacheEntry"/> for a successful parse.
/// </summary>
/// <param name="fileInfo">The <see cref="RazorFileInfo"/> of the file being cached.</param>
/// <param name="syntaxTree">The <see cref="CodeAnalysis.SyntaxTree"/> to cache.</param>
public PrecompilationCacheEntry([NotNull] RazorFileInfo fileInfo,
[NotNull] SyntaxTree syntaxTree)
{
FileInfo = fileInfo;
SyntaxTree = syntaxTree;
}
/// <summary>
/// Initializes a new instance of <see cref="PrecompilationCacheEntry"/> for a failed parse.
/// </summary>
/// <param name="diagnostics">The <see cref="IReadOnlyList{Diagnostic}"/> produced from parsing the Razor
/// file. This does not contain <see cref="Diagnostic"/>s produced from compiling the parsed
/// <see cref="CodeAnalysis.SyntaxTree"/>.</param>
public PrecompilationCacheEntry([NotNull] IReadOnlyList<Diagnostic> diagnostics)
{
Diagnostics = diagnostics;
}
/// <summary>
/// Gets the <see cref="RazorFileInfo"/> associated with this cache entry instance.
/// </summary>
/// <remarks>
/// This property is not <c>null</c> if <see cref="Success"/> is <c>true</c>.
/// </remarks>
public RazorFileInfo FileInfo { get; }
/// <summary>
/// Gets the <see cref="SyntaxTree"/> produced from parsing the Razor file.
/// </summary>
/// <remarks>
/// This property is not <c>null</c> if <see cref="Success"/> is <c>true</c>.
/// </remarks>
public SyntaxTree SyntaxTree { get; }
/// <summary>
/// Gets the <see cref="Diagnostic"/>s produced from parsing the generated contents of the file
/// specified by <see cref="FileInfo"/>. This does not contain <see cref="Diagnostic"/>s produced from
/// compiling the parsed <see cref="CodeAnalysis.SyntaxTree"/>.
/// </summary>
/// <remarks>
/// This property is <c>null</c> if <see cref="Success"/> is <c>true</c>.
/// </remarks>
public IReadOnlyList<Diagnostic> Diagnostics { get; }
/// <summary>
/// Gets a value that indicates if parsing was successful.
/// </summary>
public bool Success
{
get { return SyntaxTree != null; }
}
}
}

View File

@ -12,16 +12,17 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
private string _fileFormat; private string _fileFormat;
protected IReadOnlyList<RazorFileInfo> FileInfos { get; private set; } public RazorFileInfoCollectionGenerator([NotNull] IEnumerable<RazorFileInfo> fileInfos,
protected CompilationSettings CompilationSettings { get; }
public RazorFileInfoCollectionGenerator([NotNull] IReadOnlyList<RazorFileInfo> fileInfos,
[NotNull] CompilationSettings compilationSettings) [NotNull] CompilationSettings compilationSettings)
{ {
FileInfos = fileInfos; FileInfos = fileInfos;
CompilationSettings = compilationSettings; CompilationSettings = compilationSettings;
} }
protected IEnumerable<RazorFileInfo> FileInfos { get; }
protected CompilationSettings CompilationSettings { get; }
public virtual SyntaxTree GenerateCollection() public virtual SyntaxTree GenerateCollection()
{ {
var builder = new StringBuilder(); var builder = new StringBuilder();

View File

@ -4,8 +4,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNet.FileSystems; using Microsoft.AspNet.FileSystems;
using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis;
using Microsoft.Framework.Cache.Memory;
using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.OptionsModel; using Microsoft.Framework.OptionsModel;
using Microsoft.Framework.Runtime; using Microsoft.Framework.Runtime;
@ -17,37 +20,42 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
private readonly IServiceProvider _serviceProvider; private readonly IServiceProvider _serviceProvider;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IMvcRazorHost _host;
public RazorPreCompiler([NotNull] IServiceProvider designTimeServiceProvider, public RazorPreCompiler([NotNull] IServiceProvider designTimeServiceProvider,
[NotNull] IMemoryCache precompilationCache,
[NotNull] CompilationSettings compilationSettings) : [NotNull] CompilationSettings compilationSettings) :
this(designTimeServiceProvider, this(designTimeServiceProvider,
designTimeServiceProvider.GetRequiredService<IMvcRazorHost>(),
designTimeServiceProvider.GetRequiredService<IOptions<RazorViewEngineOptions>>(), designTimeServiceProvider.GetRequiredService<IOptions<RazorViewEngineOptions>>(),
precompilationCache,
compilationSettings) compilationSettings)
{ {
} }
public RazorPreCompiler([NotNull] IServiceProvider designTimeServiceProvider, public RazorPreCompiler([NotNull] IServiceProvider designTimeServiceProvider,
[NotNull] IMvcRazorHost host,
[NotNull] IOptions<RazorViewEngineOptions> optionsAccessor, [NotNull] IOptions<RazorViewEngineOptions> optionsAccessor,
[NotNull] IMemoryCache precompilationCache,
[NotNull] CompilationSettings compilationSettings) [NotNull] CompilationSettings compilationSettings)
{ {
_serviceProvider = designTimeServiceProvider; _serviceProvider = designTimeServiceProvider;
_host = host;
_fileSystem = optionsAccessor.Options.FileSystem; _fileSystem = optionsAccessor.Options.FileSystem;
CompilationSettings = compilationSettings; CompilationSettings = compilationSettings;
PreCompilationCache = precompilationCache;
} }
protected CompilationSettings CompilationSettings { get; } protected CompilationSettings CompilationSettings { get; }
protected IMemoryCache PreCompilationCache { get; }
protected virtual string FileExtension { get; } = ".cshtml"; protected virtual string FileExtension { get; } = ".cshtml";
protected virtual int MaxDegreesOfParallelism { get; } = Environment.ProcessorCount;
public virtual void CompileViews([NotNull] IBeforeCompileContext context) public virtual void CompileViews([NotNull] IBeforeCompileContext context)
{ {
var descriptors = CreateCompilationDescriptors(context); var descriptors = CreateCompilationDescriptors(context);
if (descriptors.Count > 0) if (descriptors.Any())
{ {
var collectionGenerator = new RazorFileInfoCollectionGenerator( var collectionGenerator = new RazorFileInfoCollectionGenerator(
descriptors, descriptors,
@ -58,93 +66,127 @@ namespace Microsoft.AspNet.Mvc.Razor
} }
} }
protected virtual IReadOnlyList<RazorFileInfo> CreateCompilationDescriptors( protected virtual IEnumerable<RazorFileInfo> CreateCompilationDescriptors(
[NotNull] IBeforeCompileContext context) [NotNull] IBeforeCompileContext context)
{ {
var list = new List<RazorFileInfo>(); var filesToProcess = new List<RelativeFileInfo>();
GetFileInfosRecursive(root: string.Empty, razorFiles: filesToProcess);
foreach (var info in GetFileInfosRecursive(string.Empty)) var razorFiles = new RazorFileInfo[filesToProcess.Count];
var syntaxTrees = new SyntaxTree[filesToProcess.Count];
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = MaxDegreesOfParallelism };
var diagnosticsLock = new object();
Parallel.For(0, filesToProcess.Count, parallelOptions, index =>
{ {
var descriptor = ParseView(info, context); var file = filesToProcess[index];
var cacheEntry = PreCompilationCache.GetOrSet(file.RelativePath,
if (descriptor != null) file,
OnCacheMiss);
if (cacheEntry != null)
{ {
list.Add(descriptor); if (cacheEntry.Success)
{
syntaxTrees[index] = cacheEntry.SyntaxTree;
razorFiles[index] = cacheEntry.FileInfo;
}
else
{
lock (diagnosticsLock)
{
foreach (var diagnostic in cacheEntry.Diagnostics)
{
context.Diagnostics.Add(diagnostic);
}
}
}
}
});
context.CSharpCompilation = context.CSharpCompilation
.AddSyntaxTrees(syntaxTrees.Where(tree => tree != null));
return razorFiles.Where(file => file != null);
}
protected IMvcRazorHost GetRazorHost()
{
return _serviceProvider.GetRequiredService<IMvcRazorHost>();
}
private PrecompilationCacheEntry OnCacheMiss(ICacheSetContext cacheSetContext)
{
var fileInfo = (RelativeFileInfo)cacheSetContext.State;
var entry = GetCacheEntry(fileInfo);
if (entry != null)
{
cacheSetContext.AddExpirationTrigger(_fileSystem.Watch(fileInfo.RelativePath));
foreach (var viewStartPath in ViewStartUtility.GetViewStartLocations(fileInfo.RelativePath))
{
cacheSetContext.AddExpirationTrigger(_fileSystem.Watch(viewStartPath));
} }
} }
return list; return entry;
} }
private IEnumerable<RelativeFileInfo> GetFileInfosRecursive(string currentPath) private void GetFileInfosRecursive(string root, List<RelativeFileInfo> razorFiles)
{ {
var path = currentPath; var fileInfos = _fileSystem.GetDirectoryContents(root);
var fileInfos = _fileSystem.GetDirectoryContents(path);
if (!fileInfos.Exists)
{
yield break;
}
foreach (var fileInfo in fileInfos) foreach (var fileInfo in fileInfos)
{ {
if (fileInfo.IsDirectory) if (fileInfo.IsDirectory)
{ {
var subPath = Path.Combine(path, fileInfo.Name); var subPath = Path.Combine(root, fileInfo.Name);
GetFileInfosRecursive(subPath, razorFiles);
foreach (var info in GetFileInfosRecursive(subPath))
{
yield return info;
}
} }
else if (Path.GetExtension(fileInfo.Name) else if (Path.GetExtension(fileInfo.Name)
.Equals(FileExtension, StringComparison.OrdinalIgnoreCase)) .Equals(FileExtension, StringComparison.OrdinalIgnoreCase))
{ {
var relativePath = Path.Combine(currentPath, fileInfo.Name); var relativePath = Path.Combine(root, fileInfo.Name);
var info = new RelativeFileInfo(fileInfo, relativePath); var info = new RelativeFileInfo(fileInfo, relativePath);
yield return info; razorFiles.Add(info);
} }
} }
} }
protected virtual RazorFileInfo ParseView([NotNull] RelativeFileInfo fileInfo, protected virtual PrecompilationCacheEntry GetCacheEntry([NotNull] RelativeFileInfo fileInfo)
[NotNull] IBeforeCompileContext context)
{ {
using (var stream = fileInfo.FileInfo.CreateReadStream()) using (var stream = fileInfo.FileInfo.CreateReadStream())
{ {
var results = _host.GenerateCode(fileInfo.RelativePath, stream); var host = GetRazorHost();
var results = host.GenerateCode(fileInfo.RelativePath, stream);
foreach (var parserError in results.ParserErrors) if (results.Success)
{ {
var diagnostic = parserError.ToDiagnostics(fileInfo.FileInfo.PhysicalPath); var syntaxTree = SyntaxTreeGenerator.Generate(results.GeneratedCode,
context.Diagnostics.Add(diagnostic);
}
var generatedCode = results.GeneratedCode;
if (generatedCode != null)
{
var syntaxTree = SyntaxTreeGenerator.Generate(generatedCode,
fileInfo.FileInfo.PhysicalPath, fileInfo.FileInfo.PhysicalPath,
CompilationSettings); CompilationSettings);
var fullTypeName = results.GetMainClassName(_host, syntaxTree); var fullTypeName = results.GetMainClassName(host, syntaxTree);
if (fullTypeName != null) if (fullTypeName != null)
{ {
context.CSharpCompilation = context.CSharpCompilation.AddSyntaxTrees(syntaxTree);
var hash = RazorFileHash.GetHash(fileInfo.FileInfo); var hash = RazorFileHash.GetHash(fileInfo.FileInfo);
var razorFileInfo = new RazorFileInfo
return new RazorFileInfo()
{ {
FullTypeName = fullTypeName,
RelativePath = fileInfo.RelativePath, RelativePath = fileInfo.RelativePath,
LastModified = fileInfo.FileInfo.LastModified, LastModified = fileInfo.FileInfo.LastModified,
Length = fileInfo.FileInfo.Length, Length = fileInfo.FileInfo.Length,
Hash = hash, FullTypeName = fullTypeName
}; };
return new PrecompilationCacheEntry(razorFileInfo, syntaxTree);
} }
} }
else
{
var diagnostics = results.ParserErrors
.Select(error => error.ToDiagnostics(fileInfo.FileInfo.PhysicalPath))
.ToList();
return new PrecompilationCacheEntry(diagnostics);
}
} }
return null; return null;

View File

@ -9,7 +9,8 @@
"Microsoft.AspNet.Mvc.Core": "6.0.0-*", "Microsoft.AspNet.Mvc.Core": "6.0.0-*",
"Microsoft.AspNet.Mvc.Razor.Host": "6.0.0-*", "Microsoft.AspNet.Mvc.Razor.Host": "6.0.0-*",
"Microsoft.CodeAnalysis.CSharp": "1.0.0-rc1-*", "Microsoft.CodeAnalysis.CSharp": "1.0.0-rc1-*",
"Microsoft.Framework.Runtime.Roslyn.Common": "1.0.0-*" "Microsoft.Framework.Cache.Memory": "1.0.0-*",
"Microsoft.Framework.Runtime.Roslyn.Common": "1.0.0-*"
}, },
"frameworks": { "frameworks": {
"aspnet50": { "aspnet50": {
@ -21,6 +22,10 @@
"System.Threading.Tasks": "" "System.Threading.Tasks": ""
} }
}, },
"aspnetcore50": {} "aspnetcore50": {
"dependencies": {
"System.Threading.Tasks.Parallel": "4.0.0-beta-*"
}
}
} }
} }

View File

@ -9,6 +9,7 @@ using Microsoft.AspNet.Hosting;
using Microsoft.AspNet.Mvc.Razor; using Microsoft.AspNet.Mvc.Razor;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp;
using Microsoft.Framework.Cache.Memory;
using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.DependencyInjection.Fallback; using Microsoft.Framework.DependencyInjection.Fallback;
using Microsoft.Framework.DependencyInjection.ServiceLookup; using Microsoft.Framework.DependencyInjection.ServiceLookup;
@ -19,10 +20,15 @@ namespace Microsoft.AspNet.Mvc
public abstract class RazorPreCompileModule : ICompileModule public abstract class RazorPreCompileModule : ICompileModule
{ {
private readonly IServiceProvider _appServices; private readonly IServiceProvider _appServices;
private readonly IMemoryCache _memoryCache;
public RazorPreCompileModule(IServiceProvider services) public RazorPreCompileModule(IServiceProvider services)
{ {
_appServices = services; _appServices = services;
// When ListenForMemoryPressure is true, the MemoryCache evicts items at every gen2 collection.
// In DTH, gen2 happens frequently enough to make it undesirable for caching precompilation results. We'll
// disable listening for memory pressure for the MemoryCache instance used by precompilation.
_memoryCache = new MemoryCache(new MemoryCacheOptions { ListenForMemoryPressure = false });
} }
protected virtual string FileExtension { get; } = ".cshtml"; protected virtual string FileExtension { get; } = ".cshtml";
@ -39,7 +45,7 @@ namespace Microsoft.AspNet.Mvc
sc.AddMvc(); sc.AddMvc();
var serviceProvider = BuildFallbackServiceProvider(sc, _appServices); var serviceProvider = BuildFallbackServiceProvider(sc, _appServices);
var viewCompiler = new RazorPreCompiler(serviceProvider, compilationSettings); var viewCompiler = new RazorPreCompiler(serviceProvider, _memoryCache, compilationSettings);
viewCompiler.CompileViews(context); viewCompiler.CompileViews(context);
} }