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;
@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"]])
    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 : @"?";

Found my TTPhotoView bug

TTPhotoView bug ExampleThis is a tip for anyone hacking TTPhotoViewController, and specifically TTPhotoView. If you’re finding swiping only rotating between three photos, and if there’s a mix of landscape and portrait then it gets a redraw bug that looks like this:

then you’ve probably disabled updateLayer (perhaps because it wasn’t working for photos with orientations other than Up) but didn’t replace it with [self setNeedsDisplay]. You’re welcome.
Also, for anyone else trying to untie TTPhotoView from TTURLCache and instead work with images you can’t load asynchronously: don’t. Instead make it work with fake URLs, force-feeding the cache as you go. Hey ALAssetLibrary, thanks for being async in all the wrong places.