Ben Copsey

Add tests for upload / download throttling

Cleanup API
More comments as I'll never remember what all this stuff was for
... ... @@ -462,8 +462,17 @@ extern unsigned long const ASIWWANBandwidthThrottleAmount;
+ (unsigned long)maxBandwidthPerSecond;
+ (void)setMaxBandwidthPerSecond:(unsigned long)bytes;
// Get a rough average (for the last 5 seconds) of how much bandwidth is being used, in bytes
+ (unsigned long)averageBandwidthUsedPerSecond;
// Will return YES is bandwidth throttling is currently in use
+ (BOOL)isBandwidthThrottled;
// Used internally to record bandwidth use, and by ASIInputStreams when uploading. It's probably best if you don't mess with this.
+ (void)incrementBandwidthUsedInLastSecond:(unsigned long)bytes;
// On iPhone only, ASIHTTPRequest can automatically turn throttling on and off as the connection type changes between WWAN and WiFi
#if TARGET_OS_IPHONE
// Set to YES to automatically turn on throttling when WWAN is connected, and automatically turn it off when it isn't
+ (void)setShouldThrottleBandwidthForWWAN:(BOOL)throttle;
... ...
... ... @@ -72,16 +72,14 @@ unsigned long const ASIWWANBandwidthThrottleAmount = 14800;
// YES when bandwidth throttling is active
// This flag does not denote whether throttling is turned on - rather whether it is currently in use
// It will be set to NO when throttling is turned on, but a WI-FI connection is active
BOOL shouldThrottleBandwidth = NO;
BOOL isBandwidthThrottled = NO;
// Private stuff
@interface ASIHTTPRequest ()
- (BOOL)askDelegateForCredentials;
- (BOOL)askDelegateForProxyCredentials;
+ (void)incrementBandwidthUsedInLastSecond:(unsigned long)bytes;
+ (void)measureBandwidthUsage;
+ (BOOL)shouldThrottleBandwidth;
+ (void)recordBandwidthUsage;
@property (assign) BOOL complete;
... ... @@ -1660,7 +1658,7 @@ BOOL shouldThrottleBandwidth = NO;
// Reduce the buffer size if we're receiving data too quickly when bandwidth throttling is active
// This just augments the throttling done in measureBandwidthUsage to reduce the amount we go over the limit
if ([[self class] shouldThrottleBandwidth]) {
if ([[self class] isBandwidthThrottled]) {
[bandwidthThrottlingLock lock];
if (maxBandwidthPerSecond > 0) {
long long maxSize = (long long)maxBandwidthPerSecond-(long long)bandwidthUsedInLastSecond;
... ... @@ -2319,11 +2317,11 @@ BOOL shouldThrottleBandwidth = NO;
#pragma mark bandwidth measurement / throttling
+ (BOOL)shouldThrottleBandwidth
+ (BOOL)isBandwidthThrottled
{
#if TARGET_OS_IPHONE
[bandwidthThrottlingLock lock];
BOOL throttle = shouldThrottleBandwidth;
BOOL throttle = isBandwidthThrottled;
[bandwidthThrottlingLock unlock];
return throttle;
#else
... ... @@ -2419,7 +2417,7 @@ BOOL shouldThrottleBandwidth = NO;
}
#if TARGET_OS_IPHONE
+ (void)setShouldThrottleBandwidthForWWAN:(BOOL)throttle
+ (void)setisBandwidthThrottledForWWAN:(BOOL)throttle
{
if (throttle) {
[ASIHTTPRequest throttleBandwidthForWWANUsingLimit:ASIWWANBandwidthThrottleAmount];
... ... @@ -2443,9 +2441,9 @@ BOOL shouldThrottleBandwidth = NO;
{
[bandwidthThrottlingLock lock];
if ([[Reachability sharedReachability] internetConnectionStatus] == ReachableViaCarrierDataNetwork) {
shouldThrottleBandwidth = YES;
isBandwidthThrottled = YES;
} else {
shouldThrottleBandwidth = NO;
isBandwidthThrottled = NO;
}
[bandwidthThrottlingLock unlock];
}
... ... @@ -2455,15 +2453,18 @@ BOOL shouldThrottleBandwidth = NO;
{
[bandwidthThrottlingLock lock];
unsigned long toRead = 4096;
if (maxBandwidthPerSecond) {
toRead = maxBandwidthPerSecond/32;
}
// We'll split our bandwidth allowance into 4 (which is the default for an ASINetworkQueue's max concurrent operations count) to give all running requests a fighting chance of reading data this cycle
long long toRead = maxBandwidthPerSecond/4;
if (maxBandwidthPerSecond > 0 && (bandwidthUsedInLastSecond + toRead > maxBandwidthPerSecond)) {
toRead = maxBandwidthPerSecond-bandwidthUsedInLastSecond;
if (toRead < 0) {
toRead = 0;
}
}
if (toRead == 0 || !bandwidthMeasurementDate || [bandwidthMeasurementDate timeIntervalSinceNow] < -0) {
NSLog(@"sleep");
[NSThread sleepUntilDate:bandwidthMeasurementDate];
[self recordBandwidthUsage];
}
... ...
... ... @@ -31,32 +31,48 @@
[super dealloc];
}
// Ok, so this works, but I don't really understand why.
// Ideally, we'd just return the stream's hasBytesAvailable, but CFNetwork seems to want to monopolise our run loop until (presumably) its buffer is full, which will cause timeouts if we're throttling the bandwidth
// We return NO when we shouldn't be uploading any more data because our bandwidth limit has run out (for now)
// The call to maxUploadReadLength will recognise that we've run out of our allotted bandwidth limit, and sleep this thread for the rest of the measurement period
// This method will be called again, but we'll almost certainly return YES the next time around, because we'll have more limit to use up
// The NO returns seem to snap CFNetwork out of its reverie, and return control to the main loop in loadRequest, so that we can manage timeouts and progress delegate updates
- (BOOL)hasBytesAvailable
{
if ([ASIHTTPRequest isBandwidthThrottled]) {
if ([ASIHTTPRequest maxUploadReadLength] == 0) {
NSLog(@"no");
return NO;
}
NSLog(@"yes");
}
return [[self stream] hasBytesAvailable];
}
// Called when CFNetwork wants to read more of our request body
// When throttling is on, we ask ASIHTTPRequest for the maximum amount of data we can read
- (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len
{
unsigned long toRead = [ASIHTTPRequest maxUploadReadLength];
//NSLog(@"may read %lu",toRead);
unsigned long toRead = len;
if ([ASIHTTPRequest isBandwidthThrottled]) {
toRead = [ASIHTTPRequest maxUploadReadLength];
if (toRead > len) {
toRead = len;
// Hopefully this won't happen because hasBytesAvailable will have returned NO, but just in case - we need to read at least 1 byte, or bad things might happen
} else if (toRead == 0) {
toRead = 1;
}
//toRead = len;
NSLog(@"Throttled read %u",toRead);
} else {
NSLog(@"Unthrottled read %u",toRead);
}
[ASIHTTPRequest incrementBandwidthUsedInLastSecond:toRead];
//NSLog(@"will read %lu",toRead);
return [[self stream] read:buffer maxLength:toRead];
}
// If we get asked to perform a method we don't have (which is almost all of them), we'll just forward the message to our stream
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
return [[self stream] methodSignatureForSelector:aSelector];
... ...
... ... @@ -37,4 +37,6 @@
- (void)testCompression;
- (void)testSubclass;
- (void)testTimeOutWithoutDownloadDelegate;
- (void)testThrottlingDownloadBandwidth;
- (void)testThrottlingUploadBandwidth;
@end
... ...
... ... @@ -794,6 +794,68 @@
}
- (void)testThrottlingDownloadBandwidth
{
[ASIHTTPRequest setMaxBandwidthPerSecond:0];
// This content is around 128KB in size, and it won't be gzipped, so it should take more than 8 seconds to download at 14.5KB / second
// We'll test first without throttling
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:@"http://asi/ASIHTTPRequest/tests/the_great_american_novel_%28abridged%29.txt"]];
NSDate *date = [NSDate date];
[request start];
NSTimeInterval interval =[date timeIntervalSinceNow];
BOOL success = (interval > -7);
GHAssertTrue(success,@"Downloaded the file too slowly - either this is a bug, or your internet connection is too slow to run this test (must be able to download 128KB in less than 7 seconds, without throttling)");
// Now we'll test with throttling
[ASIHTTPRequest setMaxBandwidthPerSecond:ASIWWANBandwidthThrottleAmount];
request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:@"http://asi/ASIHTTPRequest/tests/the_great_american_novel_%28abridged%29.txt"]];
date = [NSDate date];
[request start];
[ASIHTTPRequest setMaxBandwidthPerSecond:0];
interval =[date timeIntervalSinceNow];
success = (interval < -7);
GHAssertTrue(success,@"Failed to throttle download");
GHAssertNil([request error],@"Request generated an error - timeout?");
}
- (void)testThrottlingUploadBandwidth
{
[ASIHTTPRequest setMaxBandwidthPerSecond:0];
// Create a 64KB request body
NSData *data = [[[NSMutableData alloc] initWithLength:64*1024] autorelease];
// We'll test first without throttling
ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:@"http://asi/ignore"]];
[request appendPostData:data];
NSDate *date = [NSDate date];
[request start];
NSTimeInterval interval =[date timeIntervalSinceNow];
BOOL success = (interval > -3);
GHAssertTrue(success,@"Uploaded the data too slowly - either this is a bug, or your internet connection is too slow to run this test (must be able to upload 64KB in less than 3 seconds, without throttling)");
// Now we'll test with throttling
[ASIHTTPRequest setMaxBandwidthPerSecond:ASIWWANBandwidthThrottleAmount];
request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:@"http://asi/ignore"]];
[request appendPostData:data];
date = [NSDate date];
[request start];
[ASIHTTPRequest setMaxBandwidthPerSecond:0];
interval =[date timeIntervalSinceNow];
success = (interval < -3);
GHAssertTrue(success,@"Failed to throttle upload");
GHAssertNil([request error],@"Request generated an error - timeout?");
}
@end
... ...