|
| 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 | +} |
0 commit comments