1
0
mirror of https://github.com/golang/go synced 2024-11-18 19:54:44 -07:00

dashboard: server app UI changes for performance dashboard

This CL moves code from code.google.com/p/dvyukov-go-perf-dashboard,
which was previously reviewed.

LGTM=adg
R=adg
CC=golang-codereviews
https://golang.org/cl/96180043
This commit is contained in:
Dmitriy Vyukov 2014-05-13 11:01:50 +04:00
parent 9bb1e09cc4
commit 904c4641c7
13 changed files with 1640 additions and 73 deletions

View File

@ -13,8 +13,9 @@ handlers:
static_dir: static
- url: /(|gccgo/)log/.+
script: _go_app
- url: /(|gccgo/)(|commit|packages|result|tag|todo)
- url: /(|gccgo/)(|commit|packages|result|perf-result|tag|todo|perf|perfdetail|perfgraph|updatebenchmark)
script: _go_app
- url: /(|gccgo/)(init|buildtest|key|_ah/queue/go/delay)
- url: /(|gccgo/)(init|buildtest|key|perflearn|_ah/queue/go/delay)
script: _go_app
login: admin

View File

@ -0,0 +1,251 @@
// Copyright 2013 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"
"fmt"
"html/template"
"net/http"
"sort"
"strconv"
"appengine"
"appengine/datastore"
)
func init() {
http.HandleFunc("/perf", perfChangesHandler)
}
// perfSummaryHandler draws the main benchmarking page.
func perfChangesHandler(w http.ResponseWriter, r *http.Request) {
d := dashboardForRequest(r)
c := d.Context(appengine.NewContext(r))
page, _ := strconv.Atoi(r.FormValue("page"))
if page < 0 {
page = 0
}
pc, err := GetPerfConfig(c, r)
if err != nil {
logErr(w, r, err)
return
}
commits, err := dashPerfCommits(c, page)
if err != nil {
logErr(w, r, err)
return
}
// Fetch PerfResult's for the commits.
var uiCommits []*perfChangesCommit
rc := MakePerfResultCache(c, commits[0], false)
// But first compare tip with the last release.
if page == 0 {
res0 := &PerfResult{CommitHash: knownTags[lastRelease]}
if err := datastore.Get(c, res0.Key(c), res0); err != nil && err != datastore.ErrNoSuchEntity {
logErr(w, r, fmt.Errorf("getting PerfResult: %v", err))
return
}
if err != datastore.ErrNoSuchEntity {
uiCom, err := handleOneCommit(pc, commits[0], rc, res0)
if err != nil {
logErr(w, r, err)
return
}
uiCom.IsSummary = true
uiCom.ParentHash = lastRelease
uiCommits = append(uiCommits, uiCom)
}
}
for _, com := range commits {
uiCom, err := handleOneCommit(pc, com, rc, nil)
if err != nil {
logErr(w, r, err)
return
}
uiCommits = append(uiCommits, uiCom)
}
p := &Pagination{}
if len(commits) == commitsPerPage {
p.Next = page + 1
}
if page > 0 {
p.Prev = page - 1
p.HasPrev = true
}
data := &perfChangesData{d, p, uiCommits}
var buf bytes.Buffer
if err := perfChangesTemplate.Execute(&buf, data); err != nil {
logErr(w, r, err)
return
}
buf.WriteTo(w)
}
func handleOneCommit(pc *PerfConfig, com *Commit, rc *PerfResultCache, baseRes *PerfResult) (*perfChangesCommit, error) {
uiCom := new(perfChangesCommit)
uiCom.Commit = com
res1 := rc.Get(com.Num)
for builder, benchmarks1 := range res1.ParseData() {
for benchmark, data1 := range benchmarks1 {
if benchmark != "meta-done" || !data1.OK {
uiCom.NumResults++
}
if !data1.OK {
v := new(perfChangesChange)
v.diff = 10000
v.Style = "fail"
v.Builder = builder
v.Link = fmt.Sprintf("log/%v", data1.Artifacts["log"])
v.Val = builder
v.Hint = builder
if benchmark != "meta-done" {
v.Hint += "/" + benchmark
}
m := findMetric(uiCom, "failure")
m.BadChanges = append(m.BadChanges, v)
}
}
res0 := baseRes
if res0 == nil {
var err error
res0, err = rc.NextForComparison(com.Num, builder)
if err != nil {
return nil, err
}
if res0 == nil {
continue
}
}
changes := significantPerfChanges(pc, builder, res0, res1)
for _, ch := range changes {
v := new(perfChangesChange)
v.Builder = builder
v.Benchmark, v.Procs = splitBench(ch.bench)
v.diff = ch.diff
v.Val = fmt.Sprintf("%+.2f%%", ch.diff)
v.Hint = fmt.Sprintf("%v/%v", builder, ch.bench)
v.Link = fmt.Sprintf("perfdetail?commit=%v&commit0=%v&builder=%v&benchmark=%v", com.Hash, res0.CommitHash, builder, v.Benchmark)
m := findMetric(uiCom, ch.metric)
if v.diff > 0 {
v.Style = "bad"
m.BadChanges = append(m.BadChanges, v)
} else {
v.Style = "good"
m.GoodChanges = append(m.GoodChanges, v)
}
}
}
// Sort metrics and changes.
for _, m := range uiCom.Metrics {
sort.Sort(m.GoodChanges)
sort.Sort(m.BadChanges)
}
sort.Sort(uiCom.Metrics)
// Need at least one metric for UI.
if len(uiCom.Metrics) == 0 {
uiCom.Metrics = append(uiCom.Metrics, &perfChangesMetric{})
}
uiCom.Metrics[0].First = true
return uiCom, nil
}
func findMetric(c *perfChangesCommit, metric string) *perfChangesMetric {
for _, m := range c.Metrics {
if m.Name == metric {
return m
}
}
m := new(perfChangesMetric)
m.Name = metric
c.Metrics = append(c.Metrics, m)
return m
}
type uiPerfConfig struct {
Builders []uiPerfConfigElem
Benchmarks []uiPerfConfigElem
Metrics []uiPerfConfigElem
Procs []uiPerfConfigElem
}
type uiPerfConfigElem struct {
Name string
Selected bool
}
var perfChangesTemplate = template.Must(
template.New("perf_changes.html").Funcs(tmplFuncs).ParseFiles("build/perf_changes.html"),
)
type perfChangesData struct {
Dashboard *Dashboard
Pagination *Pagination
Commits []*perfChangesCommit
}
type perfChangesCommit struct {
*Commit
IsSummary bool
NumResults int
Metrics perfChangesMetricSlice
}
type perfChangesMetric struct {
Name string
First bool
BadChanges perfChangesChangeSlice
GoodChanges perfChangesChangeSlice
}
type perfChangesChange struct {
Builder string
Benchmark string
Link string
Hint string
Style string
Val string
Procs int
diff float64
}
type perfChangesMetricSlice []*perfChangesMetric
func (l perfChangesMetricSlice) Len() int { return len(l) }
func (l perfChangesMetricSlice) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
func (l perfChangesMetricSlice) Less(i, j int) bool {
if l[i].Name == "failure" || l[j].Name == "failure" {
return l[i].Name == "failure"
}
return l[i].Name < l[j].Name
}
type perfChangesChangeSlice []*perfChangesChange
func (l perfChangesChangeSlice) Len() int { return len(l) }
func (l perfChangesChangeSlice) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
func (l perfChangesChangeSlice) Less(i, j int) bool {
vi, vj := l[i].diff, l[j].diff
if vi > 0 && vj > 0 {
return vi > vj
} else if vi < 0 && vj < 0 {
return vi < vj
} else {
panic("comparing positive and negative diff")
}
}

View File

@ -0,0 +1,88 @@
<!doctype html>
<html>
<head>
<title>{{$.Dashboard.Name}} Dashboard</title>
<link rel="stylesheet" href="/static/style.css"/>
</head>
<body>
<header id="topbar">
<h1>Go Dashboard</h1>
<nav>
<a href="{{$.Dashboard.RelPath}}">Test</a>
<a href="{{$.Dashboard.RelPath}}perf">Perf</a>
<a href="{{$.Dashboard.RelPath}}perfgraph">Graphs</a>
</nav>
<div class="clear"></div>
</header>
<div class="page">
<div class="build-container">
<table class="build">
<colgroup class="col-hash"></colgroup>
<colgroup class="col-numresults"></colgroup>
<colgroup class="col-metric"></colgroup>
<colgroup class="col-result"></colgroup>
<colgroup class="col-result"></colgroup>
<colgroup class="col-user"></colgroup>
<colgroup class="col-time"></colgroup>
<colgroup class="col-desc"></colgroup>
<tbody>
{{range $c := $.Commits}}
{{range $m := $c.Metrics}}
{{if $m.First}}
<tr class="row-commit">
{{if $c.IsSummary}}
<td class="hash">tip vs {{$c.ParentHash}}</td>
{{else}}
<td class="hash"><a href="{{repoURL $.Dashboard.Name $c.Hash ""}}">{{shortHash $c.Hash}}</a></td>
{{end}}
<td class="numresults">{{$c.NumResults}}</td>
{{else}}
<tr>
<td class="user">&nbsp;</td>
<td class="numresults">&nbsp;</td>
{{end}}
<td>{{$m.Name}}</td>
<td>
{{range $ch := $m.BadChanges}}
<a class="{{$ch.Style}}" href="{{$ch.Link}}" title="{{$ch.Hint}}">{{$ch.Val}}</a> &nbsp;
{{end}}
</td>
<td>
{{range $ch := $m.GoodChanges}}
<a class="{{$ch.Style}}" href="{{$ch.Link}}" title="{{$ch.Hint}}">{{$ch.Val}}</a> &nbsp;
{{end}}
</td>
{{if $m.First}}
<td class="user" title="{{$c.User}}">{{shortUser $c.User}}</td>
<td class="time">{{$c.Time.Format "Mon 02 Jan 15:04"}}</td>
<td class="desc" title="{{$c.Desc}}">{{shortDesc $c.Desc}}</td>
{{else}}
<td class="user">&nbsp;</td>
<td class="time">&nbsp;</td>
<td class="desc">&nbsp;</td>
{{end}}
</tr>
{{end}}
{{if $c.IsSummary}}
<tr class="row-commit"><td>---</td></tr>
{{end}}
{{end}}
</tbody>
</table>
{{with $.Pagination}}
<div class="paginate">
<nav>
<a {{if .HasPrev}}href="?page={{.Prev}}"{{else}}class="inactive"{{end}}>newer</a>
<a {{if .Next}}href="?page={{.Next}}"{{else}}class="inactive"{{end}}>older</a>
<a {{if .HasPrev}}href="."{{else}}class="inactive"{{end}}>latest</a>
</nav>
</div>
{{end}}
</div>
<div class="clear"></div>
</div>
</body>
</html>

View File

@ -0,0 +1,221 @@
// Copyright 2013 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"
"fmt"
"html/template"
"net/http"
"sort"
"strconv"
"strings"
"appengine"
"appengine/datastore"
)
func init() {
for _, d := range dashboards {
http.HandleFunc(d.RelPath+"perfdetail", perfDetailUIHandler)
}
}
func perfDetailUIHandler(w http.ResponseWriter, r *http.Request) {
d := dashboardForRequest(r)
c := d.Context(appengine.NewContext(r))
pc, err := GetPerfConfig(c, r)
if err != nil {
logErr(w, r, err)
return
}
kind := r.FormValue("kind")
builder := r.FormValue("builder")
benchmark := r.FormValue("benchmark")
if kind == "" {
kind = "benchmark"
}
if kind != "benchmark" && kind != "builder" {
logErr(w, r, fmt.Errorf("unknown kind %s", kind))
return
}
// Fetch the new commit.
com1 := new(Commit)
com1.Hash = r.FormValue("commit")
if hash, ok := knownTags[com1.Hash]; ok {
com1.Hash = hash
}
if err := datastore.Get(c, com1.Key(c), com1); err != nil {
logErr(w, r, fmt.Errorf("failed to fetch commit %s: %v", com1.Hash, err))
return
}
// Fetch the associated perf result.
ress1 := &PerfResult{CommitHash: com1.Hash}
if err := datastore.Get(c, ress1.Key(c), ress1); err != nil {
logErr(w, r, fmt.Errorf("failed to fetch perf result %s: %v", com1.Hash, err))
return
}
// Fetch the old commit.
var ress0 *PerfResult
com0 := new(Commit)
com0.Hash = r.FormValue("commit0")
if hash, ok := knownTags[com0.Hash]; ok {
com0.Hash = hash
}
if com0.Hash != "" {
// Have an exact commit hash, fetch directly.
if err := datastore.Get(c, com0.Key(c), com0); err != nil {
logErr(w, r, fmt.Errorf("failed to fetch commit %s: %v", com0.Hash, err))
return
}
ress0 = &PerfResult{CommitHash: com0.Hash}
if err := datastore.Get(c, ress0.Key(c), ress0); err != nil {
logErr(w, r, fmt.Errorf("failed to fetch perf result for %s: %v", com0.Hash, err))
return
}
} else {
// Don't have the commit hash, find the previous commit to compare.
rc := MakePerfResultCache(c, com1, false)
ress0, err = rc.NextForComparison(com1.Num, "")
if err != nil {
logErr(w, r, err)
return
}
if ress0 == nil {
logErr(w, r, fmt.Errorf("no previous commit with results"))
return
}
// Now that we know the right result, fetch the commit.
com0.Hash = ress0.CommitHash
if err := datastore.Get(c, com0.Key(c), com0); err != nil {
logErr(w, r, fmt.Errorf("failed to fetch commit %s: %v", com0.Hash, err))
return
}
}
res0 := ress0.ParseData()
res1 := ress1.ParseData()
var benchmarks []*uiPerfDetailBenchmark
var list []string
if kind == "builder" {
list = pc.BenchmarksForBuilder(builder)
} else {
list = pc.BuildersForBenchmark(benchmark)
}
for _, other := range list {
if kind == "builder" {
benchmark = other
} else {
builder = other
}
var procs []*uiPerfDetailProcs
allProcs := pc.ProcList(builder)
for _, p := range allProcs {
BenchProcs := fmt.Sprintf("%v-%v", benchmark, p)
if res0[builder] == nil || res0[builder][BenchProcs] == nil {
continue
}
pp := &uiPerfDetailProcs{Procs: p}
for metric, val := range res0[builder][BenchProcs].Metrics {
var pm uiPerfDetailMetric
pm.Name = metric
pm.Val0 = fmt.Sprintf("%v", val)
val1 := uint64(0)
if res1[builder] != nil && res1[builder][BenchProcs] != nil {
val1 = res1[builder][BenchProcs].Metrics[metric]
}
pm.Val1 = fmt.Sprintf("%v", val1)
v0 := val
v1 := val1
valf := perfDiff(v0, v1)
pm.Delta = fmt.Sprintf("%+.2f%%", valf)
pm.Style = perfChangeStyle(pc, valf, builder, BenchProcs, pm.Name)
pp.Metrics = append(pp.Metrics, pm)
}
sort.Sort(pp.Metrics)
for artifact, hash := range res0[builder][BenchProcs].Artifacts {
var pm uiPerfDetailMetric
pm.Val0 = fmt.Sprintf("%v", artifact)
pm.Link0 = fmt.Sprintf("log/%v", hash)
pm.Val1 = fmt.Sprintf("%v", artifact)
if res1[builder] != nil && res1[builder][BenchProcs] != nil && res1[builder][BenchProcs].Artifacts[artifact] != "" {
pm.Link1 = fmt.Sprintf("log/%v", res1[builder][BenchProcs].Artifacts[artifact])
}
pp.Metrics = append(pp.Metrics, pm)
}
procs = append(procs, pp)
}
benchmarks = append(benchmarks, &uiPerfDetailBenchmark{other, procs})
}
cfg := new(uiPerfConfig)
for _, v := range pc.BuildersForBenchmark("") {
cfg.Builders = append(cfg.Builders, uiPerfConfigElem{v, v == builder})
}
for _, v := range pc.BenchmarksForBuilder("") {
cfg.Benchmarks = append(cfg.Benchmarks, uiPerfConfigElem{v, v == benchmark})
}
data := &uiPerfDetailTemplateData{d, cfg, kind == "builder", com0, com1, benchmarks}
var buf bytes.Buffer
if err := uiPerfDetailTemplate.Execute(&buf, data); err != nil {
logErr(w, r, err)
return
}
buf.WriteTo(w)
}
func perfResultSplit(s string) (builder string, benchmark string, procs int) {
s1 := strings.Split(s, "|")
s2 := strings.Split(s1[1], "-")
procs, _ = strconv.Atoi(s2[1])
return s1[0], s2[0], procs
}
type uiPerfDetailTemplateData struct {
Dashboard *Dashboard
Config *uiPerfConfig
KindBuilder bool
Commit0 *Commit
Commit1 *Commit
Benchmarks []*uiPerfDetailBenchmark
}
type uiPerfDetailBenchmark struct {
Name string
Procs []*uiPerfDetailProcs
}
type uiPerfDetailProcs struct {
Procs int
Metrics uiPerfDetailMetrics
}
type uiPerfDetailMetric struct {
Name string
Val0 string
Val1 string
Link0 string
Link1 string
Delta string
Style string
}
type uiPerfDetailMetrics []uiPerfDetailMetric
func (l uiPerfDetailMetrics) Len() int { return len(l) }
func (l uiPerfDetailMetrics) Swap(i, j int) { l[i], l[j] = l[j], l[i] }
func (l uiPerfDetailMetrics) Less(i, j int) bool { return l[i].Name < l[j].Name }
var uiPerfDetailTemplate = template.Must(
template.New("perf_detail.html").Funcs(tmplFuncs).ParseFiles("build/perf_detail.html"),
)

View File

@ -0,0 +1,100 @@
<!doctype html>
<html>
<head>
<title>{{$.Dashboard.Name}} Dashboard</title>
<link rel="stylesheet" href="/static/style.css"/>
<script type="text/javascript">
function kindBuilder() {
document.getElementById('checkBuilder').checked = true;
document.getElementById('controlBuilder').style.display='inline';
document.getElementById('controlBenchmark').style.display='none';
}
function kindBenchmark() {
document.getElementById('checkBenchmark').checked = true;
document.getElementById('controlBenchmark').style.display='inline';
document.getElementById('controlBuilder').style.display='none';
}
window.onload = {{if $.KindBuilder}} kindBuilder {{else}} kindBenchmark {{end}};
</script>
</head>
<body>
<header id="topbar">
<h1>Go Dashboard</h1>
<nav>
<a href="{{$.Dashboard.RelPath}}">Test</a>
<a href="{{$.Dashboard.RelPath}}perf">Perf</a>
<a href="{{$.Dashboard.RelPath}}perfgraph">Graphs</a>
</nav>
<div class="clear"></div>
</header>
<div class="page">
<div class="diff-container">
<div class="diff-meta">
<form>
<div><b>New: </b><input type="edit" name="commit" value="{{$.Commit1.Hash}}" /> {{shortUser $.Commit1.User}} {{$.Commit1.Time.Format "Mon 02 Jan 15:04"}} {{shortDesc $.Commit1.Desc}} </div>
<div><b>Old: </b><input type="edit" name="commit0" value="{{$.Commit0.Hash}}" /> {{shortUser $.Commit0.User}} {{$.Commit0.Time.Format "Mon 02 Jan 15:04"}} {{shortDesc $.Commit0.Desc}} </div>
<div>
<input id="checkBuilder" type="radio" name="kind" value="builder" required onclick="kindBuilder()">builder</input>
<input id="checkBenchmark" type="radio" name="kind" value="benchmark" required onclick="kindBenchmark()">benchmark</input>
<select id="controlBuilder" name="builder">
{{range $.Config.Builders}}
<option {{if .Selected}}selected{{end}}>{{.Name}}</option>
{{end}}
</select>
<select id="controlBenchmark" name="benchmark">
{{range $.Config.Benchmarks}}
<option {{if .Selected}}selected{{end}}>{{.Name}}</option>
{{end}}
</select>
<input type="submit" value="Refresh" />
</div>
</form>
</div>
<p></p>
{{range $b := $.Benchmarks}}
<div class="diff-benchmark">
<h2>{{$b.Name}}</h2>
{{range $p := $b.Procs}}
<div class="diff">
<h1>GOMAXPROCS={{$p.Procs}}</h1>
<table>
<thead>
<tr>
<th>Metric</th>
<th>old</th>
<th>new</th>
<th>delta</th>
</tr>
</thead>
<tbody>
{{range $m := $p.Metrics}}
<tr>
<td class="metric">{{$m.Name}}</td>
{{if $m.Link0}}
<td><a href="{{$.Dashboard.RelPath}}{{$m.Link0}}">{{$m.Val0}}</td>
{{else}}
<td>{{$m.Val0}}</td>
{{end}}
{{if $m.Link1}}
<td><a href="{{$.Dashboard.RelPath}}{{$m.Link1}}">{{$m.Val1}}</td>
{{else}}
<td>{{$m.Val1}}</td>
{{end}}
<td class="result"><span class="{{$m.Style}}">{{$m.Delta}}</span></td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{end}}
</div>
{{end}}
<div class="clear"></div>
</div>
<div class="clear"></div>
</div>
</body>
</html>

View File

@ -0,0 +1,291 @@
// Copyright 2013 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"
"fmt"
"html/template"
"net/http"
"strconv"
"appengine"
"appengine/datastore"
)
func init() {
for _, d := range dashboards {
http.HandleFunc(d.RelPath+"perfgraph", perfGraphHandler)
}
}
func perfGraphHandler(w http.ResponseWriter, r *http.Request) {
d := dashboardForRequest(r)
c := d.Context(appengine.NewContext(r))
pc, err := GetPerfConfig(c, r)
if err != nil {
logErr(w, r, err)
return
}
allBuilders := pc.BuildersForBenchmark("")
allBenchmarks := pc.BenchmarksForBuilder("")
allMetrics := pc.MetricsForBenchmark("")
allProcs := pc.ProcList("")
r.ParseForm()
absolute := r.FormValue("absolute") != ""
selBuilders := r.Form["builder"]
selBenchmarks := r.Form["benchmark"]
selMetrics := r.Form["metric"]
selProcs := r.Form["procs"]
if len(selBuilders) == 0 {
selBuilders = append(selBuilders, allBuilders[0])
}
if len(selBenchmarks) == 0 {
selBenchmarks = append(selBenchmarks, "json")
}
if len(selMetrics) == 0 {
selMetrics = append(selMetrics, "time")
}
if len(selProcs) == 0 {
selProcs = append(selProcs, "1")
}
// TODO(dvyukov): validate input
present := func(set []string, s string) bool {
for _, s1 := range set {
if s1 == s {
return true
}
}
return false
}
cfg := &uiPerfConfig{}
for _, v := range allBuilders {
cfg.Builders = append(cfg.Builders, uiPerfConfigElem{v, present(selBuilders, v)})
}
for _, v := range allBenchmarks {
cfg.Benchmarks = append(cfg.Benchmarks, uiPerfConfigElem{v, present(selBenchmarks, v)})
}
for _, v := range allMetrics {
cfg.Metrics = append(cfg.Metrics, uiPerfConfigElem{v, present(selMetrics, v)})
}
for _, v := range allProcs {
cfg.Procs = append(cfg.Procs, uiPerfConfigElem{strconv.Itoa(v), present(selProcs, strconv.Itoa(v))})
}
// Select last commit.
startCommit := 0
commitsToDisplay := 100
if r.FormValue("startcommit") != "" {
startCommit, _ = strconv.Atoi(r.FormValue("startcommit"))
commitsToDisplay, _ = strconv.Atoi(r.FormValue("commitnum"))
} else {
var commits1 []*Commit
_, err = datastore.NewQuery("Commit").
Ancestor((&Package{}).Key(c)).
Order("-Num").
Filter("NeedsBenchmarking =", true).
Limit(1).
GetAll(c, &commits1)
if err != nil || len(commits1) != 1 {
logErr(w, r, err)
return
}
startCommit = commits1[0].Num
}
if r.FormValue("zoomin") != "" {
commitsToDisplay /= 2
} else if r.FormValue("zoomout") != "" {
commitsToDisplay *= 2
} else if r.FormValue("older") != "" {
startCommit -= commitsToDisplay / 2
} else if r.FormValue("newer") != "" {
startCommit += commitsToDisplay / 2
}
// TODO(dvyukov): limit number of lines on the graph?
startCommitNum := startCommit - commitsToDisplay + 1
if startCommitNum < 0 {
startCommitNum = 0
}
var vals [][]float64
var hints [][]string
var certainty [][]bool
var headers []string
commits2, err := GetCommits(c, startCommitNum, commitsToDisplay)
if err != nil {
logErr(w, r, err)
return
}
for _, builder := range selBuilders {
for _, metric := range selMetrics {
for _, benchmark := range selBenchmarks {
for _, procs := range selProcs {
benchProcs := fmt.Sprintf("%v-%v", benchmark, procs)
vv, err := GetPerfMetricsForCommits(c, builder, benchProcs, metric, startCommitNum, commitsToDisplay)
if err != nil {
logErr(w, r, err)
return
}
nonzero := false
min := ^uint64(0)
max := uint64(0)
for _, v := range vv {
if v == 0 {
continue
}
if max < v {
max = v
}
if min > v {
min = v
}
nonzero = true
}
if nonzero {
noise := pc.NoiseLevel(builder, benchProcs, metric)
diff := (float64(max) - float64(min)) / float64(max) * 100
// Scale graph passes through 2 points: (noise, minScale) and (growthFactor*noise, 100).
// Plus it's bottom capped at minScale and top capped at 100.
// Intention:
// Diffs below noise are scaled to minScale.
// Diffs above growthFactor*noise are scaled to 100.
// Between noise and growthFactor*noise scale growths linearly.
const minScale = 5
const growthFactor = 4
scale := diff*(100-minScale)/(noise*(growthFactor-1)) + (minScale*growthFactor-100)/(growthFactor-1)
if scale < minScale {
scale = minScale
}
if scale > 100 {
scale = 100
}
descBuilder := "/" + builder
descBenchmark := "/" + benchProcs
descMetric := "/" + metric
if len(selBuilders) == 1 {
descBuilder = ""
}
if len(selBenchmarks) == 1 && len(selProcs) == 1 {
descBenchmark = ""
}
if len(selMetrics) == 1 && (len(selBuilders) > 1 || len(selBenchmarks) > 1 || len(selProcs) > 1) {
descMetric = ""
}
desc := fmt.Sprintf("%v%v%v", descBuilder, descBenchmark, descMetric)[1:]
hh := make([]string, commitsToDisplay)
valf := make([]float64, commitsToDisplay)
cert := make([]bool, commitsToDisplay)
lastval := uint64(0)
lastval0 := uint64(0)
for i, v := range vv {
cert[i] = true
if v == 0 {
if lastval == 0 {
continue
}
nextval := uint64(0)
nextidx := 0
for i2, v2 := range vv[i+1:] {
if v2 != 0 {
nextval = v2
nextidx = i + i2 + 1
break
}
}
if nextval == 0 {
continue
}
cert[i] = false
v = lastval + uint64(int64(nextval-lastval)/int64(nextidx-i+1))
_, _ = nextval, nextidx
}
f := float64(v)
if !absolute {
f = (float64(v) - float64(min)) * 100 / (float64(max) - float64(min))
f = f*scale/100 + (100-scale)/2
f += 0.000001
}
valf[i] = f
com := commits2[i]
comLink := "https://code.google.com/p/go/source/detail?r=" + com.Hash
if cert[i] {
d := ""
if lastval0 != 0 {
d = fmt.Sprintf(" (%.02f%%)", perfDiff(lastval0, v))
}
cmpLink := fmt.Sprintf("/perfdetail?commit=%v&builder=%v&benchmark=%v", com.Hash, builder, benchmark)
hh[i] = fmt.Sprintf("%v: <a href='%v'>%v%v</a><br><a href='%v'>%v</a><br>%v", desc, cmpLink, v, d, comLink, com.Desc, com.Time.Format("Jan 2, 2006 1:04"))
} else {
hh[i] = fmt.Sprintf("%v: NO DATA<br><a href='%v'>%v</a><br>%v", desc, comLink, com.Desc, com.Time.Format("Jan 2, 2006 1:04"))
}
lastval = v
if cert[i] {
lastval0 = v
}
}
vals = append(vals, valf)
hints = append(hints, hh)
certainty = append(certainty, cert)
headers = append(headers, fmt.Sprintf("%s (%.2f%% [%.2f%%])", desc, diff, noise))
}
}
}
}
}
var commits []perfGraphCommit
if len(vals) != 0 && len(vals[0]) != 0 {
for i := range vals[0] {
if !commits2[i].NeedsBenchmarking {
continue
}
var c perfGraphCommit
for j := range vals {
c.Vals = append(c.Vals, perfGraphValue{float64(vals[j][i]), certainty[j][i], hints[j][i]})
}
commits = append(commits, c)
}
}
data := &perfGraphData{d, cfg, startCommit, commitsToDisplay, absolute, headers, commits}
var buf bytes.Buffer
if err := perfGraphTemplate.Execute(&buf, data); err != nil {
logErr(w, r, err)
return
}
buf.WriteTo(w)
}
var perfGraphTemplate = template.Must(
template.New("perf_graph.html").ParseFiles("build/perf_graph.html"),
)
type perfGraphData struct {
Dashboard *Dashboard
Config *uiPerfConfig
StartCommit int
CommitNum int
Absolute bool
Headers []string
Commits []perfGraphCommit
}
type perfGraphCommit struct {
Name string
Vals []perfGraphValue
}
type perfGraphValue struct {
Val float64
Certainty bool
Hint string
}

View File

@ -0,0 +1,116 @@
<!doctype html>
<html>
<head>
<title>{{$.Dashboard.Name}} Dashboard</title>
<link rel="stylesheet" href="/static/style.css"/>
<style>
.graph-container { background: #eee; }
</style>
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
<script type="text/javascript">
google.load("visualization", "1", {packages:["corechart"]});
google.setOnLoadCallback(drawCharts);
function drawCharts() {
var data = new google.visualization.DataTable();
data.addColumn('string', 'Commit');
{{range $.Headers}}
data.addColumn('number', '{{.}}');
data.addColumn({type: 'boolean', role: 'certainty'});
data.addColumn({type: 'string', role: 'tooltip', p: {html: true}});
{{end}}
data.addRows([
{{range $.Commits}}
[ '{{.Name}}',
{{range .Vals}}
{{if .Val}}
{{.Val}}, {{.Certainty}}, '{{.Hint}}',
{{else}}
,,,
{{end}}
{{end}}
],
{{end}}
]);
new google.visualization.LineChart(document.getElementById('graph_div')).
draw(data, {
width: "100%",
height: 600,
legend: {position: "bottom"},
tooltip: {isHtml: true},
{{if not $.Absolute}}
vAxis: {textPosition: "none", ticks: [0,10,20,30,40,50,60,70,80,90,100]},
{{end}}
chartArea: {left: "10%", top: "5%", width: "85%", height:"85%"}
})
}
</script>
</head>
<body>
<header id="topbar">
<h1>Go Dashboard</h1>
<nav>
<a href="{{$.Dashboard.RelPath}}">Test</a>
<a href="{{$.Dashboard.RelPath}}perf">Perf</a>
<a href="{{$.Dashboard.RelPath}}perfgraph">Graphs</a>
</nav>
<div class="clear"></div>
</header>
<div class="page">
<div id="graph_div" class="main-content graph-container">
</div>
<aside>
<form>
<div class="panel">
<h1>Builders</h1>
<select required multiple name="builder" size="{{len $.Config.Builders}}">
{{range $.Config.Builders}}
<option {{if .Selected}}selected{{end}}>{{.Name}}</option>
{{end}}
</select>
</div>
<div class="panel">
<h1>Benchmarks</h1>
<select required multiple name="benchmark" size="{{len $.Config.Benchmarks}}">
{{range $.Config.Benchmarks}}
<option {{if .Selected}}selected{{end}}>{{.Name}}</option>
{{end}}
</select>
</div>
<div class="panel">
<h1>Procs</h1>
<select required multiple name="procs" size="{{len $.Config.Procs}}">
{{range $.Config.Procs}}
<option {{if .Selected}}selected{{end}}>{{.Name}}</option>
{{end}}
</select>
</div>
<div class="panel">
<h1>Metrics</h1>
<select required multiple name="metric" size="{{len $.Config.Metrics}}">
{{range $.Config.Metrics}}
<option {{if .Selected}}selected{{end}}>{{.Name}}</option>
{{end}}
</select>
</div>
<input type="checkbox" name="absolute" value="1" {{if $.Absolute}} checked {{end}} >absolute scale</input>
<input type="hidden" name="startcommit" value="{{$.StartCommit}}" />
<input type="hidden" name="commitnum" value="{{$.CommitNum}}" />
<input class="button" type="submit" value="Refresh" name="refresh"/>
<input class="button" type="submit" value="Zoom in" name="zoomin"/>
<input class="button" type="submit" value="Zoom out" name="zoomout"/>
<input class="button" type="submit" value="Newer" name="newer"/>
<input class="button" type="submit" value="Older" name="older"/>
</form>
</aside>
<div class="clear"></div>
</div>
</body>
</html>

View File

@ -0,0 +1,186 @@
// Copyright 2013 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"
"fmt"
"html/template"
"net/http"
"sort"
"appengine"
"appengine/datastore"
)
func init() {
http.HandleFunc("/perflearn", perfLearnHandler)
}
const (
learnPercentile = 0.95
learnSignalMultiplier = 1.2
learnMinSignal = 0.5
)
func perfLearnHandler(w http.ResponseWriter, r *http.Request) {
d := dashboardForRequest(r)
c := d.Context(appengine.NewContext(r))
pc, err := GetPerfConfig(c, r)
if err != nil {
logErr(w, r, err)
return
}
p, err := GetPackage(c, "")
if err != nil {
logErr(w, r, err)
return
}
update := r.FormValue("update") != ""
noise := make(map[string]string)
data := &perfLearnData{}
commits, err := GetCommits(c, 0, p.NextNum)
if err != nil {
logErr(w, r, err)
return
}
for _, builder := range pc.BuildersForBenchmark("") {
for _, benchmark := range pc.BenchmarksForBuilder(builder) {
for _, metric := range pc.MetricsForBenchmark(benchmark) {
for _, procs := range pc.ProcList(builder) {
values, err := GetPerfMetricsForCommits(c, builder, fmt.Sprintf("%v-%v", benchmark, procs), metric, 0, p.NextNum)
if err != nil {
logErr(w, r, err)
return
}
var dd []float64
last := uint64(0)
for i, v := range values {
if v == 0 {
if commits[i].NeedsBenchmarking {
last = 0
}
continue
}
if last != 0 {
v1 := v
if v1 < last {
v1, last = last, v1
}
diff := float64(v1)/float64(last)*100 - 100
dd = append(dd, diff)
}
last = v
}
if len(dd) == 0 {
continue
}
sort.Float64s(dd)
baseIdx := int(float64(len(dd)) * learnPercentile)
baseVal := dd[baseIdx]
signalVal := baseVal * learnSignalMultiplier
if signalVal < learnMinSignal {
signalVal = learnMinSignal
}
signalIdx := -1
noiseNum := 0
signalNum := 0
var diffs []*perfLearnDiff
for i, d := range dd {
if d > 3*signalVal {
d = 3 * signalVal
}
diffs = append(diffs, &perfLearnDiff{Num: i, Val: d})
if signalIdx == -1 && d >= signalVal {
signalIdx = i
}
if d < signalVal {
noiseNum++
} else {
signalNum++
}
}
diffs[baseIdx].Hint = "95%"
if signalIdx != -1 {
diffs[signalIdx].Hint = "signal"
}
diffs = diffs[len(diffs)*4/5:]
name := fmt.Sprintf("%v/%v-%v/%v", builder, benchmark, procs, metric)
data.Entries = append(data.Entries, &perfLearnEntry{len(data.Entries), name, baseVal, noiseNum, signalVal, signalNum, diffs})
if len(dd) >= 100 || r.FormValue("force") != "" {
nname := fmt.Sprintf("%v|%v-%v", builder, benchmark, procs)
n := noise[nname] + fmt.Sprintf("|%v=%.2f", metric, signalVal)
noise[nname] = n
}
}
}
}
}
if update {
var noiseLevels []string
for k, v := range noise {
noiseLevels = append(noiseLevels, k+v)
}
tx := func(c appengine.Context) error {
pc, err := GetPerfConfig(c, r)
if err != nil {
return err
}
pc.NoiseLevels = noiseLevels
if _, err := datastore.Put(c, PerfConfigKey(c), pc); err != nil {
return fmt.Errorf("putting PerfConfig: %v", err)
}
return nil
}
if err := datastore.RunInTransaction(c, tx, nil); err != nil {
logErr(w, r, err)
return
}
}
var buf bytes.Buffer
if err := perfLearnTemplate.Execute(&buf, data); err != nil {
logErr(w, r, err)
return
}
buf.WriteTo(w)
}
var perfLearnTemplate = template.Must(
template.New("perf_learn.html").Funcs(tmplFuncs).ParseFiles("build/perf_learn.html"),
)
type perfLearnData struct {
Entries []*perfLearnEntry
}
type perfLearnEntry struct {
Num int
Name string
BaseVal float64
NoiseNum int
SignalVal float64
SignalNum int
Diffs []*perfLearnDiff
}
type perfLearnDiff struct {
Num int
Val float64
Hint string
}

View File

@ -0,0 +1,45 @@
<!doctype html>
<html>
<head>
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
<script type="text/javascript">
google.load("visualization", "1", {packages:["corechart"]});
google.setOnLoadCallback(drawCharts);
function drawCharts() {
{
{{range $ent := $.Entries}}
var data = new google.visualization.DataTable();
data.addColumn('number', 'idx');
data.addColumn('number', '95%');
data.addColumn({type: 'boolean', role: 'certainty'});
data.addColumn('number', 'signal');
data.addColumn({type: 'boolean', role: 'certainty'});
data.addColumn('number', 'diff');
data.addColumn({type: 'string', role: 'annotation'});
data.addRows([
{{range .Diffs}} [{{.Num}}, {{$ent.BaseVal}}, false, {{$ent.SignalVal}}, false, {{.Val}}, '{{.Hint}}'], {{end}}
]);
new google.visualization.LineChart(document.getElementById('graph{{.Num}}')).
draw(data, {
width: 600,
height: 200,
legend: {position: "none"},
vAxis: {minValue: 0},
chartArea: {left: "10%", top: "1%", width: "90%", height:"95%"}
}
)
{{end}}
}
}
</script>
</head>
<body>
{{range $.Entries}}
<p>
{{.Name}}: base={{printf "%.2f[%d]" .BaseVal .NoiseNum}} signal={{printf "%.2f[%d]" .SignalVal .SignalNum}}
<div id="graph{{.Num}}" width="100px" height="100px"> </div>
</p>
{{end}}
</body>
</html>

View File

@ -0,0 +1,11 @@
Change {{shortHash .Commit.Hash}} caused perf changes on {{.Builder}}:
{{.Commit.Desc}}
http://code.google.com/p/go/source/detail?r={{shortHash .Commit.Hash}}
{{range $b := .Benchmarks}}
{{printf "%-16s %12s %12s %10s" $b.Name "old" "new" "delta"}}
{{range $m := $b.Metrics}}{{printf "%-16s %12v %12v %+10.2f" $m.Name $m.Old $m.New $m.Delta}}
{{end}}{{end}}
{{.Url}}

View File

@ -71,7 +71,7 @@ func uiHandler(w http.ResponseWriter, r *http.Request) {
builders := commitBuilders(commits)
var tipState *TagState
if pkg.Kind == "" && page == 0 {
if pkg.Kind == "" && page == 0 && commits != nil {
// only show sub-repo state on first page of normal repo view
tipState, err = TagStateByName(c, "tip")
if err != nil {
@ -354,7 +354,7 @@ func shortDesc(desc string) string {
if i := strings.Index(desc, "\n"); i != -1 {
desc = desc[:i]
}
return desc
return limitStringLength(desc, 100)
}
// shortHash returns a short version of a hash.

View File

@ -2,72 +2,7 @@
<html>
<head>
<title>{{$.Dashboard.Name}} Build Dashboard</title>
<style>
body {
font-family: sans-serif;
padding: 0; margin: 0;
}
h1, h2 {
margin: 0;
padding: 5px;
}
h1 {
background: #eee;
}
h2 {
margin-top: 20px;
}
.build, .packages {
margin: 5px;
border-collapse: collapse;
}
.build td, .build th, .packages td, .packages th {
vertical-align: top;
padding: 2px 4px;
font-size: 10pt;
}
.build tr.commit:nth-child(2n) {
background-color: #f0f0f0;
}
.build .hash {
font-family: monospace;
font-size: 9pt;
}
.build .result {
text-align: center;
width: 2em;
}
.col-hash, .col-result {
border-right: solid 1px #ccc;
}
.build .arch {
font-size: 66%;
font-weight: normal;
}
.build .time {
color: #666;
}
.build .ok {
font-size: 83%;
}
.build .desc, .build .time, .build .user {
white-space: nowrap;
}
.dashboards, .paginate {
padding: 0.5em;
}
.dashboards > a, .paginate a {
padding: 0.5em;
background: #eee;
color: blue;
}
.paginate a.inactive {
color: #999;
}
.fail {
color: #C00;
}
</style>
<link rel="stylesheet" href="/static/style.css"/>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<script>
var showUnsupported = window.location.hash.substr(1) != "short";
@ -82,8 +17,18 @@
})
</script>
</head>
<body>
<h1>{{$.Dashboard.Name}} Build Status</h1>
<header id="topbar">
<h1>Go Dashboard</h1>
<nav>
<a href="{{$.Dashboard.RelPath}}">Test</a>
<a href="{{$.Dashboard.RelPath}}perf">Perf</a>
<a href="{{$.Dashboard.RelPath}}perfgraph">Graphs</a>
</nav>
<div class="clear"></div>
</header>
<nav class="dashboards">
{{range buildDashboards}}
<a href="{{.RelPath}}">{{.Name}}</a>
@ -95,7 +40,9 @@
</nav>
{{with $.Package.Name}}<h2>{{.}}</h2>{{end}}
{{if $.Commits}}
<div class="page">
{{if $.Commits}}
<table class="build">
<colgroup class="col-hash" {{if $.Package.Path}}span="2"{{end}}></colgroup>
@ -256,5 +203,6 @@
{{end}}
{{end}}
</div>
</body>
</html>

View File

@ -0,0 +1,309 @@
* { box-sizing: border-box; }
.dashboards {
padding: 0.5em;
}
.dashboards a {
padding: 0.5em;
background: #eee;
color: blue;
}
body {
margin: 0;
font-family: sans-serif;
padding: 0; margin: 0;
color: #222;
}
.container {
max-width: 900px;
margin: 0 auto;
}
p, pre, ul, ol { margin: 20px; }
h1, h2, h3, h4 {
margin: 20px 0;
padding: 0;
color: #375EAB;
font-weight: bold;
}
h1 { font-size: 24px; }
h2 { font-size: 20px; }
h3 { font-size: 20px; }
h4 { font-size: 16px; }
h2 { background: #E0EBF5; padding: 2px 5px; }
h3, h4 { margin: 20px 5px; }
dl, dd { font-size: 14px; }
dl { margin: 20px; }
dd { margin: 2px 20px; }
.clear {
clear: both;
}
.button {
padding: 10px;
color: #222;
border: 1px solid #375EAB;
background: #E0EBF5;
border-radius: 5px;
cursor: pointer;
margin-left: 60px;
}
/* navigation bar */
#topbar {
padding: 10px 10px;
background: #E0EBF5;
}
#topbar a {
color: #222;
}
#topbar h1 {
float: left;
margin: 0;
padding-top: 5px;
}
#topbar nav {
float: left;
margin-left: 20px;
}
#topbar nav a {
display: inline-block;
padding: 10px;
margin: 0;
margin-right: 5px;
color: white;
background: #375EAB;
text-decoration: none;
font-size: 16px;
border: 1px solid #375EAB;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
}
.page {
margin-top: 20px;
}
/* settings panels */
aside {
margin-top: 5px;
}
.panel {
border: 1px solid #aaa;
border-radius: 5px;
margin-bottom: 5px;
}
.panel h1 {
font-size: 16px;
margin: 0;
padding: 2px 8px;
}
.panel select {
padding: 5px;
border: 0;
width: 100%;
}
/* results table */
table {
margin: 5px;
border-collapse: collapse;
font-size: 11px;
}
table td, table th, table td, table th {
vertical-align: top;
padding: 2px 6px;
}
table tr:nth-child(2n+1) {
background: #F4F4F4;
}
table thead tr {
background: #fff !important;
}
/* build results */
.build td, .build th, .packages td, .packages th {
vertical-align: top;
padding: 2px 4px;
font-size: 10pt;
}
.build .hash {
font-family: monospace;
font-size: 9pt;
}
.build .result {
text-align: center;
width: 2em;
}
.build .col-hash, .build .col-result, .build .col-metric, .build .col-numresults {
border-right: 1px solid #ccc;
}
.build .row-commit {
border-top: 2px solid #ccc;
}
.build .arch {
font-size: 83%;
font-weight: normal;
}
.build .time {
color: #666;
}
.build .ok {
font-size: 83%;
}
.build .desc, .build .date, .build .user {
white-space: nowrap;
}
.build .desc {
text-align: left;
max-width: 470px;
overflow: hidden;
text-overflow: ellipsis;
}
.good { text-decoration: none; text-shadow: 1px 1px 0 #BBF8AB; color: #000000; background: #38FF38;}
.bad { text-decoration: none; text-shadow: 1px 1px 0 #000000; color: #FFFFFF; background: #E70000;}
.noise { text-decoration: none; color: #888; }
.fail { color: #C00; }
/* pagination */
.paginate nav {
text-align: center;
padding: 0.5em;
margin: 10px 0;
}
.paginate nav a {
padding: 0.5em;
background: #E0EBF5;
color: blue;
-webkit-border-radius: 5px;
-moz-border-radius: 5px;
border-radius: 5px;
}
.paginate nav a.inactive {
color: #888;
cursor: default;
text-decoration: none;
}
/* diffs */
.diff-meta {
font-family: monospace;
margin-bottom: 10px;
}
.diff-container {
padding: 10px;
}
.diff table .metric {
font-weight: bold;
}
.diff {
border: 1px solid #aaa;
border-radius: 5px;
margin-bottom: 5px;
margin-right: 10px;
float: left;
}
.diff h1 {
font-size: 16px;
margin: 0;
padding: 2px 8px;
}
.diff-benchmark {
clear: both;
padding-top: 5px;
}
/* positioning elements */
.page {
position: relative;
width: 100%;
}
aside {
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 200px;
}
.main-content {
position: absolute;
top: 0;
left: 210px;
right: 5px;
min-height: 200px;
overflow: hidden;
}
@media only screen and (max-width: 900px) {
aside {
position: relative;
display: block;
width: auto;
}
.main-content {
position: static;
padding: 0;
}
aside .panel {
float: left;
width: auto;
margin-right: 5px;
}
aside .button {
float: left;
margin: 0;
}
}