blob: 713a0af54f8a5e4aadd831778f85c646e9d858bf [file] [log] [blame]
Andrew Bonventre1389a952019-10-07 09:58:01 -04001// Copyright 2019 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package main
6
7import (
8 "bytes"
9 "html/template"
10 "io"
11 "log"
12 "math"
13 "net/http"
14 "strings"
15 "time"
16
17 "golang.org/x/build/maintner"
18)
19
20// handleStats serves dev.golang.org/stats.
21func (s *server) handleStats(t *template.Template, w http.ResponseWriter, r *http.Request) {
22 s.cMu.RLock()
23 dirty := s.data.stats.dirty
24 s.cMu.RUnlock()
25 if dirty {
26 s.updateStatsData()
27 }
28
29 w.Header().Set("Content-Type", "text/html; charset=utf-8")
30 var buf bytes.Buffer
31 s.cMu.RLock()
32 defer s.cMu.RUnlock()
33 data := struct {
34 DataJSON interface{}
35 }{
36 DataJSON: s.data.stats,
37 }
38 if err := t.Execute(&buf, data); err != nil {
39 http.Error(w, err.Error(), http.StatusInternalServerError)
40 return
41 }
42 if _, err := io.Copy(w, &buf); err != nil {
43 log.Printf("io.Copy(w, %+v) = %v", buf, err)
44 return
45 }
46}
47
48type statsData struct {
49 Charts []*chart
50
51 // dirty is set if this data needs to be updated due to a corpus change.
52 dirty bool
53}
54
55// A chart holds data used by the Google Charts JavaScript API to render
56// an interactive visualization.
57type chart struct {
58 Title string `json:"title"`
59 Columns []*chartColumn `json:"columns"`
60 Data [][]interface{} `json:"data"`
61}
62
63// A chartColumn is analogous to a Google Charts DataTable column.
64type chartColumn struct {
65 // Type is the data type of the values of the column.
66 // Supported values are 'string', 'number', 'boolean',
67 // 'timeofday', 'date', and 'datetime'.
68 Type string `json:"type"`
69
70 // Label is an optional label for the column.
71 Label string `json:"label"`
72}
73
74func (s *server) updateStatsData() {
75 log.Println("Updating stats data ...")
76 s.cMu.Lock()
77 defer s.cMu.Unlock()
78
79 var (
80 windowStart = time.Now().Add(-1 * 365 * 24 * time.Hour)
81 intervals []*clInterval
82 )
83 s.corpus.Gerrit().ForeachProjectUnsorted(filterProjects(func(p *maintner.GerritProject) error {
84 p.ForeachCLUnsorted(withoutDeletedCLs(p, func(cl *maintner.GerritCL) error {
85 closed := cl.Status == "merged" || cl.Status == "abandoned"
86
87 // Discard CL if closed and last updated before windowStart.
88 if closed && cl.Meta.Commit.CommitTime.Before(windowStart) {
89 return nil
90 }
91 intervals = append(intervals, newIntervalFromCL(cl))
92 return nil
93 }))
94 return nil
95 }))
96
97 var chartData [][]interface{}
98 for t0, t1 := windowStart, windowStart.Add(24*time.Hour); t0.Before(time.Now()); t0, t1 = t0.Add(24*time.Hour), t1.Add(24*time.Hour) {
99 var (
100 open int
101 withIssues int
102 )
103
104 for _, i := range intervals {
105 if !i.intersects(t0, t1) {
106 continue
107 }
108 open++
109 if len(i.cl.GitHubIssueRefs) > 0 {
110 withIssues++
111 }
112 }
113 chartData = append(chartData, []interface{}{
114 t0, open, withIssues,
115 })
116 }
117 cols := []*chartColumn{
118 {Type: "date", Label: "date"},
119 {Type: "number", Label: "All CLs"},
120 {Type: "number", Label: "With issues"},
121 }
122 var charts []*chart
123 charts = append(charts, &chart{
124 Title: "Open CLs (1 Year)",
125 Columns: cols,
126 Data: chartData,
127 })
128 charts = append(charts, &chart{
129 Title: "Open CLs (30 Days)",
130 Columns: cols,
131 Data: chartData[len(chartData)-30:],
132 })
133 charts = append(charts, &chart{
134 Title: "Open CLs (7 Days)",
135 Columns: cols,
136 Data: chartData[len(chartData)-7:],
137 })
138 s.data.stats.Charts = charts
139}
140
141// A clInterval describes a time period during which a CL is open.
142// points on the interval are seconds since the epoch.
143type clInterval struct {
144 start, end int64 // seconds since epoch
145 cl *maintner.GerritCL
146}
147
148// returns true iff the interval contains any seconds
149// in the timespan [t0,t1]. t0 must be before t1.
150func (i *clInterval) intersects(t0, t1 time.Time) bool {
151 if t1.Before(t0) {
152 panic("t0 cannot be before t1")
153 }
154 return i.end >= t0.Unix() && i.start <= t1.Unix()
155}
156
157func newIntervalFromCL(cl *maintner.GerritCL) *clInterval {
158 interval := &clInterval{
159 start: cl.Created.Unix(),
160 end: math.MaxInt64,
161 cl: cl,
162 }
163
164 closed := cl.Status == "merged" || cl.Status == "abandoned"
165 if closed {
166 for i := len(cl.Metas) - 1; i >= 0; i-- {
167 if !strings.Contains(cl.Metas[i].Commit.Msg, "autogenerated:gerrit") {
168 continue
169 }
170
171 if strings.Contains(cl.Metas[i].Commit.Msg, "autogenerated:gerrit:merged") ||
172 strings.Contains(cl.Metas[i].Commit.Msg, "autogenerated:gerrit:abandon") {
173 interval.end = cl.Metas[i].Commit.CommitTime.Unix()
174 }
175 }
176 if interval.end == math.MaxInt64 {
177 log.Printf("Unable to determine close time of CL: %+v", cl)
178 }
179 }
180 return interval
181}