A hugely popular app is a double-edged sword. On the one hand you have lots of users. On the other hand, pretty much every edge case will be hit by somebody – which often reveals pesky performance problems or bugs.
Analytics are a good way to keep tabs on users, but how can you track more technical data such as app performance and network lag?
Well, there is now an easy way to do this – thanks to a relatively new app performance monitoring service called Pulse.io.
With Pulse.io, you’ll know if your users are experiencing long waits, poor frame rates, memory warnings or similar problems — and you’ll be able to drill down to find out which parts of your code are responsible for these issues.
In this tutorial, you’ll take a look at an example project called Tourist Helper that has some performance issues, and Pulse.io to detect and solve them.
Getting Started
Download the sample project, unzip the contents and open the project in Xcode.
You’ll need a Flickr account and Flickr API key to continue. Don’t worry — both the account and the API key are free.
Log in to your Flickr account, or sign up for a new account, then visit https://www.flickr.com/services/apps/create/noncommercial/ to register for an API key.
Once that’s done you’ll receive both the API key and a secret key as hexadecimal strings. Head back to Xcode and modify the kFlickrGeoPhotoAPIKey
and kFlickrGeoPhotoSecret
constants in FlickrServices.h to match your key and secret strings.
#define kFlickrGeoPhotoAPIKey (@"Your Flickr API Key") #define kFlickrGeoPhotoSecret (@"Your Flickr API Secret") |
That’s all you need for now to work with Flickr — on to integrating Pulse.io into your app.
Integrating Pulse.io Into Your App
Head to pulse.io in your browser. Sign up for an account if you don’t already have one; the process is simple as shown by the streamlined signup form below:
As soon as you have filled out and submitted the form your account will be ready to use. You’re now ready to add your app to Pulse.io. After signing up and logging in, click on the New Application button.
Enter TouristHelper for the name of your app, ensure iOS is selected and click Create Application as shown below:
Next you’ll see a page of instructions describing how to install the Pulse.io SDK. Keep this page open in a tab because it contains a link to the SDK and your app’s Pulse.io token.
Adding the Pulse.io framework to your app is quite straightforward, especially if you’ve worked with third-party frameworks before.
Download the latest version of the SDK (there’s a link at the top of the page you landed on after creating your app), unzip and open the folder that unzips. Then find the PulseSDK.framework file and drag it into your Xcode project. Ensure it’s been added to the TouristHelper target and copied into the destination group as shown below:
Keep your project tidy by putting your Pulse.io framework in the Frameworks group of your Xcode project like so:
Pulse.io has a few dependencies of its own. Select the project in the top left of the left-hand pane, select the Build Phases tab and expand Link Binary With Libraries. You should see something similar to the following screenshot:
Pulse.io requires the following frameworks:
- CFNetwork.framework
- libz.dylib
- SystemConfiguration.framework
- CoreTelephony.framework
Click the “+” icon and begin typing the name of each library. Select each of the above frameworks as you see it appear in the list. If any of these libraries have already been added to the project don’t worry about it for the purposes of this tutorial.
Once you’ve finished adding the appropriate libraries, your project should look like this:
Open main.m and add the following header import:
#import <PulseSDK/PulseSDK.h> |
Next, add the following line directly before return UIApplicationMain()
inside main
:
[PulseSDK monitor:@"Your Pulse.io API key goes here"]; |
You’ll need to insert your own Pulse.io API key in this space; you can find it on the Pulse.io instructions page you saw earlier, or alternatively from your list of Pulse.io applications that’s displayed when you’re signed in.
Note: If you’ve used other third-party frameworks such as Google Analytics, Crashlytics or TestFlight, you might have expected Pulse.io to start up in you app delegate’s application:didFinishLaunchingWithOptions:
. Pulse.io hooks directly into various classes using deep Objective-C runtime tools. Therefore you need to initialize the SDK very early in your app’s startup, before even UIKit has started up any part of your UI.
That’s all you need to get started!
Generating Initial Test Data
Make sure you aren’t running in a 64-bit environment, such as the 64-bit simulator, iPhone 5S or iPad Air. Otherwise you’ll see a message warning you that Pulse.io doesn’t yet run in 64-bit environments. For now, select a different target for building your app.
Build and run your app and watch it for a while as it runs. Notice that it doesn’t do much yet. In fact, the observant reader will notice there appears to be an error logged to the console. More on that shortly.
The app, when working, searches Flickr for some interesting images with a default search tag of “beer” around your location and places pins on the map as photos are found. The app then calculates the route between these places and draws it on the map. Finally, the app fetches thumbnails and large images using the image’s Flickr URL and the instant tour is ready for use.
Head back to the Pulse.io page that you landed on after creating the app (you kept it open like I said, right?!); the message at the bottom of the page will eventually update to let you know the app has successfully contacted Pulse.io.
Excellent! You’re up and running and collecting data. As the message in the app states, it could take a little while for results to appear in the dashboard. While you’re waiting for that you can generate some interesting data by exploring images at other locations around the world.
If you haven’t discovered it yet, find the symbol in the Debugger control bar that looks just like the location symbol in iOS maps. Simply tap it and Xcode presents you with a list of simulated locations around the world; you can even use GPX files to create custom simulated locations.
Change the simulated location a few times while the app is running; you’ll notice that no pictures of any interesting sights appear. Hmm, that’s curious.
It turns out that Flickr doesn’t like connections over regular http and wants everything to be https. The good news is that you’ve just logged a few network errors to analyze later! :]
Open FlickrServices.h and change the kFlickrBaseURL
constant as shown below:
#define kFlickrBaseURL (@"https://api.flickr.com") |
That should do it!
Build and run your app and set a simulated location in Xcode. Here’s an example of what you might see if you start near Apple headquarters in Cupertino:
It’s likely that your app runs smoothly in the simulator on your nice, fast Mac. However, the story might be quite different on a slower device. Will your app create a memory-packed day for a tourist, or a memory-packed app for iOS to kill after too many allocations?
You have just a few more customizations to make before checking out the full story in your Pulse.io dashboard.
Instrumenting Custom Classes and Methods
By default, Pulse.io automatically instruments the key parts of your app to check for things such as excessive wait time and network access.
If you have custom classes that perform a lot of work such as the route calculator object in your app, you can add instrumentation to them using instrumentClass:
. As well, you can add a specific selector using instrumentClass:selector:
rather than instrumenting every message from the class.
Add the following import to the top of main.m:
#import "PathRouter.h" |
Next, add the following line immediately after the PulseSDK monitor
line you added in the previous section:
[PulseSDK instrumentClass:[PathRouter class] selector:@selector(orderPathPoints:)]; |
Pulse.io will now trace every time a PathRouter
instance receives the orderPathPoints:
message. Adding instrumentation to a method adds around 200-1000ns of execution time to that method call. This isn’t a real big hit — unless you call that method in a tight loop. Plan your instrumentation accordingly.
Take a look at PathRouter.m; you’ll see that orderPathPoints:
performs the heavy lifting of finding a reasonable path between points. This is an algorithmically difficult problem, and probably a good spot to add some instrumentation.
Customizing Action Names
The actions reported by Pulse.io are grouped by the user action that triggered them. For example, if the user pans a map, you’ll see methods that result from that pan operation descending from a UIPanGestureRecognizer
. It would be a great idea to attach more meaningful names to some operations to make them easier to find and understand.
Open MapSightsViewController.m and add the following Pulse.io header import to the top of the file:
#import <PulseSDK/PulseSDK.h> |
Next add the following line to the beginning of mapView:viewForAnnotation:
:
[PulseSDK nameUserAction:@"Providing Annotation View"]; |
Now when you look at the results on the Pulse.io dashboard, you’ll see this method is reported using the friendly name instead.
Analyzing Data
Build and run the app again to generate some more data to analyze. After a little time has passed, say a minute or so, check the Pulse.io dashboard statistics for your application to see what they show. You should see something similar to that below. If you don’t, wait a little while longer and try again. It can take up to an hour for data to trickle in. That’s not a problem in practice though, because you will want to collect a lot of data and then work on it when it’s all in.
Things look a little sparse with just one user generating data with one app, but you can imagine the distributions would smooth out with many users contributing more data. The overview presented above serves as a signpost to the areas you should examine in more detail.
At the top of the browser window there is a row of buttons which you can use to drill down into each metric, as shown below:
The first item to drill down into is the total spinner time of your app.
Evaluating Spinner Times
Click on the UI Performance\Spinner Time button on the web site for a more detailed view of your data. Pulse.io hooks into UIActivityIndicator
to determine how much time your users spend looking at that spinning animation while they wait, as shown below:
Er, that’s pretty ugly! The app displays a spinner for an average of nearly two seconds each time it appears. There’s no way a user would find that acceptable. Click on the Providing Annotation View user action to see more detail as illustrated below:
Those large image requests are taking a lot of time. Note that down as one problem to fix once you’re done reviewing the data on other problem areas.
Note: If you display a custom spinner or other view while the user is waiting, you can use startSpinner:trackVisibility:
and stopSpinner:
to track your spinner events as detailed in the Tracking a Custom Spinner Pulse.io API documentation.
Evaluating Frame Rate
Next, click on the UI Performance\Frame Rate button to show the detail view like so:
This view shows that there was a total of six seconds of low frame rate when displaying annotations descending from the method you annotated with the name Providing Annotation View. That’s beyond unacceptable for your app!
Note this issue on your fix list as well; you’ll have to revisit how you’re handling annotations in order to fix this.
Network Details
Next, click on the Network\Speed button to drill down into the data. Then select Network\Errors. Generally speed and error count are the two things to investigate when it comes to networking.
The image below shows an example of Pulse.io catching some network errors:
The normal HTTP status response is 200, meaning OK. However a couple of times the app received a 403 code meaning Forbidden. Recently Flickr mandated use of https and started denying plain http requests, which you made earlier in your app before you modified it to use https exclusively.
It’s good to know that Pulse.io will report any client-side, server-side, or network access issues your app will encounter.
Here’s a sample view of Pulse.io’s view of your networking speed:
Pulse.io reports speed as time per request and breaks the results down into time waiting and time receiving. This is useful data; if you find your user base growing and your server being overwhelmed by traffic you’ll see the wait time slowly creep up as time goes on.
Consider the sheer number of requests shown here from only one user with one app. It looks like there are an excessive number of requests for the limited amount of time you’ve used your app. Making excessive requests eats up bandwidth and keeping the network active wastes battery. Note this as another item to fix.
Below the chart you can see a list titled Aggregate URL Response Times. Select flickr.com/*
in this list to see some of the RESTful queries your app made to Flickr.
Take a closer look at the results: there’s some sensitive information there, including your app’s Flickr API key and secret! That should be stripped out of your request before the wrong people see it.
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!
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.
Fixing the Frame Rate
iOS tries to render a frame once every sixtieth of a second, and your apps should aim for that same performance benchmark. If the code execution to prepare a frame exceeds ~1/60 second (less the actual time to display the frame), then you’ll end up with a reduced frame rate.
If you’re only slowed down by one or two frames per second most users won’t even notice. However, when your frame rate drops to 20 frames/second you can bet most users will find it highly annoying. Using Pulse.io to track your frame rate keeps you ahead of your users and lets you detect slow frame rates before they are noticed by too many users.
One of the changes you made to the app was adding the label Providing Annotation View to a user action. The dashboard showed that slow frame rates were taking place in this specific user action. Pulse.io tells you exactly what your users are experiencing so you don’t have to guess whether or not smooth scrolling on older devices is something you need to handle in your app design.
Map views like the one in this app require multiple annotation views to work together to provide smooth scrolling performance. Map Kit includes a reuse API since reusing an annotation is much faster than allocating a new one every time. Your app isn’t reusing annotation views at the moment, which might explain at least some of the performance issues.
Open MapSightsViewController.m and find mapView:viewForAnnotation:
. Find the following two lines that allocate new annotations:
sightAnnotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:kSightAnnotationIdentifier]; sightAnnotationView.canShowCallout = YES; |
Replace the above lines with the following implementation:
sightAnnotationView = [mapview dequeueReusableAnnotationViewWithIdentifier:kSightAnnotationIdentifier]; if (sightAnnotationView == nil) { sightAnnotationView = [[MKPinAnnotationView alloc] initWithAnnotation:annotation reuseIdentifier:kSightAnnotationIdentifier]; sightAnnotationView.canShowCallout = YES; } |
This mechanism is similar to the way table views or collection view cells get reused and should be somewhat familiar. The new implementation attempts to dequeue an existing annotation and only creates a new annotation if it fails to get one from the map view.
While this change has the least dramatic effect of all the changes made so far, you should always reuse objects whenever UIKit offers you the chance.
Now that you’ve completed all of the items on your fix list, you need to generate some more analytics in Pulse.io to see how your app performance has improved.
Build and run the app; pick several simulated locations and scroll around the map as an average user would. The question is — will the Pulse.io results show some improvement? Or has all your hard work been for naught?
Verifying Your Improvements
Here’s a look at some sample data collected after making the code improvements and playing with the app for a bit:
The dashboard shows that the results above arrived a few hours after the first batch of results. Step One in any performance improvement process is to be sure you’re looking at the right data! :]
At first glance things look like they might have improved, but you can’t really tell until you dig down into the nitty gritty details.
Verifying Network and Memory
Take a close look at the number of network requests made now that you’ve reduced the excessive image loading:
That’s a step in the right direction — you used the app for roughly the same amount of time and in approximately the same way that you did before, but the app made less than half of the network requests than before.
Drill down into Aggregate URL Response Times again and examine the queries made to flickr.com/*
– are those URLs still exposing too much information?
All URLs logged now have the query part of the request stripped thanks to setURLStrippingEnabled:
. You could easily share these results without exposing any details of your web API or compromising other secrets. And even if you didn’t have any secrets to hide, at least URLs in this format are a heck of a lot easier to read! :]
Verifying Spinners
Spinners were particularly worrying — no user wants to waste five seconds of their life staring at a spinner. What does Pulse.io say about the end result of your spinner improvements?
The total spinner time is now 14 seconds, and average spinner time has dropped somewhat but not as dramatically as you might first think. Does that mean your improvements had no effect on average spinner time? How should you interpret these results?
You made two huge reductions to spinner time in the code: first, the new version of the app is making requests for thumbnail (small) images at about the same rate as the first version, but deferring the fetch of the large images to when the user taps on the thumbnail.
Second, you switched the route calculation to a much more sensible next-closest algorithm and only perform it once per set of points.
So the cost of bringing up a spinner and performing an operation has dropped, on average, as you’re requesting smaller pieces of data, but the total number of spinners displayed has dropped dramatically as you’re making far fewer image requests.
What was that previous result again?
Yep – that’s a huge improvement; the app is now much more usable.
Although you’ve achieved some success with your spinner time, you really should aim to avoid any spinner time at all.
One possible way to cut down on spinner use in your app is to make the initial image request to Flickr and display the points on the map immediately. The route calculation would be performed in the background and leave the UI thread responsive so the user can interact with the map. You would then display the calculated route once the algorithm was done.
This would be a great change to attempt on your own to see the effect it has on spinner use. If you do choose to try this, please share your before and after performance results in the forum discussion below!
All that’s left to check on is the frame rate issue that you solved by re-using annotations. What did Pulse.io detect as your improved frame rate?
Verifying Frame Rate
The small change you made to reuse annotation views has resulted in some performance gains, as shown below:
The recorded low frame rate time descending from Providing Annotation Views has dropped to around four seconds. That’s not a bad improvement in the context of this tutorial, but you should be shooting for no recorded instances at all. On modern hardware, with some code tweaks appropriate to your app, this goal is well within your reach.
Where To Go From Here?
Here’s the final version of the sample project that implements the changes discussed in this tutorial. If you use this version of the project, remember to set your own Pulse.io key, Flickr key and secret!
By adding Pulse.io reporting to this sample app, you’ve learned how an app that seems to work well enough on your simulator and test devices can fail to impress your users; in fact, it can lead to a really poor user experience. With Pulse.io in your toolbox, you can gather information about user experience issues on many fronts:
- Excessive memory usage
- Spinner time
- Low frame rates
- Network issues
It’s incredibly important to gather metrics and identify fixes for you app before you start seeing these issues mentioned in the reviews of your app. Additionally, you won’t waste time improving areas that, in actual fact, aren’t causing any problems for your users.
Pulse.io is undergoing rapid development and improvement. While there’s an impressive level of detail in the product already, the team is anything but idle. The Pulse.io team recently introduced the Weekly Performance Report as shown below:
This shows you the changes in app usage from week to week. There isn’t much data here with just one user (i.e. the author) and an app still in the development stage, but you can see how useful this would be when your users number in the thousands.
Support for Swift is on the horizon, so keep an eye out for updated versions of the Pulse.io SDK and instructions on integrating Pulse.io into your Swift projects soon.
Have you used Pulse.io in your own apps and found any interesting performance issues or tuning tips? Share your experiences with us in the comments below!
Sponsored Tutorial: Improving Your App’s Performance with Pulse.io Tutorial is a post from: Ray Wenderlich
The post Sponsored Tutorial: Improving Your App’s Performance with Pulse.io Tutorial appeared first on Ray Wenderlich.