blob: 3cc18b6726924c2ffd25c0ea2afd9239e8fe96a1 [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
Takuto Ikuta96fdf7c2018-12-03 09:18:39 +000026import subprocess
Takuto Ikuta9af233a2018-11-29 03:53:53 +000027import sys
Takuto Ikuta36248fc2019-01-11 03:02:32 +000028import time
Takuto Ikuta9af233a2018-11-29 03:53:53 +000029
Takuto Ikuta5c00a222019-02-18 04:32:51 +000030
31ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
32sys.path.insert(0, os.path.join(ROOT_DIR, 'third_party'))
33
34import httplib2
Takuto Ikuta9af233a2018-11-29 03:53:53 +000035
Takuto Ikutac8069af2019-01-09 06:24:56 +000036# These build configs affect build performance a lot.
37# TODO(tikuta): Add 'blink_symbol_level', 'closure_compile' and
38# 'use_jumbo_build'.
39WHITELISTED_CONFIGS = (
40 'symbol_level', 'use_goma', 'is_debug', 'is_component_build', 'enable_nacl',
41 'host_os', 'host_cpu', 'target_os', 'target_cpu'
42)
43
Takuto Ikuta9af233a2018-11-29 03:53:53 +000044def IsGoogler(server):
45 """Check whether this script run inside corp network."""
46 try:
47 h = httplib2.Http()
48 _, content = h.request('https://'+server+'/should-upload', 'GET')
49 return content == 'Success'
50 except httplib2.HttpLib2Error:
51 return False
52
Takuto Ikuta96fdf7c2018-12-03 09:18:39 +000053def ParseGNArgs(gn_args):
Takuto Ikuta61cb9d62018-12-17 23:45:49 +000054 """Parse gn_args as json and return config dictionary."""
Takuto Ikuta96fdf7c2018-12-03 09:18:39 +000055 configs = json.loads(gn_args)
56 build_configs = {}
Takuto Ikutac8069af2019-01-09 06:24:56 +000057
Takuto Ikuta96fdf7c2018-12-03 09:18:39 +000058 for config in configs:
Takuto Ikutac8069af2019-01-09 06:24:56 +000059 key = config["name"]
60 if key not in WHITELISTED_CONFIGS:
61 continue
62 if 'current' in config:
63 build_configs[key] = config['current']['value']
64 else:
65 build_configs[key] = config['default']['value']
66
Takuto Ikuta96fdf7c2018-12-03 09:18:39 +000067 return build_configs
68
Takuto Ikutacf56a4b2018-12-18 05:47:26 +000069def GetBuildTargetFromCommandLine(cmdline):
70 """Get build targets from commandline."""
71
72 # Skip argv0.
73 idx = 1
74
75 # Skipping all args that involve these flags, and taking all remaining args
76 # as targets.
77 onearg_flags = ('-C', '-f', '-j', '-k', '-l', '-d', '-t', '-w')
78 zeroarg_flags = ('--version', '-n', '-v')
79
80 targets = []
81
82 while idx < len(cmdline):
83 if cmdline[idx] in onearg_flags:
84 idx += 2
85 continue
86
87 if (cmdline[idx][:2] in onearg_flags or
88 cmdline[idx] in zeroarg_flags):
89 idx += 1
90 continue
91
92 targets.append(cmdline[idx])
93 idx += 1
94
95 return targets
96
Takuto Ikutac8069af2019-01-09 06:24:56 +000097def GetJflag(cmdline):
98 """Parse cmdline to get flag value for -j"""
99
100 for i in range(len(cmdline)):
101 if (cmdline[i] == '-j' and i + 1 < len(cmdline) and
102 cmdline[i+1].isdigit()):
103 return int(cmdline[i+1])
104
105 if (cmdline[i].startswith('-j') and
106 cmdline[i][len('-j'):].isdigit()):
107 return int(cmdline[i][len('-j'):])
108
Takuto Ikuta96fdf7c2018-12-03 09:18:39 +0000109
Takuto Ikuta9af233a2018-11-29 03:53:53 +0000110def GetMetadata(cmdline, ninjalog):
Takuto Ikutac8069af2019-01-09 06:24:56 +0000111 """Get metadata for uploaded ninjalog.
112
113 Returned metadata has schema defined in
114 https://cs.chromium.org?q="type+Metadata+struct+%7B"+file:%5Einfra/go/src/infra/appengine/chromium_build_stats/ninjalog/
115
116 TODO(tikuta): Collect GOMA_* env var.
117 """
Takuto Ikuta9af233a2018-11-29 03:53:53 +0000118
Takuto Ikuta9af233a2018-11-29 03:53:53 +0000119 build_dir = os.path.dirname(ninjalog)
Takuto Ikuta96fdf7c2018-12-03 09:18:39 +0000120
121 build_configs = {}
122
123 try:
Takuto Ikutac8069af2019-01-09 06:24:56 +0000124 args = ['gn', 'args', build_dir, '--list', '--short', '--json']
Takuto Ikuta96fdf7c2018-12-03 09:18:39 +0000125 if sys.platform == 'win32':
126 # gn in PATH is bat file in windows environment (except cygwin).
127 args = ['cmd', '/c'] + args
128
129 gn_args = subprocess.check_output(args)
130 build_configs = ParseGNArgs(gn_args)
131 except subprocess.CalledProcessError as e:
132 logging.error("Failed to call gn %s", e)
133 build_configs = {}
134
135 # Stringify config.
136 for k in build_configs:
137 build_configs[k] = str(build_configs[k])
138
Takuto Ikuta9af233a2018-11-29 03:53:53 +0000139 metadata = {
140 'platform': platform.system(),
Takuto Ikuta9af233a2018-11-29 03:53:53 +0000141 'cpu_core': multiprocessing.cpu_count(),
Takuto Ikuta96fdf7c2018-12-03 09:18:39 +0000142 'build_configs': build_configs,
Takuto Ikutac8069af2019-01-09 06:24:56 +0000143 'targets': GetBuildTargetFromCommandLine(cmdline),
Takuto Ikuta9af233a2018-11-29 03:53:53 +0000144 }
145
Takuto Ikutac8069af2019-01-09 06:24:56 +0000146 jflag = GetJflag(cmdline)
147 if jflag is not None:
148 metadata['jobs'] = jflag
149
Takuto Ikuta9af233a2018-11-29 03:53:53 +0000150 return metadata
151
152def GetNinjalog(cmdline):
Takuto Ikuta61cb9d62018-12-17 23:45:49 +0000153 """GetNinjalog returns the path to ninjalog from cmdline."""
Takuto Ikuta9af233a2018-11-29 03:53:53 +0000154 # ninjalog is in current working directory by default.
155 ninjalog_dir = '.'
156
157 i = 0
158 while i < len(cmdline):
159 cmd = cmdline[i]
160 i += 1
161 if cmd == '-C' and i < len(cmdline):
162 ninjalog_dir = cmdline[i]
163 i += 1
164 continue
165
166 if cmd.startswith('-C') and len(cmd) > len('-C'):
167 ninjalog_dir = cmd[len('-C'):]
168
169 return os.path.join(ninjalog_dir, '.ninja_log')
170
171def main():
172 parser = argparse.ArgumentParser()
173 parser.add_argument('--server',
174 default='chromium-build-stats.appspot.com',
175 help='server to upload ninjalog file.')
176 parser.add_argument('--ninjalog', help='ninjalog file to upload.')
177 parser.add_argument('--verbose', action='store_true',
178 help='Enable verbose logging.')
179 parser.add_argument('--cmdline', required=True, nargs=argparse.REMAINDER,
180 help='command line args passed to ninja.')
181
182 args = parser.parse_args()
183
184 if args.verbose:
185 logging.basicConfig(level=logging.INFO)
186 else:
187 # Disable logging.
188 logging.disable(logging.CRITICAL)
189
190 if not IsGoogler(args.server):
191 return 0
192
193
194 ninjalog = args.ninjalog or GetNinjalog(args.cmdline)
195 if not os.path.isfile(ninjalog):
196 logging.warn("ninjalog is not found in %s", ninjalog)
197 return 1
198
Takuto Ikuta36248fc2019-01-11 03:02:32 +0000199 # We assume that each ninja invocation interval takes at least 2 seconds.
200 # This is not to have duplicate entry in server when current build is no-op.
201 if os.stat(ninjalog).st_mtime < time.time() - 2:
202 logging.info("ninjalog is not updated recently %s", ninjalog)
203 return 0
204
Takuto Ikuta9af233a2018-11-29 03:53:53 +0000205 output = cStringIO.StringIO()
206
207 with open(ninjalog) as f:
208 with gzip.GzipFile(fileobj=output, mode='wb') as g:
209 g.write(f.read())
210 g.write('# end of ninja log\n')
211
212 metadata = GetMetadata(args.cmdline, ninjalog)
Takuto Ikutac8069af2019-01-09 06:24:56 +0000213 logging.info('send metadata: %s', json.dumps(metadata))
Takuto Ikuta9af233a2018-11-29 03:53:53 +0000214 g.write(json.dumps(metadata))
215
216 h = httplib2.Http()
217 resp_headers, content = h.request(
218 'https://'+args.server+'/upload_ninja_log/', 'POST',
219 body=output.getvalue(), headers={'Content-Encoding': 'gzip'})
220
221 if resp_headers.status != 200:
222 logging.warn("unexpected status code for response: %s",
223 resp_headers.status)
224 return 1
225
226 logging.info('response header: %s', resp_headers)
227 logging.info('response content: %s', content)
228 return 0
229
230if __name__ == '__main__':
231 sys.exit(main())