Tuchka.Dnn.SelfPinger

Recently I was thinking how to overcome a first-hit slowness problem of a website.

In many of true large production environments with the custom built applications you can have the application precompiled, running on an always-on application pool and first hit will happen infrequently. But even then it will still take time to launch the application or load / combine all resources together to show every single page.

This problem is even more [annoying] in a shared hosting environment with an application that does not support precompilation. This was the case in my situation too. Having installed the very powerful DotNetNuke CMS in a not so fast environment, I observed pretty much the same behavior - application is restarted 5-10 times a week and every first hit on a page takes time to process.

The solution I came up with is a DotNetNuke Scheduler task that runs after the application starts and crawls all the pages it can and loads all modules that are used in 2 phases:

  1. It locates all Modules that are used by DNN in any of the Portals and loads all controls (User Controls) associated with those modules. Once they are loaded, .Net will not have to load classes associated with them into the Application Domain and generate intermediate dynamic classes  any more. As a result, the Tab using those Modules will load fast even on a first hit.

    However, this approach does not let loading User Controls that are used by Skin Objects as there is nothing that lets tracking them, so those would still need to load on a first hit.

    Alternative to the #1 can be loading just all User Controls that are there in the Application and so I have created a method LoadAllControls(), but I am not actively using it as it loads all User Controls, including those that are not used, thus unnecessarily using resources.

    This method can be effectively used in some other websites that do not have unused controls, or have lots of resources. At the same time it can also be used in any .Net application to pre-load all User Controls.

  2. For every website, it tries to locate an Http Alias that it can use to reach out to the website. As some of the DotNetNuke deployments cannot connect to the internet or self using an fqdn, you might need to a new Http Alias that would reference the website as localhost{/virtaulPath}.

    After getting a working alias, it attempts to ping (send http GET request) to every Tab of a website, forcing .Net to compile the page, loading Skins, Skin Objects and any other controls missed by #1.

    However, many pages on the website might require authorization and the scheduling task does not log in, so it can crawl only public pages (visible for All Users).
Pinger is compatible and works fine for me with DotNetNuke 5, however as DNN tries to process the jobs associated with APPLICATION_START even synchronously, attempting to run Pinger at APPLICATION_START event will result in a halt, that is why you should let it run only on some time interval basis - every 5 - 10 minutes. Once DotNetNuke application is started, and Pinger is triggered the first time, it will do its work and note that it has already ran. All subsequent job executions Pinger will ignore until the application restarts.

Please keep all of the above in mind if using the Pinger as many Modules / Controls can have various functionality triggered when they get loaded and you might not want it executed by the Pinger.

WARNING: Pinger is provided AS IS. Use it at your own risk. The author or tuchka.info are not responsible for any damage being direct or indirect result of using this module.

NOTE: You may use / modify the source code provided below keeping the reference to the original version at www.tuchka.info.

Downloads

 TitleModified DateSize Clicks
Tuchka.Dnn.SelfPinger.zip7/31/20104.98 KBDownload1570

Configuration

To install and configure the module, please follow the following steps:

1. Download the archive with Tuchka.Dnn.SelfPinger.dll and unpack it.
2. Copy Tuchka.Dnn.SelfPinger.dll to your website /bin folder.
3. Login as Host account and go to Host -> Schedule
4. Create a new schedule:
  • Friendly Name: Dnn Self Pinger (tuchka.info)
  • Full Class Name and Assembly: Tuchka.Dnn.SelfPinger.PingerClient, Tuchka.Dnn.SelfPinger
  • Schedule Enabled: Yes
  • Time Lapse: 5-10 Minutes
  • Retry Frequency 10-20 Minutes
  • Retain Schedule History: 1
  • Run On Event: None (Important!)
  • Catch Up Enabled: No
  • Dependencies: Empty


WARNING: Do not select any event in 'Run on Event' drop down.

You might also try setting it to run every 5 minutes, but keep in mind that it still logs few lines when running even if there is nothing to do, so if you decide to run it too frequently, it can log too much useless info.

5. Click Update to Save configuration.

The job will run every 10 minutes, that means that within 10 minutes after the application starts, it will pre-load pages and controls.

Source Code

///
/// Tuchka.Dnn.SelfPinger - DotNetNuke scheduler client that pings public tabs and loads used Module's User Controls
/// This greatly speeds up the DotNetNuke site as everything is loaded and there is no first hit penalty.
/// 
/// The job runs on a schedule every 5 - 10 minutes, but effectively works only the first time after application restart.
/// All other times it will do nothing as all controls / pages are already in cache.
/// 
/// Warning:
/// As DNN runs APPLICATION_START event synchronously, you cannot have the job run on application start.
/// If you try to do so, your web site will no longer work and you will have to disable the task through the DataBase
/// Schedule table.
/// 
/// Warning:
/// As some controls / pages can do various things at load, you might not want them to be hit / loaded. 
/// Do not use this module in this case.
///
/// Warning:
/// The module is provided AS IS. Use it at your own risk. The author or tuchka.info are not responsible for any damage being 
/// direct or indirect result of using this module.
/// 
/// Note:
/// You may use / modify the code keeping the reference to the original version at www.tuchka.info.
///


using System;
using System.Collections.Generic;
using System.Collections;
using System.Linq;
using System.Text;
using DotNetNuke.Entities.Portals;
using DotNetNuke.Entities.Tabs;
using DotNetNuke.Entities.Modules;
using DotNetNuke.Entities.Modules.Definitions;
using DotNetNuke.Services.Scheduling;

namespace Tuchka.Dnn.SelfPinger
{
    public class PingerClient : SchedulerClient
    {
        public PingerClient(ScheduleHistoryItem objScheduleHistoryItem)
            : base()
        {
            this.ScheduleHistoryItem = objScheduleHistoryItem;
        }

        /// <summary>
        /// Flag to check if the job has already executed.
        /// </summary>
        private static bool alreadyRan = false;

        /// <summary>
        /// Syncroot to make sure only one job runs
        /// </summary>
        private static object syncRoot = new object();

        /// <summary>
        /// Main Scheduling Client Job method
        /// </summary>
        public override void DoWork()
        {
            try
            {
                lock (syncRoot)
                {
                    if (!alreadyRan)
                    {
                        alreadyRan = true;

                        // Loading controls which belong to Modules that are used by the site.
                        // Does not load User Controls for SkinObjects. To load those as well, consider using method below.
                        LoadUsedControls();

                        // Uncommenting this call will load All controls regardless of whether they are used on website or not
                        // But might be not very efficient to load those controls which are not needed.
                        //LoadAllControls();

                        // Pinging public tabs
                        PingTabs();
                        
                        LogMessage("Pinging job completed. ");
                    }
                    else
                    {
                        LogMessage("Ping job already ran at this start.");
                    }

                    this.ScheduleHistoryItem.Succeeded = true;
                }
            }
            catch (Exception exc)
            {
                this.ScheduleHistoryItem.Succeeded = false;
                LogMessage("Pinging job failed: " + exc.Message);
                this.Errored(ref exc);
            }
        }

        /// <summary>
        /// Loads all User Controls for those Modules that are used on any Portal
        /// </summary>
        private void LoadUsedControls()
        {
            try
            {
                LogMessage("Starting used UserControl pre caching");

                ArrayList portals = new PortalController().GetPortals();

                System.Web.UI.Page page = new System.Web.UI.Page();

                foreach (ModuleDefinitionInfo mdi in DotNetNuke.Entities.Modules.Definitions.ModuleDefinitionController.GetModuleDefinitions().Values)
                {
                    LogMessage("Researching module " + mdi.FriendlyName);
                    bool used = false;
                    foreach (PortalInfo pi in portals)
                    {
                        var modules = new ModuleController().GetModulesByDefinition(pi.PortalID, mdi.FriendlyName);
                        used = (modules != null && modules.Count > 0);
                        if (used) break;
                    }

                    if (used)
                    {
                        LogMessage("Used. Loading its controls.");
                        foreach (var mc in mdi.ModuleControls.Values)
                        {
                            try
                            {
                                string url = mc.ControlSrc;
                                LogMessage("Loading control " + url);
                                page.LoadControl(url);
                            }
                            catch (Exception ex)
                            {
                                LogMessage("Error loading control: " + ex.Message);
                            }
                        }
                    }
                    else
                        LogMessage("Not used.");
                }

            }

            catch (Exception ex)
            {
                LogMessage(string.Format("Preload all used controls cycle failed: " + ex.Message));
            }
        }

        /// <summary>
        /// Loads all User Control within the .Net Application Root.
        /// </summary>
        private void LoadAllControls()
        {
            try
            {
                LogMessage("Starting UserControl pre caching");

                string root = System.Web.HttpRuntime.AppDomainAppPath;
                string virtualRoot = System.Web.HttpRuntime.AppDomainAppVirtualPath + "/";

                LogMessage("Root retrieved: " + root);
                LogMessage("Virtual Root retrieved: " + virtualRoot);

                System.Web.UI.Page page = new System.Web.UI.Page();

                foreach (var file in System.IO.Directory.GetFiles(root, "*.ascx", System.IO.SearchOption.AllDirectories))
                {
                    try
                    {
                        string virtualFile = file.Replace(root, virtualRoot).Replace(@"\","/");
                        LogMessage("Loading control " + virtualFile);
                        page.Controls.Add(page.LoadControl(virtualFile));
                    }
                    catch (Exception ex)
                    {
                        LogMessage("Error loading control: " + ex.Message);
                    }
                }
            }
            catch (Exception ex)
            {
                LogMessage(string.Format("Preload all controls cycle failed: " + ex.Message));
            }
        }

        /// <summary>
        /// Locate the usable Http Alias for every Portal and try to hit every Tab on every Portal
        /// </summary>
        private void PingTabs()
        {
            try
            {
                LogMessage("Starting pinging");

                ArrayList portals = new PortalController().GetPortals();
                foreach (PortalInfo pi in portals)
                {
                    var ps = new PortalSettings(pi.PortalID);

                    string alias = "";
                    if ((alias = FindAlias(pi, ps)) == null)
                    {
                        LogMessage(string.Format("Cannot find working alias for portal {0}", pi.PortalID));
                        continue;
                    }

                    var tabs = DotNetNuke.Entities.Tabs.TabController.GetPortalTabs(pi.PortalID, 0, true, true);
                    tabs.ForEach((TabInfo tab) =>
                            PingTab(ps, tab, alias, 10 * 1000)); // We don't really care if we get response or not, so the timeout is small. Page will be compiled / loaded regardless.
                }
            }
            catch (Exception ex)
            {
                LogMessage("Error pinging tabs: " + ex.Message);
            }

        }

        /// <summary>
        /// Tries to locate a working alias for the given Portal
        /// </summary>
        /// <param name="pi"></param>
        /// <param name="ps"></param>
        /// <returns></returns>
        private string FindAlias(PortalInfo pi, PortalSettings ps)
        {
            var homeTab = new TabController().GetTab(ps.HomeTabId, pi.PortalID, true);

            foreach (DictionaryEntry de in new PortalAliasController().GetPortalAliasByPortalID(pi.PortalID))
            {
                DotNetNuke.Entities.Portals.PortalAliasInfo pai = (DotNetNuke.Entities.Portals.PortalAliasInfo)de.Value;
                LogMessage(string.Format("Testing alias '{0}'", pai.HTTPAlias));
                if (PingTab(ps, homeTab, pai.HTTPAlias, 3*60 * 1000))
                {
                    LogMessage(string.Format("Found working alias: {0}", pai.HTTPAlias));
                    return pai.HTTPAlias;
                }
            }

            return null;
        }

        /// <summary>
        /// Sends a request for a Tab. This causes .Net to compile the page and load all User Controls for it
        /// </summary>
        /// <param name="ps"></param>
        /// <param name="ti"></param>
        /// <param name="siteAlias"></param>
        /// <param name="millisecondTimeout"></param>
        /// <returns></returns>
        private bool PingTab(PortalSettings ps, TabInfo ti, string siteAlias, int millisecondTimeout)
        {
            try
            {
                string protocol = "http";
                if (ps.SSLEnabled && ti.IsSecure)
                    protocol = "https";

                string url = "{0}://{1}/default.aspx?tabid={2}";

                url = string.Format(url, protocol, siteAlias, ti.TabID);

                SendRequest(url, millisecondTimeout);

                return true;
            }
            catch (Exception ex)
            {
                LogMessage("Error pinging tab: " + ex.Message);
                return false;
            }
        }

        /// <summary>
        /// Sends direct Http request
        /// </summary>
        /// <param name="url"></param>
        /// <param name="timeoutMilleseconds"></param>
        private void SendRequest(string url, int timeoutMilleseconds)
        {
            try
            {
                LogMessage("Requesting url " + url);

                System.Net.HttpWebRequest request = (System.Net.HttpWebRequest)System.Net.HttpWebRequest.Create(url);
                request.Timeout = timeoutMilleseconds;

                System.Net.HttpWebResponse response = (System.Net.HttpWebResponse)request.GetResponse();
                if (response.StatusCode == System.Net.HttpStatusCode.OK)
                    LogMessage("Request succeeded");
                else
                    throw new ApplicationException(string.Format("Request result unknown. Return Code: {0} - {1}.", response.StatusCode, response.StatusCode.ToString()));

            }
            catch (Exception ex)
            {
                LogMessage("Ping of url " + url + " failed: " + ex.Message);
                throw ex;
            }
        }

        /// <summary>
        /// Log a message
        /// </summary>
        /// <param name="message"></param>
        private void LogMessage(string message)
        {
            this.ScheduleHistoryItem.AddLogNote(string.Format("{0:yyyy/MM/dd HH:mm:ss}: {1}<br />", DateTime.Now, message));
        }
    }
}
K.K. 31 Jul 2010 
Share

Comments

Comments

Indicates required fields

Your Contact Information

Your Feedback


Terms Of Use | Privacy Statement | (c) 2010 tuchka.info