go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/aggrmetrics/pm.go (about)

     1  // Copyright 2021 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 aggrmetrics
    16  
    17  import (
    18  	"context"
    19  
    20  	"go.chromium.org/luci/common/errors"
    21  	"go.chromium.org/luci/common/logging"
    22  	"go.chromium.org/luci/common/retry/transient"
    23  	"go.chromium.org/luci/common/tsmon/field"
    24  	"go.chromium.org/luci/common/tsmon/metric"
    25  	"go.chromium.org/luci/common/tsmon/types"
    26  	"go.chromium.org/luci/gae/service/datastore"
    27  
    28  	"go.chromium.org/luci/cv/internal/prjmanager"
    29  )
    30  
    31  var (
    32  	metricPMStateCLs = metric.NewInt(
    33  		"cv/internal/pm/cls",
    34  		"Number of tracked CLs(# of vertices in the CL graph).",
    35  		nil,
    36  		field.String("project"),
    37  	)
    38  	metricPMStateDeps = metric.NewInt(
    39  		"cv/internal/pm/deps",
    40  		"Number of tracked CL dependencies (# of edges in the CL graph)",
    41  		nil,
    42  		field.String("project"),
    43  	)
    44  	metricPMStateComponents = metric.NewInt(
    45  		"cv/internal/pm/components",
    46  		"Number of tracked CL components (~ connected components in the CL graph)",
    47  		nil,
    48  		field.String("project"),
    49  	)
    50  	metricPMEntitySize = metric.NewInt(
    51  		"cv/internal/pm/entity_size",
    52  		"Approximate size of the Project entity in Datastore",
    53  		&types.MetricMetadata{Units: types.Bytes},
    54  		field.String("project"),
    55  	)
    56  )
    57  
    58  // maxProjects is the max number of projects to which the pmReporter will
    59  // scale in its current implementation.
    60  //
    61  // If a bigger amount is desired, batching should be implemented to avoid
    62  // high memory usage.
    63  const maxProjects = 500
    64  
    65  type pmReporter struct{}
    66  
    67  // metrics implements aggregator interface.
    68  func (*pmReporter) metrics() []types.Metric {
    69  	return []types.Metric{
    70  		metricPMStateCLs,
    71  		metricPMStateDeps,
    72  		metricPMStateComponents,
    73  		metricPMEntitySize,
    74  	}
    75  }
    76  
    77  // report implements aggregator interface.
    78  func (*pmReporter) report(ctx context.Context, projects []string) error {
    79  	if len(projects) > maxProjects {
    80  		logging.Errorf(ctx, "FIXME: too many active projects (>%d) to report metrics for", maxProjects)
    81  		return errors.New("too many active projects")
    82  	}
    83  
    84  	// We need to load & decode Datastore entities as prjmanager.Project objects
    85  	// in order to compute metrics like the number of components.
    86  	// But we also need to figure out the Datastore entities' size in bytes, which
    87  	// is unfortunately not easy to compute off the prjmanager.Project objects.
    88  	//
    89  	// So, load each project entity as both prjmanager.Project and the
    90  	// projectEntitySizeEstimator objects. Note, that both objects have
    91  	// datastore's Kind is exactly the same (see `gae:"$kind..."` attribute in
    92  	// both structs.
    93  	//
    94  	// Finally, behind the scenes, datastore library should optimize this to load
    95  	// only N entities instead of 2*N.
    96  	eSizes := make([]projectEntitySizeEstimator, len(projects))
    97  	entities := make([]prjmanager.Project, len(projects))
    98  	i := 0
    99  	for _, p := range projects {
   100  		eSizes[i].ID = p
   101  		entities[i].ID = p
   102  		i++
   103  	}
   104  	err := datastore.Get(ctx, eSizes, entities)
   105  	if err != nil {
   106  		return errors.Annotate(err, "failed to fetch all projects").Tag(transient.Tag).Err()
   107  	}
   108  
   109  	for i, e := range entities {
   110  		project := eSizes[i].ID
   111  		metricPMEntitySize.Set(ctx, eSizes[i].bytes, project)
   112  		metricPMStateCLs.Set(ctx, int64(len(e.State.GetPcls())), project)
   113  		metricPMStateComponents.Set(ctx, int64(len(e.State.GetComponents())), project)
   114  		deps := 0
   115  		for _, pcl := range e.State.GetPcls() {
   116  			deps += len(pcl.GetDeps())
   117  		}
   118  		metricPMStateDeps.Set(ctx, int64(deps), project)
   119  	}
   120  	return nil
   121  }
   122  
   123  type projectEntitySizeEstimator struct {
   124  	// Kind must match the kind of the prjmanager.Project entity.
   125  	_kind string `gae:"$kind,Project"`
   126  	ID    string `gae:"$id"`
   127  	bytes int64
   128  }
   129  
   130  // Load implements datastore.PropertyLoadSaver.
   131  func (e *projectEntitySizeEstimator) Load(m datastore.PropertyMap) error {
   132  	e.bytes = m.EstimateSize()
   133  	return nil
   134  }
   135  
   136  // Save implements datastore.PropertyLoadSaver.
   137  func (e *projectEntitySizeEstimator) Save(withMeta bool) (datastore.PropertyMap, error) {
   138  	panic("not needed")
   139  }