Disclaimer The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.
© Copyright 2010 Grant Archibald
In this blog entry I will cover some the factors effecting perceived performance and the asynchronous code used to optimise the user experience.
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:
So let's start of by profiling the current application see what has been done.
Looking at the HTTP profile of the application there are three distinct phases:
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); } }
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:
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.
In summary when looking Silverlight applications one aim is to improve the perceived user performance by:
Comments [0] Sunday, November 16, 2008 9:07:56 AM (GMT Standard Time, UTC+00:00) Related posts:Three Little Pigs – Silverlight E-BookMy Mix09 10k Contest Entry Is Live – Spin And WinSilverlight BabySmash Audio FilesBabySmash At PDCTracking Silverlight And Moonlight Enabled Browsers via Google AnalyticsBabySmash Silverlight Refactorings - Part 2 - Routed Events babysmash | silverlight