Commit 9181f6a8 authored by Quentin Smith's avatar Quentin Smith

analysis: basic analysis app

The app can a parse query and fetch relevant results. It can run on
both App Engine and locally.

Change-Id: I34a6415750fc29ed468997f05bc6d8c0ad068235
Reviewed-on: https://go-review.googlesource.com/35493Reviewed-by: default avatarRuss Cox <rsc@golang.org>
parent 6c208574
// 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 app implements the performance data analysis server.
package app
import (
"net/http"
"golang.org/x/perf/storage"
)
// App manages the analysis server logic.
// Construct an App instance and call RegisterOnMux to connect it with an HTTP server.
type App struct {
// StorageClient is used to talk to the storage server.
StorageClient *storage.Client
}
// RegisterOnMux registers the app's URLs on mux.
func (a *App) RegisterOnMux(mux *http.ServeMux) {
mux.HandleFunc("/search", a.search)
mux.HandleFunc("/compare", a.compare)
}
// search handles /search.
// This currently just runs the compare handler, until more analysis methods are implemented.
func (a *App) search(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), 500)
return
}
// TODO(quentin): Intelligently choose an analysis method
// based on the results from the query, once there is more
// than one analysis method.
//q := r.Form.Get("q")
a.compare(w, r)
}
// 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 app
import (
"fmt"
"net/http"
"sort"
"golang.org/x/perf/storage/benchfmt"
)
// A resultGroup holds a list of results and tracks the distinct labels found in that list.
type resultGroup struct {
// Raw list of results.
results []*benchfmt.Result
// LabelValues is the count of results found with each distinct (key, value) pair found in labels.
LabelValues map[string]map[string]int
}
// add adds res to the resultGroup.
func (g *resultGroup) add(res *benchfmt.Result) {
g.results = append(g.results, res)
if g.LabelValues == nil {
g.LabelValues = make(map[string]map[string]int)
}
for k, v := range res.Labels {
if g.LabelValues[k] == nil {
g.LabelValues[k] = make(map[string]int)
}
g.LabelValues[k][v]++
}
}
// splitOn returns a new set of groups sharing a common value for key.
func (g *resultGroup) splitOn(key string) []*resultGroup {
groups := make(map[string]*resultGroup)
var values []string
for _, res := range g.results {
value := res.Labels[key]
if groups[value] == nil {
groups[value] = &resultGroup{}
values = append(values, value)
}
groups[value].add(res)
}
sort.Strings(values)
var out []*resultGroup
for _, value := range values {
out = append(out, groups[value])
}
return out
}
// compare handles queries that require comparison of the groups in the query.
func (a *App) compare(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), 500)
return
}
q := r.Form.Get("q")
// Parse query
queries := parseQueryString(q)
// Send requests
// TODO(quentin): Issue requests in parallel?
var groups []*resultGroup
for _, q := range queries {
group := &resultGroup{}
res := a.StorageClient.Query(q)
defer res.Close() // TODO: Should happen each time through the loop
for res.Next() {
group.add(res.Result())
}
if err := res.Err(); err != nil {
// TODO: If the query is invalid, surface that to the user.
http.Error(w, err.Error(), 500)
return
}
groups = append(groups, group)
}
// Attempt to automatically split results.
if len(groups) == 1 {
group := groups[0]
// Matching a single upload with multiple files -> split by file
if len(group.LabelValues["upload"]) == 1 && len(group.LabelValues["upload-part"]) > 1 {
groups = group.splitOn("upload-part")
}
}
// TODO: Compute benchstat
// TODO: Render template. This is just temporary output to confirm the above works.
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
for i, g := range groups {
fmt.Fprintf(w, "Group #%d: %d results\n", i, len(g.results))
for k, vs := range g.labelValues {
fmt.Fprintf(w, "\t%s: %#v\n", k, vs)
}
fmt.Fprintf(w, "\n")
}
}
// 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 app
import (
"reflect"
"strings"
"testing"
"golang.org/x/perf/storage/benchfmt"
)
func TestResultGroup(t *testing.T) {
data := `key: value
BenchmarkName 1 ns/op
key: value2
BenchmarkName 1 ns/op`
var results []*benchfmt.Result
br := benchfmt.NewReader(strings.NewReader(data))
g := &resultGroup{}
for br.Next() {
results = append(results, br.Result())
g.add(br.Result())
}
if err := br.Err(); err != nil {
t.Fatalf("Err() = %v, want nil", err)
}
if !reflect.DeepEqual(g.results, results) {
t.Errorf("g.results = %#v, want %#v", g.results, results)
}
if want := map[string]map[string]int{"key": {"value": 1, "value2": 1}}; !reflect.DeepEqual(g.LabelValues, want) {
t.Errorf("g.LabelValues = %#v, want %#v", g.LabelValues, want)
}
groups := g.splitOn("key")
if len(groups) != 2 {
t.Fatalf("g.splitOn returned %d groups, want 2", len(groups))
}
for i, results := range [][]*benchfmt.Result{
{results[0]},
{results[1]},
} {
if !reflect.DeepEqual(groups[i].results, results) {
t.Errorf("groups[%d].results = %#v, want %#v", i, groups[i].results, results)
}
}
}
// 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 app
import "strings"
// parseQueryString splits a user-entered query into one or more storage server queries.
// The supported query formats are:
// prefix | one vs two - parsed as {"prefix one", "prefix two"}
// prefix one vs two - parsed as {"prefix one", "two"}
// anything else - parsed as {"anything else"}
// The vs and | separators must not be quoted.
func parseQueryString(q string) []string {
var queries []string
var parts []string
var prefix string
quoting := false
for r := 0; r < len(q); {
switch c := q[r]; {
case c == '"' && quoting:
quoting = false
r++
case quoting:
if c == '\\' {
r++
}
r++
case c == '"':
quoting = true
r++
case c == ' ', c == '\t':
switch part := q[:r]; {
case part == "|" && prefix == "":
prefix = strings.Join(parts, " ") + " "
parts = nil
case part == "vs":
queries = append(queries, prefix+strings.Join(parts, " "))
parts = nil
default:
parts = append(parts, part)
}
q = q[r+1:]
r = 0
default:
if c == '\\' {
r++
}
r++
}
}
if len(q) > 0 {
parts = append(parts, q)
}
if len(parts) > 0 {
queries = append(queries, prefix+strings.Join(parts, " "))
}
return queries
}
// 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 app
import (
"reflect"
"testing"
)
func TestParseQueryString(t *testing.T) {
tests := []struct {
q string
want []string
}{
{"prefix | one vs two", []string{"prefix one", "prefix two"}},
{"prefix one vs two", []string{"prefix one", "two"}},
{"anything else", []string{"anything else"}},
{`one vs "two vs three"`, []string{"one", `"two vs three"`}},
{"mixed\ttabs \"and\tspaces\"", []string{"mixed tabs \"and\tspaces\""}},
}
for _, test := range tests {
t.Run(test.q, func(t *testing.T) {
have := parseQueryString(test.q)
if !reflect.DeepEqual(have, test.want) {
t.Fatalf("parseQueryString = %#v, want %#v", have, test.want)
}
})
}
}
// 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 appengine contains an AppEngine app for perf.golang.org
package appengine
import (
"log"
"net/http"
"os"
"golang.org/x/perf/analysis/app"
"golang.org/x/perf/storage"
"google.golang.org/appengine"
"google.golang.org/appengine/urlfetch"
)
func mustGetenv(k string) string {
v := os.Getenv(k)
if v == "" {
log.Panicf("%s environment variable not set.", k)
}
return v
}
// appHandler is the default handler, registered to serve "/".
// It creates a new App instance using the appengine Context and then
// dispatches the request to the App. The environment variable
// STORAGE_URL_BASE must be set in app.yaml with the name of the bucket to
// write to.
func appHandler(w http.ResponseWriter, r *http.Request) {
ctx := appengine.NewContext(r)
app := &app.App{
StorageClient: &storage.Client{
BaseURL: mustGetenv("STORAGE_URL_BASE"),
HTTPClient: urlfetch.Client(ctx),
},
}
mux := http.NewServeMux()
app.RegisterOnMux(mux)
mux.ServeHTTP(w, r)
}
func init() {
http.HandleFunc("/", appHandler)
}
# Update with
# google_appengine/appcfg.py [-V dev-test] update .
#
# Using -V dev-test will run as dev-test.perf.golang.org.
application: golang-org
module: perf
version: main
runtime: go
api_version: go1
handlers:
- url: /_ah/remote_api
script: _go_app
- url: /.*
script: _go_app
secure: always
env_variables:
STORAGE_URL_BASE: "https://perfdata.golang.org"
// 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))
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment