blob: 467ceb8550c10ab6277da8bf2fee6255f50a40ba [file] [log] [blame]
Takuto Ikuta9af233a2018-11-29 03:53:53 +00001#!/usr/bin/env python
2# Copyright 2018 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""
7This is script to upload ninja_log from googler.
8
9Server side implementation is in
10https://cs.chromium.org/chromium/infra/go/src/infra/appengine/chromium_build_stats/
11
12Uploaded ninjalog is stored in BigQuery table having following schema.
13https://cs.chromium.org/chromium/infra/go/src/infra/appengine/chromium_build_stats/ninjaproto/ninjalog.proto
14
15The log will be used to analyze user side build performance.
16"""
17
18import argparse
19import cStringIO
20import gzip
21import json
22import logging
23import multiprocessing
24import os
25import platform
26import socket
Takuto Ikuta96fdf7c2018-12-03 09:18:39 +000027import subprocess
Takuto Ikuta9af233a2018-11-29 03:53:53 +000028import sys
Takuto Ikuta36248fc2019-01-11 03:02:32 +000029import time
Takuto Ikuta9af233a2018-11-29 03:53:53 +000030
Takuto Ikuta5c00a222019-02-18 04:32:51 +000031
32ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
33sys.path.insert(0, os.path.join(ROOT_DIR, 'third_party'))
34
35import httplib2
Takuto Ikuta9af233a2018-11-29 03:53:53 +000036
Takuto Ikutac8069af2019-01-09 06:24:56 +000037# These build configs affect build performance a lot.
38# TODO(tikuta): Add 'blink_symbol_level', 'closure_compile' and
39# 'use_jumbo_build'.
40WHITELISTED_CONFIGS = (
41 'symbol_level', 'use_goma', 'is_debug', 'is_component_build', 'enable_nacl',
42 'host_os', 'host_cpu', 'target_os', 'target_cpu'
43)
44
Takuto Ikuta9af233a2018-11-29 03:53:53 +000045def IsGoogler(server):
46 """Check whether this script run inside corp network."""
47 try:
48 h = httplib2.Http()
49 _, content = h.request('https://'+server+'/should-upload', 'GET')
50 return content == 'Success'
51 except httplib2.HttpLib2Error:
52 return False
53
Takuto Ikuta96fdf7c2018-12-03 09:18:39 +000054def ParseGNArgs(gn_args):
Takuto Ikuta61cb9d62018-12-17 23:45:49 +000055 """Parse gn_args as json and return config dictionary."""
Takuto Ikuta96fdf7c2018-12-03 09:18:39 +000056 configs = json.loads(gn_args)
57 build_configs = {}
Takuto Ikutac8069af2019-01-09 06:24:56 +000058
Takuto Ikuta96fdf7c2018-12-03 09:18:39 +000059 for config in configs:
Takuto Ikutac8069af2019-01-09 06:24:56 +000060 key = config["name"]
61 if key not in WHITELISTED_CONFIGS:
62 continue
63 if 'current' in config:
64 build_configs[key] = config['current']['value']
65 else:
66 build_configs[key] = config['default']['value']
67
Takuto Ikuta96fdf7c2018-12-03 09:18:39 +000068 return build_configs
69
Takuto Ikutacf56a4b2018-12-18 05:47:26 +000070def GetBuildTargetFromCommandLine(cmdline):
71 """Get build targets from commandline."""
72
73 # Skip argv0.
74 idx = 1
75
76 # Skipping all args that involve these flags, and taking all remaining args
77 # as targets.
78 onearg_flags = ('-C', '-f', '-j', '-k', '-l', '-d', '-t', '-w')
79 zeroarg_flags = ('--version', '-n', '-v')
80
81 targets = []
82
83 while idx < len(cmdline):
84 if cmdline[idx] in onearg_flags:
85 idx += 2
86 continue
87
88 if (cmdline[idx][:2] in onearg_flags or
89 cmdline[idx] in zeroarg_flags):
90 idx += 1
91 continue
92
93 targets.append(cmdline[idx])
94 idx += 1
95
96 return targets
97
Takuto Ikutac8069af2019-01-09 06:24:56 +000098def GetJflag(cmdline):
99 """Parse cmdline to get flag value for -j"""
100
101 for i in range(len(cmdline)):
102 if (cmdline[i] == '-j' and i + 1 < len(cmdline) and
103 cmdline[i+1].isdigit()):
104 return int(cmdline[i+1])
105
106 if (cmdline[i].startswith('-j') and
107 cmdline[i][len('-j'):].isdigit()):
108 return int(cmdline[i][len('-j'):])
109
Takuto Ikuta96fdf7c2018-12-03 09:18:39 +0000110
Takuto Ikuta9af233a2018-11-29 03:53:53 +0000111def GetMetadata(cmdline, ninjalog):
Takuto Ikutac8069af2019-01-09 06:24:56 +0000112 """Get metadata for uploaded ninjalog.
113
114 Returned metadata has schema defined in
115 https://cs.chromium.org?q="type+Metadata+struct+%7B"+file:%5Einfra/go/src/infra/appengine/chromium_build_stats/ninjalog/
116
117 TODO(tikuta): Collect GOMA_* env var.
118 """
Takuto Ikuta9af233a2018-11-29 03:53:53 +0000119
Takuto Ikuta9af233a2018-11-29 03:53:53 +0000120 build_dir = os.path.dirname(ninjalog)
Takuto Ikuta96fdf7c2018-12-03 09:18:39 +0000121
122 build_configs = {}
123
124 try:
Takuto Ikutac8069af2019-01-09 06:24:56 +0000125 args = ['gn', 'args', build_dir, '--list', '--short', '--json']
Takuto Ikuta96fdf7c2018-12-03 09:18:39 +0000126 if sys.platform == 'win32':
127 # gn in PATH is bat file in windows environment (except cygwin).
128 args = ['cmd', '/c'] + args
129
130 gn_args = subprocess.check_output(args)
131 build_configs = ParseGNArgs(gn_args)
132 except subprocess.CalledProcessError as e:
133 logging.error("Failed to call gn %s", e)
134 build_configs = {}
135
136 # Stringify config.
137 for k in build_configs:
138 build_configs[k] = str(build_configs[k])
139
Takuto Ikuta9af233a2018-11-29 03:53:53 +0000140 metadata = {
141 'platform': platform.system(),
Takuto Ikuta9af233a2018-11-29 03:53:53 +0000142 'cpu_core': multiprocessing.cpu_count(),
Takuto Ikuta96fdf7c2018-12-03 09:18:39 +0000143 'build_configs': build_configs,
Takuto Ikutac8069af2019-01-09 06:24:56 +0000144 'targets': GetBuildTargetFromCommandLine(cmdline),
Takuto Ikuta9af233a2018-11-29 03:53:53 +0000145 }
146
Takuto Ikutac8069af2019-01-09 06:24:56 +0000147 jflag = GetJflag(cmdline)
148 if jflag is not None:
149 metadata['jobs'] = jflag
150
Takuto Ikuta9af233a2018-11-29 03:53:53 +0000151 return metadata
152
153def GetNinjalog(cmdline):
Takuto Ikuta61cb9d62018-12-17 23:45:49 +0000154 """GetNinjalog returns the path to ninjalog from cmdline."""
Takuto Ikuta9af233a2018-11-29 03:53:53 +0000155 # ninjalog is in current working directory by default.
156 ninjalog_dir = '.'
157
158 i = 0
159 while i < len(cmdline):
160 cmd = cmdline[i]
161 i += 1
162 if cmd == '-C' and i < len(cmdline):
163 ninjalog_dir = cmdline[i]
164 i += 1
165 continue
166
167 if cmd.startswith('-C') and len(cmd) > len('-C'):
168 ninjalog_dir = cmd[len('-C'):]
169
170 return os.path.join(ninjalog_dir, '.ninja_log')
171
172def main():
173 parser = argparse.ArgumentParser()
174 parser.add_argument('--server',
175 default='chromium-build-stats.appspot.com',
176 help='server to upload ninjalog file.')
177 parser.add_argument('--ninjalog', help='ninjalog file to upload.')
178 parser.add_argument('--verbose', action='store_true',
179 help='Enable verbose logging.')
180 parser.add_argument('--cmdline', required=True, nargs=argparse.REMAINDER,
181 help='command line args passed to ninja.')
182
183 args = parser.parse_args()
184
185 if args.verbose:
186 logging.basicConfig(level=logging.INFO)
187 else:
188 # Disable logging.
189 logging.disable(logging.CRITICAL)
190
191 if not IsGoogler(args.server):
192 return 0
193
194
195 ninjalog = args.ninjalog or GetNinjalog(args.cmdline)
196 if not os.path.isfile(ninjalog):
197 logging.warn("ninjalog is not found in %s", ninjalog)
198 return 1
199
Takuto Ikuta36248fc2019-01-11 03:02:32 +0000200 # We assume that each ninja invocation interval takes at least 2 seconds.
201 # This is not to have duplicate entry in server when current build is no-op.
202 if os.stat(ninjalog).st_mtime < time.time() - 2:
203 logging.info("ninjalog is not updated recently %s", ninjalog)
204 return 0
205
Takuto Ikuta9af233a2018-11-29 03:53:53 +0000206 output = cStringIO.StringIO()
207
208 with open(ninjalog) as f:
209 with gzip.GzipFile(fileobj=output, mode='wb') as g:
210 g.write(f.read())
211 g.write('# end of ninja log\n')
212
213 metadata = GetMetadata(args.cmdline, ninjalog)
Takuto Ikutac8069af2019-01-09 06:24:56 +0000214 logging.info('send metadata: %s', json.dumps(metadata))
Takuto Ikuta9af233a2018-11-29 03:53:53 +0000215 g.write(json.dumps(metadata))
216
217 h = httplib2.Http()
218 resp_headers, content = h.request(
219 'https://'+args.server+'/upload_ninja_log/', 'POST',
220 body=output.getvalue(), headers={'Content-Encoding': 'gzip'})
221
222 if resp_headers.status != 200:
223 logging.warn("unexpected status code for response: %s",
224 resp_headers.status)
225 return 1
226
227 logging.info('response header: %s', resp_headers)
228 logging.info('response content: %s', content)
229 return 0
230
231if __name__ == '__main__':
232 sys.exit(main())