1
votes

I have an MVC site and we are using MVCSiteMapProvider 4.4.3 with Autofac. We construct our site using a mixture of XML and Attributes. We have a few hundred dynamic nodes and we have security trimming enabled. Site has been getting larger over the last year with approximately 120 controllers. All controllers are secured using authorize attributes that vary per role etc.

In our layout we call @Html.MvcSiteMap().SiteMapPath() this adds approximately 950ms onto page load time. If we remove the line our pages load almost instantly.

Our menu used to take a second to load - however we put it in a RenderAction and simply cached the result which largely fixed that issue.

Is this common performance? Is there any obvious way to enhance performance for SiteMapPath or things that may cause the performance to be so poor

If we reload the page it takes as long the second time as the first

Just browsing about ten pages and profiling but about 70% of CPU cyles seem to have gone to:

MvcSiteMapProvider.Caching.RequestCache.GetValue(String)
MvcSiteMapProvider.RequestCacheableSiteMapNode.GetCacheKey(String)
MvcSiteMapProvider.Collections.Specialized.RouteValueDictionary.GetCacheKey()
MvcSiteMapProvider.RequestCacheableSiteMap.GetCacheKey(String)
MvcSiteMapProvider.Web.Mvc.MvcContextFactory.CreateHttpContext(ISiteMapNode)
MvcSiteMapProvider.RequestCacheableSiteMapNode.get_AreRouteParametersPreserved()
MvcSiteMapProvider.SiteMap.GetChildNodes(ISiteMapNode)
MvcSiteMapProvider.SiteMap.FindSiteMapNodeFromControllerAction(ISiteMapNode, IDictionary[StringObject], RoutMvcSiteMapProvider.Collections.CacheableDictionary`2.ContainsKey(TKey)
eBase)
MvcSiteMapProvider.RequestCacheableSiteMap.IsAccessibleToUser(ISiteMapNode)
MvcSiteMapProvider.Collections.CacheableDictionary`2.get_ReadOperationDictionary()

The total calls to the MVCSiteMapProvder Namespace were 434 million vs 1 million for all of our own code Namespaces.

Our Autofac module is:

    public class MvcSiteMapProviderModule : global::Autofac.Module
    {
        protected override void Load(ContainerBuilder builder)
        {
            const bool SecurityTrimmingEnabled = false;
            const bool EnableLocalization = false;
            var absoluteFileName = HostingEnvironment.MapPath("~/Mvc.sitemap");
            var absoluteCacheExpiration = TimeSpan.FromMinutes(60);

            var includeAssembliesForScan = new[] { "OnboardWeb" };

            var currentAssembly = this.GetType().Assembly;
            var siteMapProviderAssembly = typeof(SiteMaps).Assembly;
            var allAssemblies = new Assembly[] { currentAssembly, siteMapProviderAssembly };
            var excludeTypes = new Type[] 
            {
                typeof(SiteMapNodeVisibilityProviderStrategy),
                typeof(SiteMapXmlReservedAttributeNameProvider),
                typeof(SiteMapBuilderSetStrategy)
            };
            var multipleImplementationTypes = new Type[] 
            {
                typeof(ISiteMapNodeUrlResolver),
                typeof(ISiteMapNodeVisibilityProvider),
                typeof(IDynamicNodeProvider)
            };

            // Single implementations of interface with matching name (minus the "I").
            CommonConventions.RegisterDefaultConventions(
                (interfaceType, implementationType) => builder.RegisterType(implementationType).As(interfaceType).SingleInstance(),
                new Assembly[] { siteMapProviderAssembly },
                allAssemblies,
                excludeTypes,
                string.Empty);


            // Multiple implementations of strategy based extension points
            CommonConventions.RegisterAllImplementationsOfInterface(
                (interfaceType, implementationType) => builder.RegisterType(implementationType).As(interfaceType).SingleInstance(),
                multipleImplementationTypes,
                allAssemblies,
                excludeTypes,
                "^Composite");

            // Registration of internal controllers
            CommonConventions.RegisterAllImplementationsOfInterface(
                (interfaceType, implementationType) => builder.RegisterType(implementationType).As(interfaceType).AsSelf().InstancePerDependency(),
                new Type[] { typeof(IController) },
                new Assembly[] { siteMapProviderAssembly },
                new Type[0],
                string.Empty);

            // Visibility Providers
            builder.RegisterType<SiteMapNodeVisibilityProviderStrategy>()
                .As<ISiteMapNodeVisibilityProviderStrategy>()
                .WithParameter("defaultProviderName", string.Empty);
            //.WithParameter("defaultProviderName", "MvcSiteMapProvider.FilteredSiteMapNodeVisibilityProvider, MvcSiteMapProvider");

            //builder.RegisterType<BreadCrumbOnlyVisibilityProvider>()
            //    .As<ISiteMapNodeVisibilityProvider>().InstancePerLifetimeScope();

            // Pass in the global controllerBuilder reference
            builder.RegisterInstance(ControllerBuilder.Current)
                   .As<ControllerBuilder>();

            builder.RegisterType<BuildManagerAdaptor>()
                   .As<IBuildManager>();

            builder.RegisterType<ControllerBuilderAdaptor>()
                   .As<IControllerBuilder>();

            builder.RegisterType<ControllerTypeResolverFactory>()
                .As<IControllerTypeResolverFactory>()
                .WithParameter("areaNamespacesToIgnore", new string[0]);

            // Configure Security
            builder.RegisterType<AuthorizeAttributeAclModule>()
                .Named<IAclModule>("authorizeAttributeAclModule");

            builder.RegisterType<XmlRolesAclModule>()
                .Named<IAclModule>("xmlRolesAclModule");
            builder.RegisterType<CompositeAclModule>()
                .As<IAclModule>()
                .WithParameter(
                    (p, c) => p.Name == "aclModules",
                    (p, c) => new[]
                        {
                            c.ResolveNamed<IAclModule>("authorizeAttributeAclModule"),
                            c.ResolveNamed<IAclModule>("xmlRolesAclModule")
                        });



            builder.RegisterInstance(System.Runtime.Caching.MemoryCache.Default)
                   .As<System.Runtime.Caching.ObjectCache>();

            builder.RegisterGeneric(typeof(RuntimeCacheProvider<>))
                .As(typeof(ICacheProvider<>));

            builder.RegisterType<RuntimeFileCacheDependency>()
                .Named<ICacheDependency>("cacheDependency1")
                .WithParameter("fileName", absoluteFileName);

            builder.RegisterType<CacheDetails>()
                .Named<ICacheDetails>("cacheDetails1")
                .WithParameter("absoluteCacheExpiration", absoluteCacheExpiration)
                .WithParameter("slidingCacheExpiration", TimeSpan.MinValue)
                .WithParameter(
                    (p, c) => p.Name == "cacheDependency",
                    (p, c) => c.ResolveNamed<ICacheDependency>("cacheDependency1"));

            // Configure the visitors
            builder.RegisterType<UrlResolvingSiteMapNodeVisitor>()
                   .As<ISiteMapNodeVisitor>();

            // Prepare for our node providers
            builder.RegisterType<FileXmlSource>()
                .Named<IXmlSource>("xmlSource1")
                .WithParameter("fileName", absoluteFileName);

            builder.RegisterType<SiteMapXmlReservedAttributeNameProvider>()
                .As<ISiteMapXmlReservedAttributeNameProvider>()
                .WithParameter("attributesToIgnore", new string[0]);


            // Register the sitemap node providers
            builder.RegisterType<XmlSiteMapNodeProvider>()
                .Named<ISiteMapNodeProvider>("xmlSiteMapNodeProvider1")
                .WithParameter("includeRootNode", true)
                .WithParameter("useNestedDynamicNodeRecursion", false)
                .WithParameter(
                    (p, c) => p.Name == "xmlSource",
                    (p, c) => c.ResolveNamed<IXmlSource>("xmlSource1"));

            builder.RegisterType<ReflectionSiteMapNodeProvider>()
                .Named<ISiteMapNodeProvider>("reflectionSiteMapNodeProvider1")
                .WithParameter("includeAssemblies", includeAssembliesForScan)
                .WithParameter("excludeAssemblies", new string[0]);

            builder.RegisterType<CompositeSiteMapNodeProvider>()
                .Named<ISiteMapNodeProvider>("siteMapNodeProvider1")
                .WithParameter(
                    (p, c) => p.Name == "siteMapNodeProviders",
                    (p, c) => new[]
                        {
                            c.ResolveNamed<ISiteMapNodeProvider>("xmlSiteMapNodeProvider1"),
                            c.ResolveNamed<ISiteMapNodeProvider>("reflectionSiteMapNodeProvider1")
                        });

            // Register the sitemap builders
            builder.RegisterType<SiteMapBuilder>()
                .Named<ISiteMapBuilder>("siteMapBuilder1")
                .WithParameter(
                    (p, c) => p.Name == "siteMapNodeProvider",
                    (p, c) => c.ResolveNamed<ISiteMapNodeProvider>("siteMapNodeProvider1"));


            // Configure the builder sets
            builder.RegisterType<SiteMapBuilderSet>()
                   .Named<ISiteMapBuilderSet>("builderSet1")
                   .WithParameter("instanceName", "default")
                   .WithParameter("securityTrimmingEnabled", SecurityTrimmingEnabled) 
                   .WithParameter("enableLocalization", EnableLocalization)
                   .WithParameter(
                        (p, c) => p.Name == "siteMapBuilder",
                        (p, c) => c.ResolveNamed<ISiteMapBuilder>("siteMapBuilder1"))
                   .WithParameter(
                        (p, c) => p.Name == "cacheDetails",
                        (p, c) => c.ResolveNamed<ICacheDetails>("cacheDetails1"));

            builder.RegisterType<SiteMapBuilderSetStrategy>()
                .As<ISiteMapBuilderSetStrategy>()
                .WithParameter(
                    (p, c) => p.Name == "siteMapBuilderSets",
                    (p, c) => c.ResolveNamed<IEnumerable<ISiteMapBuilderSet>>("builderSet1"));
        }
    }
}

We have one dynamic node provider adding a few hundred nodes (if we turn it off it is faster but not significantly so)

    public class LocationsDynamicNodeProvider : DynamicNodeProviderBase
    {
        private List<Country> countries;

        /// <summary>
        /// Lazy loading of countries. Only create the graph when we actually need it.
        /// Previously it was in the constructor, but for lightweight object composition we must
        /// not do any work in the constructor.
        /// </summary>
        /// <returns></returns>
        private List<Country> GetCountries()
        {
            if (countries == null)
            {
                var countryRepository = DependencyResolver.Current.GetService<ICountryRepository>();
                countries = countryRepository.AllWithLocations().ToList();
            }

            return countries;
        }


        public override IEnumerable<DynamicNode> GetDynamicNodeCollection(ISiteMapNode node)
        {
            countries = GetCountries();
            foreach (var country in countries)
            {
                var countrynode = new DynamicNode
                                      {
                                          Title = country.Name,
                                          Controller = "Assets",
                                          Action = "Index",
                                          Area = "OnboardAsset",
                                          RouteValues = new RouteValueDictionary
                                                            {
                                                                { "countryname", country.Name }, 
                                                                { "locationname", "" }, 
                                                                { "sitename", "" }
                                                            },
                                          ParentKey = "All Assets",
                                          Key = "countrynode_" + country.CountryId
                                      };

                yield return countrynode;
                foreach (var site in country.Sites)
                {
                    var sitenode = new DynamicNode
                    {
                        Title = site.Name,
                        Controller = "Assets",
                        Action = "Index",
                        Area = "OnboardAsset",
                        RouteValues =
                            new RouteValueDictionary()
                            {
                                { "countryname", country.Name }, 
                                { "sitename", site.Name }, 
                                { "locationname", "" }
                            },
                        ParentKey = "countrynode_" + country.CountryId,
                        Key = "sitenode_" + site.SiteId
                    };


                    yield return sitenode;
                    foreach (var location in site.Locations)
                    {
                        var locationNode = new DynamicNode
                        {
                            Title = location.Name,
                            Controller = "Assets",
                            Action = "Index",
                            Area = "OnboardAsset",
                            RouteValues =
                                new RouteValueDictionary 
                                { 
                                { "countryname", country.Name }, 
                                { "sitename", site.Name }, 
                                { "locationname", location.Name } 
                                },
                            ParentKey = "sitenode_" + site.SiteId
                        };

                        yield return locationNode;
                    }
                }
            }
        }
    }
}

SiteMap config:

<mvcSiteMap xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-4.0" xsi:schemaLocation="http://mvcsitemap.codeplex.com/schemas/MvcSiteMap-File-4.0 MvcSiteMapSchema.xsd">

  <mvcSiteMapNode title="Home" controller="HomePage" action="Index" key="Home">

    <mvcSiteMapNode title="People" key="PeopleTop" controller="People" action="Index" area="OnboardTeam" >
      <mvcSiteMapNode title="All People" key="PeopleIndex" controller="People" action="Index" area="OnboardTeam" visibility="hideChildren" />
    </mvcSiteMapNode>

    <mvcSiteMapNode title="Assets" key="Assets" controller="Home" action="Index" area="OnboardAsset">
      <mvcSiteMapNode title="All Assets" key="All Assets" controller="Assets" action="Index" route="AllAssets">
        <mvcSiteMapNode title="LocationNodes" dynamicNodeProvider="Onboard.Web.Infrastructure.Menu.LocationsDynamicNodeProvider, OnboardWeb" />
      </mvcSiteMapNode>
    </mvcSiteMapNode>

    <mvcSiteMapNode title="Jobs" controller="Jobs" action="Index" area="Core" key="Jobs" visibility="hideChildren" />

    <mvcSiteMapNode title="Reports" key="Report" clickable="false">
      <mvcSiteMapNode title="Certifications" key="Report_Certifications" clickable="false" />
    </mvcSiteMapNode>

    <mvcSiteMapNode title="CRM" controller= "CRM" area="CRM" key="CRM" action="Index">
    </mvcSiteMapNode>

    <mvcSiteMapNode title="PO" key="PO" action="GeneralList" controller= "PurchaseOrders">
      <mvcSiteMapNode title="Purchase Orders" action="GeneralList" controller= "PurchaseOrders" area="PO" key="PO_List" />
    </mvcSiteMapNode>

    <mvcSiteMapNode title="Training" key="OnboardTraining" controller="PersonTrainingBookings" action="Index" />

    <mvcSiteMapNode title="Document Store" key="Documents" area="Documents" controller="DocumentStore" action="Browse" />

    <mvcSiteMapNode title="Admin" key="Admin" roles="Administrator" clickable="false">
      <mvcSiteMapNode title="Competence" key="Competences" area="OnboardTeam" controller="Competences" action="Index" />
      <mvcSiteMapNode title="Certification" key="Certifications" area="OnboardTeam" controller="Certification" action="Index" />
      <mvcSiteMapNode title="Supporting Entities" key="LookupTable" clickable="false" />
      <mvcSiteMapNode title="Entity Types" key="LookupTypes" clickable="false" />
      <mvcSiteMapNode title="Users and Teams" key="UsersAndTeams" area="Core" controller="UserManagement" action="Index" clickable="false" />
      <mvcSiteMapNode title="Companies" key="Organisations" area="Core" controller="Companies" action="Index" clickable="false" />
      <mvcSiteMapNode title="Geographic Data" key="Geographic" area="Core" controller="Countries" action="Index" clickable="false" />
    </mvcSiteMapNode>

  </mvcSiteMapNode>

</mvcSiteMap>

Rest of nodes are adding using attributes on controller actions

We are running in release mode

2
Most of the methods you have posted are specifically to enhance performance by using request caching. Could you post your node configuration (including dynamic node providers) and your Autofac module? Also, how does performance compare if you disable security trimming?NightOwl888
Updated. Just seems a crazy number of lines of code being excuted. Updated with configs etc. Disabling security trimming doesn't make any significant difference.GraemeMiller
The fact that you are getting so many calls is highly unusual, and I suspect it has to do with something in your configuration. You could compare your setup with the MvcMusicStore demo @ GitHub which demonstrates this working with several hundred dynamic nodes and Autofac (with the compilation symbol set for Autofac). But your best bet is to build a small demo project that reproduces the behavior, make it available for download, and open a new issue @ GitHub. You may even figure it out when building the demo.NightOwl888
Is it possible that you get loops etc in your sitemap config or would that just cause StackOverflow?GraemeMiller
How/where is your MvcSiteMapProviderModule being registered? It could be this isn't happening only 1 time per application startup as it was intended to run. If you are merging into an existing DI configuration, you should install the Modules-only package instead of the full one. To downgrade, just run Uninstall-Package MvcSiteMapProvider.MVC4.DI.Autofac (don't remove dependencies) and then follow these instructions: github.com/maartenba/MvcSiteMapProvider/tree/master/src/…NightOwl888

2 Answers

2
votes

I am pretty sure this was caused by issues with our sitemap config/setup. We have been relying on I think a bug in SiteMap that was previosuly preserving route data. However in v4 this was fixed.

Performance issue was caused when we visited actions which had a node attribute placed on but did not contain required preserved route data. Sitemap provider seemed to go a bit nuts I think trying to locate an appropriate node/route.

We are now placing a lot of preserveRouteData throughout code and it fixes the issues. We ideally would like to now create dynamic nodes (as our whole sitemap is based on various major entities e.g. People with lots of child nodes hung off them) to avoid this. However we have issue as we also want to use attributes to add children to dynamic nodes see here

0
votes

If your site is running in debug mode it will cause performance degradation with the html helpers. Note that this is for V3 and V4 should have improved this.

from http://mvcsitemap.codeplex.com/wikipage?title=HtmlHelper%20functions

Known performance issues and the solution A performance degradation may be noticed working with HtmlHelper functions from Visual Studio. This is because during debugging, no caching occurs internally in ASP.NET MVC regarding view rendering. The solution to this is running the application in release mode or changing Web.config to run under release mode:

<compilation debug="false">