| // 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" |
| "context" |
| "errors" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "net/http" |
| "sort" |
| "strconv" |
| "strings" |
| "time" |
| |
| "google.golang.org/appengine/datastore" |
| |
| "golang.org/x/build/app/cache" |
| |
| "golang.org/x/build/internal/loghash" |
| ) |
| |
| const ( |
| maxDatastoreStringLen = 500 |
| PerfRunLength = 1024 |
| ) |
| |
| // 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 context.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 context.Context) (*Commit, error) { |
| var commits []*Commit |
| _, err := datastore.NewQuery("Commit"). |
| Ancestor(p.Key(c)). |
| Order("-Time"). |
| Limit(1). |
| GetAll(c, &commits) |
| 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 context.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) |
| } |
| return p, err |
| } |
| |
| type builderAndGoHash struct { |
| builder, goHash string |
| } |
| |
| // 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 { |
| PackagePath string // (empty for main repo commits) |
| Hash string |
| ParentHash string |
| Num int // Internal monotonic counter unique to this package. |
| |
| User string |
| Desc string `datastore:",noindex"` |
| Time time.Time |
| NeedsBenchmarking bool |
| TryPatch bool |
| Branch string |
| |
| // 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"` |
| |
| // PerfResults holds a set of “builder|benchmark” tuples denoting |
| // what benchmarks have been executed on the commit. |
| PerfResults []string `datastore:",noindex"` |
| |
| FailNotificationSent bool |
| |
| buildingURLs map[builderAndGoHash]string |
| } |
| |
| func (com *Commit) Key(c context.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 |
| } |
| |
| func putCommit(c context.Context, com *Commit) error { |
| if err := com.Valid(); err != nil { |
| return fmt.Errorf("putting Commit: %v", err) |
| } |
| if com.Num == 0 && com.ParentHash != "0000" { // 0000 is used in tests |
| return fmt.Errorf("putting Commit: invalid Num (must be > 0)") |
| } |
| if _, err := datastore.Put(c, com.Key(c), com); err != nil { |
| return fmt.Errorf("putting Commit: %v", err) |
| } |
| 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 ResultData field. |
| // It must be called from inside a datastore transaction. |
| func (com *Commit) AddResult(c context.Context, r *Result) error { |
| if err := datastore.Get(c, com.Key(c), com); err != nil { |
| return fmt.Errorf("getting Commit: %v", err) |
| } |
| |
| var resultExists bool |
| for i, s := range com.ResultData { |
| // if there already exists result data for this builder at com, overwrite it. |
| if strings.HasPrefix(s, r.Builder+"|") && strings.HasSuffix(s, "|"+r.GoHash) { |
| 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) |
| } |
| return putCommit(c, com) |
| } |
| |
| // removeResult removes the denormalized Result data from the ResultData field |
| // for the given builder and go hash. |
| // It must be called from within the datastore transaction that gets and puts |
| // the Commit. Note this is slightly different to AddResult, above. |
| func (com *Commit) RemoveResult(r *Result) { |
| var rd []string |
| for _, s := range com.ResultData { |
| if strings.HasPrefix(s, r.Builder+"|") && strings.HasSuffix(s, "|"+r.GoHash) { |
| continue |
| } |
| rd = append(rd, s) |
| } |
| com.ResultData = rd |
| } |
| |
| // 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 context.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) |
| return putCommit(c, com) |
| } |
| |
| 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 { |
| if !strings.HasPrefix(r, builder) { |
| // Avoid strings.SplitN alloc in the common case. |
| continue |
| } |
| p := strings.SplitN(r, "|", 4) |
| if len(p) != 4 || p[0] != builder || p[3] != goHash { |
| continue |
| } |
| return partsToResult(c, p) |
| } |
| if u, ok := c.buildingURLs[builderAndGoHash{builder, goHash}]; ok { |
| return &Result{ |
| Builder: builder, |
| BuildingURL: u, |
| Hash: c.Hash, |
| GoHash: goHash, |
| } |
| } |
| return nil |
| } |
| |
| // Results returns the build Results for this Commit. |
| func (c *Commit) Results() (results []*Result) { |
| for _, r := range c.ResultData { |
| p := strings.SplitN(r, "|", 4) |
| if len(p) != 4 { |
| continue |
| } |
| results = append(results, partsToResult(c, p)) |
| } |
| return |
| } |
| |
| func (c *Commit) ResultGoHashes() []string { |
| // For the main repo, just return the empty string |
| // (there's no corresponding main repo hash for a main repo Commit). |
| // This function is only really useful for sub-repos. |
| if c.PackagePath == "" { |
| return []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] |
| } |
| } |
| |
| // 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 context.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 context.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 context.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 context.Context, startCommitNum, n int) ([]*Commit, error) { |
| if startCommitNum < 0 || n <= 0 { |
| return nil, fmt.Errorf("GetCommits: invalid args (%v, %v)", startCommitNum, n) |
| } |
| |
| p := &Package{} |
| t := datastore.NewQuery("CommitRun"). |
| Ancestor(p.Key(c)). |
| Filter("StartCommitNum >=", startCommitNum/PerfRunLength*PerfRunLength). |
| Order("StartCommitNum"). |
| Limit(100). |
| Run(c) |
| |
| res := make([]*Commit, n) |
| for { |
| cr := new(CommitRun) |
| _, err := t.Next(cr) |
| if err == datastore.Done { |
| break |
| } |
| if err != nil { |
| return nil, err |
| } |
| if cr.StartCommitNum >= startCommitNum+n { |
| break |
| } |
| // Calculate start index for copying. |
| i := 0 |
| if cr.StartCommitNum < startCommitNum { |
| i = startCommitNum - cr.StartCommitNum |
| } |
| // Calculate end index for copying. |
| e := PerfRunLength |
| if cr.StartCommitNum+e > startCommitNum+n { |
| e = startCommitNum + n - cr.StartCommitNum |
| } |
| for ; i < e; 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[cr.StartCommitNum-startCommitNum+i] = com |
| } |
| if e != PerfRunLength { |
| break |
| } |
| } |
| return res, nil |
| } |
| |
| // partsToResult converts a Commit and ResultData substrings to a Result. |
| func partsToResult(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. |
| // |
| // Each Result entity is a descendant of its associated Package entity. |
| type Result struct { |
| PackagePath string // (empty for Go commits) |
| Builder string // "os-arch[-note]" |
| Hash string |
| |
| // The Go Commit this was built against (empty for Go commits). |
| GoHash string |
| |
| BuildingURL string `datastore:"-"` // non-empty if currently building |
| 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 context.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) |
| } |
| |
| // 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 context.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 context.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 context.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 context.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 context.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) |
| } |
| |
| p := &Package{} |
| t := datastore.NewQuery("PerfMetricRun"). |
| Ancestor(p.Key(c)). |
| Filter("Builder =", builder). |
| Filter("Benchmark =", benchmark). |
| Filter("Metric =", metric). |
| Filter("StartCommitNum >=", startCommitNum/PerfRunLength*PerfRunLength). |
| Order("StartCommitNum"). |
| Limit(100). |
| Run(c) |
| |
| res := make([]uint64, n) |
| for { |
| metrics := new(PerfMetricRun) |
| _, err := t.Next(metrics) |
| if err == datastore.Done { |
| break |
| } |
| if err != nil { |
| return nil, err |
| } |
| if metrics.StartCommitNum >= startCommitNum+n { |
| break |
| } |
| // Calculate start index for copying. |
| i := 0 |
| if metrics.StartCommitNum < startCommitNum { |
| i = startCommitNum - metrics.StartCommitNum |
| } |
| // Calculate end index for copying. |
| e := PerfRunLength |
| if metrics.StartCommitNum+e > startCommitNum+n { |
| e = startCommitNum + n - metrics.StartCommitNum |
| } |
| for ; i < e; i++ { |
| res[metrics.StartCommitNum-startCommitNum+i] = uint64(metrics.Vals[i]) |
| } |
| if e != PerfRunLength { |
| break |
| } |
| } |
| 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 context.Context) *datastore.Key { |
| p := Package{} |
| return datastore.NewKey(c, "PerfConfig", "PerfConfig", 0, p.Key(c)) |
| } |
| |
| const perfConfigCacheKey = "perf-config" |
| |
| func GetPerfConfig(c context.Context, r *http.Request) (*PerfConfig, error) { |
| pc := new(PerfConfig) |
| now := cache.Now(c) |
| if cache.Get(c, 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(c, 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 context.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 |
| } |
| |
| type MetricList []string |
| |
| func (l MetricList) Len() int { |
| return len(l) |
| } |
| |
| func (l MetricList) Less(i, j int) bool { |
| bi := strings.HasPrefix(l[i], "build-") || strings.HasPrefix(l[i], "binary-") |
| bj := strings.HasPrefix(l[j], "build-") || strings.HasPrefix(l[j], "binary-") |
| if bi == bj { |
| return l[i] < l[j] |
| } |
| return !bi |
| } |
| |
| func (l MetricList) Swap(i, j int) { |
| l[i], l[j] = l[j], l[i] |
| } |
| |
| 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.Sort(MetricList(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 context.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 context.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 |
| } |
| |
| // 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 context.Context, text string) (hash string, err error) { |
| b := new(bytes.Buffer) |
| z, _ := gzip.NewWriterLevel(b, gzip.BestCompression) |
| io.WriteString(z, text) |
| z.Close() |
| hash = loghash.New(text) |
| 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 // "release", or "tip" |
| Name string // the tag itself (for example: "release.r60") |
| Hash string |
| } |
| |
| func (t *Tag) String() string { |
| if t.Kind == "tip" { |
| return "tip" |
| } |
| return t.Name |
| } |
| |
| func (t *Tag) Key(c context.Context) *datastore.Key { |
| p := &Package{} |
| s := t.Kind |
| if t.Kind == "release" { |
| s += "-" + t.Name |
| } |
| return datastore.NewKey(c, "Tag", s, 0, p.Key(c)) |
| } |
| |
| func (t *Tag) Valid() error { |
| if t.Kind != "release" && t.Kind != "tip" { |
| return errors.New("invalid Kind") |
| } |
| if t.Kind == "release" && t.Name == "" { |
| return errors.New("release must have Name") |
| } |
| 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 context.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 context.Context, kind, name string) (*Tag, error) { |
| t := &Tag{Kind: kind, Name: name} |
| if err := datastore.Get(c, t.Key(c), t); err != nil { |
| 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 context.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 err == datastore.Done { |
| break |
| } else if err != nil { |
| return nil, err |
| } |
| if pkg.Path != "" { |
| pkgs = append(pkgs, pkg) |
| } |
| } |
| return pkgs, nil |
| } |