all: improve local server workflow

- document local servers in README
- rename local servers to "localperf" and "localperfdata" so one can
  "go get" them.
- find static/templates dirs automatically
- allow configuring a persistent database in localperfdata

Change-Id: I4e62f23c38be6978f091ccbbda8002d9f588b8a4
Reviewed-on: https://go-review.googlesource.com/37717
Reviewed-by: Russ Cox <rsc@golang.org>
diff --git a/README b/README
deleted file mode 100644
index 4ed5c68..0000000
--- a/README
+++ /dev/null
@@ -1,4 +0,0 @@
-This subrepository holds the source for various packages and tools
-related to performance measurement, storage, and analysis.
-
-To submit changes to this repository, see http://golang.org/doc/contribute.html.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..d923b4e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,37 @@
+# Go performance measurement, storage, and analysis tools
+
+This subrepository holds the source for various packages and tools
+related to performance measurement, storage, and analysis.
+
+[cmd/benchstat](cmd/benchstat) contains a command-line tool that
+computes and compares statistics about benchmarks.
+
+[cmd/benchsave](cmd/benchsave) contains a command-line tool for
+publishing benchmark results.
+
+[storage](storage) contains the https://perfdata.golang.org/ benchmark
+result storage system.
+
+[analysis](analysis) contains the https://perf.golang.org/ benchmark
+result analysis system.
+
+Both storage and analysis can be run locally; the following commands will run
+the complete stack on your machine with an in-memory datastore.
+
+```
+go get -u golang.org/x/perf/storage/localperfdata
+go get -u golang.org/x/perf/analysis/localperf
+localperfdata -addr=:8081 -view_url_base=http://localhost:8080/search?q=upload: &
+localperf -addr=:8080 -storage=localhost:8081
+```
+
+The storage system is designed to have a
+[standardized API](storage/appengine/static/index.html), and we
+encourage additional analysis tools to be written against the API. A
+client can be found in [storage/client](storage/client).
+
+--
+
+Contributions to Go are appreciated. See http://golang.org/doc/contribute.html.
+
+* Bugs can be filed at the [Go issue tracker](https://golang.org/issue/new?title=x/perf:+).
diff --git a/analysis/app/app.go b/analysis/app/app.go
index d4bd819..120d3eb 100644
--- a/analysis/app/app.go
+++ b/analysis/app/app.go
@@ -16,6 +16,10 @@
 type App struct {
 	// StorageClient is used to talk to the storage server.
 	StorageClient *storage.Client
+
+	// BaseDir is the directory containing the "template" directory.
+	// If empty, the current directory will be used.
+	BaseDir string
 }
 
 // RegisterOnMux registers the app's URLs on mux.
diff --git a/analysis/app/compare.go b/analysis/app/compare.go
index f154b1e..3e47fca 100644
--- a/analysis/app/compare.go
+++ b/analysis/app/compare.go
@@ -11,6 +11,7 @@
 	"html/template"
 	"io/ioutil"
 	"net/http"
+	"path/filepath"
 	"sort"
 	"strconv"
 	"strings"
@@ -140,7 +141,7 @@
 
 	q := r.Form.Get("q")
 
-	tmpl, err := ioutil.ReadFile("template/compare.html")
+	tmpl, err := ioutil.ReadFile(filepath.Join(a.BaseDir, "template/compare.html"))
 	if err != nil {
 		http.Error(w, err.Error(), 500)
 		return
diff --git a/analysis/app/index.go b/analysis/app/index.go
index 99b5af3..edd4ef4 100644
--- a/analysis/app/index.go
+++ b/analysis/app/index.go
@@ -8,6 +8,7 @@
 	"html/template"
 	"io/ioutil"
 	"net/http"
+	"path/filepath"
 
 	"golang.org/x/perf/storage"
 )
@@ -16,7 +17,7 @@
 func (a *App) index(w http.ResponseWriter, r *http.Request) {
 	ctx := requestContext(r)
 
-	tmpl, err := ioutil.ReadFile("template/index.html")
+	tmpl, err := ioutil.ReadFile(filepath.Join(a.BaseDir, "template/index.html"))
 	if err != nil {
 		http.Error(w, err.Error(), 500)
 		return
diff --git a/analysis/app/trend.go b/analysis/app/trend.go
index 9b47f6f..af03dfe 100644
--- a/analysis/app/trend.go
+++ b/analysis/app/trend.go
@@ -14,6 +14,7 @@
 	"io/ioutil"
 	"math"
 	"net/http"
+	"path/filepath"
 	"sort"
 	"strconv"
 	"strings"
@@ -38,7 +39,7 @@
 
 	q := r.Form.Get("q")
 
-	tmpl, err := ioutil.ReadFile("template/trend.html")
+	tmpl, err := ioutil.ReadFile(filepath.Join(a.BaseDir, "template/trend.html"))
 	if err != nil {
 		http.Error(w, err.Error(), 500)
 		return
diff --git a/analysis/localperf/app.go b/analysis/localperf/app.go
new file mode 100644
index 0000000..a82b0c9
--- /dev/null
+++ b/analysis/localperf/app.go
@@ -0,0 +1,60 @@
+// Copyright 2017 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Localperf runs an HTTP server for benchmark analysis.
+//
+// Usage:
+//
+//     localperf [-addr address] [-storage url] [-base_dir ../appengine]
+package main
+
+import (
+	"flag"
+	"fmt"
+	"log"
+	"net/http"
+	"os"
+
+	"golang.org/x/perf/analysis/app"
+	"golang.org/x/perf/internal/basedir"
+	"golang.org/x/perf/storage"
+)
+
+var (
+	addr       = flag.String("addr", "localhost:8080", "serve HTTP on `address`")
+	storageURL = flag.String("storage", "https://perfdata.golang.org", "storage server base `url`")
+	baseDir    = flag.String("base_dir", basedir.Find("golang.org/x/perf/analysis/appengine"), "base `directory` for templates")
+)
+
+func usage() {
+	fmt.Fprintf(os.Stderr, `Usage of localperf:
+	localperf [flags]
+`)
+	flag.PrintDefaults()
+	os.Exit(2)
+}
+
+func main() {
+	log.SetPrefix("localperf: ")
+	flag.Usage = usage
+	flag.Parse()
+	if flag.NArg() != 0 {
+		flag.Usage()
+	}
+
+	if *baseDir == "" {
+		log.Print("base_dir is required and could not be automatically found")
+		flag.Usage()
+	}
+
+	app := &app.App{
+		StorageClient: &storage.Client{BaseURL: *storageURL},
+		BaseDir:       *baseDir,
+	}
+	app.RegisterOnMux(http.DefaultServeMux)
+
+	log.Printf("Listening on %s", *addr)
+
+	log.Fatal(http.ListenAndServe(*addr, nil))
+}
diff --git a/analysis/localserver/app.go b/analysis/localserver/app.go
deleted file mode 100644
index 6076aab..0000000
--- a/analysis/localserver/app.go
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright 2017 The Go Authors.  All rights reserved.
-// Use of this source code is governed by a BSD-style
-// license that can be found in the LICENSE file.
-
-// Localserver runs an HTTP server for benchmark analysis.
-//
-// Usage:
-//
-//     localserver [-addr address] [-storage url]
-package main
-
-import (
-	"flag"
-	"fmt"
-	"log"
-	"net/http"
-	"os"
-
-	"golang.org/x/perf/analysis/app"
-	"golang.org/x/perf/storage"
-)
-
-var (
-	addr       = flag.String("addr", "localhost:8080", "serve HTTP on `address`")
-	storageURL = flag.String("storage", "https://perfdata.golang.org", "storage server base `url`")
-)
-
-func usage() {
-	fmt.Fprintf(os.Stderr, `Usage of localserver:
-	localserver [flags]
-`)
-	flag.PrintDefaults()
-	os.Exit(2)
-}
-
-func main() {
-	log.SetPrefix("localserver: ")
-	flag.Usage = usage
-	flag.Parse()
-	if flag.NArg() != 0 {
-		flag.Usage()
-	}
-
-	app := &app.App{StorageClient: &storage.Client{BaseURL: *storageURL}}
-	app.RegisterOnMux(http.DefaultServeMux)
-
-	log.Printf("Listening on %s", *addr)
-
-	log.Fatal(http.ListenAndServe(*addr, nil))
-}
diff --git a/internal/basedir/basedir.go b/internal/basedir/basedir.go
new file mode 100644
index 0000000..90985c0
--- /dev/null
+++ b/internal/basedir/basedir.go
@@ -0,0 +1,58 @@
+// Copyright 2017 The Go Authors.  All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package basedir finds templates and static files associated with a binary.
+package basedir
+
+import (
+	"bytes"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"runtime"
+	"strings"
+)
+
+// Find locates a directory for the given package.
+// pkg should be the directory that contains the templates and/or static directories.
+// If pkg cannot be found, an empty string will be returned.
+func Find(pkg string) string {
+	cmd := exec.Command("go", "list", "-e", "-f", "{{.Dir}}", pkg)
+	if out, err := cmd.Output(); err == nil && len(out) > 0 {
+		return string(bytes.TrimRight(out, "\r\n"))
+	}
+	gopath := os.Getenv("GOPATH")
+	if gopath == "" {
+		gopath = defaultGOPATH()
+	}
+	if gopath != "" {
+		for _, dir := range strings.Split(gopath, ":") {
+			p := filepath.Join(dir, pkg)
+			if _, err := os.Stat(p); err == nil {
+				return p
+			}
+		}
+	}
+	return ""
+}
+
+// Copied from go/build/build.go
+func defaultGOPATH() string {
+	env := "HOME"
+	if runtime.GOOS == "windows" {
+		env = "USERPROFILE"
+	} else if runtime.GOOS == "plan9" {
+		env = "home"
+	}
+	if home := os.Getenv(env); home != "" {
+		def := filepath.Join(home, "go")
+		if filepath.Clean(def) == filepath.Clean(runtime.GOROOT()) {
+			// Don't set the default GOPATH to GOROOT,
+			// as that will trigger warnings from the go tool.
+			return ""
+		}
+		return def
+	}
+	return ""
+}
diff --git a/storage/app/app.go b/storage/app/app.go
index 558ff47..48e0a85 100644
--- a/storage/app/app.go
+++ b/storage/app/app.go
@@ -9,6 +9,7 @@
 import (
 	"errors"
 	"net/http"
+	"path/filepath"
 
 	"golang.org/x/perf/storage/db"
 	"golang.org/x/perf/storage/fs"
@@ -30,6 +31,10 @@
 	// "viewurl" in the response from /upload. If it is non-empty,
 	// the upload ID will be appended to ViewURLBase.
 	ViewURLBase string
+
+	// BaseDir is the directory containing the "template" directory.
+	// If empty, the current directory will be used.
+	BaseDir string
 }
 
 // ErrResponseWritten can be returned by App.Auth to abort the normal /upload handling.
@@ -46,5 +51,5 @@
 
 // index serves the readme on /
 func (a *App) index(w http.ResponseWriter, r *http.Request) {
-	http.ServeFile(w, r, "static/index.html")
+	http.ServeFile(w, r, filepath.Join(a.BaseDir, "static/index.html"))
 }
diff --git a/storage/app/upload.go b/storage/app/upload.go
index aa983b3..eb9304e 100644
--- a/storage/app/upload.go
+++ b/storage/app/upload.go
@@ -12,6 +12,7 @@
 	"mime/multipart"
 	"net/http"
 	"net/url"
+	"path/filepath"
 	"sort"
 	"strings"
 	"time"
@@ -36,7 +37,7 @@
 	}
 
 	if r.Method == http.MethodGet {
-		http.ServeFile(w, r, "static/upload.html")
+		http.ServeFile(w, r, filepath.Join(a.BaseDir, "static/upload.html"))
 		return
 	}
 	if r.Method != http.MethodPost {
diff --git a/storage/localserver/app.go b/storage/localperfdata/app.go
similarity index 61%
rename from storage/localserver/app.go
rename to storage/localperfdata/app.go
index 3a76e38..9a8d560 100644
--- a/storage/localserver/app.go
+++ b/storage/localperfdata/app.go
@@ -2,6 +2,11 @@
 // Use of this source code is governed by a BSD-style
 // license that can be found in the LICENSE file.
 
+// Localperfdata runs an HTTP server for benchmark storage.
+//
+// Usage:
+//
+//     localperfdata [-addr address] [-view_url_base url] [-base_dir ../appengine] [-dsn file.db]
 package main
 
 import (
@@ -9,6 +14,7 @@
 	"log"
 	"net/http"
 
+	"golang.org/x/perf/internal/basedir"
 	"golang.org/x/perf/storage/app"
 	"golang.org/x/perf/storage/db"
 	_ "golang.org/x/perf/storage/db/sqlite3"
@@ -18,12 +24,19 @@
 var (
 	addr        = flag.String("addr", ":8080", "serve HTTP on `address`")
 	viewURLBase = flag.String("view_url_base", "", "/upload response with `URL` for viewing")
+	dsn         = flag.String("dsn", ":memory:", "sqlite `dsn`")
+	baseDir     = flag.String("base_dir", basedir.Find("golang.org/x/perf/storage/appengine"), "base `directory` for static files")
 )
 
 func main() {
 	flag.Parse()
 
-	db, err := db.OpenSQL("sqlite3", ":memory:")
+	if *baseDir == "" {
+		log.Print("base_dir is required and could not be automatically found")
+		flag.Usage()
+	}
+
+	db, err := db.OpenSQL("sqlite3", *dsn)
 	if err != nil {
 		log.Fatalf("open database: %v", err)
 	}
@@ -34,6 +47,7 @@
 		FS:          fs,
 		ViewURLBase: *viewURLBase,
 		Auth:        func(http.ResponseWriter, *http.Request) (string, error) { return "", nil },
+		BaseDir:     *baseDir,
 	}
 	app.RegisterOnMux(http.DefaultServeMux)