Ben Copsey

Use magic to narrow the NSURLConnection performance gap to almost nothing :)

@@ -21,7 +21,7 @@ @@ -21,7 +21,7 @@
21 #import "ASIInputStream.h" 21 #import "ASIInputStream.h"
22 22
23 // Automatically set on build 23 // Automatically set on build
24 -NSString *ASIHTTPRequestVersion = @"v1.2-53 2009-12-19"; 24 +NSString *ASIHTTPRequestVersion = @"v1.2-54 2009-12-19";
25 25
26 NSString* const NetworkRequestErrorDomain = @"ASIHTTPRequestErrorDomain"; 26 NSString* const NetworkRequestErrorDomain = @"ASIHTTPRequestErrorDomain";
27 27
@@ -56,6 +56,10 @@ static NSError *ASITooMuchRedirectionError; @@ -56,6 +56,10 @@ static NSError *ASITooMuchRedirectionError;
56 static NSMutableArray *bandwidthUsageTracker = nil; 56 static NSMutableArray *bandwidthUsageTracker = nil;
57 static unsigned long averageBandwidthUsedPerSecond = 0; 57 static unsigned long averageBandwidthUsedPerSecond = 0;
58 58
  59 +// These are used for queuing persistent connections on the same connection
  60 +static unsigned char streamNumber = 0;
  61 +static void *streamIDs[4];
  62 +
59 // Records how much bandwidth all requests combined have used in the last second 63 // Records how much bandwidth all requests combined have used in the last second
60 static unsigned long bandwidthUsedInLastSecond = 0; 64 static unsigned long bandwidthUsedInLastSecond = 0;
61 65
@@ -116,6 +120,8 @@ static BOOL isiPhoneOS2; @@ -116,6 +120,8 @@ static BOOL isiPhoneOS2;
116 // Start the read stream. Called by loadRequest, and again to restart the request when authentication is needed 120 // Start the read stream. Called by loadRequest, and again to restart the request when authentication is needed
117 - (void)startRequest; 121 - (void)startRequest;
118 122
  123 +- (void)markAsFinished;
  124 +
119 #if TARGET_OS_IPHONE 125 #if TARGET_OS_IPHONE
120 + (void)registerForNetworkReachabilityNotifications; 126 + (void)registerForNetworkReachabilityNotifications;
121 + (void)unsubscribeFromNetworkReachabilityNotifications; 127 + (void)unsubscribeFromNetworkReachabilityNotifications;
@@ -177,6 +183,11 @@ static BOOL isiPhoneOS2; @@ -177,6 +183,11 @@ static BOOL isiPhoneOS2;
177 ASIUnableToCreateRequestError = [[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIUnableToCreateRequestErrorType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Unable to create request (bad url?)",NSLocalizedDescriptionKey,nil]] retain]; 183 ASIUnableToCreateRequestError = [[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIUnableToCreateRequestErrorType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"Unable to create request (bad url?)",NSLocalizedDescriptionKey,nil]] retain];
178 ASITooMuchRedirectionError = [[NSError errorWithDomain:NetworkRequestErrorDomain code:ASITooMuchRedirectionErrorType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"The request failed because it redirected too many times",NSLocalizedDescriptionKey,nil]] retain]; 184 ASITooMuchRedirectionError = [[NSError errorWithDomain:NetworkRequestErrorDomain code:ASITooMuchRedirectionErrorType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:@"The request failed because it redirected too many times",NSLocalizedDescriptionKey,nil]] retain];
179 185
  186 + // IDs that will be used for the four streams we'll create (see
  187 + char i;
  188 + for (i=0; i<4; i++) {
  189 + streamIDs[i] = (void *)CFNumberCreate(kCFAllocatorDefault, kCFNumberCharType, &i);
  190 + }
180 #if TARGET_OS_IPHONE 191 #if TARGET_OS_IPHONE
181 isiPhoneOS2 = ((floorf([[[UIDevice currentDevice] systemVersion] floatValue]) == 2.0) ? YES : NO); 192 isiPhoneOS2 = ((floorf([[[UIDevice currentDevice] systemVersion] floatValue]) == 2.0) ? YES : NO);
182 #else 193 #else
@@ -502,7 +513,11 @@ static BOOL isiPhoneOS2; @@ -502,7 +513,11 @@ static BOOL isiPhoneOS2;
502 513
503 // On Leopard, we'll create the thread ourselves 514 // On Leopard, we'll create the thread ourselves
504 } else { 515 } else {
  516 + if ([self shouldRunInBackgroundThread]) {
505 [self performSelectorInBackground:@selector(startAsynchronous) withObject:nil]; 517 [self performSelectorInBackground:@selector(startAsynchronous) withObject:nil];
  518 + } else {
  519 + [self startAsynchronous];
  520 + }
506 } 521 }
507 #endif 522 #endif
508 } 523 }
@@ -811,8 +826,23 @@ static BOOL isiPhoneOS2; @@ -811,8 +826,23 @@ static BOOL isiPhoneOS2;
811 } 826 }
812 827
813 // Use a persistent connection if possible 828 // Use a persistent connection if possible
  829 + if (shouldAttemptPersistentConnection) {
814 CFReadStreamSetProperty(readStream, kCFStreamPropertyHTTPAttemptPersistentConnection, kCFBooleanTrue); 830 CFReadStreamSetProperty(readStream, kCFStreamPropertyHTTPAttemptPersistentConnection, kCFBooleanTrue);
815 831
  832 + // Based on http://lists.apple.com/archives/macnetworkprog/2008/Dec/msg00001.html
  833 + // Basically, we aim to open a maximum of 4 connections (each one with a different id), and then subsequent requests will try to re-use the same stream, assuming we're connecting to the same server
  834 + // I'm guessing this will perform less well when you're connecting to several different servers at once
  835 + // But if you aren't, this appears to be the magic bullet for matching NSURLConnection's performance
  836 +
  837 + // We will re-use the previous ID for a synchronous request, since that probably gives us a greater chance of maximimising connection re-use
  838 + if (![self isSynchronous]) {
  839 + streamNumber++;
  840 + if (streamNumber == 4) {
  841 + streamNumber = 0;
  842 + }
  843 + }
  844 + CFReadStreamSetProperty(readStream, CFSTR("ASIStreamID"), streamIDs[streamNumber]);
  845 + }
816 846
817 // Handle proxy settings 847 // Handle proxy settings
818 848
@@ -889,6 +919,7 @@ static BOOL isiPhoneOS2; @@ -889,6 +919,7 @@ static BOOL isiPhoneOS2;
889 return; 919 return;
890 } 920 }
891 921
  922 +
892 [[self cancelledLock] unlock]; 923 [[self cancelledLock] unlock];
893 924
894 if (shouldResetProgressIndicators) { 925 if (shouldResetProgressIndicators) {
@@ -919,14 +950,14 @@ static BOOL isiPhoneOS2; @@ -919,14 +950,14 @@ static BOOL isiPhoneOS2;
919 950
920 [NSTimer scheduledTimerWithTimeInterval:0.25 target:self selector:@selector(updateStatus:) userInfo:nil repeats:YES]; 951 [NSTimer scheduledTimerWithTimeInterval:0.25 target:self selector:@selector(updateStatus:) userInfo:nil repeats:YES];
921 952
922 - // If we're running asynchronously on the main thread, the runloop will already be running 953 + // If we're running asynchronously on the main thread, the runloop will already be running and we can return control
923 if (![NSThread isMainThread]) { 954 if (![NSThread isMainThread]) {
924 - // Will stop automatically when the request is done 955 + while (!complete) {
925 CFRunLoopRun(); 956 CFRunLoopRun();
926 } 957 }
  958 + }
927 } 959 }
928 960
929 -  
930 // This is the main loop for synchronous requests. 961 // This is the main loop for synchronous requests.
931 - (void)loadSynchronous 962 - (void)loadSynchronous
932 { 963 {
@@ -935,31 +966,22 @@ static BOOL isiPhoneOS2; @@ -935,31 +966,22 @@ static BOOL isiPhoneOS2;
935 [self scheduleReadStream]; 966 [self scheduleReadStream];
936 } 967 }
937 968
938 -// if ([NSThread isMainThread]) { 969 + // If we don't need to track progress or throttle bandwidth, we won't bother to check up the status of the request (faster)
939 -// [NSTimer scheduledTimerWithTimeInterval:0.25 target:self selector:@selector(updateStatus:) userInfo:nil repeats:YES]; 970 + if (downloadProgressDelegate || uploadProgressDelegate || queue || [[self class] isBandwidthThrottled]) {
940 -// CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeOutSeconds, NO); 971 + [NSTimer scheduledTimerWithTimeInterval:0.25 target:self selector:@selector(updateStatus:) userInfo:nil repeats:YES];
941 -// 972 + }
942 -// } else if (!uploadProgressDelegate && !downloadProgressDelegate) {  
943 -// CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeOutSeconds, NO);  
944 -// [self checkRequestStatus];  
945 -// } else {  
946 -  
947 while (!complete) { 973 while (!complete) {
948 - [self checkRequestStatus]; 974 + CFRunLoopRun();
949 - CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0.25, NO);  
950 } 975 }
951 - //} 976 +
952 } 977 }
953 978
954 // This gets fired every 1/4 of a second in asynchronous requests to update the progress and work out if we need to timeout 979 // This gets fired every 1/4 of a second in asynchronous requests to update the progress and work out if we need to timeout
955 - (void)updateStatus:(NSTimer*)timer 980 - (void)updateStatus:(NSTimer*)timer
956 { 981 {
  982 + //NSLog(@"foo");
957 [self checkRequestStatus]; 983 [self checkRequestStatus];
958 if ([self complete] || [self error]) { 984 if ([self complete] || [self error]) {
959 - if (![self error]) {  
960 - [self willChangeValueForKey:@"isFinished"];  
961 - [self didChangeValueForKey:@"isFinished"];  
962 - }  
963 [timer invalidate]; 985 [timer invalidate];
964 CFRunLoopStop(CFRunLoopGetCurrent()); 986 CFRunLoopStop(CFRunLoopGetCurrent());
965 } 987 }
@@ -1025,6 +1047,9 @@ static BOOL isiPhoneOS2; @@ -1025,6 +1047,9 @@ static BOOL isiPhoneOS2;
1025 // readStream will be null if we aren't currently running (perhaps we're waiting for a delegate to supply credentials) 1047 // readStream will be null if we aren't currently running (perhaps we're waiting for a delegate to supply credentials)
1026 if (readStream) { 1048 if (readStream) {
1027 1049
  1050 + // If we have a post body
  1051 + if ([self postLength]) {
  1052 +
1028 // Find out if we've sent any more data than last time, and reset the timeout if so 1053 // Find out if we've sent any more data than last time, and reset the timeout if so
1029 if (totalBytesSent > lastBytesSent) { 1054 if (totalBytesSent > lastBytesSent) {
1030 [self setLastActivityTime:[NSDate date]]; 1055 [self setLastActivityTime:[NSDate date]];
@@ -1035,6 +1060,8 @@ static BOOL isiPhoneOS2; @@ -1035,6 +1060,8 @@ static BOOL isiPhoneOS2;
1035 [self setTotalBytesSent:[[(NSNumber *)CFReadStreamCopyProperty(readStream, kCFStreamPropertyHTTPRequestBytesWrittenCount) autorelease] unsignedLongLongValue]]; 1060 [self setTotalBytesSent:[[(NSNumber *)CFReadStreamCopyProperty(readStream, kCFStreamPropertyHTTPRequestBytesWrittenCount) autorelease] unsignedLongLongValue]];
1036 [ASIHTTPRequest incrementBandwidthUsedInLastSecond:(unsigned long)(totalBytesSent-lastBytesSent)]; 1061 [ASIHTTPRequest incrementBandwidthUsedInLastSecond:(unsigned long)(totalBytesSent-lastBytesSent)];
1037 1062
  1063 + }
  1064 +
1038 [self updateProgressIndicators]; 1065 [self updateProgressIndicators];
1039 1066
1040 } 1067 }
@@ -1458,12 +1485,12 @@ static BOOL isiPhoneOS2; @@ -1458,12 +1485,12 @@ static BOOL isiPhoneOS2;
1458 if ([failedRequest didFailSelector] && [[failedRequest delegate] respondsToSelector:[failedRequest didFailSelector]]) { 1485 if ([failedRequest didFailSelector] && [[failedRequest delegate] respondsToSelector:[failedRequest didFailSelector]]) {
1459 [[failedRequest delegate] performSelectorOnMainThread:[failedRequest didFailSelector] withObject:failedRequest waitUntilDone:[NSThread isMainThread]]; 1486 [[failedRequest delegate] performSelectorOnMainThread:[failedRequest didFailSelector] withObject:failedRequest waitUntilDone:[NSThread isMainThread]];
1460 } 1487 }
1461 - [self willChangeValueForKey:@"isFinished"];  
1462 - [self didChangeValueForKey:@"isFinished"];  
1463 1488
1464 if ([self mainRequest]) { 1489 if ([self mainRequest]) {
1465 [[self mainRequest] failWithError:[self error]]; 1490 [[self mainRequest] failWithError:[self error]];
1466 } 1491 }
  1492 +
  1493 + [self markAsFinished];
1467 } 1494 }
1468 1495
1469 #pragma mark parsing HTTP response headers 1496 #pragma mark parsing HTTP response headers
@@ -2258,14 +2285,12 @@ static BOOL isiPhoneOS2; @@ -2258,14 +2285,12 @@ static BOOL isiPhoneOS2;
2258 if ([self needsRedirect]) { 2285 if ([self needsRedirect]) {
2259 return; 2286 return;
2260 } 2287 }
2261 -// long long bufferSize = 2048; 2288 + long long bufferSize = 16384;
2262 -// if (contentLength > 262144) { 2289 + if (contentLength > 262144) {
2263 -// bufferSize = 65536; 2290 + bufferSize = 262144;
2264 -// } else if (contentLength > 65536) { 2291 + } else if (contentLength > 65536) {
2265 -// bufferSize = 16384; 2292 + bufferSize = 65536;
2266 -// } 2293 + }
2267 -  
2268 - long long bufferSize = 262144;  
2269 2294
2270 // Reduce the buffer size if we're receiving data too quickly when bandwidth throttling is active 2295 // Reduce the buffer size if we're receiving data too quickly when bandwidth throttling is active
2271 // This just augments the throttling done in measureBandwidthUsage to reduce the amount we go over the limit 2296 // This just augments the throttling done in measureBandwidthUsage to reduce the amount we go over the limit
@@ -2289,7 +2314,7 @@ static BOOL isiPhoneOS2; @@ -2289,7 +2314,7 @@ static BOOL isiPhoneOS2;
2289 } 2314 }
2290 2315
2291 2316
2292 - 2317 + //NSLog(@"read");
2293 UInt8 buffer[bufferSize]; 2318 UInt8 buffer[bufferSize];
2294 CFIndex bytesRead = CFReadStreamRead(readStream, buffer, sizeof(buffer)); 2319 CFIndex bytesRead = CFReadStreamRead(readStream, buffer, sizeof(buffer));
2295 2320
@@ -2398,6 +2423,14 @@ static BOOL isiPhoneOS2; @@ -2398,6 +2423,14 @@ static BOOL isiPhoneOS2;
2398 } else { 2423 } else {
2399 [self requestFinished]; 2424 [self requestFinished];
2400 } 2425 }
  2426 + [self markAsFinished];
  2427 +}
  2428 +
  2429 +- (void)markAsFinished
  2430 +{
  2431 + [self willChangeValueForKey:@"isFinished"];
  2432 + [self didChangeValueForKey:@"isFinished"];
  2433 + CFRunLoopStop(CFRunLoopGetCurrent());
2401 } 2434 }
2402 2435
2403 2436
@@ -2410,7 +2443,6 @@ static BOOL isiPhoneOS2; @@ -2410,7 +2443,6 @@ static BOOL isiPhoneOS2;
2410 2443
2411 if (![self error]) { // We may already have handled this error 2444 if (![self error]) { // We may already have handled this error
2412 2445
2413 -  
2414 NSString *reason = @"A connection failure occurred"; 2446 NSString *reason = @"A connection failure occurred";
2415 2447
2416 // We'll use a custom error message for SSL errors, but you should always check underlying error if you want more details 2448 // We'll use a custom error message for SSL errors, but you should always check underlying error if you want more details
@@ -2424,6 +2456,7 @@ static BOOL isiPhoneOS2; @@ -2424,6 +2456,7 @@ static BOOL isiPhoneOS2;
2424 2456
2425 [self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIConnectionFailureErrorType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:reason,NSLocalizedDescriptionKey,underlyingError,NSUnderlyingErrorKey,nil]]]; 2457 [self failWithError:[NSError errorWithDomain:NetworkRequestErrorDomain code:ASIConnectionFailureErrorType userInfo:[NSDictionary dictionaryWithObjectsAndKeys:reason,NSLocalizedDescriptionKey,underlyingError,NSUnderlyingErrorKey,nil]]];
2426 } 2458 }
  2459 + [self checkRequestStatus];
2427 } 2460 }
2428 2461
2429 #pragma mark managing the read stream 2462 #pragma mark managing the read stream
@@ -24,7 +24,8 @@ @@ -24,7 +24,8 @@
24 24
25 - (void)setUp 25 - (void)setUp
26 { 26 {
27 - [self setTestURL:[NSURL URLWithString:@"http://allseeing-i.com"]]; 27 + [self setTestURL:[NSURL URLWithString:@"http://allseeing-i.com/ASIHTTPRequest/tests/the_great_american_novel_%28abridged%29.txt"]];
  28 + //[self setTestURL:[NSURL URLWithString:@"http://allseeing-i.com"]];
28 } 29 }
29 30
30 - (void)testASIHTTPRequestSynchronousPerformance 31 - (void)testASIHTTPRequestSynchronousPerformance
@@ -47,7 +48,6 @@ @@ -47,7 +48,6 @@
47 [request addRequestHeader:@"Accept-Language" value:@"en/us"]; 48 [request addRequestHeader:@"Accept-Language" value:@"en/us"];
48 [request setUseCookiePersistance:NO]; 49 [request setUseCookiePersistance:NO];
49 [request setUseSessionPersistance:NO]; 50 [request setUseSessionPersistance:NO];
50 - //[request setShouldRunInBackgroundThread:YES];  
51 [request startSynchronous]; 51 [request startSynchronous];
52 if ([request error]) { 52 if ([request error]) {
53 NSLog(@"Request failed - cannot proceed with test"); 53 NSLog(@"Request failed - cannot proceed with test");
@@ -116,7 +116,7 @@ @@ -116,7 +116,7 @@
116 [self performSelectorOnMainThread:@selector(startASIHTTPRequests) withObject:nil waitUntilDone:NO]; 116 [self performSelectorOnMainThread:@selector(startASIHTTPRequests) withObject:nil waitUntilDone:NO];
117 } 117 }
118 118
119 -- (void)testASIHTTPRequestAsyncPerformanceWithQueue 119 +- (void)testQueuedASIHTTPRequestAsyncPerformance
120 { 120 {
121 [self performSelectorOnMainThread:@selector(startASIHTTPRequestsWithQueue) withObject:nil waitUntilDone:NO]; 121 [self performSelectorOnMainThread:@selector(startASIHTTPRequestsWithQueue) withObject:nil waitUntilDone:NO];
122 } 122 }
@@ -129,13 +129,15 @@ @@ -129,13 +129,15 @@
129 [self setTestStartDate:[NSDate date]]; 129 [self setTestStartDate:[NSDate date]];
130 int i; 130 int i;
131 for (i=0; i<10; i++) { 131 for (i=0; i<10; i++) {
132 - ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:@"http://allseeing-i.com/ASIHTTPRequest/tests/the_great_american_novel_(abridged).txt"]]; 132 + ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:testURL];
133 //Send the same headers as NSURLRequest 133 //Send the same headers as NSURLRequest
134 [request addRequestHeader:@"Pragma" value:@"no-cache"]; 134 [request addRequestHeader:@"Pragma" value:@"no-cache"];
135 [request addRequestHeader:@"Accept" value:@"*/*"]; 135 [request addRequestHeader:@"Accept" value:@"*/*"];
136 [request addRequestHeader:@"Accept-Language" value:@"en/us"]; 136 [request addRequestHeader:@"Accept-Language" value:@"en/us"];
  137 + [request setUseCookiePersistance:NO];
  138 + [request setUseSessionPersistance:NO];
137 [request setDelegate:self]; 139 [request setDelegate:self];
138 - [request start]; 140 + [request startAsynchronous];
139 } 141 }
140 } 142 }
141 143
@@ -146,13 +148,17 @@ @@ -146,13 +148,17 @@
146 [self setTestStartDate:[NSDate date]]; 148 [self setTestStartDate:[NSDate date]];
147 int i; 149 int i;
148 NSOperationQueue *queue = [[[NSOperationQueue alloc] init] autorelease]; 150 NSOperationQueue *queue = [[[NSOperationQueue alloc] init] autorelease];
  151 + [queue setMaxConcurrentOperationCount:4];
149 for (i=0; i<10; i++) { 152 for (i=0; i<10; i++) {
150 - ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:[NSURL URLWithString:@"http://allseeing-i.com/ASIHTTPRequest/tests/the_great_american_novel_(abridged).txt"]]; 153 + ASIHTTPRequest *request = [ASIHTTPRequest requestWithURL:testURL];
151 //Send the same headers as NSURLRequest 154 //Send the same headers as NSURLRequest
152 [request addRequestHeader:@"Pragma" value:@"no-cache"]; 155 [request addRequestHeader:@"Pragma" value:@"no-cache"];
153 [request addRequestHeader:@"Accept" value:@"*/*"]; 156 [request addRequestHeader:@"Accept" value:@"*/*"];
154 [request addRequestHeader:@"Accept-Language" value:@"en/us"]; 157 [request addRequestHeader:@"Accept-Language" value:@"en/us"];
  158 + [request setUseCookiePersistance:NO];
  159 + [request setUseSessionPersistance:NO];
155 [request setDelegate:self]; 160 [request setDelegate:self];
  161 + [request setShouldRunInBackgroundThread:YES];
156 [queue addOperation:request]; 162 [queue addOperation:request];
157 } 163 }
158 } 164 }
@@ -185,7 +191,7 @@ @@ -185,7 +191,7 @@
185 191
186 int i; 192 int i;
187 for (i=0; i<10; i++) { 193 for (i=0; i<10; i++) {
188 - NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://allseeing-i.com/ASIHTTPRequest/tests/the_great_american_novel_(abridged).txt"] cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:10]; 194 + NSURLRequest *request = [NSURLRequest requestWithURL:testURL cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:10];
189 [[self responseData] addObject:[NSMutableData data]]; 195 [[self responseData] addObject:[NSMutableData data]];
190 NSURLConnectionSubclass *connection = [[[NSURLConnectionSubclass alloc] initWithRequest:request delegate:self startImmediately:YES] autorelease]; 196 NSURLConnectionSubclass *connection = [[[NSURLConnectionSubclass alloc] initWithRequest:request delegate:self startImmediately:YES] autorelease];
191 [connection setTag:i]; 197 [connection setTag:i];