1// 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 ( 21 "bufio" 22 "fmt" 23 "io/ioutil" 24 "os" 25 "path/filepath" 26 "strconv" 27 "strings" 28 "time" 29 30 "android/soong/shared" 31 "android/soong/ui/metrics" 32 33 "google.golang.org/protobuf/proto" 34 35 bazel_metrics_proto "android/soong/ui/metrics/bazel_metrics_proto" 36 upload_proto "android/soong/ui/metrics/upload_proto" 37) 38 39const ( 40 // Used to generate a raw protobuf file that contains information 41 // of the list of metrics files from host to destination storage. 42 uploadPbFilename = ".uploader.pb" 43) 44 45var ( 46 // For testing purpose. 47 tmpDir = ioutil.TempDir 48) 49 50// 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() { 65 if l, err := ioutil.ReadDir(p); err != nil { 66 _, _ = fmt.Fprintf(os.Stderr, "Failed to find files under %s\n", p) 67 } else { 68 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 81func 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 141func processBazelMetrics(bazelProfileFile string, bazelMetricsFile string, ctx Context) { 142 if bazelProfileFile == "" { 143 return 144 } 145 146 readBazelProto := func(filepath string) bazel_metrics_proto.BazelMetrics { 147 //serialize the proto, write it 148 bazelMetrics := bazel_metrics_proto.BazelMetrics{} 149 150 file, err := os.ReadFile(filepath) 151 if err != nil { 152 ctx.Fatalln("Error reading metrics file\n", err) 153 } 154 155 scanner := bufio.NewScanner(strings.NewReader(string(file))) 156 scanner.Split(bufio.ScanLines) 157 158 var phaseTimings []*bazel_metrics_proto.PhaseTiming 159 for scanner.Scan() { 160 line := scanner.Text() 161 if strings.HasPrefix(line, "Total run time") { 162 total := parseTotal(line) 163 bazelMetrics.Total = &total 164 } else if strings.HasPrefix(line, "Total") { 165 phaseTiming := parsePhaseTiming(line) 166 phaseTimings = append(phaseTimings, &phaseTiming) 167 } 168 } 169 bazelMetrics.PhaseTimings = phaseTimings 170 171 return bazelMetrics 172 } 173 174 if _, err := os.Stat(bazelProfileFile); err != nil { 175 // We can assume bazel didn't run if the profile doesn't exist 176 return 177 } 178 bazelProto := readBazelProto(bazelProfileFile) 179 shared.Save(&bazelProto, bazelMetricsFile) 180} 181 182// UploadMetrics uploads a set of metrics files to a server for analysis. 183// The metrics files are first copied to a temporary directory 184// and the uploader is then executed in the background to allow the user/system 185// to continue working. Soong communicates to the uploader through the 186// upload_proto raw protobuf file. 187func UploadMetrics(ctx Context, config Config, simpleOutput bool, buildStarted time.Time, bazelProfileFile string, bazelMetricsFile string, paths ...string) { 188 ctx.BeginTrace(metrics.RunSetupTool, "upload_metrics") 189 defer ctx.EndTrace() 190 191 uploader := config.MetricsUploaderApp() 192 if uploader == "" { 193 // If the uploader path was not specified, no metrics shall be uploaded. 194 return 195 } 196 197 processBazelMetrics(bazelProfileFile, bazelMetricsFile, ctx) 198 // Several of the files might be directories. 199 metricsFiles := pruneMetricsFiles(paths) 200 if len(metricsFiles) == 0 { 201 return 202 } 203 204 // The temporary directory cannot be deleted as the metrics uploader is started 205 // in the background and requires to exist until the operation is done. The 206 // uploader can delete the directory as it is specified in the upload proto. 207 tmpDir, err := tmpDir("", "upload_metrics") 208 if err != nil { 209 ctx.Fatalf("failed to create a temporary directory to store the list of metrics files: %v\n", err) 210 } 211 212 for i, src := range metricsFiles { 213 dst := filepath.Join(tmpDir, filepath.Base(src)) 214 if _, err := copyFile(src, dst); err != nil { 215 ctx.Fatalf("failed to copy %q to %q: %v\n", src, dst, err) 216 } 217 metricsFiles[i] = dst 218 } 219 220 // For platform builds, the branch and target name is hardcoded to specific 221 // values for later extraction of the metrics in the data metrics pipeline. 222 data, err := proto.Marshal(&upload_proto.Upload{ 223 CreationTimestampMs: proto.Uint64(uint64(buildStarted.UnixNano() / int64(time.Millisecond))), 224 CompletionTimestampMs: proto.Uint64(uint64(time.Now().UnixNano() / int64(time.Millisecond))), 225 BranchName: proto.String("developer-metrics"), 226 TargetName: proto.String("platform-build-systems-metrics"), 227 MetricsFiles: metricsFiles, 228 DirectoriesToDelete: []string{tmpDir}, 229 }) 230 if err != nil { 231 ctx.Fatalf("failed to marshal metrics upload proto buffer message: %v\n", err) 232 } 233 234 pbFile := filepath.Join(tmpDir, uploadPbFilename) 235 if err := ioutil.WriteFile(pbFile, data, 0644); err != nil { 236 ctx.Fatalf("failed to write the marshaled metrics upload protobuf to %q: %v\n", pbFile, err) 237 } 238 239 // Start the uploader in the background as it takes several milliseconds to start the uploader 240 // and prepare the metrics for upload. This affects small shell commands like "lunch". 241 cmd := Command(ctx, config, "upload metrics", uploader, "--upload-metrics", pbFile) 242 if simpleOutput { 243 cmd.RunOrFatal() 244 } else { 245 cmd.RunAndStreamOrFatal() 246 } 247} 248