go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/milo/internal/buildsource/console.go (about) 1 // Copyright 2017 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package buildsource 16 17 import ( 18 "context" 19 "encoding/hex" 20 "fmt" 21 22 "go.chromium.org/luci/gae/service/datastore" 23 24 "go.chromium.org/luci/common/errors" 25 "go.chromium.org/luci/common/logging" 26 "go.chromium.org/luci/common/sync/parallel" 27 28 "go.chromium.org/luci/milo/frontend/ui" 29 "go.chromium.org/luci/milo/internal/model" 30 "go.chromium.org/luci/milo/internal/projectconfig" 31 "go.chromium.org/luci/milo/internal/utils" 32 projectconfigpb "go.chromium.org/luci/milo/proto/projectconfig" 33 ) 34 35 // ConsoleRow is one row of a particular console. 36 // 37 // It has the git commit for the row, as well as a mapping of column index to 38 // the Builds associated with it for this commit. The columns are defined by the 39 // order of the Builder messages in the Console config message (one column per 40 // Builder message). 41 // 42 // Builds is a map since most commit rows have a small subset of the available 43 // builders. 44 type ConsoleRow struct { 45 Commit string 46 Builds map[int][]*model.BuildSummary 47 } 48 49 // GetConsoleRows returns a row-oriented collection of BuildSummary 50 // objects. Each row corresponds to the similarly-indexed commit in the 51 // `commits` slice. 52 func GetConsoleRows(c context.Context, project string, console *projectconfigpb.Console, commits []string) ([]*ConsoleRow, error) { 53 rawCommits := make([][]byte, len(commits)) 54 for i, c := range commits { 55 var err error 56 if rawCommits[i], err = hex.DecodeString(c); err != nil { 57 return nil, errors.Annotate(err, "bad commit[%d]: %q", i, c).Err() 58 } 59 } 60 61 // Maps all builderIDs to the indexes of the columns it appears in. 62 columnMap := map[string][]int{} 63 for columnIdx, b := range console.Builders { 64 bidString := utils.LegacyBuilderIDString(b.Id) 65 columnMap[bidString] = append(columnMap[bidString], columnIdx) 66 } 67 68 ret := make([]*ConsoleRow, len(commits)) 69 url := console.RepoUrl 70 // HACK(iannucci): This little hack should be removed when console definitions 71 // no longer use a manifest name of "REVISION". REVISION was used to index the 72 // 'got_revision' value before manifests were implemented. 73 if console.ManifestName == "REVISION" { 74 url = "" 75 } 76 partialKey := model.NewPartialManifestKey(project, console.Id, console.ManifestName, url) 77 q := datastore.NewQuery("BuildSummary") 78 err := parallel.WorkPool(4, func(ch chan<- func() error) { 79 for i := range rawCommits { 80 i := i 81 r := &ConsoleRow{Commit: commits[i]} 82 ret[i] = r 83 ch <- func() error { 84 fullQ := q.Eq("ManifestKeys", partialKey.AddRevision(rawCommits[i])) 85 return datastore.Run(c, fullQ, func(bs *model.BuildSummary) { 86 if bs.Experimental && !console.IncludeExperimentalBuilds { 87 return 88 } 89 if columnIdxs, ok := columnMap[bs.BuilderID]; ok { 90 if r.Builds == nil { 91 r.Builds = map[int][]*model.BuildSummary{} 92 } 93 for _, columnIdx := range columnIdxs { 94 r.Builds[columnIdx] = append(r.Builds[columnIdx], bs) 95 } 96 } 97 }) 98 } 99 } 100 }) 101 102 return ret, err 103 } 104 105 // GetConsoleSummariesFromDefs returns a map of consoleID -> summary from the 106 // datastore using a slice of console definitions as input. 107 // 108 // This expects all builders in all consoles coming from the same projectID. 109 func GetConsoleSummariesFromDefs(c context.Context, consoleEnts []*projectconfig.Console, projectID string) ( 110 map[projectconfig.ConsoleID]*ui.BuilderSummaryGroup, error) { 111 112 // Maps consoleID -> console config definition. 113 consoles := make(map[projectconfig.ConsoleID]*projectconfigpb.Console, len(consoleEnts)) 114 115 // Maps the BuilderID to the per-console pointer-to-summary in the summaries 116 // map. Note that builders with multiple builderIDs in the same console will 117 // all map to the same BuilderSummary. 118 columns := map[BuilderID]map[projectconfig.ConsoleID][]*model.BuilderSummary{} 119 120 // The return result. 121 summaries := map[projectconfig.ConsoleID]*ui.BuilderSummaryGroup{} 122 123 for _, ent := range consoleEnts { 124 cid := ent.ConsoleID() 125 consoles[cid] = &ent.Def 126 127 summaries[cid] = &ui.BuilderSummaryGroup{ 128 Builders: make([]*model.BuilderSummary, len(ent.Def.Builders)), 129 Name: ui.NewLink( 130 ent.ID, 131 fmt.Sprintf("/p/%s/g/%s/console", ent.ProjectID(), ent.ID), 132 fmt.Sprintf("Console %s in project %s", ent.ID, ent.ProjectID()), 133 ), 134 } 135 136 for i, column := range ent.Def.Builders { 137 s := &model.BuilderSummary{ 138 BuilderID: utils.LegacyBuilderIDString(column.Id), 139 ProjectID: projectID, 140 } 141 summaries[cid].Builders[i] = s 142 name := BuilderID(utils.LegacyBuilderIDString(column.Id)) 143 // Find/populate the BuilderID -> {console: summary} 144 colMap, ok := columns[name] 145 if !ok { 146 colMap = map[projectconfig.ConsoleID][]*model.BuilderSummary{} 147 columns[name] = colMap 148 } 149 150 colMap[cid] = append(colMap[cid], s) 151 } 152 } 153 154 // Now grab ALL THE DATA. 155 bs := make([]*model.BuilderSummary, 0, len(columns)) 156 for builderID := range columns { 157 bs = append(bs, &model.BuilderSummary{ 158 BuilderID: string(builderID), 159 // TODO: change builder ID format to include project id. 160 ProjectID: projectID, 161 }) 162 } 163 if err := datastore.Get(c, bs); err != nil { 164 me := err.(errors.MultiError) 165 lme := errors.NewLazyMultiError(len(me)) 166 for i, ierr := range me { 167 if ierr == datastore.ErrNoSuchEntity { 168 logging.Infof(c, "Missing builder: %s", bs[i].BuilderID) 169 ierr = nil // ignore ErrNoSuchEntity 170 bs[i] = nil // nil out the BuilderSummary, want to skip this below 171 } 172 lme.Assign(i, ierr) 173 } 174 175 // Return an error only if we encounter an error other than datastore.ErrNoSuchEntity. 176 if err := lme.Get(); err != nil { 177 return nil, err 178 } 179 } 180 181 // Now we have the mapping from BuilderID -> summaries, and ALL THE DATA, map 182 // the data back into the summaries. 183 for _, summary := range bs { 184 if summary == nil { // We got ErrNoSuchEntity above 185 continue 186 } 187 188 for cid, curSummaries := range columns[BuilderID(summary.BuilderID)] { 189 cons := consoles[cid] 190 191 // If this console doesn't show experimental builds, skip all summaries of 192 // experimental builds. 193 if !cons.IncludeExperimentalBuilds && summary.LastFinishedExperimental { 194 continue 195 } 196 197 for _, curSummary := range curSummaries { 198 // If the new summary's build was created before the current summary's 199 // build, skip it. 200 if summary.LastFinishedCreated.Before((*curSummary).LastFinishedCreated) { 201 continue 202 } 203 204 // Looks like this is the best summary for this slot so far, so save it. 205 *curSummary = *summary 206 } 207 } 208 } 209 210 return summaries, nil 211 }