Starter Step

Archive for October 2009

I spent some time this week and wrote some small plugins. I’ll do a small write for each to explain the purpose and usage. The first plugin is the proxy_field plugin. For Bidbuddy (personal project) I only have about 10 models, but for each model there are a ton of fields that each represent a time duration. For example a field may represent how long a specific aircraft is used for a given schedule. The time is a duration so I have to store the value in seconds in the database. What I found was that I had helpers that would convert those column values in to hours, minutes, days, etc. While this works, it doesn’t feel right. I would rather just get the value of the column and call to_hours on it. I could open the Integer class and add my to_hours method to it, but I would like something a bit more generic that could be reused for other situations. What I came up with was this syntax:

class Block < ActiveRecord::Base
  proxy_field [:tduty, :tblk], :as => DurationField
end

class DurationField
  def initialize(seconds)
    @seconds = seconds
  end

  def to_hours
    @seconds / 60 / 60
  end

  #Other useful methods would go here
  #to_seconds, to_minutes, to_days, to_weeks, etc...
end

This allows me to proxy any ActiveRecord field into another object. It basically allows you to deserialize any column data.

Old Way:

  def seconds_to_hours(seconds)
    seconds / 60 / 60
  end

  b = Block.find(CONDITIONS_HERE)
  puts seconds_to_hours(b.tduty)

New Way:

  b = Block.find(CONDITIONS_HERE)
  puts b.tduty.to_hours

Install:

  ./script/plugin install git://github.com/angelo0000/proxy_field.git

I have not decided yet how to handle nil columns. Its not a technical challenge but more of a design decision. The plugin could return nil when you called the method to get the proxy: b.tduty would return nil. Or that could be up to the proxy object class to return nil for each method if the inializer got a nil. In my duration example to_hours method would nil if @seconds was nil. I’m not sure what I would prefer. I would love a suggestion…

Brian and I have been working on improving the startup of TripCase and have found some interesting items. Our startup on TripCase slow…eventually we think we will move to a more facebook style model where much of your data is stored locally on the device. This will allow us to show the main view of TripCase without the 6-N seconds of time to talk to the server first. While the page loads with the cached data we can asyncronously retrieve the live data and update the view….but I digress.

We were debugging our message stream on the main view and noticed that the first cell was taking almost a full second to create. All subsequent cells were taking on the order of .15 – .20 seconds. After some NSLogs here and there we finally tracked down the culprit! In our app we implemented a “time ago” feature that tells you relatively how long ago something occurred. It is a nice feature and lets users see that something was created a few minutes ago, a few hours ago, etc. Its pretty granular and makes for a good experience. The times we convert to timeago are given to the client through our server and are all in GMT. This means that in timeago we need to create a NSDateFormatter with a mask that includes timezone. Our poor performing code looked something like the following:

+(NSString *)timeAgo:(NSString *)_date {
	static NSDateFormatter *parser = nil;
	if (parser == nil) {
		parser = [[NSDateFormatter alloc] init];
                [parser setLocale:[[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"] autorelease]];      
		[parser setFormatterBehavior:NSDateFormatterBehavior10_4];
		[parser setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss z"];
	}
	NSDate *date = [parser dateFromString:[_date stringByAppendingString:@" GMT"]];
	double littleOverAWeek = 11520.0f;
	NSDate *now = [NSDate date];
	double deltaMinutes = fabs([date timeIntervalSinceDate:now]) / 60.0f;
	if(deltaMinutes < littleOverAWeek){
		return [NSString stringWithFormat:@"%@ ago", [self distanceOfTimeInWords:deltaMinutes]];
	} else {
		return [NSString stringWithFormat:@"on %@", [DateFormatter shortDateFromDate:date]];
	}
}

In the above method, we create a NSDateFormatter with a mask of “yyyy-MM-dd’T’HH:mm:ss z”. Seems pretty harmless right? Wrong! Brian and I removed the “z” and timed the creation of the NSDateFormatter. We saw that with the “z” in the mask, the creation of the NSDateFormatter was .7 seconds slower!! 700 milleseconds is a lifetime. We saw that during startup we create two NSDateFormatters that were doing this…that is 1.5 seconds of wasted time that we wanted to remove.

In the docs we noticed that you can set the timezone for the NSDateFormatter explicitly. We did that and the .7 seconds of invocation time went away…BAM! We just shaved 1.5 seconds from our startup time. Our new code looks something like this:

+(NSString *)timeAgo:(NSString *)_date {
	static NSDateFormatter *parser = nil;
	if (parser == nil) {
		parser = [[NSDateFormatter alloc] init];
                [parser setLocale:[[[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"] autorelease]];       
		[parser setFormatterBehavior:NSDateFormatterBehavior10_4];
		[parser setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss"];
		[parser setTimeZone:[NSTimeZone timeZoneWithName:@"GMT"]];
	}
	NSDate *date = [parser dateFromString:_date];
	double littleOverAWeek = 11520.0f;
	NSDate *now = [NSDate date];
	double deltaMinutes = fabs([date timeIntervalSinceDate:now]) / 60.0f;
	if(deltaMinutes < littleOverAWeek){
		return [NSString stringWithFormat:@"%@ ago", [self distanceOfTimeInWords:deltaMinutes]];
	} else {
		return [NSString stringWithFormat:@"on %@", [DateFormatter shortDateFromDate:date]];
	}
}

Two things to notice in the above code:
1. We set the timezone context directly on the NSDateFormatter which allowed us to remove the “z” from the mask.
2. Doing this also allowed us to remove the ugly code that was appending GMT to the time returned from the server.

All of our timings were done on a 3G device. In all we shaved roughly 1.5 seconds because this pattern was used in two places during startup. Score 1 for super sleuthing!



  • Dave: I can tell you're a ruby guy because you forgot the 'return' keyword. Thanks for the tip though!
  • Chandrashekhar H M: Hi, Thanks its working fine in iOS 6 but not in iOS 7.0. Any Suggestion on this.
  • Coeur: To change a rootViewController, without all this TVNavigationController : myNewRoot = [[UIViewController alloc] init]; myNavigationController.view