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  }