0

I am fetching 700.000 rows from a web service and my app is crashing after a while being terminated due to memory issue, it consumes about 1 GB of ram before it crashes, the code is fairly simple, I fetch the JSON, I put into an array, I loop the array and insert into core data and once done I save the context

the code is shown below

+ (void)fetchTillDataAll:(int)tillId :(int)startAtRow :(int)takeNoOfRows {



    if ([NWTillHelper isDebug] == 1) {
        NSLog(@"WebServices:fetchTillDataAll:tillId = %d, startAtRow = %d, takeNoOfRows = %d", tillId, startAtRow, takeNoOfRows);
    }

    NSString *finalURL = [NSString stringWithFormat:@"https://host.domain.com:5443/api/till/tilldatav2/%d?StartAtRow=%d&TakeNoOfRows=%d",tillId, startAtRow, takeNoOfRows];

    [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:finalURL]
                                 completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {

                                     if (error != nil) {
                                         if ([NWTillHelper isDebug] == 1) {
                                             NSLog(@"WebServices:fetchTillDataAll:Transport error %@", error);
                                         }
                                     } else {
                                         NSHTTPURLResponse *responseHTTP;
                                         responseHTTP = (NSHTTPURLResponse *) response;

                                         if(responseHTTP.statusCode != 200) {
                                             if ([NWTillHelper isDebug] == 1) {
                                                 NSLog(@"WebServices:fetchTillDataAll:Server Error %d", (int) responseHTTP.statusCode);
                                             }
                                         } else {
                                             NSArray *tillBasicDataArray = [NSJSONSerialization JSONObjectWithData:data
                                                                                                           options:0
                                                                                                             error:NULL];
                                             if ([NWTillHelper isDebug] == 1) {
                                                 NSLog(@"WebServices:fetchTillDataAll:tillBasicDataArray count = %lu", (unsigned long)[tillBasicDataArray count]);
                                                 NSLog(@"WebServices:fetchTillDataAll:tillBasicDataArray looks like %@",tillBasicDataArray);
                                             }

                                             AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication]delegate];

                                             //NSManagedObjectContext *context =
                                             //appDelegate.persistentContainer.viewContext;
                                             //context.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy;

                                             NSPersistentContainer *container = appDelegate.persistentContainer;

                                             [container performBackgroundTask:^(NSManagedObjectContext *context ) {
                                                 context.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy;

                                                 NSDictionary *tillBasicDataDict = Nil;

                                                 //Loop through the array and for each dictionary insert into local DB
                                                 // lets work on concurrency here
                                                 for (id element in tillBasicDataArray){
                                                     tillBasicDataDict = element;

                                                     NSString *itemId = [tillBasicDataDict objectForKey:@"itemId"];
                                                     NSString *brandId = [tillBasicDataDict objectForKey:@"companyId"];
                                                     NSString *languageId = [tillBasicDataDict objectForKey:@"languageCode"];
                                                     NSString *colorCode = [NSString stringWithFormat:@"%@", [tillBasicDataDict objectForKey:@"colorCode"]];
                                                     NSString *discountable = [tillBasicDataDict objectForKey:@"discountable"];
                                                     NSString *exchangeable = [tillBasicDataDict objectForKey:@"exchangeable"];
                                                     NSString *noos14 = [tillBasicDataDict objectForKey:@"noos14"];
                                                     NSString *sizeCode = [NSString stringWithFormat:@"%@", [tillBasicDataDict objectForKey:@"sizeCode"]];
                                                     NSString *taxGroup = [tillBasicDataDict objectForKey:@"taxGroupId"];
                                                     NSString *taxRegion = [tillBasicDataDict objectForKey:@"taxRegion"];
                                                     NSString *tradeItemDesc = [tillBasicDataDict objectForKey:@"tradeItemDesc"];
                                                     NSString *withTax = [tillBasicDataDict objectForKey:@"withTax"];
                                                     NSString *status = [tillBasicDataDict objectForKey:@"status"];

                                                     // Use Core Data FMD


                                                     NSManagedObject *newPimItem = Nil;
                                                     newPimItem = [NSEntityDescription
                                                                   insertNewObjectForEntityForName:@"TillData"
                                                                   inManagedObjectContext:context];

                                                     [newPimItem setValue:itemId forKey:@"itemId"];
                                                     [newPimItem setValue:brandId forKey:@"brandId"];
                                                     [newPimItem setValue:languageId forKey:@"languageCode"];
                                                     [newPimItem setValue:colorCode forKey:@"colorCode"];
                                                     [newPimItem setValue:discountable forKey:@"discountable"];
                                                     [newPimItem setValue:exchangeable forKey:@"exchangeable"];
                                                     [newPimItem setValue:noos14 forKey:@"noos14"];
                                                     [newPimItem setValue:sizeCode forKey:@"sizeCode"];
                                                     [newPimItem setValue:[NSNumber numberWithInt:[taxGroup intValue]] forKey:@"taxGroup"];
                                                     [newPimItem setValue:taxRegion forKey:@"taxRegion"];
                                                     [newPimItem setValue:tradeItemDesc forKey:@"tradeItemDesc"];
                                                     [newPimItem setValue:[NSNumber numberWithInt:[withTax intValue]] forKey:@"withTax"];
                                                     [newPimItem setValue:[NSNumber numberWithInt:[status intValue]] forKey:@"status"];

                                                     if ([NWTillHelper isDebug] == 1) {
                                                         NSLog(@"WebServices:fetchTillDataAll:ItemId in loop = %@", itemId);
                                                         NSLog(@"WebServices:fetchTillDataAll:newPimItem = %@", newPimItem);
                                                         NSLog(@"WebServices:fetchTillDataAll:CoreData error = %@", error);
                                                     }

                                                 }
                                                 NSError *error = nil;
                                                 if (![context save:&error]) {
                                                     NSLog(@"Failure to save context: %@\n%@", [error localizedDescription], [error userInfo]);
                                                     abort();
                                                 } else {
                                                     NSUserDefaults *tillUserDefaults = [NSUserDefaults standardUserDefaults];
                                                     [tillUserDefaults setInteger:1 forKey:@"hasTillData"];
                                                     [tillUserDefaults synchronize];
                                                 }
                                             }];
                                         }
                                     }
                                 }] resume];
}

What can I do minimize the foot print so that I am able to download the data? I absolutely must have the data locally in order to allow offline capabilities in the app

----- EDIT -----

After implementing splitting the NSArray into an array of array I still get the same problem, below if the new code as suggested:

split method

+ (NSArray *) splitIntoArraysOfBatchSize:(NSArray *)originalArray :(int)batchSize {

    NSMutableArray *arrayOfArrays = [NSMutableArray array];

    for(int j = 0; j < [originalArray count]; j += batchSize) {

        NSArray *subarray = [originalArray subarrayWithRange:NSMakeRange(j, MIN(batchSize, [originalArray count] - j))];
        [arrayOfArrays addObject:subarray];
    }

    return arrayOfArrays;
}

Looping through the array as follows

+(void)fetchPricelistAll:(int)pricelistId :(int)startAtRow :(int)takeNoOfRows;
{
    if ([NWTillHelper isDebug] == 1) {
        NSLog(@"WebServices:fetchPriceList:priceListId = %d", pricelistId);
    }

    NSString *finalURL = [NSString stringWithFormat:@"https://host.domain.com:5443/api/till/tillpricelistv2/%d?StartAtRow=%d&TakeNoOfRows=%d",pricelistId, startAtRow, takeNoOfRows];

    [[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:finalURL]
                                 completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {

                                     if (error != nil) {
                                         if ([NWTillHelper isDebug] == 1) {
                                             NSLog(@"WebServices:fetchPriceList:Transport error %@", error);
                                         }
                                     } else {
                                         NSHTTPURLResponse *responseHTTP;
                                         responseHTTP = (NSHTTPURLResponse *) response;

                                         if(responseHTTP.statusCode != 200) {
                                             if ([NWTillHelper isDebug] == 1) {
                                                 NSLog(@"WebServices:fetchPriceList:Server Error %d", (int) responseHTTP.statusCode);
                                             }
                                         } else {
                                             NSArray *priceListObjectArray = [NSJSONSerialization JSONObjectWithData:data
                                                                                                             options:0
                                                                                                               error:NULL];
                                             if ([NWTillHelper isDebug] == 1) {
                                                 NSLog(@"WebServices:fetchPriceList:count = %lu", (unsigned long)[priceListObjectArray count]);
                                                 NSLog(@"WebServices:fetchPriceList:PricelistObjectArray looks like %@",priceListObjectArray);
                                             }

                                             AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication]delegate];

                                             NSPersistentContainer *container = appDelegate.persistentContainer;

                                             NSArray *arrayOfArrays = [NWTillHelper splitIntoArraysOfBatchSize:priceListObjectArray :1000];

                                             for(NSArray *batch in arrayOfArrays) {

                                                 [container performBackgroundTask:^(NSManagedObjectContext *context ) {
                                                     context.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy;


                                                     NSDictionary *priceListObjectDict;

                                                     //Loop through the array and for each dictionary insert into local DB
                                                     for (id element in batch) {
                                                         priceListObjectDict = element;

                                                         NSString *currencyName = [priceListObjectDict objectForKey:@"currencyName"];
                                                         NSString *price = [priceListObjectDict objectForKey:@"price"];
                                                         NSString *priceIncTax = [priceListObjectDict objectForKey:@"priceIncTAX"];
                                                         NSString *validFrom = [priceListObjectDict objectForKey:@"validFromDate"];
                                                         NSString *validTo = [priceListObjectDict objectForKey:@"validToDate"];
                                                         NSString *itemId = [priceListObjectDict objectForKey:@"itemID"];

                                                         NSDateFormatter *dateFormat = [[NSDateFormatter alloc] init];
                                                         [dateFormat setDateFormat:@"YYYY-MM-dd'T'HH:mm:ss"];
                                                         NSDate *validToDate = [dateFormat dateFromString:validTo];
                                                         NSDate *validFromDate = [dateFormat dateFromString:validFrom];

                                                         if([NWTillHelper isDebug] == 1) {
                                                             NSLog(@"WebServices:fetchPriceList:validToDate: >>>> %@ <<<<", validToDate);
                                                             NSLog(@"WebServices:fetchPriceList:validFromDate: >>>> %@ <<<<", validFromDate);
                                                         }

                                                         if([NWTillHelper isDebug] == 1) {
                                                             NSLog(@"PimItemListView:tableView:context = %@", context);
                                                         }

                                                         NSManagedObject *newPrlItem = Nil;
                                                         newPrlItem = [NSEntityDescription
                                                                       insertNewObjectForEntityForName:@"PriceList"
                                                                       inManagedObjectContext:context];

                                                         [newPrlItem setValue:itemId forKey:@"itemId"];
                                                         [newPrlItem setValue:validToDate forKey:@"validTo"];
                                                         [newPrlItem setValue:validFromDate forKey:@"validFrom"];
                                                         [newPrlItem setValue:price forKey:@"price"];
                                                         [newPrlItem setValue:priceIncTax forKey:@"priceIncTax"];
                                                         [newPrlItem setValue:currencyName forKey:@"currencyName"];

                                                         if ([NWTillHelper isDebug] == 1) {
                                                             NSLog(@"WebServices:fetchTillData:ItemId in loop = %@", itemId);
                                                             NSLog(@"WebServices:fetchTillData:newPrlItem = %@", newPrlItem);
                                                             NSLog(@"WebServices:fetchTillData:CoreData error = %@", error);
                                                         }
                                                     }
                                                     NSError *error = nil;
                                                     if (![context save:&error]) {
                                                         NSLog(@"Failure to save context: %@\n%@", [error localizedDescription], [error userInfo]);
                                                         abort();
                                                     } else {
                                                         NSUserDefaults *tillUserDefaults = [NSUserDefaults standardUserDefaults];
                                                         [tillUserDefaults setInteger:1 forKey:@"hasPriceList"];
                                                         [tillUserDefaults synchronize];
                                                     }
                                                 }];
                                             }
                                         }
                                     }
                                 }] resume];
}

It still raises to 2 GB and gets terminated, when the NSURLSession completion block hits memory usage is about 250, I assume that is after it downloads the entire data set into the NSArray, but after that when I want to write to core data all goes terribly wrong and it goes to 2 GB and gets terminated

Why is that?

Matt Douhan
  • 2,053
  • 1
  • 21
  • 40

1 Answers1

0

First split up your large array into an array of arrays. Experiment with a good batch size that is not too large (that the app will crash) or too small (that will take a long time). I would suggest starting with 500. See here how do do this: What is an easy way to break an NSArray with 4000+ objects in it into multiple arrays with 30 objects each?. I assume you can turn that code into an array extension..

    NSArray *arrayOfArrays = [tillBasicDataArray splitIntoArrayBatchSize:500];

Next you can enqueue many block that will each process only one of the many array and save it at the end.

    for(NSArray *batch in arrayOfArrays){
         [container performBackgroundTask:^(NSManagedObjectContext *context ) {
          ...
          for (id element in batch){
          ...

each performBackgroundTask is enqueued into an internal operation and will process one at a time.

The rest of your code remains basically the same.

Jon Rose
  • 8,373
  • 1
  • 30
  • 36
  • this don't make any difference, the memory usage after the download of the originalArray is about 250 MB, but when it starts looping and putting it into core data it reaches 2 GB very fast and crashes I am adding my new code into the original question, I am now splitting it into an array or arrays but it makes no difference – Matt Douhan Jun 14 '17 at 16:15
  • There is a good solution to that here: https://stackoverflow.com/questions/15932492/ipad-parsing-an-extremely-huge-json-file-between-50-and-100-mb. – Jon Rose Jun 15 '17 at 06:41
  • I am already doing that, splitting the API up and downloading in batches etc, the problem is that when I process the batches they are all run in parallel, s I get 300 batches trying to process 1000 objects and put into core data, I cannot figure out how to use Operation queues or another mechanism to stop that, I tried adding Queue Blocks in the for loop that processes each batch but that didn't work so I must be doing something wrong – Matt Douhan Jun 15 '17 at 07:14
  • I suspect that would have trouble with just parsing such a huge amount of data even without core data. Can you try just saving each array to a file on disk and see if it still runs out of memory. (if it does it may show that the memory issue happens before the core-data stuff) – Jon Rose Jun 15 '17 at 07:26
  • Same problem, how can I ensure I process the batches in series? wether I save to file or core data like you say is probably irrelevant, I have to ensure I process the arrays one by one so I give myself a chance to free up resources in between, thats where I am stuck I am creating the arrayOfArrays that part works fine, its after that in my for loop that I don't understand how to serialize the processing and release resources before processing the next array in the batch – Matt Douhan Jun 15 '17 at 07:27
  • writeToURL:atomically: – Jon Rose Jun 15 '17 at 07:31
  • Yeah I just tried same problem as you say it's the processing more likely – Matt Douhan Jun 15 '17 at 07:32
  • So after downloading the file save it to disk. The parse it piecewise using something like https://github.com/gabriel/yajl-objc. Then process the core data in batches. (or better- as many people have already stated - get a smaller file) – Jon Rose Jun 15 '17 at 07:35
  • its not a file its a JSON API, I want to process inline in batches so to say – Matt Douhan Jun 15 '17 at 07:36
  • and I can get a smaller batch from the server thats not an issue either, the issue is that even I get 100 records at the from the server, I don't know how to process them serially when I loop over the total set, a for loop that launched NSURSession that each takes 100 records means I will get 3000 parallel NSURLSessions etc thats my real problem nothing else – Matt Douhan Jun 15 '17 at 08:49
  • so make an operation to process them. – Jon Rose Jun 15 '17 at 08:59
  • Sure but how? This is where I get stuck I don't know how and the operation will wait for the completion handler ? Or when will it release the next thing in queue ? – Matt Douhan Jun 15 '17 at 09:00
  • the culrpit is here right, for(NSArray *batch in arrayOfArrays) { [container performBackgroundTask:^(NSManagedObjectContext *context ) { context.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy; it will create 300 contexts in the container and try and process all of them in parallel – Matt Douhan Jun 15 '17 at 09:15
  • performBackgroundTask blocks are run is series. spliting up the array itself will double its memory. – Jon Rose Jun 15 '17 at 09:16
  • Yeah but my for loop is creating 300 background tasks is it not? or thats handled automatically? – Matt Douhan Jun 15 '17 at 09:19
  • the tasks are created but they enqueued into an operation queue – Jon Rose Jun 15 '17 at 09:20
  • ok then I have no idea how to solve this issue? what can I do then? it seems I am doing the right thing but it still eats memory like it hasn't eaten in lightyears – Matt Douhan Jun 15 '17 at 09:21
  • did you read this: https://stackoverflow.com/questions/15932492/ipad-parsing-an-extremely-huge-json-file-between-50-and-100-mb – Jon Rose Jun 15 '17 at 09:23
  • Yes and as I said above, I control the server so I can tell server to give me smaller pieces, like batches of 100 items at the time, which the app processes fine if I do it one time, but again, how to I ask for the second batch? and so on from the server? and avoid it being run in parallel? if I limit the API call to 100 items it works great, I just call https://domain.foo.bar/api/startat1%take100rows and NSURLSession happily fixes that, but how do I release those resources and then start at 100 take 100 ? I tried a for loop but that fires all in parallel so I am so confused – Matt Douhan Jun 15 '17 at 09:29
  • There has to be a way to: Ask for 100, process and save to core data, ask for another 100, repeat, I know how to ask for 100, I know how to process and save, but I don't know how to wait and then ask for the next 100 once the first is saved and done – Matt Douhan Jun 15 '17 at 09:31
  • NSOperationQueue – Jon Rose Jun 15 '17 at 09:32
  • Ok where exactly do I implement that? – Matt Douhan Jun 15 '17 at 09:33