The Problem
The ScriptingBridge API is an excellent way to tap into the internals of OS X applications with little effort. However, it does have its drawbacks. Not the least of which being lazy evaluation of SBObject's attributes. Lazy evaluation makes the retrieval of SBObject's very efficient since all of the objects attributes are not retrieved at the same time. However, if you need to sort those objects by a particular key (say the name of a movie) the latency of ScriptingBridge becomes glaringly obvious to the point of becoming unusable.
The Solution
We can solve this problem using a combination of NSCache and NSProxy. NSProxy provides a relatively simple API for wrapping NSObject's and intercepting messages sent to them. By doing so, you can interecept the message and if it's been previously called, retrieve the value from an NSCache.
The Implementation
For our example, we’ll use iTunes. To do so we first have to generate an iTunes objc header file using sdef and sdp.
% sdef /Applications/iTunes.app | sdp -fh --basename iTunes
This produces a header file called "iTunes.h" that we include in our project to instantiate the iTunes SBApplication object.
This is what our source looks like so far:
NSArray* getTracks() { iTunesApplication *itunes; itunes = [SBApplication applicationWithBundleIdentifier:@"com.apple.iTunes"]; // Retrieve a list of movies in our iTunes library. NSArray *sources = [[itunes sources] get]; NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name == 'Library' && kind == %i", iTunesESrcLibrary]; NSArray *libs = [sources filteredArrayUsingPredicate:predicate]; NSMutableArray *theMovies = [NSMutableArray array]; NSArray *playlists; NSArray *movieLists; for (iTunesSource *source in libs) { playlists = [source playlists]; movieLists = [playlists filteredArrayUsingPredicate: [NSPredicate predicateWithFormat:@"name == 'Movies'"]]; for (iTunesPlaylist *playlist in movieLists) { for (iTunesTrack *track in [playlist tracks]) { [theMovies addObject:track]; } } } return theMovies; };
The iTunesTrackProxy Object
Not terribly interesting, but now to the fun part. We create a proxy object to wrap the tracks we retrieved in our getTracks method and act as an intermediary to any requests for track attributes.
For our purposes we need to override three of the NSProxy objects methods to handle NSInvocations sent to the track object.
- - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel
- - (void)forwardInvocation:(NSInvocation *)invocation
- - (BOOL)respondsToSelector:(SEL)aSelector
- (BOOL)respondsToSelector:(SEL)aSelector { return [self.track respondsToSelector:aSelector]; }
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { NSMethodSignature *sig; sig = [self.track methodSignatureForSelector:sel]; return sig; }
The real action
The real action happens in the forwardInvocation. This is where we catch message invocations intended for the iTunesTrack object and return the values we want from the sources we want (e.g. NSCache). Since I’m not covering the uninteresting boiler plate code in detail, here's our iTunesTrackProxy header and implementation source so far:
#import <Foundation/Foundation.h> @class iTunesTrack; @interface iTunesTrackProxy : NSProxy { @private iTunesTrack *_track; NSCache *_cache; } - (id)initWithTrack:(iTunesTrack *)track; @property(readonly) iTunesTrack *track; @end
#import "iTunesTrackProxy.h" #import "iTunes.h" @implementation iTunesTrackProxy - (id)initWithTrack:(iTunesTrack *)track { _track = [track retain]; _cache = [[NSCache alloc] init]; return self; } - (void)dealloc { [_track release]; [_cache release]; [super dealloc]; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel { NSMethodSignature *sig; sig = [self.track methodSignatureForSelector:sel]; return sig; } - (BOOL)respondsToSelector:(SEL)aSelector { return [self.track respondsToSelector:aSelector]; } - (void)forwardInvocation:(NSInvocation *)invocation { } @synthesize track = _track; @end
First attempt at the forwardInvocation method
A naive first attempt might look similar to this:
- (void)forwardInvocation:(NSInvocation *)invocation { // Using the string representation of the selector as the NSCache key. NSString *key = NSStringFromSelector([invocation selector]); // First check and see if we've already cached the object id result = [_cache objectForKey:key]; // If the object is cached, use it as the returnValue if (result) { [invocation setReturnValue:&result]; } else { // if not cached, forward it to the track object, then cache the return [invocation invokeWithTarget:self.track]; [invocation getReturnValue:&result]; [_cache setObject:result forKey:key]; } }
To resolve this oversight we create a few more methods.
- - (void)setValueFromTrack:(NSInvocation *)invocation
- - (void)setValueFromCacheObj:(id)obj invocation:(NSInvocation *)inv
- - (id)mapBuffer:(voidPtr)buffer type:(NSString *)type
- - (void *)mapObject:(id)obj key:(NSString *)key
Final implementation
Let's walk through each of our new methods and then the final implementation of the forwardInvocation method.
setValueFromTrack
If the result is not found in the cache, this method retrieves it from track itself. If it's an NSObject, it stores the value directly in the NSCache. If it's a primitive, it passes the value to the mapBuffer:type method to retrieve an appropriate object for storing in the NSCache.
- (void)setValueFromTrack:(NSInvocation *)invocation { NSString *key = NSStringFromSelector([invocation selector]); NSString *returnType = [NSString stringWithUTF8String:[[invocation methodSignature] methodReturnType]]; id result; // retrieve value from track object [invocation invokeWithTarget:self.track]; // if object is of type NSObject. if ([returnType isEqualToString:@"@"]) { [invocation getReturnValue:&result]; if (!result) { // retrieved value does not exist for track // set to default result = @"None"; [invocation setReturnValue:&result]; } [_cache setObject:result forKey:key]; } else { void* buffer; NSUInteger length = [[invocation methodSignature] methodReturnLength]; buffer = (void*)malloc(length); [invocation getReturnValue:buffer]; id obj = [self mapBuffer:buffer type:returnType]; // done with buffer free(buffer); // cache value [_cache setObject:obj forKey:key]; } }
mapBuffer:type:
For iTunesTrack objects, NSNumber is perfectly adequate at storing all the primitives we're interested in storing. Also, to make mapping of buffers to void pointers easier, we create a union.
typedef union { char *c; int *i; short *s; long *l; long long *q; unsigned char *C; unsigned int *I; unsigned short *S; unsigned long *L; unsigned long long *Q; float *f; double *d; _Bool *B; void *v; } TRACKDATA;
- (id)mapBuffer:(voidPtr)buffer type:(NSString *)type { id obj; TRACKDATA data; data.v = buffer; if ([type isEqualToString:@"c"]) { obj = [NSNumber numberWithChar:*data.c]; } else if ([type isEqualToString:@"i"]) { obj = [NSNumber numberWithInt:*data.i]; } else if ([type isEqualToString:@"s"]) { obj = [NSNumber numberWithShort:*data.s]; } else if ([type isEqualToString:@"l"]) { obj = [NSNumber numberWithLong:*data.l]; } else if ([type isEqualToString:@"q"]) { obj = [NSNumber numberWithLongLong:*data.q]; } else if ([type isEqualToString:@"C"]) { obj = [NSNumber numberWithUnsignedChar:*data.C]; } else if ([type isEqualToString:@"I"]) { obj = [NSNumber numberWithUnsignedInt:*data.I]; } else if ([type isEqualToString:@"S"]) { obj = [NSNumber numberWithUnsignedShort:*data.S]; } else if ([type isEqualToString:@"L"]) { obj = [NSNumber numberWithUnsignedLong:*data.L]; } else if ([type isEqualToString:@"Q"]) { obj = [NSNumber numberWithUnsignedLongLong:*data.Q]; } else if ([type isEqualToString:@"f"]) { obj = [NSNumber numberWithFloat:*data.f]; } else if ([type isEqualToString:@"d"]) { obj = [NSNumber numberWithDouble:*data.d]; } else if ([type isEqualToString:@"B"]) { // The BOOL type is a special case, so we do a little dance here. BOOL val; if (*data.B) { val = YES; } else { val = NO; } obj = [NSNumber numberWithBool:val]; } else { // Raise an exception if we receive a data type we're not prepared // for... [NSException raise:@"Unhandled NSMethodSignature:methodReturnType:" format:@"NSMethodSignature:methodReturnType: %@", type]; } return obj; }
setValueFromCacheObj:invocation:
The reverse case is retrieving an object from the cache. We accomplish that by first checking if the invocation's returnType is an object and if not transforming our cache object into a primitive type with mapObject:type.
- (void)setValueFromCacheObj:(id)obj invocation:(NSInvocation *)inv { NSString *returnType = [NSString stringWithUTF8String:[[inv methodSignature] methodReturnType]]; if ([returnType isEqualToString:@"@"]) { [inv setReturnValue:&obj]; } else { void *buffer = [self mapObject:obj type:returnType]; [inv setReturnValue:buffer]; // done with buffer free(buffer); } }
mapObject:type:
The reverse mapping from object to buffer is done by the mapObject:type: method. It's pretty much just the inverse of the mapBuffer:type: method.
- (void *)mapObject:(id)obj type:(NSString *)type { void *buffer; if ([type isEqualToString:@"c"]) { buffer = (char *)malloc(sizeof(char)); *(char *)buffer = [obj charValue]; } else if ([type isEqualToString:@"i"]) { buffer = (int *)malloc(sizeof(int)); *(int *)buffer = [obj intValue]; } else if ([type isEqualToString:@"s"]) { buffer = (short *)malloc(sizeof(short)); *(short *)buffer = [obj shortValue]; } else if ([type isEqualToString:@"l"]) { buffer = (long *)malloc(sizeof(long)); *(long *)buffer = [obj longValue]; } else if ([type isEqualToString:@"q"]) { buffer = (long long *)malloc(sizeof(long long)); *(long long *)buffer = [obj longLongValue]; } else if ([type isEqualToString:@"C"]) { buffer = (unsigned char *)malloc(sizeof(unsigned char)); *(unsigned char *)buffer = [obj unsignedCharValue]; } else if ([type isEqualToString:@"I"]) { buffer = (unsigned int *)malloc(sizeof(unsigned int)); *(unsigned int *)buffer = [obj unsignedIntValue]; } else if ([type isEqualToString:@"S"]) { buffer = (unsigned short *)malloc(sizeof(unsigned short)); *(unsigned short *)buffer = [obj unsignedShortValue]; } else if ([type isEqualToString:@"L"]) { buffer = (unsigned long *)malloc(sizeof(unsigned long)); *(unsigned long *)buffer = [obj unsignedLongValue]; } else if ([type isEqualToString:@"Q"]) { buffer = (unsigned long long *)malloc(sizeof(unsigned long long)); *(unsigned long long *)buffer = [obj unsignedLongLongValue]; } else if ([type isEqualToString:@"f"]) { buffer = (float *)malloc(sizeof(float)); *(float *)buffer = [obj floatValue]; } else if ([type isEqualToString:@"d"]) { buffer = (double *)malloc(sizeof(double)); *(double *)buffer = [obj doubleValue]; } else if ([type isEqualToString:@"B"]) { _Bool val; if ([obj boolValue]) { val = true; } else { val = false; } buffer = (_Bool *)malloc(sizeof(_Bool)); *(_Bool *)buffer = val; } else { [NSException raise:@"Unhandled NSMethodSignature:methodReturnType:" format:@"NSMethodSignature:methodReturnType: %@", type]; } return buffer; }
Summary & Benchmarks
That pretty much wraps it up. We can use our iTunesProxy object as a drop in replacement for the iTunesTrack object and it will cache all values for that track. This vastly improves query intensive operations such as sorting.
In fact, we'll use sorting as a demonstration on what we've gained from our work. The following output is from the main function attached to the article.
We retrieved 254 unproxied movies We retrieved 254 proxied movies Time elapsed for track sort: -7.293006 Time elapsed for proxy sort: -0.567359Not the fastest sort in the world, but we did get a 13x improvement bringing the performance from "application breaking" to perfectly workable. Of course, most of the overhead in that second run is from the initial caching of the track values. Let’s see what we get on a second run of our proxy array:
Time elapsed for proxy sort second run: -0.017478
That's more like it! After the initial hit when caching our variables, we are
rewarded with a 365x increase in sorting speed.
It's easy to see how this pattern is extendible outside the scope of iTunes tracks or even ScriptingBridge to any situation where the cost of retrieval far out weighs a minor hit to memory.
Full Source Files
For the write up on why I chose Rol Wheels over the many other custom and commercial options for wheels, see my original post My experience as a newbie cyclist buying his first set of non stock wheels. .
The Unboxing
Though packed in nothing but cardboard, the wheels were well protected and secure. It couldn’t have withstood a pallet of cinder blocks being dropped on them or anything, but appeared perfectly suitable for normal shipping bumps and bangs.
The wheels themselves looked absolutely sexy. Polished with nice looking decals. A quick ping test on spoke tension gave an equal tone for all spokes. I didn’t put them up on a truing rack right off the bat, but a visual check on the mounted wheels didn’t indicate any warping. In hindsight, I should have driven them up to the shop if only for a reference on the 300 mile followup. As others have mentioned on many of the Rol Wheels reviews on roadbikereview.com , the only negative as far as construction might be the plastic pokers for the hubs. But, in my book, this is a minor detail.
The Ride
Well, of course the real test of a new wheel set is when they’re doing what their meant to do…rolling! And I couldn’t have had a better impression of the Volants at that task. They spin up well and corner great. Now, as I mentioned in my original post I’m a newbie so I’m likely not pushing the absolute limits of my bike. However, I did seem to notice a difference between the Volants and my stock Mavic Open Sport’s with shimano 105 hubs. They seemed to hold the line a bit better without flex on tight turns. It could all be in my head or maybe I was just really trying to push the new wheels and just got more out of them because of that. Bottom line is, no dissappointment on that front.
The only possible negative to the wheels is the hubs are of the noisy variety when freewheeling. It doesn’t bother me, but if such things
do
bother you it’s something to take into consideration. Personally, in some ways I even like it. The only time I freewheel is when I’m slowing for other riders or pedestrians on bike paths and the audible click usually lets them know I’ve rolled up behind them. However, the noise is low enough to be drowned out by the wind for the most part.The 300 Mile Followup
To make one thing clear off the bat, after 300+ miles of riding I don’t expect a new set of wheels to remain true. . . even if they’ve been pre-stressed. You should always do a followup check on your new wheels to make sure they held tru after teh first few weeks of riding. As such, none of the adjustments I note below in the followup should be taken as a slight against the quality of Rol Wheels.
After approximately 300 to 400 miles of road time, I brought the wheels into my local bike coop Bikerowave, free to all Santa Monica College students! While there I threw the wheels up on a truing stand.
The rear wheel had become dished toward the side with the shorter spoke angle. One thing I found odd is that it was perfectly dished, by which I mean they were evenly dished arround the entire wheel. This is something that the naked eye likely wouldn’t catch on a quick spin check unless you were paying close attention to the spacing between the wheel and the rear fork near the bottom bracket. This is why I wished I’d taken the time to bring them to the shop out of the box to see if they came from the factory like that. If they had, it may be an indication that one of Sean’s truing stands is dished. However, it’s not a big deal to correct which is exactly what I did with a few 1/4 turns around the circumference of the wheel.
The front wheel was still very, very close to tru. I tightened it up a bit bringing it to within 1mm which is good enough for government work.
Final Verdict
So, the final verdict is I’m very happy with the new wheel set. If given the choice to do over I’d make the same decision. The weight to price to performance ratio is stellar and when I called with questions Sean, the owner, was very responsive (picked up on the first ring).
So, I’ve been rocking stock Open Sport’s for about 10 or 11 thousand kilometers and they’ve about had it. Couple of good dings and not really holding true anymore. Time for some new wheels.
The Contenders
I looked at a few commercial wheel options and at four wheel builders:
Bycycle Wheel Warehouse
Bicycle Wheel Warehouse looks like they pull from a subset of commercial components and have their own in-house line called Pure (hubs) and Blackset Race (rims). I noticed their address was on my daily bike route in Huntington Beach…but they don’t have a walk in store. I also ran into mixed satisfaction with customer service (mainly responsiveness) in reviews and forum discussions (these and others). Though the majority seem happy with the wheels themselves. I attempted to contact the shop, but no one answered. Left a message, but received no return call. This being my first wheel set I’d prefer a bit more hand holding/accessibility.
My other contention with BWW was a certain level of uncertainty I wasn’t quite comfortable with. There was very vocal support on the Road Bike Review forums (whom BWW sponsors). However, it mainly comes from a few people. There were more than a few anecdotal, positive stories. But a deep review history of their products does not exist.
None of these would have been deal breakers. BWW’s prices are quite good and they’re somewhat local despite the lack of a store front. However, in combination they put me off as a new buyer. Frankly, if I could name one thing that was the deterrant it would be responsiveness. If they’d have answered or returned my call within a day or so, they’d have probably made a sale.
Predator Cycling
So, Predator Cycling only deals with high end. They’re local (Los Angeles) and have a walk in shop that I’ve been fit in. Their main focus is building custom bikes from the ground up, wheels being part of that. So I was still tempted. But their wheels started at around 900-1200 a set which was a bit more than I wanted to pay. Was really impressed with Aram’s customer service and the fit feels quite good. He also said he’d work with me with any wheels I picked up.
I liked the feel of the shop and Aram seemed very passionate about his work. When it’s time to upgrade my bike, I’ll seriously consider purchasing it from them. No negatives with Predator Cycling whatsoever, just a bit out of my range.
Wheel Builder
Wheel Builder is an outfit located in El Monte, CA. which is semi-local for me at approximately 30 miles away. They were recommended by Aram at predator as a solid wheel builder and I have no reason to doubt that recommendation. The only negative was that they were a bit pricey for the components being built. Not a deal breaker in and of itself, after all they are custom built wheels. But a few hundred dollars is still a few hundred dollars.
Main stream options
No need to really cover these in detail. Shiimano, DT, WI, etc. They’re solid, they’re known, there’s plenty of information out there. Most in the price range I was looking at are factory built. Easton would be one exception, but I’ve received so many anecdotal reports of busted spokes, loose hubs, etc. that I was hesitant to dive in.
Having worked in plants and factories before, there was one thing I was sure of. When it comes to precision work, a hand built product done by someone who cares about their work is vastly superior to assembly line fabrication. So I was leaning toward a hand built wheel by a small shop.
Rol Wheels
Rol Wheels is based in Austin, TX. which has a lively and strong cycling scene. They’ve been a sponsor of Road Bike Review for quite some time. They’ve been in the Road Bike Review “Best of XX Year” list for 4 years in a row with over 150 reviews non of which are below a 4.5….that caught my attention A recurring theme amongst reviewers and forum commenters was the outstandingly, over the top customer service. That held my attention.
In contrast to BWW whose feedback and users seemed to mainly congregate around Road Bike Review, I was able to find discussions of Rol Wheels in many other forums, blogs, and a couple of magazines. Peloton Magazine (formally testrider.com) also covered the line. The greater presence could simply be indicative of better marketing. Regardless, it was nice to have a more diverse set of sources. Finally their prices are quite good and fell squarely within my price range.
As a final test, I decided to see if the raves about customer service could hold water. I called up Rol first thing in the morning. Sean, the owner, picked up on the second ring. He asked my weight and my intended uses (durability, reliability, dual purpose train/race). He said I was light enough to ride any of their wheelsets regularly, even the 20/24, 1478g D"Huez. However, the Volant R/T is their most robust line weighing in at 1655g with 24/28 spoke configuration. I mentioned that I wanted something that could take a bit of abuse and not come away like a taco. To which he replied that if I never irreparably dent the rim, they’d be happy to repair it for $60 dollars…so a busted rim doesn’t result in a trip to the savings account.
That’s the sort of feedback that inspires purchasing confidence. It may be superficial, but talking to a human being that sounds excited about their work and generally eager to make you a customer without trying to upsell is the kind of interaction that wins customers and creates loyalty.
The Winner
I went with Rol Wheels (could probably tell that already) and I plinked down for the Volant R/T. I decided as a newbie that cutting 2268 grams off the waste for free was a cheaper way to lighten my bike than cutting 177 grams for $250 dollars. They’re still a lot lighter than the Open Sports I’m using now.
I’ll make an unboxing post when I receive them with first impressions and, of course, a final impression after I put a few thousand kilometers on them.
I keep a lot of my user installed applications in /Applications/User Utilities. Keeps some modicum of organization and reduces a bit of clutter in my /Applications directory. After moving MacVim to this directory, I began getting several errors when opening files. They were all of the same type.
E172: Only one file name allowedWith occurrances on several lines in the runtime/syntax/vim.vim file. Opening the file, I found syntax like the following
exe "syn include @vimPerlScript ".s:perlpathYou may notice the perpath variable is where the problem lies. the space gives the appearance of two arguments instead of a single path. We can prevent this by using fnameescape
For me, these errors occurred on lines 585, 607, and 628.
- Line 585
exe "syn include @vimPerlScript ".s:perlpath
becomesexe "syn include @vimPerlScript ".s:fnameescape(perlpath)
- Line 607
exe "syn include @vimRubyScript ".s:rubypath
becomesexe "syn include @vimRubyScript ".s:fnameescape(rubypath)
- Line 628
exe "syn include @vimPythonScript ".s:pythonpath
becomesexe "syn include @vimPythonScript ".s:fnameescape(pythonpath)
All done.
Ran across a very nice, brief run down of creating user profiles for django on Turnkey Linux.
I was particularly pleased of this "ism" that creates user profiles on the fly on an as needed basis. As a bonus, it provides access to the profiles through a user.profile property. All you have to do is place the following property declaration at the bottom of your profile’s models.py.
User.profile = property(lambda u: UserProfile.objects.get_or_create(user=u)[0])
And that’s it. Now, when someuser.profile is called, it’ll create a default profile if needed or return that user’s profile if it exists. See the original article for a full User Profile quickstart.

