Cocoa terse backtrace using NSRegularExpression

I recently wanted to improve some logging code to print out the callers to some methods (I’m tracking down errant retains / releases). [NSThread callStackSymbols] is excellent and easy, but I found that simply logging a chunk of the call stack was too verbose. I wanted to include callers from just my app and also parse it to include just the method names, ie. to turn this output from callStackSymbols:

0   MyApp                               0x0005da3a -[UnsavedPhoto retain] + 136,
1   CoreFoundation                      0x02b75fac CFRetain + 92,
2   CoreFoundation                      0x02b7ea52 __CFBasicHashAddValue + 98,
3   CoreFoundation                      0x02b86219 CFDictionarySetValue + 105,
4   CoreFoundation                      0x02c620d5 -[__NSCFDictionary setObject:forKey:] + 117,
5   MyApp                               0x000d631e -[OrderedDictionary setObject:forKey:] + 165,
6   MyApp                               0x0005c668 -[PhotoSaver addPhoto:] + 224,
...

into just this: "-[OrderedDictionary removeObjectForKey:] < -[PhotoSaver discardPhoto:]".

I made a method in NSString category (um, because it generates a string), which I call like this:

logthis = [NSString appBacktraceOfDepth:2 fromStackSymbols:[NSThread callStackSymbols]];

Here it is, fairly concise. Note that it does something reasonable if pre-iOS4 and NSRegularExpression is not present (namely just returns a chunk of the line from the top stack frame), fires an assertion if the regular expression fails (because it shouldn’t), and restarts without the restriction of matching only app calls if there are none in the stack before main.

I mostly decided to blog this because I didn’t initially spot any good examples of using NSRegularExpression to extract capture groups. The answer was to use firstMatchInString:options:range: to get a NSTextCheckingResult and then replacementStringForResult:inString:offset:template:.

@interface NSString (backtraceOfDepth_fromStackSymbols)
+ (NSString *)appBacktraceOfDepth:(int)depth fromStackSymbols:(NSArray *)frames;
+ (NSString *)backtraceOfDepth:(int)depth fromStackSymbols:(NSArray *)frames;
+ (NSString *)backtraceOfDepth:(int)depth fromStackSymbols:(NSArray *)frames matching:(NSString *)from;
@end
@implementation NSString (backtraceOfDepth_fromStackSymbols)
+ (NSString *)appBacktraceOfDepth:(int)depth fromStackSymbols:(NSArray *)frames { return [self backtraceOfDepth:depth fromStackSymbols:frames matching:[[NSBundle mainBundle] objectForInfoDictionaryKey:(NSString *)kCFBundleNameKey]]; }
+ (NSString *)backtraceOfDepth:(int)depth fromStackSymbols:(NSArray *)frames { return [self backtraceOfDepth:depth fromStackSymbols:frames matching:nil]; }
+ (NSString *)backtraceOfDepth:(int)depth fromStackSymbols:(NSArray *)frames matching:(NSString *)from {
  NSRegularExpression *regex = nil;
  if (!NSClassFromString(@"NSRegularExpression") || !(regex = [NSRegularExpression regularExpressionWithPattern:@"[0-9]+ +(.+[^ ]) +0x[0-9a-f]+ (.+) \\+ [0-9a-f]+" options:0 error:nil]))
    return [[frames objectAtIndex:1] substringFromIndex:51]; // no regex, be lame and rely on column counts
  for (int goodframes=0, framenum=1; goodframes < depth && framenum < [frames count]; ++framenum) {
    NSString *frame = [frames objectAtIndex:framenum];
    NSTextCheckingResult *match = [regex firstMatchInString:frame options:0 range:NSMakeRange(0, [frame length])];
    if (!match)
      NSAssert1(NO, @"unparsed stack frame: %@", frame);
    if (from && ![from isEqualToString:[regex replacementStringForResult:match inString:frame offset:0 template:@"$1"]])
      continue;
    ++goodframes;
    NSString *caller = [regex replacementStringForResult:match inString:frame offset:0 template:@"$2"];
    if (from && goodframes == 1 && [caller isEqualToString:@"main"])
      return [self backtraceOfDepth:depth fromStackSymbols:frames matching:nil]; // no useful calls from us, take ones from anyone instead
    result = !result ? caller : [result stringByAppendingFormat:@" < %@", caller];
  }
  return result ? result : @"?";
}
@end
Advertisements

One Comment on “Cocoa terse backtrace using NSRegularExpression”

  1. smallduck says:

    (commenting on my own blog post instead of editing it) Oh, if pre-iOS4 then [NSThread callStackSymbols] doesn’t work anyway. That make the test to see if NSRegularExpression is present a little moot, but no harm.

    I now wrap this in a macro that tests for callStackSymbols, evaluating to an empty string when it’s not present, and when it is, [NSString backtraceOfDepth appBacktraceOfDepth:2 fromStackSymbols:[NSThread callStackSymbols]]


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s