| // 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" | |
| "slices" | |
| "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 any | |
| }{ | |
| 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 [][]any `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 [][]any | |
| 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, []any{ | |
| 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 _, v := range slices.Backward(cl.Metas) { | |
| if !strings.Contains(v.Commit.Msg, "autogenerated:gerrit") { | |
| continue | |
| } | |
| if strings.Contains(v.Commit.Msg, "autogenerated:gerrit:merged") || | |
| strings.Contains(v.Commit.Msg, "autogenerated:gerrit:abandon") { | |
| interval.end = v.Commit.CommitTime.Unix() | |
| } | |
| } | |
| if interval.end == math.MaxInt64 { | |
| log.Printf("Unable to determine close time of CL: %+v", cl) | |
| } | |
| } | |
| return interval | |
| } |