What (tf) are the secrets behind PDF memory allocation (CGPDFDocumentRef) - iphone

For a PDF reader I want to prepare a document by taking 'screenshots' of each page and save them to disc. First approach is
CGPDFDocumentRef document = CGPDFDocumentCreateWithURL((CFURLRef) someURL);
for (int i = 1; i<=pageCount; i++)
{
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
CGPDFPageRef page = CGPDFDocumentGetPage(document, i);
...//getting + manipulating graphics context etc.
...
CGContextDrawPDFPage(context, page);
...
UIImage *resultingImage = UIGraphicsGetImageFromCurrentImageContext();
...//saving the image to disc
[pool drain];
}
CGPDFDocumentRelease(document);
This results in a lot of memory which seems not to be released after the first run of the loop (preparing the 1st document), but no more unreleased memory in additional runs:
MEMORY BEFORE: 6 MB
MEMORY DURING 1ST DOC: 40 MB
MEMORY AFTER 1ST DOC: 25 MB
MEMORY DURING 2ND DOC: 40 MB
MEMORY AFTER 2ND DOC: 25 MB
....
Changing the code to
for (int i = 1; i<=pageCount; i++)
{
CGPDFDocumentRef document = CGPDFDocumentCreateWithURL((CFURLRef) someURL);
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
CGPDFPageRef page = CGPDFDocumentGetPage(document, i);
...//getting + manipulating graphics context etc.
...
CGContextDrawPDFPage(context, page);
...
UIImage *resultingImage = UIGraphicsGetImageFromCurrentImageContext();
...//saving the image to disc
CGPDFDocumentRelease(document);
[pool drain];
}
changes the memory usage to
MEMORY BEFORE: 6 MB
MEMORY DURING 1ST DOC: 9 MB
MEMORY AFTER 1ST DOC: 7 MB
MEMORY DURING 2ND DOC: 9 MB
MEMORY AFTER 2ND DOC: 7 MB
....
but is obviously a step backwards in performance.
When I start reading a PDF (later in time, different thread) in the first case no more memory is allocated (staying at 25 MB), while in the second case memory goes up to 20 MB (from 7).
In both cases, when I remove the CGContextDrawPDFPage(context, page); line memory is (nearly) constant at 6 MB during and after all preparations of documents.
Can anybody explain whats going on there?

CGPDFDocument caches pretty aggressively and you have very little control over that, apart from - as you've done - releasing the document and reloading it from disk.
The reason you don't see a lot of allocations when you remove the CGContextDrawPDFPage call is that Quartz loads page resources lazily. When you just call CGPDFDocumentGetPage, all that happens is that it loads some basic metadata, like bounding boxes and annotations (very small in memory).
Fonts, images, etc. are only loaded when you actually draw the page - but then they're retained for a relatively long time in an internal cache. This is meant to make rendering faster, because page resources are often shared between multiple pages. Also, it's fairly common to render a page multiple times (e.g. when zooming in). You'll notice that it's significantly faster to render a page the second time.

Related

NSCache is not evicting data

NSCache is a rarely used tool which actually looks quite useful. I created a simple experiment to see how it works and it looks like it does not auto-evict data in low memory situations (or I am doing something wrong!)
- (void)viewDidLoad
{
_testCache = [[NSCache alloc] init];
// Allocate 600 MB of zeros and save to NSCache
NSMutableData* largeData = [[NSMutableData alloc] init];
[largeData setLength:1024 * 1024 * 600];
[_testCache setObject:largeData forKey:#"original_Data"];
}
- (IBAction)buttonWasTapped:(id)sender {
// Allocate & save to cache 300 MB each time the button is pressed
NSMutableData* largeData = [[NSMutableData alloc] init];
[largeData setLength:1024 * 1024 * 300];
static int count = 2;
NSString* key = [NSString stringWithFormat:#"test_data_%d", count++];
[_testCache setObject:largeData forKey:key];
NSMutableData* dataRecoveredFromCache = [_testCache objectForKey:#"original_Data"];
if (dataRecoveredFromCache) {
NSLog(#"Original data is ok");
} else {
NSLog(#"Original data is missing (purged from cache)");
}
}
So I ran the app in the simulator, and taped the button a few times however no items were evicted... The app eventually crashed:
2012-07-17 14:19:36.877 NSCacheTest[15302:f803] Data is ok
2012-07-17 14:19:37.365 NSCacheTest[15302:f803] Data is ok
2012-07-17 14:19:37.861 NSCacheTest[15302:f803] Data is ok
2012-07-17 14:19:38.341 NSCacheTest[15302:f803] Data is ok
2012-07-17 14:19:38.821 NSCacheTest[15302:f803] Data is ok
NSCacheTest(15302,0xac0942c0) malloc: *** mmap(size=393216000) failed (error code=12)
*** error: can't allocate region
From the doc (Emphasis mine): The NSCache class incorporates various auto-removal policies, which ensure that it does not use too much of the system’s memory. The system automatically carries out these policies if memory is needed by other applications. When invoked, these policies remove some items from the cache, minimizing its memory footprint.
Apple does not state that the memory will be freed on memory warning - in my experience, the cache is most often purged when the app goes to background or when you add more large elements.
Here's quoted docs ...
The NSCache class incorporates various auto-removal policies, which
ensure that it does not use too much of the system’s memory. The
system automatically carries out these policies if memory is needed by
other applications. When invoked, these policies remove some items
from the cache, minimizing its memory footprint.
... as you can see it states that it removes some items, not all items. It depends on NSCache internal policies, available memory, device status, etc. You shouldn't worry about these policies.
You can control them with countLimit, totalCostLimit properties and you can add object with cost, look at setObject:forKey:cost:.
Also you can evict objects by yourself. Add NSDiscardableContent protocol implementation to your objects and setEvictsObjectsWithDiscardedContent: to YES.
I am using that class too. Note that the documentation states the NSCache is tied into the OS and probably has access to memory information deep within the OS. The memory warning is just that - it just sends a memory warning to the appDelegate/viewControllers.
If you really want to test your code out, you will probably need a test mode where you start mallocing lots of memory (creating a huge leak so to speak). You might need parcel this out in chunks during each main runloop, so the OS has the opportunity to see he memory going down (I have an app that chew ups lots of memory, and it does it so fast on the 3GS it just gets killed never having got a memory warning.

Get amount of memory used by app in iOS

I'm working on an upload app that splits files before upload. It splits the files to prevent being closed by iOS for using too much memory as some of the files can be rather large. It would be great if I could, instead of setting the max "chunk" size, set the max memory usage and determine the size using that.
Something like this
#define MAX_MEM_USAGE 20000000 //20MB
#define MIN_CHUNK_SIZE 5000 //5KB
-(void)uploadAsset:(ALAsset*)asset
{
long totalBytesRead = 0;
ALAssetRepresentation *representation = [asset defaultRepresentation];
while(totalBytesRead < [representation size])
{
long chunkSize = MAX_MEM_USAGE - [self getCurrentMemUsage];
chunkSize = min([representation size] - totalBytesRead,max(chunkSize,MIN_CHUNK_SIZE));//if I can't get 5KB without getting killed then I'm going to get killed
uint8_t *buffer = malloc(chunkSize);
//read file chunk in here, adding the result to totalBytesRead
//upload chunk here
}
}
Is essentially what I'm going for. I can't seem to find a way to get the current memory usage of my app specifically. I don't really care about the amount of system memory left.
The only way I've been able to think of is one I don't like much. Grab the amount of system memory on the first line of main in my app, then store it in a static variable in a globals class then the getCurrentMemUsage would go something like this
-(long)getCurrentMemUsage
{
long sysUsage = [self getSystemMemoryUsed];
return sysUsage - [Globals origSysUsage];
}
This has some serious drawbacks. The most obvious one to me is that another app might get killed in the middle of my upload, which could drop sysUsage lower than origSysUsage resulting in a negative number even if my app is using 10MB of memory which could result in my app using 40MB for a request rather than the maximum which is 20MB. I could always set it up so it clamps the value between MIN_CHUNK_SIZE and MAX_MEM_USAGE, but that would just be a workaround instead of an actual solution.
If there are any suggestions as to getting the amount of memory used by an app or even different methods for managing a dynamic chunk size I would appreciate either.
Now, as with any virtual memory operating system, getting the "memory used" is not very well defined and is notoriously difficult to define and calculate.
Fortunately, thanks to the virtual memory manager, your problem can be solved quite easily: the mmap() C function. Basically, it allows your app to virtually load the file into memory, treating it as if it were in RAM, but it is actually swapped in from storage as it is accessed, and swapped out when iOS is low on memory.
This function is really easy to use in iOS with the Cocoa APIs for it:
- (void) uploadMyFile:(NSString*)fileName {
NSData* fileData = [NSData dataWithContentsOfMappedFile:fileName];
// Work with the data as with any NSData* object. The iOS kernel
// will take care of loading the file as needed.
}

Memory issue or something else?

I'm running an application on my iPad that loads images into a carousel. In total there are 138 images. If I reduce that number to 100, the application loads fine. At 138, however, the application pauses, rather than crashes.
The viewDidLoad and first for statement are being reached, as indicated by a breakpoint. The issue lies in the second for statement.
// loading images into the queue
loadImagesOperationQueue = [[NSOperationQueue alloc] init];
NSMutableArray *tmpArray = [[NSMutableArray alloc] initWithCapacity:14];
for (int i = 0; i < 137; i++) {
[tmpArray addObject:[NSString stringWithFormat:#"cover_%d.jpg", i]];
}
for (int i = 0; i < 137; i++) {
int index = arc4random() % [tmpArray count];
NSString *imageName = [tmpArray objectAtIndex:index];
[tmpArray removeObjectAtIndex:index];
[(AFOpenFlowView *)self.view setImage:[UIImage imageNamed:imageName] forIndex:i];
}
I'm guessing that there is a memory issue, although since I'm using iOS 5 with ARC I shouldn't have to be doing any manual memory management.
Could it possibly be that there is just too much going on to hold in memory? It's 138 images # ~146 KB per. That's roughly 20 MB, but I don't think that alone could cause the issue.
GDB quits without any useful output, actually no output at all. Running instruments reveals that when the pause takes place real memory usage is only at 6.11 MB, 77.4% CPU but 175 MB of virtual mem.
What I'm concerned about is the fact that there is no memory warning or even an actual crash, the thread just pauses and cannot be resumed or killed automatically, you have to kill it from within xcode.
It looks pretty like your application gets killed because it takes too much memory. Indeed, 20MB is the limit that I have experienced, and this also corresponds to what you could find by googling it.
About the fact that you don't receive any memory warning, this happens because you are loading the images in memory in a tight loop that does not returns control to the main loop. So you don't have the chance to receive the message didReceiveMemoryWarning.
The solution is pretty simple, pre-load a number of images and as you move through the carousel, pre-load some more images, while also releasing the older ones. In reality, you don't need to have in memory more than 5 images at any time to keep the balls running. If you are concerned about not having to load the images almost each and every time, then you can increase that number.

Understanding iOS Instruments

I am creating an iPhone app. Running into memory issues I started using Instruments to track down any memory problems. Am running into some strange behavior that leads me to believe that I am either mis-using Instruments or mis-reading its data.
These are the LiveBytes values recorded when moving in and out of a location:
**Expensive Location-**
World (12 MB)
Loc (27 MB)
World (13 MB )
Loc (28 MB)
World (14 MB)
-Crash
**Cheap Location-**
World (12 MB)
Loc (23 MB)
World (13 MB )
Loc (24 MB)
World (14 MB)
-Crash
Notice how I still crash even though the cheap location's memory has come no where near the expensive locations memory. Could anyone help me out here?
I'm not sure if this is related to the problem you have but I hope it helps: I was recently tracking the memory footprint of an app and I noticed that even though the dealloc message was being sent to a view controller after hitting "back" on the UINavigator controller, I still had a few dozen live objects left over from this operation (you can see this in the 'Allocations' panel of the instruments app). To solve this I used a mix of a few things:
First, I added the following three methods to NSLog the retain counters of my Custom subviews (found here on SO at iOS4 - fast context switching):
#pragma mark - RETAIN DEBUG MAGIC
// -----------------------------------------------------------------------------
- (id)retain
{
NSLog(#"retain \t%s \tretainCount: %i", __PRETTY_FUNCTION__ , [self retainCount]);
return [super retain];
}
- (void)release
{
NSLog(#"release \t%s \tretainCount: %i", __PRETTY_FUNCTION__ , [self retainCount]);
[super release];
}
- (id)autorelease
{
NSLog(#"autorelease \t%s \tretainCount: %i", __PRETTY_FUNCTION__ , [self retainCount]);
return [super autorelease];
}
Then, I isolated each one of the view building blocks leaving only one simple task (for example loading a UIButton as a subview) and went back to the instruments app to track the live objects (under Product > Profile in Xcode) and disabled all the objects with 'NS', 'CF' and 'Malloc' prefixes (you can do this clicking on the little i button next to the 'Allocations' tab). After this, selected "Call Trees" on the bottom right pane and kept drilling until I found a few places where the object counter went up as I navigated back and forth.
Notice that you can double click on the symbol to see the details related to the calls made to the processor. Additionally, clicking on the little i icon will bring a pop up with the backtraces for the highlighted call.
When looking at the backtraces you will see that some of them have a small icon that depicts a person on a frame (the text next to these icons is significantly darker as a visual cue). Double clicking on these will take you to the line in your code responsible for this call.
Below are a few links that might give you a hand in understanding more about instruments:
http://www.raywenderlich.com/2696/how-to-debug-memory-leaks-with-xcode-and-instruments-tutorial
http://developer.apple.com/library/mac/#documentation/DeveloperTools/Conceptual/InstrumentsUserGuide/ViewingandAnalyzingData/ViewingandAnalyzingData.html
Note:
At the end of my journey, all I had to do was release my views after adding them to their 'super' views to ensure they would be dealloc'd. i.e.,
[[self view] addSubView:aButton];
[aButton release];

Quartz PDF API Causing Out of Memory Crashes

I'm having crashing issues using the Quartz PDF API for iOS. At the moment I am compiling with the SDK 4.0 GM Seed and running on my 3.2 iPad (I have tried using the 3.2 SDK with identical results).
All the code I am using is based on the standard Apple Quartz documentation and from various sources around the internets. So I can't image I'm doing something drastically different or wrong.
The code runs perfectly in the Simulator (all versions, it's a Universal app) and even while using the "Simulate Memory Warning" function. I've used the Leaks tool and there are no leaks that it finds. Build and Analyze also finds nothing. No crash or low memory log is left in my Library.
All this leads me to believe the device is running out of memory. This happens after running through say 50 pdf pages, with about 35% having an image of some sort (some full page some icon). It does not crash on any particular page. The pdf I am loading is about 75 pages and 3.5MB.
I've perused similar issues on this site and around the internets, and have applied some of the advice in the code below. I now release the pdf document reference on every page turn and I no longer retain/release a page reference. I've also simplified the image swapping from using CGImages to just using the UIGraphicsGetImageFromCurrentImageContext function. I've tried various implementations for switching the images, including replacing the pdfImgView completely with a newly allocated temp instance (using [[UIImageView alloc] iniWithImage:UIGraphicsGetImageFromCurrentImageContext()]), using the setter for pdfImgView and releasing the temp. All of the variations pass the Leaks and Analyzer tests, but still exhibit the same crashing behavior.
So, before I move away from PDFs altogether, is there something I should try or something I am missing?
View controller code that is called in interface handlers to swap pages and on first load:
[self drawPage];
// ...animation code...simple CATransition animation...crashes with or without
// scrollView is a UIScrollView that is a subview of self.view
[scrollView.layer addAnimation:transition forKey:nil];
// pdfImgView is a UIImageView that is a subview of scrollView
pdfImgView.image = UIGraphicsGetImageFromCurrentImageContext();
drawPage method used to configure and draw PDF page to the context:
[CFURLRef pdfURL = CFBundleCopyResourceURL(CFBundleGetMainBundle(), CFSTR("BME_interior.pdf"), NULL, NULL);
pdfRef = CGPDFDocumentCreateWithURL((CFURLRef)pdfURL); // instance variable, not a property
CFRelease(pdfURL);
CGPDFPageRef page = CGPDFDocumentGetPage(pdfRef, currentPage);
CGRect box = CGPDFPageGetBoxRect(page, kCGPDFMediaBox);
// ...setting scale and imageHeight, both floats...
if (UIGraphicsBeginImageContextWithOptions != NULL) {
UIGraphicsBeginImageContextWithOptions(CGSizeMake(self.view.frame.size.width, imageHeight), NO, 0.0);
} else {
UIGraphicsBeginImageContext(CGSizeMake(self.view.frame.size.width, imageHeight));
}
CGContextRef context = UIGraphicsGetCurrentContext();
NSLog(#"page is %d, context is %d, pdf doc is %d, pdf page is %d", currentPage, context, pdfRef, page); // all prints properly
// ...setting up scrollView for new page, using same instance...
CGContextTranslateCTM(context, (self.view.frame.size.width-(box.size.width*scale))/2.0f, imageHeight);
CGContextScaleCTM(context, scale, -1.0*scale);
CGContextSaveGState(context);
CGContextDrawPDFPage(context, page);
CGContextRestoreGState(context);
CGPDFDocumentRelease(pdfRef);
pdfRef = NULL;
Aha! I've fixed the crashes by adding a UIGraphicsEndImageContext(); before beginning a new image context. I don't even get memory warnings now...
Calling
CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
CGContextSetRenderingIntent(context, kCGRenderingIntentDefault);
before CGContextDrawPDFPage solved a similar problem of mine.
Credits goes to this answer of Johann:
CGContextDrawPDFPage taking up large amounts of memory