golang.org/x/build@v0.0.0-20240506185731-218518f32b70/devapp/stats.go (about) 1 // 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 5 package main 6 7 import ( 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. 21 func (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 48 type 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. 57 type 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. 64 type 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 74 func (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. 143 type 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. 150 func (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 157 func 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 }