123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274 |
- #!/usr/bin/env python3
- # Copyright 2018 The Chromium Authors. All rights reserved.
- # Use of this source code is governed by a BSD-style license that can be
- # found in the LICENSE file.
- """
- This is script to upload ninja_log from googler.
- Server side implementation is in
- https://cs.chromium.org/chromium/infra/go/src/infra/appengine/chromium_build_stats/
- Uploaded ninjalog is stored in BigQuery table having following schema.
- https://cs.chromium.org/chromium/infra/go/src/infra/appengine/chromium_build_stats/ninjaproto/ninjalog.proto
- The log will be used to analyze user side build performance.
- See also the privacy review. http://eldar/assessments/656778450
- """
- import argparse
- import gzip
- import io
- import json
- import logging
- import multiprocessing
- import os
- import platform
- import subprocess
- import sys
- import time
- import urllib.request
- # These build configs affect build performance.
- ALLOWLISTED_CONFIGS = (
- "symbol_level",
- "use_goma",
- "use_remoteexec",
- "use_siso",
- "is_debug",
- "is_component_build",
- "enable_nacl",
- "host_os",
- "host_cpu",
- "target_os",
- "target_cpu",
- "blink_symbol_level",
- "is_java_debug",
- "treat_warnings_as_errors",
- "disable_android_lint",
- "use_errorprone_java_compiler",
- "incremental_install",
- "android_static_analysis",
- )
- def IsGoogler():
- """Check whether this user is Googler or not."""
- p = subprocess.run(
- "cipd auth-info",
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- text=True,
- shell=True,
- )
- if p.returncode != 0:
- return False
- lines = p.stdout.splitlines()
- if len(lines) == 0:
- return False
- l = lines[0]
- # |l| will be like 'Logged in as <user>@google.com.' for googler using
- # reclient.
- return l.startswith("Logged in as ") and l.endswith("@google.com.")
- def ParseGNArgs(gn_args):
- """Parse gn_args as json and return config dictionary."""
- configs = json.loads(gn_args)
- build_configs = {}
- for config in configs:
- key = config["name"]
- if key not in ALLOWLISTED_CONFIGS:
- continue
- if "current" in config:
- build_configs[key] = config["current"]["value"]
- else:
- build_configs[key] = config["default"]["value"]
- return build_configs
- def GetBuildTargetFromCommandLine(cmdline):
- """Get build targets from commandline."""
- # Skip argv0, argv1: ['/path/to/python3', '/path/to/depot_tools/ninja.py']
- idx = 2
- # Skipping all args that involve these flags, and taking all remaining args
- # as targets.
- onearg_flags = ("-C", "-d", "-f", "-j", "-k", "-l", "-p", "-t", "-w")
- zeroarg_flags = ("--version", "-n", "-v")
- targets = []
- while idx < len(cmdline):
- arg = cmdline[idx]
- if arg in onearg_flags:
- idx += 2
- continue
- if arg[:2] in onearg_flags or arg in zeroarg_flags:
- idx += 1
- continue
- # A target doesn't start with '-'.
- if arg.startswith("-"):
- idx += 1
- continue
- # Avoid uploading absolute paths accidentally. e.g. b/270907050
- if os.path.isabs(arg):
- idx += 1
- continue
- targets.append(arg)
- idx += 1
- return targets
- def GetJflag(cmdline):
- """Parse cmdline to get flag value for -j"""
- for i in range(len(cmdline)):
- if (cmdline[i] == "-j" and i + 1 < len(cmdline)
- and cmdline[i + 1].isdigit()):
- return int(cmdline[i + 1])
- if cmdline[i].startswith("-j") and cmdline[i][len("-j"):].isdigit():
- return int(cmdline[i][len("-j"):])
- def GetMetadata(cmdline, ninjalog):
- """Get metadata for uploaded ninjalog.
- Returned metadata has schema defined in
- https://cs.chromium.org?q="type+Metadata+struct+%7B"+file:%5Einfra/go/src/infra/appengine/chromium_build_stats/ninjalog/
- TODO(tikuta): Collect GOMA_* env var.
- """
- build_dir = os.path.dirname(ninjalog)
- build_configs = {}
- try:
- args = ["gn", "args", build_dir, "--list", "--short", "--json"]
- if sys.platform == "win32":
- # gn in PATH is bat file in windows environment (except cygwin).
- args = ["cmd", "/c"] + args
- gn_args = subprocess.check_output(args)
- build_configs = ParseGNArgs(gn_args)
- except subprocess.CalledProcessError as e:
- logging.error("Failed to call gn %s", e)
- build_configs = {}
- # Stringify config.
- for k in build_configs:
- build_configs[k] = str(build_configs[k])
- metadata = {
- "platform": platform.system(),
- "cpu_core": multiprocessing.cpu_count(),
- "build_configs": build_configs,
- "targets": GetBuildTargetFromCommandLine(cmdline),
- }
- jflag = GetJflag(cmdline)
- if jflag is not None:
- metadata["jobs"] = jflag
- return metadata
- def GetNinjalog(cmdline):
- """GetNinjalog returns the path to ninjalog from cmdline."""
- # ninjalog is in current working directory by default.
- ninjalog_dir = "."
- i = 0
- while i < len(cmdline):
- cmd = cmdline[i]
- i += 1
- if cmd == "-C" and i < len(cmdline):
- ninjalog_dir = cmdline[i]
- i += 1
- continue
- if cmd.startswith("-C") and len(cmd) > len("-C"):
- ninjalog_dir = cmd[len("-C"):]
- return os.path.join(ninjalog_dir, ".ninja_log")
- def main():
- parser = argparse.ArgumentParser()
- parser.add_argument(
- "--server",
- default="chromium-build-stats.appspot.com",
- help="server to upload ninjalog file.",
- )
- parser.add_argument("--ninjalog", help="ninjalog file to upload.")
- parser.add_argument("--verbose",
- action="store_true",
- help="Enable verbose logging.")
- parser.add_argument(
- "--cmdline",
- required=True,
- nargs=argparse.REMAINDER,
- help="command line args passed to ninja.",
- )
- args = parser.parse_args()
- if args.verbose:
- logging.basicConfig(level=logging.INFO)
- else:
- # Disable logging.
- logging.disable(logging.CRITICAL)
- if not IsGoogler():
- return 0
- ninjalog = args.ninjalog or GetNinjalog(args.cmdline)
- if not os.path.isfile(ninjalog):
- logging.warning("ninjalog is not found in %s", ninjalog)
- return 1
- # We assume that each ninja invocation interval takes at least 2 seconds.
- # This is not to have duplicate entry in server when current build is no-op.
- if os.stat(ninjalog).st_mtime < time.time() - 2:
- logging.info("ninjalog is not updated recently %s", ninjalog)
- return 0
- output = io.BytesIO()
- with open(ninjalog) as f:
- with gzip.GzipFile(fileobj=output, mode="wb") as g:
- g.write(f.read().encode())
- g.write(b"# end of ninja log\n")
- metadata = GetMetadata(args.cmdline, ninjalog)
- logging.info("send metadata: %s", json.dumps(metadata))
- g.write(json.dumps(metadata).encode())
- resp = urllib.request.urlopen(
- urllib.request.Request(
- "https://" + args.server + "/upload_ninja_log/",
- data=output.getvalue(),
- headers={"Content-Encoding": "gzip"},
- ))
- if resp.status != 200:
- logging.warning("unexpected status code for response: %s", resp.status)
- return 1
- logging.info("response header: %s", resp.headers)
- logging.info("response content: %s", resp.read())
- return 0
- if __name__ == "__main__":
- sys.exit(main())
|