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  }