read

Many people notice an apparent degradation of latency of concurrent WCF calls in a Silverlight application compared to a similar code in a Windows Console application. This post explains this phenomenon and shows how to optimize performance of concurrent calls in Silverlight.

WCF proxies in Silverlight applications use the SynchronizationContext of the thread from which the web service call is initiated to schedule the invocation of the async event handler when the response is received. When the web service call is initiated from the UI thread of a Silverlight application, the async event handler code will also execute on the UI thread. Given that there is only one UI thread in the Silverlight application, and given it has other chores (primarily managing the UI itself), the average latency of multiple concurrent WCF calls as measured by the code executing on the UI thread may appear to be long. This is because the execution of the event handlers for individual calls needs to be serialized on a single thread and interlaced with other tasks that thread has to perform. The benefit of this model is that the code in the event handler can directly manipulate controls on the page, which is a privilege reserved for code executing on the UI thread. The example below shows how to measure an average latency of multiple WCF calls initiated from a UI thread:

 1 int responseCount;       
 2 int totalLatency;        
 3 const int MessageCount = 100;     
 4 
 5 void MainPage_Loaded(object sender, RoutedEventArgs e)       
 6 {        
 7     TestService.ServiceClient proxy = new TestService.ServiceClient();        
 8     proxy.HelloCompleted += new EventHandler<WcfPerf.TestService.HelloCompletedEventArgs>(proxy_HelloCompleted);        
 9     for (int i = 0; i < MessageCount; i++)        
10     {        
11         proxy.HelloAsync("foo", Environment.TickCount);        
12     }        
13 }     
14 
15 void proxy_HelloCompleted(object sender, WcfPerf.TestService.HelloCompletedEventArgs e)       
16 {        
17     int end = Environment.TickCount;        
18     this.responseCount++;        
19     this.totalLatency += end - (int)e.UserState;              
20     if (this.responseCount == MessageCount)        
21     {        
22         this.Log.Text = ((double)(this.totalLatency) / MessageCount).ToString();        
23     }        
24 }   

In order to avoid synchronizing the invocations of event handlers for individual calls on a single UI thread, a worker thread can be used to initiate the web service calls. Newly created worker threads do not have a SynchronizationContext set, which means event handler code for every response from the server will execute on its own thread. This greatly reduces contention in the client application and reduces the average latency of a series of concurrent calls. Consider the code below which uses the worker thread approach:

 1 int responseCount;       
 2 int totalLatency;        
 3 const int MessageCount = 100;        
 4 Thread workerThread;     
 5 
 6 void MainPage_Loaded(object sender, RoutedEventArgs e)       
 7 {        
 8     this.workerThread = new Thread(this.StartSending);        
 9     this.workerThread.Start();        
10 }     
11 
12 void StartSending()       
13 {        
14     TestService.ServiceClient proxy = new TestService.ServiceClient();        
15     proxy.HelloCompleted += new EventHandler<WcfPerf.TestService.HelloCompletedEventArgs>(proxy_HelloCompleted);        
16     for (int i = 0; i < MessageCount; i++)        
17     {        
18         proxy.HelloAsync("foo", Environment.TickCount);        
19     }        
20 }     
21 
22 void proxy_HelloCompleted(object sender, WcfPerf.TestService.HelloCompletedEventArgs e)       
23 {        
24     int end = Environment.TickCount;        
25     lock (this.workerThread)        
26     {        
27         this.responseCount++;        
28         this.totalLatency += end - (int)e.UserState;        
29     }        
30     if (this.responseCount == MessageCount)        
31     {        
32         this.Dispatcher.BeginInvoke(delegate        
33         {        
34             this.Log.Text = ((double)(this.totalLatency) / MessageCount).ToString();        
35         });        
36     }        
37 }

My ad-hoc measurements indicate the average latency of the worker thread approach is about 20% of the average latency of the UI thread approach. The downside of this approach is the more complex way of manipulating the UI controls on the page. The worker thread has to explicitly schedule the code that manipulates the UI to execute on the UI thread, as shown in the proxy_HelloCompleted implementation above.

Blog Logo

Tomasz Janczuk


Published

Image

Tomek on Software

Software - shaken, not stirred

Back to Overview