]> Cypherpunks repositories - gostls13.git/commitdiff
dashboard: present benchmarks
authorRuss Cox <rsc@golang.org>
Fri, 5 Feb 2010 10:58:40 +0000 (02:58 -0800)
committerRuss Cox <rsc@golang.org>
Fri, 5 Feb 2010 10:58:40 +0000 (02:58 -0800)
  * fix bug in benchmark collection: bad benchmark data key
    meant that all the builders collided when writing data.
  * report benchmarks even if make bench exits non-zero.
  * graphical and json presentations

R=agl1
CC=golang-dev
https://golang.org/cl/201065

misc/dashboard/builder.sh
misc/dashboard/godashboard/app.yaml
misc/dashboard/godashboard/benchmark1.html [new file with mode: 0644]
misc/dashboard/godashboard/benchmarks.html [new file with mode: 0644]
misc/dashboard/godashboard/gobuild.py
misc/dashboard/godashboard/main.html

index 0eaed8b344c50b2cc37af0351934092f89598bae..fb2e6defb5ab0a1e2d7457e34b0ece74b1448a46 100644 (file)
@@ -69,9 +69,7 @@ while true ; do
             echo "Running benchmarks"
             cd pkg || fatal "failed to cd to pkg"
             make bench > ../../benchmarks 2>&1
-            if [ $? -eq 0 ] ; then
-                python ../../../buildcontrol.py benchmarks $BUILDER $rev ../../benchmarks || fatal "Cannot record benchmarks"
-            fi
+            python ../../../buildcontrol.py benchmarks $BUILDER $rev ../../benchmarks || fatal "Cannot record benchmarks"
             cd .. || fatal "failed to cd out of pkg"
         fi
     fi
index 06681def10bf5a78a722055e22e6adda7547215f..ec4d8d9c103f4d000329a8bbb5ff21d07494b20e 100644 (file)
@@ -1,5 +1,5 @@
 application: godashboard
-version: 1
+version: 3
 runtime: python
 api_version: 1
 
diff --git a/misc/dashboard/godashboard/benchmark1.html b/misc/dashboard/godashboard/benchmark1.html
new file mode 100644 (file)
index 0000000..e174b24
--- /dev/null
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <title>Go benchmarks</title>
+
+    <style>
+      h1 {
+          font-size: 1em;
+          font-weight: bold;
+      }
+      h2 {
+          font-size: 1em;
+          font-weight: bold;
+      }
+      table.alternate {
+          white-space: nowrap;
+      }
+      table.alternate tr td {
+        padding-right: 10px;
+      }
+      table.alternate tr td:last-child {
+        padding-right: 0;
+      }
+      table.alternate tr:nth-child(2n) {
+        background-color: #f8f8f8;
+      }
+      td.revision {
+      }
+      span.hash {
+        font-family: monospace;
+        font-size: small;
+        color: #aaa;
+      }
+      span.nodeseq {
+      }
+      td.user {
+        font-size: small;
+      }
+      td.date {
+        color: #aaa;
+        font-size: small;
+      }
+      td.result {
+        text-align: center;
+      }
+      span.ok {
+      }
+      td.desc {
+        font-size: small;
+        font-family: sans-serif;
+      }
+      th.builder {
+        font-weight: bold;
+        padding-right: 0.5em;
+      }
+    </style>
+  </head>
+
+  <body>
+    <h1>Go dashboard - {{benchmark}}</h1>
+    
+    <a href="/">build status</a> <br>
+    <a href="/benchmarks">benchmarks</a>
+
+    <h2>{{benchmark}}</h2>
+    <a href="{{benchmark}}?fmt=json">json</a>
+
+    {% for g in graphs %}
+      <h3>{{g.builder}}</h3>
+      {% if g.url %}
+        <img src="{{g.url}}&chs=600x150&chf=bg,s,00000000&chco=000000ff&chls=1,1,0">
+      {% else %}
+        (no data available)
+      {% endif %}
+    {% endfor %}
+    
+    <br><br>
+    
+    <table class="alternate" cellpadding="0" cellspacing="0">
+      <tr>
+        <th></th>
+        {% for b in builders %}
+          <th class="builder">{{b.goos}}<br>{{b.goarch}}<br>{{b.note}}</th>
+        {% endfor %}
+        <th></th>
+        <th></th>
+        <th></th>
+      </tr>
+
+      {% for r in revs %}
+      <tr>
+        <td class="revision"><span class="hash"><a href="https://code.google.com/p/go/source/detail?r={{r.node}}">{{r.node|slice:":12"}}</a></span></td>
+
+        {% for ns in r.ns_by_builder %}
+          <td class="result">
+          {% if ns %}
+            {{ns}}
+          {% endif %}
+          </td>
+        {% endfor %}
+        <td class="user">{{r.user|escape}}</td>
+        <td class="date">{{r.date|escape}}</td>
+        <td class="desc">{{r.shortdesc|escape}}</td>
+      </tr>
+      {% endfor %}
+  </body>
+</html>
diff --git a/misc/dashboard/godashboard/benchmarks.html b/misc/dashboard/godashboard/benchmarks.html
new file mode 100644 (file)
index 0000000..044bccc
--- /dev/null
@@ -0,0 +1,83 @@
+<!DOCTYPE HTML>
+<html>
+  <head>
+    <title>Go benchmarks</title>
+
+    <style>
+      h1 {
+          font-size: 1em;
+          font-weight: bold;
+      }
+      h2 {
+          font-size: 1em;
+          font-weight: bold;
+      }
+      table.alternate {
+          white-space: nowrap;
+      }
+      table.alternate tr td {
+        padding-right: 10px;
+      }
+      table.alternate tr td:last-child {
+        padding-right: 0;
+      }
+      table.alternate tr:nth-child(2n) {
+        background-color: #f8f8f8;
+      }
+      td.revision {
+      }
+      span.nodeseq {
+      }
+      td.user {
+        font-size: small;
+      }
+      td.date {
+        color: #aaa;
+        font-size: small;
+      }
+      td.result {
+        text-align: center;
+      }
+      span.ok {
+      }
+      td.desc {
+        font-size: small;
+        font-family: sans-serif;
+      }
+      th.builder {
+        font-weight: bold;
+        padding-right: 0.5em;
+      }
+    </style>
+  </head>
+
+  <body>
+    <h1>Go dashboard - benchmarks</h1>
+    
+    <a href="/">build status</a>
+
+    <h2>Benchmarks</h2>
+    <table class="alternate" cellpadding="0" cellspacing="0">
+      <tr>
+        <th></th>
+        {% for b in builders %}
+          <th class="builder">{{b.goos}}<br>{{b.goarch}}<br>{{b.note}}</th>
+        {% endfor %}
+      </tr>
+
+      {% for m in benchmarks %}
+      <tr>
+        <td class="name"><a href="/benchmarks/{{m.name}}">{{m.name}}</a></td>
+
+        {% for b in m.builds %}
+          <td class="result">
+          {% if b.url %}
+            <img src="{{b.url}}&chs=80x20&chf=bg,s,00000000&chco=000000ff&chls=1,1,0">
+          {% endif %}
+          </td>
+        {% endfor %}
+      </tr>
+      {% endfor %}
+    </table>
+  </body>
+</html>
index 32f95ca3d8c11136e09dd49b78cb4dd0e274c914..c10d92dbdb8513b2ed16ac19c877114bf82afdcf 100644 (file)
@@ -5,6 +5,7 @@
 # This is the server part of the continuous build system for Go. It must be run
 # by AppEngine.
 
+from google.appengine.api import memcache
 from google.appengine.ext import db
 from google.appengine.ext import webapp
 from google.appengine.ext.webapp import template
@@ -18,6 +19,7 @@ import os
 import re
 import struct
 
+# local imports
 import key
 
 # The majority of our state are commit objects. One of these exists for each of
@@ -44,6 +46,7 @@ class Commit(db.Model):
 
 class Benchmark(db.Model):
     name = db.StringProperty()
+    version = db.IntegerProperty()
 
 class BenchmarkResult(db.Model):
     num = db.IntegerProperty()
@@ -64,6 +67,15 @@ class Highwater(db.Model):
 
 N = 30
 
+def builderInfo(b):
+    f = b.split('-', 3)
+    goos = f[0]
+    goarch = f[1]
+    note = ""
+    if len(f) > 2:
+        note = f[2]
+    return {'name': b, 'goos': goos, 'goarch': goarch, 'note': note}
+
 class MainPage(webapp.RequestHandler):
     def get(self):
         self.response.headers['Content-Type'] = 'text/html; charset=utf-8'
@@ -77,13 +89,7 @@ class MainPage(webapp.RequestHandler):
 
         for r in revs:
             for b in r['builds']:
-                f = b['builder'].split('-', 3)
-                goos = f[0]
-                goarch = f[1]
-                note = ""
-                if len(f) > 2:
-                    note = f[2]
-                builders[b['builder']] = {'goos': goos, 'goarch': goarch, 'note': note}
+                builders[b['builder']] = builderInfo(b['builder'])
 
         for r in revs:
             have = set(x['builder'] for x in r['builds'])
@@ -91,7 +97,6 @@ class MainPage(webapp.RequestHandler):
             for n in need:
                 r['builds'].append({'builder': n, 'log':'', 'ok': False})
             r['builds'].sort(cmp = byBuilder)
-            r['shortdesc'] = r['desc'].split('\n', 2)[0]
 
         builders = list(builders.items())
         builders.sort()
@@ -269,22 +274,131 @@ class Build(webapp.RequestHandler):
         self.response.set_status(200)
 
 class Benchmarks(webapp.RequestHandler):
-    def get(self):
+    def json(self):
         q = Benchmark.all()
+        q.filter('__key__ >', Benchmark.get_or_insert('v002.').key())
         bs = q.fetch(10000)
 
         self.response.set_status(200)
-        self.response.headers['Content-Type'] = 'application/json; charset=utf-8'
-        self.response.out.write('{"benchmarks": [\n')
+        self.response.headers['Content-Type'] = 'text/plain; charset=utf-8'
+        self.response.out.write('{"benchmarks": [')
 
         first = True
+        sep = "\n\t"
         for b in bs:
-            if not first:
-                self.response.out.write(',"' + b.name + '"\n')
-            else:
-                self.response.out.write('"' + b.name + '"\n')
-                first = False
-        self.response.out.write(']}\n')
+            self.response.out.write('%s"%s"' % (sep, b.name))
+            sep = ",\n\t"
+        self.response.out.write('\n]}\n')
+
+    def get(self):
+        if self.request.get('fmt') == 'json':
+            return self.json()
+        self.response.set_status(200)
+        self.response.headers['Content-Type'] = 'text/html; charset=utf-8'
+        q = Commit.all()
+        q.order('-__key__')
+        n = q.fetch(1)[0]
+        key = "bench(%d)" % n.num
+        page = None # memcache.get(key)
+        if not page:
+            page = self.compute()
+            memcache.set(key, page, 3600)
+        self.response.out.write(page)
+    
+    def compute(self):
+        q = Benchmark.all()
+        q.filter('__key__ >', Benchmark.get_or_insert('v002.').key())
+        bs = q.fetch(10000)
+        
+        # Collect table giving all the data we need.
+        builders = {}
+        data = {}
+        for b in bs:
+            # TODO(rsc): Will want to limit benchmarks to a certain
+            # number of commits eventually, but there aren't enough
+            # commits yet to worry.
+            q = BenchmarkResult.all()
+            q.ancestor(b)
+            q.order('-__key__')
+            results = q.fetch(10000)
+            m = {}
+            revs = {}
+            for r in results:
+                if r.builder not in m:
+                    m[r.builder] = {}
+                m[r.builder][r.num] = r.nsperop
+                revs[r.num] = 0
+                builders[r.builder] = 0
+            data[b.name] = m
+
+        builders = list(builders.keys())
+        builders.sort()
+
+        revs = list(revs.keys())
+        revs.sort()
+        first = revs[0]
+        last = revs[-1]
+        if len(revs) > 80:   # At most 80 commits back
+            last = revs[-80]
+
+        names = list(data.keys())
+        names.sort()
+
+        # Build list of rows, one per benchmark
+        benchmarks = []
+        for name in names:
+            # Build list of cells, one per builder.
+            m = data[name]
+            builds = []
+            for builder in builders:
+                # Build cell: a URL for the chart server or an empty string.
+                if builder not in m:
+                    builds.append({"url":""})
+                    continue
+                d = m[builder]
+                max = 0
+                tot = 0
+                ntot = 0
+                for i in range(first, last+1):
+                    if i not in d:
+                        continue
+                    val = d[i]
+                    if max < val:
+                        max = val
+                    tot += val
+                    ntot += 1
+                if max == 0:
+                    builds.append({"url":""})
+                    continue
+                avg = tot / ntot
+                if 2*avg > max:
+                    max = 2*avg
+                # Encoding is 0-61, which is fine enough granularity for our tiny graphs.  _ means missing.
+                encoding = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
+                s = ""
+                for i in range(first, last+1):
+                    if i not in d:
+                        s += "_"
+                        continue
+                    val = d[i]
+                    s += encoding[int((len(encoding)-1)*val/max)]
+                builds.append({"url": "http://chart.apis.google.com/chart?cht=ls&chd=s:"+s})
+            benchmarks.append({"name": name, "builds": builds})
+
+        bs = []
+        for b in builders:
+            f = b.split('-', 3)
+            goos = f[0]
+            goarch = f[1]
+            note = ""
+            if len(f) > 2:
+                note = f[2]
+            bs.append({'goos': goos, 'goarch': goarch, 'note': note})
+
+        values = {"benchmarks": benchmarks, "builders": bs}
+
+        path = os.path.join(os.path.dirname(__file__), 'benchmarks.html')
+        return template.render(path, values)
 
     def post(self):
         if not auth(self.request):
@@ -327,24 +441,29 @@ class Benchmarks(webapp.RequestHandler):
             return
 
         for (benchmark, (iterations, time)) in benchmarks.items():
-            b = Benchmark.get_or_insert(benchmark.encode('base64'), name = benchmark)
-            r = BenchmarkResult(key_name = '%08x/builder' % n.num, parent = b, num = n.num, iterations = iterations, nsperop = time, builder = builder)
+            b = Benchmark.get_or_insert('v002.' + benchmark.encode('base64'), name = benchmark, version = 2)
+            r = BenchmarkResult(key_name = '%08x/%s' % (n.num, builder), parent = b, num = n.num, iterations = iterations, nsperop = time, builder = builder)
             r.put()
-
+        key = "bench(%d)" % n.num
+        memcache.delete(key)
         self.response.set_status(200)
 
+def node(num):
+    q = Commit.all()
+    q.filter('num =', num)
+    n = q.get()
+    return n
+
 class GetBenchmarks(webapp.RequestHandler):
     def get(self):
-        self.response.headers['Content-Type'] = 'application/json; charset=utf-8'
-        benchmark = self.request.path[12:].decode('hex').encode('base64')
-
-        b = Benchmark.get_by_key_name(benchmark)
-        if b is None:
+        benchmark = self.request.path[12:]
+        bm = Benchmark.get_by_key_name('v002.' + benchmark.encode('base64'))
+        if bm is None:
             self.response.set_status(404)
             return
 
         q = BenchmarkResult.all()
-        q.ancestor(b)
+        q.ancestor(bm)
         q.order('-__key__')
         results = q.fetch(10000)
 
@@ -352,26 +471,86 @@ class GetBenchmarks(webapp.RequestHandler):
             self.response.set_status(404)
             return
 
-        max = -1
-        min = 2000000000
+        maxv = -1
+        minv = 2000000000
         builders = set()
         for r in results:
-            if max < r.num:
-                max = r.num
-            if min > r.num:
-                min = r.num
+            if maxv < r.num:
+                maxv = r.num
+            if minv > r.num:
+                minv = r.num
             builders.add(r.builder)
 
         res = {}
         for b in builders:
-            res[b] = [[-1] * ((max - min) + 1), [-1] * ((max - min) + 1)]
+            res[b] = [[-1] * ((maxv - minv) + 1), [-1] * ((maxv - minv) + 1)]
 
         for r in results:
-            res[r.builder][0][r.num - min] = r.iterations
-            res[r.builder][1][r.num - min] = r.nsperop
-
-        self.response.out.write(str(res))
+            res[r.builder][0][r.num - minv] = r.iterations
+            res[r.builder][1][r.num - minv] = r.nsperop
+        
+        minhash = node(minv).node
+        maxhash = node(maxv).node
+        if self.request.get('fmt') == 'json':
+            self.response.headers['Content-Type'] = 'text/plain; charset=utf-8'
+            self.response.out.write('{"min": "%s", "max": "%s", "data": {' % (minhash, maxhash))
+            sep = "\n\t"
+            for b in builders:
+                self.response.out.write('%s"%s": {"iterations": %s, "nsperop": %s}' % (sep, b, str(res[b][0]).replace("L", ""), str(res[b][1]).replace("L", "")))
+                sep = ",\n\t"
+            self.response.out.write("\n}}\n")
+            return
 
+        def bgraph(builder):
+            data = res[builder][1]
+            encoding = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.-"
+            m = max(data)  # max ns timing
+            if m == -1:
+                return ""
+            tot = 0
+            ntot = 0
+            for d in data:
+                if d < 0:
+                    continue
+                tot += d
+                ntot += 1
+            avg = tot / ntot
+            if 2*avg > m:
+                m = 2*avg
+            s = ""
+            for d in data:
+                if d < 0:
+                    s += "__"
+                    continue
+                val = int(d*4095.0/m)
+                s += encoding[val/64] + encoding[val%64]
+            return "http://chart.apis.google.com/chart?cht=lc&chxt=x,y&chxl=0:|%s|%s|1:|0|%g ns|%g ns&chd=e:%s" % (minhash[0:12], maxhash[0:12], m/2, m, s)
+            
+        graphs = []
+        for b in builders:
+            graphs.append({"builder": b, "url": bgraph(b)})
+        
+        revs = []
+        for i in range(minv, maxv+1):
+            r = nodeInfo(node(i))
+            ns = []
+            for b in builders:
+                t = res[b][1][i - minv]
+                if t < 0:
+                    t = None
+                ns.append(t)
+            r["ns_by_builder"] = ns
+            revs.append(r)
+        
+        path = os.path.join(os.path.dirname(__file__), 'benchmark1.html')
+        data = {
+            "benchmark": bm.name,
+            "builders": [builderInfo(b) for b in builders],
+            "graphs": graphs,
+            "revs": revs
+        }
+        self.response.out.write(template.render(path, data))
+        
 class FixedOffset(datetime.tzinfo):
     """Fixed offset in minutes east from UTC."""
 
@@ -430,13 +609,19 @@ def parseBuild(build):
     [builder, logblob] = build.split('`')
     return {'builder': builder, 'log': logblob, 'ok': len(logblob) == 0}
 
+def nodeInfo(c):
+    return {
+        "node": c.node,
+        "user": toUsername(c.user),
+        "date": dateToShortStr(c.date),
+        "desc": c.desc,
+        "shortdesc": c.desc.split('\n', 2)[0]
+    }
+
 def toRev(c):
-        b = { "node": c.node,
-              "user": toUsername(c.user),
-              "date": dateToShortStr(c.date),
-              "desc": c.desc}
-        b['builds'] = [parseBuild(build) for build in c.builds]
-        return b
+    b = nodeInfo(c)
+    b['builds'] = [parseBuild(build) for build in c.builds]
+    return b
 
 def byBuilder(x, y):
     return cmp(x['builder'], y['builder'])
index 388149ec312489d2b18f1b256a57fb5d9eea4584..7ba9aeed9894b1149a6416d48e5679ea538c50a1 100644 (file)
@@ -59,6 +59,8 @@
   <body>
     <h1>Go dashboard</h1>
     
+    <a href="benchmarks">benchmarks</a>
+    
     <h2>Build status</h2>
     <table class="alternate" cellpadding="0" cellspacing="0">
       <tr>