github.com/graybobo/golang.org-package-offline-cache@v0.0.0-20200626051047-6608995c132f/x/tools/dashboard/app/build/build.go (about)

     1  // Copyright 2011 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // +build appengine
     6  
     7  package build
     8  
     9  import (
    10  	"bytes"
    11  	"compress/gzip"
    12  	"crypto/sha1"
    13  	"errors"
    14  	"fmt"
    15  	"io"
    16  	"io/ioutil"
    17  	"net/http"
    18  	"sort"
    19  	"strconv"
    20  	"strings"
    21  	"time"
    22  
    23  	"appengine"
    24  	"appengine/datastore"
    25  
    26  	"cache"
    27  )
    28  
    29  const (
    30  	maxDatastoreStringLen = 500
    31  	PerfRunLength         = 1024
    32  )
    33  
    34  // A Package describes a package that is listed on the dashboard.
    35  type Package struct {
    36  	Kind    string // "subrepo", "external", or empty for the main Go tree
    37  	Name    string
    38  	Path    string // (empty for the main Go tree)
    39  	NextNum int    // Num of the next head Commit
    40  }
    41  
    42  func (p *Package) String() string {
    43  	return fmt.Sprintf("%s: %q", p.Path, p.Name)
    44  }
    45  
    46  func (p *Package) Key(c appengine.Context) *datastore.Key {
    47  	key := p.Path
    48  	if key == "" {
    49  		key = "go"
    50  	}
    51  	return datastore.NewKey(c, "Package", key, 0, nil)
    52  }
    53  
    54  // LastCommit returns the most recent Commit for this Package.
    55  func (p *Package) LastCommit(c appengine.Context) (*Commit, error) {
    56  	var commits []*Commit
    57  	_, err := datastore.NewQuery("Commit").
    58  		Ancestor(p.Key(c)).
    59  		Order("-Time").
    60  		Limit(1).
    61  		GetAll(c, &commits)
    62  	if err != nil {
    63  		return nil, err
    64  	}
    65  	if len(commits) != 1 {
    66  		return nil, datastore.ErrNoSuchEntity
    67  	}
    68  	return commits[0], nil
    69  }
    70  
    71  // GetPackage fetches a Package by path from the datastore.
    72  func GetPackage(c appengine.Context, path string) (*Package, error) {
    73  	p := &Package{Path: path}
    74  	err := datastore.Get(c, p.Key(c), p)
    75  	if err == datastore.ErrNoSuchEntity {
    76  		return nil, fmt.Errorf("package %q not found", path)
    77  	}
    78  	return p, err
    79  }
    80  
    81  // A Commit describes an individual commit in a package.
    82  //
    83  // Each Commit entity is a descendant of its associated Package entity.
    84  // In other words, all Commits with the same PackagePath belong to the same
    85  // datastore entity group.
    86  type Commit struct {
    87  	PackagePath string // (empty for main repo commits)
    88  	Hash        string
    89  	ParentHash  string
    90  	Num         int // Internal monotonic counter unique to this package.
    91  
    92  	User              string
    93  	Desc              string `datastore:",noindex"`
    94  	Time              time.Time
    95  	NeedsBenchmarking bool
    96  	TryPatch          bool
    97  	Branch            string
    98  
    99  	// ResultData is the Data string of each build Result for this Commit.
   100  	// For non-Go commits, only the Results for the current Go tip, weekly,
   101  	// and release Tags are stored here. This is purely de-normalized data.
   102  	// The complete data set is stored in Result entities.
   103  	ResultData []string `datastore:",noindex"`
   104  
   105  	// PerfResults holds a set of “builder|benchmark” tuples denoting
   106  	// what benchmarks have been executed on the commit.
   107  	PerfResults []string `datastore:",noindex"`
   108  
   109  	FailNotificationSent bool
   110  }
   111  
   112  func (com *Commit) Key(c appengine.Context) *datastore.Key {
   113  	if com.Hash == "" {
   114  		panic("tried Key on Commit with empty Hash")
   115  	}
   116  	p := Package{Path: com.PackagePath}
   117  	key := com.PackagePath + "|" + com.Hash
   118  	return datastore.NewKey(c, "Commit", key, 0, p.Key(c))
   119  }
   120  
   121  func (c *Commit) Valid() error {
   122  	if !validHash(c.Hash) {
   123  		return errors.New("invalid Hash")
   124  	}
   125  	if c.ParentHash != "" && !validHash(c.ParentHash) { // empty is OK
   126  		return errors.New("invalid ParentHash")
   127  	}
   128  	return nil
   129  }
   130  
   131  func putCommit(c appengine.Context, com *Commit) error {
   132  	if err := com.Valid(); err != nil {
   133  		return fmt.Errorf("putting Commit: %v", err)
   134  	}
   135  	if com.Num == 0 && com.ParentHash != "0000" { // 0000 is used in tests
   136  		return fmt.Errorf("putting Commit: invalid Num (must be > 0)")
   137  	}
   138  	if _, err := datastore.Put(c, com.Key(c), com); err != nil {
   139  		return fmt.Errorf("putting Commit: %v", err)
   140  	}
   141  	return nil
   142  }
   143  
   144  // each result line is approx 105 bytes. This constant is a tradeoff between
   145  // build history and the AppEngine datastore limit of 1mb.
   146  const maxResults = 1000
   147  
   148  // AddResult adds the denormalized Result data to the Commit's Result field.
   149  // It must be called from inside a datastore transaction.
   150  func (com *Commit) AddResult(c appengine.Context, r *Result) error {
   151  	if err := datastore.Get(c, com.Key(c), com); err != nil {
   152  		return fmt.Errorf("getting Commit: %v", err)
   153  	}
   154  
   155  	var resultExists bool
   156  	for i, s := range com.ResultData {
   157  		// if there already exists result data for this builder at com, overwrite it.
   158  		if strings.HasPrefix(s, r.Builder+"|") && strings.HasSuffix(s, "|"+r.GoHash) {
   159  			resultExists = true
   160  			com.ResultData[i] = r.Data()
   161  		}
   162  	}
   163  	if !resultExists {
   164  		// otherwise, add the new result data for this builder.
   165  		com.ResultData = trim(append(com.ResultData, r.Data()), maxResults)
   166  	}
   167  	return putCommit(c, com)
   168  }
   169  
   170  // AddPerfResult remembers that the builder has run the benchmark on the commit.
   171  // It must be called from inside a datastore transaction.
   172  func (com *Commit) AddPerfResult(c appengine.Context, builder, benchmark string) error {
   173  	if err := datastore.Get(c, com.Key(c), com); err != nil {
   174  		return fmt.Errorf("getting Commit: %v", err)
   175  	}
   176  	if !com.NeedsBenchmarking {
   177  		return fmt.Errorf("trying to add perf result to Commit(%v) that does not require benchmarking", com.Hash)
   178  	}
   179  	s := builder + "|" + benchmark
   180  	for _, v := range com.PerfResults {
   181  		if v == s {
   182  			return nil
   183  		}
   184  	}
   185  	com.PerfResults = append(com.PerfResults, s)
   186  	return putCommit(c, com)
   187  }
   188  
   189  func trim(s []string, n int) []string {
   190  	l := min(len(s), n)
   191  	return s[len(s)-l:]
   192  }
   193  
   194  func min(a, b int) int {
   195  	if a < b {
   196  		return a
   197  	}
   198  	return b
   199  }
   200  
   201  // Result returns the build Result for this Commit for the given builder/goHash.
   202  func (c *Commit) Result(builder, goHash string) *Result {
   203  	for _, r := range c.ResultData {
   204  		p := strings.SplitN(r, "|", 4)
   205  		if len(p) != 4 || p[0] != builder || p[3] != goHash {
   206  			continue
   207  		}
   208  		return partsToHash(c, p)
   209  	}
   210  	return nil
   211  }
   212  
   213  // Results returns the build Results for this Commit.
   214  func (c *Commit) Results() (results []*Result) {
   215  	for _, r := range c.ResultData {
   216  		p := strings.SplitN(r, "|", 4)
   217  		if len(p) != 4 {
   218  			continue
   219  		}
   220  		results = append(results, partsToHash(c, p))
   221  	}
   222  	return
   223  }
   224  
   225  func (c *Commit) ResultGoHashes() []string {
   226  	// For the main repo, just return the empty string
   227  	// (there's no corresponding main repo hash for a main repo Commit).
   228  	// This function is only really useful for sub-repos.
   229  	if c.PackagePath == "" {
   230  		return []string{""}
   231  	}
   232  	var hashes []string
   233  	for _, r := range c.ResultData {
   234  		p := strings.SplitN(r, "|", 4)
   235  		if len(p) != 4 {
   236  			continue
   237  		}
   238  		// Append only new results (use linear scan to preserve order).
   239  		if !contains(hashes, p[3]) {
   240  			hashes = append(hashes, p[3])
   241  		}
   242  	}
   243  	// Return results in reverse order (newest first).
   244  	reverse(hashes)
   245  	return hashes
   246  }
   247  
   248  func contains(t []string, s string) bool {
   249  	for _, s2 := range t {
   250  		if s2 == s {
   251  			return true
   252  		}
   253  	}
   254  	return false
   255  }
   256  
   257  func reverse(s []string) {
   258  	for i := 0; i < len(s)/2; i++ {
   259  		j := len(s) - i - 1
   260  		s[i], s[j] = s[j], s[i]
   261  	}
   262  }
   263  
   264  // A CommitRun provides summary information for commits [StartCommitNum, StartCommitNum + PerfRunLength).
   265  // Descendant of Package.
   266  type CommitRun struct {
   267  	PackagePath       string // (empty for main repo commits)
   268  	StartCommitNum    int
   269  	Hash              []string    `datastore:",noindex"`
   270  	User              []string    `datastore:",noindex"`
   271  	Desc              []string    `datastore:",noindex"` // Only first line.
   272  	Time              []time.Time `datastore:",noindex"`
   273  	NeedsBenchmarking []bool      `datastore:",noindex"`
   274  }
   275  
   276  func (cr *CommitRun) Key(c appengine.Context) *datastore.Key {
   277  	p := Package{Path: cr.PackagePath}
   278  	key := strconv.Itoa(cr.StartCommitNum)
   279  	return datastore.NewKey(c, "CommitRun", key, 0, p.Key(c))
   280  }
   281  
   282  // GetCommitRun loads and returns CommitRun that contains information
   283  // for commit commitNum.
   284  func GetCommitRun(c appengine.Context, commitNum int) (*CommitRun, error) {
   285  	cr := &CommitRun{StartCommitNum: commitNum / PerfRunLength * PerfRunLength}
   286  	err := datastore.Get(c, cr.Key(c), cr)
   287  	if err != nil && err != datastore.ErrNoSuchEntity {
   288  		return nil, fmt.Errorf("getting CommitRun: %v", err)
   289  	}
   290  	if len(cr.Hash) != PerfRunLength {
   291  		cr.Hash = make([]string, PerfRunLength)
   292  		cr.User = make([]string, PerfRunLength)
   293  		cr.Desc = make([]string, PerfRunLength)
   294  		cr.Time = make([]time.Time, PerfRunLength)
   295  		cr.NeedsBenchmarking = make([]bool, PerfRunLength)
   296  	}
   297  	return cr, nil
   298  }
   299  
   300  func (cr *CommitRun) AddCommit(c appengine.Context, com *Commit) error {
   301  	if com.Num < cr.StartCommitNum || com.Num >= cr.StartCommitNum+PerfRunLength {
   302  		return fmt.Errorf("AddCommit: commit num %v out of range [%v, %v)",
   303  			com.Num, cr.StartCommitNum, cr.StartCommitNum+PerfRunLength)
   304  	}
   305  	i := com.Num - cr.StartCommitNum
   306  	// Be careful with string lengths,
   307  	// we need to fit 1024 commits into 1 MB.
   308  	cr.Hash[i] = com.Hash
   309  	cr.User[i] = shortDesc(com.User)
   310  	cr.Desc[i] = shortDesc(com.Desc)
   311  	cr.Time[i] = com.Time
   312  	cr.NeedsBenchmarking[i] = com.NeedsBenchmarking
   313  	if _, err := datastore.Put(c, cr.Key(c), cr); err != nil {
   314  		return fmt.Errorf("putting CommitRun: %v", err)
   315  	}
   316  	return nil
   317  }
   318  
   319  // GetCommits returns [startCommitNum, startCommitNum+n) commits.
   320  // Commits information is partial (obtained from CommitRun),
   321  // do not store them back into datastore.
   322  func GetCommits(c appengine.Context, startCommitNum, n int) ([]*Commit, error) {
   323  	if startCommitNum < 0 || n <= 0 {
   324  		return nil, fmt.Errorf("GetCommits: invalid args (%v, %v)", startCommitNum, n)
   325  	}
   326  
   327  	p := &Package{}
   328  	t := datastore.NewQuery("CommitRun").
   329  		Ancestor(p.Key(c)).
   330  		Filter("StartCommitNum >=", startCommitNum/PerfRunLength*PerfRunLength).
   331  		Order("StartCommitNum").
   332  		Limit(100).
   333  		Run(c)
   334  
   335  	res := make([]*Commit, n)
   336  	for {
   337  		cr := new(CommitRun)
   338  		_, err := t.Next(cr)
   339  		if err == datastore.Done {
   340  			break
   341  		}
   342  		if err != nil {
   343  			return nil, err
   344  		}
   345  		if cr.StartCommitNum >= startCommitNum+n {
   346  			break
   347  		}
   348  		// Calculate start index for copying.
   349  		i := 0
   350  		if cr.StartCommitNum < startCommitNum {
   351  			i = startCommitNum - cr.StartCommitNum
   352  		}
   353  		// Calculate end index for copying.
   354  		e := PerfRunLength
   355  		if cr.StartCommitNum+e > startCommitNum+n {
   356  			e = startCommitNum + n - cr.StartCommitNum
   357  		}
   358  		for ; i < e; i++ {
   359  			com := new(Commit)
   360  			com.Hash = cr.Hash[i]
   361  			com.User = cr.User[i]
   362  			com.Desc = cr.Desc[i]
   363  			com.Time = cr.Time[i]
   364  			com.NeedsBenchmarking = cr.NeedsBenchmarking[i]
   365  			res[cr.StartCommitNum-startCommitNum+i] = com
   366  		}
   367  		if e != PerfRunLength {
   368  			break
   369  		}
   370  	}
   371  	return res, nil
   372  }
   373  
   374  // partsToHash converts a Commit and ResultData substrings to a Result.
   375  func partsToHash(c *Commit, p []string) *Result {
   376  	return &Result{
   377  		Builder:     p[0],
   378  		Hash:        c.Hash,
   379  		PackagePath: c.PackagePath,
   380  		GoHash:      p[3],
   381  		OK:          p[1] == "true",
   382  		LogHash:     p[2],
   383  	}
   384  }
   385  
   386  // A Result describes a build result for a Commit on an OS/architecture.
   387  //
   388  // Each Result entity is a descendant of its associated Package entity.
   389  type Result struct {
   390  	PackagePath string // (empty for Go commits)
   391  	Builder     string // "os-arch[-note]"
   392  	Hash        string
   393  
   394  	// The Go Commit this was built against (empty for Go commits).
   395  	GoHash string
   396  
   397  	OK      bool
   398  	Log     string `datastore:"-"`        // for JSON unmarshaling only
   399  	LogHash string `datastore:",noindex"` // Key to the Log record.
   400  
   401  	RunTime int64 // time to build+test in nanoseconds
   402  }
   403  
   404  func (r *Result) Key(c appengine.Context) *datastore.Key {
   405  	p := Package{Path: r.PackagePath}
   406  	key := r.Builder + "|" + r.PackagePath + "|" + r.Hash + "|" + r.GoHash
   407  	return datastore.NewKey(c, "Result", key, 0, p.Key(c))
   408  }
   409  
   410  func (r *Result) Valid() error {
   411  	if !validHash(r.Hash) {
   412  		return errors.New("invalid Hash")
   413  	}
   414  	if r.PackagePath != "" && !validHash(r.GoHash) {
   415  		return errors.New("invalid GoHash")
   416  	}
   417  	return nil
   418  }
   419  
   420  // Data returns the Result in string format
   421  // to be stored in Commit's ResultData field.
   422  func (r *Result) Data() string {
   423  	return fmt.Sprintf("%v|%v|%v|%v", r.Builder, r.OK, r.LogHash, r.GoHash)
   424  }
   425  
   426  // A PerfResult describes all benchmarking result for a Commit.
   427  // Descendant of Package.
   428  type PerfResult struct {
   429  	PackagePath string
   430  	CommitHash  string
   431  	CommitNum   int
   432  	Data        []string `datastore:",noindex"` // "builder|benchmark|ok|metric1=val1|metric2=val2|file:log=hash|file:cpuprof=hash"
   433  
   434  	// Local cache with parsed Data.
   435  	// Maps builder->benchmark->ParsedPerfResult.
   436  	parsedData map[string]map[string]*ParsedPerfResult
   437  }
   438  
   439  type ParsedPerfResult struct {
   440  	OK        bool
   441  	Metrics   map[string]uint64
   442  	Artifacts map[string]string
   443  }
   444  
   445  func (r *PerfResult) Key(c appengine.Context) *datastore.Key {
   446  	p := Package{Path: r.PackagePath}
   447  	key := r.CommitHash
   448  	return datastore.NewKey(c, "PerfResult", key, 0, p.Key(c))
   449  }
   450  
   451  // AddResult add the benchmarking result to r.
   452  // Existing result for the same builder/benchmark is replaced if already exists.
   453  // Returns whether the result was already present.
   454  func (r *PerfResult) AddResult(req *PerfRequest) bool {
   455  	present := false
   456  	str := fmt.Sprintf("%v|%v|", req.Builder, req.Benchmark)
   457  	for i, s := range r.Data {
   458  		if strings.HasPrefix(s, str) {
   459  			present = true
   460  			last := len(r.Data) - 1
   461  			r.Data[i] = r.Data[last]
   462  			r.Data = r.Data[:last]
   463  			break
   464  		}
   465  	}
   466  	ok := "ok"
   467  	if !req.OK {
   468  		ok = "false"
   469  	}
   470  	str += ok
   471  	for _, m := range req.Metrics {
   472  		str += fmt.Sprintf("|%v=%v", m.Type, m.Val)
   473  	}
   474  	for _, a := range req.Artifacts {
   475  		str += fmt.Sprintf("|file:%v=%v", a.Type, a.Body)
   476  	}
   477  	r.Data = append(r.Data, str)
   478  	r.parsedData = nil
   479  	return present
   480  }
   481  
   482  func (r *PerfResult) ParseData() map[string]map[string]*ParsedPerfResult {
   483  	if r.parsedData != nil {
   484  		return r.parsedData
   485  	}
   486  	res := make(map[string]map[string]*ParsedPerfResult)
   487  	for _, str := range r.Data {
   488  		ss := strings.Split(str, "|")
   489  		builder := ss[0]
   490  		bench := ss[1]
   491  		ok := ss[2]
   492  		m := res[builder]
   493  		if m == nil {
   494  			m = make(map[string]*ParsedPerfResult)
   495  			res[builder] = m
   496  		}
   497  		var p ParsedPerfResult
   498  		p.OK = ok == "ok"
   499  		p.Metrics = make(map[string]uint64)
   500  		p.Artifacts = make(map[string]string)
   501  		for _, entry := range ss[3:] {
   502  			if strings.HasPrefix(entry, "file:") {
   503  				ss1 := strings.Split(entry[len("file:"):], "=")
   504  				p.Artifacts[ss1[0]] = ss1[1]
   505  			} else {
   506  				ss1 := strings.Split(entry, "=")
   507  				val, _ := strconv.ParseUint(ss1[1], 10, 64)
   508  				p.Metrics[ss1[0]] = val
   509  			}
   510  		}
   511  		m[bench] = &p
   512  	}
   513  	r.parsedData = res
   514  	return res
   515  }
   516  
   517  // A PerfMetricRun entity holds a set of metric values for builder/benchmark/metric
   518  // for commits [StartCommitNum, StartCommitNum + PerfRunLength).
   519  // Descendant of Package.
   520  type PerfMetricRun struct {
   521  	PackagePath    string
   522  	Builder        string
   523  	Benchmark      string
   524  	Metric         string // e.g. realtime, cputime, gc-pause
   525  	StartCommitNum int
   526  	Vals           []int64 `datastore:",noindex"`
   527  }
   528  
   529  func (m *PerfMetricRun) Key(c appengine.Context) *datastore.Key {
   530  	p := Package{Path: m.PackagePath}
   531  	key := m.Builder + "|" + m.Benchmark + "|" + m.Metric + "|" + strconv.Itoa(m.StartCommitNum)
   532  	return datastore.NewKey(c, "PerfMetricRun", key, 0, p.Key(c))
   533  }
   534  
   535  // GetPerfMetricRun loads and returns PerfMetricRun that contains information
   536  // for commit commitNum.
   537  func GetPerfMetricRun(c appengine.Context, builder, benchmark, metric string, commitNum int) (*PerfMetricRun, error) {
   538  	startCommitNum := commitNum / PerfRunLength * PerfRunLength
   539  	m := &PerfMetricRun{Builder: builder, Benchmark: benchmark, Metric: metric, StartCommitNum: startCommitNum}
   540  	err := datastore.Get(c, m.Key(c), m)
   541  	if err != nil && err != datastore.ErrNoSuchEntity {
   542  		return nil, fmt.Errorf("getting PerfMetricRun: %v", err)
   543  	}
   544  	if len(m.Vals) != PerfRunLength {
   545  		m.Vals = make([]int64, PerfRunLength)
   546  	}
   547  	return m, nil
   548  }
   549  
   550  func (m *PerfMetricRun) AddMetric(c appengine.Context, commitNum int, v uint64) error {
   551  	if commitNum < m.StartCommitNum || commitNum >= m.StartCommitNum+PerfRunLength {
   552  		return fmt.Errorf("AddMetric: CommitNum %v out of range [%v, %v)",
   553  			commitNum, m.StartCommitNum, m.StartCommitNum+PerfRunLength)
   554  	}
   555  	m.Vals[commitNum-m.StartCommitNum] = int64(v)
   556  	if _, err := datastore.Put(c, m.Key(c), m); err != nil {
   557  		return fmt.Errorf("putting PerfMetricRun: %v", err)
   558  	}
   559  	return nil
   560  }
   561  
   562  // GetPerfMetricsForCommits returns perf metrics for builder/benchmark/metric
   563  // and commits [startCommitNum, startCommitNum+n).
   564  func GetPerfMetricsForCommits(c appengine.Context, builder, benchmark, metric string, startCommitNum, n int) ([]uint64, error) {
   565  	if startCommitNum < 0 || n <= 0 {
   566  		return nil, fmt.Errorf("GetPerfMetricsForCommits: invalid args (%v, %v)", startCommitNum, n)
   567  	}
   568  
   569  	p := &Package{}
   570  	t := datastore.NewQuery("PerfMetricRun").
   571  		Ancestor(p.Key(c)).
   572  		Filter("Builder =", builder).
   573  		Filter("Benchmark =", benchmark).
   574  		Filter("Metric =", metric).
   575  		Filter("StartCommitNum >=", startCommitNum/PerfRunLength*PerfRunLength).
   576  		Order("StartCommitNum").
   577  		Limit(100).
   578  		Run(c)
   579  
   580  	res := make([]uint64, n)
   581  	for {
   582  		metrics := new(PerfMetricRun)
   583  		_, err := t.Next(metrics)
   584  		if err == datastore.Done {
   585  			break
   586  		}
   587  		if err != nil {
   588  			return nil, err
   589  		}
   590  		if metrics.StartCommitNum >= startCommitNum+n {
   591  			break
   592  		}
   593  		// Calculate start index for copying.
   594  		i := 0
   595  		if metrics.StartCommitNum < startCommitNum {
   596  			i = startCommitNum - metrics.StartCommitNum
   597  		}
   598  		// Calculate end index for copying.
   599  		e := PerfRunLength
   600  		if metrics.StartCommitNum+e > startCommitNum+n {
   601  			e = startCommitNum + n - metrics.StartCommitNum
   602  		}
   603  		for ; i < e; i++ {
   604  			res[metrics.StartCommitNum-startCommitNum+i] = uint64(metrics.Vals[i])
   605  		}
   606  		if e != PerfRunLength {
   607  			break
   608  		}
   609  	}
   610  	return res, nil
   611  }
   612  
   613  // PerfConfig holds read-mostly configuration related to benchmarking.
   614  // There is only one PerfConfig entity.
   615  type PerfConfig struct {
   616  	BuilderBench []string `datastore:",noindex"` // "builder|benchmark" pairs
   617  	BuilderProcs []string `datastore:",noindex"` // "builder|proc" pairs
   618  	BenchMetric  []string `datastore:",noindex"` // "benchmark|metric" pairs
   619  	NoiseLevels  []string `datastore:",noindex"` // "builder|benchmark|metric1=noise1|metric2=noise2"
   620  
   621  	// Local cache of "builder|benchmark|metric" -> noise.
   622  	noise map[string]float64
   623  }
   624  
   625  func PerfConfigKey(c appengine.Context) *datastore.Key {
   626  	p := Package{}
   627  	return datastore.NewKey(c, "PerfConfig", "PerfConfig", 0, p.Key(c))
   628  }
   629  
   630  const perfConfigCacheKey = "perf-config"
   631  
   632  func GetPerfConfig(c appengine.Context, r *http.Request) (*PerfConfig, error) {
   633  	pc := new(PerfConfig)
   634  	now := cache.Now(c)
   635  	if cache.Get(r, now, perfConfigCacheKey, pc) {
   636  		return pc, nil
   637  	}
   638  	err := datastore.Get(c, PerfConfigKey(c), pc)
   639  	if err != nil && err != datastore.ErrNoSuchEntity {
   640  		return nil, fmt.Errorf("GetPerfConfig: %v", err)
   641  	}
   642  	cache.Set(r, now, perfConfigCacheKey, pc)
   643  	return pc, nil
   644  }
   645  
   646  func (pc *PerfConfig) NoiseLevel(builder, benchmark, metric string) float64 {
   647  	if pc.noise == nil {
   648  		pc.noise = make(map[string]float64)
   649  		for _, str := range pc.NoiseLevels {
   650  			split := strings.Split(str, "|")
   651  			builderBench := split[0] + "|" + split[1]
   652  			for _, entry := range split[2:] {
   653  				metricValue := strings.Split(entry, "=")
   654  				noise, _ := strconv.ParseFloat(metricValue[1], 64)
   655  				pc.noise[builderBench+"|"+metricValue[0]] = noise
   656  			}
   657  		}
   658  	}
   659  	me := fmt.Sprintf("%v|%v|%v", builder, benchmark, metric)
   660  	n := pc.noise[me]
   661  	if n == 0 {
   662  		// Use a very conservative value
   663  		// until we have learned the real noise level.
   664  		n = 200
   665  	}
   666  	return n
   667  }
   668  
   669  // UpdatePerfConfig updates the PerfConfig entity with results of benchmarking.
   670  // Returns whether it's a benchmark that we have not yet seem on the builder.
   671  func UpdatePerfConfig(c appengine.Context, r *http.Request, req *PerfRequest) (newBenchmark bool, err error) {
   672  	pc, err := GetPerfConfig(c, r)
   673  	if err != nil {
   674  		return false, err
   675  	}
   676  
   677  	modified := false
   678  	add := func(arr *[]string, str string) {
   679  		for _, s := range *arr {
   680  			if s == str {
   681  				return
   682  			}
   683  		}
   684  		*arr = append(*arr, str)
   685  		modified = true
   686  		return
   687  	}
   688  
   689  	BenchProcs := strings.Split(req.Benchmark, "-")
   690  	benchmark := BenchProcs[0]
   691  	procs := "1"
   692  	if len(BenchProcs) > 1 {
   693  		procs = BenchProcs[1]
   694  	}
   695  
   696  	add(&pc.BuilderBench, req.Builder+"|"+benchmark)
   697  	newBenchmark = modified
   698  	add(&pc.BuilderProcs, req.Builder+"|"+procs)
   699  	for _, m := range req.Metrics {
   700  		add(&pc.BenchMetric, benchmark+"|"+m.Type)
   701  	}
   702  
   703  	if modified {
   704  		if _, err := datastore.Put(c, PerfConfigKey(c), pc); err != nil {
   705  			return false, fmt.Errorf("putting PerfConfig: %v", err)
   706  		}
   707  		cache.Tick(c)
   708  	}
   709  	return newBenchmark, nil
   710  }
   711  
   712  type MetricList []string
   713  
   714  func (l MetricList) Len() int {
   715  	return len(l)
   716  }
   717  
   718  func (l MetricList) Less(i, j int) bool {
   719  	bi := strings.HasPrefix(l[i], "build-") || strings.HasPrefix(l[i], "binary-")
   720  	bj := strings.HasPrefix(l[j], "build-") || strings.HasPrefix(l[j], "binary-")
   721  	if bi == bj {
   722  		return l[i] < l[j]
   723  	}
   724  	return !bi
   725  }
   726  
   727  func (l MetricList) Swap(i, j int) {
   728  	l[i], l[j] = l[j], l[i]
   729  }
   730  
   731  func collectList(all []string, idx int, second string) (res []string) {
   732  	m := make(map[string]bool)
   733  	for _, str := range all {
   734  		ss := strings.Split(str, "|")
   735  		v := ss[idx]
   736  		v2 := ss[1-idx]
   737  		if (second == "" || second == v2) && !m[v] {
   738  			m[v] = true
   739  			res = append(res, v)
   740  		}
   741  	}
   742  	sort.Sort(MetricList(res))
   743  	return res
   744  }
   745  
   746  func (pc *PerfConfig) BuildersForBenchmark(bench string) []string {
   747  	return collectList(pc.BuilderBench, 0, bench)
   748  }
   749  
   750  func (pc *PerfConfig) BenchmarksForBuilder(builder string) []string {
   751  	return collectList(pc.BuilderBench, 1, builder)
   752  }
   753  
   754  func (pc *PerfConfig) MetricsForBenchmark(bench string) []string {
   755  	return collectList(pc.BenchMetric, 1, bench)
   756  }
   757  
   758  func (pc *PerfConfig) BenchmarkProcList() (res []string) {
   759  	bl := pc.BenchmarksForBuilder("")
   760  	pl := pc.ProcList("")
   761  	for _, b := range bl {
   762  		for _, p := range pl {
   763  			res = append(res, fmt.Sprintf("%v-%v", b, p))
   764  		}
   765  	}
   766  	return res
   767  }
   768  
   769  func (pc *PerfConfig) ProcList(builder string) []int {
   770  	ss := collectList(pc.BuilderProcs, 1, builder)
   771  	var procs []int
   772  	for _, s := range ss {
   773  		p, _ := strconv.ParseInt(s, 10, 32)
   774  		procs = append(procs, int(p))
   775  	}
   776  	sort.Ints(procs)
   777  	return procs
   778  }
   779  
   780  // A PerfTodo contains outstanding commits for benchmarking for a builder.
   781  // Descendant of Package.
   782  type PerfTodo struct {
   783  	PackagePath string // (empty for main repo commits)
   784  	Builder     string
   785  	CommitNums  []int `datastore:",noindex"` // LIFO queue of commits to benchmark.
   786  }
   787  
   788  func (todo *PerfTodo) Key(c appengine.Context) *datastore.Key {
   789  	p := Package{Path: todo.PackagePath}
   790  	key := todo.Builder
   791  	return datastore.NewKey(c, "PerfTodo", key, 0, p.Key(c))
   792  }
   793  
   794  // AddCommitToPerfTodo adds the commit to all existing PerfTodo entities.
   795  func AddCommitToPerfTodo(c appengine.Context, com *Commit) error {
   796  	var todos []*PerfTodo
   797  	_, err := datastore.NewQuery("PerfTodo").
   798  		Ancestor((&Package{}).Key(c)).
   799  		GetAll(c, &todos)
   800  	if err != nil {
   801  		return fmt.Errorf("fetching PerfTodo's: %v", err)
   802  	}
   803  	for _, todo := range todos {
   804  		todo.CommitNums = append(todo.CommitNums, com.Num)
   805  		_, err = datastore.Put(c, todo.Key(c), todo)
   806  		if err != nil {
   807  			return fmt.Errorf("updating PerfTodo: %v", err)
   808  		}
   809  	}
   810  	return nil
   811  }
   812  
   813  // A Log is a gzip-compressed log file stored under the SHA1 hash of the
   814  // uncompressed log text.
   815  type Log struct {
   816  	CompressedLog []byte
   817  }
   818  
   819  func (l *Log) Text() ([]byte, error) {
   820  	d, err := gzip.NewReader(bytes.NewBuffer(l.CompressedLog))
   821  	if err != nil {
   822  		return nil, fmt.Errorf("reading log data: %v", err)
   823  	}
   824  	b, err := ioutil.ReadAll(d)
   825  	if err != nil {
   826  		return nil, fmt.Errorf("reading log data: %v", err)
   827  	}
   828  	return b, nil
   829  }
   830  
   831  func PutLog(c appengine.Context, text string) (hash string, err error) {
   832  	h := sha1.New()
   833  	io.WriteString(h, text)
   834  	b := new(bytes.Buffer)
   835  	z, _ := gzip.NewWriterLevel(b, gzip.BestCompression)
   836  	io.WriteString(z, text)
   837  	z.Close()
   838  	hash = fmt.Sprintf("%x", h.Sum(nil))
   839  	key := datastore.NewKey(c, "Log", hash, 0, nil)
   840  	_, err = datastore.Put(c, key, &Log{b.Bytes()})
   841  	return
   842  }
   843  
   844  // A Tag is used to keep track of the most recent Go weekly and release tags.
   845  // Typically there will be one Tag entity for each kind of hg tag.
   846  type Tag struct {
   847  	Kind string // "weekly", "release", or "tip"
   848  	Name string // the tag itself (for example: "release.r60")
   849  	Hash string
   850  }
   851  
   852  func (t *Tag) Key(c appengine.Context) *datastore.Key {
   853  	p := &Package{}
   854  	return datastore.NewKey(c, "Tag", t.Kind, 0, p.Key(c))
   855  }
   856  
   857  func (t *Tag) Valid() error {
   858  	if t.Kind != "weekly" && t.Kind != "release" && t.Kind != "tip" {
   859  		return errors.New("invalid Kind")
   860  	}
   861  	if !validHash(t.Hash) {
   862  		return errors.New("invalid Hash")
   863  	}
   864  	return nil
   865  }
   866  
   867  // Commit returns the Commit that corresponds with this Tag.
   868  func (t *Tag) Commit(c appengine.Context) (*Commit, error) {
   869  	com := &Commit{Hash: t.Hash}
   870  	err := datastore.Get(c, com.Key(c), com)
   871  	return com, err
   872  }
   873  
   874  // GetTag fetches a Tag by name from the datastore.
   875  func GetTag(c appengine.Context, tag string) (*Tag, error) {
   876  	t := &Tag{Kind: tag}
   877  	if err := datastore.Get(c, t.Key(c), t); err != nil {
   878  		if err == datastore.ErrNoSuchEntity {
   879  			return nil, errors.New("tag not found: " + tag)
   880  		}
   881  		return nil, err
   882  	}
   883  	if err := t.Valid(); err != nil {
   884  		return nil, err
   885  	}
   886  	return t, nil
   887  }
   888  
   889  // Packages returns packages of the specified kind.
   890  // Kind must be one of "external" or "subrepo".
   891  func Packages(c appengine.Context, kind string) ([]*Package, error) {
   892  	switch kind {
   893  	case "external", "subrepo":
   894  	default:
   895  		return nil, errors.New(`kind must be one of "external" or "subrepo"`)
   896  	}
   897  	var pkgs []*Package
   898  	q := datastore.NewQuery("Package").Filter("Kind=", kind)
   899  	for t := q.Run(c); ; {
   900  		pkg := new(Package)
   901  		_, err := t.Next(pkg)
   902  		if err == datastore.Done {
   903  			break
   904  		} else if err != nil {
   905  			return nil, err
   906  		}
   907  		if pkg.Path != "" {
   908  			pkgs = append(pkgs, pkg)
   909  		}
   910  	}
   911  	return pkgs, nil
   912  }