CINXE.COM
Uncategorized | code.flickr.com
<!DOCTYPE html> <!--[if IE 6]> <html id="ie6" lang="en"> <![endif]--> <!--[if IE 7]> <html id="ie7" lang="en"> <![endif]--> <!--[if IE 8]> <html id="ie8" lang="en"> <![endif]--> <!--[if !(IE 6) | !(IE 7) | !(IE 8) ]><!--> <html lang="en"> <!--<![endif]--> <!-- generated 38 seconds ago generated in 0.542 seconds served from batcache in 0.005 seconds expires in 262 seconds --> <head><script type="text/javascript" src="/_static/js/bundle-playback.js?v=HxkREWBo" charset="utf-8"></script> <script type="text/javascript" src="/_static/js/wombat.js?v=txqj7nKC" charset="utf-8"></script> <script>window.RufflePlayer=window.RufflePlayer||{};window.RufflePlayer.config={"autoplay":"on","unmuteOverlay":"hidden"};</script> <script type="text/javascript" src="/_static/js/ruffle/ruffle.js"></script> <script type="text/javascript"> __wm.init("https://web.archive.org/web"); __wm.wombat("http://code.flickr.net:80/category/uncategorized/","20130414051835","https://web.archive.org/","web","/_static/", "1365916715"); </script> <link rel="stylesheet" type="text/css" href="/_static/css/banner-styles.css?v=S1zqJCYt" /> <link rel="stylesheet" type="text/css" href="/_static/css/iconochive.css?v=3PDvdIFv" /> <!-- End Wayback Rewrite JS Include --> <meta charset="UTF-8"/> <meta name="viewport" content="width=device-width"/> <title>Uncategorized | code.flickr.com</title> <link rel="profile" href="http://gmpg.org/xfn/11"/> <link rel="stylesheet" type="text/css" media="all" href="https://web.archive.org/web/20130414051835cs_/http://s2.wp.com/wp-content/themes/vip/flickr-code/style.css?m=1345671377g"/> <link rel="pingback" href="http://code.flickr.net/xmlrpc.php"/> <!--[if lt IE 9]> <script src="http://s2.wp.com/wp-content/themes/pub/twentyeleven/js/html5.js?m=1354160568g" type="text/javascript"></script> <![endif]--> <script src="https://web.archive.org/web/20130414051835js_/http://r-login.wordpress.com/remote-login.php?action=js&host=code.flickr.net&id=39034126&t=1365916677&back=code.flickr.net%2Fcategory%2Funcategorized%2F" type="text/javascript"></script> <script type="text/javascript"> /* <![CDATA[ */ if ( 'function' === typeof WPRemoteLogin ) { document.cookie = "wordpress_test_cookie=test; path=/"; if ( document.cookie.match( /(;|^)\s*wordpress_test_cookie\=/ ) ) { WPRemoteLogin(); } } /* ]]> */ </script> <link rel="alternate" type="application/rss+xml" title="code.flickr.com » Feed" href="https://web.archive.org/web/20130414051835/http://code.flickr.net/feed/"/> <link rel="alternate" type="application/rss+xml" title="code.flickr.com » Comments Feed" href="https://web.archive.org/web/20130414051835/http://code.flickr.net/comments/feed/"/> <link rel="alternate" type="application/rss+xml" title="code.flickr.com » Uncategorized Category Feed" href="https://web.archive.org/web/20130414051835/http://code.flickr.net/category/uncategorized/feed/"/> <script type="text/javascript"> /* <![CDATA[ */ function addLoadEvent(func){var oldonload=window.onload;if(typeof window.onload!='function'){window.onload=func;}else{window.onload=function(){oldonload();func();}}} /* ]]> */ </script> <link rel="stylesheet" id="all-css-0" href="https://web.archive.org/web/20130414051835cs_/http://s0.wp.com/_static/??-eJydjt0OgjAMRl/I2Shi8ML4LGMWGHRbs3UsvL1o4pU/MV41X7+T00JhZYIX9AIuK6bcW5+A7IQJRhTWZlKPtDUpbeA9ziGJ6kjbCEkWwhdWBnSrkHMLUtbFgoQz+u/0bBk6smaKa3PFf9SqsAnu19cHHa3vn/PTpeEAPYVW0x24uPOuOtZ1ddo3zXgDFsl89A==" type="text/css" media="all"/> <script type="text/javascript" src="https://web.archive.org/web/20130414051835js_/http://s0.wp.com/_static/??-eJzTLy/QzcxLzilNSS3WzwKiwtLUokoopZdVrKOPT4FubmZ6UWJJKkxhcn5eSWpeCUhdQX5xSW5qcXFiOjZZVGsy88oyU8sJKstKLSlITM7WLUotzqwCmWqfa2tobGZqZmJgamKeBQCnOUtl"></script> <link rel="EditURI" type="application/rsd+xml" title="RSD" href="http://flickrcode.wordpress.com/xmlrpc.php?rsd"/> <link rel="wlwmanifest" type="application/wlwmanifest+xml" href="http://flickrcode.wordpress.com/wp-includes/wlwmanifest.xml"/> <meta name="generator" content="WordPress.com"/> <link rel="shortcut icon" type="image/x-icon" href="https://web.archive.org/web/20130414051835im_/http://1.gravatar.com/blavatar/341946154e8a7e5497473810e7ef560c?s=16" sizes="16x16"/> <link rel="icon" type="image/x-icon" href="https://web.archive.org/web/20130414051835im_/http://1.gravatar.com/blavatar/341946154e8a7e5497473810e7ef560c?s=16" sizes="16x16"/> <link rel="apple-touch-icon-precomposed" href="https://web.archive.org/web/20130414051835im_/http://0.gravatar.com/blavatar/8b1d73fba9c0d02a3e78929d8cecfd82?s=114"/> <link rel="openid.server" href="http://flickrcode.wordpress.com/?openidserver=1"/> <link rel="openid.delegate" href="http://flickrcode.wordpress.com/"/> <link rel="search" type="application/opensearchdescription+xml" href="https://web.archive.org/web/20130414051835/http://code.flickr.net/osd.xml" title="code.flickr.com"/> <link rel="search" type="application/opensearchdescription+xml" href="https://web.archive.org/web/20130414051835/http://wordpress.com/opensearch.xml" title="WordPress.com"/> <style> /* <![CDATA[ */ /* Block: reblog */ .reblog-from img { margin: 0 10px 0 0; vertical-align: middle; padding: 0; border: 0; } .reblogger-note img.avatar { float: left; padding: 0; border: 0; } .reblogger-note-content { margin: 0 0 20px; } .reblog-post .wpcom-enhanced-excerpt-content { border-left: 3px solid #eee; padding-left: 15px; } .reblog-post ul.thumb-list { display: block; list-style: none; margin: 2px 0; padding: 0; clear: both; } .reblog-post ul.thumb-list li { display: inline; margin: 0; padding: 0 1px; border: 0; } .reblog-post ul.thumb-list li a { margin: 0; padding: 0; border: 0; } .reblog-post ul.thumb-list li img { margin: 0; padding: 0; border: 0; } .reblog-post .wpcom-enhanced-excerpt { clear: both; } .reblog-post .wpcom-enhanced-excerpt address, .reblog-post .wpcom-enhanced-excerpt li, .reblog-post .wpcom-enhanced-excerpt h1, .reblog-post .wpcom-enhanced-excerpt h2, .reblog-post .wpcom-enhanced-excerpt h3, .reblog-post .wpcom-enhanced-excerpt h4, .reblog-post .wpcom-enhanced-excerpt h5, .reblog-post .wpcom-enhanced-excerpt h6, .reblog-post .wpcom-enhanced-excerpt p { font-size: 100% !important; } .reblog-post .wpcom-enhanced-excerpt blockquote, .reblog-post .wpcom-enhanced-excerpt pre, .reblog-post .wpcom-enhanced-excerpt code, .reblog-post .wpcom-enhanced-excerpt q { font-size: 98% !important; } /* ]]> */ </style> <meta name="application-name" content="code.flickr.com"/><meta name="msapplication-window" content="width=device-width;height=device-height"/><meta name="msapplication-task" content="name=Subscribe;action-uri=http://code.flickr.net/feed/;icon-uri=http://1.gravatar.com/blavatar/341946154e8a7e5497473810e7ef560c?s=16"/> <style type="text/css"> #site-title, #site-description { position: absolute !important; clip: rect(1px 1px 1px 1px); /* IE6, IE7 */ clip: rect(1px, 1px, 1px, 1px); } </style> <style id="syntaxhighlighteranchor"></style> <link rel="stylesheet" id="custom-css-css" type="text/css" href="https://web.archive.org/web/20130414051835cs_/http://s1.wp.com/?custom-css=1&csblog=2DMyG&cscache=6&csrev=103"/> </head> <body class="archive category category-uncategorized category-1 typekit-enabled two-column right-sidebar highlander-enabled highlander-light"> <div id="page" class="hfeed"> <header id="branding" role="banner"> <hgroup> <h1 id="site-title"><span><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/" title="code.flickr.com" rel="home">code.flickr.com</a></span></h1> <h2 id="site-description"></h2> </hgroup> <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/"> <img src="https://web.archive.org/web/20130414051835im_/http://flickrcode.files.wordpress.com/2012/09/code-flickr-com-drawn-header-grey-large.png" width="1000" height="157" alt=""/> </a> <div class="only-search with-image"> <form method="get" id="searchform" action="https://web.archive.org/web/20130414051835/http://code.flickr.net/"> <label for="s" class="assistive-text">Search</label> <input type="text" class="field" name="s" id="s" placeholder="Search"/> <input type="submit" class="submit" name="submit" id="searchsubmit" value="Search"/> </form> </div> <nav id="access" role="navigation"> <h3 class="assistive-text">Main menu</h3> <div class="skip-link"><a class="assistive-text" href="#content" title="Skip to primary content">Skip to primary content</a></div> <div class="skip-link"><a class="assistive-text" href="#secondary" title="Skip to secondary content">Skip to secondary content</a></div> <div class="menu-menu-container"><ul id="menu-menu" class="menu"><li id="menu-item-2084" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-2084"><a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/">Flickr</a></li> <li id="menu-item-2085" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-2085"><a href="https://web.archive.org/web/20130414051835/http://blog.flickr.net/">Flickr Blog</a></li> <li id="menu-item-2250" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-2250"><a href="https://web.archive.org/web/20130414051835/http://twitter.com/flickr">@flickr</a></li> <li id="menu-item-2086" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-2086"><a href="https://web.archive.org/web/20130414051835/http://twitter.com/flickrapi">@flickrapi</a></li> <li id="menu-item-2087" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-2087"><a href="https://web.archive.org/web/20130414051835/http://developer.flickr.com/">Developer Guidelines</a></li> <li id="menu-item-2088" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-2088"><a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/services/api/">API</a></li> <li id="menu-item-2089" class="menu-item menu-item-type-custom menu-item-object-custom menu-item-2089"><a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/jobs/">Jobs</a></li> </ul></div> </nav><!-- #access --> </header><!-- #branding --> <div id="main"> <section id="primary"> <div id="content" role="main"> <header class="page-header"> <h1 class="page-title">Category Archives: <span>Uncategorized</span></h1> </header> <nav id="nav-above"> <h3 class="assistive-text">Post navigation</h3> <div class="nav-previous"><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/category/uncategorized/page/2/"><span class="meta-nav">←</span> Older posts</a></div> <div class="nav-next"></div> </nav><!-- #nav-above --> <article id="post-2350" class="post-2350 post type-post status-publish format-standard hentry category-uncategorized tag-javascript tag-node-js tag-redis"> <header class="entry-header"> <h1 class="entry-title"><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/12/21/redis-global-locks-redux/" title="Permalink to Redis Global Locks Redux" rel="bookmark">Redis Global Locks Redux</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/12/21/redis-global-locks-redux/" title="7:15 am" rel="bookmark"><time class="entry-date" datetime="2012-12-21T07:15:52+00:00">December 21, 2012</time></a><span class="by-author"> <span class="sep"> by </span> <span class="author vcard"><a class="url fn n" href="https://web.archive.org/web/20130414051835/http://code.flickr.net/author/joshuaflickr/" title="View all posts by Joshua Cohen" rel="author">Joshua Cohen</a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p>In my <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/12/12/highly-available-real-time-notifications/">last post</a> I described how we use Redis to manage a global lock that allows us to automatically failover to a backup process if there was a problem in the primary process. The method described allegedly allowed for any number of backup processes to work in conjunction to pick up on primary failures and take over processing.</p> <p class="flickr-photo"><a title="Locks #1 from Christoph Kummer, on Flickr" href="https://web.archive.org/web/20130414051835/http://www.flickr.com/photos/kuemmi/3460993750/"><img alt="Locks #1" src="https://web.archive.org/web/20130414051835im_/http://farm4.staticflickr.com/3653/3460993750_7b81638f6d_b.jpg" width="1024" height="681"/></a><br/> <span class="caption"><a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/photos/kuemmi/3460993750/">Locks #1</a> by <a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/photos/kuemmi/">Christoph Kummer</a></span></p> <p>Thanks to an astute reader, it was pointed out that the code in the blog wouldn’t actually work as advertised:</p> <blockquote class="twitter-tweet"><p>@<a href="https://web.archive.org/web/20130414051835/https://twitter.com/heyjoshua">heyjoshua</a> I might be missing something but the code looks like it'll keep trying to acquire the lock, which it'll can't, due to the SETNX.— <br/>Nolan Caudill (@nolancaudill) <a href="https://web.archive.org/web/20130414051835/http://twitter.com/#!/nolancaudill/status/279814860227870720" data-datetime="2012-12-15T05:07:16+00:00">December 15, 2012</a></p></blockquote> <p> </p> <h2>The Problem</h2> <p><a href="https://web.archive.org/web/20130414051835/https://twitter.com/nolancaudill/">Nolan</a> correctly noticed that when the backup processes attempts to acquire the lock via SETNX, that lock key will already exist from when it was acquired by the primary, and thus all subsequent attempts to acquire locks will simply end up constantly trying to acquire a lock that can never be acquired. As a reminder, here’s what we do when we check back on the status of a lock:</p> <pre class="brush: jscript; gutter: false; title: ; notranslate" title=""> function checkLock(payload, lockIdentifier) { client.get(lockIdentifier, function(error, data) { // Error handling elided for brevity if (data !== DONE_VALUE) { acquireLock(payload, data + 1, lockCallback); } else { client.del(lockIdentifier); } }); } </pre> <p>And here’s the relevant bit from acquireLock that calls SETNX:</p> <pre class="brush: jscript; gutter: false; title: ; notranslate" title=""> client.setnx(lockIdentifier, attempt, function(error, data) { if (error) { logger.error(&quot;Error trying to acquire redis lock for: %s&quot;, lockIdentifier); return callback(error, dataForCallback(false)); } return callback(null, dataForCallback(data === 1)); }); </pre> <p>So, you’re thinking, how could this vaunted failover process ever actually work? The answer is simple: the code from that post isn’t what we actually run. The actual production code has a single backup process, so it doesn’t try to re-acquire the lock in the event of failure, it just skips right to trying to send the message itself. In the previous post, I described a more general solution that would work for any number of backup processes, but I missed this one important detail.</p> <p>That being said, with some relatively minor changes, it’s absolutely possible to support an arbitrary number of backup processes and still maintain the use of the global lock. The trivial solution is to simply have the backup process delete the key before trying to re-acquire the lock (or, technically acquire it anew). However, the problem with that becomes apparent pretty quickly. If there are multiple backup processes all deleting the lock and trying to SETNX a new lock again, there’s a good chance that a race condition could arise wherein one of backups deletes a lock that was acquired by another backup process, rather than the failed lock from the primary.</p> <h2>The Solution</h2> <p>Thankfully, Redis has a solution to help us out here: <a href="https://web.archive.org/web/20130414051835/http://redis.io/topics/transactions">transactions</a>. By using a combination of <a href="https://web.archive.org/web/20130414051835/http://redis.io/commands/watch">WATCH</a>, <a href="https://web.archive.org/web/20130414051835/http://redis.io/commands/multi">MULTI</a>, and <a href="https://web.archive.org/web/20130414051835/http://redis.io/commands/exec">EXEC</a>, we can perform actions on the lock key and be confident that no one has modified it before our actions can complete. The process to acquire a lock remains the same: many processes will issue a SETNX and only one will win. The changes come into play when the processes that didn’t acquire the lock check back on its status. Whereas before, we simply checked the current value of the lock key, now we must go through the above described Redis transaction process. First we watch the key, then we do what amounts to a check and set (albeit with a few different actions to perform based on the outcome of the check):</p> <pre class="brush: jscript; gutter: false; title: ; notranslate" title=""> function checkLock(payload, lockIdentifier, lastCount) { client.watch(lockIdentifier); client.multi() .get(lockIdentifier) .exec(function(error, replies) { if (!replies) { // Lock value changed while we were checking it, someone else got the lock client.get(lockIdentifier, function(error, newCount) { setTimeout(checkLock, LOCK_EXPIRY, payload, lockIdentifier, newCount); }); return; } var currentCount = replies[0]; if (currentCount === null) { // No lock means someone else completed the work while we were checking on its status and the key has already been deleted return; } else if (currentCount === DONE_VALUE) { // Another process completed the work, let’s delete the lock key client.del(lockIdentifier); } else if (currentCount == lastCount) { // Key still exists, and no one has incremented the lock count, let’s try to reacquire the lock reacquireLock(payload, lockIdentifier, currentCount, doWork); } else { // Key still exists, but the value does not match what we expected, someone else has reacquired the lock, check back later to see how they fared setTimeout(checkLock, LOCK_EXPIRY, payload, lockIdentifier, currentCount); } }); } </pre> <p>As you can see, there are five basic cases we need to deal with after we get the value of the lock key:</p> <ol> <li>If we got a null reply back from Redis, that means that something else changed the value of our key, and our exec was aborted; i.e. someone else got the lock and changed its value before we could do anything. We just treat it as a failure to acquire the lock and check back again later.</li> <li>If we get back a reply from Redis, but the value for the key is null, that means that the work was actually completed <strong>and</strong> the key was deleted before we could do anything. In this case there’s nothing for us to do at all, so we can stop right away.</li> <li>If we get back a value for the lock key that is equal to our sentinel value, then someone else completed the work, but it’s up to us to clean up the lock key, so we issue a Redis DEL and call our job done.</li> <li>Here’s where things get interesting: if the key still exists, and its value (the number of attempts that have been made) is equal to our last attempt count, then we should try and reacquire the lock.</li> <li>The last scenario is where the key exists but its value (again, the number of attempts that have been made) does not equal our last attempt count. In this case, someone else has already tried to reacquire the lock and failed. We treat this as a failure to acquire the lock and schedule a timeout to check back later to see how whoever did acquire the lock got on. The appropriate action here is debatable. Depending on how long your underlying work takes, it may be better to actually try and reacquire the lock here as well, since whoever acquired the lock may have already failed. This can, however, lead to premature exhaustion of your attempt allotment, so to be safe, we just wait.</li> </ol> <p>So, we’ve checked on our lock, and, since the previous process with the lock failed to complete its work, it’s time to actually try and reacquire the lock. The process in this case is similar to the above inasmuch as we must use Redis transactions to manage the reacquisition process, thankfully however, the steps are (somewhat) simpler:</p> <pre class="brush: jscript; gutter: false; title: ; notranslate" title=""> function reacquireLock(payload, lockIdentifier, attemptCount, callback) { client.watch(lockIdentifier); client.get(lockIdentifier, function(error, data) { if (!data) { // Lock is gone, someone else completed the work and deleted the lock, nothing to do here, stop watching and carry on client.unwatch(); return; } var attempts = parseInt(data, 10) + 1; if (attempts &gt; MAX_ATTEMPTS) { // Our allotment has been exceeded by another process, unwatch and expire the key client.unwatch(); client.expire(lockIdentifier, ((LOCK_EXPIRY / 1000) * 2)); return; } client.multi() .set(lockIdentifier, attempts) .exec(function(error, replies) { if (!replies) { // The value changed out from under us, we didn't get the lock! client.get(lockIdentifier, function(error, currentAttemptCount) { setTimeout(checkLock, LOCK_TIMEOUT, payload, lockIdentifier, currentAttemptCount); }); } else { // Hooray, we acquired the lock! callback(null, { &quot;acquired&quot; : true, &quot;lockIdentifier&quot; : lockIdentifier, &quot;payload&quot; : payload }); } }); }); } </pre> <p>As with checkLock we start out by watching the lock key, and proceed do a (comparitively) simplified check and set. In this case, we’ve “only” got three scenarios to deal with:</p> <ol> <li>If we’ve already exceeded our allotment of attempts, it’s time to give up. In this case, the allotment was actually exceeded in another worker, so we can just stop right away. We make sure to unwatch the key, and set it expire at some point far enough in the future that any remaining processes attempting to acquire locks will also see that it’s time to give up.</li> </ol> <p>Assuming we’re still good to keep working, we try and update the lock key within a MULTI/EXEC block, where we have our remaining two scenarios:</p> <ol start="2"> <li>If we get no replies back, that again means that something changed the value of the lock key during our transaction and the EXEC was aborted. Since we failed to acquire the lock we just check back later to see what happened to whoever did acquire the lock.</li> <li>The last scenario is the one in which we managed to acquire the lock. In this case we just go ahead and do our work and hopefully complete it!</li> </ol> <h2>Bonus!</h2> <p>To make managing global locks even easier, I’ve gone ahead and generalized all the code mentioned in both this and the previous post on the subject into a tidy little event based npm package: <a href="https://web.archive.org/web/20130414051835/https://github.com/yahoo/redis-locking-worker">https://github.com/yahoo/redis-locking-worker</a>. Here’s a quick snippet of how to implement global locks using this new package:</p> <pre class="brush: jscript; gutter: false; title: ; notranslate" title=""> var RedisLockingWorker = require(&quot;redis-locking-worker”); var SUCCESS_CHANCE = 0.15; var lock = new RedisLockingWorker({ &quot;lockKey&quot; : &quot;mylock&quot;, &quot;statusLevel&quot; : RedisLockingWorker.StatusLevels.Verbose, &quot;lockTimeout&quot; : 5000, &quot;maxAttempts&quot; : 5 }); lock.on(&quot;acquired&quot;, function(lastAttempt) { if (Math.random() &lt;= SUCCESS_CHANCE) { console.log(&quot;Completed work successfully!&quot;, lastAttempt); lock.done(lastAttempt); } else { // oh no, we failed to do work! console.log(&quot;Failed to do work&quot;); } }); lock.acquire(); </pre> <p>There’s also a few other events you can use to track the lock status:</p> <pre class="brush: jscript; gutter: false; title: ; notranslate" title=""> lock.on(&quot;locked&quot;, function() { console.log(&quot;Did not acquire lock, someone beat us to it&quot;); }); lock.on(&quot;error&quot;, function(error) { console.error(&quot;Error from lock: %j&quot;, error); }); lock.on(&quot;status&quot;, function(message) { console.log(&quot;Status message from lock: %s&quot;, message); }); </pre> <h2>More Bonus!</h2> <p>If you don’t need the added complexity if multiple backup processes, I also want to give credit to npm user <a href="https://web.archive.org/web/20130414051835/https://npmjs.org/~pokehanai">pokehanai</a> who took the methodology described in the original post and created a generalized version of the two-worker solution: <a href="https://web.archive.org/web/20130414051835/https://npmjs.org/package/redis-paired-worker">https://npmjs.org/package/redis-paired-worker</a>.</p> <h2>Wrapping Up</h2> <p>So there you have it! Coordinating work on any number of processes across any number of hosts couldn’t be easier! If you have any questions or comments on this, please feel free to follow up on <a href="https://web.archive.org/web/20130414051835/http://twitter.com/heyjoshua">Twitter</a>.</p> <div class="hiring-banner"> <p class="group-photo"><a title="Flickr flamily floto by morozgrafix, on Flickr" href="https://web.archive.org/web/20130414051835/http://www.flickr.com/photos/morozgrafix/7803402076/"><img alt="Flickr flamily floto" src="https://web.archive.org/web/20130414051835im_/http://farm9.staticflickr.com/8436/7803402076_c06f87bf1f_m.jpg" width="120" height="80"/></a></p> <p>Like this post? Have a love of online photography? Want to work with us? Flickr is hiring <strong>engineers</strong>, <strong>designers</strong> and <strong>product managers</strong> in our San Francisco office. <strong>Find out more at <a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/jobs/">flickr.com/jobs</a></strong>.</p> </div> <div id="jp-post-flair" class="sharedaddy sd-like-enabled sd-sharing-enabled"></div> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"> <span class="entry-utility-prep entry-utility-prep-cat-links">Posted in</span> <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/category/uncategorized/" title="View all posts in Uncategorized" rel="category tag">Uncategorized</a> </span> <span class="sep"> | </span> <span class="tag-links"> <span class="entry-utility-prep entry-utility-prep-tag-links">Tagged</span> <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/tag/javascript/" rel="tag">javascript</a>, <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/tag/node-js/" rel="tag">node.js</a>, <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/tag/redis/" rel="tag">redis</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-2350 --> <article id="post-2339" class="post-2339 post type-post status-publish format-standard hentry category-uncategorized"> <header class="entry-header"> <h1 class="entry-title"><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/12/12/highly-available-real-time-notifications/" title="Permalink to Highly Available Real Time Push Notifications and You" rel="bookmark">Highly Available Real Time Push Notifications and You</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/12/12/highly-available-real-time-notifications/" title="6:00 am" rel="bookmark"><time class="entry-date" datetime="2012-12-12T06:00:24+00:00">December 12, 2012</time></a><span class="by-author"> <span class="sep"> by </span> <span class="author vcard"><a class="url fn n" href="https://web.archive.org/web/20130414051835/http://code.flickr.net/author/joshuaflickr/" title="View all posts by Joshua Cohen" rel="author">Joshua Cohen</a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p>One of the goals of our recently launched (and awesome!) <a href="https://web.archive.org/web/20130414051835/http://flickr.com/iphone">new Flickr iPhone app</a> was to further increase user engagement on Flickr. One of the best ways to drive engagement is to make sure Flickr users know what’s happening on Flickr in as near-real time as possible. We already have email notifications, but email is no longer a good mechanism for real-time updates. Users may have many email accounts and may not check in frequently causing timeliness to go right out the window. Clearly this called for… PUSH NOTIFICATIONS!</p> <p class="flickr-photo"><a title="Motor bike racer getting a push start at the track, Brisbane from State Library of Queensland, Australia, on Flickr" href="https://web.archive.org/web/20130414051835/http://www.flickr.com/photos/statelibraryqueensland/8219930340/"><img alt="Motor bike racer getting a push start at the track, Brisbane" src="https://web.archive.org/web/20130414051835im_/http://farm9.staticflickr.com/8062/8219930340_2fce43a926_c.jpg" width="800" height="630"/></a><br/> <span class="caption"><a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/photos/statelibraryqueensland/8219930340/">Motor bike racer getting a push start at the track, Brisbane</a> by <a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/photos/statelibraryqueensland/">State Library of Queensland, Australia</a></span></p> <p>I know, you’re thinking, “anyone can build push notifications, we’ve been doing it since 2009!” Which is, of course, absolutely true. The <a href="https://web.archive.org/web/20130414051835/http://developer.apple.com/library/mac/#documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/ApplePushService/ApplePushService.html">process for delivering push notifications</a> is well trod territory by this point. So… let’s just skip all that boring stuff and focus on how we decided on the underlying architecture for our implementation. Our decisions focused on four major factors:</p> <ol> <li>Impact to normal page serving times should be minimal</li> <li>Delivery should be in near-real time</li> <li>Handle thousands of notifications per second</li> <li>The underlying services should be highly available</li> </ol> <h2>Baby Steps</h2> <p>Given these goals, we started by looking at systems we already have in place. Everyone loves not writing new code, right? Our thoughts immediately went to Flickr’s existing <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2011/06/30/dont-be-so-pushy/">PuSH infrastructure</a>. Our PuSH implementation is a great way to get an overview of relevant activity on Flickr, but it has limitations that made it unsuitable for powering mobile push notifications. The primary concern is that it’s less-near-real time than we’d like it to be. On average, activities occurring on Flickr will be delivered to a subscribed PuSH endpoint within one minute. That’s certainly better than waiting for an email to arrive or waiting until the next time you log in to the site and see your activity feed, but it’s not good enough for mobile notifications! This delay is due to some design decisions at the core of the PuSH system. PuSH is designed to aggregate activity and deliver a periodic digest and, because of this, it has a built in window to allow multiple changes to the same photo to be accumulated. PuSH is also focused on ensured delivery, so it maintains an up to date list of all subscribers. These features, which make PuSH great for the purpose it was designed, make it not-so-great for real time notifications. So, repurposing the PuSH code for reuse in a more real time fashion proved to be untenable.</p> <h2>Tentative Plans</h2> <p>So, what to do? In the end we wound up building a new lightweight event system that is broken up into three phases:</p> <ol> <li>Event Generation</li> <li>Event Targeting</li> <li>Message Delivery</li> </ol> <h3>Event Generation</h3> <p>The event generation phase happens while processing the response to a user request. As such, we wanted to ensure that there was little to no impact on the response times as a result. To ensure this was the case, all we do here is a lightweight write into a global <a href="https://web.archive.org/web/20130414051835/http://redis.io/">Redis</a> queue. We store the minimum amount of data possible, just a few identifiers, so we don’t have to make any extra DB calls and slow down the response just to (potentially) kick off a push notification. Everything after this initial Redis action is processed out of band by our deferred task system and has no impact on site performance.</p> <h3>Event Targeting</h3> <p>Next in the process is the event targeting phase. Here we have many workers reading from the global Redis queue. When a worker receives an event from the queue it rehydrates the data and loads up any additional information necessary to act on the notification. This includes checking to see what users should be notified, whether those users have devices that are registered to receive notifications, if they’ve opted out of notifications of this type, and finally if they’ve muted activity for the object in question.</p> <h3>Message Delivery</h3> <p>Flickr’s web-serving stack is PHP, and, up until now, everything described has been processed by PHP. Unfortunately, one area where PHP does not excel is long-lived processes or network connections, both of which make delivering push notifications in real time much easier. Because of this we decided to build the final phase, message delivery, as a separate endpoint in <a href="https://web.archive.org/web/20130414051835/http://nodejs.org./">Node.js</a>.</p> <p>So, the question arose: how do we get messages pending delivery from these PHP workers over to the Node.js endpoints that will actually deliver them? For this, we again turned to Redis, this time using its built in <a href="https://web.archive.org/web/20130414051835/http://redis.io/topics/pubsub">pub/sub</a> functionality. The PHP workers simply publish a message to a Redis channel with the assumption that there’s a Node.js process subscribed to that channel eagerly awaiting some data on which it can act.</p> <p>After that the Node process delivers the notification to Apple’s APNS push notification system. Communicating with APNS is a well-documented topic, and not one that’s particularly interesting. In fact, I can sum it up with a single link: <a href="https://web.archive.org/web/20130414051835/https://github.com/argon/node-apn">https://github.com/argon/node-apn</a>, a great npm package for talking to APNS.</p> <h2>The Real Challenge</h2> <p>There is, however, a much more interesting problem to discuss at this point: how do we ensure that delivery to APNS is both scalable and highly available? At first blush, this seems like it could be problematic. What if the Node.js worker has crashed? The message will just be lost to the ether! Solving this problem turned out to be the majority of the work involved in implementing push notifications.</p> <h3>Scalability</h3> <p>The first step to ensuring a service is scalable is to divide the workload. Since Node.js is single threaded, we would already be dividing the workload across individual Node.js processes anyway, so this works out well! When we publish messages to the Redis pub/sub channel, we simply publish to a sharded channel. Each Node.js process subscribes to some subset of those sharded channels, and so will only act on that subset of messages.</p> <p class="flickr-photo"><img class="aligncenter" alt="APNS, Redis Pub/Sub" src="https://web.archive.org/web/20130414051835im_/http://farm9.staticflickr.com/8224/8260965187_c5596e5b69_o.png" width="518" height="328"/></p> <p>Configuring our Node.js processes in this way makes it easy to scale horizontally. Whenever we need to add more processing power to the cluster, we can just add more servers and more shards. This also makes it easy to pull hosts out of rotation for maintenance without impacting message delivery: we simply reconfigure the remaining processes to subscribe to additional channels to pick up the slack.</p> <h3>Availability</h3> <p>Designing for high availability proved to be somewhat more challenging. We needed to ensure that we could lose individual Node processes, a whole server or even an entire data center without degrading our ability to deliver messages. And we wanted to avoid the need for a human in the loop — automatic failover.</p> <p>We already knew that we’d have multiple hosts running in multiple data centers, so the main question was how to get them coordinating with each other so that we would not lose messages in the event of an outage while also ensuring we would not deliver the same message multiple times. Our first thought experiment along these lines was to implement a relatively complex message passing scheme, where two hosts would subscribe to a given channel, one as the primary and one as the backup. The primary would pass a message to the backup saying that it was starting to process a message, and another when it completed. The backup would wait a certain amount of time to receive the first and then the second message from the primary. If a message failed to arrive, it would assume something had gone wrong with the primary and attempt to complete delivery to Apple’s push notification gateway.</p> <p class="flickr-photo"><img class="aligncenter" alt="Initial Failover Plan" src="https://web.archive.org/web/20130414051835im_/http://farm9.staticflickr.com/8083/8262034314_e1f5b85592_o.png" width="328" height="394"/></p> <p>This plan had two major problems: hosts had to be aware of each other and increasing the number of hosts working in conjunction raised the complexity of ensuring reliable delivery.</p> <p>We liked the idea of having one host serve as a backup for another, but we didn’t like having to coordinate the interaction between so many moving pieces. To solve this issue we went with a convention based approach. Instead of each host having to maintain a list of its partners, we just use Redis to maintain a global lock. Easy enough, right? Perhaps some code is in order!</p> <h2>Finally, some code!</h2> <p>First we create our Redis clients. We need one client for regular Redis commands we use to maintain the lock, and a separate client for Redis pub/sub commands.</p> <pre class="brush: jscript; gutter: false; title: ; notranslate" title=""> var redis = require("redis"); var client = redis.createClient(config.port, config.host); var pubsubClient = redis.createClient(config.port, config.host); </pre> <p>Next, subscribe to the sharded channel and set up a message handler:</p> <pre class="brush: jscript; gutter: false; title: ; notranslate" title=""> // We could be subscribing to multiple shards, but for the sake of simplicity we’ll just subscribe to one here pubsubClient.subscribe("notification_" + shard); pubsubClient.on("message", handleMessage); </pre> <p>Now, the interesting part. We have multiple Node.js processes subscribed to the same Redis pub/sub channel, and each process is in a different data center. Whenever any of them receive a message, they attempt to acquire a lock for that message:</p> <pre class="brush: jscript; gutter: false; title: ; notranslate" title=""> function handleMessage(channel, message) { // Error handling elided for brevity var payload = JSON.parse(message); acquireLock(payload, 1, lockCallback); } </pre> <p>Managing locks with Redis is made easy using the <a href="https://web.archive.org/web/20130414051835/http://redis.io/commands/setnx">SETNX</a> command. SETNX is a “set if not exists” primitive. From the Redis docs:</p> <blockquote cite="http://en.wikisource.org/wiki/I_have_just_been_shot"><p>Set key to hold string value if key does not exist. In that case, it is equal to SET. When key already holds a value, no operation is performed.</p></blockquote> <p>If we have multiple processes calling SETNX on the same key, the command will only succeed for the process that first makes the call, and in that case the response from Redis will be 1. For subsequent SETNX commands, the key will already exist, and the response from Redis will be 0. The value we try to set with SETNX keeps track of how many attempts have been made to deliver the message, initially set to one, this allows us to retry failed messages a predefined number of times before giving up entirely.</p> <pre class="brush: jscript; gutter: false; title: ; notranslate" title=""> function acquireLock(payload, attempt, callback) { var lockIdentifier = "lock." + payload.identifier; function dataForCallback(acquired) { return { "acquired" : acquired, "lockIdentifier" : lockIdentifier, "payload" : payload, "attempt" : attempt }; } // The value of the lock key indicates how many lock attempts have been made client.setnx(lockIdentifier, attempt, function(error, data) { if (error) { logger.error("Error trying to acquire redis lock for: %s", lockIdentifier); return callback(error, dataForCallback(false)); } return callback(null, dataForCallback(data === 1)); }); } </pre> <p>At this point our attempt to acquire the lock has either succeeded or failed, and our callback is invoked. What we do next depends on whether we managed to acquire the lock. If we did acquire the lock, we simply attempt to send the message. If we did not acquire the lock, then we will check back later to see if the message was sent successfully (more on this later):</p> <pre class="brush: jscript; gutter: false; title: ; notranslate" title=""> function lockCallback(error, data) { // Again, error handling elided for brevity if (data && data.acquired) { return sendMessage(data.payload, data.lockIdentifier, data.attempt === MAX_ATTEMPTS); } else if (data && !data.acquired) { return setTimeout(checkLock, LOCK_EXPIRY, data.payload, data.lockIdentifier); } } </pre> <p>Finally, it’s time to actually send the message! We do some work to process the payload into a form we can use to pass to APNS and send it off. If all goes well, we do one of two things:</p> <ol> <li>If this was our first attempt to send the message, we update the lock key in Redis to a sentinel value indicating we were successful. This is the value the backup processes will check for to determine whether or not sending succeeded.</li> <li>If this was our last attempt to send the message (i.e. the primary process failed to deliver and now a backup process is handling delivery), we simply delete the lock key.</li> </ol> <pre class="brush: jscript; gutter: false; title: ; notranslate" title=""> function sendMessage(payload, lockIdentifier, lastAttempt) { // Does some work to process the payload and generate an APNS notification object var notification = generateApnsNotification(payload); if (notification) { // The APNS connection is defined/initialized elsewhere apnsConnection.sendNotification(notification); if (lastAttempt) { client.del(lockIdentifier); } else { client.set(lockIdentifier, DONE_VALUE); } } } </pre> <p>There’s one final piece of the puzzle: checking the lock in the process that did not acquire it initially. Here we issue a Redis <a href="https://web.archive.org/web/20130414051835/http://redis.io/commands/get">GET</a> to retrieve the current value of the lock key. If the process that won the lock managed to send the message, this key should be set to a well known sentinel value. If so, we don’t have any work to do, and we can simply delete the lock. However, if this value is not set to that sentinel value, then something went wrong with delivery in the process that originally acquired the lock and we should step up and try to deliver the message from this backup process:</p> <pre class="brush: jscript; gutter: false; title: ; notranslate" title=""> function checkLock(payload, lockIdentifier) { client.get(lockIdentifier, function(error, data) { // Error handling elided for brevity if (data !== DONE_VALUE) { acquireLock(payload, data + 1, lockCallback); } else { client.del(lockIdentifier); } }); } </pre> <h2>Summing Up</h2> <p>So, there you have it in a nutshell. This method of coordinating between processes makes it very easy to adjust the number of processes subscribing to a given shard’s channels. There’s no need for any process subscribed to a channel to be aware of how many other processes are also subscribed. As long as we have at least two processes in separate data centers subscribing to each shard we are protected from all of the from the following scenarios:</p> <ul> <li>The crash of any individual Node.js process</li> <li>The loss of a single host running the Node.js processes</li> <li>The loss of an entire data center containing many hosts running the Node.js processes</li> </ul> <p>Let’s go back over our initial goals and see how we fared:</p> <ol> <li>Impact to normal page serving times should be minimal</li> </ol> <p>We accomplish this by minimizing the workload done as part of the normal browser-driven request/response processing. The deferred task system picks up from there, out of band.</p> <ol start="2"> <li>Delivery should be in near-real time</li> </ol> <p>Processing stats from our implementation show that time from user actions leading to event generation to message delivery averages about 400ms and is completely event driven (no polling).</p> <ol start="3"> <li>Handle thousands of notifications per second</li> </ol> <p>In stress tests of our system, we were able to process more than 2,000 notifications per second on a single host (8 Node.js workers, each subscribing to multiple shards).</p> <ol start="4"> <li>The underlying services should be highly available</li> </ol> <p>The availability design is resilient to a variety of failure scenarios, and failover is automatic.</p> <p>We hope you’re enjoying push notifications in the <a href="https://web.archive.org/web/20130414051835/http://flickr.com/iphone">new Flickr iPhone app</a>.</p> <h2>Addendum!</h2> <p>There was a minor problem with the code in this post when supporting more than two workers. For a full explanation of the problem and the solution, check out <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/12/21/redis-global-locks-redux/">Global Redis Locks Redux</a>.</p> <div class="hiring-banner"> <p class="group-photo"><a title="Flickr flamily floto by morozgrafix, on Flickr" href="https://web.archive.org/web/20130414051835/http://www.flickr.com/photos/morozgrafix/7803402076/"><img alt="Flickr flamily floto" src="https://web.archive.org/web/20130414051835im_/http://farm9.staticflickr.com/8436/7803402076_c06f87bf1f_m.jpg" width="120" height="80"/></a></p> <p>Like this post? Have a love of online photography? Want to work with us? Flickr is hiring <strong>engineers</strong>, <strong>designers</strong> and <strong>product managers</strong> in our San Francisco office. <strong>Find out more at <a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/jobs/">flickr.com/jobs</a></strong>.</p> </div> <div id="jp-post-flair" class="sharedaddy sd-like-enabled sd-sharing-enabled"></div> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"> <span class="entry-utility-prep entry-utility-prep-cat-links">Posted in</span> <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/category/uncategorized/" title="View all posts in Uncategorized" rel="category tag">Uncategorized</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-2339 --> <article id="post-2070" class="post-2070 post type-post status-publish format-standard hentry category-uncategorized tag-maps"> <header class="entry-header"> <h1 class="entry-title"><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/07/11/designing-an-osm-map-style/" title="Permalink to Designing an OSM Map Style" rel="bookmark">Designing an OSM Map Style</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/07/11/designing-an-osm-map-style/" title="9:35 pm" rel="bookmark"><time class="entry-date" datetime="2012-07-11T21:35:08+00:00">July 11, 2012</time></a><span class="by-author"> <span class="sep"> by </span> <span class="author vcard"><a class="url fn n" href="https://web.archive.org/web/20130414051835/http://code.flickr.net/author/rharmes/" title="View all posts by Ross Harmes" rel="author">Ross Harmes</a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p>With the recent <a href="/web/20130414051835/http://code.flickr.net/blog/2012/06/29/the-great-map-update-of-2012/">change to our map system</a>, we introduced a new map style for our <a href="https://web.archive.org/web/20130414051835/http://www.openstreetmap.org/">OSM</a> tiles. Since 2008, we’ve used the default OSM styles, which produces map tiles like this:</p> <p class="undersized-image-container"> <img src="https://web.archive.org/web/20130414051835im_/http://farm9.staticflickr.com/8159/7537002708_6db9e51663_o.png" width="526" height="396" alt=""> </p> <p>This style is extremely good at putting a lot of information in front of you. OSM doesn’t know your intended purpose for the maps (navigation, orientation, exploration, city planning, disaster response, etc.), so they err on the side of lots of information. This is good, but with the introduction of <a href="https://web.archive.org/web/20130414051835/http://mapbox.com/tilemill/">TileMill</a>, non-professional cartographers (like myself) can now easily change map styles to better suit our needs. Using TileMill, we decided to take a crack at designing a map that is better suited to Flickr.</p> <p>On Flickr, we use maps for a very specific purpose: to provide context for a photo. This means there are a lot of map features that we can leave out entirely. We can choose to hide features that are primarily used for navigation (ferry and train routes, bus stops) or for demarcation (city and county boundaries). Roads are useful as orientation tools, but certain road features (like exit numbers on highways) aren’t needed. In the end, we can reduce the data that the map shows to much smaller and more useful subset:</p> <p class="undersized-image-container"> <img src="https://web.archive.org/web/20130414051835im_/http://farm9.staticflickr.com/8281/7537002620_9ee5d84281_o.png" width="526" height="396" alt=""> </p> <p>This is the style provided by MapBox’s excellent <a href="https://web.archive.org/web/20130414051835/https://github.com/mapbox/osm-bright">OSM Bright</a>. As a starting point, this gets us a long way towards our goal of an unobtrusive yet still useful map. We made a few changes to OSM Bright and released them on GitHub as our <a href="https://web.archive.org/web/20130414051835/https://github.com/flickr/Pandonia">Pandonia</a> map style. Here are a few examples of the changes we made:</p> <ul> <li>Toned down the road, land, and water colors, to allow greater contrast with the pink and blue dots that we use as markers</li> <li>Reduced the density of road and highway names, as well as city, town and state names</li> <li>Removed underground tram and rail line</li> <li>Removed land use overlays for residential, commercial, and industrial zones, as well as parking lots</li> <li>Removed state park overlays that overlapped the water</li> </ul> <p>This is how it looks:</p> <p class="undersized-image-container"> <img src="https://web.archive.org/web/20130414051835im_/http://farm9.staticflickr.com/8002/7537002542_5cb7151bc0_o.png" width="526" height="396" alt=""> </p> <p>We tried a lot of different color combinations on the road to this style. Here is an animation of the different styles we tried, starting with OSM Bright.</p> <p class="undersized-image-container"> <img src="https://web.archive.org/web/20130414051835im_/http://farm8.staticflickr.com/7116/7537992486_d3b6c66d83_o.gif" width="500" height="471" alt="" style="margin-left:13px;"> </p> <p>Here it is zoomed in a bit more:</p> <p class="undersized-image-container"> <img src="https://web.archive.org/web/20130414051835im_/http://farm9.staticflickr.com/8167/7537992394_3db2018af5_o.gif" width="500" height="721" alt="" style="margin-left:13px;"> </p> <p>Over the next couple of weeks, we’ll be rolling out this style to all of the places where we use OSM tiles.</p> <p>These maps are still a work in progress. The world is a big place, and creating a unified style that works well for every single location is challenging. If you notice problems with our new map styles, <a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/help/forum/en-us/72157630333262094/">please let us know</a>!</p> <div id="jp-post-flair" class="sharedaddy sd-like-enabled sd-sharing-enabled"></div> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"> <span class="entry-utility-prep entry-utility-prep-cat-links">Posted in</span> <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/category/uncategorized/" title="View all posts in Uncategorized" rel="category tag">Uncategorized</a> </span> <span class="sep"> | </span> <span class="tag-links"> <span class="entry-utility-prep entry-utility-prep-tag-links">Tagged</span> <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/tag/maps/" rel="tag">maps</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-2070 --> <article id="post-2029" class="post-2029 post type-post status-publish format-standard hentry category-uncategorized tag-cloudmade tag-geo tag-geotagging tag-leaflet tag-mapbox tag-maps tag-openstreetmaps tag-osm tag-pandonia"> <header class="entry-header"> <h1 class="entry-title"><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/06/29/the-great-map-update-of-2012/" title="Permalink to The great map update of 2012" rel="bookmark">The great map update of 2012</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/06/29/the-great-map-update-of-2012/" title="12:11 am" rel="bookmark"><time class="entry-date" datetime="2012-06-29T00:11:35+00:00">June 29, 2012</time></a><span class="by-author"> <span class="sep"> by </span> <span class="author vcard"><a class="url fn n" href="https://web.archive.org/web/20130414051835/http://code.flickr.net/author/standardpixel/" title="View all posts by Eric Gelinas" rel="author">Eric Gelinas</a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p>Today we are announcing an update to the map tiles which we use site wide. A very high majority of the globe will be represented by Nokia’s clever looking tiles. </p> <p class="undersized-image-container"> <img src="https://web.archive.org/web/20130414051835im_/http://farm8.staticflickr.com/7257/7463259956_c87ec24638_o.png" alt="Nokia map tile"/> </p> <p>We are not stopping there. As some of you may know, Flickr has been using Open Street Maps (OSM) data to make map tiles for some places. <a href="https://web.archive.org/web/20130414051835/http://blog.flickr.net/en/2008/08/12/around-the-world-and-back-again/">We started with Beijing</a> and the list has grown to twenty one additional places:</p> <table border="0"> <tr> <td style="padding:20px;vertical-align:top;"> Mogadishu<br/> Cairo<br/> Algiers<br/> Kiev<br/> Tokyo<br/> Tehran </td> <td style="padding:20px;vertical-align:top;"> Hanoi<br/> Ho Chi Minh City<br/> Manila<br/> Davao<br/> Cebu<br/> Baghdad </td> <td style="padding:20px;vertical-align:top;"> Kabul<br/> Accra<br/> Hispaniola<br/> Havana<br/> Kinshasa<br/> Harare </td> <td style="padding:20px;vertical-align:top;"> Nairobi<br/> Buenos aires<br/> Santiago </td> </tr> </table> <p>It has been a while since <a href="https://web.archive.org/web/20130414051835/http://code.flickr.com/blog/2009/07/22/horseyes/">we last updated</a> our OSM tiles. Since 2009, the OSM community has advanced quite a bit in the tools they provide and data quality. I went into a little detail about this in a <a href="https://web.archive.org/web/20130414051835/http://www.yuiblog.com/blog/2012/02/22/video-eric-gelinas-geo/">talk I gave last year</a>. </p> <h2>Introducing Pandonia</h2> <p class="undersized-image-container"> <img src="https://web.archive.org/web/20130414051835im_/http://farm9.staticflickr.com/8154/7463259904_09a82a7ddf_o.png" alt="Nokia map tile"/> </p> <p>Today we are launching <a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/map?&fLat=-34.5652&fLon=-58.4694&zl=12">Buenos Aires</a> and <a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/map?&fLat=-33.463&fLon=-70.648&zl=12">Santiago</a> in a new style. We will be launching more cities in this new style in the near future. They are built from more recent OSM data and they will also have an entirely new style which we call <a href="https://web.archive.org/web/20130414051835/https://github.com/flickr/Pandonia">Pandonia</a>. Our new style was designed in <a href="https://web.archive.org/web/20130414051835/http://mapbox.com/tilemill/">TileMill</a> from the <a href="https://web.archive.org/web/20130414051835/https://github.com/mapbox/osm-bright">osm-bright</a> template, both created by the rad team at MapBox. TileMill changes the game when it comes to styling map tiles. The interface is developed to let you quickly iterate style changes to tiles and see the changes immediately. <a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/photos/rossharmes/">Ross Harmes</a> will be writing a more detailed account of the work he did to create the Pandonia style. We appreciate the tips and guidance from Eric Gunderson, Tom MacWright, and the rest of the team at MapBox</p> <p>We are looking forward to updating all of our OSM places with the Pandonia style in the near future and growing to more places after that… Antarctica? Null Island? The Moon? Stay tuned and see…</p> <h2>Changing our Javascript API</h2> <p>To host all of these new tiles we needed to find a flexible javascript api. <a href="https://web.archive.org/web/20130414051835/http://leaflet.cloudmade.com/">Cloudmade’s Leaflet</a> is a simple and open source tile serving javascript library. The events and methods map well to our previous JS API, which made upgrading simple for us. All of our existing map interfaces will stay the same with the addition of modern map tiles. They will also support touch screen devices better than ever. Leaflet’s layers mechanism will make it easier for us to blend different tile sources together seamlessly. We have a <a href="https://web.archive.org/web/20130414051835/https://github.com/flickr/Leaflet">fork on GitHub</a> which we plan to contribute to as time goes on. We’ll keep you posted.</p> <div id="jp-post-flair" class="sharedaddy sd-like-enabled sd-sharing-enabled"></div> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"> <span class="entry-utility-prep entry-utility-prep-cat-links">Posted in</span> <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/category/uncategorized/" title="View all posts in Uncategorized" rel="category tag">Uncategorized</a> </span> <span class="sep"> | </span> <span class="tag-links"> <span class="entry-utility-prep entry-utility-prep-tag-links">Tagged</span> <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/tag/cloudmade/" rel="tag">cloudmade</a>, <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/tag/geo/" rel="tag">geo</a>, <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/tag/geotagging/" rel="tag">geotagging</a>, <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/tag/leaflet/" rel="tag">leaflet</a>, <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/tag/mapbox/" rel="tag">mapbox</a>, <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/tag/maps/" rel="tag">maps</a>, <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/tag/openstreetmaps/" rel="tag">openstreetmaps</a>, <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/tag/osm/" rel="tag">osm</a>, <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/tag/pandonia/" rel="tag">pandonia</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-2029 --> <article id="post-1925" class="post-1925 post type-post status-publish format-standard hentry category-uncategorized tag-api tag-groups"> <header class="entry-header"> <h1 class="entry-title"><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/05/24/group-apis/" title="Permalink to Group APIs" rel="bookmark">Group APIs</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/05/24/group-apis/" title="6:05 pm" rel="bookmark"><time class="entry-date" datetime="2012-05-24T18:05:59+00:00">May 24, 2012</time></a><span class="by-author"> <span class="sep"> by </span> <span class="author vcard"><a class="url fn n" href="https://web.archive.org/web/20130414051835/http://code.flickr.net/author/jfanaian/" title="View all posts by jfanaian" rel="author">jfanaian</a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p>With over 1.5 million <a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/groups">groups</a>, it’s no doubt that they are an important part of Flickr. Today, we’re releasing a few new ways to interact with groups using our API.</p> <h3>Group Membership</h3> <p class="undersized-image-container"> <a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/photos/fofurasfelinas/369354361/" title="Cat meeting... by fofurasfelinas, on Flickr"><img src="https://web.archive.org/web/20130414051835im_/http://farm1.staticflickr.com/155/369354361_62d1976e82.jpg" width="500" height="333" alt="Cat meeting..."></a> </p> <p>We are adding two new methods to manage group membership through the API.</p> <p><a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/services/api/flickr.groups.join.html">flickr.groups.join</a> to join a group. Before calling this method, check if the group has rules using <a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/services/api/flickr.groups.getInfo.html">flickr.groups.getInfo</a>. The user needs to agree to the rules before being able to join the group. Pass the accept_rules argument if the user accepted the rules.</p> <p><a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/services/api/flickr.groups.leave.html">flickr.groups.leave</a> to leave a group. The user’s photos can also be deleted when leaving the group by passing the delete_photos argument.</p> <h3>Group Discussions</h3> <p class="undersized-image-container"> <a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/photos/ohh_rissa/3472376226/" title="shut UP WALTON by larissa_allen, on Flickr"><img src="https://web.archive.org/web/20130414051835im_/http://farm4.staticflickr.com/3558/3472376226_6d62aaa1e4.jpg" width="500" height="333" alt="shut UP WALTON"></a> </p> <p>We are also opening up group discussions in the API. You can now fetch a list of discussion topics for a group using <a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/services/api/flickr.groups.discuss.topics.getList.html">flickr.groups.discuss.topics.getList</a>, with sticky topics first, then regular topics sorted from newest to oldest.</p> <pre class="brush: xml; gutter: false; title: ; notranslate" title=""> <rsp stat="ok"> <topics group_id="46744914@N00" iconserver="1" iconfarm="1" name="Tell a story in 5 frames (Visual story telling)" members="12428" privacy="3" lang="en-us" ispoolmoderated="1" total="4621" page="1" per_page="2" pages="2310"> <topic id="72157625038324579" subject="A long time ago in a galaxy far, far away..." author="53930889@N04" authorname="Smallportfolio_jm08" role="member" iconserver="5169" iconfarm="6" count_replies="8" can_edit="0" can_delete="0" can_reply="0" is_sticky="0" is_locked="" datecreate="1287070965" datelastpost="1336905518"> <message> ... </message> </topic> </topics> </rsp> </pre> <p><a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/services/api/flickr.groups.discuss.topics.add.html">flickr.groups.discuss.topics.add</a> to post a new topic to a group, passing a subject and the message content.</p> <p>Additionally, you can fetch a list of replies for a topic using <a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/services/api/flickr.groups.discuss.replies.getList.html">flickr.groups.discuss.replies.getList</a>, which includes the information for the topic along with all the replies, sorted from oldest to newest.</p> <pre class="brush: xml; gutter: false; title: ; notranslate" title=""> <rsp stat="ok"> <replies> <topic topic_id="72157625038324579" subject="A long time ago in a galaxy far, far away..." group_id="46744914@N00" iconserver="1" iconfarm="1" name="Tell a story in 5 frames (Visual story telling)" author="53930889@N04" authorname="Smallportfolio_jm08" role="member" author_iconserver="5169" author_iconfarm="6" can_edit="0" can_delete="0" can_reply="0" is_sticky="0" is_locked="" datecreate="1287070965" datelastpost="1336905518" total="8" page="1" per_page="3" pages="2"> <message> ... </message> </topic> <reply id="72157625163054214" author="41380738@N05" authorname="BlueRidgeKitties" role="member" iconserver="2459" iconfarm="3" can_edit="0" can_delete="0" datecreate="1287071539" lastedit="0"> <message> ... </message> </reply> </replies> </rsp> </pre> <p><a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/services/api/flickr.groups.discuss.replies.add.html">flickr.groups.discuss.replies.add</a> to post a reply to a topic, passing the message content.</p> <p><a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/services/api/flickr.groups.discuss.replies.edit.html">flickr.groups.discuss.replies.edit</a> to edit a reply, passing the updated message.</p> <p><a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/services/api/flickr.groups.discuss.replies.delete.html">flickr.groups.discuss.replies.delete</a> to delete a reply.</p> <p>You can only edit and delete replies when authorized as the owner of the reply. For now, it is not possible to edit or delete a topic through the API.</p> <p>If you have any questions, comments, concerns, or just want to chat about these methods or anything else related to the API, please join the <a href="https://web.archive.org/web/20130414051835/http://tech.groups.yahoo.com/group/yws-flickr">Flickr Developer mailing list</a>.</p> <p>Photos from <a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/photos/fofurasfelinas/">fofurasfelinas</a> and <a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/photos/ohh_rissa/3472376226/">larissa_allen</a>.</p> <div id="jp-post-flair" class="sharedaddy sd-like-enabled sd-sharing-enabled"></div> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"> <span class="entry-utility-prep entry-utility-prep-cat-links">Posted in</span> <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/category/uncategorized/" title="View all posts in Uncategorized" rel="category tag">Uncategorized</a> </span> <span class="sep"> | </span> <span class="tag-links"> <span class="entry-utility-prep entry-utility-prep-tag-links">Tagged</span> <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/tag/api/" rel="tag">api</a>, <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/tag/groups/" rel="tag">groups</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-1925 --> <article id="post-1826" class="post-1826 post type-post status-publish format-standard hentry category-uncategorized"> <header class="entry-header"> <h1 class="entry-title"><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/05/15/liquid-photo-page-layout/" title="Permalink to Liquid Photo Page Layout" rel="bookmark">Liquid Photo Page Layout</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/05/15/liquid-photo-page-layout/" title="5:49 pm" rel="bookmark"><time class="entry-date" datetime="2012-05-15T17:49:26+00:00">May 15, 2012</time></a><span class="by-author"> <span class="sep"> by </span> <span class="author vcard"><a class="url fn n" href="https://web.archive.org/web/20130414051835/http://code.flickr.net/author/rharmes/" title="View all posts by Ross Harmes" rel="author">Ross Harmes</a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p>The Flickr photo page has gone through several revisions over the years. It was initially designed for 800×600 pixel displays, with a 500 pixel wide photo and a 250 pixel wide sidebar.</p> <p style="color:#888;font-size:12px;text-align:center;"><a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/photos/rossharmes/6990011562/"><img style="border:1px dotted #000;margin-left:-1px;" src="https://web.archive.org/web/20130414051835im_/http://farm8.staticflickr.com/7103/7142340761_be5290854c_o.png" alt="" width="526" height="281"/></a><br/> The 500×375 photo takes up 9.1% of the 1905×1079 pixels available in my viewport</p> <p>By 2010, display resolutions had increased significantly, and 1024×768 became the new standard for our smallest supported resolution. We launched a re-designed photo page, designed for a width of 960. It featured a 640 pixel wide photo and a sidebar of 300 pixels.</p> <p style="color:#888;font-size:12px;text-align:center;"><a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/photos/rossharmes/6990011562/"><img style="border:1px dotted #000;margin-left:-1px;" src="https://web.archive.org/web/20130414051835im_/http://farm9.staticflickr.com/8016/6996251904_6554558c64_o.png" alt="" width="526" height="281"/></a><br/> The 640×480 photo takes up 14.9% of the 1905×1079 pixels available in my viewport</p> <p>Since then the number of different display resolutions has increased and larger sizes have become more popular, but the number of users still on 1024×768 displays have made it hard to increase the width of the page beyond 960. We realized that we would always have to support smaller monitors, but that there was no reason not to give bigger photos to those with larger monitors. The <a href="https://web.archive.org/web/20130414051835/http://blog.flickr.net/en/2012/05/03/introducing-two-new-photo-sizes-and-a-new-setting-for-pro-members/">recent launch</a> of the 800, 1600, and 2048 photo sizes gave us a lot of different options for showing big, beautiful photos to members, and we wanted to take advantage of that. <strong>Starting today, we will display the biggest photo that we can on the photo page for your monitor</strong>.</p> <p style="color:#888;font-size:12px;text-align:center;"><a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/photos/rossharmes/6990011562/"><img style="border:1px dotted #000;margin-left:-1px;" src="https://web.archive.org/web/20130414051835im_/http://farm8.staticflickr.com/7057/7142340725_c268ee4c4d_o.png" alt="" width="526" height="281"/></a><br/> The 1213×910 photo takes up 53.7% of the 1905×1079 pixels available in my viewport</p> <p><strong>Algorithmic</strong></p> <p>As you use the new liquid photo page, you may notice that the page content doesn’t always fill the entire viewport. This is because we created an algorithm for taking the width <em>and</em> height into account that will display content at a width that will best showcase the most common photo ratio, the 4:3. Here are the goals of that algorithm:</p> <ol style="margin-bottom:12px;font-size:108%;line-height:150%;"> <li style="list-style-type:decimal;margin-left:2em;">Show the biggest photo the window allows</li> <li style="list-style-type:decimal;margin-left:2em;">Ensure the title and the sidebar are visible</li> <li style="list-style-type:decimal;margin-left:2em;">Keep the width of the page consistent across all photo pages, regardless of the individual photo dimensions</li> <li style="list-style-type:decimal;margin-left:2em;">Whenever possible, prefer native dimensions of a photo size (i.e., resist downsampling and never upsample)</li> </ol> <p><strong>Going Big</strong></p> <p>Big photos are really compelling. We knew from using the Flickr Light Box that our members’ photos look amazing at full screen, and we wanted to give the same experience on the photo page. This part of the algorithm was easy; as soon as the page starts loading, we read the <code>innerWidth</code> and <code>innerHeight</code> of the viewport (or the browser’s equivalent), and then go through the photo sizes that the photo owner allows us to display to find the best fit. If the photo is a little too big for the space we have to work with, we scale it down in the browser.</p> <p><strong>Providing Context</strong></p> <p>As great as a giant photo is, a photo is more than just its pixels. The context and story around a photo is just as important. Imagine a photo of a tiger; it’s impressive in its own right, but throw in a map showing that the tiger is in a public park, and a title stating, “A Tiger Escaped From the Zoo!” and then you really have something.</></p> <p>We decided that the title and the sidebar are important enough to make it worth showing a slightly smaller photo on the page. We adjusted the algorithm to take into account the width of the sidebar and its gutter (335 pixels) and the height of the first line of the title (45 pixels) when calculating how much available space there is for a photo.</p> <p><strong>Site Consistency</strong></p> <p>So far, so good. However, as we used the liquid photo page we noticed that it had one fatal flaw: Since the algorithm uses the dimensions of the photo that you are viewing to adjust the page width, it changes from photo to photo. This mean that if you’re browsing through some photos, the elements of the page are moving around from page to page. This is especially problematic with the header and the Next / Previous buttons; It’s incredibly difficult to navigate around if you always have to hunt around to find them first.</p> <p>To fix this problem, we decided to make the algorithm ignore the dimensions of the currently displayed photo when calculating page width, and instead to always use the dimensions of an imaginary 4:3 photo. This means that the page width will always be the same for any given combination of viewport width and viewport height, and that the UI elements will be in the same places for each page. The downsides of this are that photos that aren’t 4:3 will have more whitespace around them and even potentially be cut off by the bottom of the page, forcing the viewer to scroll. Using a consistent width is definitely the lesser of the two evils, though. The current photo page has the same problem with photos that are taller than they are wide being below the fold, and we’ve been happily viewing them for years.</p> <p style="color:#888;font-size:12px;text-align:center;"><img src="https://web.archive.org/web/20130414051835im_/http://farm9.staticflickr.com/8142/7203797462_9a030b7689_o.png" alt="" width="526" height="370"/></p> <p><strong>Going Native</strong></p> <p>These days, browsers do a pretty good job scaling a photo down. By default, most browsers <a href="https://web.archive.org/web/20130414051835/https://developer.mozilla.org/en/CSS/image-rendering">err on the side of quality</a> rather than speed, so the resulting photo should look good regardless of the size it is displayed. That being said, if we ever downsample a photo, then we are downloading more pixels than we need and throwing them away. This isn’t good for performance.</p> <p>We adjusted the algorithm to favor native sizes, even if that means a slightly smaller photo is shown. We coded in <a href="https://web.archive.org/web/20130414051835/http://en.wikipedia.org/wiki/Detent">detents</a>, so that if a photo size is within 60 pixels of a native size, we will just use that size instead of downsampling a larger one. This means the page loads faster and that most common monitor resolutions will see photos at the native size, as this table illustrates (percentage use data from <a href="https://web.archive.org/web/20130414051835/http://gs.statcounter.com/#resolution-ww-monthly-200903-201203">StatCounter</a>):</p> <table class="data-table" style="text-align:right;width:100%;margin-bottom:25px;"> <tbody> <tr class="header"> <th>Resolution</th> <th>Use %</th> <th>Page width</th> <th>Image size</th> <th>Image width</th> <th>Efficiency</th> </tr> <tr style="background-color:#f8f8f8;"> <td>1366 x 768</td> <td>19.28%</td> <td>975px</td> <td>Medium 640</td> <td>640px</td> <td style="color:green;">100.0%</td> </tr> <tr> <td>1024 x 768</td> <td>18.60%</td> <td>975px</td> <td>Medium 640</td> <td>640px</td> <td style="color:green;">100.0%</td> </tr> <tr style="background-color:#f8f8f8;"> <td>1280 x 800</td> <td>12.95%</td> <td>1044px</td> <td>Medium 800</td> <td>709px</td> <td style="color:#ff9d00;">88.6%</td> </tr> <tr> <td>1280 x 1024</td> <td>7.48%</td> <td>1216px</td> <td>Large 1024</td> <td>881px</td> <td style="color:#ff9d00;">86.0%</td> </tr> <tr style="background-color:#f8f8f8;"> <td>1440 x 900</td> <td>6.60%</td> <td>1135px</td> <td>Medium 800</td> <td>800px</td> <td style="color:green;">100.0%</td> </tr> <tr> <td>1920 x 1080</td> <td>5.09%</td> <td>1359px</td> <td>Large 1024</td> <td>1024px</td> <td style="color:green;">100.0%</td> </tr> <tr style="background-color:#f8f8f8;"> <td>1600 x 900</td> <td>3.83%</td> <td>1135px</td> <td>Medium 800</td> <td>800px</td> <td style="color:green;">100.0%</td> </tr> <tr> <td>1680 x 1050</td> <td>3.63%</td> <td>1359px</td> <td>Large 1024</td> <td>1024px</td> <td style="color:green;">100.0%</td> </tr> <tr style="background-color:#f8f8f8;"> <td>1360 x 768</td> <td>2.32%</td> <td>975px</td> <td>Medium 640</td> <td>640px</td> <td style="color:green;">100.0%</td> </tr> </tbody> </table> <p><strong>Titles Are for Squares, Man</strong></p> <p>Square photos are an interesting loophole in the way we size photos. Because we’re targeting an imaginary 4:3 photo, square photos will be displayed with more actual pixels than any other size, taking up the full width and height allotted. While browsing the site we noticed this, as well as the fact that the title is never visible. In order to bring the overall pixel count more in line with landscape and portrait photos, we reduce the size of square photos a bit more than the others. This helps ensure that the titles are always visible as well.</p> <p style="color:#888;font-size:12px;text-align:center;"><img src="https://web.archive.org/web/20130414051835im_/http://farm8.staticflickr.com/7225/7204017498_b9b3463bb7_o.png" alt="" width="526" height="370"/></p> <p><strong>Making it Fast</strong></p> <p>Now that the algorithm is complete, we need to work on the performance. We noticed that reading the viewport dimensions and resizing the page every single time you go to a photo is unnecessary and distracting (since the page loads with a width of 960 and must be adjusted after the JavaScript loads on the page). To fix this, we cache the viewport dimensions in a cookie that can be read by the PHP code that generates the page. The first time you go to a liquid photo page, we have no choice but to adjust the page width on the fly. But every other photo page you visit will have the dimensions stored from the last page, and the page will be rendered with the correct width from the start.</p> <p><strong>More to Come</strong></p> <p>We have a lot more changes in store for this year. Stay tuned!</p> <div id="jp-post-flair" class="sharedaddy sd-like-enabled sd-sharing-enabled"></div> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"> <span class="entry-utility-prep entry-utility-prep-cat-links">Posted in</span> <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/category/uncategorized/" title="View all posts in Uncategorized" rel="category tag">Uncategorized</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-1826 --> <article id="post-1864" class="post-1864 post type-post status-publish format-standard hentry category-uncategorized"> <header class="entry-header"> <h1 class="entry-title"><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/05/11/building-the-flickr-web-uploadr-the-grid/" title="Permalink to Building The Flickr Web Uploadr: The Grid" rel="bookmark">Building The Flickr Web Uploadr: The Grid</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/05/11/building-the-flickr-web-uploadr-the-grid/" title="9:40 pm" rel="bookmark"><time class="entry-date" datetime="2012-05-11T21:40:17+00:00">May 11, 2012</time></a><span class="by-author"> <span class="sep"> by </span> <span class="author vcard"><a class="url fn n" href="https://web.archive.org/web/20130414051835/http://code.flickr.net/author/scottschiller/" title="View all posts by scottschiller" rel="author">scottschiller</a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p><i>The new Flickr Web Uploadr is the result of a good amount of prototyping, research and good old-fashioned testing across the team that built it. This article goes into some of the details behind the “grid” – the area where photo thumbnails are shown – and sheds a little light on some of the thinking and logic behind the scenes. It’s a little lengthy, but don’t worry, there are pictures!</i></p> <p>In <a href="https://web.archive.org/web/20130414051835/http://code.flickr.com/blog/2012/04/25/raising-the-bar-on-web-uploads/">April 2012</a>, Flickr started rolling out its new web-based upload UI to the masses. We’re stoked to see it out there, and user feedback has been overwhelmingly positive. The product is an ongoing work in progress and enhancements are still being added, but the core is quite well-established and the experience is a significant upgrade over the one provided by the previous web-based uploadr.</p> <p class="figure"> <img src="https://web.archive.org/web/20130414051835im_/http://farm9.staticflickr.com/8020/6964585290_6163268b0a_c.jpg" width="800" height="548" alt="Flickr Web Uploader UI (2012)"><br/> <span class="caption">The new Flickr Web Uploadr. It’s powerful, it’s got a dark background, and it’s <i>fast</i>.</span> </p> <p style="margin-top:1em;">The new uploadr has also simply been <i>fun</i> to work on; there are numerous interesting challenges in terms of UI, interactions, performance and sheer scale on the front-end that we had to feel confident in tackling before we were able to commit to moving forward with the project.</p> <p><b>Building The Grid: Prototypes</b></p> <p>Initial discussions about the new Flickr uploadr weren’t too detailed, because I think everyone already had a pretty good idea of what we wanted to see in a browser: Something more desktop-like, feature-wise (like our older XUL-based Flickr Uploadr application) that would load and show photo thumbnails in a grid arrangement, with a desktop-like selection and batch editing model.</p> <p>The next step was to start building a prototype in plain old HTML, CSS and JavaScript, and then figure out how many photos we could potentially get into the thing before it broke down. Could the grid handle selection and editing of 1,000 items? 10,000 items? I was cautiously optimistic. A continuous joke I had with the team was that I had built this before, in 2005: The project was an adventurous <a href="https://web.archive.org/web/20130414051835/http://www.schillmania.com/content/entries/2009/yahoo-photos-frontend-thoughts/" title="Front-end thoughts from the Yahoo! Photos redesign, 2006">redesign of Yahoo! Photos</a>, and joking aside, it actually did share a lot of design and interaction elements in common with what we were about to build. In 2005, we were targeting IE 6 and Firefox 1.5, so the landscape has changed a <i>lot</i> in terms of support and performance. Seven years later, it was fun to review some of the lessons and fun bits from the Y! Photos redesign as applicable to Flickr.</p> <p><b>Prototype: Fluid Grid Layout</b></p> <p>Some of the first prototypes involved building a grid layout, forming a two-column page that would be fluid to the browser width. We wanted to guarantee at least three photos per row would show in the grid, so the thumbnails could scale themselves relative to the browser size in order to fit in the space – easily done via CSS’ <code>min-width</code> and <code>max-width</code> attributes.</p> <p class="figure"> <img src="https://web.archive.org/web/20130414051835im_/http://farm8.staticflickr.com/7102/7177079200_25fa0339c2_c.jpg" width="800" height="443" alt=""><br/> <span class="caption">A very early version of the uploadr UI.</span> </p> <p style="margin-top:1em;">The earliest prototypes simply populated the DOM with a few hundred copies of a cloned photo item “template”, to give the idea of what a busy UI might look like. It was mostly just HTML and CSS at this point.</p> <p>With the grid rendering in fluid form as a series of <code>inline-block</code> <li> elements, the next thing to start was the selection model.</p> <p><b>Selection and Drag Events</b></p> <p>Building a desktop-like selection and drag-and-drop model can be a technical challenge, given the underlying complexity. As anyone who’s built one of these will understand, there are a whole ton of interactions one must consider and account for between event monitoring, coordinate tracking, drag-to-select vs. rearrange intents, event cancellation, handling of invalid actions and so on.</p> <p><b>Selection</b></p> <p>In general, all user interactions start with watching <code>mousedown()</code> events inside the grid area. If <code>mousedown()</code> fires within “whitespace”, any existing selection is reset and <code>mousemove()</code> events are then used to draw a selection marquee which compares coordinates to the grid, highlighting items based on basic region intersection logic (for example, <code>xyToRowCol()</code>, points can be checked to see what grid row/column they fall within and thus “from/to” ranges can be established for a given marquee box.) Once a <code>mouseup()</code> event fires, selection can be completed and the <code>mousemove()</code> and <code>mouseup()</code> handlers released.</p> <p class="figure"> <img src="https://web.archive.org/web/20130414051835im_/http://farm8.staticflickr.com/7225/7178265008_a669afe4f3_c.jpg" width="800" height="435" alt=""><br/> <span class="caption">Testing the selection UI at various grid sizes.</span> </p> <p style="margin-top:1em;">The above marquee drawing and intersection logic is not terribly fancy, but things start to get interesting when you throw in additional positioning considerations like vertical offset from window scrolling (and drag-initiated window scrolling), browser window resizing affecting layout, positioning of the marquee UI vs. coordinates of the underlying grid items and so forth. Keyboard modifiers can also affect selection mode – whether selection is exclusive, additive or toggle-based – so an intersect does not also always mean “select this item”, too.</p> <p class="figure"> <img src="https://web.archive.org/web/20130414051835im_/http://farm6.staticflickr.com/5326/7178353922_8226a9a7ea_c.jpg" width="800" height="427" alt="Flickr Web Upload UI: Selection Screenshot"><br/> <span class="caption">Marquee selection mode in action.</span> </p> <p style="margin-top:1em;"><b>Dragging + Rearrange</b></p> <p>When <code>mousedown()</code> fires on an unselected grid item, selection can immediately change to only that item (unless selection mode is additive or toggle-based via a modifier key.) If firing on an already-selected item, <code>mousemove()</code> is watched for a “threshold” of perhaps 4+ pixels of movement from the original coordinates, at which point “dragging” becomes active.</p> <p>Once dragging has begun, the selected grid DOM elements are marked with a “disabled” CSS class, greying them out somewhat to indicate drag state, and <code>mousemove()</code> now moves around a cursor trailer that shows the count of items being rearranged.</p> <p>Rearrange mode, once entered, is similar to the marquee selection mode except that now only a single mouse coordinate is checked in order to determine what row and column is the current “target” for rearrange – that is, what position the user intends to drag the selected photo(s) to. The logic here can get interesting in edge cases, because the user is able to insert both “before” and “after” a given target point based on whether the cursor is on the left side, or the right side of the target.</p> <p>In terms of the UI, the current drag target simply has an “insert-before” or “insert-after” CSS class appended to it which results in the appropriate “insert point” marker (a CSS border) being applied to it.</p> <p class="figure"> <img src="https://web.archive.org/web/20130414051835im_/http://farm8.staticflickr.com/7078/7178354078_2f82548434_c.jpg" width="800" height="427" alt="Flickr Web Upload UI: Rearrange"><br/> <span class="caption">Rearrange mode in action.</span> </p> <p style="margin-top:1em;">Once <code>mouseup()</code> fires on a valid rearrange target, the actual rearrange action is applied to both the UI and data model. The underlying JavaScript re-appends the dragged DOM nodes next to their new target sibling node and then splices the photo item array, matching the order of the array to the new layout shown in the UI.</p> <p><b>Additional Selection Interactions</b></p> <p>A few other use cases to consider: Clicking an item, then shift + clicking another should have the effect of setting an “anchor point”, and selecting a range of items from X-Y within the grid. The user should be able, once setting an anchor point, to “pivot” from that point by clicking while continuing to hold the shift key. (Put another way, holding shift should not set the anchor point when clicking.)</p> <p>By holding CTRL (or the Command/Apple key on OS X), selection should be additive and toggle-based. My approach to this meant taking a “snapshot” of the selection when marquee drawing begins, and then applying the logic based on mouse coordinates and keypresses with each draw action. This way, you can draw a marquee over and out of an existing selection, causing it to “toggle” and reset accordingly without losing your original state. A new snapshot is only taken once the selection is finalized at <code>mouseup()</code> time.</p> <p><b>Demo video: Uploadr Prototype UI</b></p> <p>Here is a screencast of a very early version of the Uploadr grid UI, showing the basics of mouse-based selection interactions, scrolling and resizing. By this time, selection events were also firing and updating the “editr panel” area as well.</p> <p class="undersized-image-container"> <object type="application/x-shockwave-flash" width="500" height="398" data="https://web.archive.org/web/20130414051835im_/http://www.flickr.com/apps/video/stewart.swf?v=1.161" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"> <param name="flashvars" value="photo_id=7177694856&photo_secret=3fb0a325e4&flickr_show_info_box=true"></param><param name="movie" value="http://www.flickr.com/apps/video/stewart.swf?v=1.161"></param><param name="bgcolor" value="#000000"></param><param name="allowFullScreen" value="true"></param><param name="wmode" value="opaque"></param><embed type="application/x-shockwave-flash" src="https://web.archive.org/web/20130414051835oe_/http://www.flickr.com/apps/video/stewart.swf?v=1.161" bgcolor="#000000" allowfullscreen="true" flashvars="photo_id=7177694856&photo_secret=3fb0a325e4&flickr_show_info_box=true" wmode="opaque" height="398" width="500"></embed></object> </p> <p><b>Enter The Keyboard</b></p> <p>With mouse events working, additional consideration was given to keyboard shortcuts. We intended to have a UI that supported most if not all of the same selection, editing and rearrange actions that could be achieved via the mouse. An important part to making this work involved watching focus inside the grid, tracking the last-known selected item, and supporting the use of the arrow keys as a means of changing focus between grid items.</p> <p>Focus-based navigation in the grid is interesting, more akin to mouse movement and hover behaviour. It is intentionally separate from keyboard-based selection (which is invoked with a toggle behaviour via the spacebar, <i>or</i> selection and editing of a single item via the return key.) Using this approach, it is relatively easy to navigate and build up a selection of items via the arrow keys and spacebar.</p> <p>For rearrange, a cut-and-paste approach was used; CTRL or Command/Apple + X (“cut”) are used to begin rearrange, arrow keys set the target rearrange point, and CTRL + V or return will apply the rearrange at the given target. If active, pressing escape will exit rearrange mode.</p> <p><b>Performance: Scaling The Front-End</b></p> <p>An important step in the grid prototype, once it was rendering in a fluid fashion, was to see find all the ways in which we could get it to break down. Which browsers were first to choke under the DOM load as more nodes were written out? Was layout and rendering the bottleneck? Were too many events firing? Was the JS engine spending too much time updating the DOM?</p> <p>After rendering several hundred photos in the UI, we started to see evidence of browsers getting laggy in terms of responsiveness, and CPU + RAM use trending upward. With plans to extend this UI to handle numbers of photos in the thousands, a number of optimizations were made up front including aggressive pruning of the DOM as the user scrolled the page.</p> <p>In brief, the trick is to create a large page with no content and only generate the DOM to reflect the slice of the whole view being shown.</p> <p>Given events like window scrolling and resize affecting browser coordinates and DOM layout, we are easily able to calculate and cache the changes as they happen, making quick lookups to determine precisely what range of grid items are in view for the user. A single “page” of grid items can then be generated on the fly, appended to the DOM and shown to the browser. Events like browser resize invalidates the coordinate cache, so the DOM reflows and the grid refresh / display process repeats itself in a throttled fashion when this happens.</p> <p><b>Event Throttling: Responsiveness’ Dirty Little Secret</b></p> <p>Native DOM events are useful, but they can fire quite aggressively and left unchecked, can really hurt the performance of your application. Scrolling and resize are good examples for the grid case, as we want the UI to respond with an updated display pretty quickly when scrolling – but we know that we only have to show new items when a new row comes into view, which is typically only every 200 vertical pixels. With resizing, we only need to reflow the grid when resizing has added or removed enough horizontal room that we’ve lost or gained a new column.</p> <p>In short, if you know events will fire often, subscribe to all of them but only do expensive work if there are real changes to apply. Alternately, you could only let resize handlers (for example) fire once every 500 milliseconds and do the work every time, so your handler only fires twice a second in the worst-case scenario.</p> <p><b>Cache The Hell Out Of The DOM</b></p> <p>This was hinted at previously, but is worth repeating: Get references and read values once, particularly from the DOM, and cache them when initially retrieving and updating them in response to events. If you know what a value is going to be, don’t query for it.</p> <p>In JavaScript, an internal lookup is far faster than reaching out to query the DOM for attributes like offsetWidth, for example. Simply reading certain attributes of DOM nodes can cause layout and reflow to happen in the browser, which means you’re making the browser do more work for information that is likely unchanged. Thrown into a loop mixed with DOM writes, this makes for pretty disastrous browser performance.</p> <p>JavaScript frameworks like YUI et al should do their own caching of this data, but I see no downside in grabbing and storing this stuff locally yourself; as the implementer, you have the best idea of what data is most static and what is not.</p> <p>Additionally, try to read at once and write at once to the DOM; don’t have loops that do a write and then a read, for example. Try to write DOM interactions that follow the browser’s rendering model, minimizing the back-and-forth of layout/reflow/display calculations. Use document fragments to build up collections of DOM nodes, and append them once to the DOM vs. using <code>innerHTML</code>, or – worse – multiple <code>appendChild()</code> calls. Don’t query <code>className</code> when you likely know what it’s going to be; track that state internally in JS, instead, and only write changes out to the DOM.</p> <p><b>“Stateful” CSS Class Names</b></p> <p>I’ve been a fan of the concept of “stateful” CSS – eg., <code>.is_selected { border: red; }</code> for years. Not only is state consistent, but using CSS in this way also encourages better separation of concerns (and less temptation to add or remove DOM nodes via JS when making changes.)</p> <p>When you want to grey something out, for example, you may set a <code>disabled</code> property to <code>true</code> on a JS object. That easily translates to a CSS class name change including <code>.disabled {}</code> applied to the relevant DOM node. As a result, your DOM is logically reflecting your JS state. It’s also helpful when troubleshooting, because you can add the class name to nodes ad-hoc when testing UI features.</p> <p>For the grid’s purposes, every grid item contains all relevant “states” and the markup for those states – selection, thumbnail, progress, overlay icons, messages, errors and so forth. This makes it very easy to change the item’s display with a single, or few additional CSS class names, and minimizes the amount of work JS has to do to update the DOM. It is also trivial to combine states this way, also – e.g., a photo upload that has a thumbnail, but is in a “failed” state because it’s over-size.</p> <p>While uploading, for example, a grid item may have <code>class="has-thumbnail working selected"</code>, then completes with <code>class="has-thumbnail has-fullsize-thumbnail complete"</code> when the upload has finished. All JS did here was update the class name (and while actively uploading, redraw a small progress meter on the item.) Thus, JS/DOM interaction is fairly minimal.</p> <p>A single CSS change can also completely change the display of the grid, also. “Info view” is one example of this. When enabled, a single additional class on the grid container causes all photo items to show overlay icons with their privacy state, and additional icons if they have tags, are in a set and so on.</p> <p class="figure"> <img src="https://web.archive.org/web/20130414051835im_/http://farm8.staticflickr.com/7239/7178488438_1c69f20439_c.jpg" width="792" height="250" alt="Flickr Web Upload UI: Info View"><br/> <span class="caption">“Info” view, showing overlays with privacy, state and other information.</span> </p> <p style="margin-top:1em;"><b>Broadcast Events FTW</b></p> <p>Events are a great way for modular bits of code, written by the same or separate people, to work on separate problems independently. Among other things, the grid listens for events regarding file addition, removal, progress and success / failure states from the upload queue module. The grid generates and fires events itself reflecting changes around selection, editing and arrangement as the user is doing their work, which are picked up by the “editr panel” at left that updates to reflect the selection state. Provided that events are kept as simple notifications and relatively one-way, there is little risk of complex event-related tracing in the unlikely, er – event – that something that goes wrong.</p> <p>Flickr uses YUI 3 extensively, and we write and plug our application code into the system as YUI 3 modules. In addition to the excellent modular framework approach, we take advantage of the DOM and Event functionality in particular.</p> <p><b>In Summary</b></p> <p>The grid is only one of several modules that make up the new Flickr Web Uploadr, and is primarily responsible for the display and updating of photo thumbnails, selection, arrangement and basic metadata. There is a lot more going on in terms of JavaScript and network state under the hood, including API calls and permissions; posts highlighting some of the other fun areas are forthcoming.</p> <p>As it turns out, building a feature-rich browser-based application for millions of people that looks good, is fast and supports many use cases including constraints and unexpected error conditions, can be a challenge. It’s also part of the fun.</p> <div class="hiring-banner"> <p class="group-photo"> <a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/photos/morozgrafix/7803402076/" title="Flickr flamily floto by morozgrafix, on Flickr"><img src="https://web.archive.org/web/20130414051835im_/http://farm9.staticflickr.com/8436/7803402076_c06f87bf1f_m.jpg" width="120" height="80" alt="Flickr flamily floto"></a> </p> <p> Like this post? Have a love of online photography? Want to work with us? Flickr is hiring <strong>engineers</strong>, <strong>designers</strong> and <strong>product managers</strong> in our San Francisco office. <strong>Find out more at <a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/jobs/">flickr.com/jobs</a></strong>. </p> </div> <div id="jp-post-flair" class="sharedaddy sd-like-enabled sd-sharing-enabled"></div> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"> <span class="entry-utility-prep entry-utility-prep-cat-links">Posted in</span> <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/category/uncategorized/" title="View all posts in Uncategorized" rel="category tag">Uncategorized</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-1864 --> <article id="post-1773" class="post-1773 post type-post status-publish format-standard hentry category-uncategorized tag-css3 tag-fileapi tag-html5 tag-javascript tag-uploader tag-uploadr"> <header class="entry-header"> <h1 class="entry-title"><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/04/25/raising-the-bar-on-web-uploads/" title="Permalink to Raising the bar on web uploads" rel="bookmark">Raising the bar on web uploads</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/04/25/raising-the-bar-on-web-uploads/" title="6:05 pm" rel="bookmark"><time class="entry-date" datetime="2012-04-25T18:05:44+00:00">April 25, 2012</time></a><span class="by-author"> <span class="sep"> by </span> <span class="author vcard"><a class="url fn n" href="https://web.archive.org/web/20130414051835/http://code.flickr.net/author/scottschiller/" title="View all posts by scottschiller" rel="author">scottschiller</a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p>With over seven billion photos uploaded since day one, it’s safe to say that uploading is an important part of the Flickr experience.</p> <p>There are numerous ways to get photos onto Flickr, but the native web-based one at <code>flickr.com/photos/upload/</code> is especially important as it typically accounts for a majority of uploads to the site.</p> <h2>A brief history of Flickr “Web Uploadrs”</h2> <div> <div style="float:right;display:inline;width:320px;padding:1em 1em 0;"> <a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/photos/schill/7112862941/in/set-72157629533547504/" title="Flickr "Flashy" Uploadr UI (2008) vs. Basic Uploadr UI"><img src="https://web.archive.org/web/20130414051835im_/http://farm6.staticflickr.com/5117/7112862941_f383735379_n.jpg" width="320" height="128" title="Flickr "Flashy" Uploadr UI (2008) vs. Basic Uploadr UI"/></a> <p style="font-family:helvetica, arial, sans-serif;font-size:.9em;color:#666;margin:0;text-align:center;">Flickr “Flashy” Uploadr UI (2008) vs. Basic Uploadr UI</p> </p></div> <p>Earlier versions of Flickr’s web-based upload UI used a simple <code><form></code> with six file inputs, and no more. As the site grew in scale, the native web upload experience had to scale to match. In early 2008, an <a href="https://web.archive.org/web/20130414051835/http://code.flickr.com/blog/2008/04/22/making-a-better-flickr-web-uploadr-or-web-browsers-arent-good-at-uploading-files-by-themselves/">HTML/Flash hybrid upgrade</a> added support for batch file selection, allowing up to several gigabytes of files to be uploaded in one session. This was a much-needed step in the right direction.</p> </div> <p>The “flashy” uploader does one thing – sending lots of files – fast, and reliably. However, it was not designed to tackle the other tasks one often performs on photos including adding and editing of metadata, sorting and organizing. As a result, “upload and organize” has traditionally been reinforced as two separate actions on Flickr when using the web-based UI.</p> <h2>The new (mostly-HTML5-based) shiny</h2> <p>Thanks to HTML5-based features in newer browsers, we have been able to build a new uploader that’s pretty slick, and is more desktop application-like than ever before; it brings us closer to the idea of a one-stop “upload and organize” experience. At the same time, the UI also retains common web conventions and has a distinct Flickr feel to it. We think the result is a pretty good mix, combining some of the best parts of both.</p> <p>As feedback from a group of beta testers have confirmed, it can also be <i>deceivingly</i> fast.</p> <p style="text-align:center;margin:0;"><a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/photos/schill/6964585290/sizes/o/in/photostream/"><img src="https://web.archive.org/web/20130414051835im_/http://farm9.staticflickr.com/8020/6964585290_6163268b0a.jpg" title="Screenshot: The new (2012) Flickr Web Upload UI (click for full-size)"/></a></p> <p style="font-family:helvetica, arial, sans-serif;color:#666;font-size:90%;text-align:center;margin:0;">The new Flickr Web Uploader. It’s powerful, it’s got a dark background, and it’s <i>fast</i>.</p> <h2 style="margin-top:1em;">Features: An Overview</h2> <p>Here are a few fun things the new uploader does:</p> <ul> <li> <p>Drag and drop batches of files from your OS. Where present and supported, EXIF thumbnails are shown in the UI almost immediately.</p> </li> <li> <p>Fluid photo “grid” shows photo thumbnails, allows larger, lightbox-style previews, inline editing of description/title and rotation.</p> </li> <li> <p>Mouse and keyboard-based grid selection and rearrange functionality similar to that of desktops.</p> </li> <li> <p>“Editor panel” shows state of current selection, provides powerful batch editing features (title + description, adding of tags, people, sets, license, privacy etc.)</p> </li> <li> <p>“Info” mode shows overlay icons on grid items, allowing for a quick overview of pending edits (privacy, people, tags etc.)</p> </li> <li> <p>Auto-retry and recovery cases for dropped / lost connection cases</p> </li> </ul> <h2 style="margin-top:1em;">Technical Bits</h2> <p>A small book could probably be written on the process, prototypes and technology decisions made during the development of this uploader, but we’ll save the gory details for a couple of in-depth blog posts which will highlight specific parts of the UI. In the meantime, here are some notes on the tech used:</p> <ul> <li> <p><b>HTML5 File APIs</b></p> <p>Modern browser file APIs make up the core of file handling functionality, including drag-and-dropping of files right into the browser. <code>FileReader</code>-type APIs allow access to data from disk, enabling things like EXIF thumbnail parsing and retrieval where supported. EXIF parsing is almost instantaneous and thumbnails are hugely valuable, of course, in prompting users’ editing decisions.</p> <p>(For browsers without the relevant file APIs, a Flash-based fallback is used in which case file drag-and-drop is not supported, and EXIF thumb previews are not implemented.)</p> </li> <li> <p><b>CSS3</b></p> <p>Thanks to growing support across newer browsers, we’ve been able to produce a modern design that takes advantage of CSS-based gradients to achieve visual goals that would have traditionally required external images, and occasionally, hacks or shims in our HTML and JavaScript.</p> <p>CSS3′s <code>border-radius</code>, <code>text-shadow</code> and <code>box-shadow</code> are also featured nicely in this new design, alongside visual <code>transform</code> effects such as <code>rotate</code>, <code>zoom</code> and <code>scale</code>. Eagle-eyed users of newer Webkit builds such as Chrome Canary may even see a little use of <code>filter</code> with <code>blur</code> here and there.</p> <p>CSS transitions are also featured extensively in the new uploader, a notable shift away from animation sequences which would traditionally have been calculated and rendered by JavaScript. Good candidates for transitions include the expanding or collapsing of a menu section, or a background color fade when a text area is focused, for example.</p> <p>While triggering transitions and/or transforms can be a little quirky depending on the current “state” of the element (for example, an element just added to the DOM may need a moment to settle and be rendered before transitioning,) the advantage of using CSS vs. JS for “enhancement”-style UI effects like these is absolutely clear.</p> </li> <li> <p><b>YUI3</b></p> <p>Thanks to <a href="https://web.archive.org/web/20130414051835/http://yuilibrary.com/">YUI3</a>, the new Flickr Uploader is a highly-modularized, component-based application. The editr module itself is comprised of about 35 sub-modules, following YUI’s standard module pattern. In Flickr’s case, modules are defined as being JavaScript, CSS or string (i.e., language translation) components. This compartmentalization approach reduces the overall complexity of code, encourages extensibility and allows developers to work on features within a specific scope.</p> </li> </ul> <h2>A sneak peek: Screencast (Beta Version)</h2> <p>At time of writing, the new uploader is being gradually rolled out to the masses. For those who haven’t seen it yet, here’s a <a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/photos/schill/6928227556/in/set-72157629533547504/?likes_hd=1">demo screencast of an earlier beta version</a> showing some of the interactions for common upload and editing use cases. (Best viewed full-screen, and with “HD” on.) The video gives an idea of what the experience is like, but it’s best seen in person. We’ve really had a lot of fun building this one.</p> <p class="undersized-image-container"> <object type="application/x-shockwave-flash" width="500" height="281" data="https://web.archive.org/web/20130414051835im_/http://www.flickr.com/apps/video/stewart.swf?v=1.161" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"> <param name="flashvars" value="photo_id=6928227556&photo_secret=11b73352d1&flickr_show_info_box=true"></param><param name="movie" value="http://www.flickr.com/apps/video/stewart.swf?v=1.161"></param><param name="bgcolor" value="#000000"></param><param name="allowFullScreen" value="true"></param><param name="wmode" value="opaque"></param><embed type="application/x-shockwave-flash" src="https://web.archive.org/web/20130414051835oe_/http://www.flickr.com/apps/video/stewart.swf?v=1.161" bgcolor="#000000" allowfullscreen="true" flashvars="photo_id=6928227556&photo_secret=11b73352d1&flickr_show_info_box=true" wmode="opaque" height="281" width="500"></embed></object></p> <div id="jp-post-flair" class="sharedaddy sd-like-enabled sd-sharing-enabled"></div> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"> <span class="entry-utility-prep entry-utility-prep-cat-links">Posted in</span> <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/category/uncategorized/" title="View all posts in Uncategorized" rel="category tag">Uncategorized</a> </span> <span class="sep"> | </span> <span class="tag-links"> <span class="entry-utility-prep entry-utility-prep-tag-links">Tagged</span> <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/tag/css3/" rel="tag">css3</a>, <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/tag/fileapi/" rel="tag">fileapi</a>, <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/tag/html5/" rel="tag">html5</a>, <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/tag/javascript/" rel="tag">javascript</a>, <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/tag/uploader/" rel="tag">uploader</a>, <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/tag/uploadr/" rel="tag">uploadr</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-1773 --> <article id="post-1748" class="post-1748 post type-post status-publish format-standard hentry category-uncategorized tag-aviary tag-cors tag-flash tag-html5"> <header class="entry-header"> <h1 class="entry-title"><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/04/18/building-an-html5-photo-editor/" title="Permalink to Building an HTML5 Photo Editor" rel="bookmark">Building an HTML5 Photo Editor</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/04/18/building-an-html5-photo-editor/" title="11:16 pm" rel="bookmark"><time class="entry-date" datetime="2012-04-18T23:16:35+00:00">April 18, 2012</time></a><span class="by-author"> <span class="sep"> by </span> <span class="author vcard"><a class="url fn n" href="https://web.archive.org/web/20130414051835/http://code.flickr.net/author/standardpixel/" title="View all posts by Eric Gelinas" rel="author">Eric Gelinas</a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p style="padding:6px 8px;background:#f8f8f8;"> <a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/photos/35998464@N07/"><img src="https://web.archive.org/web/20130414051835im_/http://farm5.staticflickr.com/4008/buddyicons/35998464@N07.jpg?1263791643#35998464@N07" style="float:left;margin-right:8px;" width="48px" height="48px"></a>Introducing guest blogger, <b>Ari Fuchs</b>. He is a Lead API Engineer and Developer Evangelist at <b><a href="https://web.archive.org/web/20130414051835/http://aviary.com/">Aviary</a></b>. He has spent the last 3 years building out Aviary’s internal and external facing APIs, and is now working with partners to bring Aviary’s tools to the masses. He also did a lot of work to bring the Aviary editor to Flickr. <a href="https://web.archive.org/web/20130414051835/https://twitter.com/arifuchs">Follow him on Twitter</a> and send him a nice message to make him feel better about his <a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/photos/35998464@N07/5193379076/in/photostream">stolen bike</a>. Now, on to his post… </p> <p>At Aviary, we’ve been passionate about photos since day one. It’s been five years since we released our first creative tool, Phoenix, a powerful, free Flash-based photo editor. Phoenix offered functionality on par with Adobe Photoshop 5 and a price point that opened its usage to anyone with an internet connection. As amateur photographers worldwide began trying their hand at editing, we watched our product join the ranks of a small number of companies working to democratize the process of photo editing for the first time.</p> <p>Around two years ago we began rethinking the future of our tool set. While our original tools offered incredible functionality, they did have a learning curve which meant that the average person couldn’t just sit down and begin editing without investing time to become familiar with the tools. We wanted to build a powerful editor that anyone could use.</p> <p>Because we were rebuilding the editor from the ground up, we took the opportunity to switch from a Flash based solution to one built using HTML5 technologies. We saw this as an opportunity to build on a growing standard, and to support the most platforms.</p> <p>In fall of 2010 we released our HTML5 photo editor which has evolved into the product we’re proud to share with you today.</p> <h2>Widget Encapsulation</h2> <p>During our initial foray into the online editor space, we took a straightforward approach by having API users launch our editor in a new page or window. This simplified integrations and allowed us to own the editing experience.</p> <p>When we rebuilt our editor in JavaScript, we took the opportunity to re-architect our API as well. Our first big change was making the editor embeddable. This meant that third party developers could load the editor on their own sites, maintaining user engagement while controlling their experience. We built out customization options that allowed the site owner to decide which tools appeared in the editor. A real estate site, for example, might not want its users adding mustache stickers to appliances in photos.</p> <p>Our editor, unlike many rich HTML widgets, does not require an iframe and is truly embedded into a hosting webpage. This posed many challenges during development, but the result is a more seamless, lightweight integration. </p> <p class="undersized-image-container"><img src="https://web.archive.org/web/20130414051835im_/http://farm6.staticflickr.com/5080/6945711452_34af520b41.jpg" width="500" height="392" alt="Aviary embeded in Flickr" border="0"/></p> <h2>Constructor API</h2> <p>When we rebuilt our API, we took a leap by assuming that web developers integrating our editor would have experience with other JavaScript libraries and plugins. We built our API to use a Constructor method that accepts a configuration object to allow for the aforementioned tool customization. The configuration object is also used to configure callbacks, image URLs, language settings, etc., and allows us to continue building out our API without losing backwards compatibility.</p> <h2>Simplifying the Save Process</h2> <p>Saving image data is always a challenge in the browser, and can require various cross-browser workarounds. An obvious method would be to initiate a form post to the server and include the base64 image data in a hidden field. This breaks in Safari, where form fields have an undocumented value length limit. We worked around this by switching to an ajax post with the appropriate CORS headers to get around cross domain issues. In browsers that don’t support CORS, we fall back to the form post method.</p> <p>To hide this complexity from the developer, we’ve abstracted the save process completely. When a user saves an edited image, we temporarily save the image data to our own servers and return a public URL so the host application can download the image to their own.</p> <h2>High Resolution Photos</h2> <p>One of the coolest features of our editor is the high resolution image support — that being said, it certainly has a number of challenges. There’s the practical issue of limited real estate in the browser (keep an eye out for updates addressing this in the near future), as well as performance issues that are harder to quantify. Even in Flash based tools, the size of the image you can edit in the browser is limited by a number of gating factors: hardware specs, number of running processes, etc. To get around these client limitations, we’ve set a configurable maxSize on the editor and added a configuration field for an original-resolution version of the image to be edited: hiresUrl.</p> <p>When a hiresUrl is supplied, every user edit action is logged. On save, the aptly named “actionlist” is sent to our server along with the hiresUrl. When it hits our render farm, the actionlist is replayed on the high resolution image, and the final results are returned to the host site via a new hiresUrl.</p> <pre> { "metadata": { "imageorigsize": [ 800, 530 ] }, "actionlist": [ { "action": "setfeathereditsize", "width": 800, "height": 530 }, { "action": "flatten" }, { "action": "redeye", "radius": 5, "pointlist": [ [545, 183], [546,183], [547,182], [548,181], [548,179], [548,177], [547,177], [545,177], [544,177], [543,177], [542,177], [541,179], [541,181], [541,183], [542,184] ] }, { "action": "redeye", "radius": 5, "pointlist": [ [481, 191], [481,193], [481,195], [482,196], [483,197], [484,198], [485,197], [485,196], [485,193], [485,190], [485,189], [485,188], [484,188], [482,188], [480,189], [480,190], [480, 191] ] }, { "action": "sharpen", "value": 21.69312, "flatten": true } ] } </pre> <p>As a side note, we maintain feature parity across all of our platforms (mobile included) by prototyping new tools and filters in the JavaScript first, and then porting them to C for our render farm and Android, and then to Objective-C for our iPhone SDK. By maintaining feature parity and synchronizing output across platforms, we’re able to ensure that users get the edits they expect on their high resolution photos, and we keep the door open for future server-side support for our mobile SDKs where the original photo might not be stored on the device.</p> <h2>Tools and Libraries</h2> <p>We use some pretty awesome tools to help us maintain cross-browser compatibility.</p> <h2><a href="https://web.archive.org/web/20130414051835/http://lesscss.org/">LESS CSS</a></h2> <p>We moved a lot of the cross-browser concerns to build-time with LESS and a library of mix-ins inspired initially by Twitter Bootstrap, though the final result is wholly our own. LESS’s color math and variables let us achieve a textured and rounded look and feel while minimizing complexity during development.</p> <p><pre> /* LESS */ .avpw_inset_button_group { #gradient > .vertical(lighten(@conveyorBelt, 4%), darken(@conveyorBelt, 1%)); .box-shadow(inset 0 0 4px darken(@conveyorBelt, 20%)); .border-radius(8px); } /* EXPANDED */ .avpw_inset_button_group { background-color: #2a2a2a; background-repeat: repeat-x; background-image: -khtml-gradient(linear, left top, left bottom, from(#383838), to(#2a2a2a)); background-image: -moz-linear-gradient(top, #383838, #2a2a2a); background-image: -ms-linear-gradient(top, #383838, #2a2a2a); background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #383838), color-stop(100%, #2a2a2a)); background-image: -webkit-linear-gradient(top, #383838, #2a2a2a); background-image: -o-linear-gradient(top, #383838, #2a2a2a); background-image: linear-gradient(top, #383838, #2a2a2a); filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#383838', endColorstr='#2a2a2a', GradientType=0); -webkit-box-shadow: inset 0 0 4px #000000; -moz-box-shadow: inset 0 0 4px #000000; box-shadow: inset 0 0 4px #000000; -webkit-border-radius: 8px; -moz-border-radius: 8px; border-radius: 8px; } </pre> </p> <h2><a href="https://web.archive.org/web/20130414051835/https://developer.mozilla.org/en/CSS">CSS3</a></h2> <p>With CSS3, we’ve just about managed a complete break from the DHTML effects of the past. The new UI uses CSS3 transitions and transforms wherever possible to remain future-proof.</p> <h2>Flash</h2> <p>Yes, our editor does indeed have a Flash fallback for browsers that lack certain HTML5 features (namely <a href="https://web.archive.org/web/20130414051835/https://developer.mozilla.org/en/HTML/Element/canvas">canvas</a>). We initially built the editor as a move away from Flash, but because of the legacy IE7 and IE8 userbases on our larger partner sites, we had to go back and rebuild certain components in Flash to support those browsers.</p> <p>We’ve architected the editor so that Flash is only being used where necessary. Some tools, such as draw, have been completely rebuilt in Flash; for others, like effects, the bitmap data is being exported and manipulated in JavaScript (using a reverse implementation of pibeca). This allows for code reuse, and enables us to build new features faster with more backwards compatibility.</p> <h2>Future</h2> <p>While the feedback for our editor has been overwhelmingly fantastic, we’re continuing to work hard building out new tools and features, and performance enhancements to our existing set.</p> <div id="jp-post-flair" class="sharedaddy sd-like-enabled sd-sharing-enabled"></div> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"> <span class="entry-utility-prep entry-utility-prep-cat-links">Posted in</span> <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/category/uncategorized/" title="View all posts in Uncategorized" rel="category tag">Uncategorized</a> </span> <span class="sep"> | </span> <span class="tag-links"> <span class="entry-utility-prep entry-utility-prep-tag-links">Tagged</span> <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/tag/aviary/" rel="tag">aviary</a>, <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/tag/cors/" rel="tag">CORS</a>, <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/tag/flash/" rel="tag">flash</a>, <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/tag/html5/" rel="tag">html5</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-1748 --> <article id="post-1740" class="post-1740 post type-post status-publish format-standard hentry category-uncategorized"> <header class="entry-header"> <h1 class="entry-title"><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/02/28/scott-schiller-on-web-audio/" title="Permalink to Scott Schiller on Web Audio" rel="bookmark">Scott Schiller on Web Audio</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/02/28/scott-schiller-on-web-audio/" title="9:46 pm" rel="bookmark"><time class="entry-date" datetime="2012-02-28T21:46:41+00:00">February 28, 2012</time></a><span class="by-author"> <span class="sep"> by </span> <span class="author vcard"><a class="url fn n" href="https://web.archive.org/web/20130414051835/http://code.flickr.net/author/rharmes/" title="View all posts by Ross Harmes" rel="author">Ross Harmes</a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p>We recently had a Flickr Frontend Night at <a href="https://web.archive.org/web/20130414051835/http://www.meetup.com/BayJax/">BayJax</a>, the Bay Area JavaScript group. We’ll be posting the videos from those talks over the next couple of weeks.</p> <p>First up! Frontend Engineer, DJ, and all-around nice guy <a href="https://web.archive.org/web/20130414051835/http://www.flickr.com/photos/schill/">Scott Schiller</a>, with his great talk on Web Audio.</p> <p><iframe width="526" height="296" src="https://web.archive.org/web/20130414051835if_/http://www.youtube.com/embed/C2Tw0BeZb8Q" frameborder="0" allowfullscreen></iframe></p> <p>Scott used <a href="https://web.archive.org/web/20130414051835/http://bartaz.github.com/impress.js/">impress.js</a> to make his <a href="https://web.archive.org/web/20130414051835/http://isflashdeadyet.com/talks/html5/bayjax_yahoo_sunnyvale_02-06-2012/#/start">awesome slides</a> (using HTML and CSS Transitions). If you want to dig deeper into Scott’s talk, there is the <a href="https://web.archive.org/web/20130414051835/http://www.youtube.com/watch?v=C2Tw0BeZb8Q&list=PLCC8AC96847080476&hd=1">HD version on YouTube</a>, <a href="https://web.archive.org/web/20130414051835/http://isflashdeadyet.com/talks/html5/bayjax_yahoo_sunnyvale_02-06-2012/#/start">slides</a>, the <a href="https://web.archive.org/web/20130414051835/http://wheelsofsteel.net/">Wheels of Steel demo</a>, and the HTML5 game he created, <a href="https://web.archive.org/web/20130414051835/https://github.com/scottschiller/SURVIVOR">SURVIVOR</a>.</p> <p>Big thanks to Gonzalo Cordero for organizing the event, and to Ryan Grove and Allen Rabinovich for their great work filming it.</p> <p>On a side note, if you’re in Austin for SxSW next week, be sure to check out talks by Flickr’s own Eric Gelinas (<a href="https://web.archive.org/web/20130414051835/http://austin2012.sched.org/event/259fea3227cf3c94c2486c9e6384e832">Geo Interfaces for Actual Humans</a>) and Stephen Woods (<a href="https://web.archive.org/web/20130414051835/http://austin2012.sched.org/event/083a2ef70e6698ba6dbcb43812fdba01">Creating Responsive HTML5 Touch Interfaces</a>).</p> <p><span style="color: #999; font-size: 12px;">Thanks to <a href="https://web.archive.org/web/20130414051835/http://softdroid.net/">softdroid.net</a> for creating a <a href="https://web.archive.org/web/20130414051835/http://softdroid.net/skott-shiller-na-veb-audio">Ukrainian translation of this post</a>.</span></p> <div id="jp-post-flair" class="sharedaddy sd-like-enabled sd-sharing-enabled"></div> </div><!-- .entry-content --> <footer class="entry-meta"> <span class="cat-links"> <span class="entry-utility-prep entry-utility-prep-cat-links">Posted in</span> <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/category/uncategorized/" title="View all posts in Uncategorized" rel="category tag">Uncategorized</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-1740 --> <nav id="nav-below"> <h3 class="assistive-text">Post navigation</h3> <div class="nav-previous"><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/category/uncategorized/page/2/"><span class="meta-nav">←</span> Older posts</a></div> <div class="nav-next"></div> </nav><!-- #nav-above --> </div><!-- #content --> </section><!-- #primary --> <div id="secondary" class="widget-area" role="complementary"> <aside id="search-2" class="widget widget_search"> <form method="get" id="searchform" action="https://web.archive.org/web/20130414051835/http://code.flickr.net/"> <label for="s" class="assistive-text">Search</label> <input type="text" class="field" name="s" id="s" placeholder="Search"/> <input type="submit" class="submit" name="submit" id="searchsubmit" value="Search"/> </form> </aside> <aside id="recent-posts-2" class="widget widget_recent_entries"> <h3 class="widget-title">Recent Posts</h3> <ul> <li> <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2013/03/26/using-redis-as-a-secondary-index-for-mysql/" title="Using Redis as a Secondary Index for MySQL">Using Redis as a Secondary Index for MySQL</a> </li> <li> <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/12/21/redis-global-locks-redux/" title="Redis Global Locks Redux">Redis Global Locks Redux</a> </li> <li> <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/12/12/highly-available-real-time-notifications/" title="Highly Available Real Time Push Notifications and You">Highly Available Real Time Push Notifications and You</a> </li> <li> <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/12/10/drag-n-drop/" title="Avoiding Dragons: A Practical Guide to Drag ’n’ Drop">Avoiding Dragons: A Practical Guide to Drag ’n’ Drop</a> </li> <li> <a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/10/26/flickr-at-sf-web-performance/" title="Flickr at SF Web Performance">Flickr at SF Web Performance</a> </li> </ul> </aside><aside id="archives-2" class="widget widget_archive"><h3 class="widget-title">Archives</h3> <ul> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2013/03/" title="March 2013">March 2013</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/12/" title="December 2012">December 2012</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/10/" title="October 2012">October 2012</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/07/" title="July 2012">July 2012</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/06/" title="June 2012">June 2012</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/05/" title="May 2012">May 2012</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/04/" title="April 2012">April 2012</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/02/" title="February 2012">February 2012</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2012/01/" title="January 2012">January 2012</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2011/12/" title="December 2011">December 2011</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2011/10/" title="October 2011">October 2011</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2011/09/" title="September 2011">September 2011</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2011/08/" title="August 2011">August 2011</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2011/07/" title="July 2011">July 2011</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2011/06/" title="June 2011">June 2011</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2011/03/" title="March 2011">March 2011</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2011/02/" title="February 2011">February 2011</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2011/01/" title="January 2011">January 2011</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2010/11/" title="November 2010">November 2010</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2010/10/" title="October 2010">October 2010</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2010/09/" title="September 2010">September 2010</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2010/08/" title="August 2010">August 2010</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2010/07/" title="July 2010">July 2010</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2010/05/" title="May 2010">May 2010</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2010/04/" title="April 2010">April 2010</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2010/03/" title="March 2010">March 2010</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2010/02/" title="February 2010">February 2010</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2010/01/" title="January 2010">January 2010</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2009/12/" title="December 2009">December 2009</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2009/11/" title="November 2009">November 2009</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2009/10/" title="October 2009">October 2009</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2009/09/" title="September 2009">September 2009</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2009/07/" title="July 2009">July 2009</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2009/06/" title="June 2009">June 2009</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2009/05/" title="May 2009">May 2009</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2009/04/" title="April 2009">April 2009</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2009/03/" title="March 2009">March 2009</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2009/02/" title="February 2009">February 2009</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2009/01/" title="January 2009">January 2009</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2008/12/" title="December 2008">December 2008</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2008/11/" title="November 2008">November 2008</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2008/10/" title="October 2008">October 2008</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2008/09/" title="September 2008">September 2008</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2008/08/" title="August 2008">August 2008</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2008/07/" title="July 2008">July 2008</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2008/06/" title="June 2008">June 2008</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2008/05/" title="May 2008">May 2008</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/2008/04/" title="April 2008">April 2008</a></li> </ul> </aside><aside id="categories-2" class="widget widget_categories"><h3 class="widget-title">Categories</h3> <ul> <li class="cat-item cat-item-564792"><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/category/change-log/" title="View all posts filed under changelog">changelog</a> </li> <li class="cat-item cat-item-5784"><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/category/event/" title="View all posts filed under event">event</a> </li> <li class="cat-item cat-item-29160"><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/category/geo/" title="All things geo related">geo</a> </li> <li class="cat-item cat-item-34412"><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/category/kittens/" title="View all posts filed under kittens">kittens</a> </li> <li class="cat-item cat-item-171"><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/category/meta/" title="View all posts filed under meta">meta</a> </li> <li class="cat-item cat-item-1 current-cat"><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/category/uncategorized/" title="View all posts filed under Uncategorized">Uncategorized</a> </li> <li class="cat-item cat-item-249276"><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/category/uploadr/" title="View all posts filed under uploadr">uploadr</a> </li> <li class="cat-item cat-item-830560"><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/category/xulrunner/" title="View all posts filed under xulrunner">xulrunner</a> </li> </ul> </aside><aside id="meta-2" class="widget widget_meta"><h3 class="widget-title">Meta</h3> <ul> <li><a href="https://web.archive.org/web/20130414051835/http://flickrcode.wordpress.com/wp-login.php?action=register">Register</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://flickrcode.wordpress.com/wp-login.php">Log in</a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/feed/" title="Syndicate this site using RSS 2.0">Entries <abbr title="Really Simple Syndication">RSS</abbr></a></li> <li><a href="https://web.archive.org/web/20130414051835/http://code.flickr.net/comments/feed/" title="The latest comments to all posts in RSS">Comments <abbr title="Really Simple Syndication">RSS</abbr></a></li> Powered by <a href="https://web.archive.org/web/20130414051835/http://vip.wordpress.com/" rel="generator nofollow" class="powered-by-wpcom">WordPress.com VIP</a> </ul> </aside> </div><!-- #secondary .widget-area --> </div><!-- #main --> <footer id="colophon" role="contentinfo"> <div id="site-generator"> Theme: Twenty Eleven <span class="sep"> | </span> Powered by <a href="https://web.archive.org/web/20130414051835/http://vip.wordpress.com/" rel="generator nofollow" class="powered-by-wpcom">WordPress.com VIP</a> </div> </footer><!-- #colophon --> </div><!-- #page --> <script type="text/javascript"> var _qevents = _qevents || [], wpcomQuantcastData = {"qacct":"p-18-mFEk4J448M","labels":",language.en,type.wpcom,vip.flickrcode"}; function wpcomQuantcastPixel( labels, options ) { var i, defaults = wpcomQuantcastData, data = { event: 'ajax' }; labels = labels || ''; options = options || {}; if ( typeof labels != 'string' ) options = labels; for ( i in defaults ) { data[i] = defaults[i]; } for ( i in options ) { data[i] = options[i]; } if ( data.labels ) { data.labels += ',' + labels; } else { data.labels = labels; } _qevents.push( data ); }; (function() {var elem = document.createElement('script');elem.src = (document.location.protocol == "https:" ? "https://web.archive.org/web/20130414051835/https://secure" : "https://web.archive.org/web/20130414051835/http://edge") + ".quantserve.com/quant.js";elem.async = true;elem.type = "text/javascript";var scpt = document.getElementsByTagName('script')[0];scpt.parentNode.insertBefore(elem, scpt); })(); _qevents.push( wpcomQuantcastData ); </script> <noscript><div style="display: none;"><img src="//web.archive.org/web/20130414051835im_/http://pixel.quantserve.com/pixel/p-18-mFEk4J448M.gif?labels=%2Clanguage.en%2Ctype.wpcom%2Cvip.flickrcode" height="1" width="1" alt=""/></div></noscript> <script type="text/javascript" src="//web.archive.org/web/20130414051835js_/http://0.gravatar.com/js/gprofiles.js?ver=201315ac"></script> <script type="text/javascript"> /* <![CDATA[ */ var WPGroHo = {"my_hash":""}; /* ]]> */ </script> <script type="text/javascript" src="https://web.archive.org/web/20130414051835js_/http://s0.wp.com/wp-content/mu-plugins/gravatar-hovercards/wpgroho.js?m=1351637563g"></script> <script>jQuery(document).ready(function($){ Gravatar.profile_cb = function( h, d ) { WPGroHo.syncProfileData( h, d ); }; Gravatar.my_hash = WPGroHo.my_hash; Gravatar.init( 'body', '#wp-admin-bar-my-account' ); });</script> <div style="display:none"> </div> <script type="text/javascript" src="https://web.archive.org/web/20130414051835js_/http://s2.wp.com/_static/??-eJzTLy/QTc7PK0nNK9EvyClNz8wr1i+uzCtJrMjITM/IAeKS1CJMEWP94uSizIISoOIM5/yiVL2sYh19yo1yKiotzvAKBvOpaWREbg7QOPtcW0NjM2NjAxMTQ5MsAD8OYFI="></script> <script type="text/javascript"> (function(){ var corecss = document.createElement('link'); var themecss = document.createElement('link'); var corecssurl = "https://web.archive.org/web/20130414051835/http://s0.wp.com/wp-content/plugins/syntaxhighlighter/syntaxhighlighter3/styles/shCore.css?m=1363661091g&ver=3.0.83c"; if ( corecss.setAttribute ) { corecss.setAttribute( "rel", "stylesheet" ); corecss.setAttribute( "type", "text/css" ); corecss.setAttribute( "href", corecssurl ); } else { corecss.rel = "stylesheet"; corecss.href = corecssurl; } document.getElementsByTagName("head")[0].insertBefore( corecss, document.getElementById("syntaxhighlighteranchor") ); var themecssurl = "https://web.archive.org/web/20130414051835/http://s0.wp.com/wp-content/plugins/syntaxhighlighter/syntaxhighlighter3/styles/shThemeDefault.css?m=1363304414g&ver=3.0.83c"; if ( themecss.setAttribute ) { themecss.setAttribute( "rel", "stylesheet" ); themecss.setAttribute( "type", "text/css" ); themecss.setAttribute( "href", themecssurl ); } else { themecss.rel = "stylesheet"; themecss.href = themecssurl; } //document.getElementById("syntaxhighlighteranchor").appendChild(themecss); document.getElementsByTagName("head")[0].insertBefore( themecss, document.getElementById("syntaxhighlighteranchor") ); })(); SyntaxHighlighter.config.strings.expandSource = '+ expand source'; SyntaxHighlighter.config.strings.help = '?'; SyntaxHighlighter.config.strings.alert = 'SyntaxHighlighter\n\n'; SyntaxHighlighter.config.strings.noBrush = 'Can\'t find brush for: '; SyntaxHighlighter.config.strings.brushNotHtmlScript = 'Brush wasn\'t configured for html-script option: '; SyntaxHighlighter.defaults['pad-line-numbers'] = false; SyntaxHighlighter.defaults['toolbar'] = false; SyntaxHighlighter.all(); </script> <script type="text/javascript" src="https://web.archive.org/web/20130414051835js_/http://s0.wp.com/wp-content/js/devicepx.js?m=1354656609g"></script> <script type="text/javascript" src="https://web.archive.org/web/20130414051835js_/http://platform.twitter.com/widgets.js?ver=20111117"></script> <script type="text/javascript" src="https://web.archive.org/web/20130414051835js_/http://s1.wp.com/wp-content/mu-plugins/twitter-blackbird-pie/pending.js?m=1365535071g"></script> <script type="text/javascript"> // <![CDATA[ (function() { try{ if ( window.external &&'msIsSiteMode' in window.external) { if (window.external.msIsSiteMode()) { var jl = document.createElement('script'); jl.type='text/javascript'; jl.async=true; jl.src='/wp-content/plugins/ie-sitemode/custom-jumplist.php'; var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(jl, s); } } }catch(e){} })(); // ]]> </script><script src="https://web.archive.org/web/20130414051835js_/http://s.stats.wordpress.com/w.js?21" type="text/javascript"></script> <script type="text/javascript"> st_go({'blog':'39034126','v':'wpcom','tz':'0','user_id':'0','subd':'flickrcode'}); function st_vt() {var x=document.createElement("img");x.src="https://web.archive.org/web/20130414051835/http://stats.wordpress.com/g.gif?blog=39034126&v=wpcomvt&tz=0&user_id=0&subd=flickrcode&rand="+Math.random();} ex_go({'crypt':'UE40eW5QN0p8M2Y/RE1BNmNJfGhxNCVxUDExYmtXRThKbHcwXTdETWI1alhvb1oseHImN101ZFpEakVpYjlQYVFLYzBaVHRtPz0wXS9bM1lKdVZKQS1NTGJmdUM0ZHJxVkdbSmJHXy45YXpOSn5mZ3omWzBFWFYzeF1FV0FMJkxzLH5QUlZKc2JNW0lXVncsWGwvMm15RXRLWixxNWp5eC9mfkhKN21EeHNYNVRMPXdaLy53ZVRrbHQ/YjglUVAmYWVCK2pRNG9nVXg9Z0JqPWQ2TTklUUhXbC5SQm58ODUsWDdzdlhIcEU5NXNaOWZUOD9KSV1YazBpL2xmRm5E'}); addLoadEvent(function(){linktracker_init('39034126',0);}); </script> <noscript><img src="https://web.archive.org/web/20130414051835im_/http://stats.wordpress.com/b.gif?v=noscript" style="height:0px;width:0px;overflow:hidden" alt=""/></noscript> </body> </html><!-- FILE ARCHIVED ON 05:18:35 Apr 14, 2013 AND RETRIEVED FROM THE INTERNET ARCHIVE ON 00:42:17 Dec 04, 2024. JAVASCRIPT APPENDED BY WAYBACK MACHINE, COPYRIGHT INTERNET ARCHIVE. ALL OTHER CONTENT MAY ALSO BE PROTECTED BY COPYRIGHT (17 U.S.C. SECTION 108(a)(3)). --> <!-- playback timings (ms): captures_list: 0.582 exclusion.robots: 0.029 exclusion.robots.policy: 0.017 esindex: 0.012 cdx.remote: 9.149 LoadShardBlock: 155.259 (3) PetaboxLoader3.datanode: 94.65 (4) PetaboxLoader3.resolve: 300.741 (2) load_resource: 311.46 -->