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 }