DTrace

Few have heard of DTrace, a little gem tucked away somewhere inside the OS. It's a debugging power tool — powerful because it's extremely flexible, and probably relatively unknown because it is so very different from other tools out there.

Quite often, real users or testers of your app will see some unexpected behavior. DTrace lets you answer arbitrary questions about the app, on a production version of the app, and without relaunching the app.

Dynamic Tracing

Almost 10 years ago, Sun Microsystems built DTrace, short for Dynamic Trace. And at the end of 2007, Apple integrated it into its operating system.

DTrace is a dynamic tracing framework that provides zero disable cost, i.e. probes in code have no overhead when disabled — we can leave the probes in our code even for production builds. You only pay the cost of the probe when using it.

DTrace is dynamic, which means that we can attach to an already running program and detach from it again without interrupting the program itself. No need to recompile or restart.

In this article, we’ll focus on using DTrace to investigate our own program, but it’s worth mentioning that DTrace is systemwide: a single script could, for example, watch allocations made by all processes on the system. Take a look inside /usr/share/examples/DTTk for some great examples.

OS X vs. iOS

As you might have guessed by now, DTrace is only available on OS X. Apple also uses DTrace on iOS, in order to power tools such as Instruments, but for third-party developers, it is only available on OS X or the iOS simulator.

At Wire, DTrace has been very helpful on iOS, even though we're limited to using it in the iOS simulator. If you're reading this article and think real DTrace support on iOS devices is a good idea, please file an enhancement request with Apple.

Probes and Scripts

There are two parts to DTrace: the DTrace probes, and the DTrace scripts that attach to those probes.

Probes

There are build-in probes, and you can add (so-called static) probes to your code. A probe looks very similar to a normal C function. At Wire, our syncing code has an internal state machine, and we define these two probes:

								provider syncengine_sync {
    probe strategy_go_to_state(int);
}

							

Probes are grouped into so-called providers. The int argument is the state that we're entering. In our Objective-C (or Swift) code, we simply insert the following:

								- (void)goToState:(ZMSyncState *)state
{
    [self.currentState didLeaveState];
    self.currentState = state;
    SYNCENGINE_SYNC_STRATEGY_GO_TO_STATE(state.identifier);
    [self.currentState didEnterState];
}

							

We'll talk about how to integrate all of this and clean it up in a bit.

Scripts

We can now write a small DTrace script that shows us state transitions:

								syncengine_sync*:::strategy_go_to_state
{
    printf("Transitioning to state %d\n", arg0);
}

							

(We will go into more detail below about how DTrace scripts work.)

If we save this DTrace into state.d, we can then run it using the dtrace(1) command line tool:

								% sudo dtrace -q -s state.d

							

We will see the following:

								Transitioning to state 1
Transitioning to state 2
Transitioning to state 5

							

Just as we'd expect. Nothing too exciting. Hit ^C to exit DTrace.

A Timing Example

Since DTrace is very low cost, it's a perfect fit for measuring performance — even when times measured are very small. The resolution of the timers inside DTrace is in nanoseconds.

If we extend the trivial example from above, we can output the time spent in each state:

								uint64_t last_state;
uint64_t last_state_timestamp;

dtrace:::BEGIN
{
    syncState[4] = "EventProcessing";
    syncState[5] = "QuickSync1";
    syncState[6] = "QuickSync2";
}

syncengine_sync*:::strategy_go_to_state
/ last_state_timestamp != 0 /
{
    t = (walltimestamp - last_state_timestamp) / 1000000;
    printf("Spent %d ms in state %s\n", t, syncState[last_state]);
}

syncengine_sync*:::strategy_go_to_state
{
    printf("Transitioning to state %s\n", syncState[arg0]);
    last_state = arg0;
    last_state_timestamp = walltimestamp;
}

							

This will output the following:

								Transitioning to state QuickSync1
Spent 2205 ms in state QuickSync1
Transitioning to state QuickSync2
Spent 115 ms in state QuickSync2
Transitioning to state EventProcessing

							

There are a few new things in this script. The dtrace:::BEGIN clause will run when the script starts. There's a matching END probe for when the script exits.

We also added a predicate, / last_state_timestamp != 0 /, to the first probe.

Finally, we're using variables in the global scope to keep track of what the last state was, and at what point in time we entered it.

The built-in walltimestamp variable returns the current number of nanoseconds since the Unix epoch.

There's another timestamp variable, vtimestamp, which is a virtualized timestamp, also in nanoseconds. It is the amount of time the current thread has been running on a CPU, minus the time spent in DTrace. Finally, machtimestamp corresponds to mach_absolute_time().

For the above script, the order of execution is important. We have two so-called clauses, which match the same probe, (syncengine_sync*:::strategy_go_to_state). These will run in the order in which they appear in the D program.

Combining with System-Provided Probes

The operating system, particularly the kernel, provides thousands of probes, all grouped into various providers. A lot of these are the ones documented by Oracle's DTrace documentation.

We can use the ip provider's send probe to check how many bytes are sent over the network before we transition to the next state with this script:

								uint64_t bytes_sent;

syncengine_sync$target:::strategy_go_to_state
{
    printf("Transitioning to state %d\n", arg0);
    printf("Sent %d bytes in previous state\n", bytes_sent);
    bytes_sent = 0;
}

ip:::send
/ pid == $target /
{
    bytes_sent += args[2]->ip_plength;
}

							

This time, we are targeting a specific process — the ip:::send would otherwise match all processes on the system, while we're only interested in the Wire process. We run this script with the following:

								sudo dtrace -q -s sample-timing-3.d -p 198

							

Here, 198 is the process identifier (aka PID) of the process. We can find this number in the Activity Monitor app, or by using the ps(1) command line tool.

We'll get this:

								Transitioning to state 6
Sent 2043 bytes in previous state
Transitioning to state 4
Sent 581 bytes in previous state

							

D Programming Language

Note: This is not D by W. Bright and A. Alexandrescu.

Most parts of D are very similar to the C programming language, but the overall structure is different. Each DTrace script consists of multiple so-called probe clauses.

In the above examples, we have seen a few of these probe clauses. They all follow this general form:

								probe descriptions
/ predicate /
{
    action statements
}

							

The predicate and the action statement parts are both optional.

Probe Descriptions

The probe description defines what probe the clause matches. These are all of the forms where parts can be left out:

								provider:module:function:name

							

For example, syscall::: matches all probes by the syscall provider. We can use * to match any string so that syscall::*lwp*:entry matches all entry probes by the syscall provider, where the function name contains lwp.

A probe description can consist of multiple probes, such as this:

								syscall::*lwp*:entry, syscall::*sock*:entry
{
    trace(timestamp);
}

							

Predicates

We can use predicates to limit when the action statements are to be run. The predicates are evaluated when the specified probes fire. If the predicate evaluates to non-zero, the action statements will run, similar to an if statement in C.

We can list the same probe multiple times with different predicates. If multiples of them match, they will be run in the order that they appear in the D program.

Actions

The actions are enclosed in curly braces. The D language is lightweight, small, and simple.

D does not support control flow, such as loops or branches. We can not define any user functions. And variable declaration is optional.

This limits what we can do. But the simplicity also gives us a lot of flexibility once we know a few common patterns, which we'll look into in the next section. Be sure to also check out the D Programming Language guide for details.

Common D Programming Patterns

The following examples will give us a good feel for some of the things we can do.

This example measures the accumulated time the App Store application spends inside any syscall (i.e. a system call, or a call into the kernel):

								syscall:::entry
/ execname == "App Store" /
{
    self->ts = timestamp;
}

syscall:::return
/ execname == "App Store" && self->ts != 0 /
{
    @totals[probefunc] = sum(timestamp - self->ts);
}

							

If we run this and launch the Mac App Store application, then exit the D Trace script with ^C, we will get output like this:

								dtrace: script 'app-store.d' matched 980 probes
^C

  __disable_threadsignal                                         2303
  __pthread_sigmask                                              2438
  psynch_cvclrprepost                                            3216
  ftruncate                                                      3663
  bsdthread_register                                             3754
  shared_region_check_np                                         3939
  getpid                                                         4189
  getegid                                                        4276
  gettimeofday                                                   4285
  flock                                                          4825
  sigaltstack                                                    4874
  kdebug_trace                                                   5430
  kqueue                                                         5860
  workq_open                                                     6155
  sigprocmask                                                    6188
  setrlimit                                                      7085
  psynch_cvsignal                                                8909
  
  [...]
  
  stat64                                                      6451260
  read                                                        6657207
  fsync                                                       8231130
  rename                                                      8340468
  open_nocancel                                               8856035
  workq_kernreturn                                           15835068
  getdirentries64                                            17978504
  bsdthread_ctl                                              25418263
  open                                                       29503041
  psynch_mutexwait                                          453338483
  ioctl                                                    1049412360
  __semwait_signal                                         1373514528
  select                                                   1632760820
  kevent64                                                 3656884980

							

In this example, the App Store spent 3.6 seconds of CPU time inside the kevent64 syscall.

There are two very interesting things inside this small script: thread local variables (self->ts) and aggregations.

Scope of Variable

D has three different scopes for variables: global, thread local, and probe clause local.

A global variable such as foo or bar is visible throughout the D program.

Thread local variables are named self->foo, self->bar, etc., and are local to the specific thread.

Probe clause local variables are similar to local variables in C or Swift. They can be useful for intermediate results.

In the script, we use the first probe clause to match when we enter a syscall. We set the thread local variable, self->ts, to the current timestamp:

								syscall:::entry
/ execname == "App Store" /
{
    self->ts = timestamp;
}

							

The second clause matches when the thread returns from the syscall. It will be the same thread that entered, hence we can be sure that self->ts has the expected value, even when multiple threads do system calls at the same time.

We add self->ts != 0 to the predicate to be sure that our script works, even if we attach to the application while it is inside a system call. Otherwise, timestamp - self->ts would result in a very large value, because self->ts would not have been set:

								syscall:::return
/ execname == "App Store" && self->ts != 0 /
{
    @totals[probefunc] = sum(timestamp - self->ts);
}

							

For all the nitty-gritty details on variables, be sure to check the Dynamic Tracing Guide, “Variables.”

Aggregations

This line uses aggregations:

								@totals[probefunc] = sum(timestamp - self->ts);

							

This is an extremely powerful feature of DTrace.

We're calling our aggregation variable totals. The @ in front of the name turns it into an aggregation. probefunc is a built-in variable — it is the name of the probe function. For syscall probes, as in our case, probefunc is the name of the system call being made.

sum is the aggregation function. In our case, the aggregation is summing up timestamp - self->ts for each probefunc.

The DTrace Guide shows this small example script that uses aggregations to print the number of system calls per second of the 10 applications that do the most system calls:

								#pragma D option quiet

BEGIN
{
    last = timestamp;
}

syscall:::entry
{
    @func[execname] = count();
}

tick-10sec
{
    trunc(@func, 10);
    normalize(@func, (timestamp - last) / 1000000000);
    printa(@func);
    clear(@func);
    last = timestamp;
}

							

On a mostly idle OS X, it may show something like this:

								kextd                                                             7
ntpd                                                              8
mds_stores                                                       19
cfprefsd                                                         20
dtrace                                                           20
UserEventAgent                                                   34
launchd                                                          42
Safari                                                          109
cloudd                                                          115
com.apple.WebKi                                                 177

mds                                                               8
Wire                                                              8
Terminal                                                         10
com.apple.iClou                                                  15
dtrace                                                           20
securityd                                                        20
tccd                                                             37
syncdefaultsd                                                    98
Safari                                                          109
com.apple.WebKi                                                 212

							

We see Safari, WebKit, and cloudd being active.

Here is the total list of aggregation functions:

								Function Name     | Result 
------------------|---------
count             | Number of times called
sum               | Sum of the passed in values
avg               | Average of the passed in values
min               | Smallest of the passed in values
max               | Largest of the passed in values
lquantize         | Linear frequency distribution
quantize          | Power-of-two frequency distribution

							

The quantize and lquantize functions can be very helpful in providing an overview of what quantities are being passed:

								ip:::send
{
    @bytes_sent[execname] = quantize(args[2]->ip_plength);
}

							

The above will output something like this:

								discoveryd                                        
         value  ------------- Distribution ------------- count    
            16 |                                         0        
            32 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 2        
            64 |                                         0        

syncdefaultsd                                     
         value  ------------- Distribution ------------- count    
           256 |                                         0        
           512 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 4        
          1024 |                                         0        

kernel_task                                       
         value  ------------- Distribution ------------- count    
             8 |                                         0        
            16 |@@@@@@@@@@@@@@                           37       
            32 |@@@@@@@@@@@@@@@@@@@@@@@@@@               67       
            64 |                                         0        

com.apple.WebKi                                   
         value  ------------- Distribution ------------- count    
            16 |                                         0        
            32 |@@@@@@@@@@@@@@@@                         28       
            64 |@@@@                                     7        
           128 |@@@@                                     6        
           256 |                                         0        
           512 |@@@@@@@@@@@@@@@@                         27       
          1024 |                                         0        

							

Be sure to check out the samples from the Dynamic Tracing Guide for information on how to use lquantize.

Associative Arrays

Despite the name, arrays in D are more akin to dictionaries in Swift or Objective-C. In addition, they are multidimensional.

We can define an associative array with this:

								int x[unsigned long long, char];

							

Then, we can assign to it, like so:

								BEGIN
{
    x[123ull, ’a’] = 456;
}

							

For the Wire app, we want to track the roundtrip times for NSURLSessionTask instances. We fire a statically defined probe when we resume such a task, and a second probe when it completes. With that, we can write a simple script like this:

								syncengine_sync$target:::operation_loop_enqueue
/ arg0 == 4 /
{
    start_transport_request_timestamp[arg1] = timestamp;
}

syncengine_sync$target:::operation_loop_enqueue
/ arg0 == 6 && start_transport_request_timestamp[arg1] != 0 /
{
    @time["time for transport request round-trip"] = quantize(timestamp - start_transport_request_timestamp[arg1]);
}

							

We pass in the taskIdentifer as arg1, and arg0 is set to 4 when the task is resumed, and 6 when it completes.

Associative arrays are also very helpful in providing a good description for enum values passed into a clause, as we've seen in the first example, “A Timing Example,” above.

Probes and Providers

Let's take a step back and look at the probes that are available.

We can get a list of all available providers with this:

								sudo dtrace -l | awk '{ match($2, "([a-z,A-Z]*)"); print substr($2, RSTART, RLENGTH); }' | sort -u

							

On OS X 10.10, there are 79 providers. Many of these are specific to the kernel and system calls.

Some of these providers are part of the original set documented in the Dynamic Tracing Guide. Let's look at a few of those that are available to us.

dtrace Provider

We mentioned the BEGIN and END probes above. The dtrace:::END is particularly helpful for outputting summaries when running DTrace in the quiet mode. There's also an ERROR probe for when an error occurs.

profile Provider

The profile provider can be used for sampling in a way that should be familiar to users of Instruments.

We can sample the stack depth at 1001 Herz:

								profile-1001
/pid == $1/
{
    @proc[execname] = lquantize(stackdepth, 0, 20, 1);
}

							

The output will look something like this:

								Safari                                            
         value  ------------- Distribution ------------- count    
           < 0 |                                         0        
             0 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@     704      
             1 |@                                        12       
             2 |@@                                       30       
             3 |@                                        17       
             4 |                                         7        
             5 |                                         6        
             6 |                                         1        
             7 |                                         2        
             8 |                                         1        
             9 |                                         7        
            10 |                                         5        
            11 |                                         1        
            12 |                                         0        

							

Similarly, the tick- probes will fire every fixed interval at a high interrupt level. The profile- probes will fire on all CPUs, and the tick- probes only on a single CPU per interval. We used tick-10sec in our aggregation example above.

pid Provider

The pid provider is a bit of a brute force provider. In most cases, we should really use static probes as described below.

pid is short for process identifier. It lets us probe on the function entry and exit of a process. This works in most cases. Note that function entry and return are not always well defined, particularly due to tail-call optimization. And some functions don’t have to create a stack frame, etc.

When you can't change the code to add static probes, pid is a powerful tool.

You can trace any function that's visible. For example, look at this probe:

								pid123:libSystem:printf:return

							

It would attach to printf, returning in the process with the process identifier (PID) 123.

objc Provider

A direct counterpart to the pid provider is the objc provider. It provides probes for Objective-C method entry and exit. Again, using static probes provides more flexibility.

The format for objc probe specifiers is this:

								objcpid:[class-name[(category-name)]]:[[+|-]method-name]:[name]

							

And

								objc207:NSTableView:-*:entry

							

would match entry into all instance methods of NSTableView in the process with PID 207. Since the colon symbol : is part of the DTrace probe specifier scheme, : symbols in Objective-C method names need to be replaced with a question mark, ?. For example, to match entry into -[NSDate dateByAddingTimeInterval:], we'd use this:

								objc207:NSDate:-dateByAddingTimeInterval?:entry

							

For additional details, check the dtrace(1) man page.

io Provider

To trace activity related to disk input and output, the io provider defines six probes:

								start
done
wait-start
wait-done
journal-start
journal-done

							

The sample from the Oracle documentation shows how this can be put to use:

								#pragma D option quiet

BEGIN
{
    printf("%10s %58s %2s\n", "DEVICE", "FILE", "RW");
}

io:::start
{
    printf("%10s %58s %2s\n", args[1]->dev_statname,
        args[2]->fi_pathname, args[0]->b_flags & B_READ ? "R" : "W");
}

							

The above will output something like this:

								??                   ??/com.apple.Safari.savedState/data.data  R
??            ??/Preferences/com.apple.Terminal.plist.kn0E7LJ  W
??                                            ??/vm/swapfile0  R
??              ??/Preferences/com.apple.Safari.plist.jEQRQ5N  W
??           ??/Preferences/com.apple.HIToolbox.plist.yBPXSnY  W
??       ??/fsCachedData/F2BF76DB-740F-49AF-94DC-71308E08B474  W
??                           ??/com.apple.Safari/Cache.db-wal  W
??                           ??/com.apple.Safari/Cache.db-wal  W
??       ??/fsCachedData/88C00A4D-4D8E-4DD8-906E-B1796AC949A2  W

							

ip Provider

There are two probes, send and receive, inside the ip provider. They are triggered whenever data is either sent or received over the IP. The arguments arg0 to arg5 provide access to the kernel structures related to the IP packet being sent or received.

This can be put together into very powerful network debugging tools. It will make using tcpdump(1) seem like yesteryear's trick. The ip provider lets you print exactly the information you are interested in, at the point in time that you're interested in it.

Check the documentation for some excellent examples.

Defining Our Own Static Probes

DTrace lets us create our own probes, and with that, we can unlock the real power of DTrace for our own apps.

These kinds of probes are called static probes in DTrace. We briefly talked about this in the first examples. The Wire app defines its own provider with its own probe:

								provider syncengine_sync {
    probe strategy_go_to_state(int);
}

							

We then call this probe in the code:

								- (void)goToState:(ZMSyncState *)state
{
    [self.currentState didLeaveState];
    self.currentState = state;
    SYNCENGINE_SYNC_STRATEGY_GO_TO_STATE(state.identifier);
    [self.currentState didEnterState];
}

							

One might argue that we could have just used the objc provider; using our own probe gives us more flexibility. We can later change the Objective-C code without affecting the DTrace probes.

Additionally, the static probes give us easy access to the arguments. We saw above how we can use this both for timing and logging.

The power of DTrace static probes is that they give us a stable interface to debug our code, and an interface that exists even in production code. When someone is seeing odd behavior, we can attach a DTrace script to the running app, even for a production build of that app. And the flexibility of DTrace allows us to use the same probes for multiple purposes.

We can use DTrace as a logging tool. But it also allows us to gather detailed quantitative information about timing, network requests, etc.

The reason we can leave the probes in production code is that probes are zero cost, or to be fair, equivalent to a test-and-branch CPU instruction.

Let's take a look at how we add static probes to our project.

Provider Description

First we need to create a .d file that defines the providers and probes. If we create a provider.d file with this content, we will have two providers:

								provider syncengine_sync {
    probe operation_loop_enqueue(int, int, intptr_t);
    probe operation_loop_push_channel_data(int, int);
    probe strategy_go_to_state(int);
    probe strategy_leave_state(int);
    probe strategy_update_event(int, int);
    probe strategy_update_event_string(int, char *);
};

provider syncengine_ui {
    probe notification(int, intptr_t, char *, char *, int, int, int, int);
};

							

These providers are syncengine_sync and syncengine_ui. Within each one, we define a set of probes with their argument types.

Creating a Header File

We now need to add the provider.d file to an Xcode target. It is important to make sure that the type is set to DTrace source. Xcode now automatically processes it when building. During this processing, DTrace creates a corresponding provider.h header file that we can include. It is important to add the provider.d file to both the Xcode project and the corresponding target.

Under the hood, Xcode calls the dtrace(1) tool:

								dtrace -h -s provider.d

							

This will generate the corresponding header file. The header file ends up in the `DERIVED_FILE_DIR, and can be imported with

								#import "provider.h"

							

in any source file in the project. Xcode has a built-in, so-called build rule for DTrace provider descriptions. Compare objc.io issue #6, “The Build Process,” for more information on build rules and the build process in general.

Adding the Probe

For each static probe, the header file will contain two macros:

								PROVIDER_PROBENAME()
PROVIDER_PROBENAME_ENABLED()

							

The first one is the probe itself. The second one will evaluate to 0 when the probe is not enabled.

The DTrace probes themselves are zero cost when not enabled, i.e. as long as no one attached to a probe, they're for free. Sometimes, however, we may want to pre-calculate / preprocess some data before sending it to a probe. In those rare cases, we can use the _ENABLED() macro, like so:

								if (SYNCENGINE_SYNC_STRATEGY_GO_TO_STATE_ENABLED()) {
    argument = /* Expensive argument calculation code here */;
    SYNCENGINE_SYNC_STRATEGY_GO_TO_STATE(argument);
};

							

Wrapping DTrace Probes

Code readability is very important. As we add more and more DTrace probes to our code, we need to make sure the code doesn't get cluttered and incomprehensible due to the probes. After all, the probes are here to help us, not to make things more difficult.

What we did was to add another simple wrapper. These wrappers both make the code a bit more readable and also add the if (…_ENABLED*()) check in one go.

If we go back to the state machine example, here is our probe macro:

								SYNCENGINE_SYNC_STRATEGY_GO_TO_STATE(state);

							

In order to make that a bit easier on the eye, we created another header which defines the following:

								static inline void ZMTraceSyncStrategyGoToState(int d) {
    SYNCENGINE_SYNC_STRATEGY_GO_TO_STATE(d);
}

							

With that, we can call the following:

								ZMTraceSyncStrategyGoToState(state);

							

That may seem like a small feat, but the camel case blends in better with normal Objective-C and Swift coding style.

In other cases we went further. If we look at the

								provider syncengine_ui {
    probe notification(int, intptr_t, char *, char *, int, int, int, int);
};

							

we defined above, we have a long list of arguments. In the Wire app, we use this to log UI notifications.

This probe has a very long list of arguments. We decided that we wanted to use a single probe for a lot of different notifications that the syncing code sends to the UI to notify it about changes. The first argument, arg0, specifies what the notification is, and the second one specifies the object of the NSNotification. This makes it easy for our DTrace scripts to narrow in on those specific notifications of interest in the given situation.

The remainder of the arguments are less loosely defined on a per-notification basis, and we have multiple wrapper functions for the individual cases. When we want to pass two NSUUID instances, we call a wrapper function like this:

								ZMTraceUserInterfaceNotification_UUID(1, note.object,
    conversation.remoteIdentifier, participant.user.remoteIdentifier,
    wasJoined, participant.isJoined, currentState.connectionState, 0);

							

This wrapper function is defined as this:

								static inline void ZMTraceUserInterfaceNotification_UUID(int d, NSObject *obj, NSUUID *remoteID1, NSUUID *remoteID2, int e, int f, int g, int h) {
    if (SYNCENGINE_UI_NOTIFICATION_ENABLED()) {
        SYNCENGINE_UI_NOTIFICATION(d, (intptr_t) (__bridge void *) obj, remoteID1.transportString.UTF8String, remoteID2.transportString.UTF8String, e, f, g, h);
    }
}

							

As noted, this serves two purposes. For one, we don't clutter our code with things like (intptr_t) (__bridge void *). Additionally, we don't spend CPU cycles converting an NSUUID to an NSString and then to a char const *, unless we need to because someone is attached to the probe.

Along this pattern, we can define multiple wrapper / helper functions that funnel into the same DTrace probe.

DTrace and Swift

Wrapping DTrace probes like this also gives us integration between Swift code and DTrace static probes. static line function can now be called directly from Swift code:

								func goToState(state: ZMSyncState) {
    currentState.didLeaveState()
    currentState = state
    currentState.didEnterState()
    ZMTraceSyncStrategyGoToState(state.identifier)
}

							

How DTrace Works

The D programming language is a compiled language. When we run the dtrace(1) tool, it compiles the script we pass to it into byte code. This byte code is then passed down into the kernel. Inside the kernel, there's an interpreter that runs this byte code.

That's one of the reasons why the programming language was kept simple. No one wants a bug in a DTrace script to put the kernel into an infinite loop and hang the system.

When we add static probes to an executable (an app or a framework), these are added to the executable as S_DTRACE_DOF (DTrace Object Format) sections and loaded into the kernel when the executable runs. That is how DTrace knows about the static probes.

Final Words

It should be obvious that DTrace is extremely powerful and flexible. However, it is important to note that DTrace is not a replacement for tried and true tools such as malloc_history, heap, etc. As always, use the right tool for the job at hand.

Also, DTrace is not magic. You still have to understand the problem you're trying to solve.

That said, DTrace can bring your development skills and abilities to a new level. It allows you to track down problems in production code which are otherwise difficult or even impossible to locate.

If you have code that has #ifdef TRACING or #if LOG_LEVEL == 1 in it, these are probably good candidates for code to be replaced by DTrace.

Be sure to check out the Dynamic Tracing Guide (PDF version). And peek into your system's /usr/share/examples/DTTk directory for some inspiration.

Happy debugging!