# Sunday, November 16, 2008

Silverlight BabySmash Performance – The Asynchronous Story

Overview

In this blog entry I will cover some the factors effecting perceived performance and the asynchronous code used to optimise the user experience.

Introduction

When looking at the performance of the BabySmash Silverlight application it was important to look at a number of factors to help improve the perceived performance of the application. These included:

  • Initial download size.
  • Use of a secondary assembly.
  • Use of media files.

So let's start of by profiling the current application see what has been done.

Http Download Profile

Initial DownloadInitial DownloadSecondary DownloadSecondary DownloadLoad On DemandLoad On Demand

Looking at the HTTP profile of the application there are three distinct phases:

  1. Synchronous download of the initial html code and the Silverlight application.
  2. Asynchronous download of secondary components.
  3. Asynchronous download of media files on demand as the user presses keys.

Initial Download

The default.htm file together with the initial BabySmashWeb.xap combined are around 60kb. This xap file contains all the code necessary to interact with the user, request the additional resources and log information to the central ADO.Net Data Service.

Using this approach the loading phase of the the Silverlight application is very short and the user immediately shown the “Smash Canvas” as quickly a possible.

    <div id="silverlightControlHost">
        <object data="data:application/x-silverlight-2," type="application/x-silverlight-2" width="100%" height="100%">
            <param name="source" value="ClientBin/BabySmashWeb.xap"/>
            <param name="onerror" value="onSilverlightError" />
            <param name="background" value="white" />
            <param name="minRuntimeVersion" value="2.0.31005.0" />
            <param name="autoUpgrade" value="true" />
            <param name="initParams" value="logKeys=true" />
            <a href="http://go.microsoft.com/fwlink/?LinkID=124807" style="text-decoration: none;">
	            <img src="http://go.microsoft.com/fwlink/?LinkId=108181" alt="Get Microsoft Silverlight" style="border-style: none"/>
            </a>
        </object>
        <iframe style='visibility:hidden;height:0;width:0;border:0px'></iframe>
    </div>

Secondary Download

Once the application is loaded, the constructor of the main code behind Page class is called

        public Page()
        {
            InitializeComponent();

            var controller = new Controller();
            controller.Launch();
            mainPage = controller.Windows[0];

            KeyUp += new KeyEventHandler(Page_KeyUp);

            LayoutRoot.Children.Add(mainPage);
        }

Which calls starts a Controller class, which in turn starts the asynchronous requests for the letters and the initial media file.

       public void Launch()
       {
           CheckForUpdatesAsync();

           SetupInitialWindowState();

           //here to pre-cache letter
           CoolLetter.InitLetterStateAsync();

           //Startup sound
           audio.PlayWavResourceYield(".Resources.Sounds." + "EditedJackPlaysBabySmash.wav");
       }

The rest of this section will discuss how the letters stored in the secondary BabySmashEnglishLetters.xap are downloaded and stored for use when the user presses a key. The initial technique for loading the audio files is discussed in the implementation of PlayWavResourceYeild in the Load On Demand section below.

   public partial class CoolLetter 
	{
      public CoolLetter()
      {
         this.InitializeComponent();
      }

	    static readonly Dictionary<string, string> _letters = InitLetters();

        public static void InitLetterStateAsync()
        {
            // No work to do as the private _letters will be initialised above
        }

The InitLetters method is called by the .Net Framework when the static class is initialised. The method utilises a WebClient class instance to asynchronously download the required secondary assembly

       private static Dictionary<string, string> InitLetters()
       {
           var addressUri = new Uri("BabySmashEnglishLetters.xap", UriKind.Relative);
           var letterLoader = new WebClient();
           letterLoader.OpenReadCompleted += letterAssemblyLoaded;
           letterLoader.OpenReadAsync(addressUri);
           return new Dictionary<string, string>();
       }

Once the asynchronous call is completed, reflection is used to dynamically call the GenerateAlphanumericCharacters method of the AlphaNumericLetterGenerator class.

	    private static void letterAssemblyLoaded(object sender, OpenReadCompletedEventArgs e)
	    {
	        if ((e.Error != null) || e.Cancelled) return;

	        // Convert the downloaded stream into an assembly
	        var a = LoadAssemblyFromXap("BabySmashLetters.dll", e.Result);

	        var generator = a.CreateInstance("BabySmashLetters.AlphaNumericLetterGenerator");

	        if (generator == null) return;


	        var generated = generator.GetType().InvokeMember("GenerateAlpanumericCharacters", BindingFlags.Public | BindingFlags.InvokeMethod | BindingFlags.Instance, null, generator,null) as Dictionary<string, string>;
            if ( generated == null ) return;
	        foreach (var pair in generated)
	        {
	            _letters.Add(pair.Key, pair.Value);
	        }
	    }
Loading the assembly is achieved by extracting the requested assembly from the xap stream as follows.

	    public static Assembly LoadAssemblyFromXap(string relativeUriString, Stream xapPackageStream)
        {
            var uri = new Uri(relativeUriString, UriKind.Relative);
            var xapPackageSri = new StreamResourceInfo(xapPackageStream, null);
            var assemblySri = Application.GetResourceStream(xapPackageSri, uri);

            var assemblyPart = new AssemblyPart();
            var a = assemblyPart.Load(assemblySri.Stream);
            return a;
        }

Load On Demand

By the time that the third stage of load on demand media files is reached the main application is loaded, the secondary assembly is loading or loaded and the introduction sound file has played. This has resulted in the download of around 160kb of files and kept the time wait time for the user to a minimum.

Loading the external dependencies has also given us the following advantages:

  • Allow for the possibility of localising the application to other non-english languages.
  • Allow media files to be updated independently of the compiled code.
  • Allow the media files to to be sourced from alternative locations e.g. File stored locally on the client computer.
  • Reduced bandwidth from 406kb for users that visit the page but do not start using the application.

The process for loading the additional media files is similar to the process to load the assembly secondary xap file but has the additional complexity that the user can be pressing keys very quickly. As a result the application needs to respond very quickly by queuing the key requests then asynchronously downloading and playing the associated sound file.

        public void PlayWavResourceYield(string s)
        {
            if (player == null)
                return;

            if (inPlay && soundsToPlay.Count < 5)
            {
                soundsToPlay.Enqueue(s);
                return;
            }

            PlaySound(s);
        }

If the application is currently playing a media file then the request is placed on queue for later playback otherwise the sound will be played. If the media file is requested for the first time ten it will be asynchronously downloaded, added to the available media list then played.

        private void PlaySound(string resourceName)
        {
            var mediaFile = GetMediaFileFromResourceName(resourceName);

            lock (media)
            {
                if (media.ContainsKey(mediaFile) == false)
                {
                    var wc = new WebClient();
                    wc.OpenReadCompleted += delegate(object sender, OpenReadCompletedEventArgs e)
                                                {
                                                    lock (media)
                                                    {
                                                        if (media.ContainsKey(mediaFile) != false) return;
                                                        media.Add(mediaFile, e.Result);
                                                        player.SetSource(e.Result);
                                                    }
                                                };
                    wc.OpenReadAsync(new Uri(mediaFile, UriKind.Relative));
                    return;
                }
            }

            var sound = media[mediaFile];
            if (sound == null) return;
            if (sound.CanSeek) sound.Position = 0;
            player.SetSource(sound);
        }

If the media file is requested again then the version available in the media list will be reused and played to the user.

Summary

In summary when looking Silverlight applications one aim is to improve the perceived user performance by:

  • Quick Feedback – Provide quick feedback to the user so that the can interact with application. Small initial xap file allowed the “Smash Canvas” and initial introduction message where played without the overhead of other resources.
  • Thinking Asynchronously - Performing actions asynchronously so that the user action is not blocked while necessary processing is occurring.  Requesting media on demand allowed content to played as needed.
  • Separating The Application – Look at architecting the application so that it can be split into different parts that can be managed independently if required. Using separated code/media files allows for further expansion points and hosting options.
Comments are closed.