2013-07-31 21:23:51 -06:00
|
|
|
// Copyright 2011 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.
|
|
|
|
|
|
|
|
// +build appengine
|
|
|
|
|
|
|
|
package build
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"compress/gzip"
|
|
|
|
"crypto/sha1"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
2014-05-13 01:00:32 -06:00
|
|
|
"net/http"
|
|
|
|
"sort"
|
|
|
|
"strconv"
|
2013-07-31 21:23:51 -06:00
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"appengine"
|
|
|
|
"appengine/datastore"
|
2014-05-13 01:00:32 -06:00
|
|
|
|
|
|
|
"cache"
|
2013-07-31 21:23:51 -06:00
|
|
|
)
|
|
|
|
|
2014-05-13 01:00:32 -06:00
|
|
|
const (
|
|
|
|
maxDatastoreStringLen = 500
|
|
|
|
PerfRunLength = 1024
|
|
|
|
)
|
2013-07-31 21:23:51 -06:00
|
|
|
|
|
|
|
// A Package describes a package that is listed on the dashboard.
|
|
|
|
type Package struct {
|
|
|
|
Kind string // "subrepo", "external", or empty for the main Go tree
|
|
|
|
Name string
|
|
|
|
Path string // (empty for the main Go tree)
|
|
|
|
NextNum int // Num of the next head Commit
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *Package) String() string {
|
|
|
|
return fmt.Sprintf("%s: %q", p.Path, p.Name)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (p *Package) Key(c appengine.Context) *datastore.Key {
|
|
|
|
key := p.Path
|
|
|
|
if key == "" {
|
|
|
|
key = "go"
|
|
|
|
}
|
|
|
|
return datastore.NewKey(c, "Package", key, 0, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
// LastCommit returns the most recent Commit for this Package.
|
|
|
|
func (p *Package) LastCommit(c appengine.Context) (*Commit, error) {
|
|
|
|
var commits []*Commit
|
|
|
|
_, err := datastore.NewQuery("Commit").
|
|
|
|
Ancestor(p.Key(c)).
|
|
|
|
Order("-Time").
|
|
|
|
Limit(1).
|
|
|
|
GetAll(c, &commits)
|
|
|
|
if _, ok := err.(*datastore.ErrFieldMismatch); ok {
|
|
|
|
// Some fields have been removed, so it's okay to ignore this error.
|
|
|
|
err = nil
|
|
|
|
}
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if len(commits) != 1 {
|
|
|
|
return nil, datastore.ErrNoSuchEntity
|
|
|
|
}
|
|
|
|
return commits[0], nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetPackage fetches a Package by path from the datastore.
|
|
|
|
func GetPackage(c appengine.Context, path string) (*Package, error) {
|
|
|
|
p := &Package{Path: path}
|
|
|
|
err := datastore.Get(c, p.Key(c), p)
|
|
|
|
if err == datastore.ErrNoSuchEntity {
|
|
|
|
return nil, fmt.Errorf("package %q not found", path)
|
|
|
|
}
|
|
|
|
if _, ok := err.(*datastore.ErrFieldMismatch); ok {
|
|
|
|
// Some fields have been removed, so it's okay to ignore this error.
|
|
|
|
err = nil
|
|
|
|
}
|
|
|
|
return p, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// A Commit describes an individual commit in a package.
|
|
|
|
//
|
|
|
|
// Each Commit entity is a descendant of its associated Package entity.
|
|
|
|
// In other words, all Commits with the same PackagePath belong to the same
|
|
|
|
// datastore entity group.
|
|
|
|
type Commit struct {
|
2013-09-23 18:06:49 -06:00
|
|
|
PackagePath string // (empty for main repo commits)
|
2013-07-31 21:23:51 -06:00
|
|
|
Hash string
|
|
|
|
ParentHash string
|
|
|
|
Num int // Internal monotonic counter unique to this package.
|
|
|
|
|
2014-05-13 01:00:32 -06:00
|
|
|
User string
|
|
|
|
Desc string `datastore:",noindex"`
|
|
|
|
Time time.Time
|
|
|
|
NeedsBenchmarking bool
|
2014-05-19 10:51:04 -06:00
|
|
|
TryPatch bool
|
2013-07-31 21:23:51 -06:00
|
|
|
|
|
|
|
// ResultData is the Data string of each build Result for this Commit.
|
|
|
|
// For non-Go commits, only the Results for the current Go tip, weekly,
|
|
|
|
// and release Tags are stored here. This is purely de-normalized data.
|
|
|
|
// The complete data set is stored in Result entities.
|
|
|
|
ResultData []string `datastore:",noindex"`
|
|
|
|
|
2014-05-13 01:00:32 -06:00
|
|
|
// PerfResults holds a set of “builder|benchmark” tuples denoting
|
|
|
|
// what benchmarks have been executed on the commit.
|
|
|
|
PerfResults []string `datastore:",noindex"`
|
|
|
|
|
2013-07-31 21:23:51 -06:00
|
|
|
FailNotificationSent bool
|
|
|
|
}
|
|
|
|
|
|
|
|
func (com *Commit) Key(c appengine.Context) *datastore.Key {
|
|
|
|
if com.Hash == "" {
|
|
|
|
panic("tried Key on Commit with empty Hash")
|
|
|
|
}
|
|
|
|
p := Package{Path: com.PackagePath}
|
|
|
|
key := com.PackagePath + "|" + com.Hash
|
|
|
|
return datastore.NewKey(c, "Commit", key, 0, p.Key(c))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Commit) Valid() error {
|
|
|
|
if !validHash(c.Hash) {
|
|
|
|
return errors.New("invalid Hash")
|
|
|
|
}
|
|
|
|
if c.ParentHash != "" && !validHash(c.ParentHash) { // empty is OK
|
|
|
|
return errors.New("invalid ParentHash")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// each result line is approx 105 bytes. This constant is a tradeoff between
|
|
|
|
// build history and the AppEngine datastore limit of 1mb.
|
|
|
|
const maxResults = 1000
|
|
|
|
|
|
|
|
// AddResult adds the denormalized Result data to the Commit's Result field.
|
|
|
|
// It must be called from inside a datastore transaction.
|
|
|
|
func (com *Commit) AddResult(c appengine.Context, r *Result) error {
|
|
|
|
if err := datastore.Get(c, com.Key(c), com); err != nil {
|
|
|
|
return fmt.Errorf("getting Commit: %v", err)
|
|
|
|
}
|
2014-05-19 10:51:04 -06:00
|
|
|
|
|
|
|
var resultExists bool
|
|
|
|
for i, s := range com.ResultData {
|
|
|
|
// if there already exists result data for this builder at com, overwrite it.
|
|
|
|
if strings.Contains(s, r.Builder) {
|
|
|
|
resultExists = true
|
|
|
|
com.ResultData[i] = r.Data()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !resultExists {
|
|
|
|
// otherwise, add the new result data for this builder.
|
|
|
|
com.ResultData = trim(append(com.ResultData, r.Data()), maxResults)
|
|
|
|
}
|
2013-07-31 21:23:51 -06:00
|
|
|
if _, err := datastore.Put(c, com.Key(c), com); err != nil {
|
|
|
|
return fmt.Errorf("putting Commit: %v", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2014-05-13 01:00:32 -06:00
|
|
|
// AddPerfResult remembers that the builder has run the benchmark on the commit.
|
|
|
|
// It must be called from inside a datastore transaction.
|
|
|
|
func (com *Commit) AddPerfResult(c appengine.Context, builder, benchmark string) error {
|
|
|
|
if err := datastore.Get(c, com.Key(c), com); err != nil {
|
|
|
|
return fmt.Errorf("getting Commit: %v", err)
|
|
|
|
}
|
|
|
|
if !com.NeedsBenchmarking {
|
|
|
|
return fmt.Errorf("trying to add perf result to Commit(%v) that does not require benchmarking", com.Hash)
|
|
|
|
}
|
|
|
|
s := builder + "|" + benchmark
|
|
|
|
for _, v := range com.PerfResults {
|
|
|
|
if v == s {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
com.PerfResults = append(com.PerfResults, s)
|
|
|
|
if _, err := datastore.Put(c, com.Key(c), com); err != nil {
|
|
|
|
return fmt.Errorf("putting Commit: %v", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2013-07-31 21:23:51 -06:00
|
|
|
func trim(s []string, n int) []string {
|
|
|
|
l := min(len(s), n)
|
|
|
|
return s[len(s)-l:]
|
|
|
|
}
|
|
|
|
|
|
|
|
func min(a, b int) int {
|
|
|
|
if a < b {
|
|
|
|
return a
|
|
|
|
}
|
|
|
|
return b
|
|
|
|
}
|
|
|
|
|
|
|
|
// Result returns the build Result for this Commit for the given builder/goHash.
|
|
|
|
func (c *Commit) Result(builder, goHash string) *Result {
|
|
|
|
for _, r := range c.ResultData {
|
|
|
|
p := strings.SplitN(r, "|", 4)
|
|
|
|
if len(p) != 4 || p[0] != builder || p[3] != goHash {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
return partsToHash(c, p)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2014-01-27 20:30:48 -07:00
|
|
|
// Results returns the build Results for this Commit.
|
|
|
|
func (c *Commit) Results() (results []*Result) {
|
2013-07-31 21:23:51 -06:00
|
|
|
for _, r := range c.ResultData {
|
|
|
|
p := strings.SplitN(r, "|", 4)
|
2014-01-27 20:30:48 -07:00
|
|
|
if len(p) != 4 {
|
2013-07-31 21:23:51 -06:00
|
|
|
continue
|
|
|
|
}
|
|
|
|
results = append(results, partsToHash(c, p))
|
|
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2014-01-27 20:30:48 -07:00
|
|
|
func (c *Commit) ResultGoHashes() []string {
|
|
|
|
var hashes []string
|
|
|
|
for _, r := range c.ResultData {
|
|
|
|
p := strings.SplitN(r, "|", 4)
|
|
|
|
if len(p) != 4 {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
// Append only new results (use linear scan to preserve order).
|
|
|
|
if !contains(hashes, p[3]) {
|
|
|
|
hashes = append(hashes, p[3])
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Return results in reverse order (newest first).
|
|
|
|
reverse(hashes)
|
|
|
|
return hashes
|
|
|
|
}
|
|
|
|
|
|
|
|
func contains(t []string, s string) bool {
|
|
|
|
for _, s2 := range t {
|
|
|
|
if s2 == s {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
func reverse(s []string) {
|
|
|
|
for i := 0; i < len(s)/2; i++ {
|
|
|
|
j := len(s) - i - 1
|
|
|
|
s[i], s[j] = s[j], s[i]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2014-05-13 01:00:32 -06:00
|
|
|
// A CommitRun provides summary information for commits [StartCommitNum, StartCommitNum + PerfRunLength).
|
|
|
|
// Descendant of Package.
|
|
|
|
type CommitRun struct {
|
|
|
|
PackagePath string // (empty for main repo commits)
|
|
|
|
StartCommitNum int
|
|
|
|
Hash []string `datastore:",noindex"`
|
|
|
|
User []string `datastore:",noindex"`
|
|
|
|
Desc []string `datastore:",noindex"` // Only first line.
|
|
|
|
Time []time.Time `datastore:",noindex"`
|
|
|
|
NeedsBenchmarking []bool `datastore:",noindex"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (cr *CommitRun) Key(c appengine.Context) *datastore.Key {
|
|
|
|
p := Package{Path: cr.PackagePath}
|
|
|
|
key := strconv.Itoa(cr.StartCommitNum)
|
|
|
|
return datastore.NewKey(c, "CommitRun", key, 0, p.Key(c))
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetCommitRun loads and returns CommitRun that contains information
|
|
|
|
// for commit commitNum.
|
|
|
|
func GetCommitRun(c appengine.Context, commitNum int) (*CommitRun, error) {
|
|
|
|
cr := &CommitRun{StartCommitNum: commitNum / PerfRunLength * PerfRunLength}
|
|
|
|
err := datastore.Get(c, cr.Key(c), cr)
|
|
|
|
if err != nil && err != datastore.ErrNoSuchEntity {
|
|
|
|
return nil, fmt.Errorf("getting CommitRun: %v", err)
|
|
|
|
}
|
|
|
|
if len(cr.Hash) != PerfRunLength {
|
|
|
|
cr.Hash = make([]string, PerfRunLength)
|
|
|
|
cr.User = make([]string, PerfRunLength)
|
|
|
|
cr.Desc = make([]string, PerfRunLength)
|
|
|
|
cr.Time = make([]time.Time, PerfRunLength)
|
|
|
|
cr.NeedsBenchmarking = make([]bool, PerfRunLength)
|
|
|
|
}
|
|
|
|
return cr, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (cr *CommitRun) AddCommit(c appengine.Context, com *Commit) error {
|
|
|
|
if com.Num < cr.StartCommitNum || com.Num >= cr.StartCommitNum+PerfRunLength {
|
|
|
|
return fmt.Errorf("AddCommit: commit num %v out of range [%v, %v)",
|
|
|
|
com.Num, cr.StartCommitNum, cr.StartCommitNum+PerfRunLength)
|
|
|
|
}
|
|
|
|
i := com.Num - cr.StartCommitNum
|
|
|
|
// Be careful with string lengths,
|
|
|
|
// we need to fit 1024 commits into 1 MB.
|
|
|
|
cr.Hash[i] = com.Hash
|
|
|
|
cr.User[i] = shortDesc(com.User)
|
|
|
|
cr.Desc[i] = shortDesc(com.Desc)
|
|
|
|
cr.Time[i] = com.Time
|
|
|
|
cr.NeedsBenchmarking[i] = com.NeedsBenchmarking
|
|
|
|
if _, err := datastore.Put(c, cr.Key(c), cr); err != nil {
|
|
|
|
return fmt.Errorf("putting CommitRun: %v", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetCommits returns [startCommitNum, startCommitNum+n) commits.
|
|
|
|
// Commits information is partial (obtained from CommitRun),
|
|
|
|
// do not store them back into datastore.
|
|
|
|
func GetCommits(c appengine.Context, startCommitNum, n int) ([]*Commit, error) {
|
|
|
|
if startCommitNum < 0 || n <= 0 {
|
|
|
|
return nil, fmt.Errorf("GetCommits: invalid args (%v, %v)", startCommitNum, n)
|
|
|
|
}
|
|
|
|
var res []*Commit
|
|
|
|
for n > 0 {
|
|
|
|
cr, err := GetCommitRun(c, startCommitNum)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
idx := startCommitNum - cr.StartCommitNum
|
|
|
|
cnt := PerfRunLength - idx
|
|
|
|
if cnt > n {
|
|
|
|
cnt = n
|
|
|
|
}
|
|
|
|
for i := idx; i < idx+cnt; i++ {
|
|
|
|
com := new(Commit)
|
|
|
|
com.Hash = cr.Hash[i]
|
|
|
|
com.User = cr.User[i]
|
|
|
|
com.Desc = cr.Desc[i]
|
|
|
|
com.Time = cr.Time[i]
|
|
|
|
com.NeedsBenchmarking = cr.NeedsBenchmarking[i]
|
|
|
|
res = append(res, com)
|
|
|
|
}
|
|
|
|
startCommitNum += cnt
|
|
|
|
n -= cnt
|
|
|
|
}
|
|
|
|
return res, nil
|
|
|
|
}
|
|
|
|
|
2013-07-31 21:23:51 -06:00
|
|
|
// partsToHash converts a Commit and ResultData substrings to a Result.
|
|
|
|
func partsToHash(c *Commit, p []string) *Result {
|
|
|
|
return &Result{
|
|
|
|
Builder: p[0],
|
|
|
|
Hash: c.Hash,
|
|
|
|
PackagePath: c.PackagePath,
|
|
|
|
GoHash: p[3],
|
|
|
|
OK: p[1] == "true",
|
|
|
|
LogHash: p[2],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// A Result describes a build result for a Commit on an OS/architecture.
|
|
|
|
//
|
2013-11-24 23:15:56 -07:00
|
|
|
// Each Result entity is a descendant of its associated Package entity.
|
2013-07-31 21:23:51 -06:00
|
|
|
type Result struct {
|
2014-05-13 01:00:32 -06:00
|
|
|
PackagePath string // (empty for Go commits)
|
2013-07-31 21:23:51 -06:00
|
|
|
Builder string // "os-arch[-note]"
|
|
|
|
Hash string
|
|
|
|
|
|
|
|
// The Go Commit this was built against (empty for Go commits).
|
|
|
|
GoHash string
|
|
|
|
|
|
|
|
OK bool
|
|
|
|
Log string `datastore:"-"` // for JSON unmarshaling only
|
|
|
|
LogHash string `datastore:",noindex"` // Key to the Log record.
|
|
|
|
|
|
|
|
RunTime int64 // time to build+test in nanoseconds
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *Result) Key(c appengine.Context) *datastore.Key {
|
|
|
|
p := Package{Path: r.PackagePath}
|
|
|
|
key := r.Builder + "|" + r.PackagePath + "|" + r.Hash + "|" + r.GoHash
|
|
|
|
return datastore.NewKey(c, "Result", key, 0, p.Key(c))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *Result) Valid() error {
|
|
|
|
if !validHash(r.Hash) {
|
|
|
|
return errors.New("invalid Hash")
|
|
|
|
}
|
|
|
|
if r.PackagePath != "" && !validHash(r.GoHash) {
|
|
|
|
return errors.New("invalid GoHash")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Data returns the Result in string format
|
|
|
|
// to be stored in Commit's ResultData field.
|
|
|
|
func (r *Result) Data() string {
|
|
|
|
return fmt.Sprintf("%v|%v|%v|%v", r.Builder, r.OK, r.LogHash, r.GoHash)
|
|
|
|
}
|
|
|
|
|
2014-05-13 01:00:32 -06:00
|
|
|
// A PerfResult describes all benchmarking result for a Commit.
|
|
|
|
// Descendant of Package.
|
|
|
|
type PerfResult struct {
|
|
|
|
PackagePath string
|
|
|
|
CommitHash string
|
|
|
|
CommitNum int
|
|
|
|
Data []string `datastore:",noindex"` // "builder|benchmark|ok|metric1=val1|metric2=val2|file:log=hash|file:cpuprof=hash"
|
|
|
|
|
|
|
|
// Local cache with parsed Data.
|
|
|
|
// Maps builder->benchmark->ParsedPerfResult.
|
|
|
|
parsedData map[string]map[string]*ParsedPerfResult
|
|
|
|
}
|
|
|
|
|
|
|
|
type ParsedPerfResult struct {
|
|
|
|
OK bool
|
|
|
|
Metrics map[string]uint64
|
|
|
|
Artifacts map[string]string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *PerfResult) Key(c appengine.Context) *datastore.Key {
|
|
|
|
p := Package{Path: r.PackagePath}
|
|
|
|
key := r.CommitHash
|
|
|
|
return datastore.NewKey(c, "PerfResult", key, 0, p.Key(c))
|
|
|
|
}
|
|
|
|
|
|
|
|
// AddResult add the benchmarking result to r.
|
|
|
|
// Existing result for the same builder/benchmark is replaced if already exists.
|
|
|
|
// Returns whether the result was already present.
|
|
|
|
func (r *PerfResult) AddResult(req *PerfRequest) bool {
|
|
|
|
present := false
|
|
|
|
str := fmt.Sprintf("%v|%v|", req.Builder, req.Benchmark)
|
|
|
|
for i, s := range r.Data {
|
|
|
|
if strings.HasPrefix(s, str) {
|
|
|
|
present = true
|
|
|
|
last := len(r.Data) - 1
|
|
|
|
r.Data[i] = r.Data[last]
|
|
|
|
r.Data = r.Data[:last]
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ok := "ok"
|
|
|
|
if !req.OK {
|
|
|
|
ok = "false"
|
|
|
|
}
|
|
|
|
str += ok
|
|
|
|
for _, m := range req.Metrics {
|
|
|
|
str += fmt.Sprintf("|%v=%v", m.Type, m.Val)
|
|
|
|
}
|
|
|
|
for _, a := range req.Artifacts {
|
|
|
|
str += fmt.Sprintf("|file:%v=%v", a.Type, a.Body)
|
|
|
|
}
|
|
|
|
r.Data = append(r.Data, str)
|
|
|
|
r.parsedData = nil
|
|
|
|
return present
|
|
|
|
}
|
|
|
|
|
|
|
|
func (r *PerfResult) ParseData() map[string]map[string]*ParsedPerfResult {
|
|
|
|
if r.parsedData != nil {
|
|
|
|
return r.parsedData
|
|
|
|
}
|
|
|
|
res := make(map[string]map[string]*ParsedPerfResult)
|
|
|
|
for _, str := range r.Data {
|
|
|
|
ss := strings.Split(str, "|")
|
|
|
|
builder := ss[0]
|
|
|
|
bench := ss[1]
|
|
|
|
ok := ss[2]
|
|
|
|
m := res[builder]
|
|
|
|
if m == nil {
|
|
|
|
m = make(map[string]*ParsedPerfResult)
|
|
|
|
res[builder] = m
|
|
|
|
}
|
|
|
|
var p ParsedPerfResult
|
|
|
|
p.OK = ok == "ok"
|
|
|
|
p.Metrics = make(map[string]uint64)
|
|
|
|
p.Artifacts = make(map[string]string)
|
|
|
|
for _, entry := range ss[3:] {
|
|
|
|
if strings.HasPrefix(entry, "file:") {
|
|
|
|
ss1 := strings.Split(entry[len("file:"):], "=")
|
|
|
|
p.Artifacts[ss1[0]] = ss1[1]
|
|
|
|
} else {
|
|
|
|
ss1 := strings.Split(entry, "=")
|
|
|
|
val, _ := strconv.ParseUint(ss1[1], 10, 64)
|
|
|
|
p.Metrics[ss1[0]] = val
|
|
|
|
}
|
|
|
|
}
|
|
|
|
m[bench] = &p
|
|
|
|
}
|
|
|
|
r.parsedData = res
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
// A PerfMetricRun entity holds a set of metric values for builder/benchmark/metric
|
|
|
|
// for commits [StartCommitNum, StartCommitNum + PerfRunLength).
|
|
|
|
// Descendant of Package.
|
|
|
|
type PerfMetricRun struct {
|
|
|
|
PackagePath string
|
|
|
|
Builder string
|
|
|
|
Benchmark string
|
|
|
|
Metric string // e.g. realtime, cputime, gc-pause
|
|
|
|
StartCommitNum int
|
|
|
|
Vals []int64 `datastore:",noindex"`
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *PerfMetricRun) Key(c appengine.Context) *datastore.Key {
|
|
|
|
p := Package{Path: m.PackagePath}
|
|
|
|
key := m.Builder + "|" + m.Benchmark + "|" + m.Metric + "|" + strconv.Itoa(m.StartCommitNum)
|
|
|
|
return datastore.NewKey(c, "PerfMetricRun", key, 0, p.Key(c))
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetPerfMetricRun loads and returns PerfMetricRun that contains information
|
|
|
|
// for commit commitNum.
|
|
|
|
func GetPerfMetricRun(c appengine.Context, builder, benchmark, metric string, commitNum int) (*PerfMetricRun, error) {
|
|
|
|
startCommitNum := commitNum / PerfRunLength * PerfRunLength
|
|
|
|
m := &PerfMetricRun{Builder: builder, Benchmark: benchmark, Metric: metric, StartCommitNum: startCommitNum}
|
|
|
|
err := datastore.Get(c, m.Key(c), m)
|
|
|
|
if err != nil && err != datastore.ErrNoSuchEntity {
|
|
|
|
return nil, fmt.Errorf("getting PerfMetricRun: %v", err)
|
|
|
|
}
|
|
|
|
if len(m.Vals) != PerfRunLength {
|
|
|
|
m.Vals = make([]int64, PerfRunLength)
|
|
|
|
}
|
|
|
|
return m, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (m *PerfMetricRun) AddMetric(c appengine.Context, commitNum int, v uint64) error {
|
|
|
|
if commitNum < m.StartCommitNum || commitNum >= m.StartCommitNum+PerfRunLength {
|
|
|
|
return fmt.Errorf("AddMetric: CommitNum %v out of range [%v, %v)",
|
|
|
|
commitNum, m.StartCommitNum, m.StartCommitNum+PerfRunLength)
|
|
|
|
}
|
|
|
|
m.Vals[commitNum-m.StartCommitNum] = int64(v)
|
|
|
|
if _, err := datastore.Put(c, m.Key(c), m); err != nil {
|
|
|
|
return fmt.Errorf("putting PerfMetricRun: %v", err)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetPerfMetricsForCommits returns perf metrics for builder/benchmark/metric
|
|
|
|
// and commits [startCommitNum, startCommitNum+n).
|
|
|
|
func GetPerfMetricsForCommits(c appengine.Context, builder, benchmark, metric string, startCommitNum, n int) ([]uint64, error) {
|
|
|
|
if startCommitNum < 0 || n <= 0 {
|
|
|
|
return nil, fmt.Errorf("GetPerfMetricsForCommits: invalid args (%v, %v)", startCommitNum, n)
|
|
|
|
}
|
|
|
|
var res []uint64
|
|
|
|
for n > 0 {
|
|
|
|
metrics, err := GetPerfMetricRun(c, builder, benchmark, metric, startCommitNum)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
idx := startCommitNum - metrics.StartCommitNum
|
|
|
|
cnt := PerfRunLength - idx
|
|
|
|
if cnt > n {
|
|
|
|
cnt = n
|
|
|
|
}
|
|
|
|
for _, v := range metrics.Vals[idx : idx+cnt] {
|
|
|
|
res = append(res, uint64(v))
|
|
|
|
}
|
|
|
|
startCommitNum += cnt
|
|
|
|
n -= cnt
|
|
|
|
}
|
|
|
|
return res, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// PerfConfig holds read-mostly configuration related to benchmarking.
|
|
|
|
// There is only one PerfConfig entity.
|
|
|
|
type PerfConfig struct {
|
|
|
|
BuilderBench []string `datastore:",noindex"` // "builder|benchmark" pairs
|
|
|
|
BuilderProcs []string `datastore:",noindex"` // "builder|proc" pairs
|
|
|
|
BenchMetric []string `datastore:",noindex"` // "benchmark|metric" pairs
|
|
|
|
NoiseLevels []string `datastore:",noindex"` // "builder|benchmark|metric1=noise1|metric2=noise2"
|
|
|
|
|
|
|
|
// Local cache of "builder|benchmark|metric" -> noise.
|
|
|
|
noise map[string]float64
|
|
|
|
}
|
|
|
|
|
|
|
|
func PerfConfigKey(c appengine.Context) *datastore.Key {
|
|
|
|
p := Package{}
|
|
|
|
return datastore.NewKey(c, "PerfConfig", "PerfConfig", 0, p.Key(c))
|
|
|
|
}
|
|
|
|
|
|
|
|
const perfConfigCacheKey = "perf-config"
|
|
|
|
|
|
|
|
func GetPerfConfig(c appengine.Context, r *http.Request) (*PerfConfig, error) {
|
|
|
|
pc := new(PerfConfig)
|
|
|
|
now := cache.Now(c)
|
|
|
|
if cache.Get(r, now, perfConfigCacheKey, pc) {
|
|
|
|
return pc, nil
|
|
|
|
}
|
|
|
|
err := datastore.Get(c, PerfConfigKey(c), pc)
|
|
|
|
if err != nil && err != datastore.ErrNoSuchEntity {
|
|
|
|
return nil, fmt.Errorf("GetPerfConfig: %v", err)
|
|
|
|
}
|
|
|
|
cache.Set(r, now, perfConfigCacheKey, pc)
|
|
|
|
return pc, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (pc *PerfConfig) NoiseLevel(builder, benchmark, metric string) float64 {
|
|
|
|
if pc.noise == nil {
|
|
|
|
pc.noise = make(map[string]float64)
|
|
|
|
for _, str := range pc.NoiseLevels {
|
|
|
|
split := strings.Split(str, "|")
|
|
|
|
builderBench := split[0] + "|" + split[1]
|
|
|
|
for _, entry := range split[2:] {
|
|
|
|
metricValue := strings.Split(entry, "=")
|
|
|
|
noise, _ := strconv.ParseFloat(metricValue[1], 64)
|
|
|
|
pc.noise[builderBench+"|"+metricValue[0]] = noise
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
me := fmt.Sprintf("%v|%v|%v", builder, benchmark, metric)
|
|
|
|
n := pc.noise[me]
|
|
|
|
if n == 0 {
|
|
|
|
// Use a very conservative value
|
|
|
|
// until we have learned the real noise level.
|
|
|
|
n = 200
|
|
|
|
}
|
|
|
|
return n
|
|
|
|
}
|
|
|
|
|
|
|
|
// UpdatePerfConfig updates the PerfConfig entity with results of benchmarking.
|
|
|
|
// Returns whether it's a benchmark that we have not yet seem on the builder.
|
|
|
|
func UpdatePerfConfig(c appengine.Context, r *http.Request, req *PerfRequest) (newBenchmark bool, err error) {
|
|
|
|
pc, err := GetPerfConfig(c, r)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
modified := false
|
|
|
|
add := func(arr *[]string, str string) {
|
|
|
|
for _, s := range *arr {
|
|
|
|
if s == str {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
*arr = append(*arr, str)
|
|
|
|
modified = true
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
BenchProcs := strings.Split(req.Benchmark, "-")
|
|
|
|
benchmark := BenchProcs[0]
|
|
|
|
procs := "1"
|
|
|
|
if len(BenchProcs) > 1 {
|
|
|
|
procs = BenchProcs[1]
|
|
|
|
}
|
|
|
|
|
|
|
|
add(&pc.BuilderBench, req.Builder+"|"+benchmark)
|
|
|
|
newBenchmark = modified
|
|
|
|
add(&pc.BuilderProcs, req.Builder+"|"+procs)
|
|
|
|
for _, m := range req.Metrics {
|
|
|
|
add(&pc.BenchMetric, benchmark+"|"+m.Type)
|
|
|
|
}
|
|
|
|
|
|
|
|
if modified {
|
|
|
|
if _, err := datastore.Put(c, PerfConfigKey(c), pc); err != nil {
|
|
|
|
return false, fmt.Errorf("putting PerfConfig: %v", err)
|
|
|
|
}
|
|
|
|
cache.Tick(c)
|
|
|
|
}
|
|
|
|
return newBenchmark, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func collectList(all []string, idx int, second string) (res []string) {
|
|
|
|
m := make(map[string]bool)
|
|
|
|
for _, str := range all {
|
|
|
|
ss := strings.Split(str, "|")
|
|
|
|
v := ss[idx]
|
|
|
|
v2 := ss[1-idx]
|
|
|
|
if (second == "" || second == v2) && !m[v] {
|
|
|
|
m[v] = true
|
|
|
|
res = append(res, v)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
sort.Strings(res)
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
func (pc *PerfConfig) BuildersForBenchmark(bench string) []string {
|
|
|
|
return collectList(pc.BuilderBench, 0, bench)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (pc *PerfConfig) BenchmarksForBuilder(builder string) []string {
|
|
|
|
return collectList(pc.BuilderBench, 1, builder)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (pc *PerfConfig) MetricsForBenchmark(bench string) []string {
|
|
|
|
return collectList(pc.BenchMetric, 1, bench)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (pc *PerfConfig) BenchmarkProcList() (res []string) {
|
|
|
|
bl := pc.BenchmarksForBuilder("")
|
|
|
|
pl := pc.ProcList("")
|
|
|
|
for _, b := range bl {
|
|
|
|
for _, p := range pl {
|
|
|
|
res = append(res, fmt.Sprintf("%v-%v", b, p))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return res
|
|
|
|
}
|
|
|
|
|
|
|
|
func (pc *PerfConfig) ProcList(builder string) []int {
|
|
|
|
ss := collectList(pc.BuilderProcs, 1, builder)
|
|
|
|
var procs []int
|
|
|
|
for _, s := range ss {
|
|
|
|
p, _ := strconv.ParseInt(s, 10, 32)
|
|
|
|
procs = append(procs, int(p))
|
|
|
|
}
|
|
|
|
sort.Ints(procs)
|
|
|
|
return procs
|
|
|
|
}
|
|
|
|
|
|
|
|
// A PerfTodo contains outstanding commits for benchmarking for a builder.
|
|
|
|
// Descendant of Package.
|
|
|
|
type PerfTodo struct {
|
|
|
|
PackagePath string // (empty for main repo commits)
|
|
|
|
Builder string
|
|
|
|
CommitNums []int `datastore:",noindex"` // LIFO queue of commits to benchmark.
|
|
|
|
}
|
|
|
|
|
|
|
|
func (todo *PerfTodo) Key(c appengine.Context) *datastore.Key {
|
|
|
|
p := Package{Path: todo.PackagePath}
|
|
|
|
key := todo.Builder
|
|
|
|
return datastore.NewKey(c, "PerfTodo", key, 0, p.Key(c))
|
|
|
|
}
|
|
|
|
|
|
|
|
// AddCommitToPerfTodo adds the commit to all existing PerfTodo entities.
|
|
|
|
func AddCommitToPerfTodo(c appengine.Context, com *Commit) error {
|
|
|
|
var todos []*PerfTodo
|
|
|
|
_, err := datastore.NewQuery("PerfTodo").
|
|
|
|
Ancestor((&Package{}).Key(c)).
|
|
|
|
GetAll(c, &todos)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("fetching PerfTodo's: %v", err)
|
|
|
|
}
|
|
|
|
for _, todo := range todos {
|
|
|
|
todo.CommitNums = append(todo.CommitNums, com.Num)
|
|
|
|
_, err = datastore.Put(c, todo.Key(c), todo)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("updating PerfTodo: %v", err)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2013-07-31 21:23:51 -06:00
|
|
|
// A Log is a gzip-compressed log file stored under the SHA1 hash of the
|
|
|
|
// uncompressed log text.
|
|
|
|
type Log struct {
|
|
|
|
CompressedLog []byte
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l *Log) Text() ([]byte, error) {
|
|
|
|
d, err := gzip.NewReader(bytes.NewBuffer(l.CompressedLog))
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("reading log data: %v", err)
|
|
|
|
}
|
|
|
|
b, err := ioutil.ReadAll(d)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("reading log data: %v", err)
|
|
|
|
}
|
|
|
|
return b, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func PutLog(c appengine.Context, text string) (hash string, err error) {
|
|
|
|
h := sha1.New()
|
|
|
|
io.WriteString(h, text)
|
|
|
|
b := new(bytes.Buffer)
|
|
|
|
z, _ := gzip.NewWriterLevel(b, gzip.BestCompression)
|
|
|
|
io.WriteString(z, text)
|
|
|
|
z.Close()
|
|
|
|
hash = fmt.Sprintf("%x", h.Sum(nil))
|
|
|
|
key := datastore.NewKey(c, "Log", hash, 0, nil)
|
|
|
|
_, err = datastore.Put(c, key, &Log{b.Bytes()})
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
// A Tag is used to keep track of the most recent Go weekly and release tags.
|
|
|
|
// Typically there will be one Tag entity for each kind of hg tag.
|
|
|
|
type Tag struct {
|
|
|
|
Kind string // "weekly", "release", or "tip"
|
|
|
|
Name string // the tag itself (for example: "release.r60")
|
|
|
|
Hash string
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *Tag) Key(c appengine.Context) *datastore.Key {
|
|
|
|
p := &Package{}
|
|
|
|
return datastore.NewKey(c, "Tag", t.Kind, 0, p.Key(c))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *Tag) Valid() error {
|
|
|
|
if t.Kind != "weekly" && t.Kind != "release" && t.Kind != "tip" {
|
|
|
|
return errors.New("invalid Kind")
|
|
|
|
}
|
|
|
|
if !validHash(t.Hash) {
|
|
|
|
return errors.New("invalid Hash")
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Commit returns the Commit that corresponds with this Tag.
|
|
|
|
func (t *Tag) Commit(c appengine.Context) (*Commit, error) {
|
|
|
|
com := &Commit{Hash: t.Hash}
|
|
|
|
err := datastore.Get(c, com.Key(c), com)
|
|
|
|
return com, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetTag fetches a Tag by name from the datastore.
|
|
|
|
func GetTag(c appengine.Context, tag string) (*Tag, error) {
|
|
|
|
t := &Tag{Kind: tag}
|
|
|
|
if err := datastore.Get(c, t.Key(c), t); err != nil {
|
|
|
|
if err == datastore.ErrNoSuchEntity {
|
|
|
|
return nil, errors.New("tag not found: " + tag)
|
|
|
|
}
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err := t.Valid(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return t, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// Packages returns packages of the specified kind.
|
|
|
|
// Kind must be one of "external" or "subrepo".
|
|
|
|
func Packages(c appengine.Context, kind string) ([]*Package, error) {
|
|
|
|
switch kind {
|
|
|
|
case "external", "subrepo":
|
|
|
|
default:
|
|
|
|
return nil, errors.New(`kind must be one of "external" or "subrepo"`)
|
|
|
|
}
|
|
|
|
var pkgs []*Package
|
|
|
|
q := datastore.NewQuery("Package").Filter("Kind=", kind)
|
|
|
|
for t := q.Run(c); ; {
|
|
|
|
pkg := new(Package)
|
|
|
|
_, err := t.Next(pkg)
|
|
|
|
if _, ok := err.(*datastore.ErrFieldMismatch); ok {
|
|
|
|
// Some fields have been removed, so it's okay to ignore this error.
|
|
|
|
err = nil
|
|
|
|
}
|
|
|
|
if err == datastore.Done {
|
|
|
|
break
|
|
|
|
} else if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if pkg.Path != "" {
|
|
|
|
pkgs = append(pkgs, pkg)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return pkgs, nil
|
|
|
|
}
|