Scrolling, scrolling, scrolling. There I was, scrolling a table view in the Simulator like so many other iOS engineers. And just like them, my table view was fetching images over the network as I was scrolling through cells. Each time a cell became visible it would make an HTTP request for an image.

Usually this would be the point where I would start up Charles and re-start the simulator. The simulator has a bug that prevents it from noticing network configuration changes like starting a proxy. With a debugging proxy like Charles you can see the HTTP traffic going back and forth, though with App Transport Security setting this up can be pretty cumbersome.

To get the best user experience we want to minimize the number of times those images are actually coming from the network. If you know the HTTP 1.1 specification pretty well you can read the response headers to see how something should be cached. I know it fairly well but reading those headers can be a chore (and RedBot reads them better that I do). Just reading the headers though doesn’t always tell you want you want to know - on iOS the URL loading system doesn’t always interpret the specification the way you might think it should.

So why not just directly measure how the URL loading system cache itself is caching the response? Wouldn’t it be great if we could easily see that, and even see it in Instruments?

Well, we can. In fact, just about anything in your application that you would want to time or measure can be made available to Instruments.

Enter DTrace

Instruments is a great application, and it is mostly just a front end to a system-level tool called DTrace. MacOS and iOS both have many built in “probes” that provide data to DTrace and can be visualized in Instruments.

There are many examples out there of how to use the built in dtrace probes and script them. There are not that many examples of how to create a probe for your application and read the data in Instruments.

Creating a custom DTrace probe

Create a new, empty file in Xcode:

Name it with a “.d” file extension:

Edit the file you just created. First we will define our provider:

provider http_cache { };

For our cache probe we want to be able to pass it two strings and an integer: the host name, path, and 1 for a cached response or 0 for a response that is not cached. Add the probe to the provider:

provider http_cache { probe response_from_cache(char *, char *, int); };

Now build the project. When Xcode builds the project it will execute a build rule for this file that builds probe and produces a header file to make it accessible from C. That will file will be placed in the DERIVED_SOURCES directory.

After building you can now access the probe from your application. To measure wether the response is coming from the cache or not, we are going to subclass NSURLCache and override one method:

#import “ProbedURLCache.h” @implementation ProbedURLCache - (nullable NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request { return [super cachedResponseForRequest:request]; } @end

The header file that Xcode generated for the probe contains two macros: HTTP_CACHE_RESPONSE_FROM_CACHE_ENABLED() and HTTP_CACHE_RESPONSE_FROM_CACHE(arg0, arg1, arg2) . HTTP_CACHE_RESPONSE_FROM_CACHE_ENABLED() returns 0 if the application is not being observed with DTrace, which is useful if you are performing heavy work to prepare data for your probe function. HTTP_CACHE_RESPONSE_FROM_CACHE(arg0, arg1, arg2) is the probe function itself, which takes the three arguments we specified in the probe file.

#import “ProbedURLCache.h” #import “http_cache.h” @implementation ProbedURLCache - (nullable NSCachedURLResponse *)cachedResponseForRequest:(NSURLRequest *)request { NSCachedURLResponse *result = nil; int cached = 0; result = [super cachedResponseForRequest:request]; if (result != nil){ cached = 1; } if (HTTP_CACHE_RESPONSE_FROM_CACHE_ENABLED()){ HTTP_CACHE_RESPONSE_FROM_CACHE([[[request URL] host] UTF8String], [[[request URL] path] UTF8String], cached); } return result; } @end

You can use the NSURLCache subclass with NSURLSession by setting it in the NSURLSessionConfiguration, or with NSURLConnection by setting it as the shared global cache. Without an NSURLCache set the URL loading system performs only limited, in-memory caching. Explicitly setting an NSURLCache not only allows your application to control how large the in-memory cache is, but also allows on-disk caching. Almost all applications benefit greatly from on-disk caching.

Swift

Unfortunately in Swift things are a little more complicated. The C preprocessor macros that recent versions of Xcode produce for the probe are not usable from Swift even through a bridging header. If this is functionality you would want to access from Swift, please file a radar.

With that in place we are ready to start tracing in Instruments.

Using the probe in Instruments

Start up an Instruments profiling session. When it prompts you to select a template, choose “Blank”:

Now from the Instrument menu, select “Build New Instrument…”. This is where you can configure Instruments to attach to DTrace probes and execute scripts with the probe data.

To attach to the probe we just defined we fill in the provider and probe function names in the predicate editor:

For our purposes we don’t need any scripting of the probe data, but we want to make the data available to the Instruments trace. Scroll down in the instrument editor and you can specify what data to record:

Each of the 3 arguments we pass to our probe is something we want in the trace. For whatever reason Instruments insists the last item to record is “Record No Data”.

Save the Instrument and it will be added to your Library (accessible through the Window menu).

Now you are ready to trace. Start profiling your application. If you do not see data showing up when you expect it, you may need to quit Instruments and start a new trace. Unfortunately recent versions of Instruments will not save a trace with a custom Instrument - Instruments will not even quit, it has to be force quit..

Measure, don’t log

You can measure almost anything in your application using Instruments with a custom probe. Often this can be much more useful and enlightening than using the debugger or logging. For example, in a game you could create a probe for different game states or for tracking collision detection - which would be very impractical for debugging.