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