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