blob: 2640e17251bf1e52e7cef0335d3ee3f3fe355ae9 [file] [log] [blame]
package main
import (
"encoding/json"
"flag"
"fmt"
ehtml "html"
"html/template"
"math/rand"
"net/http"
"os"
"path/filepath"
"regexp"
"runtime"
"strconv"
"strings"
"time"
)
import (
"github.com/fiorix/go-web/autogzip"
"github.com/golang/glog"
)
import (
"skia.googlesource.com/buildbot.git/go/common"
"skia.googlesource.com/buildbot.git/go/gitinfo"
"skia.googlesource.com/buildbot.git/go/login"
"skia.googlesource.com/buildbot.git/go/metadata"
"skia.googlesource.com/buildbot.git/go/util"
"skia.googlesource.com/buildbot.git/perf/go/activitylog"
"skia.googlesource.com/buildbot.git/perf/go/alerting"
"skia.googlesource.com/buildbot.git/perf/go/annotate"
"skia.googlesource.com/buildbot.git/perf/go/clustering"
"skia.googlesource.com/buildbot.git/perf/go/config"
"skia.googlesource.com/buildbot.git/perf/go/db"
"skia.googlesource.com/buildbot.git/perf/go/filetilestore"
"skia.googlesource.com/buildbot.git/perf/go/human"
"skia.googlesource.com/buildbot.git/perf/go/parser"
"skia.googlesource.com/buildbot.git/perf/go/shortcut"
"skia.googlesource.com/buildbot.git/perf/go/stats"
"skia.googlesource.com/buildbot.git/perf/go/trybot"
"skia.googlesource.com/buildbot.git/perf/go/types"
"skia.googlesource.com/buildbot.git/perf/go/vec"
)
const (
COOKIESALT_METADATA_KEY = "cookiesalt"
CLIENT_ID_METADATA_KEY = "clientid"
CLIENT_SECRET_METADATA_KEY = "clientsecret"
)
var (
// indexTemplate is the main index.html page we serve.
indexTemplate *template.Template = nil
// clusterTemplate is the /clusters/ page we serve.
clusterTemplate *template.Template = nil
alertsTemplate *template.Template = nil
clTemplate *template.Template = nil
activityTemplate *template.Template = nil
helpTemplate *template.Template = nil
// compareTemplate is the /compare/ page we serve.
compareTemplate *template.Template = nil
jsonHandlerPath = regexp.MustCompile(`/json/([a-z]*)$`)
shortcutHandlerPath = regexp.MustCompile(`/shortcuts/([0-9]*)$`)
// The three capture groups are dataset, tile scale, and tile number.
tileHandlerPath = regexp.MustCompile(`/tiles/([0-9]*)/([-0-9]*)/$`)
// The optional capture group is a githash.
singleHandlerPath = regexp.MustCompile(`/single/([0-9a-f]+)?$`)
// The three capture groups are tile scale, tile number, and an optional 'trace.
queryHandlerPath = regexp.MustCompile(`/query/([0-9]*)/([-0-9]*)/(traces/)?$`)
clHandlerPath = regexp.MustCompile(`/cl/([0-9]*)$`)
activityHandlerPath = regexp.MustCompile(`/activitylog/([0-9]*)$`)
git *gitinfo.GitInfo = nil
commitLinkifyRe = regexp.MustCompile("(?m)^commit (.*)$")
)
// flags
var (
port = flag.String("port", ":8000", "HTTP service address (e.g., ':8000')")
local = flag.Bool("local", false, "Running locally if true. As opposed to in production.")
gitRepoDir = flag.String("git_repo_dir", "../../../skia", "Directory location for the Skia repo.")
tileStoreDir = flag.String("tile_store_dir", "/tmp/tileStore", "What directory to look for tiles in.")
graphiteServer = flag.String("graphite_server", "skia-monitoring:2003", "Where is Graphite metrics ingestion server running.")
apikey = flag.String("apikey", "", "The API Key used to make issue tracker requests. Only for local testing.")
)
var (
nanoTileStore types.TileStore
)
func Init() {
rand.Seed(time.Now().UnixNano())
// Change the current working directory to two directories up from this source file so that we
// can read templates and serve static (res/) files.
_, filename, _, _ := runtime.Caller(0)
cwd := filepath.Join(filepath.Dir(filename), "../..")
if err := os.Chdir(cwd); err != nil {
glog.Fatalln(err)
}
indexTemplate = template.Must(template.ParseFiles(
filepath.Join(cwd, "templates/index.html"),
filepath.Join(cwd, "templates/titlebar.html"),
filepath.Join(cwd, "templates/header.html"),
))
clusterTemplate = template.Must(template.ParseFiles(
filepath.Join(cwd, "templates/clusters.html"),
filepath.Join(cwd, "templates/titlebar.html"),
filepath.Join(cwd, "templates/header.html"),
))
alertsTemplate = template.Must(template.ParseFiles(
filepath.Join(cwd, "templates/alerting.html"),
filepath.Join(cwd, "templates/titlebar.html"),
filepath.Join(cwd, "templates/header.html"),
))
clTemplate = template.Must(template.ParseFiles(
filepath.Join(cwd, "templates/cl.html"),
filepath.Join(cwd, "templates/titlebar.html"),
filepath.Join(cwd, "templates/header.html"),
))
activityTemplate = template.Must(template.ParseFiles(
filepath.Join(cwd, "templates/activitylog.html"),
filepath.Join(cwd, "templates/titlebar.html"),
filepath.Join(cwd, "templates/header.html"),
))
compareTemplate = template.Must(template.ParseFiles(
filepath.Join(cwd, "templates/compare.html"),
filepath.Join(cwd, "templates/titlebar.html"),
filepath.Join(cwd, "templates/header.html"),
))
helpTemplate = template.Must(template.ParseFiles(
filepath.Join(cwd, "templates/help.html"),
filepath.Join(cwd, "templates/titlebar.html"),
filepath.Join(cwd, "templates/header.html"),
))
nanoTileStore = filetilestore.NewFileTileStore(*tileStoreDir, config.DATASET_NANO, 2*time.Minute)
var err error
git, err = gitinfo.NewGitInfo(*gitRepoDir, true, false)
if err != nil {
glog.Fatal(err)
}
}
// showcutHandler handles the POST requests of the shortcut page.
//
// Shortcuts are of the form:
//
// {
// "scale": 0,
// "tiles": [-1],
// "hash": "a1092123890...",
// "ids": [
// "x86:...",
// "x86:...",
// "x86:...",
// ]
// }
//
// hash - The git hash of where a step was detected. Can be null.
//
func shortcutHandler(w http.ResponseWriter, r *http.Request) {
// TODO(jcgregorio): Add unit tests.
match := shortcutHandlerPath.FindStringSubmatch(r.URL.Path)
if match == nil {
http.NotFound(w, r)
return
}
if r.Method == "POST" {
// check header
if ct := r.Header.Get("Content-Type"); ct != "application/json" {
util.ReportError(w, r, fmt.Errorf("Error: received %s", ct), "Invalid content type.")
return
}
defer r.Body.Close()
id, err := shortcut.Insert(r.Body)
if err != nil {
util.ReportError(w, r, err, "Error inserting shortcut.")
return
}
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
if err := enc.Encode(map[string]string{"id": id}); err != nil {
util.ReportError(w, r, err, "Error while encoding response.")
}
} else {
http.NotFound(w, r)
}
}
// trybotHandler handles the GET for trybot data.
func trybotHandler(w http.ResponseWriter, r *http.Request) {
glog.Infof("Trybot Handler: %q\n", r.URL.Path)
w.Header().Set("Content-Type", "application/json")
try, err := trybot.List(50)
if err != nil {
util.ReportError(w, r, err, "Failed to retrieve trybot results.")
return
}
enc := json.NewEncoder(w)
if err = enc.Encode(try); err != nil {
util.ReportError(w, r, err, "Error while encoding response.")
}
}
// alertsHandler serves the HTML for the /alerts/ page.
//
// See alertingHandler for the JSON it uses.
func alertsHandler(w http.ResponseWriter, r *http.Request) {
glog.Infof("Alerts Handler: %q\n", r.URL.Path)
w.Header().Set("Content-Type", "text/html")
if err := alertsTemplate.Execute(w, nil); err != nil {
glog.Errorln("Failed to expand template:", err)
}
}
// alertingHandler returns the currently untriaged clusters.
//
// The return format is the same as clusteringHandler.
func alertingHandler(w http.ResponseWriter, r *http.Request) {
glog.Infof("Alerting Handler: %q\n", r.URL.Path)
w.Header().Set("Content-Type", "application/json")
tile, err := nanoTileStore.Get(0, -1)
if err != nil {
util.ReportError(w, r, err, fmt.Sprintf("Failed to load tile."))
return
}
alerts, err := alerting.ListFrom(tile.Commits[0].CommitTime)
if err != nil {
util.ReportError(w, r, err, "Error retrieving cluster summaries.")
return
}
enc := json.NewEncoder(w)
if err = enc.Encode(map[string][]*types.ClusterSummary{"Clusters": alerts}); err != nil {
util.ReportError(w, r, err, "Error while encoding response.")
}
}
// alertResetHandler deletes all the non-Bug alerts.
//
func alertResetHandler(w http.ResponseWriter, r *http.Request) {
glog.Infof("AlertResetHandler: %q\n", r.URL.Path)
if login.LoggedInAs(r) == "" {
util.ReportError(w, r, fmt.Errorf("Not logged in."), "You must be logged in to change an alert status.")
return
}
if r.Method != "POST" {
http.NotFound(w, r)
return
}
if err := alerting.Reset(); err != nil {
glog.Errorln("Failed to delete all non-Bug alerts:", err)
}
http.Redirect(w, r, "/alerts/", 303)
}
// clHandler serves the HTML for the /cl/<id> page.
//
// These are shortcuts to individual clusters.
//
// See alertingHandler for the JSON it uses.
//
func clHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
match := clHandlerPath.FindStringSubmatch(r.URL.Path)
if r.Method != "GET" || match == nil || len(match) != 2 {
http.NotFound(w, r)
return
}
id, err := strconv.ParseInt(match[1], 10, 0)
if err != nil {
util.ReportError(w, r, err, "Failed parsing ID.")
return
}
cl, err := alerting.Get(id)
if err != nil {
util.ReportError(w, r, err, "Failed to find cluster with that ID.")
return
}
if err := clTemplate.Execute(w, cl); err != nil {
glog.Errorln("Failed to expand template:", err)
}
}
// activityHandler serves the HTML for the /activitylog/ page.
//
// If an optional number n is appended to the path, returns the most recent n
// activities. Otherwise returns the most recent 100 results.
//
func activityHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
match := activityHandlerPath.FindStringSubmatch(r.URL.Path)
if r.Method != "GET" || match == nil || len(match) != 2 {
http.NotFound(w, r)
return
}
n := 100
if len(match[1]) > 0 {
num, err := strconv.ParseInt(match[1], 10, 0)
if err != nil {
util.ReportError(w, r, err, "Failed parsing the given number.")
return
}
n = int(num)
}
a, err := activitylog.GetRecent(n)
if err != nil {
util.ReportError(w, r, err, "Failed to retrieve activity.")
return
}
if err := activityTemplate.Execute(w, a); err != nil {
glog.Errorln("Failed to expand template:", err)
}
}
// compareHandler handles the GET of the compare page.
func compareHandler(w http.ResponseWriter, r *http.Request) {
glog.Infof("Compare Handler: %q\n", r.URL.Path)
w.Header().Set("Content-Type", "text/html")
if err := compareTemplate.Execute(w, nil); err != nil {
glog.Errorln("Failed to expand template:", err)
}
}
// clustersHandler handles the GET of the clusters page.
func clustersHandler(w http.ResponseWriter, r *http.Request) {
glog.Infof("Cluster Handler: %q\n", r.URL.Path)
w.Header().Set("Content-Type", "text/html")
if err := clusterTemplate.Execute(w, nil); err != nil {
glog.Errorln("Failed to expand template:", err)
}
}
// writeClusterSummaries writes out a ClusterSummaries instance as a JSON response.
func writeClusterSummaries(summary *clustering.ClusterSummaries, w http.ResponseWriter, r *http.Request) {
enc := json.NewEncoder(w)
if err := enc.Encode(summary); err != nil {
util.ReportError(w, r, err, "Error while encoding ClusterSummaries response.")
}
}
// clusteringHandler handles doing the actual k-means clustering.
//
// The return format is JSON of the form:
//
// {
// "Clusters": [
// {
// "Keys": [
// "x86:GeForce320M:MacMini4.1:Mac10.8:GM_varied_text_clipped_no_lcd_640_480:8888",...],
// "ParamSummaries": [
// [{"Value": "Win8", "Weight": 15}, {"Value": "Android", "Weight": 14}, ...]
// ],
// "StepFit": {
// "LeastSquares":0.0006582442047814354,
// "TurningPoint":162,
// "StepSize":0.023272272692293046,
// "Regression": 35.3
// }
// Traces: [[[0, -0.00007967326606768456], [1, 0.011877665949459049], [2, 0.012158129176717419],...]]
// },
// ...
// ],
// "K": 5,
// "StdDevThreshhold": 0.1
// }
//
// Note that Keys contains all the keys, while Traces only contains traces of
// the N closest cluster members and the centroid.
//
// Takes the following query parameters:
//
// _k - The K to use for k-means clustering.
// _stddev - The standard deviation to use when normalize traces
// during k-means clustering.
// _issue - The Rietveld issue ID with trybot results to include.
//
// Additionally the rest of the query parameters as returned from
// sk.Query.selectionsAsQuery().
func clusteringHandler(w http.ResponseWriter, r *http.Request) {
glog.Infof("Clustering Handler: %q\n", r.URL.Path)
tile, err := nanoTileStore.Get(0, -1)
if err != nil {
util.ReportError(w, r, err, fmt.Sprintf("Failed to load tile."))
return
}
w.Header().Set("Content-Type", "application/json")
// If there are no query parameters just return with an empty set of ClusterSummaries.
if r.FormValue("_k") == "" || r.FormValue("_stddev") == "" {
writeClusterSummaries(clustering.NewClusterSummaries(), w, r)
return
}
k, err := strconv.ParseInt(r.FormValue("_k"), 10, 32)
if err != nil {
util.ReportError(w, r, err, fmt.Sprintf("_k parameter must be an integer %s.", r.FormValue("_k")))
return
}
stddev, err := strconv.ParseFloat(r.FormValue("_stddev"), 64)
if err != nil {
util.ReportError(w, r, err, fmt.Sprintf("_stddev parameter must be a float %s.", r.FormValue("_stddev")))
return
}
issue := r.FormValue("_issue")
var tryResults *types.TryBotResults = nil
if issue != "" {
var err error
tryResults, err = trybot.Get(issue)
if err != nil {
util.ReportError(w, r, err, fmt.Sprintf("Failed to get trybot data for clustering."))
return
}
}
delete(r.Form, "_k")
delete(r.Form, "_stddev")
delete(r.Form, "_issue")
// Create a filter function for traces that match the query parameters and
// optionally tryResults.
filter := func(key string, tr *types.PerfTrace) bool {
if tryResults != nil {
if _, ok := tryResults.Values[key]; !ok {
return false
}
}
return types.Matches(tr, r.Form)
}
if issue != "" {
if tile, err = trybot.TileWithTryData(tile, issue); err != nil {
util.ReportError(w, r, err, fmt.Sprintf("Failed to get trybot data for clustering."))
return
}
}
summary, err := clustering.CalculateClusterSummaries(tile, int(k), stddev, filter)
if err != nil {
util.ReportError(w, r, err, "Failed to calculate clusters.")
return
}
writeClusterSummaries(summary, w, r)
}
// getTile retrieves a tile from the disk
func getTile(tileScale, tileNumber int) (*types.Tile, error) {
start := time.Now()
tile, err := nanoTileStore.Get(int(tileScale), int(tileNumber))
glog.Infoln("Time for tile load: ", time.Since(start).Nanoseconds())
if err != nil || tile == nil {
return nil, fmt.Errorf("Unable to get tile from tilestore: %s", err)
}
return tile, nil
}
// tileHandler accepts URIs like /tiles/0/1
// where the URI format is /tiles/<tile-scale>/<tile-number>
//
// It returns JSON of the form:
//
// {
// tiles: [20],
// scale: 0,
// paramset: {
// "os": ["Android", "ChromeOS", ..],
// "arch": ["Arm7", "x86", ...],
// },
// commits: [
// {
// "commit_time": 140329432,
// "hash": "0e03478100ea",
// "author": "someone@google.com",
// "commit_msg": "The subject line of the commit.",
// },
// ...
// ],
// ticks: [
// [1.5, "Mon"],
// [3.5, "Tue"]
// ],
// skps: [
// 5, 13, 24
// ]
// }
//
// Where skps are the commit indices where the SKPs were updated.
//
func tileHandler(w http.ResponseWriter, r *http.Request) {
glog.Infof("Tile Handler: %q\n", r.URL.Path)
handlerStart := time.Now()
match := tileHandlerPath.FindStringSubmatch(r.URL.Path)
if r.Method != "GET" || match == nil || len(match) != 3 {
http.NotFound(w, r)
return
}
tileScale, err := strconv.ParseInt(match[1], 10, 0)
if err != nil {
util.ReportError(w, r, err, "Failed parsing tile scale.")
return
}
tileNumber, err := strconv.ParseInt(match[2], 10, 0)
if err != nil {
util.ReportError(w, r, err, "Failed parsing tile number.")
return
}
glog.Infof("tile: %d %d", tileScale, tileNumber)
tile, err := getTile(int(tileScale), int(tileNumber))
if err != nil {
util.ReportError(w, r, err, "Failed retrieving tile.")
return
}
guiTile := types.NewTileGUI(tile.Scale, tile.TileIndex)
guiTile.Commits = tile.Commits
guiTile.ParamSet = tile.ParamSet
// SkpCommits goes out to the git repo, add caching if this turns out to be
// slow.
if skps, err := git.SkpCommits(tile); err != nil {
guiTile.Skps = []int{}
glog.Errorf("Failed to calculate skps: %s", err)
} else {
guiTile.Skps = skps
}
ts := []int64{}
for _, c := range tile.Commits {
if c.CommitTime != 0 {
ts = append(ts, c.CommitTime)
}
}
glog.Infof("%#v", ts)
guiTile.Ticks = human.FlotTickMarks(ts)
// Marshal and send
marshaledResult, err := json.Marshal(guiTile)
if err != nil {
util.ReportError(w, r, err, "Failed to marshal JSON.")
return
}
w.Header().Set("Content-Type", "application/json")
_, err = w.Write(marshaledResult)
if err != nil {
util.ReportError(w, r, err, "Error while marshalling results.")
}
glog.Infoln("Total handler time: ", time.Since(handlerStart).Nanoseconds())
}
// QueryResponse is for formatting the JSON output from queryHandler.
type QueryResponse struct {
Traces []*types.TraceGUI `json:"traces"`
Hash string `json:"hash"`
}
// FlatQueryResponse is for formatting the JSON output from calcHandler when the user
// requests flat=true. The output isn't formatted for input into Flot, instead the Values
// are returned as a simple slice, which is easier to work with in IPython.
type FlatQueryResponse struct {
Traces []*types.PerfTrace
}
// queryHandler handles queries for and about traces.
//
// Queries look like:
//
// /query/0/-1/?arch=Arm7&arch=x86&scale=1
//
// Where they keys and values in the query params are from the ParamSet.
// Repeated parameters are matched via OR. I.e. the above query will include
// anything that has an arch of Arm7 or x86.
//
// The first two path paramters are tile scale and tile number, where -1 means
// the last tile at the given scale.
//
// The normal response is JSON of the form:
//
// {
// "matches": 187,
// }
//
// If the path is:
//
// /query/0/-1/traces/?arch=Arm7&arch=x86&scale=1
//
// Then the response is the set of traces that match that query.
//
// {
// "traces": [
// {
// // All of these keys and values should be exactly what Flot consumes.
// data: [[1, 1.1], [20, 30]],
// label: "key1",
// _params: {"os: "Android", ...}
// },
// {
// data: [[1.2, 2.1], [20, 35]],
// label: "key2",
// _params: {"os: "Android", ...}
// }
// ]
// }
//
// If the path is:
//
// /query/0/-1/traces/?__shortcut=11
//
// Then the traces in the shortcut with that ID are returned, along with the
// git hash at the step function, if the shortcut came from an alert.
//
// {
// "traces": [
// {
// // All of these keys and values should be exactly what Flot consumes.
// data: [[1, 1.1], [20, 30]],
// label: "key1",
// _params: {"os: "Android", ...}
// },
// ...
// ],
// "hash": "a012334...",
// }
//
//
// TODO Add ability to query across a range of tiles.
func queryHandler(w http.ResponseWriter, r *http.Request) {
glog.Infof("Query Handler: %q\n", r.URL.Path)
match := queryHandlerPath.FindStringSubmatch(r.URL.Path)
glog.Infof("%#v", match)
if r.Method != "GET" || match == nil || len(match) != 4 {
http.NotFound(w, r)
return
}
if err := r.ParseForm(); err != nil {
util.ReportError(w, r, err, "Failed to parse query params.")
}
tileScale, err := strconv.ParseInt(match[1], 10, 0)
if err != nil {
util.ReportError(w, r, err, "Failed parsing tile scale.")
return
}
tileNumber, err := strconv.ParseInt(match[2], 10, 0)
if err != nil {
util.ReportError(w, r, err, "Failed parsing tile number.")
return
}
glog.Infof("tile: %d %d", tileScale, tileNumber)
tile, err := getTile(int(tileScale), int(tileNumber))
if err != nil {
util.ReportError(w, r, err, "Failed retrieving tile.")
return
}
w.Header().Set("Content-Type", "application/json")
ret := &QueryResponse{
Traces: []*types.TraceGUI{},
Hash: "",
}
if match[3] == "" {
// We only want the count.
total := 0
for _, tr := range tile.Traces {
if types.Matches(tr, r.Form) {
total++
}
}
glog.Info("Count: ", total)
inc := json.NewEncoder(w)
if err := inc.Encode(map[string]int{"matches": total}); err != nil {
util.ReportError(w, r, err, "Error while encoding query response.")
return
}
} else {
// We want the matching traces.
shortcutID := r.Form.Get("__shortcut")
if shortcutID != "" {
sh, err := shortcut.Get(shortcutID)
if err != nil {
http.NotFound(w, r)
return
}
if sh.Issue != "" {
if tile, err = trybot.TileWithTryData(tile, sh.Issue); err != nil {
util.ReportError(w, r, err, "Failed to populate shortcut data with trybot result.")
return
}
}
ret.Hash = sh.Hash
for _, k := range sh.Keys {
if tr, ok := tile.Traces[k]; ok {
tg := traceGuiFromTrace(tr.(*types.PerfTrace), k, tile)
if tg != nil {
ret.Traces = append(ret.Traces, tg)
}
} else if types.IsFormulaID(k) {
// Re-evaluate the formula and add all the results to the response.
formula := types.FormulaFromID(k)
if err := addCalculatedTraces(ret, tile, formula); err != nil {
glog.Errorf("Failed evaluating formula (%q) while processing shortcut %s: %s", formula, shortcutID, err)
}
} else if strings.HasPrefix(k, "!") {
glog.Errorf("A calculated trace is slipped through: (%s) in shortcut %s: %s", k, shortcutID, err)
}
}
} else {
for key, tr := range tile.Traces {
if types.Matches(tr, r.Form) {
tg := traceGuiFromTrace(tr.(*types.PerfTrace), key, tile)
if tg != nil {
ret.Traces = append(ret.Traces, tg)
}
}
}
}
enc := json.NewEncoder(w)
if err := enc.Encode(ret); err != nil {
util.ReportError(w, r, err, "Error while encoding query response.")
return
}
}
}
// SingleTrace is used in SingleResponse.
type SingleTrace struct {
Val float64 `json:"val"`
Params map[string]string `json:"params"`
}
// SingleResponse is for formatting the JSON output from singleHandler.
// Hash is the commit hash whose data are used in Traces.
type SingleResponse struct {
Traces []*SingleTrace `json:"traces"`
Hash string `json:"hash"`
}
// singleHandler is similar to /query/0/-1/traces?<param filters>, but takes an
// optional commit hash and returns a single value for each trace at that commit,
// or the latest value if a hash is not given or found. The resulting JSON is in
// SingleResponse format that looks like:
//
// {
// "traces": [
// {
// val: 1.1,
// params: {"os: "Android", ...}
// },
// ...
// ],
// "hash": "abc123",
// }
//
func singleHandler(w http.ResponseWriter, r *http.Request) {
glog.Infof("Single Handler: %q\n", r.URL.Path)
handlerStart := time.Now()
match := singleHandlerPath.FindStringSubmatch(r.URL.Path)
if r.Method != "GET" || match == nil || len(match) != 2 {
http.NotFound(w, r)
return
}
if err := r.ParseForm(); err != nil {
util.ReportError(w, r, err, "Failed to parse query params.")
}
hash := match[1]
tileNum, idx, err := git.TileAddressFromHash(hash, time.Time(config.BEGINNING_OF_TIME))
if err != nil {
glog.Infof("Did not find hash '%s', use latest: %q.\n", hash, err)
tileNum = -1
idx = -1
}
glog.Infof("Hash: %s tileNum: %d, idx: %d\n", hash, tileNum, idx)
tile, err := getTile(0, tileNum)
if err != nil {
util.ReportError(w, r, err, "Failed retrieving tile.")
return
}
if idx < 0 {
idx = len(tile.Commits) - 1 // Defaults to the last slice element.
}
glog.Infof("Tile: %d; Idx: %d\n", tileNum, idx)
ret := SingleResponse{
Traces: []*SingleTrace{},
Hash: tile.Commits[idx].Hash,
}
for _, tr := range tile.Traces {
if types.Matches(tr, r.Form) {
v, err := vec.FillAt(tr.(*types.PerfTrace).Values, idx)
if err != nil {
util.ReportError(w, r, err, "Error while getting value at slice index.")
return
}
t := &SingleTrace{
Val: v,
Params: tr.Params(),
}
ret.Traces = append(ret.Traces, t)
}
}
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
if err := enc.Encode(ret); err != nil {
util.ReportError(w, r, err, "Error while encoding single results.")
}
glog.Infoln("Total handler time: ", time.Since(handlerStart).Nanoseconds())
}
// traceGuiFromTrace returns a populated TraceGUI from the given trace.
func traceGuiFromTrace(trace *types.PerfTrace, key string, tile *types.Tile) *types.TraceGUI {
newTraceData := make([][2]float64, 0)
for i, v := range trace.Values {
if v != config.MISSING_DATA_SENTINEL && tile.Commits[i] != nil && tile.Commits[i].CommitTime > 0 {
//newTraceData = append(newTraceData, [2]float64{float64(tile.Commits[i].CommitTime), v})
newTraceData = append(newTraceData, [2]float64{float64(i), v})
}
}
if len(newTraceData) >= 0 {
return &types.TraceGUI{
Data: newTraceData,
Label: key,
Params: trace.Params(),
}
} else {
return nil
}
}
// addCalculatedTraces adds the traces returned from evaluating the given
// formula over the given tile to the QueryResponse.
func addCalculatedTraces(qr *QueryResponse, tile *types.Tile, formula string) error {
ctx := parser.NewContext(tile)
traces, err := ctx.Eval(formula)
if err != nil {
return fmt.Errorf("Failed to evaluate formula %q: %s", formula, err)
}
hasFormula := false
for _, tr := range traces {
if types.IsFormulaID(tr.Params()["id"]) {
hasFormula = true
}
tg := traceGuiFromTrace(tr, tr.Params()["id"], tile)
qr.Traces = append(qr.Traces, tg)
}
if !hasFormula {
// If we haven't added the formula trace to the response yet, add it in now.
f := types.NewPerfTraceN(len(tile.Commits))
tg := traceGuiFromTrace(f, types.AsFormulaID(formula), tile)
qr.Traces = append(qr.Traces, tg)
}
return nil
}
// addFlatCalculatedTraces adds the traces returned from evaluating the given
// formula over the given tile to the FlatQueryResponse. Doesn't include an empty
// formula trace. Useful for pulling data into IPython.
func addFlatCalculatedTraces(qr *FlatQueryResponse, tile *types.Tile, formula string) error {
ctx := parser.NewContext(tile)
traces, err := ctx.Eval(formula)
if err != nil {
return fmt.Errorf("Failed to evaluate formula %q: %s", formula, err)
}
for _, tr := range traces {
qr.Traces = append(qr.Traces, tr)
}
return nil
}
// calcHandler handles requests for the form:
//
// /calc/?formula=filter("config=8888")
//
// Where the formula is any formula that parser.Eval() accepts.
//
// The response is the same format as queryHandler.
func calcHandler(w http.ResponseWriter, r *http.Request) {
glog.Infof("Calc Handler: %q\n", r.URL.Path)
w.Header().Set("Content-Type", "application/json")
tile, err := nanoTileStore.Get(0, -1)
if err != nil {
util.ReportError(w, r, err, fmt.Sprintf("Failed to load tile."))
return
}
formula := r.FormValue("formula")
var data interface{} = nil
if r.FormValue("flat") == "true" {
resp := &FlatQueryResponse{
Traces: []*types.PerfTrace{},
}
if err := addFlatCalculatedTraces(resp, tile, formula); err != nil {
util.ReportError(w, r, err, fmt.Sprintf("Failed in /calc/ to evaluate formula."))
return
}
data = resp
} else {
resp := &QueryResponse{
Traces: []*types.TraceGUI{},
Hash: "",
}
if err := addCalculatedTraces(resp, tile, formula); err != nil {
util.ReportError(w, r, err, fmt.Sprintf("Failed in /calc/ to evaluate formula."))
return
}
data = resp
}
enc := json.NewEncoder(w)
if err := enc.Encode(data); err != nil {
util.ReportError(w, r, err, "Error while encoding query response.")
return
}
}
// commitsHandler handles requests for commits.
//
// Queries look like:
//
// /commits/?begin=hash1&end=hash2
//
// or if there is only one hash:
//
// /commits/?begin=hash
//
// The response is HTML of the form:
//
// <pre>
// commit <a href="http://skia.googlesource....">5bdbd13d8833d23e0da552f6817ae0b5a4e849e5</a>
// Author: Joe Gregorio <jcgregorio@google.com>
// Date: Wed Aug 6 16:16:18 2014 -0400
//
// Back to plotting lines.
//
// perf/go/skiaperf/perf.go
// perf/go/types/types.go
// perf/res/js/logic.js
//
// commit <a
// ...
// </pre>
//
func commitsHandler(w http.ResponseWriter, r *http.Request) {
glog.Infof("Query Handler: %q\n", r.URL.Path)
if r.Method != "GET" {
http.NotFound(w, r)
return
}
begin := r.FormValue("begin")
if len(begin) != 40 {
util.ReportError(w, r, fmt.Errorf("Invalid hash format: %s", begin), "Error while looking up hashes.")
return
}
end := r.FormValue("end")
body, err := git.Log(begin, end)
if err != nil {
util.ReportError(w, r, err, "Error while looking up hashes.")
return
}
escaped := ehtml.EscapeString(body)
linkified := commitLinkifyRe.ReplaceAllString(escaped, "<span class=subject>commit <a href=\"https://skia.googlesource.com/skia/+/${1}\" target=\"_blank\">${1}</a></span>")
w.Write([]byte(fmt.Sprintf("<pre>%s</pre>", linkified)))
}
// shortCommitsHandler returns basic info of a range of commits.
//
// Queries look like:
//
// /commits/?begin=hash1&end=hash2
//
// Response is JSON of ShortCommits format that looks like:
//
// {
// "commits": [
// {
// hash: "123abc",
// author: "bensong",
// subject: "Adds short commits."
// },
// ...
// ]
// }
//
func shortCommitsHandler(w http.ResponseWriter, r *http.Request) {
glog.Infof("Query Handler: %q\n", r.URL.Path)
if r.Method != "GET" {
http.NotFound(w, r)
return
}
begin := r.FormValue("begin")
if len(begin) != 40 {
util.ReportError(w, r, fmt.Errorf("Invalid begin hash format: %s", begin), "Error while looking up hashes.")
return
}
end := r.FormValue("end")
if len(end) != 40 {
util.ReportError(w, r, fmt.Errorf("Invalid end hash format: %s", end), "Error while looking up hashes.")
return
}
commits, err := git.ShortList(begin, end)
if err != nil {
util.ReportError(w, r, err, "Error while looking up hashes.")
return
}
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
if err := enc.Encode(commits); err != nil {
util.ReportError(w, r, err, "Error while encoding response.")
}
}
// mainHandler handles the GET of the main page.
func mainHandler(w http.ResponseWriter, r *http.Request) {
glog.Infof("Main Handler: %q\n", r.URL.Path)
if r.Method == "GET" {
w.Header().Set("Content-Type", "text/html")
if err := indexTemplate.Execute(w, struct{}{}); err != nil {
glog.Errorln("Failed to expand template:", err)
}
}
}
// helpHandler handles the GET of the main page.
func helpHandler(w http.ResponseWriter, r *http.Request) {
glog.Infof("Help Handler: %q\n", r.URL.Path)
if r.Method == "GET" {
w.Header().Set("Content-Type", "text/html")
ctx := parser.NewContext(nil)
if err := helpTemplate.Execute(w, ctx); err != nil {
glog.Errorln("Failed to expand template:", err)
}
}
}
func makeResourceHandler() func(http.ResponseWriter, *http.Request) {
fileServer := http.FileServer(http.Dir("./"))
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Cache-Control", string(300))
fileServer.ServeHTTP(w, r)
}
}
func main() {
common.InitWithMetrics("skiaperf", *graphiteServer)
Init()
db.Init(db.ProdDatabaseConfig(*local))
stats.Start(nanoTileStore, git)
alerting.Start(nanoTileStore, *apikey)
// By default use a set of credentials setup for localhost access.
var cookieSalt = "notverysecret"
var clientID = "31977622648-1873k0c1e5edaka4adpv1ppvhr5id3qm.apps.googleusercontent.com"
var clientSecret = "cw0IosPu4yjaG2KWmppj2guj"
var redirectURL = fmt.Sprintf("http://localhost%s/oauth2callback/", *port)
if !*local {
cookieSalt = metadata.MustGet(COOKIESALT_METADATA_KEY)
clientID = metadata.MustGet(CLIENT_ID_METADATA_KEY)
clientSecret = metadata.MustGet(CLIENT_SECRET_METADATA_KEY)
redirectURL = "http://skiaperf.com/oauth2callback/"
}
login.Init(clientID, clientSecret, redirectURL, cookieSalt)
glog.Infoln("Begin loading data.")
// Resources are served directly.
http.HandleFunc("/res/", autogzip.HandleFunc(makeResourceHandler()))
http.HandleFunc("/", autogzip.HandleFunc(mainHandler))
http.HandleFunc("/shortcuts/", shortcutHandler)
http.HandleFunc("/tiles/", tileHandler)
http.HandleFunc("/single/", singleHandler)
http.HandleFunc("/query/", queryHandler)
http.HandleFunc("/commits/", commitsHandler)
http.HandleFunc("/shortcommits/", shortCommitsHandler)
http.HandleFunc("/trybots/", autogzip.HandleFunc(trybotHandler))
http.HandleFunc("/clusters/", autogzip.HandleFunc(clustersHandler))
http.HandleFunc("/clustering/", autogzip.HandleFunc(clusteringHandler))
http.HandleFunc("/cl/", autogzip.HandleFunc(clHandler))
http.HandleFunc("/activitylog/", autogzip.HandleFunc(activityHandler))
http.HandleFunc("/alerts/", autogzip.HandleFunc(alertsHandler))
http.HandleFunc("/alerting/", autogzip.HandleFunc(alertingHandler))
http.HandleFunc("/alert_reset/", autogzip.HandleFunc(alertResetHandler))
http.HandleFunc("/annotate/", autogzip.HandleFunc(annotate.Handler))
http.HandleFunc("/compare/", autogzip.HandleFunc(compareHandler))
http.HandleFunc("/calc/", autogzip.HandleFunc(calcHandler))
http.HandleFunc("/help/", autogzip.HandleFunc(helpHandler))
http.HandleFunc("/oauth2callback/", login.OAuth2CallbackHandler)
http.HandleFunc("/logout/", login.LogoutHandler)
http.HandleFunc("/loginstatus/", login.StatusHandler)
glog.Infoln("Ready to serve.")
glog.Fatal(http.ListenAndServe(*port, nil))
}