diff --git a/src/Orchard.Web/Modules/Orchard.Setup/Controllers/SetupController.cs b/src/Orchard.Web/Modules/Orchard.Setup/Controllers/SetupController.cs index bf9272d7b..402134a4d 100644 --- a/src/Orchard.Web/Modules/Orchard.Setup/Controllers/SetupController.cs +++ b/src/Orchard.Web/Modules/Orchard.Setup/Controllers/SetupController.cs @@ -1,5 +1,6 @@ using System; using System.Web.Mvc; +using Orchard.Environment; using Orchard.FileSystems.AppData; using Orchard.Setup.Services; using Orchard.Setup.ViewModels; @@ -11,11 +12,13 @@ namespace Orchard.Setup.Controllers { [ValidateInput(false), Themed] public class SetupController : Controller { private readonly IAppDataFolder _appDataFolder; + private readonly IViewsBackgroundCompilation _viewsBackgroundCompilation; private readonly INotifier _notifier; private readonly ISetupService _setupService; - public SetupController(INotifier notifier, ISetupService setupService, IAppDataFolder appDataFolder) { + public SetupController(INotifier notifier, ISetupService setupService, IAppDataFolder appDataFolder, IViewsBackgroundCompilation viewsBackgroundCompilation) { _appDataFolder = appDataFolder; + _viewsBackgroundCompilation = viewsBackgroundCompilation; _notifier = notifier; _setupService = setupService; T = NullLocalizer.Instance; @@ -37,6 +40,17 @@ namespace Orchard.Setup.Controllers { public ActionResult Index() { var initialSettings = _setupService.Prime(); + + // On the first time installation of Orchard, the user gets to the setup screen, which + // will take a while to finish (user inputting data and the setup process itself). + // We use this opportunity to start a background task to "pre-compile" all the known + // views in the app folder, so that the application is more reponsive when the user + // hits the homepage and admin screens for the first time. + if (StringComparer.OrdinalIgnoreCase.Equals(initialSettings.Name, "Default")) { + _viewsBackgroundCompilation.Start(); + } + + // return IndexViewResult(new SetupViewModel { AdminUsername = "admin", DatabaseIsPreconfigured = !string.IsNullOrEmpty(initialSettings.DataProvider)}); } @@ -75,6 +89,11 @@ namespace Orchard.Setup.Controllers { _setupService.Setup(setupContext); + // First time installation if finally done. Tell the background views compilation + // process to stop, so that it doesn't interfere with the user (asp.net compilation + // uses a "single lock" mechanism for compiling views). + _viewsBackgroundCompilation.Stop(); + // redirect to the welcome page. return Redirect("~/"); } diff --git a/src/Orchard/Environment/OrchardStarter.cs b/src/Orchard/Environment/OrchardStarter.cs index 053379d36..2358b9af9 100644 --- a/src/Orchard/Environment/OrchardStarter.cs +++ b/src/Orchard/Environment/OrchardStarter.cs @@ -48,6 +48,7 @@ namespace Orchard.Environment { builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); + builder.RegisterType().As().SingleInstance(); RegisterVolatileProvider(builder); RegisterVolatileProvider(builder); diff --git a/src/Orchard/Environment/ViewsBackgroundCompilation.cs b/src/Orchard/Environment/ViewsBackgroundCompilation.cs new file mode 100644 index 000000000..240ab75e7 --- /dev/null +++ b/src/Orchard/Environment/ViewsBackgroundCompilation.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Timers; +using System.Web.Compilation; +using Orchard.FileSystems.VirtualPath; +using Orchard.Logging; + +namespace Orchard.Environment { + public interface IViewsBackgroundCompilation { + void Start(); + void Stop(); + } + + public class ViewsBackgroundCompilation : IViewsBackgroundCompilation { + private readonly IVirtualPathProvider _virtualPathProvider; + private volatile bool _stopping; + + public ViewsBackgroundCompilation(IVirtualPathProvider virtualPathProvider) { + _virtualPathProvider = virtualPathProvider; + + Logger = NullLogger.Instance; + } + + public ILogger Logger { get; set; } + + public void Start() { + _stopping = false; + var timer = new Timer(); + timer.Elapsed += CompileViews; + timer.Interval = TimeSpan.FromMilliseconds(100).TotalMilliseconds; + timer.AutoReset = false; + timer.Start(); + } + + public void Stop() { + _stopping = true; + } + + public class CompilationContext { + public IEnumerable DirectoriesToBrowse { get; set; } + public IEnumerable FileExtensionsToCompile { get; set; } + public HashSet ProcessedDirectories { get; set; } + } + + private void CompileViews(object sender, ElapsedEventArgs elapsedEventArgs) { + Logger.Information("Starting background compilation of views"); + ((Timer)sender).Stop(); + + // Hard-coded context based on current orchard profile + var context = new CompilationContext { + // Put most frequently used directories first in the list + DirectoriesToBrowse = new[] { + // Setup + "~/Modules/Orchard.Setup/Views", + "~/Themes/SafeMode/Views", + + // Homepage + "~/Themes/TheThemeMachine/Views", + "~/Core/Common/Views", + "~/Core/Contents/Views", + "~/Core/Routable/Views", + "~/Core/Settings/Views", + "~/Core/Shapes/Views", + "~/Core/Feeds/Views", + "~/Modules/Orchard.Tags/Views", + "~/Modules/Orchard.Widgets/Views", + + // Dashboard + "~/Core/Dashboard/Views", + "~/Themes/TheAdmin/Views", + + // "Edit" homepage + "~/Modules/TinyMce/Views", + + // Various other admin pages + "~/Modules/Orchard.Modules/Views", + "~/Modules/Orchard.Users/Views", + "~/Modules/Orchard.Media/Views", + "~/Modules/Orchard.Comments/Views", + + // Leave these at end (as a best effort) + "~/Core", "~/Modules", "~/Themes" + }, + FileExtensionsToCompile = new[] { ".cshtml", ".acsx", ".aspx" }, + ProcessedDirectories = new HashSet(StringComparer.OrdinalIgnoreCase) + }; + + var directories = context + .DirectoriesToBrowse + .SelectMany(folder => GetViewDirectories(folder, context.FileExtensionsToCompile)); + + foreach (var viewDirectory in directories) { + if (_stopping) { + if (Logger.IsEnabled(LogLevel.Information)) { + var leftOvers = directories.Except(context.ProcessedDirectories).ToList(); + Logger.Information("Background compilation stopped before all directories were processed ({0} directories left)", leftOvers.Count); + foreach (var directory in leftOvers) { + Logger.Information("Directory not processed: '{0}'", directory); + } + } + break; + } + + CompileDirectory(context, viewDirectory); + } + Logger.Information("Ending background compilation of views"); + } + + private void CompileDirectory(CompilationContext context, string viewDirectory) { + // Prevent processing of the same directories multiple times (sligh performance optimization, + // as the build manager second call to compile a view is essentially a "no-op". + if (context.ProcessedDirectories.Contains(viewDirectory)) + return; + context.ProcessedDirectories.Add(viewDirectory); + + var stopwatch = new Stopwatch(); + stopwatch.Start(); + try { + + var firstFile = _virtualPathProvider + .ListFiles(viewDirectory) + .Where(f => context.FileExtensionsToCompile.Any(e => f.EndsWith(e, StringComparison.OrdinalIgnoreCase))) + .FirstOrDefault(); + + if (firstFile != null) + BuildManager.GetCompiledAssembly(firstFile); + } + catch(Exception e) { + // Some views might not compile, this is ok and harmless in this + // context of pre-compiling views. + Logger.Information(e, "Compilation of directory '{0}' skipped", viewDirectory); + } + stopwatch.Stop(); + Logger.Information("Directory '{0}' compiled in {1} msec", viewDirectory, stopwatch.ElapsedMilliseconds); + } + + private IEnumerable GetViewDirectories(string directory, IEnumerable extensions) { + var result = new List(); + GetViewDirectories(_virtualPathProvider, directory, extensions, result); + return result; + } + + private void GetViewDirectories(IVirtualPathProvider vpp, string directory, IEnumerable extensions, ICollection files) { + if (vpp.ListFiles(directory).Where(f => extensions.Any(e => f.EndsWith(e, StringComparison.OrdinalIgnoreCase))).Any()) { + files.Add(directory); + } + + foreach (var childDirectory in vpp.ListDirectories(directory).OrderBy(d => d, StringComparer.OrdinalIgnoreCase)) { + GetViewDirectories(vpp, childDirectory, extensions, files); + } + } + } +} \ No newline at end of file diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj index 6de531196..40d72d553 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -159,6 +159,7 @@ +