Core data executeFetchRequest consumes huge amounts of memory - ios

I am inserting cca 100 000 records in core data database. Database contains 3 entities.
Player, Club, PlayerClub
Entities are in relations:
Player<-->>PlayerClub<<-->Club
While inserting in PlayerClub I have noticed a lot of memory consumption and loss of speed after about 50 000 records have been inserted. Records are never updated cause PlayerClub has only unique values.
For test reasons I have emptied Player and Club tables and run the program again. There was no speed drop but memory consumption was huge again. This is the code:
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] init];
[context setPersistentStoreCoordinator:ap.persistentStoreCoordinator];
[context setUndoManager:nil];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"PlayerClub"
inManagedObjectContext:context];
NSFetchRequest *request = [[NSFetchRequest alloc] init];
[request setEntity:entity];
NSEntityDescription *entityKlubovi = [NSEntityDescription entityForName:#"Club" inManagedObjectContext:context];
NSFetchRequest *requestKlubovi = [[NSFetchRequest alloc] init];
[requestKlubovi setEntity:entityKlubovi];
[requestKlubovi setFetchLimit:1];
NSEntityDescription *entityIgraci = [NSEntityDescription entityForName:#"Player" inManagedObjectContext:context];
NSFetchRequest *requestIgraci = [[NSFetchRequest alloc] init];
[requestIgraci setFetchLimit:1];
[requestIgraci setEntity:entityIgraci];
for (int i=0; i<[parsedKlubIgraci count]; i++) {
#autoreleasepool {
KlubIgrac *klubIgrac = [[KlubIgrac alloc] initWithEntity:entity insertIntoManagedObjectContext:context];
klubIgrac.iD = p.id;
//... add some othe properties
variables = [NSDictionary dictionaryWithObject:p.igracID forKey:#"ID"];
localPredicate = [predicateIgraci predicateWithSubstitutionVariables:variables];
[requestIgraci setPredicate:localPredicate];
klubIgrac.igrac = [self DohvatiIgracZaIgracId:p.igracID cntx:context request:requestIgraci predicate:localPredicate];
variables = [NSDictionary dictionaryWithObject:p.klubID forKey:#"ID"];
localPredicate = [predicateKlubovi predicateWithSubstitutionVariables:variables];
[requestKlubovi setPredicate:localPredicate];
klubIgrac.klub = [self DohvatiKlubZaKlubId:p.klubID cntx:context request:requestKlubovi predicate:localPredicate];
}
}
+(Club *)DohvatiKlubZaKlubId:(int)klubid cntx:(NSManagedObjectContext *)context
request:(NSFetchRequest *)request predicate:(NSPredicate *)predicate
{
#autoreleasepool {
NSError *error;
NSArray *arTmp;
arTmp = [context executeFetchRequest:request error:&error];
Club *klub;
if(arTmp != nil && [arTmp count]){
return [arTmp objectAtIndex:0];
}
}
}
DohvatiIgracZaIgracId method is basically the same method as DohvatiKlubZaKlubId so I dinnt post it.
This code is called about 100 000 times. The memory consumption before is about 150 MB. After it finishes its 650 MB. So it consumes 500 MB, without saving the context and without fetching anything from the database cause the tables are empty. If I comment
arTmp = [context executeFetchRequest:request error:&error];
line in DohvatiIgracZaIgracId and DohvatiKlubZaKlubId methods the memory consumption falls to 200 MB. So the diference is 400MB for a line of code that does nothing. This cant be. Anyone has some ideas. After a while my app consumes more than 2.5 GB.
The mesuments were done on simulator cause I need to build a database that I will preload later, so the speed is of importance :)... Thx in advance

without saving the context
This is the part of the question where experienced Core Data developers say "oh holy crap". That's your biggest problem right there. Save changes at regular intervals-- every 50 entries, or every 100, but whatever you do, don't wait until you're finished. You're forcing Core Data to keep all of those objects in memory as unsaved changes. This is the biggest reason you're having memory problems.
Some other things you should consider:
Don't fetch your objects one at a time. Fetches are relatively expensive. If you run through 100k instances and fetch each of them one at a time, your code will be spending almost all of its time executing fetches. Fetch in batches of 50-100 (you can tune the number to get the best balance of speed vs. memory use). Process one batch, then save changes at the end of the batch.
When you're done with a fetched object, tell the managed object context that you're done. Do this by calling refreshObject:mergeChanges: with NO as the second argument. That tells the context it can free up any internal memory it's using for the object. This loses any unsaved changes on the objects, but if you haven't made any changes then there's nothing to lose.
Consider getting rid of the PlayerClub entity completely. Core Data supports many-to-many relationships. This kind of entity is almost never useful. You're using Core Data, not SQL, so don't design your entity types as if you were.

Related

CoreData - delete all entities consumes RAM and take a long time

I have a CoreData configuration where I stored measured data. The data is grouped by Session, so each Session entity has a one-to-many relationship with SensorData. The Delete Rule is set to Cascade. I also have a Delete All button. When it is pressed I run the following code.
// All sessions (cascading)
[self.context performBlockAndWait:^{
// Fetch only managedObjectID to reduce memory impact
NSFetchRequest *sessionsRequest = [NSFetchRequest fetchRequestWithEntityName:#"Session"];
[sessionsRequest setIncludesPropertyValues:NO];
NSError *sessionsError;
NSArray *allSessions = [self.context executeFetchRequest:sessionsRequest error:&sessionsError];
if (sessionsError) {
NSLog(#"Failed to fetch all sessions. %#", sessionsError);
}
// Release fetch request
sessionsRequest = nil;
// Loop and delete
int i = 0;
for (NSManagedObject *session in allSessions) {
NSLog(#"Deleting session (%d / %d)", ++i, allSessions.count);
if (i != allSessions.count) {
[self.context deleteObject:session];
}
}
NSLog(#"All session deleted.");
}];
NSLog(#"Saving.");
[self.context performBlock:^{
[self.document saveToURL:self.documentPath
forSaveOperation:UIDocumentSaveForOverwriting
completionHandler:^(BOOL success){
NSLog(#"Document saved %#.", success ? #"successfully" : #"unsuccessfully");
}];
}];
NSLog(#"Done saving.");
What happens is that I get the log output as below, so that executes fairly quickly. But the UI then freezes and RAM usage rockets.
There are ~60 sessions and ~1M measurements (3 float values per measurement) and eventually RAM usage is too large and app crashes after 20 min or so, with all entries still there.
Obviously the saving is at play here, but what am I doing wrong? Grateful for any pointers.
Log output:
2015-08-13 15:16:56.825 MyApp[4201:1660697] Deleting session (1 / 61)
2015-08-13 15:16:56.826 MyApp[4201:1660697] Deleting session (2 / 61)
.
.
.
2015-08-13 15:16:56.862 MyApp[4201:1660697] Deleting session (60 / 61)
2015-08-13 15:16:56.863 MyApp[4201:1660697] Deleting session (61 / 61)
2015-08-13 15:16:56.863 MyApp[4201:1660697] All session deleted.
2015-08-13 15:16:56.864 MyApp[4201:1660697] Saving.
2015-08-13 15:16:56.864 MyApp[4201:1660697] Done saving.
Update
I edited the procedure to first delete the measurement data and by setting a fetch limit to keep RAM down (as suggested). However, if I delete 200 of SensorData, saving takes around 3s, except for the first ~1000 entries. Deletion is fast though. See trace below.
I would like to fix this without deleting the document. Feels like a hack (albeit probably a good one).
Trace
the thing that takes a lot of memory is that you are loading all sessions :
NSArray *allSessions = [self.context executeFetchRequest:sessionsRequest error:&sessionsError];
try to load for example 100 by 100 like this :
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
[self.context setUndoManager:nil];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Session" inManagedObjectContext:self.context];
[fetchRequest setEntity:entity];
[fetchRequest setIncludesPropertyValues:NO];
[fetchRequest setFetchLimit:100];
NSError *error;
NSArray *items = [self.context executeFetchRequest:fetchRequest error:&error];
while ([items count] > 0) {
#autoreleasepool {
for (NSManagedObject *item in items) {
[self.context deleteObject:item];
}
if (![self.context save:&error]) {
NSLog(#"Error deleting %# - error:%#",self.entityName, error);
}
}
items = [self.context executeFetchRequest:fetchRequest error:&error];
}
As a quick fix without knowing the answers to the above questions. I would delete all the measurement data first then delete the session data. But most importantly you should always set a batch size on your fetch request.
// Fetch only managedObjectID to reduce memory impact
NSFetchRequest *sessionsRequest = [NSFetchRequest fetchRequestWithEntityName:#"Session"];
[sessionsRequest setIncludesPropertyValues:NO];
[sessionsRequest setFetchBatchSize:20];
The batch size of the receiver.
The default value is 0. A batch size of 0 is treated as infinite, which disables the batch faulting behavior.
If you set a non-zero batch size, the collection of objects returned when the fetch is executed is broken into batches. When the fetch is executed, the entire request is evaluated and the identities of all matching objects recorded, but no more than batchSize objects’ data will be fetched from the persistent store at a time. The array returned from executing the request will be a proxy object that transparently faults batches on demand. (In database terms, this is an in-memory cursor.)
You can use this feature to restrict the working set of data in your application. In combination with fetchLimit, you can create a subrange of an arbitrary result set.
For purposes of thread safety, you should consider the array proxy returned when the fetch is executed as being owned by the managed object context the request is executed against, and treat it as if it were a managed object registered with that context.
As Tom said, it may be easier to delete the entire document, and start with a fresh document.
iOS9 introduces NSBatchDeleteRequest which should help you. You can see the WWDC video here: https://developer.apple.com/videos/wwdc/2015/?id=220, and that part is discussed starting 15 minutes into the video.
Here is a link to a similar question/answer: Core Data: delete all objects of an entity type, ie clear a table

Check if database is empty in iOS without of calling current NSManagedObjectContext (Core Data)?

My question is similar to the following one but has differences
I use Core Data. The problem is if I use the current NSManagedObjectContext then I may get an incorrect result. For example:
From the beginning the db is filled.
My app deletes all the entities via the current context.
I get check if db filled and get NO but in reality the db is still filled because the context should be saved to apply the changes.
And example of my solution:
- (BOOL)isDBFilled {
//separate context to
NSManagedObjectContext *context = [[[NSManagedObjectContext alloc] init] autorelease];
[context setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
[context setPersistentStoreCoordinator:coordinator];
NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease];
NSEntityDescription *entity = [NSEntityDescription entityForName:... inManagedObjectContext:context];
[request setEntity:entity];
[request setFetchLimit:1];
NSError *error = nil;
NSArray *results = [context executeFetchRequest:request error:&error];
if (!results) {
LOG(#"Fetch error: %#", error);
abort();
}
if ([results count] == 0) {
return NO;
}
return YES;
}
So is it normal and safe to create another context just to check if db is filled? Or Is there better ways to perform this checking except of creating a separate BOOL variable which will be changed before db changes and after them?
It is fine to use a separate context to check if the database is populated or not. The overhead is minimal, so I would have no objections.
Please note that Apple does not necessarily agree. From the "Core Data Snippets":
Neither should a view controller create a context for its own use (unless it’s a nested context).
That being said, I do not see the need for this. If you delete all entities, save and then check should get an empty database. If you delete all entities and do not save and then check, you should get a non-empty database. You can easily - and more easily - do this with one context.
PS: autorelease seems to indicate that you are using a very old SDK. How about upgrading?

Multithreaded Core Data causing UI to hang

I'm trying to execute a fetch against a fairly large data set (~60000 rows) in the background of my app. Despite using a separate thread, the UI noticeably hangs for a second or so whenever the fetch executes. Is my approach correct?
- (id)init
{
if(self = [super init])
{
ABAppDelegate *appDelegate = (ABAppDelegate *)[[UIApplication sharedApplication] delegate];
_rootManagedObjectContext = appDelegate.managedObjectContext;
_backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_backgroundContext setPersistentStoreCoordinator:_rootManagedObjectContext.persistentStoreCoordinator];
}
return self;
}
- (void)fetch {
[_backgroundContext performBlock:^{
NSFetchRequest *fetchRequest = [NSFetchRequest fetchRequestWithEntityName:#"ItemPhoto"];
NSPredicate *pred = [NSPredicate predicateWithFormat:#"full_uploaded_to_server == 0 OR thumb_uploaded_to_server == 0"];
fetchRequest.predicate = pred;
NSSortDescriptor *sort = [NSSortDescriptor sortDescriptorWithKey:#"modified" ascending:YES]; //Start at the back of the queue
fetchRequest.sortDescriptors = [NSArray arrayWithObject:sort];
fetchRequest.fetchBatchSize = 1;
fetchRequest.fetchLimit = 1;
NSError *error;
NSArray *photos = [_backgroundContext executeFetchRequest:fetchRequest error:&error];
if(error != nil) {
NSLog(#"Fetch error: %#", error.localizedDescription);
}
}];
}
Looking in Instruments, it's definitely the call to executeFetchRequest: that's taking a long time to complete, and it does seem to be running on its own thread. Why would this be causing the UI to hang?
Thanks!
Any activity against the NSPersistentStoreCoordinator is going to cause a block of other activities against it. If you are fetching on one thread and another thread attempts to access the NSPersistentStoreCoordinator it is going to be blocked.
This can be solved in one of two ways:
Reduce the fetches so that the block is not noticeable. Fetching in chunks, for example, can help to reduce this issue.
Make sure the data the UI is exposing is fully loaded into memory. This will stop the main thread from trying to hit the NSPersistentStoreCoordinator and getting blocked.
Depending on the application and the situation, one or the other (or both) of these implementations will remove the issue.
At the end of the day, background threads are not a silver bullet to solve fetch times. If you are planning on fetching on a background thread just to put them onto the main thread you are going to be disappointed with the performance.
If you are fetching on the background thread for a non-UI purpose then consider reducing the size of each fetch or change the batch size, etc. to decrease the fetch time itself.
Update
Warming up the cache doesn't work on iOS like it used to on OS X. You can get the objects fully loaded into memory by configuring your NSFetchRequest so that the objects are fully loaded, relationships are loaded (if needed), your batch size and fetch size are large enough. Naturally this needs to be balanced against memory usage.

NSEntityDescription still exist in memory?

Now I have a Core Data entity "AAA", and I use a method to fetch its result:
- (AAA *)result{
NSEntityDescription *Entity = [NSEntityDescription entityForName:#"AAA" inManagedObjectContext:self.managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc]init];
[fetchRequest setEntity:aaaEntity];
NSError *error = nil;
NSArray *fetchRequestResult = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
AAA *aaa = fetchRequestResult.lastObject;
return aaa;
}
Then I use Xcode Instruments to check the memory status, it shows:
VM:Core Data Object IDs 4.02MB(Live Bytes)
Is the entity still live in the memory?
First of all I would start to say that you should not be worried about memory when you deal with Core Data. Under the hood the framework manages stuff for you. When you retrieve an object, Core Data populate a cache where data are stored in. In this way, further fetches will not hit the disk but the cache only.
Anyway, you could rely on two different APIs to control memory footprint. The first one is [context reset]. This will clear the entire object graph (that belongs to a specific context) as if you had just created it.
The second one is [context refreshObject:yourManagedObject mergeChanges:NO]. It allows releasing an object, or turning it into a fault.
Hope it helps.

CoreData in a tableCell Causes a stutter/lag IOS/xCode

Good Morning.
I have a problem when I run my app on my device, it lags/stutters when I scroll in the main tableView.
I've narrowed the problem down to a call to core data from inside my tableCell
--In cell for row at indexPath
person is a custom class and contact manager is my file with all my calls to core data and manipulating data
person.contactSelected = [contactManager checkContactSelectedStatus:person];
--In my contactManager file the call goes to this function.
and just updates the contacts selected status (when the user presses a button to change from being allowed in the call or not in the call)
-(NSNumber *) checkContactSelectedStatus:(ContactPerson *)person{
SWNAppDelegate *delegate = [[UIApplication sharedApplication]delegate];
NSManagedObjectContext *context = [delegate managedObjectContext];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Contact" inManagedObjectContext:context];
NSFetchRequest *req =[[NSFetchRequest alloc]init];
[req setEntity:entity];
NSPredicate *pred =[NSPredicate predicateWithFormat:#"(recordID like %#)",[person.recordID stringValue]];
[req setPredicate:pred];
NSError *error;
NSManagedObject *checkStatus = [[context executeFetchRequest:req error:&error] objectAtIndex:0];
person.contactSelected = [checkStatus valueForKey:#"isSelected"];
return person.contactSelected;}
Is there an easy way to throw this into a Queue? I have read and tried to figure out how to send a NSManagedObject to queues, but when I create a child of the Parent MoC, It can not find the Entity "Contact". I don't know if there is a simpler way to do it or not!?
Thanks for your time, and
WhatWasIThinking!?!?!
Yes, this is really inefficient code. The fetch has to be done multiple times, i.e. for each cell as it becomes visible.
You should instead use an NSFetchedResultsController which is especially designed to work with table views. It will decide the appropriate number of trips to the store for fetches and optimize for speed and memory footprint.
Also, you will most likely use significantly less code.
Besides, a predicate string like recordID like %# does not make much sense. If you are using IDs they should be unique so you can index them and look them up really fast. LIKE, just like string functions such as CONTAINS are very slow in comparison.

Resources