go.talks: Google I/O talk: High Performance Apps with Go on App Engine.
R=adg
CC=golang-dev
https://golang.org/cl/9518045
diff --git a/2013/highperf.slide b/2013/highperf.slide
new file mode 100644
index 0000000..51143f8
--- /dev/null
+++ b/2013/highperf.slide
@@ -0,0 +1,232 @@
+High Performance Apps with Go on App Engine
+Google I/O, May 2013
+
+David Symonds
+Software Engineer, Google
+dsymonds@golang.org
+
+* Video
+
+This talk was presented at Google I/O in May 2013.
+
+.link http://www.youtube.com/watch?v=fc25ihfXhbg Watch the talk on YouTube
+
+* Overview
+
+- Why Go + App Engine
+- History, Status
+- Gopher Mart
+- Finding performance bottlenecks
+- Defer work
+- Batching
+- Caching
+- Concurrency
+- Control variance
+
+* Why Go + App Engine
+
+- Go compiles to native code
+- App Engine is an auto-scaling low-maintenance platform
+- Fastest runtime on App Engine: starts fast, runs fast
+
+.image highperf/aegopher.jpg
+
+* History
+
+- Go runtime for App Engine unveiled at Google I/O 2011
+- Steady growth, several high-profile users
+
+.image highperf/appenginegophercolor.jpg
+
+* Turkey doodle (Nov 2011)
+
+.image highperf/turkey.png
+
+- Written by a Go newcomer, launched in under 24 hours
+- Half the latency of a Python 2.7 version
+- Launched on google.com front page
+
+[[http://golang.org/s/turkey-doodle][golang.org/s/turkey-doodle]]
+
+* Santa Tracker (Dec 2012)
+
+.html highperf/santaembed.html
+
+- Nearly 5000 queries per second
+- Did not make any children cry
+
+.image highperf/santagraph.png
+
+* Gopher Mart
+
+* Gopher Mart
+
+- Imagine your app is the Gopher Mart, the one-stop-shop for all your gopher needs
+
+.image highperf/gophermart.png
+
+* Gopher Mart
+
+- Each checkout gopher is an app instance
+- Each shopping gopher is a user request
+- The scheduler directs requests to instances
+- A checkout gopher can deal with one customer at a time
+- Checkout gophers (instances) can be hired/fired, but there's overhead
+
+.image highperf/gophermart2.png
+
+* Gopher Mart
+
+- Gophers waiting in queues get grumpy
+- Let's make it fast
+
+* Finding performance bottlenecks
+
+- _Understand_why_your_checkout_gophers_might_work_slowly_
+- Measure first
+
+.image highperf/gopherrulespanner.png
+
+- Measure periodically; performance characteristics changes over time
+- Go 1.1 brings lots of performance improvements
+
+* Appstats
+
+- Appstats traces API RPCs, shows timeline
+- [[http://github.com/mjibson/appstats][github.com/mjibson/appstats]]
+
+- Gopher Mart baseline:
+.image highperf/appstats1.png
+
+* Performance Techniques
+
+* Defer work
+
+- Not all work needs to be done during the request
+- Use `appengine/taskqueue` or `appengine/delay` to move non-critical work outside the request scope
+- _Gopher_Mart_can_replace_slow_ `mail.Send` _with_quick_ `taskqueue.Add`
+
+* Defer work II
+
+Import `"appengine/delay"` and transform
+
+.code highperf/mart/1/mart.go /sendReceipt/
+.code highperf/mart/1/mart.go /func sendReceipt/
+
+into
+
+.code highperf/mart/2/mart.go /sendReceipt/
+.code highperf/mart/2/mart.go /var sendReceipt/
+
+.image highperf/appstats2.png
+
+* Batching
+
+- _Buying_10_boxes_of_Gopher_Flakes_is_easier_than_buying_a_single_box_10_times_
+- _Calling_for_a_price_check_on_three_items_is_faster_than_three_separate_price_checks_
+- Use `GetMulti` instead of `Get`, `PutMulti` instead of `Put`, etc.
+
+.code highperf/mart/2/mart.go /Dumb load/+1,/Print items/-1
+
+* Batching II
+
+.code highperf/mart/3/mart.go /Batch get/+1,/}/
+
+.image highperf/appstats3.png
+
+* Caching
+
+- _Checkout_gophers_can_make_notes_and_memorize_things_
+- Using datastore is fine, but it can be slow
+- Using memcache is good (shared among instances), but remember it can disappear
+- Using local memory is okay too, but it can disappear as well
+
+.html highperf/cachingembed.html
+
+* Concurrency
+
+- _A_price_check_may_delay_dealing_with_a_shopping_gopher_
+- RPC-bound requests are very common, and often least-cacheable
+- _Get_your_checkout_gopher_doing_something_else_while_waiting_
+
+* Concurrency II
+
+.code highperf/concurrency.go.notouch /func serial/+1,/^}/-1
+
+* Concurrency III
+
+- Run queries concurrently
+- Easy way to speed up RPC-bound requests
+
+.code highperf/concurrency.go.notouch /func parallel/+1,/^}/-1
+
+- Visit Sameer Ajmani's talk (today, 4:25PM, Room 7)
+
+* Control variance
+
+- _Some_shopping_gophers_can_take_much_longer_than_others_
+- Variance in request processing is very common (e.g. 99% take 10ms, 1% take 100ms)
+- Some requests are harder than others
+- Infrastructure is reliable, but not perfect
+
+* Control variance II
+
+- Effects of variance can cascade
+- Variable requests make scheduling harder, requires more instances (billed)
+- We want to control variance
+
+[[http://cacm.acm.org/magazines/2013/2/160173-the-tail-at-scale/fulltext]["The Tail at Scale", Dean, Barroso; Commun. ACM 56, 2]]
+
+* Control variance III
+
+- For example, storing in memcache is usually an optimisation, and thus optional
+- Bound time spent on optional work
+
+- First approach: save to memcache asynchronously
+
+.code highperf/longtail.go /long_tail_memcache_bad/+1,/^}/
+
+- Problem: Response will be returned to user as soon as handler returns, and outstanding API calls will be canceled
+
+* Control variance IV
+
+- Solution: Use Go's concurrency primitives to timeout waiting
+
+.code highperf/longtail.go /long_tail_memcache_good/+1,/^}/
+
+* Before and After
+
+Baseline:
+
+.image highperf/appstats1.png
+
+Defer work:
+
+.image highperf/appstats2.png
+
+Batching:
+
+.image highperf/appstats3.png
+
+* Summary
+
+- Finding performance bottlenecks
+- Defer work
+- Batching
+- Caching
+- Concurrency
+- Control variance
+
+* Finally...
+
+- [[http://golang.org][golang.org]]
+- [[http://developers.google.com/appengine/docs/go/][developers.google.com/appengine/docs/go/]]
+- [[http://golang.org/s/io13-ae-talk][golang.org/s/io13-ae-talk]] (this talk, plus gophermart app)
+- [[http://github.com/mjibson/appstats][github.com/mjibson/appstats]]
+
+More Go things:
+
+- _Go_in_Production_ (_office_hours_), 3:30PM-4:15PM, Cloud Sandbox
+- _Advanced_Go_Concurrency_Patterns_, 4:25PM, Room 7
+- _Fireside_Chat_with_the_Go_Team_, 5:20PM, Room 2
+- _Go_App_Engine_ (_office_hours_), 1:45PM-2:30PM *tomorrow*, Cloud Sandbox
diff --git a/2013/highperf/aegopher.jpg b/2013/highperf/aegopher.jpg
new file mode 100644
index 0000000..f0c0bda
--- /dev/null
+++ b/2013/highperf/aegopher.jpg
Binary files differ
diff --git a/2013/highperf/appenginegophercolor.jpg b/2013/highperf/appenginegophercolor.jpg
new file mode 100644
index 0000000..68795a9
--- /dev/null
+++ b/2013/highperf/appenginegophercolor.jpg
Binary files differ
diff --git a/2013/highperf/appstats1.png b/2013/highperf/appstats1.png
new file mode 100644
index 0000000..fe6d759
--- /dev/null
+++ b/2013/highperf/appstats1.png
Binary files differ
diff --git a/2013/highperf/appstats2.png b/2013/highperf/appstats2.png
new file mode 100644
index 0000000..d5eda22
--- /dev/null
+++ b/2013/highperf/appstats2.png
Binary files differ
diff --git a/2013/highperf/appstats3.png b/2013/highperf/appstats3.png
new file mode 100644
index 0000000..1f6e553
--- /dev/null
+++ b/2013/highperf/appstats3.png
Binary files differ
diff --git a/2013/highperf/art/gophercart.png b/2013/highperf/art/gophercart.png
new file mode 100644
index 0000000..7813cd4
--- /dev/null
+++ b/2013/highperf/art/gophercart.png
Binary files differ
diff --git a/2013/highperf/art/gophercheckout.png b/2013/highperf/art/gophercheckout.png
new file mode 100644
index 0000000..4fadb30
--- /dev/null
+++ b/2013/highperf/art/gophercheckout.png
Binary files differ
diff --git a/2013/highperf/art/gophermegaphone.png b/2013/highperf/art/gophermegaphone.png
new file mode 100644
index 0000000..e06d65b
--- /dev/null
+++ b/2013/highperf/art/gophermegaphone.png
Binary files differ
diff --git a/2013/highperf/cachingembed.html b/2013/highperf/cachingembed.html
new file mode 100644
index 0000000..50d1b33
--- /dev/null
+++ b/2013/highperf/cachingembed.html
@@ -0,0 +1,15 @@
+<table style="width: 50%;">
+ <caption>Small reads</caption>
+ <tr>
+ <td><tt>datastore.Get</tt></td>
+ <td>O(20ms)</td>
+ </tr>
+ <tr>
+ <td><tt>memcache.Get</tt></td>
+ <td>O(1ms)</td>
+ </tr>
+ <tr>
+ <td><tt>RAM</tt></td>
+ <td>O(1µs)</td>
+ </tr>
+</table>
diff --git a/2013/highperf/concurrency.go.notouch b/2013/highperf/concurrency.go.notouch
new file mode 100644
index 0000000..0ac5d81
--- /dev/null
+++ b/2013/highperf/concurrency.go.notouch
@@ -0,0 +1,30 @@
+// +build ignore
+
+package pkg
+
+func serial() {
+ var lists []List
+ var items []Item
+ _, err := datastore.NewQuery("List").GetAll(c, &lists)
+ if err != nil { /* ... */ }
+ _, err := datastore.NewQuery("Item").GetAll(c, &items)
+ if err != nil { /* ... */ }
+ // write response
+}
+
+func parallel() {
+ var lists []List
+ var items []Item
+ errc := make(chan error) // HL
+ go func() { // HL
+ _, err := datastore.NewQuery("List").GetAll(c, &lists)
+ errc <- err
+ }() // HL
+ go func() { // HL
+ _, err := datastore.NewQuery("Item").GetAll(c, &items)
+ errc <- err
+ }() // HL
+ err1, err2 := <-errc, <-errc // HL
+ if err1 != nil || err2 != nil { /* ... */ }
+ // write response
+}
diff --git a/2013/highperf/gophermart.png b/2013/highperf/gophermart.png
new file mode 100644
index 0000000..72ae11c
--- /dev/null
+++ b/2013/highperf/gophermart.png
Binary files differ
diff --git a/2013/highperf/gophermart2.png b/2013/highperf/gophermart2.png
new file mode 100644
index 0000000..9ff5f0e
--- /dev/null
+++ b/2013/highperf/gophermart2.png
Binary files differ
diff --git a/2013/highperf/gopherrulespanner.png b/2013/highperf/gopherrulespanner.png
new file mode 100644
index 0000000..24603f3
--- /dev/null
+++ b/2013/highperf/gopherrulespanner.png
Binary files differ
diff --git a/2013/highperf/longtail.go b/2013/highperf/longtail.go
new file mode 100644
index 0000000..f6d43fa
--- /dev/null
+++ b/2013/highperf/longtail.go
@@ -0,0 +1,38 @@
+// +build ignore
+
+package pkg
+
+// long_tail_memcache_bad
+func myHandler(w http.ResponseWriter, r *http.Request) {
+ c := appengine.NewContext(r)
+ // ...
+ // regular request handling
+ // ...
+
+ go memcache.Set(c, &memcache.Item{
+ Key: key,
+ Value: data,
+ })
+}
+
+// long_tail_memcache_good
+func myHandler(w http.ResponseWriter, r *http.Request) {
+ c := appengine.NewContext(r)
+ // ...
+ // regular request handling
+ // ...
+
+ // Save to memcache, but only wait up to 3ms.
+ done := make(chan bool, 1) // NB: buffered
+ go func() {
+ memcache.Set(c, &memcache.Item{
+ Key: key,
+ Value: data,
+ })
+ done <- true
+ }()
+ select { // HL
+ case <-done: // HL
+ case <-time.After(3 * time.Millisecond): // HL
+ } // HL
+}
diff --git a/2013/highperf/mart/1/app.yaml b/2013/highperf/mart/1/app.yaml
new file mode 100644
index 0000000..04e0df7
--- /dev/null
+++ b/2013/highperf/mart/1/app.yaml
@@ -0,0 +1,12 @@
+application: gophermart
+version: 1
+runtime: go
+api_version: go1
+
+handlers:
+- url: /admin/.*
+ script: _go_app
+ login: admin
+- url: /.*
+ script: _go_app
+ login: required
diff --git a/2013/highperf/mart/1/mart.go b/2013/highperf/mart/1/mart.go
new file mode 100644
index 0000000..972ffe8
--- /dev/null
+++ b/2013/highperf/mart/1/mart.go
@@ -0,0 +1,132 @@
+package mart
+
+import (
+ "bytes"
+ "fmt"
+ "math/rand"
+ "net/http"
+ "strconv"
+ "strings"
+
+ "appengine"
+ "appengine/datastore"
+ "appengine/mail"
+ "appengine/user"
+
+ "github.com/mjibson/appstats"
+)
+
+func init() {
+ http.HandleFunc("/", front)
+ http.Handle("/checkout", appstats.NewHandler(checkout))
+ http.HandleFunc("/admin/populate", adminPopulate)
+}
+
+func front(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/" {
+ http.NotFound(w, r)
+ return
+ }
+ fmt.Fprintf(w, "Hello, welcome to Gopher Mart!")
+}
+
+const (
+ numItems = 100 // number of different items for sale
+
+ appAdmin = "noreply@google.com" // an admin of this app, for sending mail
+)
+
+// Item represents an item for sale in Gopher Mart.
+type Item struct {
+ Name string
+ Price float64
+}
+
+func itemKey(c appengine.Context, i int) *datastore.Key {
+ return datastore.NewKey(c, "Item", fmt.Sprintf("item%04d", i), 0, nil)
+}
+
+func checkout(c appengine.Context, w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+ num, err := strconv.Atoi(r.FormValue("num"))
+ if err == nil && (num < 1 || num > 30) {
+ err = fmt.Errorf("%d out of range [1,30]", num)
+ }
+ if err != nil {
+ http.Error(w, fmt.Sprintf("bad number of items: %v", err), http.StatusBadRequest)
+ return
+ }
+
+ // Pick random items.
+ keys := make([]*datastore.Key, num)
+ for i := range keys {
+ keys[i] = itemKey(c, rand.Intn(numItems))
+ }
+
+ // Dumb load.
+ var items []*Item
+ for _, key := range keys {
+ item := new(Item)
+ if err := datastore.Get(c, key, item); err != nil {
+ http.Error(w, fmt.Sprintf("datastore.Get: %v", err), http.StatusBadRequest)
+ return
+ }
+ items = append(items, item)
+ }
+
+ // Print items.
+ var b bytes.Buffer
+ fmt.Fprintf(&b, "Here's what you bought:\n")
+ var sum float64
+ for _, item := range items {
+ fmt.Fprintf(&b, "\t%s", item.Name)
+ fmt.Fprint(&b, strings.Repeat("\t", (40-len(item.Name)+7)/8))
+ fmt.Fprintf(&b, "$%5.2f\n", item.Price)
+ sum += item.Price
+ }
+ fmt.Fprintln(&b, strings.Repeat("-", 55))
+ fmt.Fprintf(&b, "\tTotal:\t\t\t\t\t$%.2f\n", sum)
+
+ w.Write(b.Bytes())
+
+ sendReceipt(c, user.Current(c).Email, b.String())
+}
+
+func sendReceipt(c appengine.Context, dst, body string) {
+ msg := &mail.Message{
+ Sender: appAdmin,
+ To: []string{dst},
+ Subject: "Your Gopher Mart receipt",
+ Body: body,
+ }
+ if err := mail.Send(c, msg); err != nil {
+ c.Errorf("mail.Send: %v", err)
+ }
+}
+
+func adminPopulate(w http.ResponseWriter, r *http.Request) {
+ c := appengine.NewContext(r)
+ for i := range [numItems]struct{}{} { // r hates this. tee hee.
+ key := itemKey(c, i)
+ good := goods[rand.Intn(len(goods))]
+ item := &Item{
+ // TODO: vary names more
+ Name: fmt.Sprintf("%s %dg", good.name, i+1),
+ Price: float64(rand.Intn(1999)+1) / 100,
+ }
+ if _, err := datastore.Put(c, key, item); err != nil {
+ http.Error(w, err.Error(), 500)
+ return
+ }
+ }
+ fmt.Fprintf(w, "ok. %d items populated.", numItems)
+}
+
+var goods = [...]struct {
+ name string
+}{
+ {"Gopher Bran"},
+ {"Gopher Flakes"},
+ {"Gopher Grease"},
+ {"Gopher Litter"},
+}
diff --git a/2013/highperf/mart/2/app.yaml b/2013/highperf/mart/2/app.yaml
new file mode 100644
index 0000000..a620f68
--- /dev/null
+++ b/2013/highperf/mart/2/app.yaml
@@ -0,0 +1,15 @@
+application: gophermart
+version: 2
+runtime: go
+api_version: go1
+
+handlers:
+- url: /admin/.*
+ script: _go_app
+ login: admin
+- url: /_ah/(stats|queue).*
+ script: _go_app
+ login: admin
+- url: /.*
+ script: _go_app
+ login: required
diff --git a/2013/highperf/mart/2/mart.go b/2013/highperf/mart/2/mart.go
new file mode 100644
index 0000000..6c88523
--- /dev/null
+++ b/2013/highperf/mart/2/mart.go
@@ -0,0 +1,134 @@
+package mart
+
+import (
+ "bytes"
+ "fmt"
+ "math/rand"
+ "net/http"
+ "strconv"
+ "strings"
+
+ "appengine"
+ "appengine/datastore"
+ "appengine/delay"
+ "appengine/mail"
+ "appengine/user"
+
+ "github.com/mjibson/appstats"
+)
+
+func init() {
+ http.HandleFunc("/", front)
+ http.Handle("/checkout", appstats.NewHandler(checkout))
+ http.HandleFunc("/admin/populate", adminPopulate)
+}
+
+func front(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/" {
+ http.NotFound(w, r)
+ return
+ }
+ fmt.Fprintf(w, "Hello, welcome to Gopher Mart!")
+}
+
+const (
+ numItems = 100 // number of different items for sale
+
+ appAdmin = "noreply@google.com" // an admin of this app, for sending mail
+)
+
+// Item represents an item for sale in Gopher Mart.
+type Item struct {
+ Name string
+ Price float64
+}
+
+func itemKey(c appengine.Context, i int) *datastore.Key {
+ return datastore.NewKey(c, "Item", fmt.Sprintf("item%04d", i), 0, nil)
+}
+
+func checkout(c appengine.Context, w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+ num, err := strconv.Atoi(r.FormValue("num"))
+ if err == nil && (num < 1 || num > 30) {
+ err = fmt.Errorf("%d out of range [1,30]", num)
+ }
+ if err != nil {
+ http.Error(w, fmt.Sprintf("bad number of items: %v", err), http.StatusBadRequest)
+ return
+ }
+
+ // Pick random items.
+ keys := make([]*datastore.Key, num)
+ for i := range keys {
+ keys[i] = itemKey(c, rand.Intn(numItems))
+ }
+
+ // Dumb load.
+ var items []*Item
+ for _, key := range keys {
+ item := new(Item)
+ if err := datastore.Get(c, key, item); err != nil {
+ // ...
+ http.Error(w, fmt.Sprintf("datastore.Get: %v", err), http.StatusBadRequest) // OMIT
+ return // OMIT
+ }
+ items = append(items, item)
+ }
+
+ // Print items.
+ var b bytes.Buffer
+ fmt.Fprintf(&b, "Here's what you bought:\n")
+ var sum float64
+ for _, item := range items {
+ fmt.Fprintf(&b, "\t%s", item.Name)
+ fmt.Fprint(&b, strings.Repeat("\t", (40-len(item.Name)+7)/8))
+ fmt.Fprintf(&b, "$%5.2f\n", item.Price)
+ sum += item.Price
+ }
+ fmt.Fprintln(&b, strings.Repeat("-", 55))
+ fmt.Fprintf(&b, "\tTotal:\t\t\t\t\t$%.2f\n", sum)
+
+ w.Write(b.Bytes())
+
+ sendReceipt.Call(c, user.Current(c).Email, b.String())
+}
+
+var sendReceipt = delay.Func("send-receipt", func(c appengine.Context, dst, body string) {
+ msg := &mail.Message{
+ Sender: appAdmin,
+ To: []string{dst},
+ Subject: "Your Gopher Mart receipt",
+ Body: body,
+ }
+ if err := mail.Send(c, msg); err != nil {
+ c.Errorf("mail.Send: %v", err)
+ }
+})
+
+func adminPopulate(w http.ResponseWriter, r *http.Request) {
+ c := appengine.NewContext(r)
+ for i := range [numItems]struct{}{} { // r hates this. tee hee.
+ key := itemKey(c, i)
+ good := goods[rand.Intn(len(goods))]
+ item := &Item{
+ // TODO: vary names more
+ Name: fmt.Sprintf("%s %dg", good.name, i+1),
+ Price: float64(rand.Intn(1999)+1) / 100,
+ }
+ if _, err := datastore.Put(c, key, item); err != nil {
+ http.Error(w, err.Error(), 500)
+ return
+ }
+ }
+ fmt.Fprintf(w, "ok. %d items populated.", numItems)
+}
+
+var goods = [...]struct {
+ name string
+}{
+ {"Gopher Bran"},
+ {"Gopher Flakes"},
+ {"Gopher Grease"},
+ {"Gopher Litter"},
+}
diff --git a/2013/highperf/mart/3/app.yaml b/2013/highperf/mart/3/app.yaml
new file mode 100644
index 0000000..3e3c223
--- /dev/null
+++ b/2013/highperf/mart/3/app.yaml
@@ -0,0 +1,12 @@
+application: gophermart
+version: 3
+runtime: go
+api_version: go1
+
+handlers:
+- url: /admin/.*
+ script: _go_app
+ login: admin
+- url: /.*
+ script: _go_app
+ login: required
diff --git a/2013/highperf/mart/3/mart.go b/2013/highperf/mart/3/mart.go
new file mode 100644
index 0000000..183e831
--- /dev/null
+++ b/2013/highperf/mart/3/mart.go
@@ -0,0 +1,130 @@
+package mart
+
+import (
+ "bytes"
+ "fmt"
+ "math/rand"
+ "net/http"
+ "strconv"
+ "strings"
+
+ "appengine"
+ "appengine/datastore"
+ "appengine/delay"
+ "appengine/mail"
+ "appengine/user"
+
+ "github.com/mjibson/appstats"
+)
+
+func init() {
+ http.HandleFunc("/", front)
+ http.Handle("/checkout", appstats.NewHandler(checkout))
+ http.HandleFunc("/admin/populate", adminPopulate)
+}
+
+func front(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Path != "/" {
+ http.NotFound(w, r)
+ return
+ }
+ fmt.Fprintf(w, "Hello, welcome to Gopher Mart!")
+}
+
+const (
+ numItems = 100 // number of different items for sale
+
+ appAdmin = "noreply@google.com" // an admin of this app, for sending mail
+)
+
+// Item represents an item for sale in Gopher Mart.
+type Item struct {
+ Name string
+ Price float64
+}
+
+func itemKey(c appengine.Context, i int) *datastore.Key {
+ return datastore.NewKey(c, "Item", fmt.Sprintf("item%04d", i), 0, nil)
+}
+
+func checkout(c appengine.Context, w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/plain; charset=utf-8")
+ num, err := strconv.Atoi(r.FormValue("num"))
+ if err == nil && (num < 1 || num > 30) {
+ err = fmt.Errorf("%d out of range [1,30]", num)
+ }
+ if err != nil {
+ http.Error(w, fmt.Sprintf("bad number of items: %v", err), http.StatusBadRequest)
+ return
+ }
+
+ // Pick random items.
+ keys := make([]*datastore.Key, num)
+ for i := range keys {
+ keys[i] = itemKey(c, rand.Intn(numItems))
+ }
+
+ // Batch get
+ items := make([]Item, len(keys))
+ if err := datastore.GetMulti(c, keys, items); err != nil {
+ // ...
+ http.Error(w, fmt.Sprintf("datastore.GetMulti: %v", err), http.StatusBadRequest) // OMIT
+ return // OMIT
+ }
+
+ // Print items.
+ var b bytes.Buffer
+ fmt.Fprintf(&b, "Here's what you bought:\n")
+ var sum float64
+ for _, item := range items {
+ fmt.Fprintf(&b, "\t%s", item.Name)
+ fmt.Fprint(&b, strings.Repeat("\t", (40-len(item.Name)+7)/8))
+ fmt.Fprintf(&b, "$%5.2f\n", item.Price)
+ sum += item.Price
+ }
+ fmt.Fprintln(&b, strings.Repeat("-", 55))
+ fmt.Fprintf(&b, "\tTotal:\t\t\t\t\t$%.2f\n", sum)
+
+ w.Write(b.Bytes())
+
+ sendReceipt.Call(c, user.Current(c).Email, b.String())
+}
+
+var sendReceipt = delay.Func("send-receipt", func(c appengine.Context, dst, body string) {
+ msg := &mail.Message{
+ Sender: appAdmin,
+ To: []string{dst},
+ Subject: "Your Gopher Mart receipt",
+ Body: body,
+ }
+ if err := mail.Send(c, msg); err != nil {
+ c.Errorf("mail.Send: %v", err)
+ }
+})
+
+func adminPopulate(w http.ResponseWriter, r *http.Request) {
+ c := appengine.NewContext(r)
+ for i := range [numItems]struct{}{} { // r hates this. tee hee.
+ key := itemKey(c, i)
+ good := goods[rand.Intn(len(goods))]
+ item := &Item{
+ // TODO: vary names more
+ Name: fmt.Sprintf("%s %dg", good.name, i+1),
+ Price: float64(rand.Intn(1999)+1) / 100,
+ }
+ if _, err := datastore.Put(c, key, item); err != nil {
+ http.Error(w, err.Error(), 500)
+ return
+ }
+ }
+ fmt.Fprintf(w, "ok. %d items populated.", numItems)
+}
+
+var goods = [...]struct {
+ name string
+}{
+ {"Gopher Bran"},
+ {"Gopher Flakes"},
+ {"Gopher Grease"},
+ {"Gopher Litter"},
+}
diff --git a/2013/highperf/mart/README b/2013/highperf/mart/README
new file mode 100644
index 0000000..e4c90be
--- /dev/null
+++ b/2013/highperf/mart/README
@@ -0,0 +1,5 @@
+This is a little demonstration app called Gopher Mart.
+
+1/ is the simple, trivial, stupid, slow version.
+2/ defers work (mail).
+3/ uses batching.
diff --git a/2013/highperf/santa.png b/2013/highperf/santa.png
new file mode 100644
index 0000000..4409e9d
--- /dev/null
+++ b/2013/highperf/santa.png
Binary files differ
diff --git a/2013/highperf/santaembed.html b/2013/highperf/santaembed.html
new file mode 100644
index 0000000..746c75c
--- /dev/null
+++ b/2013/highperf/santaembed.html
@@ -0,0 +1 @@
+<img src="highperf/santa.png" style="float: right; margin-right: 10em;" />
diff --git a/2013/highperf/santagraph.png b/2013/highperf/santagraph.png
new file mode 100644
index 0000000..906aa8b
--- /dev/null
+++ b/2013/highperf/santagraph.png
Binary files differ
diff --git a/2013/highperf/turkey.png b/2013/highperf/turkey.png
new file mode 100644
index 0000000..d84e976
--- /dev/null
+++ b/2013/highperf/turkey.png
Binary files differ