More objc method tracing

Bill Bumgarner has posted a few great articles on Objective-C method tracing using dtrace:

I have a internal tool at Omni that traces message sends by creating new method IMPs to wrap the existing ones in a trampoline, but it requires some nasty assembly stubs and has only ever worked on Sparc, ppc and x86. It is very fast, but hard to maintain and a little fragile (and doesn't handle the nil case). A 90% solution can be obtained by extending Bill and other's work.

Ken sent out a partial tracing solution this morning and finally got me off my keister. Here is a D script that catches ObjC message sends and dumps a OPML fragment (which can then be wrapped in the XML goo via an external shell script or whatever) and then viewed in OmniOutliner.

Download: objc-trace-opml.d.gz

This isn't a perfect solution by any means. Some of the issues:

  • This has to be run in an essentially single-threaded program; any background threads must be totally quiescent or you'll get a mixture of output from multiple threads. It wouldn't be too hard to modify this script to include a thread identifier or maybe just filter out anything from background threads.
  • This won't handle exceptions correctly; they'll break the OPML nesting. Our internal tool handles this, but since NSError arrived on the scene, this is less of an issue. Just avoid tracing code with exceptions.
  • dtrace is slow when matching all entries in the objc provider. The startup time on this script is something like 40 seconds on my machine. Annoying, but not the worst thing ever, since you are likely to trace once and then spend a fair bit of time examining the results.
  • This particular variant of the script will only work on x86 due to the hacky way I check for objc_msgSend_stret.
  • dtrace can overflow its buffer and drop events when there are massive numbers of probe hits. Run with a big buffer and trace only the exact set of methods you need. Ideally you'll be running in the debugger, stop right before where you need to trace turn on tracing and then 'next' over one or two lines of code. The '-b' flag to dtrace can also be used to increase the buffer size.

Some of the nice bits:

  • No assembly-fu required.
  • Call hierarchy is preserved and can be expanded/hoisted and such in OmniOutliner.
  • Both the class of the receiver and the class of the method implementation are included. So, you can see that +[MySuperclass initialize] is getting called on MySubcass, for example.
  • All method invocations are shown, so you can see nested calls to super instead of just the initial call to objc_msgSend. I've not tested whether cached method IMPs get traced too; normal code should get traced as expected. Swizzling, IMP caching or dynamically registering methods will possibly not work as well.
  • The pointer value of the receiver is emitted. By the time you examine the output, it might be dead and gone, but a surprising amount of the time it isn't. For example, things like static NSStrings, NSScriptClassDescriptions, interface widgets and other cache-once data may still be around for submitting to 'po' in the debugger.