| // Copyright 2019 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. |
| |
| package main |
| |
| import ( |
| "bytes" |
| "html/template" |
| "io" |
| "log" |
| "math" |
| "net/http" |
| "strings" |
| "time" |
| |
| "golang.org/x/build/maintner" |
| ) |
| |
| // handleStats serves dev.golang.org/stats. |
| func (s *server) handleStats(t *template.Template, w http.ResponseWriter, r *http.Request) { |
| s.cMu.RLock() |
| dirty := s.data.stats.dirty |
| s.cMu.RUnlock() |
| if dirty { |
| s.updateStatsData() |
| } |
| |
| w.Header().Set("Content-Type", "text/html; charset=utf-8") |
| var buf bytes.Buffer |
| s.cMu.RLock() |
| defer s.cMu.RUnlock() |
| data := struct { |
| DataJSON interface{} |
| }{ |
| DataJSON: s.data.stats, |
| } |
| if err := t.Execute(&buf, data); err != nil { |
| http.Error(w, err.Error(), http.StatusInternalServerError) |
| return |
| } |
| if _, err := io.Copy(w, &buf); err != nil { |
| log.Printf("io.Copy(w, %+v) = %v", buf, err) |
| return |
| } |
| } |
| |
| type statsData struct { |
| Charts []*chart |
| |
| // dirty is set if this data needs to be updated due to a corpus change. |
| dirty bool |
| } |
| |
| // A chart holds data used by the Google Charts JavaScript API to render |
| // an interactive visualization. |
| type chart struct { |
| Title string `json:"title"` |
| Columns []*chartColumn `json:"columns"` |
| Data [][]interface{} `json:"data"` |
| } |
| |
| // A chartColumn is analogous to a Google Charts DataTable column. |
| type chartColumn struct { |
| // Type is the data type of the values of the column. |
| // Supported values are 'string', 'number', 'boolean', |
| // 'timeofday', 'date', and 'datetime'. |
| Type string `json:"type"` |
| |
| // Label is an optional label for the column. |
| Label string `json:"label"` |
| } |
| |
| func (s *server) updateStatsData() { |
| log.Println("Updating stats data ...") |
| s.cMu.Lock() |
| defer s.cMu.Unlock() |
| |
| var ( |
| windowStart = time.Now().Add(-1 * 365 * 24 * time.Hour) |
| intervals []*clInterval |
| ) |
| s.corpus.Gerrit().ForeachProjectUnsorted(filterProjects(func(p *maintner.GerritProject) error { |
| p.ForeachCLUnsorted(withoutDeletedCLs(p, func(cl *maintner.GerritCL) error { |
| closed := cl.Status == "merged" || cl.Status == "abandoned" |
| |
| // Discard CL if closed and last updated before windowStart. |
| if closed && cl.Meta.Commit.CommitTime.Before(windowStart) { |
| return nil |
| } |
| intervals = append(intervals, newIntervalFromCL(cl)) |
| return nil |
| })) |
| return nil |
| })) |
| |
| var chartData [][]interface{} |
| 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) { |
| var ( |
| open int |
| withIssues int |
| ) |
| |
| for _, i := range intervals { |
| if !i.intersects(t0, t1) { |
| continue |
| } |
| open++ |
| if len(i.cl.GitHubIssueRefs) > 0 { |
| withIssues++ |
| } |
| } |
| chartData = append(chartData, []interface{}{ |
| t0, open, withIssues, |
| }) |
| } |
| cols := []*chartColumn{ |
| {Type: "date", Label: "date"}, |
| {Type: "number", Label: "All CLs"}, |
| {Type: "number", Label: "With issues"}, |
| } |
| var charts []*chart |
| charts = append(charts, &chart{ |
| Title: "Open CLs (1 Year)", |
| Columns: cols, |
| Data: chartData, |
| }) |
| charts = append(charts, &chart{ |
| Title: "Open CLs (30 Days)", |
| Columns: cols, |
| Data: chartData[len(chartData)-30:], |
| }) |
| charts = append(charts, &chart{ |
| Title: "Open CLs (7 Days)", |
| Columns: cols, |
| Data: chartData[len(chartData)-7:], |
| }) |
| s.data.stats.Charts = charts |
| } |
| |
| // A clInterval describes a time period during which a CL is open. |
| // points on the interval are seconds since the epoch. |
| type clInterval struct { |
| start, end int64 // seconds since epoch |
| cl *maintner.GerritCL |
| } |
| |
| // returns true iff the interval contains any seconds |
| // in the timespan [t0,t1]. t0 must be before t1. |
| func (i *clInterval) intersects(t0, t1 time.Time) bool { |
| if t1.Before(t0) { |
| panic("t0 cannot be before t1") |
| } |
| return i.end >= t0.Unix() && i.start <= t1.Unix() |
| } |
| |
| func newIntervalFromCL(cl *maintner.GerritCL) *clInterval { |
| interval := &clInterval{ |
| start: cl.Created.Unix(), |
| end: math.MaxInt64, |
| cl: cl, |
| } |
| |
| closed := cl.Status == "merged" || cl.Status == "abandoned" |
| if closed { |
| for i := len(cl.Metas) - 1; i >= 0; i-- { |
| if !strings.Contains(cl.Metas[i].Commit.Msg, "autogenerated:gerrit") { |
| continue |
| } |
| |
| if strings.Contains(cl.Metas[i].Commit.Msg, "autogenerated:gerrit:merged") || |
| strings.Contains(cl.Metas[i].Commit.Msg, "autogenerated:gerrit:abandon") { |
| interval.end = cl.Metas[i].Commit.CommitTime.Unix() |
| } |
| } |
| if interval.end == math.MaxInt64 { |
| log.Printf("Unable to determine close time of CL: %+v", cl) |
| } |
| } |
| return interval |
| } |