diff options
Diffstat (limited to 'content/turkey-doodle.article')
-rw-r--r-- | content/turkey-doodle.article | 276 |
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. |