| // Copyright 2024 Google Inc. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| // Package execution_metrics represents the metrics system for Android Platform Build Systems. |
| package execution_metrics |
| |
| // This is the main heart of the metrics system for Android Platform Build Systems. |
| // The starting of the soong_ui (cmd/soong_ui/main.go), the metrics system is |
| // initialized by the invocation of New and is then stored in the context |
| // (ui/build/context.go) to be used throughout the system. During the build |
| // initialization phase, several functions in this file are invoked to store |
| // information such as the environment, build configuration and build metadata. |
| // There are several scoped code that has Begin() and defer End() functions |
| // that captures the metrics and is them added as a perfInfo into the set |
| // of the collected metrics. Finally, when soong_ui has finished the build, |
| // the defer Dump function is invoked to store the collected metrics to the |
| // raw protobuf file in the $OUT directory and this raw protobuf file will be |
| // uploaded to the destination. See ui/build/upload.go for more details. The |
| // filename of the raw protobuf file and the list of files to be uploaded is |
| // defined in cmd/soong_ui/main.go. See ui/metrics/event.go for the explanation |
| // of what an event is and how the metrics system is a stack based system. |
| |
| import ( |
| "context" |
| "io/fs" |
| "maps" |
| "os" |
| "path/filepath" |
| "slices" |
| "sync" |
| |
| "android/soong/ui/logger" |
| |
| fid_proto "android/soong/cmd/find_input_delta/find_input_delta_proto" |
| "android/soong/ui/metrics" |
| soong_execution_proto "android/soong/ui/metrics/execution_metrics_proto" |
| soong_metrics_proto "android/soong/ui/metrics/metrics_proto" |
| "google.golang.org/protobuf/encoding/protowire" |
| "google.golang.org/protobuf/proto" |
| ) |
| |
| type ExecutionMetrics struct { |
| MetricsAggregationDir string |
| ctx context.Context |
| logger logger.Logger |
| waitGroup sync.WaitGroup |
| fileList *fileList |
| } |
| |
| type fileList struct { |
| totalChanges uint32 |
| changes fileChanges |
| seenFiles map[string]bool |
| } |
| |
| type fileChanges struct { |
| additions changeInfo |
| deletions changeInfo |
| modifications changeInfo |
| } |
| |
| type fileChangeCounts struct { |
| additions uint32 |
| deletions uint32 |
| modifications uint32 |
| } |
| |
| type changeInfo struct { |
| total uint32 |
| list []string |
| byExtension map[string]uint32 |
| } |
| |
| var MAXIMUM_FILES uint32 = 50 |
| |
| // Setup the handler for SoongExecutionMetrics. |
| func NewExecutionMetrics(log logger.Logger) *ExecutionMetrics { |
| return &ExecutionMetrics{ |
| logger: log, |
| fileList: &fileList{seenFiles: make(map[string]bool)}, |
| } |
| } |
| |
| // Save the path for ExecutionMetrics communications. |
| func (c *ExecutionMetrics) SetDir(path string) { |
| c.MetricsAggregationDir = path |
| } |
| |
| // Start collecting SoongExecutionMetrics. |
| func (c *ExecutionMetrics) Start() { |
| if c.MetricsAggregationDir == "" { |
| return |
| } |
| |
| tmpDir := c.MetricsAggregationDir + ".rm" |
| if _, err := fs.Stat(os.DirFS("."), c.MetricsAggregationDir); err == nil { |
| if err = os.RemoveAll(tmpDir); err != nil { |
| c.logger.Fatalf("Failed to remove %s: %v", tmpDir, err) |
| } |
| if err = os.Rename(c.MetricsAggregationDir, tmpDir); err != nil { |
| c.logger.Fatalf("Failed to rename %s to %s: %v", c.MetricsAggregationDir, tmpDir) |
| } |
| } |
| if err := os.MkdirAll(c.MetricsAggregationDir, 0777); err != nil { |
| c.logger.Fatalf("Failed to create %s: %v", c.MetricsAggregationDir) |
| } |
| |
| c.waitGroup.Add(1) |
| go func(d string) { |
| defer c.waitGroup.Done() |
| os.RemoveAll(d) |
| }(tmpDir) |
| |
| c.logger.Verbosef("ExecutionMetrics running\n") |
| } |
| |
| type hasTrace interface { |
| BeginTrace(name, desc string) |
| EndTrace() |
| } |
| |
| // Aggregate any execution metrics. |
| func (c *ExecutionMetrics) Finish(ctx hasTrace) { |
| ctx.BeginTrace(metrics.RunSoong, "execution_metrics.Finish") |
| defer ctx.EndTrace() |
| if c.MetricsAggregationDir == "" { |
| return |
| } |
| c.waitGroup.Wait() |
| |
| // Find and process all of the metrics files. |
| aggFs := os.DirFS(c.MetricsAggregationDir) |
| fs.WalkDir(aggFs, ".", func(path string, d fs.DirEntry, err error) error { |
| if err != nil { |
| c.logger.Fatalf("ExecutionMetrics.Finish: Error walking %s: %v", c.MetricsAggregationDir, err) |
| } |
| if d.IsDir() { |
| return nil |
| } |
| path = filepath.Join(c.MetricsAggregationDir, path) |
| r, err := os.ReadFile(path) |
| if err != nil { |
| c.logger.Fatalf("ExecutionMetrics.Finish: Failed to read %s: %v", path, err) |
| } |
| msg := &soong_execution_proto.SoongExecutionMetrics{} |
| err = proto.Unmarshal(r, msg) |
| if err != nil { |
| c.logger.Verbosef("ExecutionMetrics.Finish: Error unmarshalling SoongExecutionMetrics message: %v\n", err) |
| return nil |
| } |
| switch { |
| case msg.GetFileList() != nil: |
| if err := c.fileList.aggregateFileList(msg.GetFileList()); err != nil { |
| c.logger.Verbosef("ExecutionMetrics.Finish: Error processing SoongExecutionMetrics message: %v\n", err) |
| } |
| // Status update for all others. |
| default: |
| tag, _ := protowire.ConsumeVarint(r) |
| id, _ := protowire.DecodeTag(tag) |
| c.logger.Verbosef("ExecutionMetrics.Finish: Unexpected SoongExecutionMetrics submessage id=%d\n", id) |
| } |
| return nil |
| }) |
| } |
| |
| func (fl *fileList) aggregateFileList(msg *fid_proto.FileList) error { |
| fl.updateChangeInfo(msg.GetAdditions(), &fl.changes.additions) |
| fl.updateChangeInfo(msg.GetDeletions(), &fl.changes.deletions) |
| fl.updateChangeInfo(msg.GetChanges(), &fl.changes.modifications) |
| return nil |
| } |
| |
| func (fl *fileList) updateChangeInfo(list []string, info *changeInfo) { |
| for _, filename := range list { |
| if fl.seenFiles[filename] { |
| continue |
| } |
| fl.seenFiles[filename] = true |
| if info.total < MAXIMUM_FILES { |
| info.list = append(info.list, filename) |
| } |
| ext := filepath.Ext(filename) |
| if info.byExtension == nil { |
| info.byExtension = make(map[string]uint32) |
| } |
| info.byExtension[ext] += 1 |
| info.total += 1 |
| fl.totalChanges += 1 |
| } |
| } |
| |
| func (c *ExecutionMetrics) Dump(path string, args []string) error { |
| if c.MetricsAggregationDir == "" { |
| return nil |
| } |
| msg := c.GetMetrics(args) |
| |
| if _, err := os.Stat(filepath.Dir(path)); err != nil { |
| if err = os.MkdirAll(filepath.Dir(path), 0775); err != nil { |
| return err |
| } |
| } |
| data, err := proto.Marshal(msg) |
| if err != nil { |
| return err |
| } |
| return os.WriteFile(path, data, 0644) |
| } |
| |
| func (c *ExecutionMetrics) GetMetrics(args []string) *soong_metrics_proto.ExecutionMetrics { |
| return &soong_metrics_proto.ExecutionMetrics{ |
| CommandArgs: args, |
| ChangedFiles: c.getChangedFiles(), |
| } |
| } |
| |
| func (c *ExecutionMetrics) getChangedFiles() *soong_metrics_proto.AggregatedFileList { |
| fl := c.fileList |
| if fl == nil { |
| return nil |
| } |
| var count uint32 |
| fileCounts := make(map[string]*soong_metrics_proto.FileCount) |
| ret := &soong_metrics_proto.AggregatedFileList{TotalDelta: proto.Uint32(c.fileList.totalChanges)} |
| |
| // MAXIMUM_FILES is the upper bound on total file names reported. |
| if limit := min(MAXIMUM_FILES-min(MAXIMUM_FILES, count), fl.changes.additions.total); limit > 0 { |
| ret.Additions = fl.changes.additions.list[:limit] |
| count += limit |
| } |
| if limit := min(MAXIMUM_FILES-min(MAXIMUM_FILES, count), fl.changes.modifications.total); limit > 0 { |
| ret.Changes = fl.changes.modifications.list[:limit] |
| count += limit |
| } |
| if limit := min(MAXIMUM_FILES-min(MAXIMUM_FILES, count), fl.changes.deletions.total); limit > 0 { |
| ret.Deletions = fl.changes.deletions.list[:limit] |
| count += limit |
| } |
| |
| addExt := func(key string) *soong_metrics_proto.FileCount { |
| // Create the fileCounts map entry if needed, and return the address to the caller. |
| if _, ok := fileCounts[key]; !ok { |
| fileCounts[key] = &soong_metrics_proto.FileCount{Extension: proto.String(key)} |
| } |
| return fileCounts[key] |
| } |
| addCount := func(loc **uint32, count uint32) { |
| if *loc == nil { |
| *loc = proto.Uint32(0) |
| } |
| **loc += count |
| } |
| for k, v := range fl.changes.additions.byExtension { |
| addCount(&addExt(k).Additions, v) |
| } |
| for k, v := range fl.changes.modifications.byExtension { |
| addCount(&addExt(k).Modifications, v) |
| } |
| for k, v := range fl.changes.deletions.byExtension { |
| addCount(&addExt(k).Deletions, v) |
| } |
| |
| keys := slices.Sorted(maps.Keys(fileCounts)) |
| for _, k := range keys { |
| ret.Counts = append(ret.Counts, fileCounts[k]) |
| } |
| return ret |
| } |