CINXE.COM

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 181 seconds ago generated in 0.368 seconds served from batcache in 0.003 seconds expires in 119 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/","20131004234920","https://web.archive.org/","web","/_static/", "1380930560"); </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>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/20131004234920cs_/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/20131004234920js_/http://r-login.wordpress.com/remote-login.php?action=js&amp;host=code.flickr.net&amp;id=39034126&amp;t=1380930379&amp;back=code.flickr.net%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/20131004234920/http://code.flickr.net/feed/"/> <link rel="alternate" type="application/rss+xml" title="code.flickr.com » Comments Feed" href="https://web.archive.org/web/20131004234920/http://code.flickr.net/comments/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/20131004234920cs_/http://s2.wp.com/_static/??-eJydjt0OgjAMRl/IWVCM8cL4LGMWGHRbs3UsvL1o4pU/MV41X7+T00JhZYIX9AIuK6bcW5+A7IQJRhTWZlKPtDUpbeA9ziGJ6kjbCEkWwhdWBnSrkHMLUtbFgoQz+u/0bBk6smaKa3PFf9SqsAnu19cHHa3vn/PTpaGBnkKr6Q5c3LneH0+Hqql29XgDFp984g==" type="text/css" media="all"/> <script type="text/javascript" src="https://web.archive.org/web/20131004234920js_/http://s1.wp.com/_static/??-eJyFzdsKwjAMgOEXsisesHghPkutcSSuaW3SDX16O1BQEHYVSD7y2ymbkFiB1ZLYmM44gKkCxfdtZ5CvqSNZ2eaQw1AvIDOke4XyeI9FYCL2xSt0EfmDv6o5iUYQack/198U8ogwLTICzT7cTAHB5/z1FI/rrXObvTu4Hb0AKYVb3w=="></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="shortlink" href="https://web.archive.org/web/20131004234920/http://wp.me/2DMyG"/> <link rel="shortcut icon" type="image/x-icon" href="https://web.archive.org/web/20131004234920im_/http://1.gravatar.com/blavatar/341946154e8a7e5497473810e7ef560c?s=16" sizes="16x16"/> <link rel="icon" type="image/x-icon" href="https://web.archive.org/web/20131004234920im_/http://1.gravatar.com/blavatar/341946154e8a7e5497473810e7ef560c?s=16" sizes="16x16"/> <link rel="apple-touch-icon-precomposed" href="https://web.archive.org/web/20131004234920im_/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/20131004234920/http://code.flickr.net/osd.xml" title="code.flickr.com"/> <link rel="search" type="application/opensearchdescription+xml" href="https://web.archive.org/web/20131004234920/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" id="twentyeleven-header-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/20131004234920cs_/http://code.flickr.net/?custom-css=1&amp;csblog=2DMyG&amp;cscache=6&amp;csrev=103"/> </head> <body class="home blog typekit-enabled two-column right-sidebar mp6 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/20131004234920/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/20131004234920/http://code.flickr.net/"> <img src="https://web.archive.org/web/20131004234920im_/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/20131004234920/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/20131004234920/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/20131004234920/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/20131004234920/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/20131004234920/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/20131004234920/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/20131004234920/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/20131004234920/http://www.flickr.com/jobs/">Jobs</a></li> </ul></div> </nav><!-- #access --> </header><!-- #branding --> <div id="main"> <div id="primary"> <div id="content" role="main"> <nav id="nav-above"> <h3 class="assistive-text">Post navigation</h3> <div class="nav-previous"><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/page/2/"><span class="meta-nav">&larr;</span> Older posts</a></div> <div class="nav-next"></div> </nav><!-- #nav-above --> <article id="post-2464" class="post-2464 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/20131004234920/http://code.flickr.net/2013/09/04/a-summer-at-flickr/" rel="bookmark">A Summer at&nbsp;Flickr</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2013/09/04/a-summer-at-flickr/" title="6:42 pm" rel="bookmark"><time class="entry-date" datetime="2013-09-04T18:42:43+00:00">September 4, 2013</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/20131004234920/http://code.flickr.net/author/josephflickr/" title="View all posts by Joseph Baena" rel="author">Joseph Baena</a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p>This summer I had the unforgettable opportunity to work side-by-side with the some of the smartest, photography-loving software engineers I’ve ever met. Looking back on my first day at Flickr HQ – beginning with a harmonious welcome during Flickr Engineering’s weekly meeting – I can confidently say that over the past ten weeks I have become a much better software engineer.</p> <p>One of my projects for the summer was to build a new and improved locking manager that controls the distribution of locks for offline tasks (or <i>OLT</i>s for short). Flickr uses OLTs all the time for data migration, uploading photos, updates to accounts, and more. An OLT needs to acquire a lock on a shared resource, such as a group or an account, to prevent other OLTs from accessing the same resource at the same time. The OLT will then release the lock when it’s done modifying the shared data. Myles wrote an excellent blog post on how Flickr uses offline tasks <a title="Flickr Engineers Do It Offline" href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2008/09/26/flickr-engineers-do-it-offline/">here</a>.</p> <p>When building a distributed lock system, we need to take into account a couple of important details. First, we need to make sure that all the lock servers are consistent. One way to maintain consistency is to elect one server to act as a <i>master</i> and the remaining servers as <i>slaves</i>, where the master server is responsible for data replication among the slave servers. Second, we need to account for network and hardware failures – for instance, if the master server goes down for some reason, we need to quickly elect a new master server from one of the slave servers. The good news is, <a title="Apache ZooKeeper" href="https://web.archive.org/web/20131004234920/http://zookeeper.apache.org/">Apache ZooKeeper</a> is an open-source implementation of master-slave data replication, automatic leader election, and atomic distributed data reads and writes.</p> <p class="figure"><img alt="" src="https://web.archive.org/web/20131004234920im_/http://flickrcode.files.wordpress.com/2013/09/locking2.png?w=800&amp;h=600" width="800" height="600"/><br/> <span class="caption">Offline tasks send lock acquire and release requests through ZooLocker. ZooLocker in turn interfaces with the ZooKeeper cluster to create and delete znodes that correspond to the individual locks.</span></p> <p>In the new locking system (dubbed “ZooLocker”), each lock is stored as a unique data node (or <i>znode</i>) on the ZooKeeper servers. When a client acquires a lock, ZooLocker creates a znode that corresponds to the lock. If the znode already exists, ZooLocker will tell the client that the lock is currently in use. When a client releases the lock, ZooLocker deletes the corresponding znode from memory. ZooLocker stores helpful debugging information, such as the owner of the lock, the host it was created on, and the maximum amount of time to hold on to the lock, in a JSON-serialized format in the znode. ZooLocker also periodically scans through each znode in the ZooKeeper ensemble to release locks that are past their expiration time.</p> <p>My locking manager is already serving locks in production. In spite of sudden spikes in lock acquire and release requests by clients, the system holds up pretty well.</p> <p class="figure"><img alt="" src="https://web.archive.org/web/20131004234920im_/http://flickrcode.files.wordpress.com/2013/09/zk_chart1.png?w=800&amp;h=600" width="800" height="600"/><br/> <span class="caption">A graph of the number of lock acquire requests in ZooLocker per second</span></p> <p>My summer internship at Flickr has been an incredibly valuable experience for me. I have demystified the process of writing, testing, and integrating code into a running system that millions of people around the world use each and every day. I have also learned about the amazing work going on in the engineering team, the ups and downs the code deploy process, and how to dodge the incoming flying finger rockets that the Flickr team members fling at each other.  My internship at Flickr is an experience I will never forget, and I am very grateful to the entire Flickr team for giving me the opportunity to work and learn from them this summer.</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/20131004234920/http://code.flickr.net/category/uncategorized/" title="View all posts in Uncategorized" rel="category tag">Uncategorized</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-2464 --> <article id="post-2457" class="post-2457 post type-post status-publish format-standard hentry category-performance"> <header class="entry-header"> <h1 class="entry-title"><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2013/06/14/pre-generating-justified-views/" rel="bookmark">Pre-generating Justified&nbsp;Views</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2013/06/14/pre-generating-justified-views/" title="5:10 pm" rel="bookmark"><time class="entry-date" datetime="2013-06-14T17:10:15+00:00">June 14, 2013</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/20131004234920/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>On May 20th, we introduced our Justified layout to the Photostream page. Ever since launch, we&#8217;ve been working hard to improve the performance of this page, and this week we’ve deployed a change that dramatically reduces the time it takes to display photos. The secret? Cookies.</p> <p>At a high level, our Justified algorithm works like this:</p> <ol> <li>Take, as input, the browser viewport width and a list of photos</li> <li>Begin to lay those photos out in a row sequentially, using the maximum allowed height and scaling the width proportionately</li> <li>If a row becomes longer than the viewport width, reduce the height of that row and all the photos in it until the width is correct</li> </ol> <p>Because we need the viewport width, we have to run this algorithm entirely in the browser. And because we won&#8217;t know which particular photo size to request until we&#8217;ve run the algorithm, we can&#8217;t start downloading the photos until very late in the process. This is why, up until Friday, when you loaded a photostream page, you saw the spinning blue and pink balls before the photos loaded.</p> <p>Last week we were able to make one key change: we now pre-generate the layout on the server. This means that we know exactly which image sizes we need at the very top of the page, and can start downloading them immediately. It also means the spinning balls aren&#8217;t needed anymore. The end result is that the first photo on the page now loads <b>seven times faster</b> than on May 20th. </p> <p class="figure"> <img src="https://web.archive.org/web/20131004234920im_/http://farm4.staticflickr.com/3718/9043860256_2ebe97d188_c.jpg" width="800" height="199" alt=""><span class="caption">&#8220;Time to First Photo&#8221; on the Photostream page</span> </p> <p>One question remains: we need client viewport width in order to generate the layout, so how are we able to pre-generate it on the server? The first time you come to any Flickr page, we store the width of your browser window in a cookie. We can then read that cookie on the server on subsequent page loads. This means we aren&#8217;t able to pre-generate the photostream layout the very first time you come to the site. It also means that the layout will occasionally be incorrect, if you have resized the browser window since the last time you visited Flickr; we deal with this by always correcting the layout on the client, if a mismatch is detected.</p> <p>This is one of many performance improvements we&#8217;re working on after our 5/20 release (we&#8217;ve also deployed some improvements to the homepage activity feed). Expect to see the performance continue to improve on the redesigned pages in the coming weeks and months.</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/20131004234920/http://code.flickr.net/category/performance/" title="View all posts in performance" rel="category tag">performance</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-2457 --> <article id="post-2416" class="post-2416 post type-post status-publish format-standard hentry category-performance tag-chrome tag-compositing tag-dev-tools tag-gpu tag-hardware-acceleration tag-painting tag-performance"> <header class="entry-header"> <h1 class="entry-title"><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2013/06/04/adventures-in-jank-busting-parallax-performance-and-the-new-flickr-home-page/" rel="bookmark">Adventures in Jank Busting: Parallax, performance, and the new Flickr Home&nbsp;Page</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2013/06/04/adventures-in-jank-busting-parallax-performance-and-the-new-flickr-home-page/" title="5:25 pm" rel="bookmark"><time class="entry-date" datetime="2013-06-04T17:25:48+00:00">June 4, 2013</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/20131004234920/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 class="aside">tl;dr version: <code>transform3d()</code> is your friend, but use sparingly. Chrome Dev Tools are awesome.</p> <h2>What&#8217;s old is new again: Stealing from the arcade</h2> <p>Back in 1985, games like Super Mario Bros. popularized the effect of horizontal parallax scrolling &#8211; a technique wherein the background moves at a slower speed relative to the foreground, giving the impression of depth. In 2013, the web is seeing a trend of vertical parallax effects as developers look to make pages feel more interactive and responsive. Your author&#8217;s $0.25, for the record, is that we&#8217;ll continue to see arcade and demoscene-era effects being ported over to the mainstream web in creative ways as client-side performance improves.</p> <p>While the effect can be aesthetically pleasing when executed correctly, parallax motion tends to be expensive to render &#8211; and when performance is lacking, the user&#8217;s impression of your site may follow suit.</p> <p>Vertical parallaxing is pretty straightforward in behaviour: For every Y pixels of vertical axis scrolling, move an absolutely-positioned image in the same direction at scale. There are some additional considerations for the offset of the element on the page and how far it can move, but the implementation remains quite simple.</p> <p>The following are some findings made while working on Flickr&#8217;s redesigned signed-out homepage at <a href="https://web.archive.org/web/20131004234920/http://flickr.com/new">flickr.com/new</a>, specifically related to rendering and scrolling performance.</p> <h2>Events and DOM performance</h2> <p>To optimize performance in the browser environment, it&#8217;s important to consider the expensive parts of DOM &#8220;I/O&#8221;. You ideally want a minimal amount of both, particularly since this is work being done during scrolling. Executing JavaScript on scroll is one of the worst ways to interrupt the browser, typically because it&#8217;s done to introduce reflow/layout and painting &#8211; thus, denying the browser the chance to use GPU/hardware-accelerated scrolling. <code>window.onscroll()</code> can also fire very rapidly on desktops, making way for a veritable flood of expensive scroll &rarr; reflow &rarr; paint operations if your &#8220;paths&#8221; are not fast.</p> <p>A typical parallax implementation will hook into <code>window.onscroll()</code>, and will update the <code>backgroundPosition</code> or <code>marginTop</code> of an element with a background image attached in order to make it move. An <code>&lt;img&gt;</code> could be used here, but backgrounds are convenient because they allow for positioning in relative units, tiling and so on.</p> <p>A minimal parallax example, just the script portion:</p> <pre class="brush: jscript; gutter: false; title: ; notranslate" title=""> window.onscroll = function(e) { var parallax = document.getElementById('parallax-background'); parallax.style.marginTop = (window.scrollY/2) + 'px'; }</pre> <p>This could work for a single element, but quickly breaks down if multiple elements are to be updated. In any case, references to the DOM should be cached for faster look-ups; reading <code>window.scrollY</code> and other DOM attributes can be expensive due to potential to cause layout/reflow themselves, and thus should also be stored in a local variable for each <code>onscroll()</code> event to minimize thrashing.</p> <p>An additional performance consideration: Should all parallax elements always be moved, even those which are outside of the viewport? Quick tests suggested savings were negligible in this case at best. Even if the additional work to determine in-view elements were free, moving only one element did not notably improve performance relative to moving only three at a time. It appears that Webkit&#8217;s rendering engine is smart about this, as the only expensive operations seen here involve painting things within the viewport.</p> <p>In any event, using <code>marginTop</code> or <code>backgroundPosition</code> alone will not perform well as neither take advantage of hardware-accelerated compositing.</p> <p>And now, VOIDH (Video Or It Didn&#8217;t Happen) of <code>marginTop</code>-based parallax performing terribly:</p> <object type="application/x-shockwave-flash" width="639" height="364" data="https://web.archive.org/web/20131004234920im_/http://www.flickr.com/apps/video/stewart.swf?v=1.161" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"> <param name="flashvars" value="photo_id=8859509880&amp;photo_secret=412e7dafff&amp;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/20131004234920oe_/http://www.flickr.com/apps/video/stewart.swf?v=1.161" bgcolor="#000000" allowfullscreen="true" flashvars="photo_id=8859509880&amp;photo_secret=412e7dafff&amp;flickr_show_info_box=true" wmode="opaque" height="364" width="639"></embed></object> <p>Look at that: Terrible. Jank city! You can try it for yourself in your browser of choice, via <a href="https://web.archive.org/web/20131004234920/http://www.flickr.com/new/?notransform=1">via ?notransform=1</a>.</p> <h2>Enter the GPU: Hardware Acceleration To The Rescue</h2> <p>Despite caching our DOM references and scroll properties, the cost of drawing all these pixels in software is very high. The trick to performance here is to have the GPU (hardware) take on the job of accelerating the compositing of the expensive bits, which can be done much faster than in software rendering.</p> <p>Elements can be promoted to a &#8220;layer&#8221; in rendering terms, via CSS transforms like <code>translate3d()</code>. When this and other <code>translateZ()</code>-style properties are applied, the element will then be composited by the GPU, avoiding expensive repaints. In this case, we are interested in having fast-moving parallax backgrounds. Thus, <code>translate3d()</code> can be applied directly to the parallax element.</p> <pre class="brush: jscript; gutter: false; title: ; notranslate" title="">window.onscroll = function(e) { // note: most browsers presently use prefixes: webkitTransform, mozTransform etc. var parallax = document.getElementById('parallax-background'); parallax.style.transform = 'translate3d(0px,' + (window.scrollY/2) + 'px, 0px)'; }</pre> <p>In webkit-based browsers like Safari and Chrome, the results speak for themselves.</p> <object type="application/x-shockwave-flash" width="640" height="448" data="https://web.archive.org/web/20131004234920im_/http://www.flickr.com/apps/video/stewart.swf?v=1.161" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"> <param name="flashvars" value="photo_id=8758952624&amp;photo_secret=f414d2f2fd&amp;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/20131004234920oe_/http://www.flickr.com/apps/video/stewart.swf?v=1.161" bgcolor="#000000" allowfullscreen="true" flashvars="photo_id=8758952624&amp;photo_secret=f414d2f2fd&amp;flickr_show_info_box=true" wmode="opaque" height="448" width="640"></embed></object> <p>Look, ma! Way less jank! The performance here is comparable to the same page <a href="https://web.archive.org/web/20131004234920/http://www.flickr.com/new/?noparallax=1">with parallax disabled</a> (i.e., regular browser scrolling/drawing.)</p> <p>GPU acceleration sounds like a magic bullet, but it comes at the cost of both video memory and cache. If you create too many layers, you may see rendering problems and even degraded performance &#8211; so use them selectively.</p> <p>It&#8217;s also worth noting that browser-based hardware acceleration and performance rests on having agreement between the browser, drivers, OS, and hardware. Firefox might be sluggish on one machine, and butter-smooth on another. High-density vs. standard-resolution screens make a big difference in paint cost. All GPUs are made equal, but some GPUs are made more equal than others. The videos and screenshots for this post were made on my work laptop, which may perform quite differently than your hardware. Ultimately, you need to test your work on a variety of devices to see what real-world performance is like &#8211; and this is where Chrome&#8217;s dev tools come in handy.</p> <h2>Debugging Render Performance</h2> <p>In brief, Chrome&#8217;s <a href="https://web.archive.org/web/20131004234920/https://developers.google.com/chrome-developer-tools/">Developer Tools</a> are awesome. Chrome Canary typically has the freshest features in regards to profiling, and Safari also has many of the same. The features most of interest to this entry are the Timeline &rarr; Frames view, and the gear icon&#8217;s &#8220;Show Paint rectangles&#8221; and &#8220;Show composited layer borders&#8221; options.</p> <p class="figure"> <img src="https://web.archive.org/web/20131004234920im_/http://farm3.staticflickr.com/2856/8845899090_59b30986bd_b.jpg" width="1024" height="612" alt=""><br/> <span class="caption"><br/> Timeline &rarr; Frames view: Helpful in identifying expensive painting operations.<br/> </span> </p> <p class="figure"> <img src="https://web.archive.org/web/20131004234920im_/http://farm4.staticflickr.com/3828/8858723401_122cff05ec_b.jpg" width="1024" height="612" alt=""><br/> <span class="caption"><br/> Paint rectangles + composited layer borders, AKA &#8220;Plaid mode.&#8221; Visually identify layers.<br/> </span> </p> <h3>Timeline &rarr; Frames view</h3> <p>Timeline&#8217;s &#8220;Frames&#8221; view allows you to see how much time is spent calculating and drawing each frame during runtime, which gives a good idea of rendering performance. To maintain a refresh rate of 60 frames per second, each frame must take no longer than 16 milliseconds to draw. If you have JS doing expensive things during scroll events, this can be particularly challenging.</p> <p>Expensive frames in Flickr&#8217;s case stem primarily from occasional decoding of JPEGs and non-cached image resizes, and more frequently, compositing and painting. The less of each that your page requires, the better.</p> <h3>Paint rectangles</h3> <p>It is interesting to see what content is being painted (and re-painted) by the browser, particularly during scroll events. Enabling paint rectangles in Chrome&#8217;s dev tools results in red highlights on paint areas. In the ideal case when scrolling, you should see red only around the scrollbar area; in the best scenario, the browser is able to efficiently move the rest of the HTML content in hardware-accelerated fashion, effectively sliding it vertically on the screen without having to perform expensive paint operations. Script-based DOM updates, CSS <code>:hover</code> and transition effects can all cause painting to happen when scrolling, so keep these things in mind as well.</p> <h3>Composited layer borders</h3> <p>As mentioned previously, layers are elements that have been promoted and are to be composited by the GPU, via CSS properties like <code>translate3d()</code>, <code>translateZ()</code> and so on. With layer borders enabled, you can get a visual representation of promoted elements and review whether your CSS is too broad or too specific in creating these layers. The browser itself may also create additional layers based on a number of scenarios such as the presence of child elements, siblings, or elements that overlap an existing layer.</p> <p>Composited borders are shown in brown. Cyan borders indicate a tile, which combines with other tiles to form a larger composited layer.</p> <h2>Other notes</h2> <h3>Image rendering costs</h3> <p>When using parallax effects, &#8220;full-bleed&#8221; images that cover the entire page width are also popular. One approach is to simply use a large centered background image for all clients, regardless of screen size; alternately, a responsive <code>@media</code> query-style approach can be taken where separate images are cut to fit within common screen widths like 1024, 1280, 1600, 2048 etc. In some cases, however, the single-size approach can work quite nicely.</p> <p>In the case of the Flickr homepage, the performance cost in using 2048-pixel-wide background images for all screens seemed to be negligible &#8211; even in spite of &#8220;wasted&#8221; pixels for those browsing at 1024&#215;768. The approach we took uses clip-friendly content, typically a centered &#8220;hero&#8221; element with shading and color that extends to the far edges. Using this approach, the images are quite width-agnostic. The hero-style images also compress quite nicely as JPEGs thanks to their soft gradients and lighting; as one example, we got a 2048&#215;950-pixel image of a flower down to 68 KB with little effort.</p> <p>Bandwidth aside, the 2048-pixel-wide images clip nicely on screens down to 1024 pixels in width and with no obvious flaws. However, Chrome&#8217;s dev tools also show that there are costs associated with decoding, compositing, re-sizing and painting images which should be considered.</p> <p>Testing on my work laptop*, &#8220;Image resized (non-cached)&#8221; is occasionally shown in Chrome&#8217;s timeline &rarr; frames view after an Image Decode (JPEG) operation &#8211; both of which appear to be expensive, contributing to a frame that took 60 msec in one case. It appears that this happens the first time a large parallax image is scrolled into the viewport. It is unclear why there is a resize (and whether it can be avoided), but I suspect it&#8217;s due to the retina display on this particular laptop. I&#8217;m not using <code>background-size</code> or otherwise applying scaling/sizing in CSS, merely positioning eg., <code>background-position:50% 50%;</code> and <code>background-repeat: no-repeat;</code>. As curiosity sets in, this author will readily admit he has some more research to do on this front. <img src="https://web.archive.org/web/20131004234920im_/http://s1.wp.com/wp-includes/images/smilies/icon_wink.gif?m=1129645325g" alt=";)" class="wp-smiley"/> </p> <p>There are also aspects to RAM and caching that can affect GPU performance. I did not dig deeply into this specifically for Flickr&#8217;s new homepage, but it is worth considering the impact of the complexity and number of layers present and active at any given time. If regular scrolling triggers non-cached image resizes each time an asset is scrolled into view, there may be a cache eviction problem stemming from having too many layers active or present at once.</p> <p class="aside" style="font-size:.95em;">* Work laptop: Mid-2012 15&#8243; Retina MBP, 16 GB RAM, 2.6 Ghz Intel i7, NVIDIA GeForce GT 650M/1024 MB, OS X 10.8.3.</p> <h3>Debugging in action</h3> <p>Here are two videos showing performance of flickr.com/new/ with transforms disabled and enabled, respectively.</p> <h3>Transforms off (marginTop-based parallax)</h3> <object type="application/x-shockwave-flash" width="640" height="361" data="https://web.archive.org/web/20131004234920im_/http://www.flickr.com/apps/video/stewart.swf?v=1.161" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"> <param name="flashvars" value="photo_id=8845365987&amp;photo_secret=cfe2155de7&amp;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/20131004234920oe_/http://www.flickr.com/apps/video/stewart.swf?v=1.161" bgcolor="#000000" allowfullscreen="true" flashvars="photo_id=8845365987&amp;photo_secret=cfe2155de7&amp;flickr_show_info_box=true" wmode="opaque" height="361" width="640"></embed></object> <p>Notice the huge spikes on the timeline with transforms disabled, indicating many frames that are taking up to 80 msec to draw; this is terrible for performance, &#8220;blowing the frame budget&#8221; of 16 ms, and lowers UI responsiveness significantly. Red paint rectangles indicate that the whole viewport is being repainted on scroll, a major contributor to performance overhead. With compositing borders, you see that every &#8220;strip&#8221; of the page &#8211; each parallax background, in effect &#8211; is rendered as a single layer. A quick check of the FPS meter and &#8220;continuous repaint&#8221; graphs does not look great.</p> <p class="aside"><b>Side note:</b> Continuous repaint is most useful when not scrolling. The feature causes repeated painting of the viewport, and displays an FPS graph with real-time performance numbers. You can go into the style editor while continuous repaint is on and flip things off, e.g., disabling <code>box-shadow</code>, <code>border-radius</code> or hiding expensive elements via <code>display</code> to see if the frame rate improves.</p> <h3>Transforms on (<code>translate3d()</code>-based parallax)</h3> <object type="application/x-shockwave-flash" width="639" height="364" data="https://web.archive.org/web/20131004234920im_/http://www.flickr.com/apps/video/stewart.swf?v=1.161" classid="clsid:D27CDB6E-AE6D-11cf-96B8-444553540000"> <param name="flashvars" value="photo_id=8845359795&amp;photo_secret=5fa8467374&amp;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/20131004234920oe_/http://www.flickr.com/apps/video/stewart.swf?v=1.161" bgcolor="#000000" allowfullscreen="true" flashvars="photo_id=8845359795&amp;photo_secret=5fa8467374&amp;flickr_show_info_box=true" wmode="opaque" height="364" width="639"></embed></object> <p>With GPU acceleration, you see much-improved frame times, thus a higher framerate and a smoother, more-responsive UI. There is still the occasional spike when a new image scrolls into view, but there is much less jank overall than before. Paint rectangles are much less common thanks to the GPU-accelerated compositing, and layer borders now indicate that more individual elements are being promoted. The FPS meter and continuous repaint mode does have a few dips and spikes, but performance is notably improved.</p> <p>You may notice that I intentionally trigger video playback in this case, to see how it performs. The flashing red is the result of repainting as the video plays back &#8211; and in spite of <code>overflow: hidden</code>-based clipping we apply for the parallax effect on the video, it&#8217;s interesting to notice that the overflowed content, while not visible, is also being painted.</p> <h2>Miscellany</h2> <h3>Random bits: HTML5 &lt;video&gt;</h3> <p class="figure"> <img src="https://web.archive.org/web/20131004234920im_/http://farm3.staticflickr.com/2865/8859573578_a1777dc0db_c.jpg" width="800" height="273" alt=""><br/> <span class="caption"><br/> A frame from a .webm and H.264-encoded video, shown in the mobile portion of Flickr&#8217;s redesigned home page.<br/> </span> </p> <p>We wanted the signed-out Flickr homepage to highlight our mobile offerings, including an animation or video showing a subtly-rotating iPhone demoing the Flickr iOS app on a static background. Instead of an inline video box, it felt appropriate to have a full-width video following the pattern used for the parallax images. The implementation is nearly the same, and simply uses a &lt;video&gt; element instead of a CSS background.</p> <p>With video, the usual questions came up around performance and file size: Would a 2048-pixel-wide video be too heavy for older computers to play back? What if we cropped the video only to be as wide as needed to cover the area being animated (eg., 500 pixels), and used a static JPEG background to fill in the remainder of the space?</p> <p>As it turned out, encoding a 2048&#215;700 video and positioning it like a background element &#8211; even including a slight parallax effect &#8211; was quite reasonable. Playback was flawless on modern laptops and desktops, and even a 2006-era 1.2 GHz Fujitsu laptop running WinXP was able to run the video at reasonable speed. Per rendering documentation from the Chrome team, &lt;video&gt; elements are automatically promoted to layers for the GPU where applicable and thus benefit from accelerated rendering. Due to the inline nature of the video, we excluded it from display on mobile devices, and show a static image to clients that don&#8217;t support HTML5 video.</p> <p>Perhaps the most interesting aspect of the video was file size. In our case, the WebM-encoded video (supported natively in Chrome, Firefox, and Opera) was clearly able to optimize for the low amount of motion within the wide frame, as eight seconds of 2048&#215;700 video at 24 fps resulted in a <code>.webm</code> file of only <i>900 KB</i>. Impressive! By comparison, the H.264-encoded equivalent ended up being about 3.8 MB, with a matching data rate of ~3.8 mbps.</p> <h3>The &#8220;Justified&#8221; View</h3> <p>It&#8217;s worth mentioning that the Justified photos at the bottom of the page lazy-load in, and have been excluded from any additional display optimizations in this case. There is an initial spike with the render and subsequent loading of images, but things settle down pretty quickly. Blindly assigning translate-type transforms to the Justified photo container &#8211; a complex beast in and of itself &#8211; causes all sorts of rendering hell to break loose.</p> <h2>In Review</h2> <p>This article represents my findings and approach to getting GPU-accelerated compositing working for background images in the Webkit-based Chrome and Safari browsers, in May 2013. With ever-changing rendering engines getting smarter over time, your mileage may vary as the best route to the &#8220;fast path&#8221; changes. As numerous other articles have said regarding performance, &#8220;don&#8217;t <i>guess</i> it, <u>test</u> it.&#8221;</p> <p>To recap:</p> <ul> <li><b>Painting</b>: Expensive. Repaints should be minimal, and limited to small areas. Reduce by carefully choosing layers.</li> <li><b>Compositing</b>: Good! Fast when done by the GPU. <li><b>Layers</b>: The secret to speed, when done correctly. Apply sparingly.</li> </ul> <p>References / further reading:</p> <ul> <li><a href="https://web.archive.org/web/20131004234920/https://www.youtube.com/watch?v=8uAYE5G1gSs">Google I/O 2013: Web Page Design with the GPU in Mind</a> (YouTube)</li> <li><a href="https://web.archive.org/web/20131004234920/http://www.chromium.org/developers/design-documents/gpu-accelerated-compositing-in-chrome">GPU Accelerated Compositing in Chrome</a> (Chromium)</li> <li><a href="https://web.archive.org/web/20131004234920/http://www.html5rocks.com/en/tutorials/speed/layers/">Accelerated Compositing in Chrome: The Layer Model</a> (HTML5Rocks)</li> <li><a href="https://web.archive.org/web/20131004234920/http://www.html5rocks.com/en/tutorials/speed/scrolling/">Scrolling</a> (HTML5Rocks)</li> <li><a href="https://web.archive.org/web/20131004234920/http://www.html5rocks.com/en/tutorials/speed/parallax/">Parallaxin&#8217;</a> (HTML5Rocks)</li> <li><a href="https://web.archive.org/web/20131004234920/http://jankfree.org/">jankfree.org</a></li> </ul> <p>&#8230; And finally, did I mention <a href="https://web.archive.org/web/20131004234920/http://www.flickr.com/new/">we&#8217;re hiring</a>? (hint: <code>view-source</code> <img src="https://web.archive.org/web/20131004234920im_/http://s0.wp.com/wp-includes/images/smilies/icon_smile.gif?m=1129645325g" alt=":)" class="wp-smiley"/> )</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/20131004234920/http://code.flickr.net/category/performance/" title="View all posts in performance" rel="category tag">performance</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/20131004234920/http://code.flickr.net/tag/chrome/" rel="tag">chrome</a>, <a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/tag/compositing/" rel="tag">compositing</a>, <a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/tag/dev-tools/" rel="tag">dev tools</a>, <a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/tag/gpu/" rel="tag">gpu</a>, <a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/tag/hardware-acceleration/" rel="tag">hardware acceleration</a>, <a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/tag/painting/" rel="tag">painting</a>, <a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/tag/performance/" rel="tag">performance</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-2416 --> <article id="post-2365" class="post-2365 post type-post status-publish format-standard hentry category-meta"> <header class="entry-header"> <h1 class="entry-title"><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2013/03/26/using-redis-as-a-secondary-index-for-mysql/" rel="bookmark">Using Redis as a Secondary Index for&nbsp;MySQL</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2013/03/26/using-redis-as-a-secondary-index-for-mysql/" title="9:10 am" rel="bookmark"><time class="entry-date" datetime="2013-03-26T09:10:51+00:00">March 26, 2013</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/20131004234920/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>Hey, did you notice, on the brand-spanking-new <a href="https://web.archive.org/web/20131004234920/http://yahoo.com/">Yahoo homepage</a>, right there on the side of the page, it’s photos from your Flickr contacts (or maybe your groups)! No? Go check it out, I’ll wait.</p> <p>Ok, great, you’re back! What you should have seen, assuming you have Flickr contacts (or are a member of some groups), is photos from your most recently active contact (or group!). Something like&#8230; this:</p> <p class="flickr-photo"><img class="aligncenter" alt="Flickr on Yahoo.com" src="https://web.archive.org/web/20131004234920im_/http://flickrcode.files.wordpress.com/2013/03/image00.png?w=300&amp;h=202" width="300" height="202"/></p> <p style="text-align:center;">(thanks <a href="https://web.archive.org/web/20131004234920/http://www.flickr.com/photos/schill/">schill</a>!)</p> <p>What you see above is the 10 most recent photos from my contact who most recently uploaded any photos. The homepage retrieves this data by making a call to a specially tailored method from the Flickr API.</p> <h2>The Latency Problem</h2> <p>In order to ensure performance for <a href="https://web.archive.org/web/20131004234920/http://yahoo.com/">Yahoo.com</a>, this API method had very tight SLAs; we can’t slow down the page for the millions of homepage visitors to pull in Flickr content, after all. While the method can return different data sets depending on the most recent activity from your Flickr contacts and groups, for the sake of this post we’re going to focus exclusively on the contacts case. The first step to returning data is to get your most recently active contact, and to do that we need a list of all of your contacts sorted by how recently they’ve uploaded a photo. In an ideal world, the SQL query would be something along the lines of:</p> <pre class="brush: sql; gutter: true; title: ; notranslate" title=""> SELECT contact_id FROM Contacts WHERE user_id = ? ORDER BY date_upload LIMIT 1; </pre> <p>Easy, right? Sadly, no. Due to the way our contact relationships are stored, the query we need to run is more complex than the above. Still, for the vast majority of Flickr users, the *actual* SQL query performed just fine, usually in less than 1ms. However, Flickr users come in all shapes and sizes. Some users have a few contacts, some have hundreds, and some&#8230; have tens of thousands. Unfortunately, the runtime for the query scaled proportionally to number of contacts the user has. I won’t delve into the specifics of the query or how we store contact relationships, but suffice it to say we investigated possible changes to both the query and to the indexes in MySQL, but neither was sufficient to give us the performance we needed across all possible use cases. With that in mind, we had to look elsewhere for optimizations.</p> <h2>The First Attempt</h2> <p>With a pure MySQL solution off the table, our first thoughts for optimization avenues turned to the obvious: denormalization. If we stored the 10 most recent photos from your most recently active contact ahead of time, then getting that list when the homepage was rendered would be trivial regardless of how many contacts you have. At Flickr we’re <a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2012/12/12/highly-available-real-time-notifications/">big</a> <a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2012/12/21/redis-global-locks-redux/">fans</a> of <a href="https://web.archive.org/web/20131004234920/http://redis.io/">Redis</a>, so our thoughts immediately turned to using it as a store for the denormalized contact data.</p> <p>In order to denormalize the data, we need to constantly process six different user actions:</p> <ul> <li>Add a contact</li> <li>Remove a contact</li> <li>Change a contact relationship</li> <li>User uploads a photo</li> <li>User deletes a photo</li> <li>User changes a photo’s privacy</li> </ul> <p>Each one of these actions can impact what photos you should see on the Yahoo homepage, and therefore must update the denormalized data appropriately. Because the photo related actions can potentially impact thousands of users, if not tens or hundreds of thousands (everyone who calls the photo owner a contact would need to have their denormalized data updated on upload), they must be processed by our offline task system to ensure site performance is not adversely impacted. Unfortunately, this is where we ran into a snag. The nature of Flickr’s offline task system is such that the order of processing is not guaranteed. In most cases this is not a problem, however when it comes to maintaining an accurate list of denormalized data not being able to predict the outcome of running multiple tasks is problematic.</p> <h3>Out of Order</h3> <p>Imagine the following scenario: you add a contact then quickly remove them. This results in two tasks being added, one to add and one to remove. If they’re processed in the order in which the actions were taken, everything is fine. However, if they’re processed out of order then when everything is said and done the denormalized data no longer accurately reflects your actual contact relationships. Maybe the next action that updates your data will correct this problem, or maybe it will introduce another problem. Over time, it’s likely that a not-insignificant number of users will have denormalized data that is out of sync with reality.</p> <p class="flickr-photo"> <a title="Out of order from Foomandoonian, on Flickr" href="https://web.archive.org/web/20131004234920/http://www.flickr.com/photos/sockrotation/4122944724/"><br/> <img alt="Out of order" src="https://web.archive.org/web/20131004234920im_/http://farm3.staticflickr.com/2537/4122944724_f4e0e37d5a_b.jpg" width="800" height="534"/><br/> </a><br/> <span class="caption"><a href="https://web.archive.org/web/20131004234920/http://www.flickr.com/photos/sockrotation/4122944724/">Out of order</a> by <a href="https://web.archive.org/web/20131004234920/http://www.flickr.com/photos/sockrotation/">Foomandoonian</a></span></p> <h2>Ok, let’s try this another way</h2> <p>Looking back at our original problem, the bottleneck was not in generating the list of photos for your most recently active contact, it was just in finding who your most recently active contact was (specifically if you have thousands or tens of thousands of contacts). What if, instead of fully denormalizing, we just maintain a list of your recently active contacts? That would allow us to optimize the slow query, much like a native MySQL index would; instead of needing to look through a list of 20,000 contacts to see which one has uploaded a photo recently, we only need to look at your most recent 5 or 10 (regardless of your total contacts count)!</p> <p>In the end, this is exactly what we ended up doing. We still process offline tasks to keep track of your contacts’ activity, but now the set of actions we need to track is smaller:</p> <ul> <li>Add a new contact</li> <li>Remove a contact</li> <li>User uploads a photo</li> </ul> <p>Each task maintains a per-user <a href="https://web.archive.org/web/20131004234920/http://redis.io/topics/data-types#sorted-sets">sorted set</a> where the set member is the contact id, and the score is the timestamp of when that contact last uploaded a photo. So, for example, if a user (user id 12345) adds a new contact (user id 98765) we simply do:</p> <pre class="brush: plain; gutter: true; title: ; notranslate" title=""> ZADD user_12345 1363665432 98765 </pre> <p>Removing a contact is the opposite, using <a href="https://web.archive.org/web/20131004234920/http://redis.io/commands/zrem">ZREM</a> as the yin to <a href="https://web.archive.org/web/20131004234920/http://redis.io/commands/zadd">ZADD</a>&#8216;s yang:</p> <pre class="brush: plain; gutter: true; title: ; notranslate" title=""> ZREM user_12345 98765 </pre> <p>When a user uploads a new photo it’s once again a ZADD (though it’s a ZADD against the set for every user that calls the uploading user a contact). If the uploader is already in a given user’s set, this will simply update the score, if not then the uploader will be added to the set.</p> <p>As things stand right now, the set will grow without bound, which is obviously not much help if our goal is to limit the number of contacts we need to check against when querying the DB. The solution to this is to cap the set. After each ZADD we check to see if the size of the set has exceeded a threshold (generally we store an additional 20% on top of the data we absolutely need), and if so, we remove all of the extra records using <a href="https://web.archive.org/web/20131004234920/http://redis.io/commands/zremrangebyrank">ZREMRANGEBYRANK</a>:</p> <pre class="brush: plain; gutter: true; title: ; notranslate" title=""> ZREMRANGEBYRANK user_12345 0 ($collection_size - $max_size) - 1 </pre> <p>Where $collection_size is the current number of members in the set and $max_size is the maximum number of members we want to store. Note that we’re removing from the head of the set. Redis stores data in sorted sets in ascending order, so the least recently active contacts are at the beginning of the set.</p> <p>Akin to how we must cap the set to keep it below a maximum size, we also have a threshold on the other end of the spectrum to keep the set above a minimum size. If a user happens to remove their 10 most recently active contacts then there would be no data in their set, and the Redis index would be of little value. With that in mind, any time the user removes a contact, we check to see if the size of the set has dropped under the minimum threshold, and if so we repopulate the data based on their remaining contacts. This is slightly more complex than a simple Redis command, so we’ll use actual PHP to explain:</p> <pre class="brush: php; gutter: true; title: ; notranslate" title=""> // // $key is the redis ZSET key, e.g. user_12345 // $count = redis_zset_zcard($key); if ($count &lt; $MIN_SET_SIZE) { if ($count &gt; 0) { $current_contacts = redis_zset_zrange($key, 0, -1); } else { $current_contacts = array(); } // // In this call, the second parameter is the number of contacts // to return and the third parameter is a list of users to // exclude from the response. Since they’re already in the // set, there’s no need to add them again // $contacts = contacts_most_recently_uploaded_list($user, $MAX_SET_SIZE - $count, $current_contacts); foreach ($contacts as $contact) { redis_zset_zadd($key, $contact['last_upload'], $contact['id']); } } </pre> <p>Remember, this is all being done outside of the context of a page request, so there’s no harm in spending a little bit of extra time to ensure when the API is called we have a reliable index that we can use to optimize the DB query.</p> <p>The final action we need to take is to actually query the set to get the list of contacts so we can actually do said DB query optimization. This is done with a standard <a href="https://web.archive.org/web/20131004234920/http://redis.io/commands/zrevrange">ZREVRANGE</a>:</p> <pre class="brush: plain; gutter: true; title: ; notranslate" title=""> ZREVRANGE user_12345 0 10 </pre> <p>Similar to how we cap the set by removing members from the beginning because those are the least recently active, when we want the most recently active we use ZREVRANGE to get members at the end of the set.</p> <p>You’re probably wondering, what about the other three events that generated tasks for the fully denormalized solution? How are we able to get by without them? Well, because we’ll just be using the list of contacts to optimize a live DB query, we can take some liberties with data purity in Redis. Because we store multiple recently active contacts, it doesn’t matter if, for example, the most recent contact in your Redis set has deleted his most recent photos or made them all private. When we query the DB, we further restrict the list based on your current relationship with a contact and photo-visibility, so any issues with Redis being out of sync sort themselves out automatically.</p> <p>Take the above scenario where a user adds and quickly removes a contact. If the remove is processed first and the contact remains in the user’s Redis set, when we go to query the live contacts DB, we’ll get no results for that relationship and move on to the next contact in the set—crisis averted. There is a slight problem with the reverse scenario wherein a user removes a contact and then adds them back. If the tasks are processed out of order, there’s a chance that the add may not end up being reflected in the Redis set. This is the only chance for corruption under this system (as opposed to the fully denormalized solution where many tasks could interact and corrupt one another), and it’s likely to be fixed the next time any of the other actions occur. Furthermore, the only downside of this corruption is a user seeing photos from their second most recently active contact; this is a condition we’re willing to live with given the overall gains provided by the solution as a whole.</p> <h2>Not All Wine and Roses</h2> <p>The dataset involved in this solution is one of the largest we’ve pushed at Redis so far, and it’s not without its pitfalls. Namely, as the size of the dataset increases, the amount of time spent doing RDB saves also increases. This can introduce latency into Redis commands while the save is in progress. From <a href="https://web.archive.org/web/20131004234920/http://redis.io/topics/persistence">Redis Persistence</a>:</p> <blockquote cite="http://redis.io/topics/persistence"><p>RDB needs to fork() often in order to persist on disk using a child process. Fork() can be time consuming if the dataset is big, and may result in Redis to stop serving clients for some millisecond or even for one second if the dataset is very big and the CPU performance not great. AOF also needs to fork() but you can tune how often you want to rewrite your logs without any trade-off on durability.</p></blockquote> <p>This is a key factor to be aware of when optimizing Redis performance. The overall speed of the system can be adversely impacted by RDB saves, therefore taking steps to minimize the time spent saving is critical. We’ve solved this problem by isolating these writes to their own Redis instance, thereby limiting the size of the dataset to only keys related to contacts activity. In the long term, as activity increases, it’s likely that we’ll need to further reduce the number of writes-per-instance by sharding this contact activity to a number of Redis instances. In some cases, multiple small Redis instances running on a single host can be preferable to one large Redis instance. As mentioned in the Redis Persistence guide, RDB saves can suffer with a slow CPU; if upgrading hardware (mostly faster CPUs to improve fork() performance), is possible, that’s certainly another option to investigate.</p> <div class="hiring-banner"> <p class="group-photo"><a title="Flickr flamily floto by morozgrafix, on Flickr" href="https://web.archive.org/web/20131004234920/http://www.flickr.com/photos/morozgrafix/7803402076/"><img alt="Flickr flamily floto" src="https://web.archive.org/web/20131004234920im_/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/20131004234920/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/20131004234920/http://code.flickr.net/category/meta/" title="View all posts in meta" rel="category tag">meta</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-2365 --> <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/20131004234920/http://code.flickr.net/2012/12/21/redis-global-locks-redux/" rel="bookmark">Redis Global Locks&nbsp;Redux</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://web.archive.org/web/20131004234920/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/20131004234920/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/20131004234920/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/20131004234920/http://www.flickr.com/photos/kuemmi/3460993750/"><img alt="Locks #1" src="https://web.archive.org/web/20131004234920im_/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/20131004234920/http://www.flickr.com/photos/kuemmi/3460993750/">Locks #1</a> by <a href="https://web.archive.org/web/20131004234920/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/20131004234920/https://twitter.com/heyjoshua">heyjoshua</a> I might be missing something but the code looks like it&#039;ll keep trying to acquire the lock, which it&#039;ll can&#039;t, due to the SETNX.&mdash; <br/>Nolan Caudill (@nolancaudill) <a href="https://web.archive.org/web/20131004234920/http://twitter.com/#!/nolancaudill/status/279814860227870720" data-datetime="2012-12-15T05:07:16+00:00">December 15, 2012</a></p></blockquote> <p>&nbsp;</p> <h2>The Problem</h2> <p><a href="https://web.archive.org/web/20131004234920/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(&amp;quot;Error trying to acquire redis lock for: %s&amp;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/20131004234920/http://redis.io/topics/transactions">transactions</a>. By using a combination of <a href="https://web.archive.org/web/20131004234920/http://redis.io/commands/watch">WATCH</a>, <a href="https://web.archive.org/web/20131004234920/http://redis.io/commands/multi">MULTI</a>, and <a href="https://web.archive.org/web/20131004234920/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 &amp;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, { &amp;quot;acquired&amp;quot; : true, &amp;quot;lockIdentifier&amp;quot; : lockIdentifier, &amp;quot;payload&amp;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 &#8220;only&#8221; 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/20131004234920/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(&amp;quot;redis-locking-worker”); var SUCCESS_CHANCE = 0.15; var lock = new RedisLockingWorker({ &amp;quot;lockKey&amp;quot; : &amp;quot;mylock&amp;quot;, &amp;quot;statusLevel&amp;quot; : RedisLockingWorker.StatusLevels.Verbose, &amp;quot;lockTimeout&amp;quot; : 5000, &amp;quot;maxAttempts&amp;quot; : 5 }); lock.on(&amp;quot;acquired&amp;quot;, function(lastAttempt) { if (Math.random() &amp;lt;= SUCCESS_CHANCE) { console.log(&amp;quot;Completed work successfully!&amp;quot;, lastAttempt); lock.done(lastAttempt); } else { // oh no, we failed to do work! console.log(&amp;quot;Failed to do work&amp;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(&amp;quot;locked&amp;quot;, function() { console.log(&amp;quot;Did not acquire lock, someone beat us to it&amp;quot;); }); lock.on(&amp;quot;error&amp;quot;, function(error) { console.error(&amp;quot;Error from lock: %j&amp;quot;, error); }); lock.on(&amp;quot;status&amp;quot;, function(message) { console.log(&amp;quot;Status message from lock: %s&amp;quot;, message); }); </pre> <h2>More Bonus!</h2> <p>If you don&#8217;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/20131004234920/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/20131004234920/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/20131004234920/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/20131004234920/http://www.flickr.com/photos/morozgrafix/7803402076/"><img alt="Flickr flamily floto" src="https://web.archive.org/web/20131004234920im_/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/20131004234920/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/20131004234920/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/20131004234920/http://code.flickr.net/tag/javascript/" rel="tag">javascript</a>, <a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/tag/node-js/" rel="tag">node.js</a>, <a href="https://web.archive.org/web/20131004234920/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/20131004234920/http://code.flickr.net/2012/12/12/highly-available-real-time-notifications/" rel="bookmark">Highly Available Real Time Push Notifications and&nbsp;You</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://web.archive.org/web/20131004234920/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/20131004234920/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/20131004234920/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&#8230; 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/20131004234920/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/20131004234920im_/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/20131004234920/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/20131004234920/http://www.flickr.com/photos/statelibraryqueensland/">State Library of Queensland, Australia</a></span></p> <p>I know, you’re thinking, &#8220;anyone can build push notifications, we’ve been doing it since 2009!&#8221; Which is, of course, absolutely true. The <a href="https://web.archive.org/web/20131004234920/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&#8230; 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/20131004234920/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/20131004234920/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/20131004234920/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/20131004234920/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/20131004234920/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/20131004234920im_/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 &#8212; 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/20131004234920im_/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(&quot;redis&quot;); 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(&quot;notification_&quot; + shard); pubsubClient.on(&quot;message&quot;, 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/20131004234920/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 = &quot;lock.&quot; + payload.identifier; function dataForCallback(acquired) { return { &quot;acquired&quot; : acquired, &quot;lockIdentifier&quot; : lockIdentifier, &quot;payload&quot; : payload, &quot;attempt&quot; : 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(&quot;Error trying to acquire redis lock for: %s&quot;, 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 &amp;&amp; data.acquired) { return sendMessage(data.payload, data.lockIdentifier, data.attempt === MAX_ATTEMPTS); } else if (data &amp;&amp; !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/20131004234920/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/20131004234920/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/20131004234920/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/20131004234920/http://www.flickr.com/photos/morozgrafix/7803402076/"><img alt="Flickr flamily floto" src="https://web.archive.org/web/20131004234920im_/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/20131004234920/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/20131004234920/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-2324" class="post-2324 post type-post status-publish format-standard hentry category-uploadr"> <header class="entry-header"> <h1 class="entry-title"><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2012/12/10/drag-n-drop/" rel="bookmark">Avoiding Dragons: A Practical Guide to Drag&nbsp;’n’&nbsp;Drop</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2012/12/10/drag-n-drop/" title="9:48 pm" rel="bookmark"><time class="entry-date" datetime="2012-12-10T21:48:04+00:00">December 10, 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/20131004234920/http://code.flickr.net/author/pdokas/" title="View all posts by Phil Dokas" rel="author">Phil Dokas</a></span></span> </div><!-- .entry-meta --> </header><!-- .entry-header --> <div class="entry-content"> <p>You, the enterprising programmer, know about <a href="/web/20131004234920/http://code.flickr.net/2012/06/01/parsing-exif-client-side-using-javascript-2/">parsing EXIF from photos</a> in the browser and even how and why to power this <a href="/web/20131004234920/http://code.flickr.net/2012/06/06/web-workers-and-yui/">parsing with web workers</a>. “Bat,” you ask yourself, “how do I get those photos into the browser in the first place?”</p> <p>The oldest and most low-tech solution is the venerable <code>&lt;input type="file" name="foo"&gt;</code>. This plops the old standby file button on your page and <code>POST</code>s the file’s contents to your server upon form submission.</p> <p>To address many of this simple control’s limitations <a href="/web/20131004234920/http://code.flickr.net/2008/04/22/making-a-better-flickr-web-uploadr-or-web-browsers-arent-good-at-uploading-files-by-themselves/">we debuted a Flash-based file uploader</a> in 2008. This workhorse has been providing per-file upload statuses, batch file selection, and robust error handling for the last four years through Flash’s file system APIs.</p> <p>These days we can thankfully <a href="/web/20131004234920/http://code.flickr.net/2012/04/25/raising-the-bar-on-web-uploads/">do this work without plugins</a>. Not only can we use <code>XHR</code> to <code>POST</code> files and provide all the other fancy info we’ve long needed Flash for, but now we can pair this with something much better than an <code>&lt;input&gt;</code>: drag and drop. This allows people drag files directly into a browser window from the iPhotos, Lightrooms, and Windows Explorers of the world.</p> <p>Let’s take a look at how this works.</p> <h2>Foundations first</h2> <p class="flickr-photo"><a href="https://web.archive.org/web/20131004234920/http://www.flickr.com/photos/boston_public_library/2387493721/" title="Workmen laying the cornerstone, construction of the McKim Building by Boston Public Library, on Flickr"><img src="https://web.archive.org/web/20131004234920im_/http://farm4.staticflickr.com/3024/2387493721_2a10e5f4b2_b.jpg" width="1024" height="760" alt="Workmen laying the cornerstone, construction of the McKim Building"/></a><span class="caption"><a href="https://web.archive.org/web/20131004234920/http://www.flickr.com/photos/boston_public_library/2387493721/">Workmen laying the cornerstone, construction of the McKim Building</a> by <a href="https://web.archive.org/web/20131004234920/http://www.flickr.com/photos/boston_public_library/">Boston Public Library</a></span></p> <p>Let’s begin with our simple fallback, a – yes – <code>&lt;input type="file"&gt;</code>.</p> <p><b>HTML</b>: <code>&lt;input type="file" multiple accept="image/*,video/*"&gt;</code><br/> <b>JS</b>: <code>Y.all('input[type=file]').on('change', handleBrowse);</code></p> <p>Here we start with an <code>&lt;input&gt;</code> that accepts multiple files and knows it only accepts images and videos. Then, we bind an event handler to its change event. That handler can be very simple:</p> <pre class="brush: jscript; gutter: false; title: ; notranslate" title=""> function handleBrowse(e) { // get the raw event from YUI var rawEvt = e._event; // pass the files handler into the loadFiles function if (rawEvt.target &amp;&amp; rawEvt.target.files) { loadFiles(rawEvt.target.files); } } </pre> <p>A simple matter of handing the event object’s file array off to our universal function that adds files to our upload queue. Let’s take a look at this file loader:</p> <pre class="brush: jscript; gutter: false; title: ; notranslate" title=""> function loadFiles(files) { updateQueueLength(count); for (var i = 0; i &lt; files.length; i++) { var file = files[i]; if (File &amp;&amp; file instanceof File) { enqueueFileAddition(file); } } } </pre> <p>Looks clear – it’s just going over the file list and adding them to a queue. “But wait,” you wonder, “why all this queue nonsense? Why not just kick off an <code>XHR</code> for the file right now?” Indeed, we’ve stuck in a layer of abstraction here that seems unnecessary. And for now it is. But suppose our pretty synchronous world were soon to become a whole lot less synchronous – that could get real fun in a hurry. For now, we’ll put that idea aside and take a look at these two queue functions themselves:</p> <pre class="brush: jscript; gutter: false; title: ; notranslate" title=""> function updateQueueLength(quantity) { state.files.total += quantity; } function enqueueFileAddition(file) { state.files.handles.push(file); // If all the files we expect have shown up, then flush the queue. if (state.files.handles.length === state.files.total) { for (var i = 0, len = state.files.total; i &lt; len; i++) { addFile(state.files.handles[i]); } // reset the state of the world state.files.handles = []; state.files.total = 0; } } </pre> <p>Pretty straightforward. One function for leaving a note of how many files we expect, one function to add files and see if we have all the files we expect. If so, pass along everything we have to <code>addFile()</code> which sends the file into our whirlwind of <code>XHR</code>s heading off to <a href="/web/20131004234920/http://code.flickr.net/2009/03/03/panda-tuesday-the-history-of-the-panda-new-apis-explore-and-you/">the great pandas in the sky</a>.</p> <h2>Droppin’ dragons</h2> <p class="flickr-photo"><a href="https://web.archive.org/web/20131004234920/http://www.flickr.com/photos/dokas/8252092817/" title="Droppin’ dragons by Phil Dokas, on Flickr"><img src="https://web.archive.org/web/20131004234920im_/http://farm9.staticflickr.com/8346/8252092817_3d298cd5d3_c.jpg" width="800" height="600" alt="Droppin’ dragons"/></a><span class="caption"><a href="https://web.archive.org/web/20131004234920/http://www.flickr.com/photos/dokas/8252092817/">Droppin’ dragons</a> by <a href="https://web.archive.org/web/20131004234920/http://www.flickr.com/photos/dokas/">Phil Dokas</a></span></p> <p>While all of that is well and good, it was all for a ho-hum <code>&lt;input&gt;</code> element! Let’s hook a modern browser’s drag and drop events into this system:</p> <pre class="brush: jscript; gutter: false; title: ; notranslate" title=""> document.addEventListener('drop', function(e) { if (e.dataTransfer &amp;&amp; e.dataTransfer.files) { loadFiles(e.dataTransfer.files); } }); </pre> <p>The <a href="https://web.archive.org/web/20131004234920/http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html">drag and drop API</a> is a fairly complicated one, but it thankfully makes the task of reading files out of a drop event easy. Every drop will have a <code>dataTransfer</code> attribute and when there’s at least one file in the drag that member will itself have a <code>files</code> attribute.</p> <p>In fact, when you’re only concerned about handling files dragged directly into the browser you could call it a day right here. The <code>loadFiles()</code> function we wrote earlier knows how to handle instances of the <code>File</code> class and that’s exactly what <code>dataTransfer.files</code> stores. Easy!</p> <h2>Put it up to eleven</h2> <p>While easy is a good thing, awesome is awesome. How could we make dragging files into a browser even better? Well, how about cutting down on the trouble of finding the folder with your photos somewhere on your desktop, opening it, and then dragging those files into the browser? What if we could just drag the folder in and call it a day?</p> <p class="flickr-photo"><a href="https://web.archive.org/web/20131004234920/http://www.flickr.com/photos/rkimpeljr/439400672/" title="goes to 11 by Rick Kimpel, on Flickr"><img src="https://web.archive.org/web/20131004234920im_/http://farm1.staticflickr.com/158/439400672_5c51fae079_b.jpg" width="1024" height="683" alt="goes to 11"/></a><span class="caption"><a href="https://web.archive.org/web/20131004234920/http://www.flickr.com/photos/rkimpeljr/439400672/">goes to 11</a> by <a href="https://web.archive.org/web/20131004234920/http://www.flickr.com/photos/rkimpeljr/">Rick Kimpel</a></span></p> <p>Try to drag a folder into the browser with the current state of our code; what happens? Our code tells the browser to treat all dropped file system objects as files. So what ultimately happens for folders is a very elaborate “nothing”. To fix this, we need to tell the browser how to handle directories. In our case, we want it to recursively walk every directory it sees and pick out the photos from each.</p> <p>From here on out we’re going to be treading over tumultuous land, rife with rapidly changing specs and swiftly updating browsers. This becomes immediately apparent in how we begin to add support for directories. We need to update our drop event handler like this:</p> <pre class="brush: jscript; gutter: false; title: ; notranslate" title=""> document.addEventListener('drop', function(e) { if (e.dataTransfer &amp;&amp; e.dataTransfer.items) { loadFiles(e.dataTransfer.items); } else if (e.dataTransfer &amp;&amp; e.dataTransfer.files) { loadFiles(e.dataTransfer.files); } }); </pre> <p>Items? Files? The difference is purely a matter of one being the newer interface where development happens and the other being the legacy interface. This is <a href="https://web.archive.org/web/20131004234920/http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#the-datatransfer-interface">spelled out a bit in the spec</a>, but the short of it is that the <code>files</code> member will be kept around for backwards compatibility while newer abilities will be built in the <code>items</code> namespace. Our code above prefers to use the <code>items</code> attribute if available, while falling back to <code>files</code> for compatibility. The real fun is what comes next.</p> <p>You see, the <code>items</code> namespace deals with <a href="https://web.archive.org/web/20131004234920/http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#the-datatransferitem-interface">Items</a>, not <a href="https://web.archive.org/web/20131004234920/http://dev.w3.org/2006/webapi/FileAPI/#file">Files</a>. Items can be thought of as pointers to things in the file system. Thankfully, that includes the directories we’re after. But unfortunately, this is the file system and the file system is slow. And JavaScript is single-threaded. These two facts together are a recipe for latency. The File System API tackles this problem with the same solution as Node.js: asynchronicity. Most of the functions in the API accept a callback that will be invoked when the disk gets around to providing the requested files. So we’ll have to update our code to do two new things: 1) translate items into files and 2) handle synchronous and asynchronous APIs.</p> <p>So what do these changes look like? Let’s turn back to <code>loadFiles()</code> and teach it how to handle these new types of files. Taking a look at <a href="https://web.archive.org/web/20131004234920/http://www.whatwg.org/specs/web-apps/current-work/multipage/dnd.html#the-datatransferitem-interface">the spec for the Item class</a>, there appears to be a <code>getAsFile()</code> function and that sounds perfect.</p> <pre class="brush: jscript; gutter: false; title: ; notranslate" title=""> function loadFiles(files) { updateQueueLength(count); for (var i = 0; i &lt; files.length; i++) { var file = files[i]; if (typeof file.getAsFile === 'function') { enqueueFileAddition(file.getAsFile()); } else if (File &amp;&amp; file instanceof File) { enqueueFileAddition(file); } } } </pre> <p>Easy – but, there’s a problem. The <code>getAsFile()</code> function is very literal. It assumes the Item points to a file. But directories aren’t files and that means this method won’t meet our needs. Fortunately, there is a solution and that’s through yet another data type, <a href="https://web.archive.org/web/20131004234920/http://www.w3.org/TR/file-system-api/#the-entry-interface">the Entry</a>. An Entry is much like a File, but it can also represent directories. As mentioned in <a href="https://web.archive.org/web/20131004234920/http://wiki.whatwg.org/wiki/DragAndDropEntries">this WHATWG wiki document</a>, there is a proposed method, <code>getAsEntry()</code>, in the Item interface that allows you to grab an Entry for its file system object. It’s browser prefixed for now, so let’s add that in as well.</p> <pre class="brush: jscript; gutter: false; title: ; notranslate" title=""> function loadFiles(files) { updateQueueLength(count); for (var i = 0; i &lt; files.length; i++) { var file = files[i]; var entry; if (file.getAsEntry) { entry = file.getAsEntry(); } else if (file.webkitGetAsEntry) { entry = file.webkitGetAsEntry(); } else if (typeof file.getAsFile === 'function') { enqueueFileAddition(file.getAsFile()); } else if (File &amp;&amp; file instanceof File) { enqueueFileAddition(file); } } } </pre> <p>So what we have now is a way of handling native files and a way of turning Items into Entries. Now we need to figure out if the Entry is a file or a directory and then handle that appropriately.</p> <p>What we’ll do is queue up any File objects we run across and skip the loop ahead to the next object. But if we have an Item and successfully turn it into an Entry then we’ll try to resolve this down to a file or a directory.</p> <pre class="brush: jscript; gutter: false; title: ; notranslate" title=""> function loadFiles(files) { updateQueueLength(count); for (var i = 0; i &lt; files.length; i++) { var file = files[i]; var entry, reader; if (file.getAsEntry) { entry = file.getAsEntry(); } else if (file.webkitGetAsEntry) { entry = file.webkitGetAsEntry(); } else if (typeof file.getAsFile === 'function') { enqueueFileAddition(file.getAsFile()); continue; } else if (File &amp;&amp; file instanceof File) { enqueueFileAddition(file); continue; } if (!entry) { updateQueueLength(-1); } else if (entry.isFile) { entry.file(function(file) { enqueueFileAddition(file); }, function(err) { console.warn(err); }); } else if (entry.isDirectory) { reader = entry.createReader(); reader.readEntries(function(entries) { loadFiles(entries); updateQueueLength(-1); }, function(err) { console.warn(err); }); } } } </pre> <p>The code is getting long, but we’re almost done. Let’s unpack this.</p> <p>The first branch of our new Entry logic ensures that what was returned by <code>webkitGetAsEntry()</code>/<code>getAsEntry()</code> is something useful. When they error they return <code>null</code> and this will happen if an application provides data in the drop event that isn’t a file. To see this in action try dragging a few files in from Preview in Mac OS X – it’s odd behavior, but this adequately cleans it up.</p> <p>Next we handle files. <a href="https://web.archive.org/web/20131004234920/http://www.w3.org/TR/file-system-api/#the-entry-interface">The Entry spec</a> provides the brilliantly simple <code>isFile</code> and <code>isDirectory</code> attributes. These guarantee whether you have a <a href="https://web.archive.org/web/20131004234920/http://www.w3.org/TR/file-system-api/#the-fileentry-interface">FileEntry</a> or a <a href="https://web.archive.org/web/20131004234920/http://www.w3.org/TR/file-system-api/#the-directoryentry-interface">DirectoryEntry</a> on your hands. These classes have useful – though as promised, asynchronous – methods and here we use FileEntry’s <code>file()</code> method and enqueue its returned file.</p> <p>Finally, the unicorn we’re chasing – handling directories. This is a tad more complicated, but the idea is straightforward. We create a <a href="https://web.archive.org/web/20131004234920/http://www.w3.org/TR/file-system-api/#the-directoryreader-interface">DirectoryReader</a> which lets us read its contents through its <code>readEntries()</code> method which provides an array of Entries. And what do we do with these Entries? We recursively call our <code>loadFiles()</code> function with them! In this step we achieve recursively walking a branch of the file system and rooting out every available image. Finally, we decrement the count of expected files by 1 to indicate that this was a directory and it has now been suitably handled.</p> <p>But there is one more thing.</p> <p>In that final directory reading step we recursively called <code>loadFiles()</code> with an array of <em>Entries</em>. As of right now, this function only expects to handle Files and Items. Let’s patch up this oversight, add a final bit of error handling, and call it a day.</p> <pre class="brush: jscript; gutter: false; title: ; notranslate" title=""> function loadFiles(files) { updateQueueLength(count); for (var i = 0; i &lt; files.length; i++) { var file = files[i]; var entry, reader; if (file.isFile || file.isDirectory) { entry = file; } else if (file.getAsEntry) { entry = file.getAsEntry(); } else if (file.webkitGetAsEntry) { entry = file.webkitGetAsEntry(); } else if (typeof file.getAsFile === 'function') { enqueueFileAddition(file.getAsFile()); continue; } else if (File &amp;&amp; file instanceof File) { enqueueFileAddition(file); continue; } else { updateQueueLength(-1); continue; } if (!entry) { updateQueueLength(-1); } else if (entry.isFile) { entry.file(function(file) { enqueueFileAddition(file); }, function(err) { console.warn(err); }); } else if (entry.isDirectory) { reader = entry.createReader(); reader.readEntries(function(entries) { loadFiles(entries); updateQueueLength(-1); }, function(err) { console.warn(err); }); } } } </pre> <p>All we need to do to handle an Entry is to rely on the fact that Entries have those oh-so-helpful <code>isFile</code> and <code>isDirectory</code> attributes. If we see those we know we have an Entry of one type or another and we know how to work with them, so just skip on down to the FileEntry and DirectoryEntry handling code.</p> <p>And that, finally, is it. There are many specs with very new data types at play here, but through this turmoil we can achieve some very nice results never before possible in browsers.</p> <h3>Further reading</h3> <ul> <li><a href="https://web.archive.org/web/20131004234920/http://www.html5rocks.com/en/tutorials/file/filesystem/">Exploring the Filesystem APIs</a>, by Eric Bidelman, Google</li> <li><a href="https://web.archive.org/web/20131004234920/http://updates.html5rocks.com/2012/07/Drag-and-drop-a-folder-onto-Chrome-now-available">Drag and Drop a Folder onto Chrome Now Available</a>, by Eiji Kitamura, Google</li> <li><a href="https://web.archive.org/web/20131004234920/http://www.google.com/url?q=http%3A%2F%2Fwww.whatwg.org%2Fspecs%2Fweb-apps%2Fcurrent-work%2Fmultipage%2Fdnd.html&amp;sa=D&amp;sntz=1&amp;usg=AFQjCNHqKBNtqsbeETblNeT9FmctdY0MXw">WHATWG’s HTML Drag and Drop spec</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://www.w3.org/TR/FileAPI/">W3C’s File API spec</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://www.w3.org/TR/file-system-api/">W3C’s File API: Directories and System spec</a></li> </ul> <div class="hiring-banner"> <p class="group-photo"><a href="https://web.archive.org/web/20131004234920/http://www.flickr.com/photos/morozgrafix/7803402076/" title="Flickr flamily floto by morozgrafix, on Flickr"><img src="https://web.archive.org/web/20131004234920im_/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/20131004234920/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/20131004234920/http://code.flickr.net/category/uploadr/" title="View all posts in uploadr" rel="category tag">uploadr</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-2324 --> <article id="post-2290" class="post-2290 post type-post status-publish format-standard hentry category-event"> <header class="entry-header"> <h1 class="entry-title"><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2012/10/26/flickr-at-sf-web-performance/" rel="bookmark">Flickr at SF Web&nbsp;Performance</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2012/10/26/flickr-at-sf-web-performance/" title="5:59 pm" rel="bookmark"><time class="entry-date" datetime="2012-10-26T17:59:37+00:00">October 26, 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/20131004234920/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 class="flickr-photo"> <a href="https://web.archive.org/web/20131004234920/http://www.flickr.com/photos/schill/8124204694/" title="Wait! Did you say they all run Webkit? by Schill, on Flickr"><img src="https://web.archive.org/web/20131004234920im_/http://farm9.staticflickr.com/8188/8124204694_297d7ec000_c.jpg" width="800" height="534" alt="Wait! Did you say they all run Webkit?"></a><br/> <span class="caption"><a href="https://web.archive.org/web/20131004234920/http://www.flickr.com/photos/schill/8124204694/">Wait! Did you say they all run Webkit?</a> by <a href="https://web.archive.org/web/20131004234920/http://www.flickr.com/photos/schill/">Schill</a></span> </p> <p>Thanks to everyone that came out to the <a href="https://web.archive.org/web/20131004234920/http://www.meetup.com/SF-Web-Performance-Group/events/76198912/">SF Web Performance meet up</a> last night! For those of you that missed it, JP and Aaron were kind enough to record the <a href="https://web.archive.org/web/20131004234920/http://www.ustream.tv/recorded/26434123">entire event on Ustream</a>.</p> <p>You can also view the slides and associated blog posts for each of the presentations:</p> <ul> <li><b>Optimizing Touch Performance</b>, by Stephen Woods: <a href="https://web.archive.org/web/20131004234920/https://speakerdeck.com/ysaw/creating-responsive-html5-touch-interfaces">slides</a> and <a href="/web/20131004234920/http://code.flickr.net/2011/07/20/lessons-learned-from-the-flickr-touch-lightbox/">blog post</a></li> <li><b>Using Web Workers for fun and profit: Parsing Exif in the client</b>, by Chris Berry: <a href="https://web.archive.org/web/20131004234920/https://speakerdeck.com/christopher_b/processing-exif-in-the-client-with-javascript-web-workers">slides</a> and <a href="/web/20131004234920/http://code.flickr.net/2012/06/01/parsing-exif-client-side-using-javascript-2/">blog post</a></li> <li><b>The Grid: How we show 10,000 photos on a page without crashing your browser</b>, by Scott Schiller: <a href="https://web.archive.org/web/20131004234920/http://isflashdeadyet.com/talks/html5/flickr_grid_sf_webperformance_10-25-2012/">slides</a> and <a href="/web/20131004234920/http://code.flickr.net/2012/05/11/building-the-flickr-web-uploadr-the-grid/">blog post</a></li> </ul> <p>Big thanks to <a href="https://web.archive.org/web/20131004234920/https://twitter.com/jphpsf">JP</a> and <a href="https://web.archive.org/web/20131004234920/https://twitter.com/gofastweb">Aaron</a> for setting it up and running the event so well!</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/20131004234920/http://code.flickr.net/category/event/" title="View all posts in event" rel="category tag">event</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-2290 --> <article id="post-2282" class="post-2282 post type-post status-publish format-standard hentry category-event tag-event"> <header class="entry-header"> <h1 class="entry-title"><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2012/10/25/join-the-flickr-frontend-team-tonight-at-the-sf-web-performance-meet-up/" rel="bookmark">Join the Flickr Frontend team tonight at the SF Web Performance meet&nbsp;up!</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2012/10/25/join-the-flickr-frontend-team-tonight-at-the-sf-web-performance-meet-up/" title="7:55 pm" rel="bookmark"><time class="entry-date" datetime="2012-10-25T19:55:58+00:00">October 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/20131004234920/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 class="flickr-photo"> <a href="https://web.archive.org/web/20131004234920/http://www.flickr.com/photos/wafer/8121133637/" title="Team Tinfoil by waferbaby, on Flickr"><img src="https://web.archive.org/web/20131004234920im_/http://farm9.staticflickr.com/8326/8121133637_6d5c8c2f3f_c.jpg" width="800" height="531" alt="Team Tinfoil" border="0"/></a><br/> <span class="caption"><a href="https://web.archive.org/web/20131004234920/http://www.flickr.com/photos/wafer/8121133637/">Team Tinfoil</a> by <a href="https://web.archive.org/web/20131004234920/http://www.flickr.com/photos/wafer/">waferbaby</a></span> </p> <p>We will be hosting the <a href="https://web.archive.org/web/20131004234920/http://www.meetup.com/SF-Web-Performance-Group/events/76198912/">SF Web Performance meet up</a> tonight at 7pm at <a href="https://web.archive.org/web/20131004234920/http://citizenspace.us/">Citizen Space</a>. Come join us for pizza, drinks, and these great talks:</p> <h3>Using Web Workers for fun and profit: Parsing Exif in the client, by Chris Berry</h3> <p>Exif, exchangeable image file format, describes various sets of metadata stored in a photo. Really interesting metadata, like image titles, descriptions, lens focal lengths, camera types, image orientation, even GPS data! I&#8217;ll go over the methods to extracting this data on the front-end, in real-time, using web workers.</p> <h3>The Grid: How we show 10,000 photos on a page without crashing your browser, by Scott Schiller</h3> <p>Flickr&#8217;s latest Web-based Uploadr interface uses HTML5 APIs to push bytes en masse. Its real power, however, is the UI which enables users to add and edit the metadata of hundreds of photos while they are uploading in the background.</p> <p> Handling the selection, display and management of large numbers of photos in a browser UI meant that the Uploadr project needed to be designed for scalability from the ground up. </p> <p> This talk will go into some of the details of the Uploadr &#8220;Grid&#8221; UI, technical notes and performance findings made during its development.</p> <h3>Optimizing Touch Performance, by Stephen Woods</h3> <p>Touch interfaces are amazing. Touch devices are amazingly slow. Stephen Woods will share hard-won advice for building responsive touch-based interfaces using HTML5, CSS, and JavaScript. He also reveals how Star Trek: The Next Generation predicted the need for instant user feedback in a touch-based UI and how Tivos slow UI was made bearable by a simple &#8220;bloop&#8221; sound.</p> <p>See you there!</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/20131004234920/http://code.flickr.net/category/event/" title="View all posts in event" rel="category tag">event</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/20131004234920/http://code.flickr.net/tag/event/" rel="tag">event</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-2282 --> <article id="post-2273" class="post-2273 post type-post status-publish format-standard hentry category-geo tag-geo tag-shapefile"> <header class="entry-header"> <h1 class="entry-title"><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2012/10/24/2273/" rel="bookmark">We saved you a&nbsp;step…</a></h1> <div class="entry-meta"> <span class="sep">Posted on </span><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2012/10/24/2273/" title="5:41 pm" rel="bookmark"><time class="entry-date" datetime="2012-10-24T17:41:45+00:00">October 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/20131004234920/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>It seems when we launched <a href="https://web.archive.org/web/20131004234920/http://code.flickr.com/blog/2011/01/08/flickr-shapefiles-public-dataset-2-0/">version 2.0</a> of our <a href="https://web.archive.org/web/20131004234920/http://code.flickr.com/blog/2008/10/30/the-shape-of-alpha/">Flickr shapes</a>, we posted them with a flaw which made them useless to most popular geo applications.</p> <p>Awwwww&#8230;</p> <p>Luckily, <a href="https://web.archive.org/web/20131004234920/http://twitter.com/cmanning88">Christopher Manning</a> wrote a <a href="https://web.archive.org/web/20131004234920/http://www.christophermanning.org/writing/make-flickr-shapes-json-parsable/">python script</a> which makes them useful.</p> <p>Yaaaayyyyy!</p> <p>The least we can do is post an update which has already been christopher-manning-ified, So, we are very happy to announce version 2.0.1 of the Flickr shape files which can be downloaded here:<br/> <a href="https://web.archive.org/web/20131004234920/http://www.flickr.com/services/shapefiles/2.0.1/">http://www.flickr.com/services/shapefiles/2.0.1/</a></p> <p>Look, it works:</p> <p class="figure"> <img src="https://web.archive.org/web/20131004234920im_/http://farm9.staticflickr.com/8196/8075284257_ef27d63253_c.jpg" width="800" height="506"><br/> <span class="caption">Flickr Shapes 2.0.1 in TileMill</span> </p> <p>A very hearty THANKS! from your friends at Flickr, Christopher.</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/20131004234920/http://code.flickr.net/category/geo/" title="View all posts in geo" rel="category tag">geo</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/20131004234920/http://code.flickr.net/tag/geo/" rel="tag">geo</a>, <a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/tag/shapefile/" rel="tag">shapefile</a> </span> </footer><!-- .entry-meta --> </article><!-- #post-2273 --> <nav id="nav-below"> <h3 class="assistive-text">Post navigation</h3> <div class="nav-previous"><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/page/2/"><span class="meta-nav">&larr;</span> Older posts</a></div> <div class="nav-next"></div> </nav><!-- #nav-above --> </div><!-- #content --> </div><!-- #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/20131004234920/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/20131004234920/http://code.flickr.net/2013/09/04/a-summer-at-flickr/" title="A Summer at Flickr">A Summer at&nbsp;Flickr</a> </li> <li> <a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2013/06/14/pre-generating-justified-views/" title="Pre-generating Justified Views">Pre-generating Justified&nbsp;Views</a> </li> <li> <a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2013/06/04/adventures-in-jank-busting-parallax-performance-and-the-new-flickr-home-page/" title="Adventures in Jank Busting: Parallax, performance, and the new Flickr Home Page">Adventures in Jank Busting: Parallax, performance, and the new Flickr Home&nbsp;Page</a> </li> <li> <a href="https://web.archive.org/web/20131004234920/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&nbsp;MySQL</a> </li> <li> <a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2012/12/21/redis-global-locks-redux/" title="Redis Global Locks Redux">Redis Global Locks&nbsp;Redux</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/20131004234920/http://code.flickr.net/2013/09/" title="September 2013">September 2013</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2013/06/" title="June 2013">June 2013</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2013/03/" title="March 2013">March 2013</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2012/12/" title="December 2012">December 2012</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2012/10/" title="October 2012">October 2012</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2012/07/" title="July 2012">July 2012</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2012/06/" title="June 2012">June 2012</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2012/05/" title="May 2012">May 2012</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2012/04/" title="April 2012">April 2012</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2012/02/" title="February 2012">February 2012</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2012/01/" title="January 2012">January 2012</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2011/12/" title="December 2011">December 2011</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2011/10/" title="October 2011">October 2011</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2011/09/" title="September 2011">September 2011</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2011/08/" title="August 2011">August 2011</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2011/07/" title="July 2011">July 2011</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2011/06/" title="June 2011">June 2011</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2011/03/" title="March 2011">March 2011</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2011/02/" title="February 2011">February 2011</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2011/01/" title="January 2011">January 2011</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2010/11/" title="November 2010">November 2010</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2010/10/" title="October 2010">October 2010</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2010/09/" title="September 2010">September 2010</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2010/08/" title="August 2010">August 2010</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2010/07/" title="July 2010">July 2010</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2010/05/" title="May 2010">May 2010</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2010/04/" title="April 2010">April 2010</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2010/03/" title="March 2010">March 2010</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2010/02/" title="February 2010">February 2010</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2010/01/" title="January 2010">January 2010</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2009/12/" title="December 2009">December 2009</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2009/11/" title="November 2009">November 2009</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2009/10/" title="October 2009">October 2009</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2009/09/" title="September 2009">September 2009</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2009/07/" title="July 2009">July 2009</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2009/06/" title="June 2009">June 2009</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2009/05/" title="May 2009">May 2009</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2009/04/" title="April 2009">April 2009</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2009/03/" title="March 2009">March 2009</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2009/02/" title="February 2009">February 2009</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2009/01/" title="January 2009">January 2009</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2008/12/" title="December 2008">December 2008</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2008/11/" title="November 2008">November 2008</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2008/10/" title="October 2008">October 2008</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2008/09/" title="September 2008">September 2008</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2008/08/" title="August 2008">August 2008</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2008/07/" title="July 2008">July 2008</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2008/06/" title="June 2008">June 2008</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/2008/05/" title="May 2008">May 2008</a></li> <li><a href="https://web.archive.org/web/20131004234920/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/20131004234920/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/20131004234920/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/20131004234920/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/20131004234920/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/20131004234920/http://code.flickr.net/category/meta/" title="View all posts filed under meta">meta</a> </li> <li class="cat-item cat-item-1930"><a href="https://web.archive.org/web/20131004234920/http://code.flickr.net/category/performance/" title="View all posts filed under performance">performance</a> </li> <li class="cat-item cat-item-1"><a href="https://web.archive.org/web/20131004234920/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/20131004234920/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/20131004234920/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/20131004234920/http://wordpress.com/signup/?ref=wplogin">Register</a></li> <li><a href="https://web.archive.org/web/20131004234920/http://flickrcode.wordpress.com/wp-login.php">Log in</a></li> <li><a href="https://web.archive.org/web/20131004234920/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/20131004234920/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> <li>Powered by <a href="https://web.archive.org/web/20131004234920/http://vip.wordpress.com/" rel="generator nofollow" class="powered-by-wpcom">WordPress.com VIP</a></li> </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/20131004234920/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/20131004234920/https://secure" : "https://web.archive.org/web/20131004234920/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/20131004234920im_/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/20131004234920js_/http://0.gravatar.com/js/gprofiles.js?ver=201340ae"></script> <script type="text/javascript"> /* <![CDATA[ */ var WPGroHo = {"my_hash":""}; /* ]]> */ </script> <script type="text/javascript" src="https://web.archive.org/web/20131004234920js_/http://s2.wp.com/wp-content/mu-plugins/gravatar-hovercards/wpgroho.js?m=1380573781g"></script> <script> //initialize and attach hovercards to all gravatars jQuery( document ).ready( function( $ ) { Gravatar.profile_cb = function( hash, id ) { WPGroHo.syncProfileData( hash, id ); }; 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/20131004234920js_/http://s2.wp.com/_static/??-eJzTLy/QTc7PK0nNK9EvyClNz8wr1i+uzCtJrMjITM/IAeKS1CJMEWP94uSizIISoOIM5/yiVL2sYh19yo1yKiotzvAKBvOpaWRwYQ41jQvISczMo6qBGQVA4+xzbQ2NzYyNDUxMDE2yAJH4nz0="></script> <script type="text/javascript"> (function(){ var corecss = document.createElement('link'); var themecss = document.createElement('link'); var corecssurl = "https://web.archive.org/web/20131004234920/http://s0.wp.com/wp-content/plugins/syntaxhighlighter/syntaxhighlighter3/styles/shCore.css?m=1370811927g&amp;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/20131004234920/http://s0.wp.com/wp-content/plugins/syntaxhighlighter/syntaxhighlighter3/styles/shThemeDefault.css?m=1363304414g&amp;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/20131004234920js_/http://s1.wp.com/wp-content/js/devicepx.js?m=1373391538g"></script> <script type="text/javascript" src="https://web.archive.org/web/20131004234920js_/http://platform.twitter.com/widgets.js?ver=20111117"></script> <script type="text/javascript" src="https://web.archive.org/web/20131004234920js_/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/20131004234920js_/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/20131004234920/http://stats.wordpress.com/g.gif?blog=39034126&v=wpcomvt&tz=0&user_id=0&subd=flickrcode&rand="+Math.random();} ex_go({'crypt':'UE40eW5QN0p8M2Y/RE1BNmNJfGhxNCVxUDExYmtXRThKbHcwXTdETWI1alhvb1oseHImN101ZFpEakVpYjlQYVFLYzBaVHRtPz0wXS9bM1lKdVZKQS1NTGJmdUM0ZHJxVkdbSmJHXy45PWpjMm9yUHFSNDJ1fCt3LERDMnBlanNhaSV1P281bXpENmJGQj0lJlVpfjlYV09DL2smZS5sW21bM25QXTldNFpLWS1rOFtNU3B4cC9MJkZLJmssN3BfSFBUTUUvMi1hTn4yY3NyZytkcHZ5OCx5UXl6RmpzTHxwQkFIU2xQeD1nfnVCRVY/SmFbX0tKRXBbbU9EbA=='}); addLoadEvent(function(){linktracker_init('39034126',0);}); </script> <noscript><img src="https://web.archive.org/web/20131004234920im_/http://stats.wordpress.com/b.gif?v=noscript" style="height:0px;width:0px;overflow:hidden" alt=""/></noscript> <script type="text/javascript"> // Trigger Quantcast pixel for each Infinite Scroll post load if ( 'function' === typeof( jQuery ) ) { jQuery( document.body ).on( 'post-load', function() { if ( 'function' === typeof( wpcomQuantcastPixel ) ) wpcomQuantcastPixel(); } ); } </script> <script> if ( 'object' === typeof wpcom_mobile_user_agent_info ) { wpcom_mobile_user_agent_info.init(); var mobileStatsQueryString = ""; if( false !== wpcom_mobile_user_agent_info.matchedPlatformName ) mobileStatsQueryString += "&x_" + 'mobile_platforms' + '=' + wpcom_mobile_user_agent_info.matchedPlatformName; if( false !== wpcom_mobile_user_agent_info.matchedUserAgentName ) mobileStatsQueryString += "&x_" + 'mobile_devices' + '=' + wpcom_mobile_user_agent_info.matchedUserAgentName; if( wpcom_mobile_user_agent_info.isIPad() ) mobileStatsQueryString += "&x_" + 'ipad_views' + '=' + 'views'; if( "" != mobileStatsQueryString ) { new Image().src = document.location.protocol + '//web.archive.org/web/20131004234920/http://stats.wordpress.com/g.gif?v=wpcom-no-pv' + mobileStatsQueryString + '&baba=' + Math.random(); } } </script> </body> </html><!-- FILE ARCHIVED ON 23:49:20 Oct 04, 2013 AND RETRIEVED FROM THE INTERNET ARCHIVE ON 19:52:09 Nov 25, 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.56 exclusion.robots: 0.022 exclusion.robots.policy: 0.014 esindex: 0.009 cdx.remote: 6.257 LoadShardBlock: 580.622 (6) PetaboxLoader3.datanode: 452.463 (7) PetaboxLoader3.resolve: 82.762 (2) load_resource: 65.131 -->

Pages: 1 2 3 4 5 6 7 8 9 10