Upload build metrics after a build is completed.
Soong now supports the ability to upload metrics to another location
by setting the ANDROID_ENABLE_METRICS_UPLOAD to an uploader that
accepts the upload.proto proto buffer message. When the environment
variable is set, a set of build metrics files (soong_metrics,
rbe_metrics.pb and build_error) is uploaded.
Bug: 140638454
Test: * Wrote unit test cases
* Setup the uploader, built a succcessful and failed aosp_arm-eng
target and monitor the uploading of the metrics.
Change-Id: I76a65739c557dc90345e098ca03119a950ece2d2
diff --git a/ui/build/Android.bp b/ui/build/Android.bp
index 2a5a51a..0a0bb16 100644
--- a/ui/build/Android.bp
+++ b/ui/build/Android.bp
@@ -56,12 +56,14 @@
"signal.go",
"soong.go",
"test_build.go",
+ "upload.go",
"util.go",
],
testSrcs: [
"cleanbuild_test.go",
"config_test.go",
"environment_test.go",
+ "upload_test.go",
"util_test.go",
"proc_sync_test.go",
],
diff --git a/ui/build/config.go b/ui/build/config.go
index d66a86c..49f506e 100644
--- a/ui/build/config.go
+++ b/ui/build/config.go
@@ -961,3 +961,14 @@
func (c *configImpl) IsPdkBuild() bool {
return c.pdkBuild
}
+
+func (c *configImpl) BuildDateTime() string {
+ return c.buildDateTime
+}
+
+func (c *configImpl) MetricsUploaderApp() string {
+ if p, ok := c.environ.Get("ANDROID_ENABLE_METRICS_UPLOAD"); ok {
+ return p
+ }
+ return ""
+}
diff --git a/ui/build/config_test.go b/ui/build/config_test.go
index df618c4..7b14c47 100644
--- a/ui/build/config_test.go
+++ b/ui/build/config_test.go
@@ -26,6 +26,7 @@
"testing"
"android/soong/ui/logger"
+ "android/soong/ui/status"
)
func testContext() Context {
@@ -33,6 +34,7 @@
Context: context.Background(),
Logger: logger.New(&bytes.Buffer{}),
Writer: &bytes.Buffer{},
+ Status: &status.Status{},
}}
}
diff --git a/ui/build/upload.go b/ui/build/upload.go
new file mode 100644
index 0000000..75a5e2f
--- /dev/null
+++ b/ui/build/upload.go
@@ -0,0 +1,80 @@
+// Copyright 2020 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 build
+
+// This file contains the functionality to upload data from one location to
+// another.
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "time"
+
+ "github.com/golang/protobuf/proto"
+
+ upload_proto "android/soong/ui/metrics/upload_proto"
+)
+
+const (
+ uploadPbFilename = ".uploader.pb"
+)
+
+// UploadMetrics uploads a set of metrics files to a server for analysis. An
+// uploader full path is required to be specified in order to upload the set
+// of metrics files. This is accomplished by defining the ANDROID_ENABLE_METRICS_UPLOAD
+// environment variable.
+func UploadMetrics(ctx Context, config Config, buildStartedMilli int64, files ...string) {
+ uploader := config.MetricsUploaderApp()
+ // No metrics to upload if the path to the uploader was not specified.
+ if uploader == "" {
+ return
+ }
+
+ // Some files may not exist. For example, build errors protobuf file
+ // may not exist since the build was successful.
+ var metricsFiles []string
+ for _, f := range files {
+ if _, err := os.Stat(f); err == nil {
+ metricsFiles = append(metricsFiles, f)
+ }
+ }
+
+ if len(metricsFiles) == 0 {
+ return
+ }
+
+ // For platform builds, the branch and target name is hardcoded to specific
+ // values for later extraction of the metrics in the data metrics pipeline.
+ data, err := proto.Marshal(&upload_proto.Upload{
+ CreationTimestampMs: proto.Uint64(uint64(buildStartedMilli)),
+ CompletionTimestampMs: proto.Uint64(uint64(time.Now().UnixNano() / int64(time.Millisecond))),
+ BranchName: proto.String("developer-metrics"),
+ TargetName: proto.String("platform-build-systems-metrics"),
+ MetricsFiles: metricsFiles,
+ })
+ if err != nil {
+ ctx.Fatalf("failed to marshal metrics upload proto buffer message: %v\n", err)
+ }
+
+ pbFile := filepath.Join(config.OutDir(), uploadPbFilename)
+ if err := ioutil.WriteFile(pbFile, data, 0644); err != nil {
+ ctx.Fatalf("failed to write the marshaled metrics upload protobuf to %q: %v\n", pbFile, err)
+ }
+ // Remove the upload file as it's not longer needed after it has been processed by the uploader.
+ defer os.Remove(pbFile)
+
+ Command(ctx, config, "upload metrics", uploader, "--upload-metrics", pbFile).RunAndStreamOrFatal()
+}
diff --git a/ui/build/upload_test.go b/ui/build/upload_test.go
new file mode 100644
index 0000000..adaa08d
--- /dev/null
+++ b/ui/build/upload_test.go
@@ -0,0 +1,120 @@
+// Copyright 2020 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 build
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strconv"
+ "strings"
+ "testing"
+ "time"
+
+ "android/soong/ui/logger"
+)
+
+func TestUploadMetrics(t *testing.T) {
+ ctx := testContext()
+ tests := []struct {
+ description string
+ uploader string
+ createFiles bool
+ files []string
+ }{{
+ description: "ANDROID_ENABLE_METRICS_UPLOAD not set",
+ }, {
+ description: "no metrics files to upload",
+ uploader: "fake",
+ }, {
+ description: "non-existent metrics files no upload",
+ uploader: "fake",
+ files: []string{"metrics_file_1", "metrics_file_2", "metrics_file_3"},
+ }, {
+ description: "trigger upload",
+ uploader: "echo",
+ createFiles: true,
+ files: []string{"metrics_file_1", "metrics_file_2"},
+ }}
+
+ for _, tt := range tests {
+ t.Run(tt.description, func(t *testing.T) {
+ defer logger.Recover(func(err error) {
+ t.Fatalf("got unexpected error: %v", err)
+ })
+
+ outDir, err := ioutil.TempDir("", "")
+ if err != nil {
+ t.Fatalf("failed to create out directory: %v", outDir)
+ }
+ defer os.RemoveAll(outDir)
+
+ var metricsFiles []string
+ if tt.createFiles {
+ for _, f := range tt.files {
+ filename := filepath.Join(outDir, f)
+ metricsFiles = append(metricsFiles, filename)
+ if err := ioutil.WriteFile(filename, []byte("test file"), 0644); err != nil {
+ t.Fatalf("failed to create a fake metrics file %q for uploading: %v", filename, err)
+ }
+ }
+ }
+
+ config := Config{&configImpl{
+ environ: &Environment{
+ "OUT_DIR=" + outDir,
+ "ANDROID_ENABLE_METRICS_UPLOAD=" + tt.uploader,
+ },
+ buildDateTime: strconv.FormatInt(time.Now().UnixNano()/int64(time.Millisecond), 10),
+ }}
+
+ UploadMetrics(ctx, config, 1591031903, metricsFiles...)
+
+ if _, err := os.Stat(filepath.Join(outDir, uploadPbFilename)); err == nil {
+ t.Error("got true, want false for upload protobuf file to exist")
+ }
+ })
+ }
+}
+
+func TestUploadMetricsErrors(t *testing.T) {
+ expectedErr := "failed to write the marshaled"
+ defer logger.Recover(func(err error) {
+ got := err.Error()
+ if !strings.Contains(got, expectedErr) {
+ t.Errorf("got %q, want %q to be contained in error", got, expectedErr)
+ }
+ })
+
+ outDir, err := ioutil.TempDir("", "")
+ if err != nil {
+ t.Fatalf("failed to create out directory: %v", outDir)
+ }
+ defer os.RemoveAll(outDir)
+
+ metricsFile := filepath.Join(outDir, "metrics_file_1")
+ if err := ioutil.WriteFile(metricsFile, []byte("test file"), 0644); err != nil {
+ t.Fatalf("failed to create a fake metrics file %q for uploading: %v", metricsFile, err)
+ }
+
+ config := Config{&configImpl{
+ environ: &Environment{
+ "ANDROID_ENABLE_METRICS_UPLOAD=fake",
+ "OUT_DIR=/bad",
+ }}}
+
+ UploadMetrics(testContext(), config, 1591031903, metricsFile)
+ t.Errorf("got nil, expecting %q as a failure", expectedErr)
+}