Instruments is the front-end for a suite of analysis, profiling, and faultfinding tools, much as Xcode is the front-end for your editors, compilers, and debuggers. You can use the Instruments app by itself, but Xcode makes it soeasy to use directly from your project, I can’t imagine why you’d want to.
For this chapter, start with the version of MyStuff after you added document support and fixed the image rotation problem (second exercise in Chapter 19). You’ll find this project in the Learn iOS Development Projects ➤ Ch 19➤ MyStuff E2 folder.
Instruments is normally launched when you profile your app. This is determined by a set of schemes defined in your project. When you run your app (using the Run button or menu command), the
Run scheme is used. When you profile your app, the Profile scheme is used. To see your project’s schemes, choose Edit Scheme... from either the Scheme control in the toolbar or by choosing Product ➤ Scheme ➤ EditSchemes... from the menu. This will present the scheme editor, as shown in Figure 23-1.
Figure 23-1. Project scheme editor
There’s nothing here to change, I just wanted you to know where it was. The default configuration for the Profile scheme is to launch Instruments and prompt you (Ask on Launch) for the type of analysis to perform, which is perfect to get started. So what are you waiting for? Close the scheme editor and profile your app by choosing Product ➤ Profile (Command+I) from the menu or from the Run button’s drop-down menu. Xcode will build your project and launch Instruments, as shown in Figure 23-2.
Figure 23-2. Instruments template picker
Each Instruments template describes a different kind of analysis. They’re organized into groups for
convenience. For your first foray into performance analysis, choose the iOS ➤ CPU group and select the TimeProfiler. This is the most commonly used instrument for measuring code performance. It works by sampling
your code as it executes, thousands of times a second. At each sample, it records the functions and methods
that are executing at that instant. By aggregating hundreds of thousands of these samples, it canpaint
a remarkably accurate picture of where your app is spending its time. These are the methods that you want tomake faster.
Click the Profile button and your app begins executing, under the watchful eye of the Time Profiler instrument, as shown in Figure 23-3. You want to find out what’s taking so long when a new image is added, so immediatelystarted adding new items and selecting images for those items from your camera roll. Repeat this several times. You should notice a sizable spike in CPU activity as you tap on an image in the photo picker, creating a series of“humps” in the CPU usage graph.
Figure 23-3. Initial sample of MyStuff
After adding a few new images, press the home button to push the app into the background, wait a few seconds, and click the Record button in the Instruments toolbar. This stops recording and terminates the app.
Congratulations, you have a baseline! You’ve captured the code activity associated with the performance
problem you’re trying to solve. Now it’s time to mine this mountain of data for some answers.
Finding the Hot Spots
Begin by isolating just the performance information you’re interested in studying. Using the mouse in the Instruments timeline (at the top of the window), drag the sample cursor (hollow triangle with dotted line) just to the left ofone of those “humps” in the graph recorded just as you tapped an image in the photo library picker. To make this easier, drag the Change Track Scale control, shown at the middle-left of the window in Figure 23-3, to zoom in on the samples you’re interested in. Click on the left mask button in the Inspection Range control in the toolbar. Drag the cursor just to the right of the hump and click on the
right mask button. Now all of the data you’re goingto work with in the lower panes will contain only samples from the highlighted range (lower pane in Figure 23-3), because this is the code you’re interested in profiling.
Now you want to find the hot spots in your code. This is optimization slang for the code that’s eating up all of your CPU time. Less colorfully, it’s the regions of code that accumulated the most samples. With the Time Profilerinstrument selected in the trace document, locate the Invert Call Tree and
Show Obj-C Only options in the Time Profiler sidebar. Check both of these, as shown in Figure 23-3.
The Show Obj-C Only option filters out all of the C functions from the analysis. I recommend this option for Objective-C programmers, particularly to get started.
The Invert Call Tree option inverts the Call Tree you see to the right. When not checked, the Call Tree summarizes the calling hierarchy of your entire app. Each line in the table shows a method or function and how much timeyour app spent in it. Expand a line and you see the methods it invoked, and the breakdown of time spent in each sub-method. Expand one of those, and you get the idea.
The Call Tree is normally sorted by “heaviest” method. That is, the first line of each group will be the method that used the most CPU time. To dig down into the pile and find the heaviest code path in the tree, keep expandingthe first line of each group.
With the Invert Call Tree option checked, the Call Tree is turned inside out. Now, the method listed at the top is the heaviest leaf method in your app. Expand it, and it lists what methods called it—instead of what methods itcalled. In Figure 23-3, you see that 32.3% of your app’s response time was spent in the -drawInRect:blendMode:alpha: method.
Working backwards, by expanding the lines, and you find that it’s the -imagePickerController:
didFinishPickingMediaWithInfo: method that’s using it (to resize and crop the chosen image).
Looking a little further down the list, the next heaviest method is the -setImage:existingKey: method. This is the method you added to store the new image in the document. When the user taps on an image in the picker, 23.7% ofthe time is spent storing it in the new document. If you expand its
caller you see that it, too, is being called from the -imagePickerController:didFinishPickingMediaW ithInfo: method.
This corroborates your suspicion that the image conversion and document storage code you added in Chapter 19 are slowing down the interface when the user picks an image. Let’s dig into the
-imagePickerController:didFinishPickingMediaWithInfo: method and find out what’s going on. Turn the Invert Call Tree option off again, and begin expanding the heaviest methods in the call tree until you uncover the -imagePickerController:didFinishPickingMediaWithInfo: method, as shown in Figure 23-4.
Figure 23-4. Details of -imagePickerController:didFinishPickingMediaWithInfo: execution time
In this set of samples, it shows that the image picker delegate method spent close to a second (860 milliseconds) responding to the tap. Expanding that line, you see the amount of time spent in each message the pickermethod sent. While this is accurate information, it’s sometimes a little hard to translate into what your code is doing. Instruments will help you here too.
Double click the -imagePickerController:didFinishPickingMediaWithInfo: method line and Time Profiler will switch to its source view, as shown in Figure 23-5.
Figure 23-5. Time Profiler source view
Instruments overlays the time spent in each line of your source code, clearly identifying the hot spots in your code. This view is particularly good at identifying loops that are taking a long time to execute.
What have you learned from Figure 23-5? You’ve learned that when the user tapped on an image in the photo picker, 37.3% of the time was spent cropping image, 26.9% storing it in the document, and 0.3% dismissing thephoto picker view controller. In a big surprise, 34.8% of the time was spent notifying any observers that the data model changed. Huh?
The Hubris of Experience
Yes, it caught me off guard too. When I was originally planning the projects for these chapters, I knew that the image compression and document storage code in Chapter 19 would add a lot of overhead to the MyStuff app. I’veworked with the LZ77 compression algorithm used by the PNG format, and
I know how CPU intensive it can be. I planned to start this chapter by showing you how much time the
image conversion and compression routines were taking up when the user chose an image, and what to do about it. But when I actually ran Instruments on MyStuff, what did I find? I find that almost a third of the
delay is tangled up in the data model notification. Something that I guessed wouldn’t have even showed up on the radar.
And this is why you can’t assume you know where your performance problems are. Even with years of programming experience, you’re going to guess wrong. I did. You must start by measuring your real-world performance. Ifyou don’t, you’ll be tilting at windmills.
So what is going on? Digging into the -postDidChangeNotification method, shown in Figure 23-6,
it turns out that the MyWhatsit change notification was observed by the view controller, and that triggered a redraw of the table view(-reloadRowsAtIndexPaths:withRowAnimation:). This, apparently, is a much more expensive operation than I thought.
Figure 23-6. Time sample details of -postDidChangeNotification
Fine, I guessed wrong. Can we get back to improving MyStuff now?
Picking the Low Hanging Fruit
With all of this knowledge, you now need to formulate an optimization plan. You’ve identified three hot spots in your photo picker handling code:
n Data model notification
n Image conversion
n Image compression and document storage
The trick is to pick the “low hanging fruit.” Find the code that is hurting performance the most and is the easiest to improve.
Graphics operations are data and CPU intensive, by their very nature, and the iOS graphics library
is already heavily optimized. It’s unlikely you’re going to get much improvement by rewriting the code that crops the image.
That leaves the data model notification and document storage as candidates for improvement. And in a nice surprise, the heaviest is also the easiest to solve.
Deferring Notifications
In iOS, notifications are delivered immediately. When you post a notification, all of the observers are sent their notification messages, and control returns to your method only after they’re all done. In this instance, the act ofnotifying everyone that the data model object changed triggered a series of expensive and time-consuming operations.
You can’t avoid this work—the data model has to send its notifications, and the observers have to
be notified—but you can procrastinate. It’s not critical to the code that picks a new image that these notifications are delivered before that code is finished. You can take advantage of this to defer the notification. The notificationwill still happen, just not at the moment the user taps an image. This will, in turn, improve the response time to the tap event.
The notification center has an oft-neglected relative called the notification queue (NSNotificationQueue). A notification queue posts notifications to a notification center on your behalf, but it provides two essential services. It doesn’tpost them immediately, so any code triggered by those notifications won’t execute until later. Secondly, it will combine duplicate notifications and only post one; a feature called coalescing. Some notifications, like data modelchanges, can occur many times, but they all mean the same thing. Rather than repeatedly send the same message to all observers, the notification queue combines them into a single notification, which is far more efficient.
Open your MyStuff project. Select the MyWhatsit.m implementation file in the project navigator and locate the -postDidChangeNotification method. Rewrite the method like this (new code in bold):
- (void)postDidChangeNotification
{
NSNotification *noti;
noti = [NSNotification notificationWithName:kWhatsitDidChangeNotification object:self];
[[NSNotificationQueue defaultQueue] enqueueNotification:noti postingStyle:NSPostWhenIdle];
}
To use a notification queue, you must create a notification object. (When you use the notification center’s -postNotification... messages, the NSNotification object is created for you.) Once you create the notification, push it ontothe queue.
Each notification queue is attached to a notification center. iOS, conveniently, creates a notification queue attached to the default notification center for you. Anything you add to the default queue will (eventually) be posted tothe default notification center.
When your modified MyWhatsit object receives a -postDidChangeNotification message, it doesn’t notify its observers immediately.
This method has become an asynchronous method. It merely queues the notification to bedelivered in the future.
So when does it get delivered? You have are a few choices, controlled by the postingStyle
parameter. The two most useful are NSPostWhenIdle and NSPostASAP.
n NSPostASAP will post the notification as soon as the current code finishes executing and control returns to the event loop. Use this to post notifications that must be sent before the next event isdispatched.
n NSPostWhenIdle saves notifications until the event loop is idle. It waits until all pending events (touch events, timers, deferred messages, user interface updates) have been dispatched. Before the event loopgoes to sleep again, any queued notifications are sent. Use NSPostWhenIdle when you want notifications to be handled immediately after you’ve responded to all user and timer events.
Now the work of updating the data model, refreshing the table view, and notifying the document of changes is still handled, but it’s handled after you’ve responded to the tap event, dismissed the view controller, and updatedeverything on the screen.
Once More into The Breach
Close and save your Instruments trace document, as shown in Figure 23-7.
Give it a descriptive name, like Baseline Profile. Close the profile window.
Figure 23-7. Saving your baseline trace document
Return to Xcode and profile your app again. Perform the same steps you did before: choose the Time
Profiler template, add images to your app, stop it, narrow the inspector range to the code that picks a new image, andreview the call tree, as shown in Figure 23-8.
Figure 23-8. Profiling revised code
This time, it only took half a second (514 milliseconds) to execute the code in the -imagePickerController: didFinishPickingMediaWithInfo: method. That’s an improvement of over 67%. Open the baseline trace document you saved andcompare two, similar, execution sections. In the modified code,
the-postDidChangeNotification method doesn’t even show up in the sample, it happens so fast!
If you dig around the call tree, you’ll eventually find where the data model notification is posted, and all of the busy work associated with it still gets done. But it’s not happening when the user taps the interface, so they don’tsee it.
Your app should feel much more responsive now. The key to creating responsive apps is to keep your event loop running and ready to react immediately to the user or other events. That often means breaking up
the work so itdoesn’t happen all at once. Notification queues are a great way to defer tasks that you don’t want interfering with your interface, and can be safely performed later.
This is great progress, but it still hasn’t gotten the response time below ½ second. The next “heavy” on the
list is the code that adds the image to the document. You’ll learn a different way of deferring that work in the nextchapter.
Save your second trace document and close it. Now you’re ready to look at a completely different aspect
of your app’s performance.
Precious Memory
The amount of RAM available to mobile apps is minuscule when compared to most computing
environments. Yet, users expect mobile apps to accomplish many of the same tasks: surf the web, play videos, read books,and edit documents. An iOS app is under tremendous pressure to conserve, and make the best use of, its memory. Xcode has several tools to help you analyze and improve your app’s memory use.
Quantifying memory use isn’t as conclusive as measuring code performance. Poor use of memory degrades your apps, other apps, and the iOS device in general, in odd and subtle ways. Generally, you want your app to useas little memory as possible. But that can be counter to making your app faster, because caching data and objects is one of the ways of improving its performance.
In this section you’re going to work through the bare minimum of responsible memory use by stress testing your app and responding to iOS’s low memory warnings. When an app starts to use too much memory, iOS firstgives it a chance to release memory it’s not relying on. It does this by sending low memory warning notifications. If the app responds, it (and other apps) can all continue to run. If it doesn’t, or can’t recover enough memory,iOS will begin terminating apps, in order to keep the active app running.
If an app continues to ignore these warnings, it runs the risk of running out of memory. This usually has disastrous results. The app will crash, and the user will be left staring at the springboard. This situation should be avoidedat all costs.
In this section you’re going to use Instruments to trace the memory usage of your app and find its breaking point. You’ll then modify your code and use Instruments again to verify that it behaves correctly. But first, you haveto push it to its limit.
Breaking MyStuff
MyStuff is an unbounded app. You put no limits on the number of items the user can add. Now you might expect that number to be reasonable, and MyStuff’s memory needs to be nominally modest. And most of the time, you’dbe right. But if I have 20 model train cars, you know there’s somebody with 300. What happens to MyStuff when the user wants to track 100, or 500, or 2,000 items? Is that crazy? Absolutely. Is it OK if MyStuff crashes after adding the 500th item? No, it is not.
When you develop any app, you need to stress test it. How you do that will depend on the nature of the app. For MyStuff, its Achilles’ heel is the memory used by all of those item image objects. If the user keeps adding itemsand images, it’s going to run out.
You’ve already written MyStuff to keep its image objects in memory, lazily loaded from the document’s file wrapper objects. You also have to establish a performance criterion: MyStuff should use its memory efficiently and notcrash if the user enters 1,000 items. That’s a reasonable goal.
The next step is to test it.
I don’t know what you’re doing this weekend, but I don’t want to spend it entering 1,000 items (with pictures) into MyStuff. I suggest you do what any respectable programmer would do; you cheat. Add some code to MyStuff togenerate hundreds of test items.
The first step is to make MyWhatsit copyable. Select the MyWhatsit.h interface file and adopt the NSCopying protocol (new code in bold):
@interface MyWhatsit : NSObject <NSCoding,NSCopying>
Switch to the MyWhatsit.m implementation file and add the necessary -copyWithZone: method (you learned how to do this in Chapter 20):
- (id)copyWithZone:(NSZone *)zone
{
MyWhatsit *copy = [[[self class] alloc] init]; if (copy!=nil)
{
copy->_name = _name;
copy->_location = _location; copy->image = image;
copy->imageKey = imageKey;
}
return copy;
}
Select the MSThingsDocument.m file, find the -loadFromContents:ofType:error: method, and locate the code that unarchives the things array. Immediately after that, add this test code (in bold):
if (thingsData!=nil)
{
things = [NSKeyedUnarchiver unarchiveObjectWithData:thingsData];
#if 1 // STRESS TESTING: generate a thousand test items if (things.count>10)
{
NSUInteger cloneIndex = 0; while (things.count<1000)
[things addObject:[things[cloneIndex++] copy]];
#endif
}
[things makeObjectsPerformSelector:@selector(setDocument:) withObject:self];
}
Run the modified MyStuff app. Add more than 10 items to the list, with images. Press the home button, to push the app to the background, and let it save its document. Run it again. This time, your list will have 1,000 items.Now it’s time to see how well MyStuff handles that!
Measuring Memory
Memory and CPU usage are such important performance metrics for apps that Xcode includes a set of “mini” instruments right in its debug navigator. Run your MyStuff app from within Xcode. Switch to the debug navigator (if itdoesn’t appear automatically), as shown in Figure 23-9.
Figure 23-9. Xcode’s CPU and memory monitors
The stress-test code you added will generate a 1,000 test items. Start scrolling through the list. As each table row reveals a new MyWhatsit item, it requests the item’s image. This, in turn, loads a new UIImage object from thedocument and saves it in the image property of the object.
As you scroll, you’ll notice the memory use of the app begins to climb, as shown in Figure 23-9. Keep scrolling and the app will eventually run out of memory. You’ll be rewarded with a fatal app crash, accompanied by themessage shown in Figure 23-10.
Figure 23-10. Memory pressure failure
Xcode explains that your app “Terminated due to Memory Pressure,” which is a polite way of saying it used up all of its available memory and died an ugly death. This is horrible. You definitely don’t want this happening to your users.
The CPU and Memory monitors in Xcode are great for spot-checking your app’s use of these precious resources, but when there’s a big problem you’ll usually need more detail. Turn again to Instruments.










No comments:
Post a Comment