Monday, August 27, 2007

Simple, synchronous AIR configuration

I've spent some time the last couple of weeks poking around with AIR. The Adobe Integrated Runtime promises to let you build desktop applications using Javascript. Basically, it gives you Webkit and a series of APIs you can access through the window.runtime object that provide all sorts of things like access to the file system, drag and drop, and a SQL Lite database. You can also use Flash and Flex, but I'm not smart enough for that. I ended up trying to build a persistent key/value configuration system and quickly ran into a frustrating reality of AIR's SQL API - everything is asynchronous. This makes using the DB for storing configuration frustratingly convoluted.

You have a simple config table - key:string, value:string. Say you want to read the value for the key "foo". You end up with code that looks something like this:


var stmt = new air.SQLStatement();
stmt.sqlConnection = conn;
stmt.text = "select value from config where key = :key";
stmt.parameters[":key"] = "foo";
stmt.addEventListener(air.SQLEvent.RESULT, function(){
var fooValue = stmt.getResults().data[0];
doStuffWithFoo...
});
stmt.execute();


Which makes me throw up in my mouth a little bit. I want to do something like this:


var fooValue = getConfig("foo");
doStuffWithFoo...


But that damned async call just keeps getting in the way.

Adobe hasn't committed to building this yet "because of the development effort it would require". My thoughts on that can be summed up in my previous post: "Programmers are expensive, CPUs are cheap" does not justify rapid development of slow code.

So, I wrote some convoluted code, scream "fuck this shit!" a couple of time, and wrote a little utility to provide a synchronous API - the Big Dumb Javascript AIR Configuration. To install, include Configuration.js:


<script src="Configuration.js"></script>


You then have to initialized the configuration system.


BigDumbDev.Configuration.init( function(){ air.trace('configuration ready'); } );


Reading and writing configs goes like this:


BigDumbDev.Configuration.getConfig('key');

BigDumbDev.Configuration.setConfig('key', 'value');

BigDumbDev.Configuration.removeConfig('key');


Sets and removes are immediately available, so you can do this:


BigDumbDev.Configuration.setConfig('foo','bar');
var foo = BigDumbDev.Configuration.getConfig('foo');
// foo is bar


You can see all the details in the source code. There is no magic - it still does the DB calls asynchronously. I cache the config values and read from the cache. Update change the cache and then kick of the async call to update the database.

You can download a sample AIR app that utilizes the system.

Wednesday, August 22, 2007

Dynamic image resizing

I found this on wonko this morning. That's pretty awesome. I wonder how long before I can use that with image magick...

Tuesday, August 14, 2007

Web development and the iPhone revisited

I read a great post over at Ajax Performance defending AJAX as the iPhone development environment. This reminded me of my own take during the iPhone brouhaha, and I decided I had a little more to say.

Web development is has come a long way, but building a local app still has a lot of advantages. But, hey, when the first cars came out, horses were still a lot better. Apple didn't decide on AJAX because you can build cooler apps - they decided on it because it is the future, it is where the talent goes, and because . Google has managed to become the most valuable brand in the world without an OS and a native API.

Apple is obviously committed to this - they were willing to not bothering with an native API. Time spent with a Cocoa iPhone is time not spent making the web better.

Sunday, August 12, 2007

Build a better skimmer - part 2

We saw in part 1 that we can build a more efficient skimming implementation by using a "film strip" image instead of a bunch of single images. For my example, I built the filmstrip by hand - it sucked. So, I whipped up a quick ruby script for creating a filmstrip of all the jpg's in a directory. If you don't already have RMagick installed, you can find instructions for installing it on the .

The script first crops the images to 160x160, then creates a vertical strip using RMagick's montage functionality.


require 'rubygems'
require 'RMagick'

files = Dir['*.jpg']
imageList = Magick::ImageList.new
numImages = 0
files.each do |file|
unless file == 'filmStrip.jpg'
puts "adding #{file}"
img = Magick::Image.read(file)[0]
img.crop_resized!(160,160)
imageList << img
numImages = numImages + 1
end
end

filmStrip = imageList.montage do
self.geometry = "160x160+0+0"
self.tile = "1x#{numImages}"
end

filmStrip.write('filmStrip.jpg')

Friday, August 10, 2007

Build a better gallery skimmer - part 1


Apple's web developers could do better. This is the first entry in a big dumb effort to implement a faster and more efficient version of Apple's new skimming functionality than their current .mac gallery implementation.

You have probably seen Apple's new . This is really cool. Surprisingly, though, the implementation is rather pedestrian. 15 seconds with firebug and it's clear - Apple sure doesn't care about doing this efficiently. Maybe they saved a few weeks developing this, but that demo site takes almost 1 minute to load - on my PowerBook G4 over DSL. But, if they did care about speeding it up and just ran out of time, wouldn't they have at least stripped all the whitespace and comments out of their javascript? Here are the last 128 lines from the main gallery.js:



/*

require('core');

Gallery.WebWidgetPanel = Mac.FormView.extend({
isPanel: true,
outlets: ["widgetCode", "photo", "reflection", "flash", "lTitle", 'lInstructionalText', 'lCopySnippetLink', 'lDoneButton'],
widgetCode: ".widgetCode?",
widgetTag: "",

lTitle : Mac.LabelView.extend({localize:true}).outletFor(".lTitle?"),
lInstructionalText : Mac.LabelView.extend({localize:true}).outletFor(".lInstructionalText?"),
lCopySnippetLink : Mac.LabelView.extend({localize:true}).outletFor(".lCopySnippetLink?"),
lDoneButton : Mac.LabelView.extend({
localize:true,
toolTip: "_VisitorExperience.AlbumOptions.WebWidget.Done.Button.Tooltip"
}).outletFor(".lDoneButton?"),


flash: ".flash?",

getSmallest: function() {
if (Gallery.albumController.get("videoSmall"))
{
return Gallery.albumController.get("videoSmall");
}
else
if (Gallery.albumController.get("videoMedium"))
{
return Gallery.albumController.get("videoMedium");
}
else
{
return Gallery.albumController.get("videoLarge");
}


},

generateData: function ()
{

if(Gallery.detailView().isVideo && Gallery.detailView().showOnIndex) {
var height= Gallery.albumController.get("thumbImageHeight") + 16;
var width = 320;
var noScript = false;
var widgetID = 'galleryWidget_' + Math.floor(Math.random() * 100000);
var widgetAlbumTitle = Gallery.albumController.get("title");
var widgetAlbumURL = Gallery.albumController.get("url");
var widgetPosterImg = Gallery.albumController.get("medium");
var widgetMovieURL = this.getSmallest();
var styleTxt ="<style type=\"text/css\" media=\"screen\">.widget_header a { color: white ! important; display: block; text-align: center; font-size: 11px ! important; font-family: \"Helvetica Neue\", helvetica; text-decoration:none ! important; background:transparent url('http://gallery.mac.com/g/flash/gall_link_arrow.png') no-repeat scroll top right; padding-top: 3px; padding-bottom: 0px; padding-right: 16px; margin-right: 0px; height: 12px; padding-left: 5px; text-overflow: ellipsis; overflow: hidden; } .widget_header:hover a { text-decoration: underline ! important; color: white; background-image: url('http://gallery.mac.com/g/flash/gall_link_arrow_on.png'); } .widget_header { overflow: hidden; text-overflow: ellipsis; background: black url('http://gallery.mac.com/g/flash/gallery_widget_bg.png') repeat-x scroll top center; height: 20px; width: 100%; display: block; top: 0px; z-index: 4; cursor: pointer; } .widget_header a{ } .movie_div { display:block; z-index: 1; position: absolute; top: 20px; left: 0px; cursor: pointer; background-color: #000; } .movie_play_badge { width: 40px; height: 30px; position: absolute; left: 50%; margin-left: -20px; display: block; top: 50%; margin-top: -23px; background: transparent url('http://gallery.mac.com/g/flash/movie_play_bg.png') no-repeat top center; filter: progid:DXImageTransform.Microsoft.AlphaImageLoader(src='http://gallery.mac.com/g/flash/movie_play_bg.png'); } .qt_ctrls { background: transparent url('http://gallery.mac.com/g/flash/qt_control_left.gif') no-repeat top left; height: 16px; text-align: right; display:block; margin: 0px; }</style>";

var htmlTxt = '<div id=\"'+ widgetID+ '\" class=\"shared_movie\" style=\"text-align: center; width: ' + width + 'px; display:block; height: '+ (height + 22) + 'px; position: relative;\"><div class=\"widget_header\"><img src=\"http://gallery.mac.com/g/flash/gallery_logo.png\" width=\"21\" height=\"19\" alt=\".Mac Gallery\" style=\"float: left\" /><a title=\"' + widgetAlbumTitle + '\" href=\"'+ widgetAlbumURL + '\" target=\"_blank\">' + widgetAlbumTitle + '</a></div><div id=\"movieDiv\" class=\"movie_div\" ><a id=\"movieLink\" class=\"movie_link\" href=\"'+ widgetAlbumURL + '/' + widgetMovieURL + '\" target=\"_blank\"><img id=\"playBadge\" class=\"movie_play_badge\" src=\"http://gallery.mac.com/g/flash/movie_play_badge.png\" border=\"0\" width=\"40\" height=\"30\" alt=\"Play Movie\" /><img class=\"movie_poster\" id=\"moviePoster\" src=\"' + widgetPosterImg + '" width=\"320\" alt=\"\" border=\"0\" style=\"padding: 0px; margin: 0px\"/></a><div class=\"qt_ctrls\" id=\"qtCtrls\"><img src=\"http://gallery.mac.com/g/flash/qt_ctrl_right.gif\" width=\"57\" height=\"16\" alt=\"\" ></div></div></div>';


var scriptTxt = "<script src=\"http://gallery.mac.com/g/flash/gallery_moviewidget.js\" type=\"text/javascript\" charset=\"utf-8\"></script><script type=\"text/javascript\" charset=\"utf-8\"> initWidget('"+ widgetAlbumURL +"','" + widgetMovieURL + "', 320, "+ height +", '"+ widgetID +"', true);</script>";

var source = Gallery.albumController.get("path");

this.flash.innerHTML = styleTxt + htmlTxt;

this.widgetTag = styleTxt + htmlTxt + scriptTxt;
this.widgetCode.value = this.widgetTag;

var links = this.flash.getElementsByTagName('a');
for (i=0; i<links.length; i++){
links[i].removeAttribute('href');
links[i].setStyle({cursor: 'default'});
}

} else {
this.widgetTag = this.createSnippetCode();
this.flash.innerHTML = this.createSnippetCode(true);
this.widgetCode.value = this.createSnippetCode();
}
},

createSnippetCode: function(previewBool) {
var height = 260;
var width = 320;
var source = Gallery.albumController.get("path");
if (previewBool) {
previewTxt = 'preview=true&';
} else {
previewTxt = '';
}
var flashvars = previewTxt + 'feed=' + source + '?webdav-method=truthget&depth=infinity&feedfmt=atom&widgetWidth=' + width + '&widgetHeight=' + height;
var widgetTag;
widgetTag = '<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" codebase="http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=8,0,0,0" width="'+width+'" height="'+height+'" id="photowidget" align="middle">';
widgetTag += '<param name="allowScriptAccess" value="always" />';
widgetTag += '<param name="movie" value="http://' + HOST + '/g/flash/photowidget.swf?def" />';
widgetTag += '<param name="quality" value="high" />';
widgetTag += '<param name="bgcolor" value="#000000" />';
widgetTag += '<param name="wmode" value="opaque" />';
widgetTag += '<param name="flashVars" value="' + flashvars + '" />';
widgetTag += '<embed src="http://' + HOST + '/g/flash/photowidget.swf?abab" quality="high" bgcolor="#000000" width="' + width + '" height="' + height + '" wmode="opaque" name="photowidget" align="middle" allowScriptAccess="always" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" flashVars="' + flashvars + '" />';
widgetTag += '</object>';
return widgetTag;



},

copyToClipboard: function()
{
var flashcopier = 'flashcopier';
if(!document.getElementById(flashcopier)) {
var divholder = document.createElement('div');
divholder.id = flashcopier;
document.body.appendChild(divholder);
}
document.getElementById(flashcopier).innerHTML = '';
var divinfo = '<embed src="/g/flash/_clipboard.swf" FlashVars="clipboard='+escape(this.widgetTag)+'" width="0" height="0" type="application/x-shockwave-flash"></embed>';
document.getElementById(flashcopier).innerHTML = divinfo;
new Effect.Highlight(this.widgetCode, {duration: .5, startcolor: "#70b0ff", endcolor: "#ffffff" });
},

commitForm: function() {
this.set("isVisible", false);
this.style.display = '';
}

});

*/


That's right, it's one big comment!


Next up, images. It requests a lot of them. A LOT of them. Here is a screenshot from firebug:



That's right, 137 images. Most of these (let's say 100) are 160x160 images for making the skim gallery effect. Looking into the gallery code, the image is swapped by changing the src on an image. This approach has some drawbacks:


  • You're eating your net latency once for each image (divided by ~4, as the browser will request more than one at a time). Even if you're latency is only 100ms, you've got 100 images / 4 threads * 100ms = 2500ms. So, two and a half seconds eaten up in overhead. This gets worse the farther away you are from the server (light is only so fast, after all).

  • You can do better than changing the source



A common web developer tool is to use one bigger image as a background to a smaller element and shift the position of the background to achieve some effect. Example: . We can use this here. Instead of 20 images for each gallery, create one image per gallery that's 160px x 3200px, then shift it to display whatever image we want. So, for 5 galleries, you're downloading 5 images instead of 100. This will cut out a lot of latency.

But is it faster? Well, how about an example. I took 5 gallery images and constructed a film strip image. I built a test page/script - same 5 image - one method uses the filmstrip and background offset, the other uses an image src.

Here is the example. The top image uses the film strip approach. The bottom uses Apple's mechinism.

I used firebug's profiler to compare the performance:



big dumb dev - .23ms.41ms average
apple - 2.1ms average

So, better than 4x improvement.

And this is where part 1 ends.

Next up - part 2 : how to build a film strip image.

Don't use "new Array()"

Check out these two lines of code:


var foo = new Array();
var bar = [];


They do the same thing. So, save yourself 9 characters, and use [].

Dynamic table search/filter using a single input

I've seen a lot of tables with column filters - each column has a filter. This is the Excel Model. My buddy Travis asked me if I had ever seen it done with one search box - the iTunes Model. Well, I haven't, and some time spent searching around didn't turn up anything. Honestly, I didn't search that hard, because I wanted to build it myself.

Here is the end product. Note, I have only tested this in Firefox and Safari. Update: tested in IE.

So, how do you build it?

First, the behavior:

  • The table to be searched has an arbitrary number of columns.

  • There is one text box for searching

  • Searching is dynamic - start typing and it starts filtering

  • When you search, each word you search for has to appear in at least one column in a row, or that row is hidden

  • All the words you search for don't have to appear in each column.



It's late, so I'll leave the rest of the details for now.

Thursday, August 2, 2007

Crapcasts

I stumbled onto a "great" screencast for preloading images in javascript today. These "crapcasts" are like puzzles in the Sunday paper - how many problems can you find? I'll leave that to the comments.