Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UIDocument & NSFileWrapper - large files taking a long time to save, despite incremental changes

I have a UIDocument based app that uses NSFileWrappers to store data. The 'master' file wrapper contains many additional directory file wrappers, each of which represents a different page of the document.

When saving a large document for which only a small proportion of one page has been modified, UIDocument spends a LONG time in the background writing the changes (in writeContents:andAttributes:safelyToURL:forSaveOperation:error:). Surely it should only be writing out this one small change to the file wrapper... what's taking so long?

My contentsForType:error: override returns a new directory file wrapper with the contents of the master file wrapper (à la WWDC 2012 Session 218 - Using iCloud with UIDocument):

- (id)contentsForType:(NSString *)typeName error:(NSError *__autoreleasing *)outError
{
    if (!_fileWrapper) {
        [self setupEmptyDocument];
    }
    return [[NSFileWrapper alloc] initDirectoryWithFileWrappers:[_fileWrapper fileWrappers]];
}

And here's a lovely picture of a stack trace from Time Profiler:

UIDocument slow write stack trace

Incidentally, it says ~1.6s in that worker thread to save - in actual run time this equated to about 8 seconds.


Edit:

Is there some way I can check whether the file wrappers require writing to disk or not? Just so I can confirm that I'm not somehow doing something strange like updating every sub file wrapper when I make a small change (although I'm sure I'm not...).


Edit:

I had a further play around with the CloudNotes sample app, and it appears that NSFileWrapper does implement incremental saving, at least in that case! I tested it by initialising a document with 100 notes, each of which contained about 5MB of data. I did a small edit here and there (a single character change to a text view flags the document as needing saving), and recorded roughly how long each save took. The test is relatively crude (and run on the simulator), but the results were something like this:

  • 1st write: ~8000ms
  • 2nd write: ~4000ms
  • 3rd write: ~300ms
  • all subsequent writes: ~40ms

Obviously there are many factors affecting the time it takes, especially since it's saving using file coordination in a background thread, but in general the trend always seems to be this sort of exponential decay, until all writes become really very fast.

But I'm still trying to figure out why this doesn't happen in my app. For a large multi-page document (large, but still many times smaller than the document for the CloudNotes test I performed above) the user can be waiting many seconds for a document to close. I don't want to have to put a spinner up for something that should be practically instantaneous.

like image 650
Stuart Avatar asked Mar 05 '13 17:03

Stuart


Video Answer


2 Answers

NSFileWrapper is actually loading the entire document into memory. So with a UIDocument, using an NSFileWrapper is actually not good for large documents. The documentation makes you think it does incremental saving, but in my case it didn't seem to do that.

UIDocument isn't restricted to just NSFileWrapper or NSData. You can use your own custom class, you just have to override certain methods. I ended up writing my own file wrapper class that simply refers to files on disk and reads/writes individual files on-demand.

This is what my UIDocument class looks like using the custom file wrapper:

@implementation LSDocument

- (BOOL)writeContents:(LSFileWrapper *)contents
        andAttributes:(NSDictionary *)additionalFileAttributes
          safelyToURL:(NSURL *)url
     forSaveOperation:(UIDocumentSaveOperation)saveOperation
                error:(NSError *__autoreleasing *)outError
{
    return [contents writeUpdatesToURL:self.fileURL error:outError];
}

- (BOOL)readFromURL:(NSURL *)url error:(NSError *__autoreleasing *)outError
{
    __block LSFileWrapper *wrapper = [[LSFileWrapper alloc] initWithURL:url isDirectory:NO];
    __block BOOL result;
    dispatch_sync(dispatch_get_main_queue(), ^(void) {
        result = [self loadFromContents:wrapper
                                 ofType:self.fileType
                                  error:outError];
    });
    [wrapper loadCache];
    return result;
}

@end

I use this as a base class and subclass it for other projects. It should give you an idea of what you have to do to integrate a custom file wrapper class.

like image 119
Luke Avatar answered Oct 06 '22 13:10

Luke


I know this is a super old thread, but to help future travelers: In my case, I had a subdirectory NSFileWrapper which was not incrementally saving.

I found that if you make a copy of an NSFileWrapper, you need to set the copy's filename, fileAttributes (and possibly preferredFilename) to the original's in order for the save to be incremental. After copying those over, the contents of the subfolder would incrementally save (i.e. only write if replaced with new NSFileWrappers).

Note to Apple: Seriously, the whole NSFileWrapper API is a mess and should be cleaned up.

like image 38
Chen Lim Avatar answered Oct 06 '22 14:10

Chen Lim