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; @@ -462,8 +462,17 @@ extern unsigned long const ASIWWANBandwidthThrottleAmount;
462 + (unsigned long)maxBandwidthPerSecond; 462 + (unsigned long)maxBandwidthPerSecond;
463 + (void)setMaxBandwidthPerSecond:(unsigned long)bytes; 463 + (void)setMaxBandwidthPerSecond:(unsigned long)bytes;
464 464
  465 +// Get a rough average (for the last 5 seconds) of how much bandwidth is being used, in bytes
465 + (unsigned long)averageBandwidthUsedPerSecond; 466 + (unsigned long)averageBandwidthUsedPerSecond;
466 467
  468 +// Will return YES is bandwidth throttling is currently in use
  469 ++ (BOOL)isBandwidthThrottled;
  470 +
  471 +// Used internally to record bandwidth use, and by ASIInputStreams when uploading. It's probably best if you don't mess with this.
  472 ++ (void)incrementBandwidthUsedInLastSecond:(unsigned long)bytes;
  473 +
  474 +// On iPhone only, ASIHTTPRequest can automatically turn throttling on and off as the connection type changes between WWAN and WiFi
  475 +
467 #if TARGET_OS_IPHONE 476 #if TARGET_OS_IPHONE
468 // Set to YES to automatically turn on throttling when WWAN is connected, and automatically turn it off when it isn't 477 // Set to YES to automatically turn on throttling when WWAN is connected, and automatically turn it off when it isn't
469 + (void)setShouldThrottleBandwidthForWWAN:(BOOL)throttle; 478 + (void)setShouldThrottleBandwidthForWWAN:(BOOL)throttle;
@@ -72,16 +72,14 @@ unsigned long const ASIWWANBandwidthThrottleAmount = 14800; @@ -72,16 +72,14 @@ unsigned long const ASIWWANBandwidthThrottleAmount = 14800;
72 // YES when bandwidth throttling is active 72 // YES when bandwidth throttling is active
73 // This flag does not denote whether throttling is turned on - rather whether it is currently in use 73 // This flag does not denote whether throttling is turned on - rather whether it is currently in use
74 // It will be set to NO when throttling is turned on, but a WI-FI connection is active 74 // It will be set to NO when throttling is turned on, but a WI-FI connection is active
75 -BOOL shouldThrottleBandwidth = NO; 75 +BOOL isBandwidthThrottled = NO;
76 76
77 // Private stuff 77 // Private stuff
78 @interface ASIHTTPRequest () 78 @interface ASIHTTPRequest ()
79 79
80 - (BOOL)askDelegateForCredentials; 80 - (BOOL)askDelegateForCredentials;
81 - (BOOL)askDelegateForProxyCredentials; 81 - (BOOL)askDelegateForProxyCredentials;
82 -+ (void)incrementBandwidthUsedInLastSecond:(unsigned long)bytes;  
83 + (void)measureBandwidthUsage; 82 + (void)measureBandwidthUsage;
84 -+ (BOOL)shouldThrottleBandwidth;  
85 + (void)recordBandwidthUsage; 83 + (void)recordBandwidthUsage;
86 84
87 @property (assign) BOOL complete; 85 @property (assign) BOOL complete;
@@ -1660,7 +1658,7 @@ BOOL shouldThrottleBandwidth = NO; @@ -1660,7 +1658,7 @@ BOOL shouldThrottleBandwidth = NO;
1660 // Reduce the buffer size if we're receiving data too quickly when bandwidth throttling is active 1658 // Reduce the buffer size if we're receiving data too quickly when bandwidth throttling is active
1661 // This just augments the throttling done in measureBandwidthUsage to reduce the amount we go over the limit 1659 // This just augments the throttling done in measureBandwidthUsage to reduce the amount we go over the limit
1662 1660
1663 - if ([[self class] shouldThrottleBandwidth]) { 1661 + if ([[self class] isBandwidthThrottled]) {
1664 [bandwidthThrottlingLock lock]; 1662 [bandwidthThrottlingLock lock];
1665 if (maxBandwidthPerSecond > 0) { 1663 if (maxBandwidthPerSecond > 0) {
1666 long long maxSize = (long long)maxBandwidthPerSecond-(long long)bandwidthUsedInLastSecond; 1664 long long maxSize = (long long)maxBandwidthPerSecond-(long long)bandwidthUsedInLastSecond;
@@ -2319,11 +2317,11 @@ BOOL shouldThrottleBandwidth = NO; @@ -2319,11 +2317,11 @@ BOOL shouldThrottleBandwidth = NO;
2319 2317
2320 #pragma mark bandwidth measurement / throttling 2318 #pragma mark bandwidth measurement / throttling
2321 2319
2322 -+ (BOOL)shouldThrottleBandwidth 2320 ++ (BOOL)isBandwidthThrottled
2323 { 2321 {
2324 #if TARGET_OS_IPHONE 2322 #if TARGET_OS_IPHONE
2325 [bandwidthThrottlingLock lock]; 2323 [bandwidthThrottlingLock lock];
2326 - BOOL throttle = shouldThrottleBandwidth; 2324 + BOOL throttle = isBandwidthThrottled;
2327 [bandwidthThrottlingLock unlock]; 2325 [bandwidthThrottlingLock unlock];
2328 return throttle; 2326 return throttle;
2329 #else 2327 #else
@@ -2419,7 +2417,7 @@ BOOL shouldThrottleBandwidth = NO; @@ -2419,7 +2417,7 @@ BOOL shouldThrottleBandwidth = NO;
2419 } 2417 }
2420 2418
2421 #if TARGET_OS_IPHONE 2419 #if TARGET_OS_IPHONE
2422 -+ (void)setShouldThrottleBandwidthForWWAN:(BOOL)throttle 2420 ++ (void)setisBandwidthThrottledForWWAN:(BOOL)throttle
2423 { 2421 {
2424 if (throttle) { 2422 if (throttle) {
2425 [ASIHTTPRequest throttleBandwidthForWWANUsingLimit:ASIWWANBandwidthThrottleAmount]; 2423 [ASIHTTPRequest throttleBandwidthForWWANUsingLimit:ASIWWANBandwidthThrottleAmount];
@@ -2443,9 +2441,9 @@ BOOL shouldThrottleBandwidth = NO; @@ -2443,9 +2441,9 @@ BOOL shouldThrottleBandwidth = NO;
2443 { 2441 {
2444 [bandwidthThrottlingLock lock]; 2442 [bandwidthThrottlingLock lock];
2445 if ([[Reachability sharedReachability] internetConnectionStatus] == ReachableViaCarrierDataNetwork) { 2443 if ([[Reachability sharedReachability] internetConnectionStatus] == ReachableViaCarrierDataNetwork) {
2446 - shouldThrottleBandwidth = YES; 2444 + isBandwidthThrottled = YES;
2447 } else { 2445 } else {
2448 - shouldThrottleBandwidth = NO; 2446 + isBandwidthThrottled = NO;
2449 } 2447 }
2450 [bandwidthThrottlingLock unlock]; 2448 [bandwidthThrottlingLock unlock];
2451 } 2449 }
@@ -2455,15 +2453,18 @@ BOOL shouldThrottleBandwidth = NO; @@ -2455,15 +2453,18 @@ BOOL shouldThrottleBandwidth = NO;
2455 { 2453 {
2456 2454
2457 [bandwidthThrottlingLock lock]; 2455 [bandwidthThrottlingLock lock];
2458 - unsigned long toRead = 4096; 2456 +
2459 - if (maxBandwidthPerSecond) { 2457 + // 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
2460 - toRead = maxBandwidthPerSecond/32; 2458 + long long toRead = maxBandwidthPerSecond/4;
2461 - }  
2462 if (maxBandwidthPerSecond > 0 && (bandwidthUsedInLastSecond + toRead > maxBandwidthPerSecond)) { 2459 if (maxBandwidthPerSecond > 0 && (bandwidthUsedInLastSecond + toRead > maxBandwidthPerSecond)) {
2463 - toRead = 0; 2460 + toRead = maxBandwidthPerSecond-bandwidthUsedInLastSecond;
  2461 + if (toRead < 0) {
  2462 + toRead = 0;
  2463 + }
2464 } 2464 }
2465 2465
2466 if (toRead == 0 || !bandwidthMeasurementDate || [bandwidthMeasurementDate timeIntervalSinceNow] < -0) { 2466 if (toRead == 0 || !bandwidthMeasurementDate || [bandwidthMeasurementDate timeIntervalSinceNow] < -0) {
  2467 + NSLog(@"sleep");
2467 [NSThread sleepUntilDate:bandwidthMeasurementDate]; 2468 [NSThread sleepUntilDate:bandwidthMeasurementDate];
2468 [self recordBandwidthUsage]; 2469 [self recordBandwidthUsage];
2469 } 2470 }
@@ -31,32 +31,48 @@ @@ -31,32 +31,48 @@
31 [super dealloc]; 31 [super dealloc];
32 } 32 }
33 33
  34 +
  35 +// Ok, so this works, but I don't really understand why.
  36 +// 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
  37 +// We return NO when we shouldn't be uploading any more data because our bandwidth limit has run out (for now)
  38 +// 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
  39 +// 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
  40 +// 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
34 - (BOOL)hasBytesAvailable 41 - (BOOL)hasBytesAvailable
35 { 42 {
36 - if ([ASIHTTPRequest maxUploadReadLength] == 0) { 43 + if ([ASIHTTPRequest isBandwidthThrottled]) {
37 - NSLog(@"no"); 44 + if ([ASIHTTPRequest maxUploadReadLength] == 0) {
38 - return NO; 45 + return NO;
  46 + }
39 } 47 }
40 - NSLog(@"yes");  
41 return [[self stream] hasBytesAvailable]; 48 return [[self stream] hasBytesAvailable];
42 49
43 } 50 }
44 51
  52 +// Called when CFNetwork wants to read more of our request body
  53 +// When throttling is on, we ask ASIHTTPRequest for the maximum amount of data we can read
45 - (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len 54 - (NSInteger)read:(uint8_t *)buffer maxLength:(NSUInteger)len
46 { 55 {
47 - unsigned long toRead = [ASIHTTPRequest maxUploadReadLength]; 56 + unsigned long toRead = len;
48 - //NSLog(@"may read %lu",toRead); 57 + if ([ASIHTTPRequest isBandwidthThrottled]) {
49 - if (toRead > len) { 58 + toRead = [ASIHTTPRequest maxUploadReadLength];
50 - toRead = len; 59 + if (toRead > len) {
51 - } else if (toRead == 0) { 60 + toRead = len;
52 - toRead = 1; 61 +
  62 + // 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
  63 + } else if (toRead == 0) {
  64 + toRead = 1;
  65 + }
  66 + NSLog(@"Throttled read %u",toRead);
  67 + } else {
  68 + NSLog(@"Unthrottled read %u",toRead);
53 } 69 }
54 - //toRead = len;  
55 [ASIHTTPRequest incrementBandwidthUsedInLastSecond:toRead]; 70 [ASIHTTPRequest incrementBandwidthUsedInLastSecond:toRead];
56 - //NSLog(@"will read %lu",toRead);  
57 return [[self stream] read:buffer maxLength:toRead]; 71 return [[self stream] read:buffer maxLength:toRead];
58 } 72 }
59 73
  74 +// 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
  75 +
60 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector 76 - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
61 { 77 {
62 return [[self stream] methodSignatureForSelector:aSelector]; 78 return [[self stream] methodSignatureForSelector:aSelector];
@@ -37,4 +37,6 @@ @@ -37,4 +37,6 @@
37 - (void)testCompression; 37 - (void)testCompression;
38 - (void)testSubclass; 38 - (void)testSubclass;
39 - (void)testTimeOutWithoutDownloadDelegate; 39 - (void)testTimeOutWithoutDownloadDelegate;
  40 +- (void)testThrottlingDownloadBandwidth;
  41 +- (void)testThrottlingUploadBandwidth;
40 @end 42 @end
@@ -794,6 +794,68 @@ @@ -794,6 +794,68 @@
794 } 794 }
795 795
796 796
  797 +- (void)testThrottlingDownloadBandwidth
  798 +{
  799 + [ASIHTTPRequest setMaxBandwidthPerSecond:0];
  800 +
  801 + // 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
  802 + // We'll test first without throttling
  803 + ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:@"http://asi/ASIHTTPRequest/tests/the_great_american_novel_%28abridged%29.txt"]];
  804 + NSDate *date = [NSDate date];
  805 + [request start];
  806 +
  807 + NSTimeInterval interval =[date timeIntervalSinceNow];
  808 + BOOL success = (interval > -7);
  809 + 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)");
  810 +
  811 + // Now we'll test with throttling
  812 + [ASIHTTPRequest setMaxBandwidthPerSecond:ASIWWANBandwidthThrottleAmount];
  813 + request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:@"http://asi/ASIHTTPRequest/tests/the_great_american_novel_%28abridged%29.txt"]];
  814 + date = [NSDate date];
  815 + [request start];
  816 +
  817 + [ASIHTTPRequest setMaxBandwidthPerSecond:0];
  818 +
  819 + interval =[date timeIntervalSinceNow];
  820 + success = (interval < -7);
  821 + GHAssertTrue(success,@"Failed to throttle download");
  822 + GHAssertNil([request error],@"Request generated an error - timeout?");
  823 +
  824 +}
  825 +
  826 +- (void)testThrottlingUploadBandwidth
  827 +{
  828 + [ASIHTTPRequest setMaxBandwidthPerSecond:0];
  829 +
  830 + // Create a 64KB request body
  831 + NSData *data = [[[NSMutableData alloc] initWithLength:64*1024] autorelease];
  832 +
  833 + // We'll test first without throttling
  834 + ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:@"http://asi/ignore"]];
  835 + [request appendPostData:data];
  836 + NSDate *date = [NSDate date];
  837 + [request start];
  838 +
  839 + NSTimeInterval interval =[date timeIntervalSinceNow];
  840 + BOOL success = (interval > -3);
  841 + 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)");
  842 +
  843 + // Now we'll test with throttling
  844 + [ASIHTTPRequest setMaxBandwidthPerSecond:ASIWWANBandwidthThrottleAmount];
  845 + request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:@"http://asi/ignore"]];
  846 + [request appendPostData:data];
  847 + date = [NSDate date];
  848 + [request start];
  849 +
  850 + [ASIHTTPRequest setMaxBandwidthPerSecond:0];
  851 +
  852 + interval =[date timeIntervalSinceNow];
  853 + success = (interval < -3);
  854 + GHAssertTrue(success,@"Failed to throttle upload");
  855 + GHAssertNil([request error],@"Request generated an error - timeout?");
  856 +
  857 +}
  858 +
797 @end 859 @end
798 860
799 861