Experience Sitecore ! | All posts tagged 'Pipelines'

Experience Sitecore !

More than 300 articles about the best DXP by Martin Miles

Using Pipeline Profiler

What is a Pipeline Profiler?

Firstly, let's answer what a pipeline is. A pipeline in Sitecore is essentially an ordered sequence of processes (called processors) that run to perform a specific task in the system. Sitecore’s pipeline profiler is a built-in diagnostic tool that tracks how long each pipeline and processor takes to execute. 

Why is this useful? In complex Sitecore solutions, it can be difficult to know what exactly is happening inside the pipelines at runtime, especially after deploying to a cloud or container environment. Pipeline profiling helps answering questions like: "Is my custom pipeline processor actually running, and in the correct order?" or "Which part of the request pipeline is slowing down page loads?". In other words, it provides visibility into the inner workings and performance of Sitecore’s processing framework.

By default, pipeline profiling is disabled for performance reasons, but when enabled, it records metrics like execution counts and timings for each pipeline processor. This data is exposed via an admin page so you can see which pipelines or steps are consuming the most time. 

Why and When to Enable Pipeline Profiling

You should consider enabling pipeline profiling only when you need to diagnose or debug pipeline behavior, especially for diagnosing bottlenecks and verifying pipeline behavior in real scenarios. It’s not something to leave on all the time (more on that in considerations below), but it’s invaluable as a temporary diagnostic tool during development, testing, or troubleshooting.

How to Enable Pipeline Profiling

You just toggle it via a config patch or environment setting.

<add key="env:define" value="Profiling" />

This above env:define setting named "Profiling" acts as a flag that enables the pipeline profiling configuration. Use the full configuration patch file, as below to enable the profiler:

<?xml version="1.0"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
  <sitecore>
    <settings>
      <!-- Enable pipeline profiling -->
      <setting name="Pipelines.Profiling.Enabled">
        <patch:attribute name="value">true</patch:attribute>
      </setting>
      <!-- (Optional) Enable CPU time measurements for profiling -->
      <setting name="Pipelines.Profiling.MeasureCpuTime">
        <patch:attribute name="value">false</patch:attribute>
      </setting>
    </settings>
  </sitecore>
</configuration>

 

How to access the Pipeline Profiler page

After enabling profiling and restarting, log in to Sitecore and navigate to the special admin page on your CM instance URL: /sitecore/admin/pipelines.aspx. You must be logged in as an administrator to access it, like you do for other admin pages.

You should see a table of all pipelines that have executed since startup:

There's a Refresh button on the page; clicking it will update the stats to include any pipelines run since the last refresh. You can filter by pipeline name or simply scroll to see various pipelines, such as initialize, renderLayout, item:saved, publishItem, etc. Each pipeline row can be expanded to show all the processors within it, in the order they execute. The key columns to pay attention to are:

  • #Executions: how many times that pipeline or processor has run since the last reset. For example, after a site startup, you might see the initialize pipeline ran once, whereas something else runs for every HTTP request, so it could be, say, 50 if you've made 50 page requests so far.
  • Wall Time / Time per Execution: the total time spent in that pipeline or processor, and the average time for a single execution. It is particularly useful for spotting slow components; if a single run of a processor takes, say, 500ms on average, that’s a potential performance hot-spot to investigate.

  • % Wall Time: the percentage of the pipeline’s total time that a given processor accounted for. This helps identify which processor within a pipeline is the major contributor to the total time. For example, if one processor is 90% of the pipeline’s execution time, that’s your primary suspect.
  • Max Wall Time: the longest single execution time observed for that pipeline or processor. This is useful to identify spikes or inconsistent performance. If the max time is significantly higher than the average, it means one of the executions had an outlier slow run.

Remember to Remove/Undo the Config

If you added a config patch or environment variable for profiling, make sure to remove or disable it after you're done. For example, in XM Cloud SaaS, that means taking it out of your code (and redeploying) or toggling off the environment variable in your cloud portal settings. This prevents someone from accidentally leaving it on. It also ensures your deployments are clean and you don't want to carry a debugging setting to higher environments unknowingly.

Enabling pipeline profiling in XM Cloud is straightforward and gives Sitecore developers a powerful lens to observe the inner workings of the platform. Whether you’re chasing down a production slowdown or ensuring your custom pipelines run as expected, this feature can save you countless hours. As always, use it wisely!

Creating XML Sitemap for the Helix solution

I am working on a solution that already has HTML sitemap as a part of Navigation feature. Now I got a request to add also a basic XML sitemap with common set requirements. Habitat ships with an interface template _Navigable, so let's extend this template by adding a checkbox field called

ShowInSitemap, stating whether a particular page will be shown in that sitemap:


In order to start, we need to create a handler. Having handlers in web.config is not the desired way of doing things, it will require also doing configuration transform for the deployments, so let's do things in a Sitecore way (Feature.Navigation.config file):

<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
    <sitecore>
        <pipelines>
            <httpRequestBegin>
                <processor type="Platform.Feature.Navigation.Pipelines.SitemapHandler, Platform.Feature.Navigation"
                           patch:before="processor[@type='Sitecore.Pipelines.HttpRequest.CustomHandlers, Sitecore.Kernel']">
                </processor>
            </httpRequestBegin>
            <preprocessRequest>
                <processor type="Sitecore.Pipelines.PreprocessRequest.FilterUrlExtensions, Sitecore.Kernel">
                    <param desc="Allowed extensions">aspx, ashx, asmx, xml</param>
                </processor>
            </preprocessRequest>
        </pipelines>
    </sitecore>
</configuration>

We rely on httpRequestBegin pipeline and incline our new SitemapHandler from Navigation feature right before CustomHandlers processor.

SitemapHandler is an ordinary pipeline processor for httpRequestBegin pipeline, so is inherited from HttpRequestProcessor:

    public class SitemapHandler : HttpRequestProcessor
    {
        const string sitemapHandler = "sitemap.xml";

        private readonly INavigationRepository _navigationRepository;

        public SitemapHandler()
        {
            _navigationRepository = new NavigationRepository(RootItem);
        }

        public override void Process(HttpRequestArgs args)
        {
            if (Context.Site == null 
                || args == null
                || string.IsNullOrEmpty(Context.Site.RootPath.Trim()) 
                || Context.Page.FilePath.Length > 0 
                || !args.Url.FilePath.Contains(sitemapHandler))
            {
                return;
            }

            Response.ClearHeaders();
            Response.ClearContent();
            Response.ContentType = "text/xml";

            try
            {
                var navigationItems = _navigationRepository.GetSitemapItems(RootItem);
                string xml = new XmlSitemapService().BuildSitemapXML(flatItems);

                Response.Write(xml);
            }
            finally
            {
                Response.Flush();
                Response.End();
            }
        }

        private Item RootItem => Context.Site.GetRootItem();

        private HttpResponse Response => HttpContext.Current.Response;
    }

And XmlSitemapService code below:

    public class XmlSitemapService
    {
        public string CreateSitemapXml(IEnumerable<NavigationItem> items)
        {
            var doc = new XmlDocument();

            var declarationNode = doc.CreateXmlDeclaration("1.0", "UTF-8", null);
            doc.AppendChild(declarationNode);

            var urlsetNode = doc.CreateElement("urlset");

            var xmlnsAttr = doc.CreateAttribute("xmlns");
            xmlnsAttr.Value = "http://www.sitemaps.org/schemas/sitemap/0.9";
            urlsetNode.Attributes.Append(xmlnsAttr);
            doc.AppendChild(urlsetNode);

            foreach (NavigationItem itm in items)
            {
                doc = CreateSitemapRecord(doc, itm);
            }
            return doc.OuterXml;
        }

        private XmlDocument CreateSitemapRecord(XmlDocument doc, NavigationItem item)
        {
            string link = item.Url;

            string lastModified = HttpUtility
             .HtmlEncode(item.Item.Statistics.Updated.ToString("yyyy-MM-ddTHH:mm:sszzz"));

            XmlNode urlsetNode = doc.LastChild;

            XmlNode url = doc.CreateElement("url");
            urlsetNode.AppendChild(url);

            XmlNode loc = doc.CreateElement("loc");
            url.AppendChild(loc);
            loc.AppendChild(doc.CreateTextNode(link));

            XmlNode lastmod = doc.CreateElement("lastmod");
            url.AppendChild(lastmod);
            lastmod.AppendChild(doc.CreateTextNode(lastModified));

            return doc;
        }
    }
Also, NavigationItem is a custom POCO:
 
public class NavigationItem
{
    public Item Item { get; set; }
    public string Title { get; set; }
    public string Url { get; set; }
    public bool IsActive { get; set; }
    public int Level { get; set; }
    public NavigationItems Children { get; set; }
    public string Target { get; set; }
    public bool ShowChildren { get; set; }
}


Few things to mention.
1. Since you are using LinkManager in order to generate the links, you need to make sure you have full URL path as required by protocol, not the site-root-relative path. So you'll need to pass custom options in that case:

2. Once deployed to production, you may face an unpleasant behavior of HTTPS links generated along with 443 port number (such as . That is thanks to LinkManager not being wise enough to predict such a case. However there is a setting that make LinkManager works as expected. Not obvious
var options = LinkManager.GetDefaultUrlOptions();
options.AlwaysIncludeServerUrl = true;
options.SiteResolving = true;
LinkManager.GetItemUrl(item, options);

or better option in Heliix to rely on Sitecore.Foundation.SitecoreExtensions:

item.Url(options) from


//TODO: Update the code with the recent



That's it!

Wildcard items ("*"-pages) with MVC, passing the correct datasources based on requested item URL

As you probably know, there is a feature in Sitecore, called "wildcard items". Let's say you have a folder where multiple subitems suppose to be and should be presented by similar pages. Sitecore allows you create a wildcard item named "*" (asterisk) so that it will serve any request to this folder. Let's take a look on example below:


Here we see clear separation of pages and data. Airports page has the only child - wildcard page item that is highlighted (and you see its details). Below, in /Data folder, there is a corresponding data sources for each of the airport.

In usual scenario, there should be a page item created for each of airports from /data folder, and page's presentation details screen should have that data items set as the datasource for corresponding rendering (yes, we are on MVC here). But how to have this working, if we have only one universal page item? We have a rendering called Airport to display airport info, but how should we specify the datasource to it?


The rendering relies on RenderingContext.Current.Rendering.DataSource as usual. And instead of datasource of specific airport, we get "*" item as the datasource in all cases, regardless of what airport's page we're loading.

I wanted to leave page's datasource and rendering untouched, as normal implementation. Instead, I decided to incline into mvc.getRenderer pipeline and resolve datasources for "*"-wildcard-items dynamically, based on the URL requested, so that rendering get corresponding data item from RenderingContext.Current.Rendering.DataSource. And of course, as "*"-items serves all possible requests to its folder, I must provide datasources only for matching data items, redirecting all the rest to 404 error page (since there's no data for the page - there's no sense in such a page).

So here's my implementation of wildcard datasource resolver:







Airport
        
      
    
  

Code referenced from configuration patch file:

    public class WildcardDatasource : GetRendererProcessor
    {
        public string RenderingName { get; set; }

        public override void Process(GetRendererArgs args)
        {
            if (args.PageContext.Item.Name == "*" && args.Rendering.RenderingItem.Name.IsSameAs(RenderingName))
            {
                string datasourceFolder = args.Rendering.DataSource.IsOK()
                    ? args.Rendering.DataSource
                    : string.Format("{0}/{1}", Paths.DataFolder.TrimEnd('/'), args.PageContext.Item.Parent.Name);

                string dataSourcePath = string.Format("{0}/{1}", datasourceFolder, 
                    args.PageContext.RequestContext.HttpContext.Request.Url.Segments.Last());

                var dataSourceItem = Sitecore.Context.Database.GetItem(dataSourcePath);
                if (dataSourceItem != null)
                {
                    args.Rendering.DataSource = dataSourcePath;
                }
                else
                {
                    // process 404 page not found
                }
            }
        }
    }


Things to improve in future:

  1. Add support for nested wildcards
  2. Find the way wildcard items work with Page Editor