Add tests for upload / download throttling
Cleanup API More comments as I'll never remember what all this stuff was for
Showing
5 changed files
with
116 additions
and
26 deletions
| @@ -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 |
-
Please register or login to post a comment