Tuesday 21 July 2009

Script and styles optimizer for ASP.NET Part1

Abstract


Since project I'm working on became stable we've started to optimise it. Naturally one of the main aims in site optimisation is to minimize load bandwidth and minimize request count between clients and server. I started digging about zipping static content and combining it. Then I hit on two great post which inspired me to my work. You can find them here and here. Thanks a lot for those posts!

Especially the first one impressed me. In the beginning I wanted to just use it, but then I realized that I need a little more flexible and extensible solution.

My requirements were:
1. Ability to pass css and js files as a single request (per type).
2. Ability to zip content.
3. Ability to cache content.
4. Ability to configure feature from both configuration file and code.
5. Ability to dynamically adding and removing content (both declaratively and imperatively, by removing I mean that it's possible to omit some part of content on particular site.
6. Put embedded scripts (*.axd) into combined and zipped resources as well.
7. Enclose it as reusable and plug-and-play project.

I've decided to follow great idea of Moiz Dhanji (thank you again) and I extended the ScriptManager control. Later I've noticed that it's good to extend ScriptManagerProxy cotrol as well.
Based on all of those requirements and two mentioned blog posts I have created a very useful tool - OptimisedScriptManager. If you are interesting in it or you are looking for similar solution you can download and try it from here.

Using Instructions


1. Add reference to "Cognifide.EPiServerControls.OptimisedScriptManager.dll" in your web application.

2. In web.config (under the section in collection) register optimised control:

<add assembly="Cognifide.EPiServerControls.OptimisedScriptManager" namespace="Cognifide.EPiServerControls.OptimisedScriptManager.UI" tagPrefix="Optimised"/>



3. Now add two GenericHandlers, one will be responsible for css calls, the second one will be responsible for javascripts calls, lets say Css.ashx and Scripts.ashx respectively. Now:

- Please delete both .cs files related to newly added handlers.
- Open Css.ashx and change Class attribute to "Cognifide.EPiServerControls.OptimisedScriptManager.CssHandler" and delete
CodeBehind attribute.
- Open Scripts.ashx do the same with one except set Class to "Cognifide.EPiServerControls.OptimisedScriptManager.JavaScriptsHandler"
Your markups should now looks like these below:

<%@ WebHandler Language="C#" Class="Cognifide.EPiServerControls.OptimisedScriptManager.CssHandler" %>


<%@ WebHandler Language="C#" Class="Cognifide.EPiServerControls.OptimisedScriptManager.JavaScriptsHandler" %>



4. Now if you have ScriptManager somewhere in your code please replace it with

<Optimised:OptimizedScriptManager runat="server" ID="sm">


if not, please add it in the same terms as common ScriptManager control.

5. Rewrite all your scripts and styles declared in page header to the OptimisedScriptManager control, look at the sample below:

<Optimised:OptimisedScriptManager runat="server" ID="sm">
<Csses>
<Optimised:CssDocumentItem Path="~/Css/Style1.css" MediaType="screen" />
<Optimised:CssDocumentItem Path="~/Css/Style2.css" MediaType="screen" />
<Optimised:CssDocumentItem Path="~/Css/Style3.css" MediaType="print" />
</Csses>
<Scripts>
<asp:ScriptReference Path="~/Scripts/Script1.js" />
<asp:ScriptReference Path="~/Scripts/Script2.js" />
</Scripts>
</Optimised:OptimisedScriptManager>

6. Last thing to do is initialize Opimizer. You can do it both from code or from config file. Please note that you have to initialize tool before first call if you are configure it form code.

Imperative example:

OptimisedContext.Instance.ScriptsEnabled = true;

OptimisedContext.Instance.ScriptsHandlerPath = "/Scripts/Scripts.ashx";

OptimisedContext.Instance.ScriptsVersion = "1";

OptimisedContext.Instance.ScriptZipContent = true;

OptimisedContext.Instance.ScriptsClientCacheDuration = 10;

OptimisedContext.Instance.ScriptsAppCacheDuration = 1;

 

OptimisedContext.Instance.CssHandlerPath = "/Css/Css.ashx";

OptimisedContext.Instance.CssEnabled = true;

OptimisedContext.Instance.CssVersion = "1";

OptimisedContext.Instance.CssZipContent = true;

OptimisedContext.Instance.CssClientCacheDuration = 10;

OptimisedContext.Instance.CssAppCacheDuration = 1;



Declarative example:

- SectionGroup registration:

<sectionGroup name="OptimisedManager" type="Cognifide.EPiServerControls.OptimisedScriptManager.Configuration.OptimisedManagerConfigurationSectionGroup, Cognifide.EPiServerControls.OptimisedScriptManager">

    <section name="CssOptimised" type="Cognifide.EPiServerControls.OptimisedScriptManager.Configuration.OptimisedManagerConfigurationSection, Cognifide.EPiServerControls.OptimisedScriptManager" allowDefinition="Everywhere" />

    <section name="ScriptOptimised" type="Cognifide.EPiServerControls.OptimisedScriptManager.Configuration.OptimisedManagerConfigurationSection, Cognifide.EPiServerControls.OptimisedScriptManager" allowDefinition="Everywhere" />

</sectionGroup>


- Config entry:

<OptimisedManager>

    <CssOptimised Enabled="true" ZipContent="true" Version="5" HandlerPath="/Scripts/Scripts.ashx" AppCacheDuration="1" ClientCacheDuration="10" />

    <ScriptOptimised Enabled="true" ZipContent="true" Version="7" HandlerPath="/Css/Css.ashx" AppCacheDuration="1" ClientCacheDuration="10" />

</OptimisedManager>



Important notes


Optimizer:
- ignores script blocks and startup scripts.
- does not affect AJAX
- cares about MicrosoftAjax and MicrosoftAjaxToolkit scripts
- will generate separate content for each combination of scripts/styles so you don't have to worry about placing various scripts and styles on various pages
- generates separate <script> tag for each MediaType
- if you use relative paths in your styles place Css.ashx in the same folder as styles, otherwise url to images will be broken
- you can mix tool initialization between config file and code.

Future work


First of all I would like to write the same feature in ASP.NET MVC or adjust the existing one, I'm really looking forward to start doing it.
There is still a little work to do in this optimiser as well:

  • Before all I want to automate capturing calls, by registering custom UrlRewriter.

  • Fact, that css handler has to be placed in css folder isn't good, so the tool which switch the url included in css files will be huge benefit

  • I'm thinking about capturing script services proxy, startup scripts and combining them as well.

  • Caching part could be improved.

  • Logging part could be improved (added ;)).



What will be in part 2


In this part which I hope publicate in next week I explain how optimizer works internally and I'll give an intructions how to:

- use OptimisedManagerProxy control (basically if you used ScriptManagerProxy before this is pretty much the same)
- add styles and scripts from embeded resources
- make file ignored for some pages (using OptimiseManagerProxy and IsIgnored attribute)
And also how the tool works internally, which is not a big secret when you have a reflector:), and why Script.ashx is called twice - second time with empty parameters.

Summary


Optimizer is a simple and ready to use tool. It minimize both bandwidth and requests which have huge impact on loading time and user experience. This solution isn't perfect, but it works currently in really big project. I will apprietiate your feedback.

Thursday 9 April 2009

Tricky recursive loop and power of logging

Recently I was working on some import functionality which was written earlier by third person. I had to move some data between two separate systems and I was assured that tool provided to me was working fine. So, yes it worked, but processing the 1MB Xml file took him about 30 minutes!
God, I know that complicated operations like creating, modifying, managing some big structures can last long, but why it took so incredibly long?

I've started coming to terms with the processing time, but then for completely different reason I have added some logging. It was very simple, I logged just most important things - successful and failed imports. I ran tool, and after another half hour I started to browse my log file. What I've noticed first was that log file had over 8MB, it was surprising, but I left it and started to look for failed updates (which interested me then).

And then flash of inside! Each thing that should be updated and saved once, in fact was updated and saved about hundred of times. So I ran into code and than I found that method:

    protected void ProcessNodes(XmlNodeList nodelist, string culture)

    {

        for (int i = 0; i < nodelist.Count; i++)

        {

            XmlNode node = nodelist[i];

            if (node.Name == "page")

                ProcessNode(node, culture);

            if (node.HasChildNodes)

            {

                for (int i2 = 0; i2 < node.ChildNodes.Count; i2++)

                {

                    if (node.ChildNodes[i2].Name == "page")

                    {

                        ProcessNode(node, culture);

                        ProcessNodes(node.ChildNodes, culture);

                    }

                }

            }

        }

    }


Many of you see it immidietaly, but when you browse somebody's code which has more than five lines and didn't analyze that line by line, then you propably going to miss it. The problem with that code is that its not a infinity loop, its actually works, but really, really slow.
Each element is saved multiple times, in fact as many times as many children it has, and additional one unnecessary time inside the loop. So, for node which has 5 childrens, 5 siblings and which parent has 3 siblings this node will be updated and saved about 75 times!!!
Yeah, that was sneaky:)

I learned two thing from there:

1. Be careful even when you do a trivial things. The easiest things are the most difficult to to find, because normally you would just skip that bit thinking, 'oh, another loop...'.
2. Always log your actions. Simple log file contains more informations than you can image.

Oh, actually there's one more... You should never trust others code:P

Friday 6 March 2009

Inheritance in JavaScript

A few days ago I had to implement multi-searcher functionality in javascript.
It was something like a box with three searchers. You may pick searcher (i.e. Google, Wikipedia or Yahoo), enter the phrase, click Search button and results were shown below.

This task was perfect to use a base class for all searchers an then each type
of searcher as derived class. Yes, indeed, but how would you do do it in javascript?

I don't wanna talk how much time I spent on it, but finally I have found very simple and pretty obvious solution.

First of all we create a base function/class

     function BaseClass() {}
     BaseClass.prototype =
     {
         type: "BaseClass",

         GetType: function() { return this.type; },

         WriteType: function() { document.writeln(this.GetType()); }
     }

This is rather the dummy class, but reffer to my aim.
So we have type field there, and two simple methods.

Now I can just write derived classes

     FirstDerivedClass.prototype = new BaseClass;
     FirstDerivedClass.constructor = FirstDerivedClass;
     function FirstDerivedClass()
     {
         BaseClass.call(this);
         this.type = "FirstDerivedClass";
     }

     SecondDerivedClass.prototype = new BaseClass;
     SecondDerivedClass.constructor = SecondDerivedClass;
     function SecondDerivedClass()
     {
         this.type = "SecondDerivedClass";
     }

And thats it!
We have a derived class/functions in javascript.

     FirstDerivedClass.prototype = new BaseClass;

that line assign prototype to FirstDerivedClass.

     FirstDerivedClass.constructor = FirstDerivedClass;

that line assigns separate contructor to FirstDerivedClass.
And in that way

     BaseClass.call(this);

we can call base class constructor, please note that it is called earlier too (here - FirstDerivedClass.prototype = new BaseClass;), but by using call method we can pass additional arguments to base class if we want to.

In that simple way we obtain the inherit-like result.
Code executed below

     new BaseClass().WriteType();
     new FirstDerivedClass().WriteType();
     new SecondDerivedClass().WriteType();

shows following text on the page

     "BaseClass FirstDerivedClass SecondDerivedClass"

and that is exactly what I needed and expected.
Cheers!