• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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