aboutsummaryrefslogtreecommitdiff
path: root/content/turkey-doodle.article
diff options
context:
space:
mode:
Diffstat (limited to 'content/turkey-doodle.article')
-rw-r--r--content/turkey-doodle.article276
1 files changed, 276 insertions, 0 deletions
diff --git a/content/turkey-doodle.article b/content/turkey-doodle.article
new file mode 100644
index 0000000..8649279
--- /dev/null
+++ b/content/turkey-doodle.article
@@ -0,0 +1,276 @@
+# From zero to Go: launching on the Google homepage in 24 hours
+13 Dec 2011
+Tags: appengine, google, guest
+Summary: How Go helped launch the Google Doodle for Thanksgiving 2011.
+OldURL: /from-zero-to-go-launching-on-google
+
+Reinaldo Aguiar
+
+## Introduction
+
+_This article was written by Reinaldo Aguiar, a software engineer from the Search team at Google. He shares his experience developing his first Go program and launching it to an audience of millions - all in one day!_
+
+I was recently given the opportunity to collaborate on a small but highly
+visible "20% project":
+the [Thanksgiving 2011 Google Doodle](http://www.google.com/logos/2011/thanksgiving.html).
+The doodle features a turkey produced by randomly combining different styles of head,
+wings, feathers and legs.
+The user can customize it by clicking on the different parts of the turkey.
+This interactivity is implemented in the browser by a combination of JavaScript,
+CSS and of course HTML, creating turkeys on the fly.
+
+.image turkey-doodle/image00.png
+
+Once the user has created a personalized turkey it can be shared with friends
+and family by posting to Google+.
+Clicking a "Share" button (not pictured here) creates in the user's Google+
+stream a post containing a snapshot of the turkey.
+The snapshot is a single image that matches the turkey the user created.
+
+With 13 alternatives for each of 8 parts of the turkey (heads,
+pairs of legs, distinct feathers, etc.) there are more than than 800 million
+possible snapshot images that could be generated.
+To pre-compute them all is clearly infeasible.
+Instead, we must generate the snapshots on the fly.
+Combining that problem with a need for immediate scalability and high availability,
+the choice of platform is obvious: Google App Engine!
+
+The next thing we needed to decide was which App Engine runtime to use.
+Image manipulation tasks are CPU-bound, so performance is the deciding factor in this case.
+
+To make an informed decision we ran a test.
+We quickly prepared a couple of equivalent demo apps for the new [Python 2.7 runtime](http://code.google.com/appengine/docs/python/python27/newin27.html)
+(which provides [PIL](http://www.pythonware.com/products/pil/),
+a C-based imaging library) and the Go runtime.
+Each app generates an image composed of several small images,
+encodes the image as a JPEG, and sends the JPEG data as the HTTP response.
+The Python 2.7 app served requests with a median latency of 65 milliseconds,
+while the Go app ran with a median latency of just 32 milliseconds.
+
+This problem therefore seemed the perfect opportunity to try the experimental Go runtime.
+
+I had no previous experience with Go and the timeline was tight:
+two days to be production ready.
+This was intimidating, but I saw it as an opportunity to test Go from a different,
+often overlooked angle:
+development velocity.
+How fast can a person with no Go experience pick it up and build something
+that performs and scales?
+
+## Design
+
+The approach was to encode the state of the turkey in the URL, drawing and encoding the snapshot on the fly.
+
+The base for every doodle is the background:
+
+.image turkey-doodle/image01.jpg
+
+A valid request URL might look like this:
+`http://google-turkey.appspot.com/thumb/20332620][http://google-turkey.appspot.com/thumb/20332620`
+
+The alphanumeric string that follows "/thumb/" indicates (in hexadecimal)
+which choice to draw for each layout element,
+as illustrated by this image:
+
+.image turkey-doodle/image03.png
+
+The program's request handler parses the URL to determine which element
+is selected for each component,
+draws the appropriate images on top of the background image,
+and serves the result as a JPEG.
+
+If an error occurs, a default image is served.
+There's no point serving an error page because the user will never see it -
+the browser is almost certainly loading this URL into an image tag.
+
+## Implementation
+
+In the package scope we declare some data structures to describe the elements of the turkey,
+the location of the corresponding images,
+and where they should be drawn on the background image.
+
+ var (
+ // dirs maps each layout element to its location on disk.
+ dirs = map[string]string{
+ "h": "img/heads",
+ "b": "img/eyes_beak",
+ "i": "img/index_feathers",
+ "m": "img/middle_feathers",
+ "r": "img/ring_feathers",
+ "p": "img/pinky_feathers",
+ "f": "img/feet",
+ "w": "img/wing",
+ }
+
+ // urlMap maps each URL character position to
+ // its corresponding layout element.
+ urlMap = [...]string{"b", "h", "i", "m", "r", "p", "f", "w"}
+
+ // layoutMap maps each layout element to its position
+ // on the background image.
+ layoutMap = map[string]image.Rectangle{
+ "h": {image.Pt(109, 50), image.Pt(166, 152)},
+ "i": {image.Pt(136, 21), image.Pt(180, 131)},
+ "m": {image.Pt(159, 7), image.Pt(201, 126)},
+ "r": {image.Pt(188, 20), image.Pt(230, 125)},
+ "p": {image.Pt(216, 48), image.Pt(258, 134)},
+ "f": {image.Pt(155, 176), image.Pt(243, 213)},
+ "w": {image.Pt(169, 118), image.Pt(250, 197)},
+ "b": {image.Pt(105, 104), image.Pt(145, 148)},
+ }
+ )
+
+The geometry of the points above was calculated by measuring the actual
+location and size of each layout element within the image.
+
+Loading the images from disk on each request would be wasteful repetition,
+so we load all 106 images (13 \* 8 elements + 1 background + 1 default) into
+global variables upon receipt of the first request.
+
+ var (
+ // elements maps each layout element to its images.
+ elements = make(map[string][]*image.RGBA)
+
+ // backgroundImage contains the background image data.
+ backgroundImage *image.RGBA
+
+ // defaultImage is the image that is served if an error occurs.
+ defaultImage *image.RGBA
+
+ // loadOnce is used to call the load function only on the first request.
+ loadOnce sync.Once
+ )
+
+ // load reads the various PNG images from disk and stores them in their
+ // corresponding global variables.
+ func load() {
+ defaultImage = loadPNG(defaultImageFile)
+ backgroundImage = loadPNG(backgroundImageFile)
+ for dirKey, dir := range dirs {
+ paths, err := filepath.Glob(dir + "/*.png")
+ if err != nil {
+ panic(err)
+ }
+ for _, p := range paths {
+ elements[dirKey] = append(elements[dirKey], loadPNG(p))
+ }
+ }
+ }
+
+Requests are handled in a straightforward sequence:
+
+ - Parse the request URL, decoding the decimal value of each character in the path.
+
+ - Make a copy of the background image as the base for the final image.
+
+ - Draw each image element onto the background image using the layoutMap to determine where they should be drawn.
+
+ - Encode the image as a JPEG
+
+ - Return the image to user by writing the JPEG directly to the HTTP response writer.
+
+Should any error occur, we serve the defaultImage to the user and log the
+error to the App Engine dashboard for later analysis.
+
+Here's the code for the request handler with explanatory comments:
+
+ func handler(w http.ResponseWriter, r *http.Request) {
+ // [[https://blog.golang.org/2010/08/defer-panic-and-recover.html][Defer]] a function to recover from any panics.
+ // When recovering from a panic, log the error condition to
+ // the App Engine dashboard and send the default image to the user.
+ defer func() {
+ if err := recover(); err != nil {
+ c := appengine.NewContext(r)
+ c.Errorf("%s", err)
+ c.Errorf("%s", "Traceback: %s", r.RawURL)
+ if defaultImage != nil {
+ w.Header().Set("Content-type", "image/jpeg")
+ jpeg.Encode(w, defaultImage, &imageQuality)
+ }
+ }
+ }()
+
+ // Load images from disk on the first request.
+ loadOnce.Do(load)
+
+ // Make a copy of the background to draw into.
+ bgRect := backgroundImage.Bounds()
+ m := image.NewRGBA(bgRect.Dx(), bgRect.Dy())
+ draw.Draw(m, m.Bounds(), backgroundImage, image.ZP, draw.Over)
+
+ // Process each character of the request string.
+ code := strings.ToLower(r.URL.Path[len(prefix):])
+ for i, p := range code {
+ // Decode hex character p in place.
+ if p < 'a' {
+ // it's a digit
+ p = p - '0'
+ } else {
+ // it's a letter
+ p = p - 'a' + 10
+ }
+
+ t := urlMap[i] // element type by index
+ em := elements[t] // element images by type
+ if p >= len(em) {
+ panic(fmt.Sprintf("element index out of range %s: "+
+ "%d >= %d", t, p, len(em)))
+ }
+
+ // Draw the element to m,
+ // using the layoutMap to specify its position.
+ draw.Draw(m, layoutMap[t], em[p], image.ZP, draw.Over)
+ }
+
+ // Encode JPEG image and write it as the response.
+ w.Header().Set("Content-type", "image/jpeg")
+ w.Header().Set("Cache-control", "public, max-age=259200")
+ jpeg.Encode(w, m, &imageQuality)
+ }
+
+For brevity, I've omitted several helper functions from these code listings.
+See the [source code](http://code.google.com/p/go-thanksgiving/source/browse/) for the full scoop.
+
+## Performance
+
+.image turkey-doodle/image02.png
+
+This chart - taken directly from the App Engine dashboard - shows average
+request latency during launch.
+As you can see, even under load it never exceeds 60 ms,
+with a median latency of 32 milliseconds.
+This is wicked fast, considering that our request handler is doing image
+manipulation and encoding on the fly.
+
+## Conclusions
+
+I found Go's syntax to be intuitive, simple and clean.
+I have worked a lot with interpreted languages in the past,
+and although Go is instead a statically typed and compiled language,
+writing this app felt more like working with a dynamic,
+interpreted language.
+
+The development server provided with the [SDK](http://code.google.com/appengine/downloads.html#Google_App_Engine_SDK_for_Go)
+quickly recompiles the program after any change,
+so I could iterate as fast as I would with an interpreted language.
+It's dead simple, too - it took less than a minute to set up my development environment.
+
+Go's great documentation also helped me put this together fast.
+The docs are generated from the source code,
+so each function's documentation links directly to the associated source code.
+This not only allows the developer to understand very quickly what a particular
+function does but also encourages the developer to dig into the package implementation,
+making it easier to learn good style and conventions.
+
+In writing this application I used just three resources:
+App Engine's [Hello World Go example](http://code.google.com/appengine/docs/go/gettingstarted/helloworld.html),
+[the Go packages documentation](https://golang.org/pkg/),
+and [a blog post showcasing the Draw package](https://blog.golang.org/2011/09/go-imagedraw-package.html).
+Thanks to the rapid iteration made possible by the development server and
+the language itself,
+I was able to pick up the language and build a super fast,
+production ready, doodle generator in less than 24 hours.
+
+Download the full app source code (including images) at [the Google Code project](http://code.google.com/p/go-thanksgiving/source/browse/).
+
+Special thanks go to Guillermo Real and Ryan Germick who designed the doodle.