1

I have an app which downloads some files from the server in few threads. The problems is that it is giving a heavy load to the CPU (hitting to 80%). What can be done to make it better? I made similar app on Windows with C#, and the cpu usage never goes above 5%.

EDIT: This code has been changed after getting some suggestions below. The problem now is, that the download never reaches 100% when I set [queue setMaxConcurrentOperationCount:6]. If I change the asynchronous NSURLConnection back to sendSynchronous call it works, when I change the above OperationCount to 1, also works.

This is how I add NSOperations to the queue (may be large, like 800).

int chunkId = 0;
for (DownloadFile *downloadFile in [download filesInTheDownload])
{
    chunkId = 0;
    for (DownloadChunk *downloadChunk in [downloadFile chunksInTheFile])
    {
        DownloadChunkOperation *operation = [[DownloadChunkOperation alloc]  initWithDownloadObject:download
      downloadFile:downloadFile                                                                                                    downloadChunk:downloadChunk                                                                                                           andChunkId:chunkId];
    [queue addOperation:operation];
    chunkId++;
    }
}


#import "DownloadChunkOperation.h"
#import "Download.h"
#import "DownloadFile.h"
#import "DownloadChunk.h"

@interface DownloadChunkOperation()
@property(assign) BOOL isExecuting;
@property(assign) BOOL isFinished;
@end

@implementation DownloadChunkOperation

@synthesize download = _download;
@synthesize downloadFile = _downloadFile;
@synthesize downloadChunk = _downloadChunk;

@synthesize isFinished = _isFinished;
@synthesize isExecuting = _isExecuting;

- (id) initWithDownloadObject:(Download *)download downloadFile:(DownloadFile *)downloadFile downloadChunk:(DownloadChunk *)downloadChunk andChunkId:(uint32_t)chunkId
{
    self = [super init];

    if (self) {
        self.download = download;
        self.downloadFile = downloadFile;
        self.downloadChunk = downloadChunk;
        self.chunkId = chunkId;
    }

    return self;
}

- (void) start
{
    if ([self isCancelled]) {
        [self setIsFinished:YES];
        [self setIsExecuting:NO];
        return;
    }

    [self setIsExecuting:YES];
    [self setIsFinished:NO];
    [self.downloadChunk setChunkState:cDownloading];

    downloadPath = [[NSString stringWithFormat:@"%@/%@", [self.download downloadFolder], [self.download escapedTitle]] stringByExpandingTildeInPath];

    NSURL *fileURL = [[NSURL alloc] initWithString:[self.downloadFile filePath]];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:fileURL];
    NSString *range = [NSString stringWithFormat:@"bytes=%lli-%lli", [self.downloadChunk startingByte], [self.downloadChunk endingByte]];
    [request setValue:range forHTTPHeaderField:@"Range"];
    connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
    // IMPORTANT! The next line is what keeps the NSOperation alive for the during of the NSURLConnection!
    [connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [connection start];

    if (connection) {
        NSLog(@"connection established!");
        do {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        } while (!self.isFinished);
    } else {
        NSLog(@"couldn't establish connection for: %@", fileURL);
    }
}

- (BOOL) isConcurrent
{
    return YES;
}

- (void) connection:(NSURLConnection *)_connection didReceiveResponse:(NSURLResponse *)response
{
    receivedData = [[NSMutableData alloc] init];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data
{
    // Not cancelled, receive data.
    if (![self isCancelled]) {
        [receivedData appendData:data];
        self.download.downloadedBytes += [data length];
        return;
    }

    // Cancelled, tear down connection.
    [self setIsExecuting:NO];
    [self setIsFinished:YES];
    [self.downloadChunk setChunkState:cConnecting];
    [self->connection cancel];
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
{
    [self setIsExecuting:NO];
    [self setIsFinished:YES];
    NSLog(@"Connection failed! Error - %@ %@",
          [error localizedDescription],
          [[error userInfo] objectForKey:NSURLErrorFailingURLStringErrorKey]);
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
    NSString *chunkPath = [downloadPath stringByAppendingFormat:@"/%@.%i", [self.downloadFile fileName], self.chunkId];
    NSError *saveError = nil;
    [receivedData writeToFile:chunkPath options:NSDataWritingAtomic error:&saveError];
    if (saveError != nil) {
        NSLog(@"Download save failed! Error: %@", [saveError description]);
    }
    else {
        NSLog(@"file has been saved!: %@", chunkPath);
    }
    [self setIsExecuting:NO];
    [self setIsFinished:YES];
    [self.downloadChunk setChunkState:cFinished];

    if ([self.download downloadedBytes] == [self.download size])
        [[NSNotificationCenter defaultCenter] postNotificationName:@"downloadFinished" object:self.download];
}

@end
Thunder
  • 611
  • 1
  • 8
  • 25

2 Answers2

2

You should not create threads yourself. Use dedicated API like NSOperationQueue or even GCD directly for this purpose. They know better about hardware limits, virtual cores, etc. and support priority settings.

You shouldn't use +sendSynchronousRequest: either. Wrapping your -downloadChunk method in a dispatch call as suggested by charith won't help you improve performance, as +sendSynchronousRequest: blocks the thread until new data comes in and forces GCD to spawn new threads.

Use the asynchronous API of NSURLConnection using delegate callbacks. You can also wrap your NSURLConnection code inside a NSOperation subclass and use NSOperationQueue to manage the downloads: Using NSURLConnections

If you don't want to write the NSOperation subclass yourself, you can also use a 3rd party framework like AFNetworking.

Fabian Kreiser
  • 8,307
  • 1
  • 34
  • 60
  • Thanks, I'll try your approach. Also you were right regarding the charith's suggestion, makes no difference to the CPU load. Also the asynchronous API will allow me to download files in smaller chunks, right? Now every file downloaded has 10MB, so I can only update the progress bar every 10MB downloaded. – Thunder Jul 31 '12 at 13:15
  • Yes you can calculate the progress. Just calculate the progress in -didReceiveData: – Fabian Kreiser Jul 31 '12 at 14:57
  • Ok, I've added a NSOperationQueue as you suggested. I didn't help the CPU usage. But another problem occurred. Now when I setMaxConcurrentOperationCount to like 6, the download never ends. Depending on how big the download is, it always stops before the end. This is only in case of asynchronous NSURLConnection call, when I change it to sendSynchronous it works, when I set setMaxConcurrentOperationCount:1 it also works. I've edited the first post, to show my NSOperation. – Thunder Aug 02 '12 at 10:13
  • Don't set the max operation count to 6, that seems too high for a single core device! Let NSOperationQueue decide how many operations can be run concurrently. You also need to use a conurrent NSOperation by overriding -isConcurrent. Look here: https://gist.github.com/578636 and here: http://stackoverflow.com/questions/9223537/asynchronous-nsurlconnection-with-nsoperation and here: https://github.com/andeh89/LibSyncTest – Fabian Kreiser Aug 02 '12 at 10:37
  • Thanks again, that changed nothing. When max op is set to 1, the download completes, if more, then the download stops before the end. What can I do to verify what's wrong? Every chunk seems to finish normally on ConnectionDidFinishLoading normally. – Thunder Aug 02 '12 at 13:00
  • Also when the download is small, like 10 chunks, it will also complete, even if it running 6 operations at a time. Seems like something bad is happening over time with multiple operations running. I've edited the code above to the current state. – Thunder Aug 02 '12 at 13:28
  • Funny thing, I've added to connectionDidFinishLoading these lines: NSLog(@"Data length: %lu", [receivedData length]); NSLog(@"Chunk size from xml: %llu", self.downloadChunk.endingByte - self.downloadChunk.startingByte + 1); In every case numbers are the same, I don't why it isn't showing 100% on the progress bar and progress string "Downloaded 883.1 MB of 883.5 MB". On one ConcurrentOp it hits 100%. Synchronization? downloadedBytes is an atomic value. – Thunder Aug 02 '12 at 13:59
  • Seems moving self.download.downloadedBytes += [receivedData length]; to connectionFinishLoading did help, it hit 100%, but now progress bar updates only when it completes a chunk (which is every 10MB). – Thunder Aug 02 '12 at 14:05
  • Are you calculating the progress after the received data has been added? I'm usually setting the progress to 100% manually in didFinishLoading to make sure it's completed. – Fabian Kreiser Aug 02 '12 at 14:54
  • I highly recommend using AFNetworking for your download code, it'll make your life a lot easier. ;) – Fabian Kreiser Aug 02 '12 at 14:59
  • Download class has a method which is called each second: - (CGFloat) percentProgress { if (self.size > 0) { return (self.downloadedBytes/(float)self.size)*100.0; } else { return 0.0; } } The line: self.download.downloadedBytes += [data length]; is in the didReceiveData: – Thunder Aug 02 '12 at 15:00
  • Maybe it's just not getting called the last time, as -didFinishLoading immediately fires when the last chunk of data has been downloaded. – Fabian Kreiser Aug 02 '12 at 15:16
  • So what would you suggest in this case? Can I somehow wait for it to fire? – Thunder Aug 02 '12 at 16:06
  • You can add a delegate pattern for your download operation and tell your managing object the progress directly. – Fabian Kreiser Aug 02 '12 at 16:10
0

Try with GCD blocks and global queues. This is the apple recommended way now for concurrency ex:

dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_async(globalQueue, ^{

        [self downloadChunk:objDownload];
    });
Charith Nidarsha
  • 4,195
  • 3
  • 28
  • 32