golang.org/x/build@v0.0.0-20240506185731-218518f32b70/internal/dash/dash.go (about)

     1  // Copyright 2022 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 dash reads build.golang.org's dashboards.
     6  package dash
     7  
     8  import (
     9  	"encoding/json"
    10  	"fmt"
    11  	"io"
    12  	"log"
    13  	"net/http"
    14  	"net/url"
    15  	"sort"
    16  	"sync"
    17  	"time"
    18  )
    19  
    20  // A Board is a single dashboard.
    21  type Board struct {
    22  	Repo      string   // repo being displayed: "go", "arch", and so on
    23  	Branch    string   // branch in repo
    24  	Builders  []string // builder columns
    25  	Revisions []*Line  // commit lines, newest to oldest
    26  }
    27  
    28  // A Line is a single commit line on a Board b.
    29  type Line struct {
    30  	Repo       string    // same as b.Repo
    31  	Branch     string    // same as b.Branch
    32  	Revision   string    // revision of Repo
    33  	GoRevision string    // for Repo != "go", revision of go repo being used
    34  	GoBranch   string    // for Repo != "go", branch of go repo being used
    35  	Date       time.Time // date of commit
    36  	Author     string    // author of commit
    37  	Desc       string    // commit description
    38  
    39  	// // Results[i] reports b.Builders[i]'s result:
    40  	// "" (not run), "ok" (passed), or the URL of the failure log
    41  	// ("https://build.golang.org/log/...")
    42  	Results []string
    43  }
    44  
    45  // Read reads and returns all the dashboards on build.golang.org
    46  // (for the main repo, the main repo release branches, and subrepos),
    47  // including all results up to the given time limit.
    48  // It guarantees that all the returned boards will have the same b.Builders slices,
    49  // so that any line.Results[i] even for different boards refers to a consistent
    50  // builder for a given i.
    51  func Read(limit time.Time) ([]*Board, error) {
    52  	return Update(nil, limit)
    53  }
    54  
    55  // Update is like Read but takes a starting set of boards from
    56  // a previous call to Read or Update and avoids redownloading
    57  // information from those boards.
    58  // It does not modify the boards passed in as input.
    59  func Update(old []*Board, limit time.Time) ([]*Board, error) {
    60  	// Read the front page to derive the Go repo branches and subrepos.
    61  	_, goBranches, repos, err := readPage("", "", 0)
    62  	if err != nil {
    63  		return nil, err
    64  	}
    65  	repos = append([]string{"go"}, repos...)
    66  
    67  	// Build cache of existing boards.
    68  	type key struct{ repo, branch string }
    69  	cache := make(map[key]*Board)
    70  	for _, b := range old {
    71  		cache[key{b.Repo, b.Branch}] = b
    72  	}
    73  
    74  	// For each repo and branch, fetch that repo's list of board pages.
    75  	var boards []*Board
    76  	var errors []error
    77  	var wg sync.WaitGroup
    78  	for _, r := range repos {
    79  		r := r
    80  		branches := []string{""}
    81  		if r == "go" {
    82  			branches = goBranches
    83  		}
    84  		for _, branch := range branches {
    85  			branch := branch
    86  			if branch == "master" || branch == "main" {
    87  				branch = ""
    88  			}
    89  			// Only read up to what we already have in old, respecting limit.
    90  			old := cache[key{r, branch}]
    91  			oldLimit := limit
    92  			if old != nil && len(old.Revisions) > 0 && old.Revisions[0].Date.After(limit) {
    93  				oldLimit = old.Revisions[0].Date
    94  			}
    95  			i := len(boards)
    96  			boards = append(boards, nil)
    97  			errors = append(errors, nil)
    98  			wg.Add(1)
    99  			go func() {
   100  				defer wg.Done()
   101  				boards[i], errors[i] = readRepo(r, branch, oldLimit)
   102  				if errors[i] == nil {
   103  					boards[i] = update(boards[i], old, limit)
   104  				}
   105  			}()
   106  		}
   107  	}
   108  	wg.Wait()
   109  
   110  	for _, err := range errors {
   111  		if err != nil {
   112  			return nil, err
   113  		}
   114  	}
   115  
   116  	// Remap all the boards to have a consistent Builders array.
   117  	// It is slightly inefficient that readRepo does this remap as well,
   118  	// but all the downloads take more time.
   119  	remap(boards)
   120  
   121  	return boards, nil
   122  }
   123  
   124  // update returns the result of merging b and old,
   125  // discarding revisions older than limit and removing duplicates.
   126  // It modifies b but not old.
   127  func update(b, old *Board, limit time.Time) *Board {
   128  	if old == nil || !same(b.Builders, old.Builders) {
   129  		if old == nil {
   130  			old = new(Board)
   131  		} else {
   132  			old = old.clone()
   133  		}
   134  		remap([]*Board{b, old})
   135  	}
   136  
   137  	type key struct {
   138  		rev   string
   139  		gorev string
   140  	}
   141  	have := make(map[key]bool)
   142  	keep := b.Revisions[:0]
   143  	for _, list := range [][]*Line{b.Revisions, old.Revisions} {
   144  		for _, r := range list {
   145  			if !r.Date.Before(limit) && !have[key{r.Revision, r.GoRevision}] {
   146  				have[key{r.Revision, r.GoRevision}] = true
   147  				keep = append(keep, r)
   148  			}
   149  		}
   150  	}
   151  	b.Revisions = keep
   152  	return b
   153  }
   154  
   155  // clone returns a deep copy of b.
   156  func (b *Board) clone() *Board {
   157  	b1 := &Board{
   158  		Repo:      b.Repo,
   159  		Branch:    b.Branch,
   160  		Builders:  make([]string, len(b.Builders)),
   161  		Revisions: make([]*Line, len(b.Revisions)),
   162  	}
   163  	copy(b1.Builders, b.Builders)
   164  	for i := range b1.Revisions {
   165  		r := new(Line)
   166  		*r = *b.Revisions[i]
   167  		results := make([]string, len(r.Results))
   168  		copy(results, r.Results)
   169  		r.Results = results
   170  		b1.Revisions[i] = r
   171  	}
   172  	return b1
   173  }
   174  
   175  // readRepo reads and returns the pages for the given repo and branch,
   176  // stopping when it finds a page that contains no results newer than limit.
   177  func readRepo(repo, branch string, limit time.Time) (*Board, error) {
   178  	path := ""
   179  	if repo != "go" {
   180  		path = "golang.org/x/" + repo
   181  	}
   182  	var pages []*Board
   183  	for page := 0; ; page++ {
   184  		b, _, _, err := readPage(path, branch, page)
   185  		if err != nil {
   186  			return merge(pages), err
   187  		}
   188  
   189  		// If there's nothing new enough on the whole page, stop.
   190  		keep := b.Revisions[:0]
   191  		for _, r := range b.Revisions {
   192  			if !r.Date.Before(limit) {
   193  				keep = append(keep, r)
   194  			}
   195  		}
   196  		if len(keep) == 0 {
   197  			break
   198  		}
   199  		b.Revisions = keep
   200  		b.Repo = repo
   201  		b.Branch = branch
   202  		pages = append(pages, b)
   203  	}
   204  	return merge(pages), nil
   205  }
   206  
   207  // merge merges all the pages into a single board.
   208  func merge(pages []*Board) *Board {
   209  	if len(pages) == 0 {
   210  		return new(Board)
   211  	}
   212  
   213  	remap(pages)
   214  	for _, b := range pages {
   215  		if !same(b.Builders, pages[0].Builders) || b.Repo != pages[0].Repo || b.Branch != pages[0].Branch {
   216  			panic("misuse of merge")
   217  		}
   218  	}
   219  
   220  	merged := &Board{Repo: pages[0].Repo, Branch: pages[0].Branch, Builders: pages[0].Builders}
   221  	for _, b := range pages {
   222  		merged.Revisions = append(merged.Revisions, b.Revisions...)
   223  	}
   224  	return merged
   225  }
   226  
   227  // remap remaps all the results in all the boards
   228  // to use a consistent set of Builders.
   229  func remap(boards []*Board) {
   230  	// Collect list of all builders across all boards.
   231  	var builders []string
   232  	index := make(map[string]int)
   233  	for _, b := range boards {
   234  		for _, builder := range b.Builders {
   235  			if index[builder] == 0 {
   236  				index[builder] = 1
   237  				builders = append(builders, builder)
   238  			}
   239  		}
   240  	}
   241  	sort.Strings(builders)
   242  	for i, builder := range builders {
   243  		index[builder] = i
   244  	}
   245  
   246  	// Remap.
   247  	for _, b := range boards {
   248  		for _, r := range b.Revisions {
   249  			results := make([]string, len(builders))
   250  			for i, ok := range r.Results {
   251  				results[index[b.Builders[i]]] = ok
   252  			}
   253  			r.Results = results
   254  		}
   255  		b.Builders = builders
   256  	}
   257  }
   258  
   259  // readPage reads the build.golang.org page for repo, branch.
   260  // It returns the board on that page.
   261  // When repo == "go" and branch == "" and page == 0,
   262  // build.golang.org also sends back information about the
   263  // other go repo branches and the subrepos.
   264  // readPage("go", "", 0) returns those lists of go branches
   265  // and subrepos as extra results.
   266  func readPage(repo, branch string, page int) (b *Board, branches, repos []string, err error) {
   267  	if repo == "" {
   268  		repo = "go"
   269  	}
   270  	u := "https://build.golang.org/?mode=json&repo=" + url.QueryEscape(repo) + "&branch=" + url.QueryEscape(branch) + "&page=" + fmt.Sprint(page)
   271  	log.Printf("read %v", u)
   272  	resp, err := http.Get(u)
   273  	if err != nil {
   274  		return nil, nil, nil, fmt.Errorf("%s page %d: %v", repo, page, err)
   275  	}
   276  	data, err := io.ReadAll(resp.Body)
   277  	resp.Body.Close()
   278  	if err != nil {
   279  		return nil, nil, nil, fmt.Errorf("%s page %d: %v", repo, page, err)
   280  	}
   281  	if resp.StatusCode != 200 {
   282  		return nil, nil, nil, fmt.Errorf("%s page %d: %s\n%s", repo, page, resp.Status, data)
   283  	}
   284  
   285  	b = new(Board)
   286  	if err := json.Unmarshal(data, b); err != nil {
   287  		return nil, nil, nil, fmt.Errorf("%s page %d: %v", repo, page, err)
   288  	}
   289  
   290  	// Use empty string consistently to denote master/main branch.
   291  	for _, r := range b.Revisions {
   292  		if r.Branch == "master" || r.Branch == "main" {
   293  			r.Branch = ""
   294  		}
   295  		if r.GoBranch == "master" || r.GoBranch == "main" {
   296  			r.GoBranch = ""
   297  		}
   298  	}
   299  
   300  	// https://build.golang.org/?mode=json (main repo, no branch, page 0)
   301  	// sends back a bit about the subrepos too. Filter that out.
   302  	if repo == "go" {
   303  		var save []*Line
   304  		for _, r := range b.Revisions {
   305  			if r.Repo == "go" {
   306  				save = append(save, r)
   307  			} else {
   308  				branches = append(branches, r.GoBranch)
   309  				repos = append(repos, r.Repo)
   310  			}
   311  		}
   312  		b.Revisions = save
   313  		branches = uniq(branches)
   314  		repos = uniq(repos)
   315  	}
   316  
   317  	return b, branches, repos, nil
   318  }
   319  
   320  // same reports whether x and y are the same slice.
   321  func same(x, y []string) bool {
   322  	if len(x) != len(y) {
   323  		return false
   324  	}
   325  	for i, s := range x {
   326  		if y[i] != s {
   327  			return false
   328  		}
   329  	}
   330  	return true
   331  }
   332  
   333  // uniq sorts and removes duplicates from list, returning the result.
   334  // uniq reuses list's storage for its result.
   335  func uniq(list []string) []string {
   336  	sort.Strings(list)
   337  	keep := list[:0]
   338  	for _, s := range list {
   339  		if len(keep) == 0 || s != keep[len(keep)-1] {
   340  			keep = append(keep, s)
   341  		}
   342  	}
   343  	return keep
   344  }