diff --git a/src/Umbraco.Core/PluginManager.cs b/src/Umbraco.Core/PluginManager.cs index 66b4126574..1a43139632 100644 --- a/src/Umbraco.Core/PluginManager.cs +++ b/src/Umbraco.Core/PluginManager.cs @@ -7,6 +7,8 @@ using System.Linq; using System.Reflection; using System.Text; using System.Threading; +using System.Web.Caching; +using System.Web.Compilation; using System.Xml; using System.Xml.Linq; using Umbraco.Core.Configuration; @@ -33,8 +35,29 @@ namespace Umbraco.Core /// internal class PluginManager { + private readonly ApplicationContext _appContext; - internal PluginManager() + /// + /// Creates a new PluginManager with an ApplicationContext instance which ensures that the plugin xml + /// file is cached temporarily until app startup completes. + /// + /// + /// + internal PluginManager(ApplicationContext appContext, bool detectBinChanges = true) + : this(detectBinChanges) + { + if (appContext == null) throw new ArgumentNullException("appContext"); + _appContext = appContext; + } + + /// + /// Creates a new PluginManager + /// + /// + /// If true will detect changes in the /bin folder and therefor load plugins from the + /// cached plugins file if one is found. If false will never use the cache file for plugins + /// + internal PluginManager(bool detectBinChanges = true) { _tempFolder = IOHelper.MapPath("~/App_Data/TEMP/PluginCache"); //create the folder if it doesn't exist @@ -42,13 +65,24 @@ namespace Umbraco.Core { Directory.CreateDirectory(_tempFolder); } - //do the check if they've changed - HaveAssembliesChanged = CachedAssembliesHash != CurrentAssembliesHash; - //if they have changed, we need to write the new file - if (HaveAssembliesChanged) + + if (detectBinChanges) { - WriteCachePluginsHash(); + //first check if the cached hash is 0, if it is then we ne + //do the check if they've changed + HaveAssembliesChanged = (CachedAssembliesHash != CurrentAssembliesHash) || CachedAssembliesHash == 0; + //if they have changed, we need to write the new file + if (HaveAssembliesChanged) + { + WriteCachePluginsHash(); + } } + else + { + //always set to true if we're not detecting (generally only for testing) + HaveAssembliesChanged = true; + } + } static PluginManager _resolver; @@ -72,7 +106,9 @@ namespace Umbraco.Core if (_resolver == null) { l.UpgradeToWriteLock(); - _resolver = new PluginManager(); + _resolver = ApplicationContext.Current == null + ? new PluginManager() + : new PluginManager(ApplicationContext.Current); } return _resolver; } @@ -180,37 +216,49 @@ namespace Umbraco.Core /// /// /// - internal Attempt>> TryGetCachedPluginsFromFile() + internal Attempt> TryGetCachedPluginsFromFile() { var filePath = Path.Combine(_tempFolder, "umbraco-plugins.list"); if (!File.Exists(filePath)) - return Attempt>>.False; + return Attempt>.False; try { - var xml = XDocument.Load(filePath); + //we will load the xml document, if the app context exist, we will load it from the cache (which is only around for 5 minutes) + //while the app boots up, this should save some IO time on app startup when the app context is there (which is always unless in unit tests) + XDocument xml; + if (_appContext != null) + { + xml = _appContext.ApplicationCache.GetCacheItem("umbraco-plugins.list", + new TimeSpan(0, 0, 5, 0), + () => XDocument.Load(filePath)); + } + else + { + xml = XDocument.Load(filePath); + } + + if (xml.Root == null) - return Attempt>>.False; + return Attempt>.False; var typeElement = xml.Root.Elements() .SingleOrDefault(x => x.Name.LocalName == "baseType" && ((string) x.Attribute("type")) == typeof (T).FullName); if (typeElement == null) - return Attempt>>.False; + return Attempt>.False; //return success - return new Attempt>>( + return new Attempt>( true, typeElement.Elements("add") - .Select(x => new Tuple( - (string) x.Attribute("type"), - (string) x.Attribute("assembly")))); + .Select(x => (string) x.Attribute("type"))); } catch (Exception) { //if the file is corrupted, etc... return false - return Attempt>>.False; + return Attempt>.False; } } @@ -268,10 +316,9 @@ namespace Umbraco.Core //now we have the type element, we need to clear any previous types as children and add/update it with new ones typeElement.ReplaceNodes(typesFound - .Select(x => - new XElement("add", - new XAttribute("type", x.FullName), - new XAttribute("assembly", x.Assembly.FullName)))); + .Select(x => + new XElement("add", + new XAttribute("type", x.AssemblyQualifiedName)))); //save the xml file xml.Save(filePath); } @@ -444,15 +491,17 @@ namespace Umbraco.Core { try { - var type = Assembly.Load(t.Item2).GetType(t.Item1); - typeList.AddType(type); + //we use the build manager to ensure we get all types loaded, this is slightly slower than + //Type.GetType but if the types in the assembly aren't loaded yet then we have problems with that. + var type = BuildManager.GetType(t, true); + typeList.AddType(type); } catch (Exception ex) { //if there are any exceptions loading types, we have to exist, this should never happen so //we will need to revert to scanning for types. successfullyLoadedFromCache = false; - LogHelper.Error("Could not load a cached plugin type: " + t.Item1 + " in assembly: " + t.Item2 + " now reverting to re-scanning assemblies for the base type: " + typeof (T).FullName, ex); + LogHelper.Error("Could not load a cached plugin type: " + t + " now reverting to re-scanning assemblies for the base type: " + typeof (T).FullName, ex); break; } } @@ -461,6 +510,10 @@ namespace Umbraco.Core //we need to manually load by scanning if loading from the file was not successful. LoadViaScanningAndUpdateCacheFile(typeList, finder); } + else + { + LogHelper.Debug("Loaded plugin types " + typeof(T).FullName + " from persisted cache"); + } } } else diff --git a/src/Umbraco.Tests/CacheRefresherFactoryTests.cs b/src/Umbraco.Tests/CacheRefresherFactoryTests.cs index e9a2c075fb..8fdef3f09a 100644 --- a/src/Umbraco.Tests/CacheRefresherFactoryTests.cs +++ b/src/Umbraco.Tests/CacheRefresherFactoryTests.cs @@ -17,7 +17,7 @@ namespace Umbraco.Tests TestHelper.SetupLog4NetForTests(); //this ensures its reset - PluginManager.Current = new PluginManager(); + PluginManager.Current = new PluginManager(false); //for testing, we'll specify which assemblies are scanned for the PluginTypeResolver PluginManager.Current.AssembliesToScan = new[] diff --git a/src/Umbraco.Tests/DataTypeFactoryTests.cs b/src/Umbraco.Tests/DataTypeFactoryTests.cs index d836b8f836..e0de0a4af6 100644 --- a/src/Umbraco.Tests/DataTypeFactoryTests.cs +++ b/src/Umbraco.Tests/DataTypeFactoryTests.cs @@ -19,7 +19,7 @@ namespace Umbraco.Tests TestHelper.SetupLog4NetForTests(); //this ensures its reset - PluginManager.Current = new PluginManager(); + PluginManager.Current = new PluginManager(false); //for testing, we'll specify which assemblies are scanned for the PluginTypeResolver PluginManager.Current.AssembliesToScan = new[] diff --git a/src/Umbraco.Tests/MacroEngineFactoryTests.cs b/src/Umbraco.Tests/MacroEngineFactoryTests.cs index 4a058e272e..f38a825f6f 100644 --- a/src/Umbraco.Tests/MacroEngineFactoryTests.cs +++ b/src/Umbraco.Tests/MacroEngineFactoryTests.cs @@ -18,7 +18,7 @@ namespace Umbraco.Tests TestHelper.SetupLog4NetForTests(); //this ensures its reset - PluginManager.Current = new PluginManager(); + PluginManager.Current = new PluginManager(false); //for testing, we'll specify which assemblies are scanned for the PluginTypeResolver PluginManager.Current.AssembliesToScan = new[] diff --git a/src/Umbraco.Tests/MediaFactoryTests.cs b/src/Umbraco.Tests/MediaFactoryTests.cs index 58fd81d446..11bf2de2fe 100644 --- a/src/Umbraco.Tests/MediaFactoryTests.cs +++ b/src/Umbraco.Tests/MediaFactoryTests.cs @@ -18,7 +18,7 @@ namespace Umbraco.Tests TestHelper.SetupLog4NetForTests(); //this ensures its reset - PluginManager.Current = new PluginManager(); + PluginManager.Current = new PluginManager(false); //for testing, we'll specify which assemblies are scanned for the PluginTypeResolver PluginManager.Current.AssembliesToScan = new[] diff --git a/src/Umbraco.Tests/PluginManagerTests.cs b/src/Umbraco.Tests/PluginManagerTests.cs index 3812835115..f7262c8814 100644 --- a/src/Umbraco.Tests/PluginManagerTests.cs +++ b/src/Umbraco.Tests/PluginManagerTests.cs @@ -1,8 +1,10 @@ using System; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; +using System.Web.Compilation; using NUnit.Framework; using SqlCE4Umbraco; using Umbraco.Core; @@ -15,6 +17,7 @@ using umbraco.MacroEngines.Iron; using umbraco.businesslogic; using umbraco.cms.businesslogic; using umbraco.editorControls; +using umbraco.interfaces; using umbraco.uicontrols; using umbraco.cms; @@ -31,7 +34,7 @@ namespace Umbraco.Tests TestHelper.SetupLog4NetForTests(); //this ensures its reset - PluginManager.Current = new PluginManager(); + PluginManager.Current = new PluginManager(false); //for testing, we'll specify which assemblies are scanned for the PluginTypeResolver //TODO: Should probably update this so it only searches this assembly and add custom types to be found @@ -69,19 +72,97 @@ namespace Umbraco.Tests return dir; } + //[Test] + //public void Scan_Vs_Load_Benchmark() + //{ + // var pluginManager = new PluginManager(false); + // var watch = new Stopwatch(); + // watch.Start(); + // for (var i = 0; i < 1000; i++) + // { + // var type2 = Type.GetType("umbraco.macroCacheRefresh, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null"); + // var type3 = Type.GetType("umbraco.templateCacheRefresh, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null"); + // var type4 = Type.GetType("umbraco.presentation.cache.MediaLibraryRefreshers, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null"); + // var type5 = Type.GetType("umbraco.presentation.cache.pageRefresher, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null"); + // } + // watch.Stop(); + // Debug.WriteLine("TOTAL TIME (1st round): " + watch.ElapsedMilliseconds); + // watch.Start(); + // for (var i = 0; i < 1000; i++) + // { + // var type2 = BuildManager.GetType("umbraco.macroCacheRefresh, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null", true); + // var type3 = BuildManager.GetType("umbraco.templateCacheRefresh, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null", true); + // var type4 = BuildManager.GetType("umbraco.presentation.cache.MediaLibraryRefreshers, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null", true); + // var type5 = BuildManager.GetType("umbraco.presentation.cache.pageRefresher, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null", true); + // } + // watch.Stop(); + // Debug.WriteLine("TOTAL TIME (1st round): " + watch.ElapsedMilliseconds); + // watch.Reset(); + // watch.Start(); + // for (var i = 0; i < 1000; i++) + // { + // var refreshers = pluginManager.ResolveTypes(false); + // } + // watch.Stop(); + // Debug.WriteLine("TOTAL TIME (2nd round): " + watch.ElapsedMilliseconds); + //} + + ////NOTE: This test shows that Type.GetType is 100% faster than Assembly.Load(..).GetType(...) so we'll use that :) + //[Test] + //public void Load_Type_Benchmark() + //{ + // var watch = new Stopwatch(); + // watch.Start(); + // for (var i = 0; i < 1000; i++) + // { + // var type2 = Type.GetType("umbraco.macroCacheRefresh, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null"); + // var type3 = Type.GetType("umbraco.templateCacheRefresh, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null"); + // var type4 = Type.GetType("umbraco.presentation.cache.MediaLibraryRefreshers, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null"); + // var type5 = Type.GetType("umbraco.presentation.cache.pageRefresher, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null"); + // } + // watch.Stop(); + // Debug.WriteLine("TOTAL TIME (1st round): " + watch.ElapsedMilliseconds); + // watch.Reset(); + // watch.Start(); + // for (var i = 0; i < 1000; i++) + // { + // var type2 = Assembly.Load("umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null") + // .GetType("umbraco.macroCacheRefresh"); + // var type3 = Assembly.Load("umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null") + // .GetType("umbraco.templateCacheRefresh"); + // var type4 = Assembly.Load("umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null") + // .GetType("umbraco.presentation.cache.MediaLibraryRefreshers"); + // var type5 = Assembly.Load("umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null") + // .GetType("umbraco.presentation.cache.pageRefresher"); + // } + // watch.Stop(); + // Debug.WriteLine("TOTAL TIME (2nd round): " + watch.ElapsedMilliseconds); + // watch.Reset(); + // watch.Start(); + // for (var i = 0; i < 1000; i++) + // { + // var type2 = BuildManager.GetType("umbraco.macroCacheRefresh, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null", true); + // var type3 = BuildManager.GetType("umbraco.templateCacheRefresh, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null", true); + // var type4 = BuildManager.GetType("umbraco.presentation.cache.MediaLibraryRefreshers, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null", true); + // var type5 = BuildManager.GetType("umbraco.presentation.cache.pageRefresher, umbraco, Version=1.0.4698.259, Culture=neutral, PublicKeyToken=null", true); + // } + // watch.Stop(); + // Debug.WriteLine("TOTAL TIME (1st round): " + watch.ElapsedMilliseconds); + //} + [Test] public void Create_Cached_Plugin_File() { var types = new[] {typeof (PluginManager), typeof (PluginManagerTests), typeof (UmbracoContext)}; - var manager = new PluginManager(); + var manager = new PluginManager(false); //yes this is silly, none of these types inherit from string, but this is just to test the xml file format manager.UpdateCachedPluginsFile(types); var plugins = manager.TryGetCachedPluginsFromFile(); Assert.IsTrue(plugins.Success); Assert.AreEqual(3, plugins.Result.Count()); - var shouldContain = types.Select(x => new System.Tuple(x.FullName, x.Assembly.FullName)); + var shouldContain = types.Select(x => x.AssemblyQualifiedName); //ensure they are all found Assert.IsTrue(plugins.Result.ContainsAll(shouldContain)); } diff --git a/src/Umbraco.Tests/Resolvers/ActionsResolverTests.cs b/src/Umbraco.Tests/Resolvers/ActionsResolverTests.cs index d9bd05c7ce..34b834753b 100644 --- a/src/Umbraco.Tests/Resolvers/ActionsResolverTests.cs +++ b/src/Umbraco.Tests/Resolvers/ActionsResolverTests.cs @@ -18,7 +18,7 @@ namespace Umbraco.Tests.Resolvers TestHelper.SetupLog4NetForTests(); //this ensures its reset - PluginManager.Current = new PluginManager(); + PluginManager.Current = new PluginManager(false); //for testing, we'll specify which assemblies are scanned for the PluginTypeResolver PluginManager.Current.AssembliesToScan = new[] diff --git a/src/Umbraco.Tests/Resolvers/MacroFieldEditorsResolverTests.cs b/src/Umbraco.Tests/Resolvers/MacroFieldEditorsResolverTests.cs index eff9f82b53..6f7ae14ce9 100644 --- a/src/Umbraco.Tests/Resolvers/MacroFieldEditorsResolverTests.cs +++ b/src/Umbraco.Tests/Resolvers/MacroFieldEditorsResolverTests.cs @@ -18,7 +18,7 @@ namespace Umbraco.Tests.Resolvers TestHelper.SetupLog4NetForTests(); //this ensures its reset - PluginManager.Current = new PluginManager(); + PluginManager.Current = new PluginManager(false); //for testing, we'll specify which assemblies are scanned for the PluginTypeResolver PluginManager.Current.AssembliesToScan = new[] diff --git a/src/Umbraco.Tests/Resolvers/PackageActionsResolverTests.cs b/src/Umbraco.Tests/Resolvers/PackageActionsResolverTests.cs index b126fde180..a12890a36f 100644 --- a/src/Umbraco.Tests/Resolvers/PackageActionsResolverTests.cs +++ b/src/Umbraco.Tests/Resolvers/PackageActionsResolverTests.cs @@ -18,7 +18,7 @@ namespace Umbraco.Tests.Resolvers TestHelper.SetupLog4NetForTests(); //this ensures its reset - PluginManager.Current = new PluginManager(); + PluginManager.Current = new PluginManager(false); //for testing, we'll specify which assemblies are scanned for the PluginTypeResolver PluginManager.Current.AssembliesToScan = new[] diff --git a/src/Umbraco.Web/UmbracoContext.cs b/src/Umbraco.Web/UmbracoContext.cs index caa62575a9..20ed9fdd1e 100644 --- a/src/Umbraco.Web/UmbracoContext.cs +++ b/src/Umbraco.Web/UmbracoContext.cs @@ -47,6 +47,8 @@ namespace Umbraco.Web if (httpContext == null) throw new ArgumentNullException("httpContext"); if (applicationContext == null) throw new ArgumentNullException("applicationContext"); + ObjectCreated = DateTime.Now; + HttpContext = httpContext; Application = applicationContext; RoutesCache = routesCache; @@ -99,6 +101,13 @@ namespace Umbraco.Web } } + /// + /// This is used internally for performance calculations, the ObjectCreated DateTime is set as soon as this + /// object is instantiated which in the web site is created during the BeginRequest phase. + /// We can then determine complete rendering time from that. + /// + internal DateTime ObjectCreated { get; private set; } + /// /// Gets the current ApplicationContext /// diff --git a/src/Umbraco.Web/UmbracoModule.cs b/src/Umbraco.Web/UmbracoModule.cs index 37125b8b4d..cad2639599 100644 --- a/src/Umbraco.Web/UmbracoModule.cs +++ b/src/Umbraco.Web/UmbracoModule.cs @@ -425,8 +425,12 @@ namespace Umbraco.Web app.EndRequest += (sender, args) => { var httpContext = ((HttpApplication)sender).Context; - //write the trace output for diagnostics at the end of the request - httpContext.Trace.Write("UmbracoModule", "Umbraco request completed"); + if (UmbracoContext.Current != null && UmbracoContext.Current.IsFrontEndUmbracoRequest) + { + //write the trace output for diagnostics at the end of the request + httpContext.Trace.Write("UmbracoModule", "Umbraco request completed"); + LogHelper.Debug("Total milliseconds for umbraco request to process: " + DateTime.Now.Subtract(UmbracoContext.Current.ObjectCreated).TotalMilliseconds); + } }; }