Wednesday, February 13, 2008

Bandwidth Throttle for ASP.Net

We were working on a project for a company that suddenly started complaining of slow ASP.Net pages. I optimised what I could, but it seemed to me that it ran pretty fast. Then I find out that some of the customers use a slow Internet connection. The only way to test this was to simulate a slow connection.

But how can one do that on IIS 5.1, the Windows XP web server? After a while of searching I realised that it was the wrong question. I don't need this for other projects and if I did I certainly wouldn't want to slow the entire web server to check it out. Because yes, changing the metadata of the server can, supposedly, change the maximum speed the pages are delivered. But it was simply too much hassle and it wasn't a reusable solution.

My way was to create a Filter for the Response of all pages. Response.Filter is supposed to be a Stream that receives as parameter the previous Response.Filter (which at the very start is Response.OutputStream) and does something to the output of the page. So I've created a BandwidthThrottleFilter object and added it in the MasterPage Page_Load:
Response.Filter=new BandwidthThrottleFilter(Response.Fitler,10000);
. It worked.

Now for the code. Follow these steps:
  1. Create a BandwidthThrottleFilter class that inherits from the abstract class Stream
  2. Add a constructor that receives as parameters a Stream and an integer
  3. Add fields that will get instantiated from these two parameters
  4. Implement all abstract methods of the Stream object and use the same methods from the Stream field
  5. Change the Write method to also call a Delay method that receives as parameter the count parameter of the Write method


That's it. You need only create the Delay method which will do a Thread.Sleep for the duration of time it normally should take to transfer that amount of bytes. Of course, that assumes that the normal speed of transfer is negligeable.

Click to see the whole class code

6 comments:

serializer said...

Thanks! This was incredibly useful. I'm implementing an AJAX-like file upload and needed to artificially throttle my local bandwidth, to test the progress bar.

Your article also pointed me towards a better solution to my original problem: how to actually tell how much of the file has been uploaded.

By implementing a filter on the Request object (instead of Response) I was able to both keep track of the upload progress (simply by incrementing my counter as the data passed through), as well as being able to slow the upload down and actually see my progress bar working even on localhost.

Evan said...

Here it is in VB.Net

The line to put in the MasterPage
Response.Filter = New BandwidthThrottleFilter(Response.Filter, 10000)



Imports System.IO
Imports System.Threading

Public Class BandwidthThrottleFilter
Inherits Stream
Private ReadOnly _bytesPerSecond As Integer
Private ReadOnly _sink As Stream

Public Sub New(ByVal sink As Stream, ByVal bytesPerSecond As Integer)
_sink = sink
_bytesPerSecond = bytesPerSecond
End Sub


Private Sub Delay(ByVal length As Integer)
Dim milliseconds As Integer = length * 1000 / _bytesPerSecond
Thread.Sleep(milliseconds)
End Sub

Public Overloads Overrides ReadOnly Property CanRead() As Boolean
Get
Return _sink.CanRead
End Get
End Property

Public Overloads Overrides ReadOnly Property CanSeek() As Boolean
Get
Return _sink.CanSeek
End Get
End Property

Public Overloads Overrides ReadOnly Property CanWrite() As Boolean
Get
Return _sink.CanWrite
End Get
End Property

Public Overloads Overrides ReadOnly Property Length() As Long
Get
Return _sink.Length
End Get
End Property

Public Overloads Overrides Property Position() As Long
Get
Return _sink.Position
End Get
Set(ByVal value As Long)
_sink.Position = value
End Set
End Property

Public Overloads Overrides Sub Flush()
_sink.Flush()
End Sub

Public Overloads Overrides Function Seek(ByVal offset As Long, ByVal origin As SeekOrigin) As Long
Return _sink.Seek(offset, origin)
End Function

Public Overloads Overrides Sub SetLength(ByVal value As Long)
_sink.SetLength(value)
End Sub

Public Overloads Overrides Function Read(ByVal buffer As Byte(), ByVal offset As Integer, ByVal count As Integer) As Integer
Return _sink.Read(buffer, offset, count)
End Function

Public Overloads Overrides Sub Write(ByVal buffer As Byte(), ByVal offset As Integer, ByVal count As Integer)
Delay(count)
_sink.Write(buffer, offset, count)
End Sub
End Class

Siderite said...

Arghh, VB on my blog! Quick, call a priest! An exorcism must be performed :)

Thanks for the code, Evan!

Anonymous said...

The first thing I did to this was make it accept Kilobits instead of Bytes, so developers can use familiar numbers like 56 for a 56K modem.

Now my question is, when it comes to bandwidth, is it actually Kilobits (1000 bits), or Kibibits (1024 bits)?

Also, if you try to download a small file with a high bandwidth setting, you could get milliseconds=0, which would call Thread.Sleep(0) and suspend the thread!

Thanks,

- Sharpzy

Siderite said...
This comment has been removed by the author.
Siderite said...

Sorry, I was not paying attention when answering your last comment. First of all thank you for pointing out that Thread.Sleep(0) blocked the thread.

Then the answer to your question is 1000. 56K means 56000 bits. However, that doesn't mean that you can just divide the size into 125 bytes, since 56kbits refers to the entire communication in a physical device, including data, communication information, stop bits, error correction, etc.