11

I am using CloudKit as a server backend for my iOS application. I'm using it to house some relatively static data along with a handful of images(CKAsset). I ran into a problem when the time came for me to actually fetch those assets from the public database. They load at an excruciatingly slow speed.

My use case is to load an image into every cell inside of a collection view. The images are only 200kb in size, but the fetch process took an average of 2.2 seconds for the download to complete and set the image in a cell. For comparison, I took URLs of similar sized stock images and loaded them in using NSURLSession. It took a mere 0.18 - 0.25 seconds for each image to load.

I have tried multiple different ways of downloading the images from CK: direct fetch of the record, query, and operation query. All of them have similar results. I am also dispatching back to the main queue within the completion block prior to setting the image for the cell.

My database is setup to have a primary object with several fields of data. I then setup a backwards reference style system for the photos, where each photo just has a reference to a primary object. That way I can load the photos on demand without bogging down the main data.

It looks something like this:

Primary Object: title: String, startDate: Date

Photo Object: owner: String(reference to primary object), image: Asset

Here is an example request that I tried to directly fetch one of the photos:

let publicDb = CKContainer.defaultContainer().publicCloudDatabase
let configRecordId = CKRecordID(recordName: "e783f542-ec0f-46j4-9e99-b3e3ez505adf")

publicDb.fetchRecordWithID(configRecordId) { (record, error) -> Void in
    dispatch_async(dispatch_get_main_queue()) {
        guard let photoRecord = record else { return }
        guard let asset = photoRecord["image"] as? CKAsset else { return }

        guard let photo = NSData(contentsOfURL: asset.fileURL) else { return }

        let image = UIImage(data: photo)!

        cell.cardImageView.image = image
    }
}

I can't seem to figure out why these image downloads are taking so long, but it's really quite the showstopper if I can't get them to load in a reasonable about of time.

Update: I tried the fetch operation with a smaller image, 23kb. The fetch was faster, anywhere from 0.3 - 1.1 seconds. That's better, but still doesn't meet the expectation that I had for what CloudKit should be able to provide.

BlueBear
  • 7,469
  • 6
  • 32
  • 47
  • you are fetching the entire record. Are there more assets in that record? You could limit it to only the image field. Besides that, you only have to execute the last statement where you update the UI on the main queue. – Edwin Vermeer Feb 17 '16 at 07:21
  • @EdwinVermeer The record with the asset simply has two fields, one being the image, the other being a string value with a reference to its owner. I know that I can move those other lines out of dispatching to the main queue, but it makes no difference. The fetch's completion block is what takes forever to get called, not the performance of what's inside that block. – BlueBear Feb 17 '16 at 14:42
  • What kind of network are you testing on? You may want to check the operation's priority, also try manually fetching the record, instead of using the convenience methods. – mattsven Feb 29 '16 at 16:29
  • @mattsven I've tried this in several different network conditions, all of which perform exceptionally well. I have also tried every form of record request that CK has to offer. That includes directly fetching the record by id as well as attempting a fetch with a high priority operation. – BlueBear Feb 29 '16 at 16:39
  • @Jonathan And yet the issue persists? Is this all on the same device? Have you tried deleting all of the data from CloudKit dashboard and doing it again? – mattsven Mar 01 '16 at 13:56
  • @mattsven The issue persists regardless of how I fetch the request, network condition, or device. I've delete the record and started it from scratch, but not improvement. – BlueBear Mar 03 '16 at 04:22
  • I have the same issue. I ported an app from AWS to CloudKit. Everything works fine except for CKAsset. I have a simple table view with some images. With AWS I could download 25 images in less than 0.5 seconds. With CloudKit and CKAsset I have a hard time getting that down under 10 seconds. I have tried rewriting the code multiple ways and it still takes a long time for download of any CKAsset. Close to a second for an 80K image. This makes CloudKit unacceptable as a solution. – Jeff Zacharias May 01 '16 at 01:49

2 Answers2

7

I am using CKQueryOperation. I found that once I added the following line to my code that downloading CKAssets sped up by about a factor of 5-10x.

    queryOperation.qualityOfService = .UserInteractive

Here is my full code:

func getReportPhotos(report:Report, completionHandler: (report:Report?, error:NSError?) -> ()) {
    let photo : Photo = report.photos![0] as! Photo
    let predicate : NSPredicate = NSPredicate(format: "recordID = %@", CKRecordID(recordName: photo.identifier!))
    let query : CKQuery = CKQuery(recordType: "Photo", predicate: predicate)
    let queryOperation : CKQueryOperation = CKQueryOperation()
    queryOperation.query = query
    queryOperation.resultsLimit = numberOfReportsPerQuery        
    queryOperation.qualityOfService = .UserInteractive
    queryOperation.recordFetchedBlock = { record in
        photo.date = record.objectForKey("date") as? NSDate
        photo.fileType = record.objectForKey("fileType") as? String
        let asset : CKAsset? = record.objectForKey("image") as? CKAsset
        if asset != nil {
            let photoData : NSData? = NSData(contentsOfURL:asset!.fileURL)
            let photo : Photo = report.photos![0] as! Photo
            photo.image = UIImage(data:photoData!)
        }

    }
    queryOperation.queryCompletionBlock = { queryCursor, error in
        dispatch_async(dispatch_get_main_queue(), {
            completionHandler(report: report, error: error)
        })
    }
    publicDatabase?.addOperation(queryOperation)
}
Jeff Zacharias
  • 381
  • 3
  • 12
  • After making that change you're now able to download an 80k image in around 0.1-0.5 seconds? How did you structure your data inside of the CK dashboard? It improved my speeds by about double. Went from roughly 2.X seconds down to 1.0-1.7 seconds. – BlueBear May 01 '16 at 20:34
  • I download 25 records each with an average image asset of 90K in less than 2 seconds, sometimes less than 1 second, so that's less than 0.08 seconds per record. I'm on a good WiFi connection, but I tested when I was out today with LTE and results were similar. Each record has 14 string fields, a location field, and date field, and an asset field that holds the image data. I'm still in the development mode and all search, query, and sort are checked. – Jeff Zacharias May 01 '16 at 21:27
  • Those are the types of speeds that I'm looking for. So your data setup has a single record with all of the different fields on it? My setup has a record with all String data along with a reference to a "Photo" record. Then when I'm wanting to fetch the image I can directly fetch only that. – BlueBear May 02 '16 at 19:32
  • I first put the photo asset in the same record. It was too slow so I moved it to a separate record and lazily loaded it. That was still slow. When I changed the quality of service the performance was good enough so I put the photo asset back in the same record. I can always use the desiredKeys to fetch everything but the image asset, or just the image asset alone if I want to. – Jeff Zacharias May 04 '16 at 02:11
3

There seems to be something slowing down your main thread which introduces a delay in executing the capture block of your dispatch_async call. Is it possible that your code calls this record fetching function multiple times in parallel ? This would cause the NSData(contentsOfURL: asset.fileURL) processing to hog the main thread and introduce cumulative delays.

In any case, if only as a good practice, loading the image with NSData should be performed in the background and not on the main thread.

Alain T.
  • 40,517
  • 4
  • 31
  • 51
  • I am currently running that code in `cellForItemAtIndexPath:`, which means that it is getting run multiple times. I can't imagine that use case would slow things down as much as it is, especially since I can do the same thing with NSURLSession with exceptional results. Is this just not how CloudKit is supposed to be used? I broke off the images from my primary data so that I can load the images on demand rather than all up front. – BlueBear Mar 07 '16 at 22:19
  • cellForItemAtIndexPath can be called rather unpredictable times and will always be executed on the main thread. It is not a very safe to perform asynchronous data loading operation. I'm even surprised that you're not getting images mixed up in your cells because the cell object that is captured in your dispatch_async call runs the risk of being reused for another item during the delay between fetch and display. Anyhow, it seems like you found a way to get it to work so I guess you won't need more info on this question. – Alain T. Mar 07 '16 at 23:28