blob: 1e6d94aad4faa237c84e5de194ca203e0353c15c [file] [log] [blame]
Patrice Arruda219eef32020-06-01 17:29:30 +00001// Copyright 2020 Google Inc. All rights reserved.
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
15package build
16
17// This file contains the functionality to upload data from one location to
18// another.
19
20import (
MarkDacek00e31522023-01-06 22:15:24 +000021 "bufio"
usta4f5d2c12022-10-28 23:32:01 -040022 "fmt"
Patrice Arruda219eef32020-06-01 17:29:30 +000023 "io/ioutil"
24 "os"
25 "path/filepath"
MarkDacek00e31522023-01-06 22:15:24 +000026 "strconv"
27 "strings"
Patrice Arruda219eef32020-06-01 17:29:30 +000028 "time"
29
MarkDacek00e31522023-01-06 22:15:24 +000030 "android/soong/shared"
Patrice Arruda7cc20742020-06-10 18:48:01 +000031 "android/soong/ui/metrics"
Dan Willemsen4591b642021-05-24 14:24:12 -070032
33 "google.golang.org/protobuf/proto"
Patrice Arruda219eef32020-06-01 17:29:30 +000034
MarkDacek00e31522023-01-06 22:15:24 +000035 bazel_metrics_proto "android/soong/ui/metrics/bazel_metrics_proto"
Patrice Arruda219eef32020-06-01 17:29:30 +000036 upload_proto "android/soong/ui/metrics/upload_proto"
37)
38
39const (
Patrice Arruda92dc64f2020-11-16 16:29:16 -080040 // Used to generate a raw protobuf file that contains information
41 // of the list of metrics files from host to destination storage.
Patrice Arruda219eef32020-06-01 17:29:30 +000042 uploadPbFilename = ".uploader.pb"
43)
44
Patrice Arruda7cc20742020-06-10 18:48:01 +000045var (
Patrice Arruda92dc64f2020-11-16 16:29:16 -080046 // For testing purpose.
47 tmpDir = ioutil.TempDir
Patrice Arruda7cc20742020-06-10 18:48:01 +000048)
49
Patrice Arruda7d235cc2020-12-09 22:43:26 +000050// pruneMetricsFiles iterates the list of paths, checking if a path exist.
51// If a path is a file, it is added to the return list. If the path is a
52// directory, a recursive call is made to add the children files of the
53// path.
54func pruneMetricsFiles(paths []string) []string {
55 var metricsFiles []string
56 for _, p := range paths {
57 fi, err := os.Stat(p)
58 // Some paths passed may not exist. For example, build errors protobuf
59 // file may not exist since the build was successful.
60 if err != nil {
61 continue
62 }
63
64 if fi.IsDir() {
usta4f5d2c12022-10-28 23:32:01 -040065 if l, err := ioutil.ReadDir(p); err != nil {
66 _, _ = fmt.Fprintf(os.Stderr, "Failed to find files under %s\n", p)
67 } else {
Patrice Arruda7d235cc2020-12-09 22:43:26 +000068 files := make([]string, 0, len(l))
69 for _, fi := range l {
70 files = append(files, filepath.Join(p, fi.Name()))
71 }
72 metricsFiles = append(metricsFiles, pruneMetricsFiles(files)...)
73 }
74 } else {
75 metricsFiles = append(metricsFiles, p)
76 }
77 }
78 return metricsFiles
79}
80
MarkDacek00e31522023-01-06 22:15:24 +000081func parseTimingToNanos(str string) int64 {
82 millisString := removeDecimalPoint(str)
83 timingMillis, _ := strconv.ParseInt(millisString, 10, 64)
84 return timingMillis * 1000000
85}
86
87func parsePercentageToTenThousandths(str string) int32 {
88 percentageString := removeDecimalPoint(str)
89 //remove the % at the end of the string
90 percentage := strings.ReplaceAll(percentageString, "%", "")
91 percentagePortion, _ := strconv.ParseInt(percentage, 10, 32)
92 return int32(percentagePortion)
93}
94
95func removeDecimalPoint(numString string) string {
96 // The format is always 0.425 or 10.425
97 return strings.ReplaceAll(numString, ".", "")
98}
99
100func parseTotal(line string) int64 {
101 words := strings.Fields(line)
102 timing := words[3]
103 return parseTimingToNanos(timing)
104}
105
106func parsePhaseTiming(line string) bazel_metrics_proto.PhaseTiming {
107 words := strings.Fields(line)
108 getPhaseNameAndTimingAndPercentage := func([]string) (string, int64, int32) {
109 // Sample lines include:
110 // Total launch phase time 0.011 s 2.59%
111 // Total target pattern evaluation phase time 0.011 s 2.59%
112 var beginning int
113 var end int
114 for ind, word := range words {
115 if word == "Total" {
116 beginning = ind + 1
117 } else if beginning > 0 && word == "phase" {
118 end = ind
119 break
120 }
121 }
122 phaseName := strings.Join(words[beginning:end], " ")
123
124 // end is now "phase" - advance by 2 for timing and 4 for percentage
125 percentageString := words[end+4]
126 timingString := words[end+2]
127 timing := parseTimingToNanos(timingString)
128 percentagePortion := parsePercentageToTenThousandths(percentageString)
129 return phaseName, timing, percentagePortion
130 }
131
132 phaseName, timing, portion := getPhaseNameAndTimingAndPercentage(words)
133 phaseTiming := bazel_metrics_proto.PhaseTiming{}
134 phaseTiming.DurationNanos = &timing
135 phaseTiming.PortionOfBuildTime = &portion
136
137 phaseTiming.PhaseName = &phaseName
138 return phaseTiming
139}
140
MarkDaceked2253e2023-05-02 21:35:39 +0000141// This method takes a file created by bazel's --analyze-profile mode and
142// writes bazel metrics data to the provided filepath.
143// TODO(b/279987768) - move this outside of upload.go
MarkDacek00e31522023-01-06 22:15:24 +0000144func processBazelMetrics(bazelProfileFile string, bazelMetricsFile string, ctx Context) {
145 if bazelProfileFile == "" {
146 return
147 }
148
149 readBazelProto := func(filepath string) bazel_metrics_proto.BazelMetrics {
150 //serialize the proto, write it
151 bazelMetrics := bazel_metrics_proto.BazelMetrics{}
152
153 file, err := os.ReadFile(filepath)
154 if err != nil {
155 ctx.Fatalln("Error reading metrics file\n", err)
156 }
157
158 scanner := bufio.NewScanner(strings.NewReader(string(file)))
159 scanner.Split(bufio.ScanLines)
160
161 var phaseTimings []*bazel_metrics_proto.PhaseTiming
162 for scanner.Scan() {
163 line := scanner.Text()
164 if strings.HasPrefix(line, "Total run time") {
165 total := parseTotal(line)
166 bazelMetrics.Total = &total
167 } else if strings.HasPrefix(line, "Total") {
168 phaseTiming := parsePhaseTiming(line)
169 phaseTimings = append(phaseTimings, &phaseTiming)
170 }
171 }
172 bazelMetrics.PhaseTimings = phaseTimings
173
174 return bazelMetrics
175 }
176
177 if _, err := os.Stat(bazelProfileFile); err != nil {
178 // We can assume bazel didn't run if the profile doesn't exist
179 return
180 }
181 bazelProto := readBazelProto(bazelProfileFile)
182 shared.Save(&bazelProto, bazelMetricsFile)
183}
184
Yu Liu6e13b402021-07-27 14:29:06 -0700185// UploadMetrics uploads a set of metrics files to a server for analysis.
186// The metrics files are first copied to a temporary directory
187// and the uploader is then executed in the background to allow the user/system
188// to continue working. Soong communicates to the uploader through the
189// upload_proto raw protobuf file.
MarkDacek00e31522023-01-06 22:15:24 +0000190func UploadMetrics(ctx Context, config Config, simpleOutput bool, buildStarted time.Time, bazelProfileFile string, bazelMetricsFile string, paths ...string) {
Patrice Arruda7cc20742020-06-10 18:48:01 +0000191 ctx.BeginTrace(metrics.RunSetupTool, "upload_metrics")
192 defer ctx.EndTrace()
193
Patrice Arruda219eef32020-06-01 17:29:30 +0000194 uploader := config.MetricsUploaderApp()
MarkDaceked2253e2023-05-02 21:35:39 +0000195 processBazelMetrics(bazelProfileFile, bazelMetricsFile, ctx)
196
Patrice Arruda219eef32020-06-01 17:29:30 +0000197 if uploader == "" {
Patrice Arruda92dc64f2020-11-16 16:29:16 -0800198 // If the uploader path was not specified, no metrics shall be uploaded.
Patrice Arruda219eef32020-06-01 17:29:30 +0000199 return
200 }
201
Patrice Arruda7d235cc2020-12-09 22:43:26 +0000202 // Several of the files might be directories.
203 metricsFiles := pruneMetricsFiles(paths)
Patrice Arruda219eef32020-06-01 17:29:30 +0000204 if len(metricsFiles) == 0 {
205 return
206 }
207
Patrice Arruda7cc20742020-06-10 18:48:01 +0000208 // The temporary directory cannot be deleted as the metrics uploader is started
209 // in the background and requires to exist until the operation is done. The
210 // uploader can delete the directory as it is specified in the upload proto.
Patrice Arruda92dc64f2020-11-16 16:29:16 -0800211 tmpDir, err := tmpDir("", "upload_metrics")
Patrice Arruda7cc20742020-06-10 18:48:01 +0000212 if err != nil {
213 ctx.Fatalf("failed to create a temporary directory to store the list of metrics files: %v\n", err)
214 }
215
216 for i, src := range metricsFiles {
217 dst := filepath.Join(tmpDir, filepath.Base(src))
218 if _, err := copyFile(src, dst); err != nil {
219 ctx.Fatalf("failed to copy %q to %q: %v\n", src, dst, err)
220 }
221 metricsFiles[i] = dst
222 }
223
Patrice Arruda219eef32020-06-01 17:29:30 +0000224 // For platform builds, the branch and target name is hardcoded to specific
225 // values for later extraction of the metrics in the data metrics pipeline.
226 data, err := proto.Marshal(&upload_proto.Upload{
Patrice Arruda73c790f2020-07-13 23:01:18 +0000227 CreationTimestampMs: proto.Uint64(uint64(buildStarted.UnixNano() / int64(time.Millisecond))),
Patrice Arruda219eef32020-06-01 17:29:30 +0000228 CompletionTimestampMs: proto.Uint64(uint64(time.Now().UnixNano() / int64(time.Millisecond))),
229 BranchName: proto.String("developer-metrics"),
230 TargetName: proto.String("platform-build-systems-metrics"),
231 MetricsFiles: metricsFiles,
Patrice Arruda7cc20742020-06-10 18:48:01 +0000232 DirectoriesToDelete: []string{tmpDir},
Patrice Arruda219eef32020-06-01 17:29:30 +0000233 })
234 if err != nil {
235 ctx.Fatalf("failed to marshal metrics upload proto buffer message: %v\n", err)
236 }
237
Patrice Arruda7cc20742020-06-10 18:48:01 +0000238 pbFile := filepath.Join(tmpDir, uploadPbFilename)
Patrice Arruda219eef32020-06-01 17:29:30 +0000239 if err := ioutil.WriteFile(pbFile, data, 0644); err != nil {
240 ctx.Fatalf("failed to write the marshaled metrics upload protobuf to %q: %v\n", pbFile, err)
241 }
Patrice Arruda219eef32020-06-01 17:29:30 +0000242
Patrice Arruda7cc20742020-06-10 18:48:01 +0000243 // Start the uploader in the background as it takes several milliseconds to start the uploader
Patrice Arruda92dc64f2020-11-16 16:29:16 -0800244 // and prepare the metrics for upload. This affects small shell commands like "lunch".
Patrice Arruda7cc20742020-06-10 18:48:01 +0000245 cmd := Command(ctx, config, "upload metrics", uploader, "--upload-metrics", pbFile)
Patrice Arrudaf445ba12020-07-28 17:49:01 +0000246 if simpleOutput {
Patrice Arruda7cc20742020-06-10 18:48:01 +0000247 cmd.RunOrFatal()
248 } else {
249 cmd.RunAndStreamOrFatal()
250 }
Patrice Arruda219eef32020-06-01 17:29:30 +0000251}