Want to show your appreciation?
Please a cup of tea.

Friday, September 19, 2008

Web Service Client Startup Performance: Spring.Net Proxy v.s. WSCodeGen

I'm going to make this post as a summary of our effort to solve the startup performance problem of Spring.Net's WebServiceProxyFactory client. We described the problem, proposed the solution, implemented it, resolved the ClickOnce deployment issue, integrated back with Spring.Net and finally a well tested release. Now, it's the time to look at what exactly we gain by doing all these.

Note: All source code we discussed is available in the WSCodeGen project site. You can download the source code, it is located in the example/CompareSpringProxy folder.

Let's start with a Model project. It has only one simplest interface IHelloWord below:

public interface IHelloWorld
{
    string SayHello(string name);
}

Then we added a Web Service project and defined two identical Web Services. Both implement the IHelloWorld Interface. Below listed the HelloWorld1.asmx and the HelloWorld2.asmx is identical except the class name.

[WebService(Namespace = "http://tempuri.org")]
[WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
[ToolboxItem(false)]
public class HelloWorld1 : System.Web.Services.WebService, IHelloWorld
{
    [WebMethod]
    public string SayHello(string name)
    {
        return "Hello " + (string.IsNullOrEmpty(name) ? "World" : name);
    }
}

Now comes the protagonist. Let's create the Client project as a console application. Add reference to Spring.Core, Spring.Service and the Model project that we created earlier. Setup the Spring.Net with following XML configuration in the App.config.

<objects default-lazy-init="true"
        xmlns="http://www.springframework.net"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
 
 
  <object id="CookieContainer" type="System.Net.CookieContainer"/>
 
  <object abstract="true" id="AbstractWebServiceProductTemplate" 
          name="AbstractWSCodeGenClient">
    <property name="CookieContainer" ref="CookieContainer" />
    <property name="UseDefaultCredentials" value="true"/>
    <property name="PreAuthenticate" value="true"/>
  </object>
 
  <object id="AbstractWebServiceClient" abstract="true"
    type="Spring.Web.Services.WebServiceProxyFactory,Spring.Services">
    <property name="ProductTemplate">
      <object parent="AbstractWebServiceProductTemplate"/>
    </property>
  </object>
 
  <object id="WarmUpHelloWorld1" parent="AbstractWebServiceClient">
    <property name="ServiceUri" value="http://localhost:4834/HelloWorld1.asmx"/>
    <property name="ServiceInterface" 
              value="CompareSpringProxy.Model.IHelloWorld, CompareSpringProxy.Model"/>
  </object>
 
  <object id="WarmUpHelloWorld2" parent="AbstractWSCodeGenClient"
          type="CompareSpringProxy.Client.WarmUpHelloWorld2, CompareSpringProxy.Client">
    <property name="Url" value="http://localhost:4834/HelloWorld2.asmx"/>
    <property name="ServiceInterface" 
              value="CompareSpringProxy.Model.IHelloWorld, CompareSpringProxy.Model"/>
  </object>
 
  <object id="SpringProxyHelloWorld1" parent="AbstractWebServiceClient">
    <property name="ServiceUri" value="http://localhost:4834/HelloWorld1.asmx"/>
    <property name="ServiceInterface" 
              value="CompareSpringProxy.Model.IHelloWorld, CompareSpringProxy.Model"/>
  </object>
 
  <object id="SpringProxyHelloWorld2" parent="AbstractWebServiceClient">
    <property name="ServiceUri" value="http://localhost:4834/HelloWorld2.asmx"/>
    <property name="ServiceInterface" 
              value="CompareSpringProxy.Model.IHelloWorld, CompareSpringProxy.Model"/>
  </object>
 
  <object id="WSCodeGenHelloWorld1" parent="AbstractWSCodeGenClient"
              type="CompareSpringProxy.Client.HelloWorld1, CompareSpringProxy.Client">
    <property name="Url" value="http://localhost:4834/HelloWorld1.asmx"/>
    <property name="ServiceInterface" 
              value="CompareSpringProxy.Model.IHelloWorld, CompareSpringProxy.Model"/>
  </object>
 
  <object id="WSCodeGenHelloWorld2" parent="AbstractWSCodeGenClient"
              type="CompareSpringProxy.Client.HelloWorld2, CompareSpringProxy.Client">
    <property name="Url" value="http://localhost:4834/HelloWorld2.asmx"/>
    <property name="ServiceInterface" 
              value="CompareSpringProxy.Model.IHelloWorld, CompareSpringProxy.Model"/>
  </object>
 
</objects>

We purposely configured the Spring.Net context to use lazy-init so that we can measure the startup time for each object individually. In the configuration, we have configured two warn up object that is used to warm up the Web server and the Spring.Net context. There are two Spring.Net dynamic proxies, SpringProxyHelloWorld1 and SpringProxyHelloWorld2, plus two WSCodeGen clients, WSCodeGenHelloWorld1 and WSCodeGenHelloWorld2, that going against the HelloWorld1.asmx and HelloWorld2.asmx respectively. They are the object that will be used to do the performance test.

In order for WSCodeGen to generate the client, we need to add an XSL file to transform the Spring.Net configuration. The content of the WebServiceClient.xsl file is listed below.

<?xml version="1.0" encoding="UTF-8" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform" 
                xmlns:spring="http://www.springframework.net">
  <xsl:output method="xml" version="1.0" encoding="UTF-8" indent="yes"/>
  <xsl:param name="namespace">CompareSpringProxy.Client</xsl:param>
  <xsl:template match="/">
    <web-service xmlns="urn:web-service-client-configuration-1.0">
      <client-group namespace="{$namespace}" xml-namespace="http://tempuri.org">
        <xsl:for-each select="//spring:object[@parent='AbstractWSCodeGenClient']">
          <client class-name="{substring(substring-before(@type, ','), string-length($namespace)+2)}">
            <interface>
              <xsl:value-of select="spring:property[@name='ServiceInterface']/@value"/>
            </interface>
          </client>
        </xsl:for-each>
      </client-group>
    </web-service>
  </xsl:template>
</xsl:stylesheet>

Add a post build event to the Model project to have the client source code generated.

$(SolutionDir)..\..\build\WebServiceClientGenerator\Debug\WebServiceClientGenerator.exe $(SolutionDir)CompareSpringProxy.Client\App.config $(SolutionDir)CompareSpringProxy.Client\WebServiceClient.cs $(SolutionDir)CompareSpringProxy.Client\WebServiceClient.xsl

The last piece is the Program.cs. I have added comment along the line so it is easy to understand. We need those warm up calls so that the loading of Spring.Net abstract objects, dependency assemblies and initialization of Web Service at the server side will not impact the performance measure of the client.

static void Main(string[] args)
{
    // start the spring context.
    IApplicationContext ctx = ContextRegistry.GetContext();
 
    // warn up the spring as well as the Web Service server
    IHelloWorld warmUp1 = (IHelloWorld)ctx.GetObject("WarmUpHelloWorld1");
    warmUp1.SayHello("warmUp1");
    IHelloWorld warmUp2 = (IHelloWorld)ctx.GetObject("WarmUpHelloWorld2");
    warmUp2.SayHello("warmUp2");
 
 
    DateTime startTime;
    TimeSpan timeSpan;
    string result;
 
    // Gather the performance
 
    startTime = DateTime.Now;
    IHelloWorld codeGen1 = (IHelloWorld)ctx.GetObject("WSCodeGenHelloWorld1");
    result = codeGen1.SayHello("codeGen1");
    timeSpan = DateTime.Now - startTime;
    Console.WriteLine("{0} took {1}", result, timeSpan);
 
    startTime = DateTime.Now;
    IHelloWorld proxy1 = (IHelloWorld)ctx.GetObject("SpringProxyHelloWorld1");
    result = proxy1.SayHello("Proxy1");
    timeSpan = DateTime.Now - startTime;
    Console.WriteLine("{0} took {1}", result, timeSpan);
 
    startTime = DateTime.Now;
    IHelloWorld proxy2 = (IHelloWorld)ctx.GetObject("SpringProxyHelloWorld2");
    result = proxy2.SayHello("Proxy2");
    timeSpan = DateTime.Now - startTime;
    Console.WriteLine("{0} took {1}", result, timeSpan);
 
    startTime = DateTime.Now;
    IHelloWorld codeGen2 = (IHelloWorld)ctx.GetObject("WSCodeGenHelloWorld2");
    result = codeGen2.SayHello("codeGen2");
    timeSpan = DateTime.Now - startTime;
    Console.WriteLine("{0} took {1}", result, timeSpan);
 
    Console.WriteLine("Press enter to continue...");
    Console.ReadLine();
}

Now, we can start the Web project and then run the client application to see the difference. On my Core2 Due X61 Thinkpad, below is the result.

Hello codeGen1 took 00:00:00.0156250
Hello Proxy1 took 00:00:00.1562500
Hello Proxy2 took 00:00:00.1875000
Hello codeGen2 took 00:00:00.0156250

The result clearly shows the advantage of using WSCodeGen. This comparison also demonstrates how easy to replace the WebServiceProxyFactory with WSCodeGen in an existing Spring.Net configuration.

It is now time to put an end to this series of the blog posts about the slow start up issue of the Spring.Net dynamic Web Service client proxy and the solution to this. But his is definitely not the end of the WSCodeGen project. Like exception propagation and using of the collection interface are two major problem of using Web Service with Spring.Net, WSCodeGen can be enhanced to provide solution to those problems.

No comments:

Post a Comment