Hey Apple-platform developers! Recently we in Machine Learning Works discovered an interesting library Watchdog and have been using it for awhile under DEBUG scheme to detect the main thread issues. It just checks that the main thread is able to process simple tasks quickly enough, otherwise it shows cops in Xcode output like this ones:

👮 Main thread was blocked for 1.25s 👮

It worked fine just until we wished to catch the problematic main thread code with asserts inside Watchdog. It did not really work as we expected, execution was stopping at random places in the code — not the most busy part of it. I had an idea to achieve sample utility behaviour built-in within an app, but there were a few problems to solve:

Collect stack traces of the main thread from a different thread

Combine stack traces in a single call stack tree

Symbolicate stack traces

Stack traces

First problem can be solved with PLCrashRepoter framework. Capturing whole app reports and extracting the main thread call stacks is not the most efficient way to achieve this, but is the most comfortable and fits good for POC. If you are able to help me to improve this step, you are welcome!

Thread call stack may looks like this one:

0 PuppyWatchdog_Tests 0x000000010fdb5004 0x10fdb4000 + 4100

1 PuppyWatchdog_Tests 0x000000010fdb50b5 0x10fdb4000 + 4277

2 CoreFoundation 0x00000001040cf05c 0x104051000 + 516188

3 CoreFoundation 0x00000001040ceee1 0x104051000 + 515809

4 XCTest 0x00000001035810dc 0x103566000 + 110812

5 XCTest 0x00000001035b98a0 0x103566000 + 342176

6 XCTest 0x0000000103580ee8 0x103566000 + 110312

7 XCTest 0x0000000103581701 0x103566000 + 112385

8 XCTest 0x000000010357e7c9 0x103566000 + 100297

9 XCTest 0x000000010357e7c9 0x103566000 + 100297

10 XCTest 0x000000010357e7c9 0x103566000 + 100297

11 XCTest 0x000000010356a918 0x103566000 + 18712

12 XCTest 0x000000010358be8f 0x103566000 + 155279

13 XCTest 0x000000010356a7b5 0x103566000 + 18357

14 XCTest 0x000000010356b602 0x103566000 + 22018

15 XCTest 0x00000001035bae28 0x103566000 + 347688

16 xctest 0x00000001034ed922 0x1034ec000 + 6434

17 libdyld.dylib 0x0000000106eb968d 0x106eb5000 + 18061

Symbolication

All symbols located in dSYM files are not accessible from inside app execution either on simulator or on real target device.

Thanks to Objective-C runtime we can symbolicate at least Objective-C methods calls: just enumerate each methods of all classes and remember its addresses. Code example using RuntimeRoutines:

RRClassEnumerateAllClasses(YES, ^(Class klass) {

RRClassEnumerateMethods(klass, ^(Method method) {

IMP imp = method_getImplementation(method);

SEL sel = method_getName(method);

NSString *value = [NSString stringWithFormat:@”%c[%s %@]”,

class_isMetaClass(klass) ? ‘+’ : ‘-’,

object_getClassName(klass),

NSStringFromSelector(sel)];

// ...

// Store IMP and value

// ...

});

});

Some of C functions (those are in Mach-O binary export table) can be enumerated with libMachO/MachOKit framework. The code sample is too large to be embedded in the article, you always can find it here.

Call stack rows have the following structure:

<index> <image-name> <code-address> <image-base-address> <code-offset-from-base>

The main assumption is that <code-address> is something greater than a function/method address pointing somewhere inside function code. Looks like a function/method can be found by <code-address> for O(log n) complexity using an ordered dictionary (for example std::map). So stack traces should be combined in a single tree after function/method resolving to align <code-address> to the nearest left function address.

Solution

Having all these problems solved in PuppyWatchdog, we are able to see such rich reports right in the Xcode output:

Diving into this problem was really exciting and informative for me, solving it — was an interesting challenge.