undefinedbehavior.net

Ruby timestamp throw-down: Strftime vs Stamp

I’m sure every programmer can think of something they do on a regular basis that, for whatever reason, they are absolutely incapable of memorizing. For me, that would be the various arguments to ruby’s strftime.

Well, after complaining about that very thing on Twitter, another developer helpfully pointed me to a ruby gem he had authored called Stamp. Stamp is an strftime replacement that thinks and behaves like you or I would: In plain old English.

t = Time.now
 => 2013-02-02 14:15:17 -0500 
t.strftime("%-I:%M %p, %b %d, %Y")
 => "2:15 PM, Feb 02, 2013"
t.stamp("1:00 AM, Jan 01, 2013")
 => "2:15 PM, Feb 02, 2013" 

I admit I was pretty blown away by what this gem can do. I love the idea of coding my date strings in such plain and obvious terms. I did, however, have one worry: How much performance am I giving up by using such a tool?

So I decided to find out. I whipped up a short and simple test to compare the speed of the two methods. My plan was as follows:

  • Calculate the time needed to generate X number of timestamps using strftime.
  • Perform the above test many times and calculate an average value.
  • Do the same again, this time using stamp.
  • Compare the strftime average with the stamp average.

And here are the results I came to. The number passed in is the number of iterations used in the test. Note that these tests were performed on a MacBook Pro with all other applications closed; Your results may vary from mine.

gregwoods$ ruby stamped.rb 10
## Results:
- strftime:  0.07 ms
- stamp:     0.19 ms
- diff:      +0.12 ms
- increase:  290.52%
gregwoods$ ruby stamped.rb 100
## Results:
- strftime:  0.6 ms
- stamp:     1.94 ms
- diff:      +1.34 ms
- increase:  324.97%
gregwoods$ ruby stamped.rb 1000
## Results:
- strftime:  5.95 ms
- stamp:     19.02 ms
- diff:      +13.07 ms
- increase:  319.45%

So across the board we can see an increase of around 300%. That may sound like a big number, but if you consider that the times we are dealing with are quite small to begin with, it doesn’t turn out to be all that bad. At 100 timestamps per page you would only be looking at an additional 1.34 milliseconds to your page request, much of which you could probably offset by using a proper caching mechanism.

When I think about stuff like this, I think about luxury car features. Nobody really needs power windows, heated seats, a stereo, or bluetooth connectivity. In fact, many of the higher-end sports cars ship without such amenities, claiming the loss of weight leads to improved performance. But when it comes to daily-driving practicality, I think most of us would agree that not having to hand-roll our windows up and down is more than worth the microscopic loss in driving speed.

So does this mean you should use the Stamp gem in your projects? Well, I guess the answer is “it depends”. Does the task at hand call for a sports car, or will a cushy daily-driver get the job done just as well? While I don’t see Stamp becoming a standard presence in my Gemfile, I’ll definitely be keeping it in my parts bin for when the situation is appropriate.

Scheduled Rake tasks via Launch Daemons

Here is something that has come in handy for me a handful of times in the past 6 months. Say you are running a Rails app, and you have a rake task that you’d like to run on a recurring basis. On a *nix based web server you would probably jump straight to the crontab - but what if your server is a Mac?

While OS X supports cron, the preferred way to handle such tasks is via Launch Daemons. So how does one do this exactly?

Finding Your Copy of Rake

Before we can go setting up any daemons, we will need to know where your copy of rake resides.

RVM Users: Create a wrapper executable using the rvm wrapper command. Then type which wrappername_rake to get the path to your wrapper.

gregs-macbook:undefined_behavior gregwoods$ rvm wrapper 1.9.3 launchctl
Saving wrappers to '/Users/gregwoods/.rvm/bin'.
gregs-macbook:undefined_behavior gregwoods$ which launchctl_rake
/Users/gregwoods/.rvm/bin/launchctl_rake

The wrapper does two things: It loads up your RVM environment, and then it calls to the actual rake exectutable wherever it happens to be installed.

Everybody Else: Simply type which rake and take note of the path. Done!

Create Your Plist

Create an empty text file under /Library/LaunchDaemons, and name it using the reverse domain naming convention with a .plist extension. The filename should roughly correlate to the application and task at hand.

In a recent example, I had a rake task that was in charge of downloading sales data from a remote server and importing it into a mysql database. So I’ll call my file net.undefinedbehavior.my_app.sales.plist.

Launch Daemon plists are a simple list of configuration key/value pairs in XML format. The available options can be found on this man page. Here is what my file looks like before adding much configuration - note how the the Label value matches the file name minus the extension.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>net.undefinedbehavior.my_app.sales</string>
</dict>
</plist>

Now let’s start filling in the values. First and perhaps most importantly, ProgramArguments defines a string of shell commands used to execute the rake task. Arguments are passed in as a array; As a general rule of thumb, every block of text seperated by a space should be entered as an item in the array. I’m using the path to rake here that I looked up earlier.

<key>ProgramArguments</key>
<array>
  <string>/Users/gregwoods/.rvm/bin/launchctl_rake</string>
  <string>-f</string>
  <string>/srv/sites/myapp.undefinedbehavior.net/Rakefile</string>
  <string>myapp:sales:refresh</string>
</array>

For reference, the above would translate into /Users/gregwoods/.rvm/bin/launchctl_rake -f /srv/sites/myapp.undefinedbehavior.net/Rakefile myapp:sales:refresh on the command line. With me so far?

Now I’d like to be able to track the output of this task, so I’m going to define a standard output and standard error log. For convenience I’ll go ahead and send both to the same file, which I’ll place in my applications /log directory.

<key>StandardOutPath</key>
<string>/srv/sites/myapp.undefinedbehavior.net/log/launchctl.log</string>
<key>StandardErrorPath</key>
<string>/srv/sites/myapp.undefinedbehavior.net/log/launchctl.log</string>

And now I’d like to schedule the job to happen at the start of every hour.

<key>StartCalendarInterval</key>
<dict>
  <key>Minute</key>
  <integer>0</integer>
</dict>

Times segments not included in the interval are considered to be wild cards. The fact that I left out the Hour key/value means this job will run every hour on the given minute.

Finally we have the full configuration, with a few other minor options tossed in for good measure. I would highly encourage you to look up the remaining configs in the man page to find out what they do.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>net.undefinedbehavior.my_app.sales</string>
  <key>UserName</key>
  <string>deploy</string>
  <key>ProgramArguments</key>
   <array>
    <string>/Users/gregwoods/.rvm/bin/launchctl_rake</string>
    <string>-f</string>
    <string>/srv/sites/myapp.undefinedbehavior.net/Rakefile</string>
    <string>myapp:sales:refresh</string>
  </array>
  <key>StandardOutPath</key>
  <string>/srv/sites/myapp.undefinedbehavior.net/log/launchctl.log</string>
  <key>StandardErrorPath</key>
  <string>/srv/sites/myapp.undefinedbehavior.net/log/launchctl.log</string>
  <key>KeepAlive</key>
  <false/>
  <key>debug</key>
  <true/>
  <key>RunAtLoad</key>
  <true/>
  <key>OnDemand</key>
  <true/>
  <key>StartCalendarInterval</key>
  <dict>
    <key>Minute</key>
    <integer>0</integer>
  </dict>
  <key>Disabled</key>
  <false/>
</dict>
</plist>

Starting and Stopping Your Daemon

Now for the final step - loading the job! At your terminal enter the following command:

sudo launchctl load /Library/LaunchDaemons/net.undefinedbehavior.my_app.sales.plist

A few more useful commands:

# Unload the daemon:
sudo launchctl unload /Library/LaunchDaemons/net.undefinedbehavior.my_app.sales.plist
# Stop the daemon without unloading it
sudo launchctl stop net.undefinedbehavior.my_app.sales
# Start the daemon if it is stopped
sudo launchctl start net.undefinedbehavior.my_app.sales
# Find loaded daemons, view the most recent exit code for your daemon
sudo launchctl status

Try launchctl help for more useful options. And if you’re having trouble with a particular job, try tailing /var/log/system.log for relevant crash reports.

More Examples

Here is one more example launch daemon, this one designed to trigger a reindex of Thinking Sphinx every night at midnight.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>net.undefinedbehavior.my_app.reindex</string>
  <key>UserName</key>
  <string>deploy</string>
  <key>ProgramArguments</key>
   <array>
    <string>/Users/gregwoods/.rvm/bin/launchctl_rake</string>
    <string>-f</string>
    <string>/srv/sites/myapp.undefinedbehavior.net/Rakefile</string>
    <string>ts:reindex</string>
  </array>
  <key>KeepAlive</key>
  <false/>
  <key>debug</key>
  <true/>
  <key>RunAtLoad</key>
  <true/>
  <key>OnDemand</key>
  <true/>
  <dict>
    <key>Minute</key>
    <integer>0</integer>
    <key>Hour</key>
    <integer>0</integer>
  </dict>
  <key>Disabled</key>
  <false/>
</dict>
</plist>

Now go forth, and launch those daemons!

Trusting It'll Work

Back in freshmen year of college, when I was first figuring out what this whole programming thing was all about, I took a class the name of which I have long forgotten. What I do remember, however, was the name on the textbook that was required for the class: Discrete Mathematics.

It had a picture of rocks, stacked upon each other. Perfectly rounded and smooth, they rested in perfect balance like geological pancakes, waiting for a slight wind to push them toppling down to the ground.

This class sticks out to me because it was undeniably the weed-out class of the 100 level CSCI courses. Designed to scare off the weaker among the pack, it entailed hours upon hours of dreadful busy work including logic equations, riddles, and psuedo-coding puzzles.

When actual programming did finally make its way into our homework assignments, it was in a crazy old scripting language none of us had ever heard of. The OS X machines had to run it under Classic Mode, an emulation of OS 9.1

I recall one late night in the computer lab at the tail-end of the semester. The entire class was there, working desperately to figure out a solution to the final piece of homework for the year. The task: Given a string of numbers in a particular format, return a new string with the numbers sorted and grouped in a specific way.

It was during this endeavor that I learned perhaps one of the most important lessons of my schooling.

What I discovered that night, as we sat huddled around those old Macs, was the idea of trusting it’ll work. I remember thinking to myself: Man, I sure wish I could just do X. That’d make this a lot easier.

And that was when everything clicked for me. Just act like you already have a function that does X, I thought. Code as though the function already works, and worry about implementing it later.

Within the hour, I found myself the first person in class to turn in the assignment.

Looking back as I write this today it all seems so comically obvious. But the Greg of 2005 did not know what the Greg of 2013 knows. These sorts of revelatory moments always seem obvious it hindsight; It only seems that way now because of how absolutely true that revelation must have been.

Such a core concept can be applicable even for more experienced programmers. It is so easy to find ones self wrapped up in the minute details of an application - how will this database query be optimized, how will this feature be secure enough - that we can quickly lose the forest for the trees and forget about the product we wanted to build in the first place.

In 2013, I’m going to make an effort to be less worried about individual implementation details. I’m going to spend more time thinking about building and shipping real products for real people to use and enjoy. I’m going to write code with the faith that, when the time comes, it’ll work.

And if it doesn’t? I’ll go back and try again.

  1. I’d be thrilled if anyone could name that language. I really have no idea. I do not think it was AppleScript.