NSOperation Queue behaving abnormally - ios

I have a task of uploading multiple images to the server one by one. So I am using the batch operation process for this. Every time I start the upload procedure, some operations specially the first one completes as soon as it starts and the image does not get uploaded, and then the batch upoad process continues fine with a rare glitch of missing the other images.
The code I am using is as follows:-
-(void)callWSToUploadRxs{
NSLog(#"the total assets maintained are %lu", (unsigned long)_arr_assetsMaintained.count);
NSMutableArray *mutableOperations = [NSMutableArray array];
int imageUploadCount = (int)[self extractFullSizeImagesToUpload].count;
// second for loop is to initialize the operations and then queue them.
for (int i = 0; i<imageUploadCount; i++) {
NSData *imageData = UIImageJPEGRepresentation([_arr_originalImagesToSend objectAtIndex:i],1.0);
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init];
[request setHTTPMethod:#"POST"];
NSLog(#"the url constructed is %#", [NSString stringWithFormat:#"%#/%#/%#/%#",uploadRxUrl,#"4004DD85-1421-4992-A811-8E2F3B2E49F7",#"5293",[_arr_imageNames objectAtIndex:i]]);
[request setURL:[NSURL URLWithString:[NSString stringWithFormat:#"%#/%#/%#/%#.jpg",uploadRxUrl,#"4004DD85-1421-4992-A811-8E2F3B2E49F7",#"5293",[_arr_imageNames objectAtIndex:i]]]];
[request setValue:#"binary/octet-stream" forHTTPHeaderField:#"Content-Type"];
[request setHTTPBody:imageData];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[mutableOperations addObject:operation];
}
currentUploadIndex++;
NSArray *operations = [AFURLConnectionOperation batchOfRequestOperations:mutableOperations progressBlock:^(NSUInteger numberOfFinishedOperations, NSUInteger totalNumberOfOperations) {
NSLog(#"%lu of %lu complete", numberOfFinishedOperations, totalNumberOfOperations);
NSIndexPath * indexOfImageTobeDeleted = [_selectedItemsIndexPaths objectAtIndex:0];//numberOfFinishedOperations-1
[_arr_assetsMaintained removeObjectAtIndex:indexOfImageTobeDeleted.item];
[_arr_images removeObjectAtIndex:indexOfImageTobeDeleted.item];
[_arr_fullSizeImages removeObjectAtIndex:indexOfImageTobeDeleted.item];
[_arr_imageNames removeObjectAtIndex:indexOfImageTobeDeleted.item];
if ( [_arr_selectedCells containsObject:[NSString stringWithFormat:#"%ld",(long)indexOfImageTobeDeleted.item]] )
{
[_arr_selectedCells removeObject:[NSString stringWithFormat:#"%ld",(long)indexOfImageTobeDeleted.item]];
//[cell.img_selctedRxs setHidden:TRUE];
}
countBeforeClearingAssets = countBeforeClearingAssets - 1;
//Reload the items of UICollectionView performBatchUpdates Block
[_albumImagesCollection performBatchUpdates:^{
[_albumImagesCollection deleteItemsAtIndexPaths:#[indexOfImageTobeDeleted]];
} completion:nil];
_selectedItemsIndexPaths = [_albumImagesCollection indexPathsForSelectedItems];
// [_selectedItemsIndexPaths removeObjectAtIndex:0];
NSLog(#"the count of selected items after updation is %lu", (unsigned long)_selectedItemsIndexPaths.count);
} completionBlock:^(NSArray *operations) {
NSLog(#"All operations in batch complete");
[self callWSToAddNoteForRxs];
[_arr_originalImagesToSend removeAllObjects];
[_arr_selectedCells removeAllObjects];
currentUploadIndex = 0;
NSLog(#"the array of image names is %#",_arr_imageNames);
}];
[[NSOperationQueue mainQueue] addOperations:operations waitUntilFinished:NO];
// third is to maintain the progress block for each image to be uploaded one after the other.
for (AFHTTPRequestOperation *operation in mutableOperations){
[operation setUploadProgressBlock:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) {
[_progressOverLayView setAlpha:0.7f];
[_progressView setHidden:FALSE];
[_progressView setProgress: totalBytesWritten*1.0f / totalBytesExpectedToWrite animated: YES];
[_lbl_progressUpdate setHidden:FALSE];
_lbl_progressUpdate.text = [NSString stringWithFormat:#"Image %d of %lu uploading", currentUploadIndex, mutableOperations.count];
NSLog(#"Sent %lld of %lld bytes and progress is %f", totalBytesWritten, totalBytesExpectedToWrite, totalBytesWritten*1.0f / totalBytesExpectedToWrite);
if(totalBytesWritten >= totalBytesExpectedToWrite)
{
//progressView.hidden = YES;
[self setComplete];
}
}];
}
}
In this code, the first operation is getting executed as soon I start uploading the images i.e only 3 out of 4 are getting uploaded. One Image is always left out. Also. if I do have only Image in the grid, that uploads successfully.
Thanks.

A few thoughts...
1) the progress update block. This doesn't tell you which operations completed; only the count of them, so I am suspicious that they might not be completing in the order the code thinks they are all the time....
2) the completion block takes an array of operations.... I wonder if this gets called once with all of the operations, or multiple times with arrays of the operations that completed? You could look at the array length to see. If it does get called for subsets of operations, this would be the place to remove the assets that have already been uploaded and do cleanup work, since you know which operations finished.
3) I would set the upload progress block before adding the operations to the queue; just for safety.
4) I couldn't find documentation for the batchOperation method, and wondered if it has been removed from a more recent version of AFNetworking - and if so - maybe it's buggy or not good API? I'd be tempted to create my own operations in a loop for clarity; and then do a little state management to check on the status of the batch and handle that appropriately.
5) You say one image is always left out.... is it stable - always the first or last? Does it behave the same way on the sim vs on a cell network or simulated slow / unreliable connection??

Related

Download huge number of images in the most efficient and fast way

In my app, I am needing to download about 6,000 images. I realize this is a lot, but it is needed.
Currently I am using doing the following:
NSArray *photos = #[hugeAmountOfPhotoObjects];
for (ZSSPhoto *photo in photos) {
[self downloadImageWithURL:photo.mobileURL progress:^(double progress) {
} completion:^(UIImage *image) {
// Save the image
} failure:^(NSError *error) {
}];
}
...
- (void)downloadImageWithURL:(NSURL *)url progress:(void (^)(double progress))progress completion:(void (^)(UIImage *image))completion failure:(void (^)(NSError *error))failure {
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setTimeoutInterval:600];
self.operationQueue.maxConcurrentOperationCount = NSOperationQueueDefaultMaxConcurrentOperationCount;
AFHTTPRequestOperation *requestOperation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
requestOperation.responseSerializer = [AFImageResponseSerializer serializer];
[requestOperation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
completion(responseObject);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
[self processOperation:operation error:error failure:failure];
}];
[requestOperation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
double percentDone = (double)totalBytesRead / (double)totalBytesExpectedToRead;
progress(percentDone);
}];
[self.operationQueue addOperation:requestOperation];
}
The problem here is that it takes forever to download using this method, and some of my users are reporting crashing because of high memory usage.
Is there a better method that I could be using to download such a large number of image files?
You could try this somewhat recursively
NSMutableArray *undownloaded;
- (void) startDownload {
undownloaded = [photos mutableCopy]; //get a list of the undownloaded images
for(int i = 0; i < 3;i++) //download 3 at a time
[self downloadImage];
}
- (void) downloadImage {
if(undownloaded.count > 0){
ZSSPhoto *photo = undownloaded.firstObject;
[undownloaded removeObjectAtIndex:0];
[self downloadImageWithURL:photo.mobileURL progress:^(double progress) {
} completion:^(UIImage *image) {
// Save the image
[self downloadImage];
} failure:^(NSError *error) {
[self downloadImage];
//[undownloaded addObject:photo]; //insert photo back into the array maybe to retry? warning, could cause infinite loop without some extra logic, maybe the object can keep a fail count itself
}];
}
}
warning: untested code, may need some tweaking
The speed problem can be solved (the speed will increase, but it might still be slow) with multithreading, downloading all the images at the same time instead of one per time. However, the memory problem is a bit more complicated.
ARC will release all the images after everything is finished, but right before that you gonna have 6,000 images in the device memory. You could optimize the images, reduce their resolution or download them in steps, like Google Images do (you download the images that will be visible at first, then when the user scrolls down you load the images in the new visible area; downloading the images only when they are needed).
Considering that you are downloading enough images to give a memory problem, you will probably take a lots of space in your user's device if you download all of them, and the 'steps' solution may solve that as well.
Now, let's suppose you must download all of them at the same time and space isn't a problem: I suppose that if you put the downloadImageWithURL:progress: method inside a concurrent queue, the images are gonna be freed from memory just after saving (it's just a supposition). Add this to your code:
dispatch_queue_t defaultPriorityQueueWithName(const char* name)
{
dispatch_queue_t dispatchQueue = dispatch_queue_create(name, DISPATCH_QUEUE_CONCURRENT);
dispatch_queue_t priorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_set_target_queue(dispatchQueue, priorityQueue);
return dispatchQueue;
}
And change your code to that:
dispatch_queue_t threadItemLoadImage = defaultPriorityQueueWithName("DownloadingImages");
NSArray *photos = #[hugeAmountOfPhotoObjects];
for (ZSSPhoto *photo in photos)
{
dispatch_async(threadItemLoadImage, ^
{
[self downloadImageWithURL:photo.mobileURL progress:^(double progress) {
} completion:^(UIImage *image) {
// Save the image
} failure:^(NSError *error) {
}];
});
}
You will need to remove setDownloadProgressBlock: in case it updates some view, since they will be downloaded simultaneously. Also, a warning: totalBytesExpectedToRead not always will be correctly retrieved at first, containing 0, which might make your app crash for dividing by zero as well in some rare occasions. In future cases, when you need to use setDownloadProgressBlock:, check totalBytesExpectedToRead value before doing that division.

Pause and resume is not working properly in AFNetwoking ios

I am using AFNetworking for downloading files having size between 1 to 4 gbs,.
Currently while downloading such a huge files I pause the current download when app enters in background state and resume when it gets active.
But what happens wrong in my case is that, first time while downloading when I minimize the app I pause it and when I again maximize app after 20 to 30 mins I resume it and download continues from where it was left paused last time. But it works only first time, second time when I again minimize the app with same download it gets paused and when I again maximizes it , it stuck at the same point showing some wrong values for progress and current transfer speed and it never moves forward or never continues current download.
Strange behaviour??
I have tried both old and new (2.0) versions but no luck.
Can you guess what is happening wrong in my case?
Or
Please suggest me some good alternatives to using AFNetworking.
UPDATE
Method called to download file
-(void) downloadTracksFromProgramArray:(NSArray*) programs
{
if (programs.count == 0) {
return;
}
queue = [[NSOperationQueue alloc] init];
[queue setMaxConcurrentOperationCount:1];
queueSize = 0;
urlString = [programs objectAtIndex:0];
NSString *filename = [urlString lastPathComponent];
// 11-09-12
// remove query string from aws
NSString *string1 = [NSHomeDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:#"Documents/%#",[filename lastPathComponent]]] ;
// remove query string from aws
NSArray *jaysarray = [string1 componentsSeparatedByString:#"?"];
NSString *downloadPath1 = [NSString stringWithFormat:#"%#",[jaysarray objectAtIndex:0]];
extract_file_path_after_download = downloadPath1;
NSLog(#"%#",[jaysarray objectAtIndex:0]);
// NSLog(#"%#",[jaysarray objectAtIndex:1]);
current_downloading_file_path = [downloadPath1 copy];
NSLog(#"download url %#",[NSURL URLWithString:urlString]);
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString]];
operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
operation.outputStream = [NSOutputStream outputStreamToFileAtPath:downloadPath1 append:NO];
//handle successful completion of each track download
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
NSLog(#"Successfully downloaded file to %#", downloadPath1);
//if ([[queue operations] count] == 0) {
NSNotification *success = [NSNotification notificationWithName:#"AudioDone" object:[NSNumber numberWithBool: YES]];
[[NSNotificationCenter defaultCenter] postNotification:success];
queueSize = 0;
//} else {
//send total track info
//get total queue size by the first success and add 1 back
if (queueSize ==0) {
queueSize = [[queue operations] count] +1.0;
}
float progress = (float)(queueSize-[[queue operations] count])/queueSize;
NSNumber * totProgress = [NSNumber numberWithFloat:progress];
NSLog(#"Total Progress: %#", totProgress);
current_downloading_file_path = #"";
//Commented by rakesh biradar - becoz #"TotalProgress" notification method does not do anything(memory).
//NSNotification * totalProgressNotification = [NSNotification notificationWithName:#"TotalProgress"
// object:totProgress];
//[[NSNotificationCenter defaultCenter] postNotification:totalProgressNotification];
//}
NSLog(#"QueueCount: %d", [[queue operations] count]); //[[self sharedQueue] operationCount]);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
//deletes the partial downloaded file from document folder
if(([current_downloading_file_path length] > 0) && [[NSFileManager defaultManager] fileExistsAtPath:current_downloading_file_path])
[[NSFileManager defaultManager] removeItemAtPath:current_downloading_file_path error:nil];
current_downloading_file_path = #"";
NSLog(#"Error: %#", error);
}];
//Send progress notification
[operation setDownloadProgressBlock:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) {
//NSLog(#"Sent %lld of %lld bytes, %#", totalBytesWritten, totalBytesExpectedToWrite, path);
float percentDone = ((float)((int)totalBytesWritten) / (float)((int)totalBytesExpectedToWrite));
//NSLog(#"Percent: %f", percentDone);
NSDictionary *userInfo = [NSDictionary dictionaryWithObjects: [NSArray arrayWithObjects:filename, [NSNumber numberWithFloat: percentDone],[NSNumber numberWithLongLong:totalBytesWritten],[NSNumber numberWithLongLong:totalBytesExpectedToWrite],[NSNumber numberWithUnsignedInteger:bytesWritten],nil]
forKeys:[NSArray arrayWithObjects:#"message", #"percent",#"totalBytesWritten",#"totalBytesExpectedToWrite",#"bytesWritten", nil]];
NSNotification * progress = [NSNotification notificationWithName:#"DownloadingAudio" object:nil userInfo:userInfo];
[[NSNotificationCenter defaultCenter] postNotification:progress];
}];
[queue addOperation:operation];
//[self enqueueHTTPRequestOperation:operation];
//NSLog(#"Operation Queue: %#", [self sharedQueue]);
}
Method when app goes in background
- (void)applicationWillResignActive:(UIApplication *)application {
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
if (operation)
{
NSLog(#"%#",operation);
//[self saveCustomObject:operation];
[operation pause];
}
}
Method called when app becomes active
- (void)applicationDidBecomeActive:(UIApplication *)application {
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
// Handle the user leaving the app while the Facebook login dialog is being shown
// For example: when the user presses the iOS "home" button while the login dialog is active
if (operation)
{
//operation = [self loadCustomObjectWithKey:#"myEncodedObjectKey"];
NSLog(#"%#",operation);
[operation resume];
}
[FBAppCall handleDidBecomeActive];
}

Queue all failed transmissions

I'm writting a client/server application that needs to send some XML to a server.
NSMutableArray *operations = [NSMutableArray array];
AFHTTPRequestOperation *operation1 = [[AFHTTPRequestOperation alloc] initWithRequest:theRequest];
[operation1 setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
float progress = (float)totalBytesRead / totalBytesExpectedToRead;
NSLog(#"Progress 1 = %f",progress);
}];
[operations addObject:operation1];
AFHTTPRequestOperation *operation2 = [[AFHTTPRequestOperation alloc] initWithRequest:theRequest];
[operation2 setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
float progress = (float)totalBytesRead / totalBytesExpectedToRead;
NSLog(#"Progress 2 = %f",progress*100);
}];
[operations addObject:operation2];
NSOperationQueue *operationQueue = [[NSOperationQueue alloc] init];
// Set the max number of concurrent operations (threads)
[operationQueue setMaxConcurrentOperationCount:3];
[operationQueue addOperations:#[operation1, operation2] waitUntilFinished:NO];
What I want to do now is to handle if the transmission fails and have a queue so it retries to send it.
What's the best way to achieve this with the AFNetworking library ?
First up it would not be very wise to just retry failed operations again. Depending on what was the source of the error, you risk severe side effects like duplicate submissions.
You're already using AFHTTPRequestOperation, so the easiest solution would be to call
setCompletionBlockWithSuccess:failure: and handle errors in the "failure"-block. After all you may also want to use the "success"-block for when the download successfully finished.
One last detail about the code you provided: You're creating the NSArray *operations in line 1 - yet you're not using it for anything since you create a new array of the operations in the last line. So either you left something out or you should simplify that.

How to track progress of multiple simultaneous downloads with AFNetworking?

I am using AFNetworking to download files that my app uses for a sync solution. At certain times, the app downloads a series of files as a batch unit. Following this example, I run the batch like this:
NSURL *baseURL = <NSURL with the base of my server>;
AFHTTPRequestOperationManager *manager = [[AFHTTPRequestOperationManager alloc] initWithBaseURL:baseURL];
// as per: https://stackoverflow.com/a/19883392/353137
dispatch_group_t group = dispatch_group_create();
for (NSDictionary *changeSet in changeSets) {
dispatch_group_enter(group);
AFHTTPRequestOperation *operation =
[manager
POST:#"download"
parameters: <my download parameters>
success:^(AFHTTPRequestOperation *operation, id responseObject) {
// handle download success...
// ...
dispatch_group_leave(group);
}
failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// handle failure...
// ...
dispatch_group_leave(group);
}];
[operation start];
}
// Here we wait for all the requests to finish
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// run code when all files are downloaded
});
This works well for the batch downloads. However, I want to display to the user an MBProgressHUD which shows them how the downloads are coming along.
AFNetworking provides a callback method
[operation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
}];
... which lets you update a progress meter pretty easily, simply by setting the progress to totalBytesRead / totalBytesExpectedToRead. But when you have multiple downloads going simultaneously that is hard to keep track of on a total basis.
I have considered having an NSMutableDictionary with a key for each HTTP operation, with this general format:
NSMutableArray *downloadProgress = [NSMutableArray arrayWithArray:#{
#"DownloadID1" : #{ #"totalBytesRead" : #0, #"totalBytesExpected" : #100000},
#"DownloadID2" : #{ #"totalBytesRead" : #0, #"totalBytesExpected" : #200000}
}];
As each operation's download progresses, I can update the totalBytesRead for that specific operation in the central NSMutableDictionary -- and then total up all the totalBytesRead and totalBytesExpected' to come up with the total for the whole batched operation. However, AFNetworking's progress callback methoddownloadProgressBlock, defined as^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead){}does not include the specific operation as a callback block variable (as opposed to thesuccessandfailure` callbacks, which do contain the specific operation as a variable, making it accessible). Which makes it impossible, as far as I can tell, to determine which operation specifically is making the callback.
Any suggestions on how to track the progress of multipole simultaneous downloads using AFNetworking?
If your block is inlined, you can access the operation directly but the compiler might warn you of the circular referencing. You can work around by declaring a weak reference and use it inside the block:
__weak AFHTTPRequestOperation weakOp = operation;
[operation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
NSURL* url = weakOp.request.URL; // sender operation's URL
}];
Actually, you can access anything inside the block, but you need to understand block to go for that. In general, any variable referred in the block is copied at the time the block created i.e. the time that the line got executed. It means my weakOp in the block will refer to the value of the weakOp variable when the setDownloadProgressBlock line got executed. You can think it like what would each variable you refer in the block would be if your block got executed immediately.
Blocks are made just to makes these things easyer ;-)
HERE you can find an example project. Simply push the + button and insert the direct URL for a file to download. There is no error checking and no URL redirection so insert only direct URLs.
For the relevant part look in these methods of the DownloadViewController:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
Here the explanation:
When you pass a variable to a block, the block makes a copy of the variables passed from outside.
Because our variable is simply an object pointers (a memory address), the pointer is copied inside the block, and since the default storage is __strong the reference is maintained until the block is destroyed.
It means that you can pass to the block a direct reference to your progress view (I use an UIProgressView since I've never used MBProgressHUD):
UIProgressView *progressView = // create a reference to the view for this specific operation
[operation setDownloadProgressBlock:^(NSUInteger bytesRead, long long totalBytesRead, long long totalBytesExpectedToRead) {
progressView.progress = // do calculation to update the view. If the callback is on a background thread, remember to add a dispatch on the main queue
}];
Doing this, every operation will have a reference to its own progressView. The reference is conserved and the progress view updated until the progress block exists.
You must be downloading some zip file , video file etc.
Make a model of that file to download containing fields like
(id, url, image , type , etc...)
Create a NSOperationQueue
and set maxConcurrentOperationCount according to your requirement
make public method. (in a singleton class)
- (void)downloadfileModel:(ModelClass *)model {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *path = [[paths objectAtIndex:0] stringByAppendingPathComponent:[NSString stringWithFormat:#"%#.zip",model.iD]];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:model.url]];
operation = [[AFDownloadRequestOperation alloc] initWithRequest:request targetPath:path shouldResume:YES];
operation.outputStream = [NSOutputStream outputStreamToFileAtPath:path append:NO];
[operation setUserInfo:[NSDictionary dictionaryWithObject:model forKey:#"model"]];
////// Saving model into operation dictionary
[operation setProgressiveDownloadProgressBlock:^(AFDownloadRequestOperation *operation, NSInteger bytesRead, long long totalBytesRead, long long totalBytesExpected, long long totalBytesReadForFile, long long totalBytesExpectedToReadForFile) {
////// Sending Notification
////// Try to send notification only on significant download
totalBytesRead = ((totalBytesRead *100)/totalBytesExpectedToReadForFile);
[[NSNotificationCenter defaultCenter] postNotificationName:DOWNLOAD_PROGRESS object:model userInfo:[NSDictionary dictionaryWithObject:[NSString stringWithFormat:#"%lld",totalBytesRead] forKey:#"progress"]];
/// Sending progress notification with model object of operation
}
}];
[[self downloadQueue] addOperation:operation]; // adding operation to queue
}
Invoke this method for multiple times with different models for multiple downloads.
Observe that notification on controller
where you show the download progress. (possibly tableView Controller).
Show all downloading operations list in this class
For showing progress Observe the Notification and fetch the model object from notification and get the file id from notification and Find that id in your Table View and Update that particular cell with the progress.
when starting the operations, you could safe each operation, downloadID and the values for totalbytesRead and totalBytesExpected together in a NSDictionary and all dicts to your downloadProgressArray.
Then when the callback methods is invoked, loop through your array and compare the calling operation with the operation in each dict. this way you should be able to identify the operation.
I just did something very similar (uploaded a bunch of files instead of downloading)
Here's an easy way to solve it.
Lets say you are downloading maximum of 10 files in one batch.
__block int random=-1;
[operation setUploadProgressBlock:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) {
if (random == -1) // This chunk of code just makes sure your random number between 0 to 10 is not repetitive
{
random = arc4random() % 10;
if(![[[self myArray]objectAtIndex:random]isEqualToString:#"0"])
{
while (![[[self myArray]objectAtIndex:random]isEqualToString:#"0"])
{
random = arc4random() % 10;
}
}
[DataManager sharedDataManager].total += totalBytesExpectedToWrite;
}
[[self myArray] replaceObjectAtIndex:random withObject:[NSString stringWithFormat:#"%lu",(unsigned long)totalBytesWritten]];
Then you calculate it like this:
NSNumber * sum = [[self myArray] valueForKeyPath:#"#sum.self"];
float percentDone;
percentDone = ((float)((int)[sum floatValue]) / (float)((int)[DataManager sharedDataManager].total));
[self array] will look like this:
array: (
0,
444840, // <-- will keep increasing until download is finished
0,
0,
0,
442144, // <-- will keep increasing until download is finished
0,
0,
0,
451580 // <-- will keep increasing until download is finished
)

Measuring response time in AFNetworking HTTP GET

I am trying to measure time taken per GET request when downloading a file using AFNetworking. I am downloading a file repeatedly in a loop.
The problem I am having is that the way I am measuring total time, it gives a much larger total time than it actually is. For example, for 50 downloads it gives 72 sec but in reality it only took around 5 sec. I also suspect 5 sec is too low for 50 downloads(the download size is 581 kb per file).
How do I effectively measure time in this case? I need time from the moment request is fired till response in received.
My method to download file:
- (void) HTTPGetRequest
{
startTime = CACurrentMediaTime(); // Start measuring time
AFHTTPClient *httpClient = [[AFHTTPClient alloc] initWithBaseURL:http://myServer];
NSMutableURLRequest *request = [httpClient requestWithMethod:#"GET"
path:#"/download/Text581KB.txt"
parameters:nil];
AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request];
[httpClient registerHTTPOperationClass:[AFHTTPRequestOperation class]];
// Save downloaded file
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *path = [[paths objectAtIndex:0] stringByAppendingPathComponent:[NSString stringWithFormat:#"Text581KB.txt"]];
operation.outputStream = [NSOutputStream outputStreamToFileAtPath:path append:NO];
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
double elapsedTime = (CACurrentMediaTime() - startTime); // Measuring time
totalTime += elapsedTime; // Measuring total time HERE!
[results setString:[NSString stringWithFormat: #"Current Transaction Time: %f sec\nTotal Time: %f sec", elapsedTime, totalTime]];
[_resultLabel performSelectorOnMainThread:#selector(setText:) withObject:results waitUntilDone:NO];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
[operation setDownloadProgressBlock:^(NSUInteger bytesWritten, long long totalBytesWritten, long long totalBytesExpectedToWrite) { ((int)totalBytesExpectedToWrite));
totalDownloadSize += totalBytesExpectedToWrite;
[_DataTransferredLabel setText:[NSString stringWithFormat:#"Total Download Size: %#", [self getFileSize:totalDownloadSize/1024]]];
}];
[operation setCacheResponseBlock:^NSCachedURLResponse *(NSURLConnection *connection, NSCachedURLResponse *cachedResponse) {
return nil;
}];
[operationQueue addOperation:operation];
}
I am creating a NSOperationQueue in my viewDidLoad:
operationQueue = [NSOperationQueue new];
[operationQueue setMaxConcurrentOperationCount:1]; // using this as I was suspecting downloads were happening in parallel & thus 50 downloads finishing in a few secs
I am invoking the HTTPGetRequest method as follows:
- (IBAction)startDownload:(UIButton *)sender {
totalCount = [[_countText text] longLongValue]; // get # of times to download
long currentCount = 1;
completedCount = 0;
totalTime = 0;
totalDownloadSize = 0;
while (currentCount <= totalCount)
{
[self HTTPGetRequest];
[results setString:#""];
currentCount++;
}
Use AFHTTPRequestOperationLogger.
In terms of calculating cumulative time (not elapsed time), I have just created a subclass of AFHTTPRequestOperation that captures the start time. Otherwise, you won't know precisely when it started:
#interface TimedAFHTTPRequestOperation : AFHTTPRequestOperation
#property (nonatomic) CFAbsoluteTime startTime;
#end
#implementation TimedAFHTTPRequestOperation
- (void)start
{
self.startTime = CFAbsoluteTimeGetCurrent();
[super start];
}
#end
(Note I'm using CFAbsoluteTimeGetCurrent versus CACurrentMediaTime; use whatever you want, but just be consistent.)
Then in the code that's doing the downloads, you can use this TimedAFHTTPRequestOperation instead of AFHTTPRequestOperation:
TimedAFHTTPRequestOperation *operation = [[TimedAFHTTPRequestOperation alloc] initWithRequest:request];
That code's completion block can then use the startTime property of TimedAFHTTPRequestOperation to calculate the time elapsed for the given operation and add it to the total time:
[operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) {
TimedAFHTTPRequestOperation *timedOperation = (id)operation;
CFTimeInterval elapsedTime = CFAbsoluteTimeGetCurrent() - timedOperation.startTime;
self.totalTime += elapsedTime; // Measuring total time HERE!
NSLog(#"finished in %.1f", elapsedTime);
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
NSLog(#"Error: %#", error);
}];
That's how you calculate the elapsedTime and append them together to calculate the totalTime.
In terms of how to know when the operations are done, I would
modify HTTPGetRequest to return a NSOperation;
have startDownload create a completion operation and then add all of these operations as dependencies:
NSOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(#"finished all in cumulative time: %.1f", self.totalTime);
}];
for (NSInteger i = 0; i < totalCount; i++)
{
NSOperation *operation = [self HTTPGetRequest];
[completionOperation addDependency:operation];
}
[self.operationQueue addOperation:completionOperation];
That achieves several goals, namely creating a completion operation, calculating the total time (as opposed to the total time elapsed).
By the way, I'd also suggest pulling the creation of AFHTTPClient out of your HTTPGetRequest. You should probably only create one per app. This is especially important in case you ever started using enqueueHTTPRequestOperation instead of creating your own operation queue. I also see no need for your call to registerHTTPOperationClass.
You are incrementing the totalElapsed by elapsedTime, but elapsedTime is calculated from startTime, which itself represents the time that the jobs were first queued, not when the download actually started. Remember that HTTPGetRequest returns almost immediately (having set elapsedTime). Thus if you're queueing five downloads, I wouldn't be surprised that HTTPGetRequest runs five times (and sets and resets startTime five times) before the first request even is initiated.
The question is further complicated by the question of whether you're doing concurrent downloads, and if so, what you then mean by "total elapsed". Let's say you have two concurrent downloads, one that takes 5 seconds, another takes 7 seconds. Do you want the answer to be 7 (because they both finished in 7 seconds)? Or do you want the answer to be 12 (because they both finished in a cumulative 12 seconds)?
I'm presuming that you're looking for, in this scenario, 7 seconds, then you should set the startTime once before you initiate all of your requests, and then only calculate when all of the downloads are done. You could, for example, rather than doing any calculations in HTTPGetRequest at all, just add your own operation that is dependent upon all the other operations you added, which calculates the total elapsed. at the very end. Or, if you want the the total elapsed to just reflect the total elapsed while you're in the process of downloading, then just set totalElapsed rather than incrementing it.
Another option is to inject the "fire" date in the operation's userInfo by observing the AFNetworkingOperationDidStartNotification notification.
//AFHTTPRequestOperation *operation = [...]
id __block observer = [[NSNotificationCenter defaultCenter] addObserverForName:AFNetworkingOperationDidStartNotification
object:operation
queue:nil
usingBlock:^(NSNotification *note) {
operation.userInfo = #{#"fireDate": [NSDate date]};
[[NSNotificationCenter defaultCenter] removeObserver:observer];
}];

Resources