blob: 9ce75cd18b50898a731fc799a7d222dfe69b61b0 [file] [log] [blame]
/** trybot is for loading and serving trybot performance results.
Implementation notes:
Regular files are in:
gs://chromium-skia-gm/nano-json-v1/2014/08/07/01/Perf-...-Release/nanobench_da7a94...8d35a_1407357280.json
while trybots are in:
gs://chromium-skia-gm/trybot/nano-json-v1/2014/08/07/05/Perf-...-Release-Trybot/85/448043002/nanobench_da7a944...d35a_1407357280.json
Note the 'trybot' dir prefix and the addition of the build number and codereview issue number in the directory.
The Rietveld issue id appears before the file name. Note that some of the tries aren't associated with an issue, we will ignore those.
Notes: I tried using both GOB and JSON as the serialization format and got the following numbers:
GOB: 40s 12MB 49s (second run)
JSON: 43s 14MB 43s (second run)
Since there isn't a material difference between the two let's go with JSON
as that's a little easier to debug.
*/
package trybot
import (
"database/sql"
"encoding/json"
"fmt"
"regexp"
"sort"
"time"
"github.com/golang/glog"
metrics "github.com/rcrowley/go-metrics"
storage "code.google.com/p/google-api-go-client/storage/v1"
"skia.googlesource.com/buildbot.git/go/util"
"skia.googlesource.com/buildbot.git/perf/go/db"
"skia.googlesource.com/buildbot.git/perf/go/ingester"
"skia.googlesource.com/buildbot.git/perf/go/types"
)
var (
// nameRegex is the regexp that a trybot filename must match. This enforces the need for a Rietveld issue number.
//
// REPL here: http://play.golang.org/p/uGmexyFxEr
nameRegex = regexp.MustCompile(`trybot/nano-json-v1/\d{4}/\d{2}/\d{2}/\d{2}/[^/]+/\d+/(\d+)/(.*)`)
st *storage.Service = nil
)
// Write the TryBotResults to the datastore.
func Write(issue string, try *types.TryBotResults) error {
b, err := json.Marshal(try)
if err != nil {
return fmt.Errorf("Failed to encode to JSON: %s", err)
}
glog.Infof("Writing: %s", issue)
_, err = db.DB.Exec("REPLACE INTO tries (issue, results, lastUpdated) VALUES (?, ?, ?)", issue, b, time.Now())
if err != nil {
return fmt.Errorf("Failed to write to database: %s", err)
}
return nil
}
// Get the TryBotResults from the datastore.
func Get(issue string) (*types.TryBotResults, error) {
var results string
err := db.DB.QueryRow("SELECT results FROM tries WHERE issue=?", issue).Scan(&results)
if err == sql.ErrNoRows {
return types.NewTryBotResults(), nil
}
if err != nil {
return nil, fmt.Errorf("Failed to load try data with id %s: %s", issue, err)
}
try := &types.TryBotResults{}
if err := json.Unmarshal([]byte(results), try); err != nil {
return nil, fmt.Errorf("Failed to decode try data with id: %s", issue)
}
return try, nil
}
// List returns the last N Rietveld issue IDs.
func List(n int) ([]string, error) {
rows, err := db.DB.Query("SELECT issue FROM tries ORDER BY lastUpdated DESC LIMIT ?", n)
if err != nil {
return nil, fmt.Errorf("Failed to read try data from database: %s", err)
}
defer rows.Close()
ret := []string{}
for rows.Next() {
var issue string
if err := rows.Scan(&issue); err != nil {
return nil, fmt.Errorf("List: Failed to read issus from row: %s", err)
}
ret = append(ret, issue)
}
sort.Sort(sort.Reverse(sort.StringSlice(ret)))
return ret, nil
}
// TileWithTryData will add all the trybot data for the given issue to the
// given Tile. A new Tile that is a copy of the original Tile will be returned,
// so we aren't modifying the underlying Tile.
func TileWithTryData(tile *types.Tile, issue string) (*types.Tile, error) {
ret := tile.Copy()
lastCommitIndex := tile.LastCommitIndex()
// The way we handle Tiles there is always empty space at the end of the
// Tile of index -1. Use that space to inject the trybot results.
ret.Commits[lastCommitIndex+1].CommitTime = time.Now().Unix()
lastCommitIndex = ret.LastCommitIndex()
tryResults, err := Get(issue)
if err != nil {
return nil, fmt.Errorf("AppendToTile: Failed to retreive trybot results: %s", err)
}
// Copy in the trybot data.
for k, v := range tryResults.Values {
if tr, ok := ret.Traces[k]; !ok {
continue
} else {
tr.(*types.PerfTrace).Values[lastCommitIndex] = v
}
}
return ret, nil
}
// addTryData copies the data from the ResultsFileLocation into the TryBotResults.
func addTryData(res *types.TryBotResults, b *ingester.ResultsFileLocation, counter metrics.Counter) {
glog.Infof("addTryData: %s", b.Name)
r, err := b.Fetch()
if err != nil {
// Don't fall over for a single failed HTTP request.
return
}
benchData, err := ingester.ParseBenchDataFromReader(r)
if err != nil {
// Don't fall over for a single corrupt file.
return
}
keyPrefix := benchData.KeyPrefix()
for testName, allConfigs := range benchData.Results {
for configName, result := range *allConfigs {
key := fmt.Sprintf("%s:%s:%s", keyPrefix, testName, configName)
res.Values[key] = result.Min
counter.Inc(1)
}
}
}
// BenchByIssue allows sorting ResultsFileLocation's by the Rietveld issue id.
//
// We sort on issue id so that we aren't doing excessive writes to the
// database.
type BenchByIssue struct {
ResultsFileLocation *ingester.ResultsFileLocation
IssueName string
}
type BenchByIssueSlice []*BenchByIssue
func (p BenchByIssueSlice) Len() int { return len(p) }
func (p BenchByIssueSlice) Less(i, j int) bool { return p[i].IssueName < p[j].IssueName }
func (p BenchByIssueSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func init() {
var err error
st, err = storage.New(util.NewTimeoutClient())
if err != nil {
panic("Can't construct HTTP client")
}
}
// TrybotIngestion implements ingester.IngestResultsFiles.
//
// Note that the TileTracker is not used as we write the files to the database.
func TrybotIngestion(_ *ingester.TileTracker, resultsFiles []*ingester.ResultsFileLocation, counter metrics.Counter) error {
benchFilesByIssue := []*BenchByIssue{}
var err error
for _, b := range resultsFiles {
match := nameRegex.FindStringSubmatch(b.Name)
if match != nil {
issue := match[1]
benchFilesByIssue = append(benchFilesByIssue, &BenchByIssue{
ResultsFileLocation: b,
IssueName: issue,
})
}
}
// Resort by issue id.
sort.Sort(BenchByIssueSlice(benchFilesByIssue))
lastIssue := ""
var cur *types.TryBotResults = nil
for _, b := range benchFilesByIssue {
if b.IssueName != lastIssue {
// Write out the current TryBotResults to the datastore and create a fresh new TryBotResults.
if cur != nil {
if err := Write(lastIssue, cur); err != nil {
return fmt.Errorf("Update failed to write trybot results: %s", err)
}
}
if cur, err = Get(b.IssueName); err != nil {
return fmt.Errorf("Failed to load existing trybot data for issue %s: %s", b.IssueName, err)
}
lastIssue = b.IssueName
glog.Infof("Switched to issue: %s", lastIssue)
}
addTryData(cur, b.ResultsFileLocation, counter)
}
if cur != nil {
if err := Write(lastIssue, cur); err != nil {
return fmt.Errorf("Update failed to write trybot results: %s", err)
}
}
glog.Infof("Finished trybot ingestion.")
return nil
}