Sponsored Tutorial: Improving Your App’s Performance with Pulse.io
Learn how you can use Pulse.io to notify you of low frame rates, app stalls and more. Let us walk you through all the features in this Pulse.io tutorial. By Adam Eberbach.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Sponsored Tutorial: Improving Your App’s Performance with Pulse.io
35 mins
- Getting Started
- Integrating Pulse.io Into Your App
- Generating Initial Test Data
- Instrumenting Custom Classes and Methods
- Customizing Action Names
- Analyzing Data
- Evaluating Spinner Times
- Evaluating Frame Rate
- Network Details
- Memory
- Fixing the Issues
- Correcting the Image Loading
- Fixing Routing Performance
- Fixing the Frame Rate
- Verifying Your Improvements
- Verifying Network and Memory
- Verifying Spinners
- Verifying Frame Rate
- Where To Go From Here?
Memory
The final button in the dashboard is Memory. Click it and you’ll see your app’s memory usage as demonstrated below:
That feels like a lot of memory for a simple app like yours. iOS is likely to terminate apps using large chunks of memory when resources get low, and making a user restart your app every time they want to use it won’t be a pleasant user experience.
This could be due to the loading of the images as you noted before, so draw a few asterisks next to the item on your fix list that addresses the loading of large images.
Fixing the Issues
Now that you’ve drilled down through the pertinent data points of your app, it’s time to address the issues exposed by Pulse.io.
A likely place to start is the image fetching strategy. Right now the app fetches a thumbnail image for each sight discovered and then pre-fetches the large image in case the user taps on the pin to view it. But not every user will tap on every pin.
What if you could defer the large image fetch until it is actually needed; that is, when the user taps the pin?
Correcting the Image Loading
Find the implementation of thumbnailImage
in Sight.m. You’ll see that you make two network requests: one for the thumbnail and one for the large image.
Replace the current implementation of thumbnailImage
with the following:
- (UIImage *)thumbnailImage {
if (_thumbnail == nil) {
NSString *urlString = [NSString stringWithFormat:@"%@_s.jpg", _baseURLString];
AFImageRequestOperation* operation
= [AFImageRequestOperation
imageRequestOperationWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:urlString]] imageProcessingBlock:nil
success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
self.thumbnail = image;
[_delegate sightDidUpdateAvailableThumbnail:self];
}
failure:nil];
[operation start];
}
return _thumbnail;
}
This looks very much like the original method – it contains an AFImageRequestOperation
whose success block notifies the delegate MapSightsViewController
that the thumbnail is available.
You’ve removed the code that kicks off the full image download. So next, you’ll need to load the large image only when the user drills down into the annotation. Find initiateImageDisplay
and replace it with the following code:
- (void)initiateImageDisplay {
if (_fullImage) {
[_delegate sightDisplayImage:self];
} else {
[_delegate sightBeginningLargeImageDownload];
NSString *urlString = [NSString stringWithFormat:@"%@_b.jpg", _baseURLString];
AFImageRequestOperation* operation = [AFImageRequestOperation
imageRequestOperationWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:urlString]]
imageProcessingBlock:nil success:^(NSURLRequest *request, NSHTTPURLResponse *response, UIImage *image) {
[_delegate sightsDownloadEnded];
self.fullImage = image;
[_delegate sightDisplayImage:self];
} failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error) {
[_delegate sightsDownloadEnded];
}];
[operation start];
return;
}
}
This loads the image the first time it’s requested and caches it for future requests. That should reduce the number of network requests for images — with the added bonus of reduced memory usage. Correcting both of those issues should help reduce the amount of spinner time users need to suffer through! :]
Since you’re already fixing network related items, you may as well strip the sensitive bits from the http requests while you’re at it.
Fortunately this is a simple one-line fix. Add the following line after the call to monitor:
in main.m:
[PulseSDK setURLStrippingEnabled:true];
This prevents all query parameters from being logged in the Network dashboard, which helps keep your Flickr API keys secret.
Fixing Routing Performance
The next thing on your list of things to fix is the algorithm that calculates the route between the various sights. It’s easy to underestimate the complexity of finding the shortest route between an arbitrary number of points. In fact, it is one of the hardest problems encountered in computer science!
Note: Better known as the Travelling Salesman Problem, this type of algorithm is a great example of the class of problems known as NP-hard. In fact, if you find a fast, general solution to this problem while working through this tutorial, there may be a million dollar prize awaiting you!
Note: Better known as the Travelling Salesman Problem, this type of algorithm is a great example of the class of problems known as NP-hard. In fact, if you find a fast, general solution to this problem while working through this tutorial, there may be a million dollar prize awaiting you!
This app uses a brute force method of finding the shortest route by calculating the complete route many times and saving the shortest one. If you think about it, though, there’s no real requirement to show the shortest route through all points — you can just display any route and let the user vary the route if they feel like it. The time spent waiting for the optimal route just isn’t worth it in this case.
Take a quick look at orderLocationsInRange:byDistanceFromLocation:
in PathRouter.m; you can see that it currently orders the discovered paths in a random fashion. A reasonably good route can be found by starting at one point and visiting the next closest point, repeating until all points are visited.
It’s quite unlikely that this is going to be even close to the optimal route, but the potential gains in performance make this approach your best option.
Inside the else
clause in this method, replace the call to sortedArrayUsingComparator:
(including the block passed to it) with the following code:
NSArray *sortedRemainingLocations = [[self.workingCopy subarrayWithRange:range] sortedArrayUsingComparator:^(id location1, id location2) {
CLLocationDistance distance1 = [location1 distanceFromLocation:currentLocation];
CLLocationDistance distance2 = [location2 distanceFromLocation:currentLocation];
if (distance1 > distance2) {
return NSOrderedDescending;
} else if (distance2 < distance1) {
return NSOrderedAscending;
} else {
return NSOrderedSame;
}
}];
Now find orderPathPoints:
and take a look at the for
loop in there. It currently tries 1000 iterations to find the best route.
But this new algorithm only needs one iteration, because it finds a decent route straight away. 1000 iterations down to 1 - nice one! :]
Find the following lines and remove them:
for (int i = 0; i < 1000; i++) {
if ([locations count] == 0) continue;
Then find the corresponding closing brace and remove it also. (The brace to remove is just above the line that reads // calculation of the path to all the sights, without blocking the main (UI) thread
).
This change cuts the path algorithm down to one iteration and should reduce spinner time even further.
That takes care of the excess spinner time. Next up are those pesky frame rate issues uncovered by Pulse.io.