Skip to content

Commit 7fadfb8

Browse files
committed
internal/dash: build.golang.org dashboard reading
Needs tests. Change-Id: I66fa350d99c8defb597a110f25ced7119960a2d6 Reviewed-on: https://go-review.googlesource.com/c/build/+/432400 Reviewed-by: Cherry Mui <[email protected]> Run-TryBot: Russ Cox <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent 841a76c commit 7fadfb8

File tree

1 file changed

+344
-0
lines changed

1 file changed

+344
-0
lines changed

internal/dash/dash.go

+344
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,344 @@
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

Comments
 (0)