This one comes from a customer who thought that they were seeing an issue in using the browser HTTP stack in Silverlight to make lots of HTTP requests. The issue seemed to be that the stack was blocking the UI thread and causing the UI to stutter.
The suspicion was that the blocking was occurring while data was being read from the stream that is handed back to you when you’re reading an HTTP response.
I thought I’d see if I could reproduce this behaviour myself and so I set out to put something together that made quite a lot of use of the browser HTTP stack and quite a lot of HTTP requests.
By the way – if this talk of browser/client HTTP stacks in Silverlight doesn’t resonate then take a look at this video over here which runs through the basics of it.
I put together a quick UI;
<Grid x:Name="LayoutRoot" Background="Black"> <MediaElement Stretch="Fill" AutoPlay="True" MediaEnded="MediaElement_MediaEnded" Source="Wildlife.wmv" /> <Viewbox> <StackPanel> <TextBlock Foreground="White" Text="{Binding MegabytesRead,StringFormat=MB Read \{0:F2\} }" /> <TextBlock Foreground="White" Text="{Binding MegabytesPerSecond,StringFormat=MBps \{0:F2\} }" /> <TextBlock Foreground="White" Text="{Binding OpenRequests,StringFormat=Requests \{0\} }" /> <TextBlock Foreground="White" Text="{Binding TimerTick,StringFormat=Timer \{0\} }" /> </StackPanel> </Viewbox> </Grid>
This is playing a video (wildlife.wmv which ships with Windows 7) which I have embedded into my XAP. It’s also attempting to display (via binding) 4 pieces of data;
- Total megabytes downloaded over HTTP
- Number of megabytes read in the last second
- Number of open HTTP requests in flight
- A “tick” text block which will simple turn True/False as a timer ticks
and I put some code together behind this;
public partial class MainPage : UserControl, INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; //#error This needs a Uri to a big file to download (mine is 1.5MB) static Uri videoFile = new Uri("http://localhost/wildlife.wmv", UriKind.Absolute); const int threadCount = 10; long bytesRead = 0; long requestsOpen = 0; DateTime startTime; List<byte[]> bufferPool; public bool TimerTick { get { return (_TimerTick); } set { _TimerTick = value; RaisePropertyChanged("TimerTick"); } } bool _TimerTick; public double MegabytesPerSecond { get { return (_MegabytesPerSecond); } set { _MegabytesPerSecond = value; RaisePropertyChanged("MegabytesPerSecond"); } } double _MegabytesPerSecond; public double MegabytesRead { get { return (_Count); } set { _Count = value; RaisePropertyChanged("MegabytesRead"); } } double _Count; public long OpenRequests { get { return (_OpenRequests); } set { _OpenRequests = value; RaisePropertyChanged("OpenRequests"); } } long _OpenRequests; public MainPage() { InitializeComponent(); // NB: this is read-only from this point onwards. bufferPool = new List<byte[]>( Enumerable.Range(1, threadCount).Select(i => new byte[4096])); this.Loaded += OnLoaded; } void MakeRequestsForEver(int i) { byte[] buffer = this.bufferPool[i]; HttpWebRequest request = (HttpWebRequest)WebRequestCreator.BrowserHttp.Create(videoFile); // request.AllowReadStreamBuffering = false; request.BeginGetResponse(iar => { Interlocked.Increment(ref this.requestsOpen); WebResponse response = request.EndGetResponse(iar); Stream stream = response.GetResponseStream(); AsyncReadStream(response, stream, buffer, i); }, null); } void AsyncReadStream( WebResponse response, Stream stream, byte[] buffer, int index) { stream.BeginRead(buffer, 0, buffer.Length, iar => { int bytesRead = stream.EndRead(iar); Interlocked.Add(ref this.bytesRead, bytesRead); if (bytesRead < buffer.Length) { stream.Close(); stream.Dispose(); response.Close(); Interlocked.Decrement(ref this.requestsOpen); MakeRequestsForEver(index); } else { AsyncReadStream(response, stream, buffer, index); } }, null); } void OnLoaded(object sender, RoutedEventArgs args) { this.DataContext = this; StartTiming(); for (int i = 0; i < 10; i++) { int j = i; ThreadPool.QueueUserWorkItem(cb => { MakeRequestsForEver(j); }); } } void StartTiming() { this.startTime = DateTime.Now; DispatcherTimer timer = new DispatcherTimer(); timer.Interval = new TimeSpan(0, 0, 0, 0, 5); timer.Tick += (s, e) => { this.MegabytesRead = this.bytesRead / (1024.0 * 1024.0); this.OpenRequests = this.requestsOpen; this.MegabytesPerSecond = (this.MegabytesRead / ((DateTime.Now - this.startTime).TotalSeconds)); this.TimerTick = !this.TimerTick; }; timer.Start(); } void RaisePropertyChanged(string property) { if (this.PropertyChanged != null) { this.PropertyChanged(this, new PropertyChangedEventArgs(property)); } } void MediaElement_MediaEnded(object sender, RoutedEventArgs e) { ((MediaElement)sender).Position = TimeSpan.FromSeconds(0); ((MediaElement)sender).Play(); } }
The idea here is that we play the video embedded in the XAP file while downloading the same video file from my local web server. To do the downloads we kick off 10 ThreadPool threads which begin a process of;
- Make an asynchronous HTTP request on the browser stack for the same video from my local web server.
- Asynchronously read the response stream and throw away the bytes before going back to step 1.
There’s not really any need to kick this off from 10 separate threads in the first place, it could have been done by just starting 10 requests from one thread.
The code also kicks off a timer which fires on a 5ms interval and tries to update the UI with the latest counts of data as it is being read.
Running this all on my laptop, I saw fairly decent results.
and so I topped out at about 130MB per second (if my calculation code holds together which it probably doesn’t ) and the UI remained completely responsive all the way through. However, you can see that it’s fairly memory intensive;
My next experiment was to uncomment line 87 back in that source listing which sets the AllowReadStreamBuffering flag. As far as I know, what this switches is whether Silverlight’s networking stack reads the entire response stream into memory before handing it back to you to consume.
Naturally, there are occasions where you might want this but, equally, there are occasions where you don’t and given that I’m not even reading the data anyway I can’t see why I would want it.
From a memory perspective, I see a much happier story (if I’m looking at a representative performance counter which is always a nagging doubt);
and this kind of makes sense because if I keep hammering the network stack asking it to read 1.5MB into buffers again and again then it’s not surprising that a lot of buffers might get allocated and discarded whereas if I’m just asking for the raw stream without pre-buffering then it seems to fit that would mean a lot less in the way of allocated buffers.
The last thing I tried was to leave buffering turned off but replace the video file that is being downloaded all the time with a 25MB version. The throughput got better;
but the UI did indeed stutter a little periodically but then it gets hard to know at that point whether I’m just melting the laptop somewhere between running the web server and the client at the same time. Memory usage grew again with the much larger download file size;
I experimented with forcing a garbage collection around line 114 to see if predictably asking for GC rather than waiting for things to pile up and it did seem to improve things but I’d want to dig deeper on that one before being too sure.
But…realistically, this is a bit of a pathological case in that I’m not sure many apps would want to be doing 10 concurrent downloads of a 25MB video file while playing another video and so, for the moment, I was reasonably happy that I can keep the UI thread responsive while downloading over HTTP.
By the way – that code was put together a little “quickly” so feel free to throw in criticisms if you spot glaring errors