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