blob: 713a0af54f8a5e4aadd831778f85c646e9d858bf [file] [log] [blame]
// 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
}