Skip to content

Commit 5a057a2

Browse files
committed
devapp: add per-release issue tracker dashboard
This imports https://swtch.com/tmp/dash.html and makes it render for any release. Initially, the only graph is the # of open issues by milestone over the course of the release. The dashboard is not currently tracking label history, which is needed to draw the second graph on that page. Change-Id: I9bd031f8709701b304e18208ae3c972bdfe3b276 Reviewed-on: https://go-review.googlesource.com/30012 Reviewed-by: Brad Fitzpatrick <[email protected]>
1 parent a64934f commit 5a057a2

File tree

4 files changed

+235
-20
lines changed

4 files changed

+235
-20
lines changed

devapp/app.yaml

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ handlers:
2626
static_dir: static
2727
application_readable: true
2828
secure: always
29-
- url: /(|dash|release|cl|stats/raw|stats/svg)
29+
- url: /(|dash|release|cl|stats/raw|stats/svg|stats/release|stats/release/data.js)
3030
script: _go_app
3131
secure: always
3232
- url: /update.*

devapp/devapp.go

+3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ func init() {
3838
// Defined in stats.go
3939
http.HandleFunc("/stats/raw", rawHandler)
4040
http.HandleFunc("/stats/svg", svgHandler)
41+
http.Handle("/stats/release", ctxHandler(release))
42+
http.Handle("/stats/release/data.js", ctxHandler(releaseData))
4143
http.Handle("/update/stats", ctxHandler(updateStats))
4244
}
4345

@@ -125,6 +127,7 @@ func update(ctx context.Context, w http.ResponseWriter, _ *http.Request) error {
125127
if cls {
126128
data.PrintCLs(&output)
127129
} else {
130+
fmt.Fprintf(&output, fmt.Sprintf(`<a href="/stats/release?cycle=%d">Go 1.%d Issue Stats Dashboard</a>`, data.GoReleaseCycle, data.GoReleaseCycle))
128131
data.PrintIssues(&output)
129132
}
130133
var html bytes.Buffer

devapp/stats.go

+162-19
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,15 @@ package devapp
77
import (
88
"encoding/json"
99
"fmt"
10+
"html/template"
1011
"image/color"
12+
"io/ioutil"
1113
"math"
1214
"net/http"
1315
"regexp"
1416
"sort"
1517
"strconv"
18+
"strings"
1619
"time"
1720

1821
"golang.org/x/build/godash"
@@ -69,6 +72,37 @@ func updateStats(ctx context.Context, w http.ResponseWriter, r *http.Request) er
6972
return writeCache(ctx, "gzstats", stats)
7073
}
7174

75+
func release(ctx context.Context, w http.ResponseWriter, req *http.Request) error {
76+
req.ParseForm()
77+
78+
tmpl, err := ioutil.ReadFile("template/release.html")
79+
if err != nil {
80+
return err
81+
}
82+
83+
t, err := template.New("main").Parse(string(tmpl))
84+
if err != nil {
85+
return err
86+
}
87+
88+
cycle, _, err := argtoi(req, "cycle")
89+
if err != nil {
90+
return err
91+
}
92+
if cycle == 0 {
93+
data, err := loadData(ctx)
94+
if err != nil {
95+
return err
96+
}
97+
cycle = data.GoReleaseCycle
98+
}
99+
100+
if err := t.Execute(w, struct{ GoReleaseCycle int }{cycle}); err != nil {
101+
return err
102+
}
103+
return nil
104+
}
105+
72106
func rawHandler(w http.ResponseWriter, r *http.Request) {
73107
ctx := appengine.NewContext(r)
74108

@@ -143,19 +177,23 @@ func (s countChangeSlice) Less(i, j int) bool { return s[i].t.Before(s[j].t) }
143177
// were open at each time. This produces columns called "Time" and
144178
// "Count".
145179
type openCount struct {
146-
// ByRelease will add a Release column and provide counts per release.
147-
ByRelease bool
180+
// By is the column to group by; if "" all issues will be
181+
// grouped together. Only "Release" and "Milestone" are
182+
// supported.
183+
By string
148184
}
149185

150186
func (o openCount) F(input table.Grouping) table.Grouping {
151187
return table.MapTables(input, func(_ table.GroupID, t *table.Table) *table.Table {
152-
releases := make(map[string]countChangeSlice)
188+
groups := make(map[string]countChangeSlice)
153189
add := func(milestone string, t time.Time, count int) {
154-
r := milestoneToRelease(milestone)
155-
if r == "" {
156-
r = milestone
190+
if o.By == "Release" {
191+
r := milestoneToRelease(milestone)
192+
if r != "" {
193+
milestone = r
194+
}
157195
}
158-
releases[r] = append(releases[r], countChange{t, count})
196+
groups[milestone] = append(groups[milestone], countChange{t, count})
159197
}
160198

161199
created := t.MustColumn("Created").([]time.Time)
@@ -189,9 +227,9 @@ func (o openCount) F(input table.Grouping) table.Grouping {
189227

190228
var times []time.Time
191229
var counts []int
192-
if o.ByRelease {
230+
if o.By != "" {
193231
var names []string
194-
for name, s := range releases {
232+
for name, s := range groups {
195233
sort.Sort(s)
196234
sum := 0
197235
for _, c := range s {
@@ -201,10 +239,10 @@ func (o openCount) F(input table.Grouping) table.Grouping {
201239
counts = append(counts, sum)
202240
}
203241
}
204-
nt.Add("Release", names)
242+
nt.Add(o.By, names)
205243
} else {
206244
var all countChangeSlice
207-
for _, s := range releases {
245+
for _, s := range groups {
208246
all = append(all, s...)
209247
}
210248
sort.Sort(all)
@@ -308,25 +346,34 @@ func plot(w http.ResponseWriter, req *http.Request, stats table.Grouping) error
308346
}
309347
switch pivot := req.Form.Get("pivot"); pivot {
310348
case "opencount":
311-
byRelease := req.Form.Get("group") == "release"
312-
plot.Stat(openCount{ByRelease: byRelease})
313-
if byRelease {
314-
plot.GroupBy("Release")
349+
o := openCount{}
350+
switch by := req.Form.Get("group"); by {
351+
case "release":
352+
o.By = "Release"
353+
case "milestone":
354+
o.By = "Milestone"
355+
case "":
356+
default:
357+
return fmt.Errorf("unknown group %q", by)
358+
}
359+
plot.Stat(o)
360+
if o.By != "" {
361+
plot.GroupBy(o.By)
315362
}
316363
plot.SortBy("Time")
317364
lp := gg.LayerPaths{
318365
X: "Time",
319366
Y: "Count",
320367
}
321-
if byRelease {
322-
lp.Color = "Release"
368+
if o.By != "" {
369+
lp.Color = o.By
323370
}
324371
plot.Add(gg.LayerSteps{LayerPaths: lp})
325-
if byRelease {
372+
if o.By != "" {
326373
plot.Add(gg.LayerTooltips{
327374
X: "Time",
328375
Y: "Count",
329-
Label: "Release",
376+
Label: o.By,
330377
})
331378
}
332379
case "":
@@ -456,3 +503,99 @@ func plot(w http.ResponseWriter, req *http.Request, stats table.Grouping) error
456503
plot.WriteSVG(w, 1200, 600)
457504
return nil
458505
}
506+
507+
func releaseData(ctx context.Context, w http.ResponseWriter, req *http.Request) error {
508+
req.ParseForm()
509+
510+
stats := &godash.Stats{}
511+
if err := loadCache(ctx, "gzstats", stats); err != nil {
512+
return err
513+
}
514+
515+
cycle, _, err := argtoi(req, "cycle")
516+
if err != nil {
517+
return err
518+
}
519+
if cycle == 0 {
520+
data, err := loadData(ctx)
521+
if err != nil {
522+
return err
523+
}
524+
cycle = data.GoReleaseCycle
525+
}
526+
527+
prefix := fmt.Sprintf("Go1.%d", cycle)
528+
529+
w.Header().Set("Content-Type", "application/javascript")
530+
531+
g := gdstats.IssueStats(stats)
532+
g = openCount{By: "Milestone"}.F(g)
533+
g = table.Filter(g, func(m string) bool { return strings.HasPrefix(m, prefix) }, "Milestone")
534+
g = table.SortBy(g, "Time")
535+
536+
// Dump data; remember that each row only affects one count, so we need to hold the counts from the previous row. Kind of like Pivot.
537+
data := [][]interface{}{{"Date"}}
538+
counts := make(map[string]int)
539+
var (
540+
maxt time.Time
541+
maxc int
542+
)
543+
for _, gid := range g.Tables() {
544+
// Find all the milestones that exist
545+
ms := g.Table(gid).MustColumn("Milestone").([]string)
546+
for _, m := range ms {
547+
counts[m] = 0
548+
}
549+
// Find the peak of the graph
550+
ts := g.Table(gid).MustColumn("Time").([]time.Time)
551+
cs := g.Table(gid).MustColumn("Count").([]int)
552+
for i, c := range cs {
553+
if c > maxc {
554+
maxc = c
555+
maxt = ts[i]
556+
}
557+
}
558+
}
559+
560+
// Only show the most recent 6 months of data.
561+
start := maxt.Add(time.Duration(-6 * 30 * 24 * time.Hour))
562+
g = table.Filter(g, func(t time.Time) bool { return t.After(start) }, "Time")
563+
564+
milestones := []string{prefix + "Early", prefix, prefix + "Maybe"}
565+
for m := range counts {
566+
switch m {
567+
case prefix + "Early", prefix, prefix + "Maybe":
568+
default:
569+
milestones = append(milestones, m)
570+
}
571+
}
572+
for _, m := range milestones {
573+
data[0] = append(data[0], m)
574+
}
575+
for _, gid := range g.Tables() {
576+
t := g.Table(gid)
577+
time := t.MustColumn("Time").([]time.Time)
578+
milestone := t.MustColumn("Milestone").([]string)
579+
count := t.MustColumn("Count").([]int)
580+
for i := range time {
581+
counts[milestone[i]] = count[i]
582+
row := []interface{}{time[i].UnixNano() / 1e6}
583+
for _, m := range milestones {
584+
row = append(row, counts[m])
585+
}
586+
data = append(data, row)
587+
}
588+
}
589+
fmt.Fprintf(w, "var ReleaseData = ")
590+
if err := json.NewEncoder(w).Encode(data); err != nil {
591+
return err
592+
}
593+
fmt.Fprintf(w, ";\n")
594+
fmt.Fprintf(w, `
595+
ReleaseData.map(function(row, i) {
596+
if (i > 0) {
597+
row[0] = new Date(row[0])
598+
}
599+
});`)
600+
return nil
601+
}

devapp/template/release.html

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
5+
<script type="text/javascript">
6+
google.load("visualization", "1", {packages:["corechart"]});
7+
google.setOnLoadCallback(drawCharts);
8+
function drawCharts() {
9+
var data = google.visualization.arrayToDataTable(ReleaseData);
10+
var options = {
11+
title: 'Go 1.{{.GoReleaseCycle}} Release Issues',
12+
isStacked: true,
13+
width: 1100, height: 450,
14+
vAxis: {minValue: 0},
15+
focusTarget: 'category',
16+
series: [
17+
// TODO: What if we change the set of labels? How to map these more intelligently?
18+
{color: '#008'}, // Early
19+
{color: '#44c'}, // Release
20+
{color: '#ccc'}, // Maybe
21+
]
22+
};
23+
var chart = new google.visualization.AreaChart(document.getElementById('ReleaseDiv'));
24+
chart.draw(data, options);
25+
26+
var data = google.visualization.arrayToDataTable(TriageData);
27+
var options = {
28+
title: 'Issue Progress',
29+
isStacked: true,
30+
width: 1100, height: 450,
31+
vAxis: {minValue: 0},
32+
focusTarget: 'category',
33+
series: [
34+
{color: '#c00'}, // Triage needed
35+
{color: '#cc0', lineDashStyle: [4, 4]}, // NeedsInvestigation
36+
{color: '#ee4', lineDashStyle: [4, 4]}, // NeedsInvestigation+Waiting
37+
{color: '#ff8', lineDashStyle: [4, 4]}, // NeedsInvestigation+Blocked
38+
{color: '#0a0', lineDashStyle: [4, 4]}, // NeedsDecision
39+
{color: '#4d4', lineDashStyle: [4, 4]}, // NeedsDecision+Waiting
40+
{color: '#8f8', lineDashStyle: [4, 4]}, // NeedsDecision+Blocked
41+
{color: '#00c', lineDashStyle: [4, 4]}, // NeedsFix
42+
{color: '#44e', lineDashStyle: [4, 4]}, // NeedsFix+Waiting
43+
{color: '#88f', lineDashStyle: [4, 4]}, // NeedsFix+Blocked
44+
]
45+
};
46+
var chart = new google.visualization.AreaChart(document.getElementById('TriageDiv'));
47+
chart.draw(data, options);
48+
}
49+
function myDate(s) {
50+
return new Date(s)
51+
}
52+
</script>
53+
<script type="text/javascript" src="/stats/release/data.js?cycle={{.GoReleaseCycle}}"></script>
54+
55+
<style>
56+
body { font-family: sans-serif; }
57+
h1 { text-align: center; }
58+
</style>
59+
</head>
60+
61+
<body>
62+
63+
<h1>Go 1.{{.GoReleaseCycle}} Issue Tracker Dashboard</h1>
64+
65+
<div id="ReleaseDiv"></div>
66+
67+
<div id="TriageDiv"></div>
68+
69+
</body>

0 commit comments

Comments
 (0)