Add SoongExecutionMetrics to ninja run

This adds metrics for partial compilation to Soong.

- Soong_ui names the directory SOONG_METRICS_AGGREGATION_DIR, which it
  empties before each ninja run.
- Find_input_delta writes metrics there
- At the end of the build, Soong_ui aggregates the metrics that were
  written to generate the aggregated metrics.

Bug: b/376287012
Test: Manual, TH
Change-Id: I123df654f5b963fcd213b5c4d815173051f5d72e
diff --git a/ui/build/Android.bp b/ui/build/Android.bp
index dc1abd9..32ffcec 100644
--- a/ui/build/Android.bp
+++ b/ui/build/Android.bp
@@ -41,6 +41,7 @@
         "soong-remoteexec",
         "soong-shared",
         "soong-ui-build-paths",
+        "soong-ui-execution-metrics",
         "soong-ui-logger",
         "soong-ui-metrics",
         "soong-ui-status",
diff --git a/ui/build/context.go b/ui/build/context.go
index fd20e26..69e5f96 100644
--- a/ui/build/context.go
+++ b/ui/build/context.go
@@ -18,6 +18,7 @@
 	"context"
 	"io"
 
+	"android/soong/ui/execution_metrics"
 	"android/soong/ui/logger"
 	"android/soong/ui/metrics"
 	soong_metrics_proto "android/soong/ui/metrics/metrics_proto"
@@ -33,7 +34,8 @@
 	context.Context
 	logger.Logger
 
-	Metrics *metrics.Metrics
+	Metrics          *metrics.Metrics
+	ExecutionMetrics *execution_metrics.ExecutionMetrics
 
 	Writer io.Writer
 	Status *status.Status
diff --git a/ui/build/dumpvars.go b/ui/build/dumpvars.go
index 5df3a95..e0b2c08 100644
--- a/ui/build/dumpvars.go
+++ b/ui/build/dumpvars.go
@@ -273,6 +273,7 @@
 		"BUILD_BROKEN_USES_BUILD_SHARED_LIBRARY",
 		"BUILD_BROKEN_USES_BUILD_STATIC_JAVA_LIBRARY",
 		"BUILD_BROKEN_USES_BUILD_STATIC_LIBRARY",
+		"RELEASE_BUILD_EXECUTION_METRICS",
 	}, exportEnvVars...), BannerVars...)
 
 	makeVars, err := dumpMakeVars(ctx, config, config.Arguments(), allVars, true, "")
diff --git a/ui/build/ninja.go b/ui/build/ninja.go
index f5f637f..04c52a2 100644
--- a/ui/build/ninja.go
+++ b/ui/build/ninja.go
@@ -244,6 +244,9 @@
 
 			// SOONG_USE_PARTIAL_COMPILE only determines which half of the rule we execute.
 			"SOONG_USE_PARTIAL_COMPILE",
+
+			// Directory for ExecutionMetrics
+			"SOONG_METRICS_AGGREGATION_DIR",
 		}, config.BuildBrokenNinjaUsesEnvVars()...)...)
 	}
 
@@ -256,6 +259,10 @@
 		// Only set RUST_BACKTRACE for n2.
 	}
 
+	// Set up the metrics aggregation directory.
+	ctx.ExecutionMetrics.SetDir(filepath.Join(config.OutDir(), "soong", "metrics_aggregation"))
+	cmd.Environment.Set("SOONG_METRICS_AGGREGATION_DIR", ctx.ExecutionMetrics.MetricsAggregationDir)
+
 	// Print the environment variables that Ninja is operating in.
 	ctx.Verboseln("Ninja environment: ")
 	envVars := cmd.Environment.Environ()
@@ -300,6 +307,8 @@
 		}
 	}()
 
+	ctx.ExecutionMetrics.Start()
+	defer ctx.ExecutionMetrics.Finish(ctx)
 	ctx.Status.Status("Starting ninja...")
 	cmd.RunAndStreamOrFatal()
 }
diff --git a/ui/execution_metrics/Android.bp b/ui/execution_metrics/Android.bp
new file mode 100644
index 0000000..542e550
--- /dev/null
+++ b/ui/execution_metrics/Android.bp
@@ -0,0 +1,35 @@
+// Copyright 2018 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 {
+    default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+bootstrap_go_package {
+    name: "soong-ui-execution-metrics",
+    pkgPath: "android/soong/ui/execution_metrics",
+    deps: [
+        "golang-protobuf-proto",
+        "soong-shared",
+        "soong-ui-logger",
+        "soong-ui-execution_metrics_proto",
+        "soong-ui-metrics_proto",
+        "soong-cmd-find_input_delta-proto",
+    ],
+    srcs: [
+        "execution_metrics.go",
+    ],
+    testSrcs: [
+    ],
+}
diff --git a/ui/execution_metrics/execution_metrics.go b/ui/execution_metrics/execution_metrics.go
new file mode 100644
index 0000000..4ea251f
--- /dev/null
+++ b/ui/execution_metrics/execution_metrics.go
@@ -0,0 +1,273 @@
+// 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()
+	msg.CommandArgs = 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() *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
+}
diff --git a/ui/execution_metrics/execution_metrics_test.go b/ui/execution_metrics/execution_metrics_test.go
new file mode 100644
index 0000000..28fa973
--- /dev/null
+++ b/ui/execution_metrics/execution_metrics_test.go
@@ -0,0 +1,63 @@
+// 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
+
+import (
+	"reflect"
+	"testing"
+
+	fid_proto "android/soong/cmd/find_input_delta/find_input_delta_proto"
+)
+
+func TestUpdateChangeInfo(t *testing.T) {
+	testCases := []struct {
+		Name     string
+		Message  *fid_proto.FileList
+		FileList *fileList
+		Expected *fileList
+	}{
+		{
+			Name: "various",
+			Message: &fid_proto.FileList{
+				Additions: []string{"file1", "file2", "file3", "file2"},
+				Deletions: []string{"file5.go", "file6"},
+			},
+			FileList: &fileList{seenFiles: make(map[string]bool)},
+			Expected: &fileList{
+				seenFiles:    map[string]bool{"file1": true, "file2": true, "file3": true, "file5.go": true, "file6": true},
+				totalChanges: 5,
+				changes: fileChanges{
+					additions: changeInfo{
+						total:       3,
+						list:        []string{"file1", "file2", "file3"},
+						byExtension: map[string]uint32{"": 3},
+					},
+					deletions: changeInfo{
+						total:       2,
+						list:        []string{"file5.go", "file6"},
+						byExtension: map[string]uint32{"": 1, ".go": 1},
+					},
+				},
+			},
+		},
+	}
+	for _, tc := range testCases {
+		tc.FileList.aggregateFileList(tc.Message)
+		if !reflect.DeepEqual(tc.FileList, tc.Expected) {
+			t.Errorf("Expected: %v, Actual: %v", tc.Expected, tc.FileList)
+		}
+	}
+}
diff --git a/ui/metrics/Android.bp b/ui/metrics/Android.bp
index 591e3cc..2e19f7a 100644
--- a/ui/metrics/Android.bp
+++ b/ui/metrics/Android.bp
@@ -26,7 +26,7 @@
         "soong-ui-metrics_proto",
         "soong-ui-mk_metrics_proto",
         "soong-shared",
-        "soong-ui-metrics_combined_proto",
+        "soong-ui-execution_metrics_proto",
     ],
     srcs: [
         "hostinfo.go",
@@ -64,15 +64,15 @@
 }
 
 bootstrap_go_package {
-    name: "soong-ui-metrics_combined_proto",
-    pkgPath: "android/soong/ui/metrics/combined_metrics_proto",
+    name: "soong-ui-execution_metrics_proto",
+    pkgPath: "android/soong/ui/metrics/execution_metrics_proto",
     deps: [
         "golang-protobuf-reflect-protoreflect",
         "golang-protobuf-runtime-protoimpl",
         "soong-cmd-find_input_delta-proto",
     ],
     srcs: [
-        "metrics_proto/metrics.pb.go",
+        "execution_metrics_proto/execution_metrics.pb.go",
     ],
 }
 
diff --git a/ui/metrics/execution_metrics_proto/execution_metrics.pb.go b/ui/metrics/execution_metrics_proto/execution_metrics.pb.go
new file mode 100644
index 0000000..a065dbd
--- /dev/null
+++ b/ui/metrics/execution_metrics_proto/execution_metrics.pb.go
@@ -0,0 +1,240 @@
+// 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.
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// 	protoc-gen-go v1.30.0
+// 	protoc        v3.21.12
+// source: execution_metrics.proto
+
+package execution_metrics_proto
+
+import (
+	find_input_delta_proto "android/soong/cmd/find_input_delta/find_input_delta_proto"
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	reflect "reflect"
+	sync "sync"
+)
+
+const (
+	// Verify that this generated code is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+	// Verify that runtime/protoimpl is sufficiently up-to-date.
+	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// These field numbers are also found in the inner message declarations.
+// We verify that the values are the same, and that every enum value is checked
+// in execution_metrics_test.go.
+// Do not change this enum without also updating:
+//   - the submessage's .proto file
+//   - execution_metrics_test.go
+type FieldNumbers int32
+
+const (
+	FieldNumbers_FIELD_NUMBERS_UNSPECIFIED FieldNumbers = 0
+	FieldNumbers_FIELD_NUMBERS_FILE_LIST   FieldNumbers = 1
+)
+
+// Enum value maps for FieldNumbers.
+var (
+	FieldNumbers_name = map[int32]string{
+		0: "FIELD_NUMBERS_UNSPECIFIED",
+		1: "FIELD_NUMBERS_FILE_LIST",
+	}
+	FieldNumbers_value = map[string]int32{
+		"FIELD_NUMBERS_UNSPECIFIED": 0,
+		"FIELD_NUMBERS_FILE_LIST":   1,
+	}
+)
+
+func (x FieldNumbers) Enum() *FieldNumbers {
+	p := new(FieldNumbers)
+	*p = x
+	return p
+}
+
+func (x FieldNumbers) String() string {
+	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (FieldNumbers) Descriptor() protoreflect.EnumDescriptor {
+	return file_execution_metrics_proto_enumTypes[0].Descriptor()
+}
+
+func (FieldNumbers) Type() protoreflect.EnumType {
+	return &file_execution_metrics_proto_enumTypes[0]
+}
+
+func (x FieldNumbers) Number() protoreflect.EnumNumber {
+	return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Do not use.
+func (x *FieldNumbers) UnmarshalJSON(b []byte) error {
+	num, err := protoimpl.X.UnmarshalJSONEnum(x.Descriptor(), b)
+	if err != nil {
+		return err
+	}
+	*x = FieldNumbers(num)
+	return nil
+}
+
+// Deprecated: Use FieldNumbers.Descriptor instead.
+func (FieldNumbers) EnumDescriptor() ([]byte, []int) {
+	return file_execution_metrics_proto_rawDescGZIP(), []int{0}
+}
+
+type SoongExecutionMetrics struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// cmd/find_input_delta/find_input_delta_proto.FileList
+	FileList *find_input_delta_proto.FileList `protobuf:"bytes,1,opt,name=file_list,json=fileList" json:"file_list,omitempty"`
+}
+
+func (x *SoongExecutionMetrics) Reset() {
+	*x = SoongExecutionMetrics{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_execution_metrics_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *SoongExecutionMetrics) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SoongExecutionMetrics) ProtoMessage() {}
+
+func (x *SoongExecutionMetrics) ProtoReflect() protoreflect.Message {
+	mi := &file_execution_metrics_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use SoongExecutionMetrics.ProtoReflect.Descriptor instead.
+func (*SoongExecutionMetrics) Descriptor() ([]byte, []int) {
+	return file_execution_metrics_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *SoongExecutionMetrics) GetFileList() *find_input_delta_proto.FileList {
+	if x != nil {
+		return x.FileList
+	}
+	return nil
+}
+
+var File_execution_metrics_proto protoreflect.FileDescriptor
+
+var file_execution_metrics_proto_rawDesc = []byte{
+	0x0a, 0x17, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x65, 0x74, 0x72,
+	0x69, 0x63, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x13, 0x73, 0x6f, 0x6f, 0x6e, 0x67,
+	0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x1a, 0x3b,
+	0x63, 0x6d, 0x64, 0x2f, 0x66, 0x69, 0x6e, 0x64, 0x5f, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x64,
+	0x65, 0x6c, 0x74, 0x61, 0x2f, 0x66, 0x69, 0x6e, 0x64, 0x5f, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f,
+	0x64, 0x65, 0x6c, 0x74, 0x61, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x66, 0x69, 0x6c, 0x65,
+	0x5f, 0x6c, 0x69, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x5e, 0x0a, 0x15, 0x53,
+	0x6f, 0x6f, 0x6e, 0x67, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74,
+	0x72, 0x69, 0x63, 0x73, 0x12, 0x45, 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x6c, 0x69, 0x73,
+	0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69,
+	0x64, 0x2e, 0x66, 0x69, 0x6e, 0x64, 0x5f, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x64, 0x65, 0x6c,
+	0x74, 0x61, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x4c, 0x69, 0x73,
+	0x74, 0x52, 0x08, 0x66, 0x69, 0x6c, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x2a, 0x4a, 0x0a, 0x0c, 0x46,
+	0x69, 0x65, 0x6c, 0x64, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x12, 0x1d, 0x0a, 0x19, 0x46,
+	0x49, 0x45, 0x4c, 0x44, 0x5f, 0x4e, 0x55, 0x4d, 0x42, 0x45, 0x52, 0x53, 0x5f, 0x55, 0x4e, 0x53,
+	0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x46, 0x49,
+	0x45, 0x4c, 0x44, 0x5f, 0x4e, 0x55, 0x4d, 0x42, 0x45, 0x52, 0x53, 0x5f, 0x46, 0x49, 0x4c, 0x45,
+	0x5f, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x01, 0x42, 0x32, 0x5a, 0x30, 0x61, 0x6e, 0x64, 0x72, 0x6f,
+	0x69, 0x64, 0x2f, 0x73, 0x6f, 0x6f, 0x6e, 0x67, 0x2f, 0x75, 0x69, 0x2f, 0x6d, 0x65, 0x74, 0x72,
+	0x69, 0x63, 0x73, 0x2f, 0x65, 0x78, 0x65, 0x63, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x6d, 0x65,
+	0x74, 0x72, 0x69, 0x63, 0x73, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+}
+
+var (
+	file_execution_metrics_proto_rawDescOnce sync.Once
+	file_execution_metrics_proto_rawDescData = file_execution_metrics_proto_rawDesc
+)
+
+func file_execution_metrics_proto_rawDescGZIP() []byte {
+	file_execution_metrics_proto_rawDescOnce.Do(func() {
+		file_execution_metrics_proto_rawDescData = protoimpl.X.CompressGZIP(file_execution_metrics_proto_rawDescData)
+	})
+	return file_execution_metrics_proto_rawDescData
+}
+
+var file_execution_metrics_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
+var file_execution_metrics_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
+var file_execution_metrics_proto_goTypes = []interface{}{
+	(FieldNumbers)(0),                       // 0: soong_build_metrics.FieldNumbers
+	(*SoongExecutionMetrics)(nil),           // 1: soong_build_metrics.SoongExecutionMetrics
+	(*find_input_delta_proto.FileList)(nil), // 2: android.find_input_delta_proto.FileList
+}
+var file_execution_metrics_proto_depIdxs = []int32{
+	2, // 0: soong_build_metrics.SoongExecutionMetrics.file_list:type_name -> android.find_input_delta_proto.FileList
+	1, // [1:1] is the sub-list for method output_type
+	1, // [1:1] is the sub-list for method input_type
+	1, // [1:1] is the sub-list for extension type_name
+	1, // [1:1] is the sub-list for extension extendee
+	0, // [0:1] is the sub-list for field type_name
+}
+
+func init() { file_execution_metrics_proto_init() }
+func file_execution_metrics_proto_init() {
+	if File_execution_metrics_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_execution_metrics_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*SoongExecutionMetrics); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_execution_metrics_proto_rawDesc,
+			NumEnums:      1,
+			NumMessages:   1,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_execution_metrics_proto_goTypes,
+		DependencyIndexes: file_execution_metrics_proto_depIdxs,
+		EnumInfos:         file_execution_metrics_proto_enumTypes,
+		MessageInfos:      file_execution_metrics_proto_msgTypes,
+	}.Build()
+	File_execution_metrics_proto = out.File
+	file_execution_metrics_proto_rawDesc = nil
+	file_execution_metrics_proto_goTypes = nil
+	file_execution_metrics_proto_depIdxs = nil
+}
diff --git a/ui/metrics/metrics_proto/combined_metrics.proto b/ui/metrics/execution_metrics_proto/execution_metrics.proto
similarity index 83%
rename from ui/metrics/metrics_proto/combined_metrics.proto
rename to ui/metrics/execution_metrics_proto/execution_metrics.proto
index 3cd9a53..381dcd1 100644
--- a/ui/metrics/metrics_proto/combined_metrics.proto
+++ b/ui/metrics/execution_metrics_proto/execution_metrics.proto
@@ -1,4 +1,4 @@
-// Copyright 2018 Google Inc. All Rights Reserved.
+// 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.
@@ -15,22 +15,22 @@
 syntax = "proto2";
 
 package soong_build_metrics;
-option go_package = "android/soong/ui/metrics/metrics_proto";
+option go_package = "android/soong/ui/metrics/execution_metrics_proto";
 
 import "cmd/find_input_delta/find_input_delta_proto/file_list.proto";
 
 // These field numbers are also found in the inner message declarations.
 // We verify that the values are the same, and that every enum value is checked
-// in combined_metrics_test.go.
+// in execution_metrics_test.go.
 // Do not change this enum without also updating:
 //  - the submessage's .proto file
-//  - combined_metrics_test.go
+//  - execution_metrics_test.go
 enum FieldNumbers {
   FIELD_NUMBERS_UNSPECIFIED = 0;
   FIELD_NUMBERS_FILE_LIST = 1;
 }
 
-message SoongCombinedMetrics {
+message SoongExecutionMetrics {
   // cmd/find_input_delta/find_input_delta_proto.FileList
   optional android.find_input_delta_proto.FileList file_list = 1;
 }
diff --git a/ui/metrics/metrics_proto/combined_metrics_test.go b/ui/metrics/execution_metrics_proto/execution_metrics_test.go
similarity index 89%
rename from ui/metrics/metrics_proto/combined_metrics_test.go
rename to ui/metrics/execution_metrics_proto/execution_metrics_test.go
index eedb12a..2f71c21 100644
--- a/ui/metrics/metrics_proto/combined_metrics_test.go
+++ b/ui/metrics/execution_metrics_proto/execution_metrics_test.go
@@ -1,4 +1,4 @@
-package metrics_proto
+package execution_metrics_proto
 
 import (
 	"testing"
@@ -6,7 +6,7 @@
 	find_input_delta_proto "android/soong/cmd/find_input_delta/find_input_delta_proto"
 )
 
-func TestCombinedMetricsMessageNums(t *testing.T) {
+func TestExecutionMetricsMessageNums(t *testing.T) {
 	testCases := []struct {
 		Name         string
 		FieldNumbers map[string]int32
diff --git a/ui/metrics/execution_metrics_proto/regen.sh b/ui/metrics/execution_metrics_proto/regen.sh
new file mode 100755
index 0000000..5339a9a
--- /dev/null
+++ b/ui/metrics/execution_metrics_proto/regen.sh
@@ -0,0 +1,17 @@
+#!/bin/bash
+
+# Generates the golang source file of metrics.proto protobuf file.
+
+set -e
+
+function die() { echo "ERROR: $1" >&2; exit 1; }
+
+readonly error_msg="Maybe you need to run 'lunch aosp_arm-eng && m aprotoc blueprint_tools'?"
+
+if ! hash aprotoc &>/dev/null; then
+  die "could not find aprotoc. ${error_msg}"
+fi
+
+if ! aprotoc --go_out=paths=source_relative:. -I .:../../.. execution_metrics.proto; then
+  die "build failed. ${error_msg}"
+fi
diff --git a/ui/metrics/metrics_proto/combined_metrics.pb.go b/ui/metrics/metrics_proto/combined_metrics.pb.go
deleted file mode 100644
index f49d64d..0000000
--- a/ui/metrics/metrics_proto/combined_metrics.pb.go
+++ /dev/null
@@ -1,239 +0,0 @@
-// Copyright 2018 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.
-
-// Code generated by protoc-gen-go. DO NOT EDIT.
-// versions:
-// 	protoc-gen-go v1.33.0
-// 	protoc        v3.21.12
-// source: combined_metrics.proto
-
-package metrics_proto
-
-import (
-	find_input_delta_proto "android/soong/cmd/find_input_delta/find_input_delta_proto"
-	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
-	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
-	reflect "reflect"
-	sync "sync"
-)
-
-const (
-	// Verify that this generated code is sufficiently up-to-date.
-	_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
-	// Verify that runtime/protoimpl is sufficiently up-to-date.
-	_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
-)
-
-// These field numbers are also found in the inner message declarations.
-// We verify that the values are the same, and that every enum value is checked
-// in combined_metrics_test.go.
-// Do not change this enum without also updating:
-//   - the submessage's .proto file
-//   - combined_metrics_test.go
-type FieldNumbers int32
-
-const (
-	FieldNumbers_FIELD_NUMBERS_UNSPECIFIED FieldNumbers = 0
-	FieldNumbers_FIELD_NUMBERS_FILE_LIST   FieldNumbers = 1
-)
-
-// Enum value maps for FieldNumbers.
-var (
-	FieldNumbers_name = map[int32]string{
-		0: "FIELD_NUMBERS_UNSPECIFIED",
-		1: "FIELD_NUMBERS_FILE_LIST",
-	}
-	FieldNumbers_value = map[string]int32{
-		"FIELD_NUMBERS_UNSPECIFIED": 0,
-		"FIELD_NUMBERS_FILE_LIST":   1,
-	}
-)
-
-func (x FieldNumbers) Enum() *FieldNumbers {
-	p := new(FieldNumbers)
-	*p = x
-	return p
-}
-
-func (x FieldNumbers) String() string {
-	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
-}
-
-func (FieldNumbers) Descriptor() protoreflect.EnumDescriptor {
-	return file_combined_metrics_proto_enumTypes[0].Descriptor()
-}
-
-func (FieldNumbers) Type() protoreflect.EnumType {
-	return &file_combined_metrics_proto_enumTypes[0]
-}
-
-func (x FieldNumbers) Number() protoreflect.EnumNumber {
-	return protoreflect.EnumNumber(x)
-}
-
-// Deprecated: Do not use.
-func (x *FieldNumbers) UnmarshalJSON(b []byte) error {
-	num, err := protoimpl.X.UnmarshalJSONEnum(x.Descriptor(), b)
-	if err != nil {
-		return err
-	}
-	*x = FieldNumbers(num)
-	return nil
-}
-
-// Deprecated: Use FieldNumbers.Descriptor instead.
-func (FieldNumbers) EnumDescriptor() ([]byte, []int) {
-	return file_combined_metrics_proto_rawDescGZIP(), []int{0}
-}
-
-type SoongCombinedMetrics struct {
-	state         protoimpl.MessageState
-	sizeCache     protoimpl.SizeCache
-	unknownFields protoimpl.UnknownFields
-
-	// cmd/find_input_delta/find_input_delta_proto.FileList
-	FileList *find_input_delta_proto.FileList `protobuf:"bytes,1,opt,name=file_list,json=fileList" json:"file_list,omitempty"`
-}
-
-func (x *SoongCombinedMetrics) Reset() {
-	*x = SoongCombinedMetrics{}
-	if protoimpl.UnsafeEnabled {
-		mi := &file_combined_metrics_proto_msgTypes[0]
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		ms.StoreMessageInfo(mi)
-	}
-}
-
-func (x *SoongCombinedMetrics) String() string {
-	return protoimpl.X.MessageStringOf(x)
-}
-
-func (*SoongCombinedMetrics) ProtoMessage() {}
-
-func (x *SoongCombinedMetrics) ProtoReflect() protoreflect.Message {
-	mi := &file_combined_metrics_proto_msgTypes[0]
-	if protoimpl.UnsafeEnabled && x != nil {
-		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
-		if ms.LoadMessageInfo() == nil {
-			ms.StoreMessageInfo(mi)
-		}
-		return ms
-	}
-	return mi.MessageOf(x)
-}
-
-// Deprecated: Use SoongCombinedMetrics.ProtoReflect.Descriptor instead.
-func (*SoongCombinedMetrics) Descriptor() ([]byte, []int) {
-	return file_combined_metrics_proto_rawDescGZIP(), []int{0}
-}
-
-func (x *SoongCombinedMetrics) GetFileList() *find_input_delta_proto.FileList {
-	if x != nil {
-		return x.FileList
-	}
-	return nil
-}
-
-var File_combined_metrics_proto protoreflect.FileDescriptor
-
-var file_combined_metrics_proto_rawDesc = []byte{
-	0x0a, 0x16, 0x63, 0x6f, 0x6d, 0x62, 0x69, 0x6e, 0x65, 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x72, 0x69,
-	0x63, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x13, 0x73, 0x6f, 0x6f, 0x6e, 0x67, 0x5f,
-	0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x1a, 0x3b, 0x63,
-	0x6d, 0x64, 0x2f, 0x66, 0x69, 0x6e, 0x64, 0x5f, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x64, 0x65,
-	0x6c, 0x74, 0x61, 0x2f, 0x66, 0x69, 0x6e, 0x64, 0x5f, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x64,
-	0x65, 0x6c, 0x74, 0x61, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x66, 0x69, 0x6c, 0x65, 0x5f,
-	0x6c, 0x69, 0x73, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x5d, 0x0a, 0x14, 0x53, 0x6f,
-	0x6f, 0x6e, 0x67, 0x43, 0x6f, 0x6d, 0x62, 0x69, 0x6e, 0x65, 0x64, 0x4d, 0x65, 0x74, 0x72, 0x69,
-	0x63, 0x73, 0x12, 0x45, 0x0a, 0x09, 0x66, 0x69, 0x6c, 0x65, 0x5f, 0x6c, 0x69, 0x73, 0x74, 0x18,
-	0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2e,
-	0x66, 0x69, 0x6e, 0x64, 0x5f, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61,
-	0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x52,
-	0x08, 0x66, 0x69, 0x6c, 0x65, 0x4c, 0x69, 0x73, 0x74, 0x2a, 0x4a, 0x0a, 0x0c, 0x46, 0x69, 0x65,
-	0x6c, 0x64, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x73, 0x12, 0x1d, 0x0a, 0x19, 0x46, 0x49, 0x45,
-	0x4c, 0x44, 0x5f, 0x4e, 0x55, 0x4d, 0x42, 0x45, 0x52, 0x53, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45,
-	0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x1b, 0x0a, 0x17, 0x46, 0x49, 0x45, 0x4c,
-	0x44, 0x5f, 0x4e, 0x55, 0x4d, 0x42, 0x45, 0x52, 0x53, 0x5f, 0x46, 0x49, 0x4c, 0x45, 0x5f, 0x4c,
-	0x49, 0x53, 0x54, 0x10, 0x01, 0x42, 0x28, 0x5a, 0x26, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64,
-	0x2f, 0x73, 0x6f, 0x6f, 0x6e, 0x67, 0x2f, 0x75, 0x69, 0x2f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63,
-	0x73, 0x2f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
-}
-
-var (
-	file_combined_metrics_proto_rawDescOnce sync.Once
-	file_combined_metrics_proto_rawDescData = file_combined_metrics_proto_rawDesc
-)
-
-func file_combined_metrics_proto_rawDescGZIP() []byte {
-	file_combined_metrics_proto_rawDescOnce.Do(func() {
-		file_combined_metrics_proto_rawDescData = protoimpl.X.CompressGZIP(file_combined_metrics_proto_rawDescData)
-	})
-	return file_combined_metrics_proto_rawDescData
-}
-
-var file_combined_metrics_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
-var file_combined_metrics_proto_msgTypes = make([]protoimpl.MessageInfo, 1)
-var file_combined_metrics_proto_goTypes = []interface{}{
-	(FieldNumbers)(0),                       // 0: soong_build_metrics.FieldNumbers
-	(*SoongCombinedMetrics)(nil),            // 1: soong_build_metrics.SoongCombinedMetrics
-	(*find_input_delta_proto.FileList)(nil), // 2: android.find_input_delta_proto.FileList
-}
-var file_combined_metrics_proto_depIdxs = []int32{
-	2, // 0: soong_build_metrics.SoongCombinedMetrics.file_list:type_name -> android.find_input_delta_proto.FileList
-	1, // [1:1] is the sub-list for method output_type
-	1, // [1:1] is the sub-list for method input_type
-	1, // [1:1] is the sub-list for extension type_name
-	1, // [1:1] is the sub-list for extension extendee
-	0, // [0:1] is the sub-list for field type_name
-}
-
-func init() { file_combined_metrics_proto_init() }
-func file_combined_metrics_proto_init() {
-	if File_combined_metrics_proto != nil {
-		return
-	}
-	if !protoimpl.UnsafeEnabled {
-		file_combined_metrics_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*SoongCombinedMetrics); i {
-			case 0:
-				return &v.state
-			case 1:
-				return &v.sizeCache
-			case 2:
-				return &v.unknownFields
-			default:
-				return nil
-			}
-		}
-	}
-	type x struct{}
-	out := protoimpl.TypeBuilder{
-		File: protoimpl.DescBuilder{
-			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
-			RawDescriptor: file_combined_metrics_proto_rawDesc,
-			NumEnums:      1,
-			NumMessages:   1,
-			NumExtensions: 0,
-			NumServices:   0,
-		},
-		GoTypes:           file_combined_metrics_proto_goTypes,
-		DependencyIndexes: file_combined_metrics_proto_depIdxs,
-		EnumInfos:         file_combined_metrics_proto_enumTypes,
-		MessageInfos:      file_combined_metrics_proto_msgTypes,
-	}.Build()
-	File_combined_metrics_proto = out.File
-	file_combined_metrics_proto_rawDesc = nil
-	file_combined_metrics_proto_goTypes = nil
-	file_combined_metrics_proto_depIdxs = nil
-}
diff --git a/ui/metrics/metrics_proto/metrics.pb.go b/ui/metrics/metrics_proto/metrics.pb.go
index 72fdbe8..0aa51b1 100644
--- a/ui/metrics/metrics_proto/metrics.pb.go
+++ b/ui/metrics/metrics_proto/metrics.pb.go
@@ -14,7 +14,7 @@
 
 // Code generated by protoc-gen-go. DO NOT EDIT.
 // versions:
-// 	protoc-gen-go v1.33.0
+// 	protoc-gen-go v1.30.0
 // 	protoc        v3.21.12
 // source: metrics.proto
 
@@ -2080,6 +2080,177 @@
 	return nil
 }
 
+// This is created by soong_ui from the various
+// android.find_input_delta_proto.FileList metrics provided to it by
+// find_input_delta.
+type AggregatedFileList struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// The arguments provided on the command line.
+	CommandArgs []string `protobuf:"bytes,1,rep,name=command_args,json=commandArgs" json:"command_args,omitempty"`
+	// The (possibly truncated list of) added files.
+	Additions []string `protobuf:"bytes,2,rep,name=additions" json:"additions,omitempty"`
+	// The (possibly truncated list of) changed files.
+	Changes []string `protobuf:"bytes,3,rep,name=changes" json:"changes,omitempty"`
+	// The (possibly truncated list of) deleted files.
+	Deletions []string `protobuf:"bytes,4,rep,name=deletions" json:"deletions,omitempty"`
+	// Count of files added/changed/deleted.
+	TotalDelta *uint32 `protobuf:"varint,5,opt,name=total_delta,json=totalDelta" json:"total_delta,omitempty"`
+	// Counts by extension.
+	Counts []*FileCount `protobuf:"bytes,6,rep,name=counts" json:"counts,omitempty"`
+}
+
+func (x *AggregatedFileList) Reset() {
+	*x = AggregatedFileList{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_metrics_proto_msgTypes[19]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *AggregatedFileList) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*AggregatedFileList) ProtoMessage() {}
+
+func (x *AggregatedFileList) ProtoReflect() protoreflect.Message {
+	mi := &file_metrics_proto_msgTypes[19]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use AggregatedFileList.ProtoReflect.Descriptor instead.
+func (*AggregatedFileList) Descriptor() ([]byte, []int) {
+	return file_metrics_proto_rawDescGZIP(), []int{19}
+}
+
+func (x *AggregatedFileList) GetCommandArgs() []string {
+	if x != nil {
+		return x.CommandArgs
+	}
+	return nil
+}
+
+func (x *AggregatedFileList) GetAdditions() []string {
+	if x != nil {
+		return x.Additions
+	}
+	return nil
+}
+
+func (x *AggregatedFileList) GetChanges() []string {
+	if x != nil {
+		return x.Changes
+	}
+	return nil
+}
+
+func (x *AggregatedFileList) GetDeletions() []string {
+	if x != nil {
+		return x.Deletions
+	}
+	return nil
+}
+
+func (x *AggregatedFileList) GetTotalDelta() uint32 {
+	if x != nil && x.TotalDelta != nil {
+		return *x.TotalDelta
+	}
+	return 0
+}
+
+func (x *AggregatedFileList) GetCounts() []*FileCount {
+	if x != nil {
+		return x.Counts
+	}
+	return nil
+}
+
+type FileCount struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	// The file extension
+	Extension *string `protobuf:"bytes,1,opt,name=extension" json:"extension,omitempty"`
+	// Number of added files with this extension.
+	Additions *uint32 `protobuf:"varint,2,opt,name=additions" json:"additions,omitempty"`
+	// Number of modified files with this extension.
+	Modifications *uint32 `protobuf:"varint,3,opt,name=modifications" json:"modifications,omitempty"`
+	// Number of deleted files with this extension.
+	Deletions *uint32 `protobuf:"varint,4,opt,name=deletions" json:"deletions,omitempty"`
+}
+
+func (x *FileCount) Reset() {
+	*x = FileCount{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_metrics_proto_msgTypes[20]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *FileCount) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*FileCount) ProtoMessage() {}
+
+func (x *FileCount) ProtoReflect() protoreflect.Message {
+	mi := &file_metrics_proto_msgTypes[20]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use FileCount.ProtoReflect.Descriptor instead.
+func (*FileCount) Descriptor() ([]byte, []int) {
+	return file_metrics_proto_rawDescGZIP(), []int{20}
+}
+
+func (x *FileCount) GetExtension() string {
+	if x != nil && x.Extension != nil {
+		return *x.Extension
+	}
+	return ""
+}
+
+func (x *FileCount) GetAdditions() uint32 {
+	if x != nil && x.Additions != nil {
+		return *x.Additions
+	}
+	return 0
+}
+
+func (x *FileCount) GetModifications() uint32 {
+	if x != nil && x.Modifications != nil {
+		return *x.Modifications
+	}
+	return 0
+}
+
+func (x *FileCount) GetDeletions() uint32 {
+	if x != nil && x.Deletions != nil {
+		return *x.Deletions
+	}
+	return 0
+}
+
 type OptimizedBuildMetrics_TargetOptimizationResult struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -2101,7 +2272,7 @@
 func (x *OptimizedBuildMetrics_TargetOptimizationResult) Reset() {
 	*x = OptimizedBuildMetrics_TargetOptimizationResult{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_metrics_proto_msgTypes[19]
+		mi := &file_metrics_proto_msgTypes[21]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -2114,7 +2285,7 @@
 func (*OptimizedBuildMetrics_TargetOptimizationResult) ProtoMessage() {}
 
 func (x *OptimizedBuildMetrics_TargetOptimizationResult) ProtoReflect() protoreflect.Message {
-	mi := &file_metrics_proto_msgTypes[19]
+	mi := &file_metrics_proto_msgTypes[21]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -2181,7 +2352,7 @@
 func (x *OptimizedBuildMetrics_TargetOptimizationResult_OutputArtifact) Reset() {
 	*x = OptimizedBuildMetrics_TargetOptimizationResult_OutputArtifact{}
 	if protoimpl.UnsafeEnabled {
-		mi := &file_metrics_proto_msgTypes[20]
+		mi := &file_metrics_proto_msgTypes[22]
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		ms.StoreMessageInfo(mi)
 	}
@@ -2194,7 +2365,7 @@
 func (*OptimizedBuildMetrics_TargetOptimizationResult_OutputArtifact) ProtoMessage() {}
 
 func (x *OptimizedBuildMetrics_TargetOptimizationResult_OutputArtifact) ProtoReflect() protoreflect.Message {
-	mi := &file_metrics_proto_msgTypes[20]
+	mi := &file_metrics_proto_msgTypes[22]
 	if protoimpl.UnsafeEnabled && x != nil {
 		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
 		if ms.LoadMessageInfo() == nil {
@@ -2637,10 +2808,33 @@
 	0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x04, 0x73, 0x69,
 	0x7a, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x5f, 0x6d,
 	0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0f, 0x69, 0x6e,
-	0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x42, 0x28, 0x5a,
-	0x26, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2f, 0x73, 0x6f, 0x6f, 0x6e, 0x67, 0x2f, 0x75,
-	0x69, 0x2f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x2f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63,
-	0x73, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+	0x63, 0x6c, 0x75, 0x64, 0x65, 0x64, 0x4d, 0x6f, 0x64, 0x75, 0x6c, 0x65, 0x73, 0x22, 0xe6, 0x01,
+	0x0a, 0x12, 0x41, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x64, 0x46, 0x69, 0x6c, 0x65,
+	0x4c, 0x69, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x5f,
+	0x61, 0x72, 0x67, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6d, 0x6d,
+	0x61, 0x6e, 0x64, 0x41, 0x72, 0x67, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x64, 0x64, 0x69, 0x74,
+	0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x09, 0x61, 0x64, 0x64, 0x69,
+	0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73,
+	0x18, 0x03, 0x20, 0x03, 0x28, 0x09, 0x52, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x12,
+	0x1c, 0x0a, 0x09, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03,
+	0x28, 0x09, 0x52, 0x09, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1f, 0x0a,
+	0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x64, 0x65, 0x6c, 0x74, 0x61, 0x18, 0x05, 0x20, 0x01,
+	0x28, 0x0d, 0x52, 0x0a, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x44, 0x65, 0x6c, 0x74, 0x61, 0x12, 0x36,
+	0x0a, 0x06, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e,
+	0x2e, 0x73, 0x6f, 0x6f, 0x6e, 0x67, 0x5f, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x5f, 0x6d, 0x65, 0x74,
+	0x72, 0x69, 0x63, 0x73, 0x2e, 0x46, 0x69, 0x6c, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x06,
+	0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x22, 0x8b, 0x01, 0x0a, 0x09, 0x46, 0x69, 0x6c, 0x65, 0x43,
+	0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69, 0x6f,
+	0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x65, 0x78, 0x74, 0x65, 0x6e, 0x73, 0x69,
+	0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18,
+	0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x61, 0x64, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73,
+	0x12, 0x24, 0x0a, 0x0d, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e,
+	0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0d, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x63,
+	0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x69,
+	0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x09, 0x64, 0x65, 0x6c, 0x65, 0x74,
+	0x69, 0x6f, 0x6e, 0x73, 0x42, 0x28, 0x5a, 0x26, 0x61, 0x6e, 0x64, 0x72, 0x6f, 0x69, 0x64, 0x2f,
+	0x73, 0x6f, 0x6f, 0x6e, 0x67, 0x2f, 0x75, 0x69, 0x2f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73,
+	0x2f, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x73, 0x5f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
 }
 
 var (
@@ -2656,7 +2850,7 @@
 }
 
 var file_metrics_proto_enumTypes = make([]protoimpl.EnumInfo, 5)
-var file_metrics_proto_msgTypes = make([]protoimpl.MessageInfo, 21)
+var file_metrics_proto_msgTypes = make([]protoimpl.MessageInfo, 23)
 var file_metrics_proto_goTypes = []interface{}{
 	(MetricsBase_BuildVariant)(0),                          // 0: soong_build_metrics.MetricsBase.BuildVariant
 	(MetricsBase_Arch)(0),                                  // 1: soong_build_metrics.MetricsBase.Arch
@@ -2682,8 +2876,10 @@
 	(*CriticalPathInfo)(nil),                               // 21: soong_build_metrics.CriticalPathInfo
 	(*JobInfo)(nil),                                        // 22: soong_build_metrics.JobInfo
 	(*OptimizedBuildMetrics)(nil),                          // 23: soong_build_metrics.OptimizedBuildMetrics
-	(*OptimizedBuildMetrics_TargetOptimizationResult)(nil), // 24: soong_build_metrics.OptimizedBuildMetrics.TargetOptimizationResult
-	(*OptimizedBuildMetrics_TargetOptimizationResult_OutputArtifact)(nil), // 25: soong_build_metrics.OptimizedBuildMetrics.TargetOptimizationResult.OutputArtifact
+	(*AggregatedFileList)(nil),                             // 24: soong_build_metrics.AggregatedFileList
+	(*FileCount)(nil),                                      // 25: soong_build_metrics.FileCount
+	(*OptimizedBuildMetrics_TargetOptimizationResult)(nil), // 26: soong_build_metrics.OptimizedBuildMetrics.TargetOptimizationResult
+	(*OptimizedBuildMetrics_TargetOptimizationResult_OutputArtifact)(nil), // 27: soong_build_metrics.OptimizedBuildMetrics.TargetOptimizationResult.OutputArtifact
 }
 var file_metrics_proto_depIdxs = []int32{
 	0,  // 0: soong_build_metrics.MetricsBase.target_build_variant:type_name -> soong_build_metrics.MetricsBase.BuildVariant
@@ -2719,14 +2915,15 @@
 	22, // 30: soong_build_metrics.CriticalPathInfo.long_running_jobs:type_name -> soong_build_metrics.JobInfo
 	10, // 31: soong_build_metrics.OptimizedBuildMetrics.analysis_perf:type_name -> soong_build_metrics.PerfInfo
 	10, // 32: soong_build_metrics.OptimizedBuildMetrics.packaging_perf:type_name -> soong_build_metrics.PerfInfo
-	24, // 33: soong_build_metrics.OptimizedBuildMetrics.target_result:type_name -> soong_build_metrics.OptimizedBuildMetrics.TargetOptimizationResult
-	10, // 34: soong_build_metrics.OptimizedBuildMetrics.TargetOptimizationResult.packaging_perf:type_name -> soong_build_metrics.PerfInfo
-	25, // 35: soong_build_metrics.OptimizedBuildMetrics.TargetOptimizationResult.output_artifact:type_name -> soong_build_metrics.OptimizedBuildMetrics.TargetOptimizationResult.OutputArtifact
-	36, // [36:36] is the sub-list for method output_type
-	36, // [36:36] is the sub-list for method input_type
-	36, // [36:36] is the sub-list for extension type_name
-	36, // [36:36] is the sub-list for extension extendee
-	0,  // [0:36] is the sub-list for field type_name
+	26, // 33: soong_build_metrics.OptimizedBuildMetrics.target_result:type_name -> soong_build_metrics.OptimizedBuildMetrics.TargetOptimizationResult
+	25, // 34: soong_build_metrics.AggregatedFileList.counts:type_name -> soong_build_metrics.FileCount
+	10, // 35: soong_build_metrics.OptimizedBuildMetrics.TargetOptimizationResult.packaging_perf:type_name -> soong_build_metrics.PerfInfo
+	27, // 36: soong_build_metrics.OptimizedBuildMetrics.TargetOptimizationResult.output_artifact:type_name -> soong_build_metrics.OptimizedBuildMetrics.TargetOptimizationResult.OutputArtifact
+	37, // [37:37] is the sub-list for method output_type
+	37, // [37:37] is the sub-list for method input_type
+	37, // [37:37] is the sub-list for extension type_name
+	37, // [37:37] is the sub-list for extension extendee
+	0,  // [0:37] is the sub-list for field type_name
 }
 
 func init() { file_metrics_proto_init() }
@@ -2964,7 +3161,7 @@
 			}
 		}
 		file_metrics_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} {
-			switch v := v.(*OptimizedBuildMetrics_TargetOptimizationResult); i {
+			switch v := v.(*AggregatedFileList); i {
 			case 0:
 				return &v.state
 			case 1:
@@ -2976,6 +3173,30 @@
 			}
 		}
 		file_metrics_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*FileCount); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_metrics_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*OptimizedBuildMetrics_TargetOptimizationResult); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_metrics_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} {
 			switch v := v.(*OptimizedBuildMetrics_TargetOptimizationResult_OutputArtifact); i {
 			case 0:
 				return &v.state
@@ -2994,7 +3215,7 @@
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_metrics_proto_rawDesc,
 			NumEnums:      5,
-			NumMessages:   21,
+			NumMessages:   23,
 			NumExtensions: 0,
 			NumServices:   0,
 		},
diff --git a/ui/metrics/metrics_proto/metrics.proto b/ui/metrics/metrics_proto/metrics.proto
index 3fbe97c..8437b65 100644
--- a/ui/metrics/metrics_proto/metrics.proto
+++ b/ui/metrics/metrics_proto/metrics.proto
@@ -451,3 +451,40 @@
     }
   }
 }
+
+// This is created by soong_ui from the various
+// android.find_input_delta_proto.FileList metrics provided to it by
+// find_input_delta.
+message AggregatedFileList {
+  // The arguments provided on the command line.
+  repeated string command_args = 1;
+
+  // The (possibly truncated list of) added files.
+  repeated string additions = 2;
+
+  // The (possibly truncated list of) changed files.
+  repeated string changes = 3;
+
+  // The (possibly truncated list of) deleted files.
+  repeated string deletions = 4;
+
+  // Count of files added/changed/deleted.
+  optional uint32 total_delta = 5;
+
+  // Counts by extension.
+  repeated FileCount counts = 6;
+}
+
+message FileCount {
+  // The file extension
+  optional string extension = 1;
+
+  // Number of added files with this extension.
+  optional uint32 additions = 2;
+
+  // Number of modified files with this extension.
+  optional uint32 modifications = 3;
+
+  // Number of deleted files with this extension.
+  optional uint32 deletions = 4;
+}
diff --git a/ui/metrics/metrics_proto/regen.sh b/ui/metrics/metrics_proto/regen.sh
index 5e5f9b8..8eb2d74 100755
--- a/ui/metrics/metrics_proto/regen.sh
+++ b/ui/metrics/metrics_proto/regen.sh
@@ -12,6 +12,6 @@
   die "could not find aprotoc. ${error_msg}"
 fi
 
-if ! aprotoc --go_out=paths=source_relative:. -I .:../../.. metrics.proto combined_metrics.proto; then
+if ! aprotoc --go_out=paths=source_relative:. metrics.proto; then
   die "build failed. ${error_msg}"
 fi