From 12243e7d97b50c4235c800d9a1ce94641cdf7d1c Mon Sep 17 00:00:00 2001 From: Pranav K Date: Thu, 8 Jan 2015 10:07:13 -0800 Subject: [PATCH] * Modifying RazorPrecompiler to use IMemoryCache to cache results when precompilation is invoked via Design Time Host. * Change compilation to occur in parallel --- .../PrecompilationCacheEntry.cs | 71 +++++++++ .../RazorFileInfoCollectionGenerator.cs | 9 +- .../Razor/PreCompileViews/RazorPreCompiler.cs | 144 +++++++++++------- src/Microsoft.AspNet.Mvc.Razor/project.json | 9 +- .../RazorPreCompileModule.cs | 8 +- 5 files changed, 183 insertions(+), 58 deletions(-) create mode 100644 src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/PrecompilationCacheEntry.cs diff --git a/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/PrecompilationCacheEntry.cs b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/PrecompilationCacheEntry.cs new file mode 100644 index 0000000000..e7a53d2509 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/PrecompilationCacheEntry.cs @@ -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 +{ + /// + /// An entry in the cache used by . + /// + public class PrecompilationCacheEntry + { + /// + /// Initializes a new instance of for a successful parse. + /// + /// The of the file being cached. + /// The to cache. + public PrecompilationCacheEntry([NotNull] RazorFileInfo fileInfo, + [NotNull] SyntaxTree syntaxTree) + { + FileInfo = fileInfo; + SyntaxTree = syntaxTree; + } + + /// + /// Initializes a new instance of for a failed parse. + /// + /// The produced from parsing the Razor + /// file. This does not contain s produced from compiling the parsed + /// . + public PrecompilationCacheEntry([NotNull] IReadOnlyList diagnostics) + { + Diagnostics = diagnostics; + } + + /// + /// Gets the associated with this cache entry instance. + /// + /// + /// This property is not null if is true. + /// + public RazorFileInfo FileInfo { get; } + + /// + /// Gets the produced from parsing the Razor file. + /// + /// + /// This property is not null if is true. + /// + public SyntaxTree SyntaxTree { get; } + + /// + /// Gets the s produced from parsing the generated contents of the file + /// specified by . This does not contain s produced from + /// compiling the parsed . + /// + /// + /// This property is null if is true. + /// + public IReadOnlyList Diagnostics { get; } + + /// + /// Gets a value that indicates if parsing was successful. + /// + public bool Success + { + get { return SyntaxTree != null; } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorFileInfoCollectionGenerator.cs b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorFileInfoCollectionGenerator.cs index 5dc2b51c4a..4db7acad5d 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorFileInfoCollectionGenerator.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorFileInfoCollectionGenerator.cs @@ -12,16 +12,17 @@ namespace Microsoft.AspNet.Mvc.Razor { private string _fileFormat; - protected IReadOnlyList FileInfos { get; private set; } - protected CompilationSettings CompilationSettings { get; } - - public RazorFileInfoCollectionGenerator([NotNull] IReadOnlyList fileInfos, + public RazorFileInfoCollectionGenerator([NotNull] IEnumerable fileInfos, [NotNull] CompilationSettings compilationSettings) { FileInfos = fileInfos; CompilationSettings = compilationSettings; } + protected IEnumerable FileInfos { get; } + + protected CompilationSettings CompilationSettings { get; } + public virtual SyntaxTree GenerateCollection() { var builder = new StringBuilder(); diff --git a/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorPreCompiler.cs b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorPreCompiler.cs index 0b59bc46a2..4bbe5b90a5 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorPreCompiler.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorPreCompiler.cs @@ -4,8 +4,11 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; +using System.Threading.Tasks; using Microsoft.AspNet.FileSystems; -using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis; +using Microsoft.Framework.Cache.Memory; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.OptionsModel; using Microsoft.Framework.Runtime; @@ -17,37 +20,42 @@ namespace Microsoft.AspNet.Mvc.Razor { private readonly IServiceProvider _serviceProvider; private readonly IFileSystem _fileSystem; - private readonly IMvcRazorHost _host; public RazorPreCompiler([NotNull] IServiceProvider designTimeServiceProvider, + [NotNull] IMemoryCache precompilationCache, [NotNull] CompilationSettings compilationSettings) : this(designTimeServiceProvider, - designTimeServiceProvider.GetRequiredService(), designTimeServiceProvider.GetRequiredService>(), + precompilationCache, compilationSettings) { } public RazorPreCompiler([NotNull] IServiceProvider designTimeServiceProvider, - [NotNull] IMvcRazorHost host, [NotNull] IOptions optionsAccessor, + [NotNull] IMemoryCache precompilationCache, [NotNull] CompilationSettings compilationSettings) { _serviceProvider = designTimeServiceProvider; - _host = host; _fileSystem = optionsAccessor.Options.FileSystem; CompilationSettings = compilationSettings; + PreCompilationCache = precompilationCache; } protected CompilationSettings CompilationSettings { get; } + protected IMemoryCache PreCompilationCache { get; } + protected virtual string FileExtension { get; } = ".cshtml"; + protected virtual int MaxDegreesOfParallelism { get; } = Environment.ProcessorCount; + + public virtual void CompileViews([NotNull] IBeforeCompileContext context) { var descriptors = CreateCompilationDescriptors(context); - if (descriptors.Count > 0) + if (descriptors.Any()) { var collectionGenerator = new RazorFileInfoCollectionGenerator( descriptors, @@ -58,93 +66,127 @@ namespace Microsoft.AspNet.Mvc.Razor } } - protected virtual IReadOnlyList CreateCompilationDescriptors( - [NotNull] IBeforeCompileContext context) + protected virtual IEnumerable CreateCompilationDescriptors( + [NotNull] IBeforeCompileContext context) { - var list = new List(); + var filesToProcess = new List(); + 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); - - if (descriptor != null) + var file = filesToProcess[index]; + var cacheEntry = PreCompilationCache.GetOrSet(file.RelativePath, + 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(); + } + + 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 GetFileInfosRecursive(string currentPath) + private void GetFileInfosRecursive(string root, List razorFiles) { - var path = currentPath; - - var fileInfos = _fileSystem.GetDirectoryContents(path); - if (!fileInfos.Exists) - { - yield break; - } + var fileInfos = _fileSystem.GetDirectoryContents(root); foreach (var fileInfo in fileInfos) { if (fileInfo.IsDirectory) { - var subPath = Path.Combine(path, fileInfo.Name); - - foreach (var info in GetFileInfosRecursive(subPath)) - { - yield return info; - } + var subPath = Path.Combine(root, fileInfo.Name); + GetFileInfosRecursive(subPath, razorFiles); } else if (Path.GetExtension(fileInfo.Name) .Equals(FileExtension, StringComparison.OrdinalIgnoreCase)) { - var relativePath = Path.Combine(currentPath, fileInfo.Name); + var relativePath = Path.Combine(root, fileInfo.Name); var info = new RelativeFileInfo(fileInfo, relativePath); - yield return info; + razorFiles.Add(info); } } } - protected virtual RazorFileInfo ParseView([NotNull] RelativeFileInfo fileInfo, - [NotNull] IBeforeCompileContext context) + protected virtual PrecompilationCacheEntry GetCacheEntry([NotNull] RelativeFileInfo fileInfo) { 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); - context.Diagnostics.Add(diagnostic); - } - - var generatedCode = results.GeneratedCode; - - if (generatedCode != null) - { - var syntaxTree = SyntaxTreeGenerator.Generate(generatedCode, + var syntaxTree = SyntaxTreeGenerator.Generate(results.GeneratedCode, fileInfo.FileInfo.PhysicalPath, CompilationSettings); - var fullTypeName = results.GetMainClassName(_host, syntaxTree); + var fullTypeName = results.GetMainClassName(host, syntaxTree); if (fullTypeName != null) { - context.CSharpCompilation = context.CSharpCompilation.AddSyntaxTrees(syntaxTree); - var hash = RazorFileHash.GetHash(fileInfo.FileInfo); - - return new RazorFileInfo() + var razorFileInfo = new RazorFileInfo { - FullTypeName = fullTypeName, RelativePath = fileInfo.RelativePath, LastModified = fileInfo.FileInfo.LastModified, 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; diff --git a/src/Microsoft.AspNet.Mvc.Razor/project.json b/src/Microsoft.AspNet.Mvc.Razor/project.json index 8b0ebed627..75da75289d 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/project.json +++ b/src/Microsoft.AspNet.Mvc.Razor/project.json @@ -9,7 +9,8 @@ "Microsoft.AspNet.Mvc.Core": "6.0.0-*", "Microsoft.AspNet.Mvc.Razor.Host": "6.0.0-*", "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": { "aspnet50": { @@ -21,6 +22,10 @@ "System.Threading.Tasks": "" } }, - "aspnetcore50": {} + "aspnetcore50": { + "dependencies": { + "System.Threading.Tasks.Parallel": "4.0.0-beta-*" + } + } } } diff --git a/src/Microsoft.AspNet.Mvc/RazorPreCompileModule.cs b/src/Microsoft.AspNet.Mvc/RazorPreCompileModule.cs index d749009194..351f3b6b18 100644 --- a/src/Microsoft.AspNet.Mvc/RazorPreCompileModule.cs +++ b/src/Microsoft.AspNet.Mvc/RazorPreCompileModule.cs @@ -9,6 +9,7 @@ using Microsoft.AspNet.Hosting; using Microsoft.AspNet.Mvc.Razor; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; +using Microsoft.Framework.Cache.Memory; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection.Fallback; using Microsoft.Framework.DependencyInjection.ServiceLookup; @@ -19,10 +20,15 @@ namespace Microsoft.AspNet.Mvc public abstract class RazorPreCompileModule : ICompileModule { private readonly IServiceProvider _appServices; + private readonly IMemoryCache _memoryCache; public RazorPreCompileModule(IServiceProvider 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"; @@ -39,7 +45,7 @@ namespace Microsoft.AspNet.Mvc sc.AddMvc(); var serviceProvider = BuildFallbackServiceProvider(sc, _appServices); - var viewCompiler = new RazorPreCompiler(serviceProvider, compilationSettings); + var viewCompiler = new RazorPreCompiler(serviceProvider, _memoryCache, compilationSettings); viewCompiler.CompileViews(context); }