зеркало из https://github.com/golang/build.git
182 строки
4.5 KiB
Go
182 строки
4.5 KiB
Go
// 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
|
|
}
|