| # Copyright 2020 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| from __future__ import print_function |
| |
| import ast |
| import argparse |
| import importlib |
| import json |
| import logging |
| import os |
| import pprint |
| import sys |
| import unittest |
| import csv |
| |
| from collections import namedtuple |
| |
| from core.tbmv3 import trace_processor |
| from cli_tools.tbmv3 import trace_downloader |
| from tracing.metrics import metric_runner |
| from tracing.value import histogram_set |
| |
| |
| SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) |
| SIMPLE_CONFIG_PATH = os.path.join(SCRIPT_DIR, 'validators', |
| 'simple_configs.pyl') |
| |
| SimpleConfig = namedtuple('SimpleConfig', ['name', 'config']) |
| |
| |
| def SetUpLogging(level): |
| logger = logging.getLogger() |
| logger.setLevel(level) |
| formatter = logging.Formatter( |
| '(%(levelname)s) %(asctime)s [%(module)s] %(message)s') |
| |
| handler = logging.StreamHandler() |
| handler.setFormatter(formatter) |
| logger.addHandler(handler) |
| |
| |
| def PrintNoLn(msg): |
| """Print |msg| without adding new line.""" |
| sys.stdout.write(msg) |
| sys.stdout.flush() |
| |
| |
| def CursorErase(length): |
| """Erase |length| chars starting from cursor.""" |
| for _ in range(length): |
| sys.stdout.write('\b') |
| # Add 80 spaces, because \b only moves back the cursor. |
| for _ in range(80): |
| sys.stdout.write(' ') |
| for _ in range(80): |
| sys.stdout.write('\b') |
| sys.stdout.flush() |
| |
| |
| def ParseArgs(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument('validator', |
| type=str, |
| default=None, |
| help=('Name of the validtor from tools/perf/' |
| 'cli_tools/tbmv3/validators/, or alternatively ' |
| 'a simple config defined in cli_tools/tbmv3/' |
| 'validators/simple_configs.pyl')) |
| parser.add_argument('--tracelist-csv', |
| type=str, |
| required=False, |
| default=None, |
| help=('Path to a csv file containing links to HTML ' |
| 'traces in CloudStorage in chrome-telemetry-output ' |
| 'bucket. Go to go/get-tbm-traces and follow ' |
| 'instructions there to generate the CSV.')) |
| parser.add_argument('--proto-trace', |
| type=str, |
| required=False, |
| help='Path to proto trace') |
| parser.add_argument('--json-trace', |
| type=str, |
| required=False, |
| help='Path to json/html trace') |
| parser.add_argument('--traces-dir', |
| type=str, |
| required=False, |
| default=trace_downloader.DEFAULT_TRACE_DIR, |
| help='Directory to store all intermediate files') |
| parser.add_argument('--trace-processor-path', |
| type=str, |
| required=False, |
| default=None, |
| help=('Path to trace_processor shell. ' |
| 'Default: Binary downloaded from cloud storage.')) |
| parser.add_argument('--force-recompute-tbmv2', |
| action='store_true', |
| help=('Recompute TBMv2 Metrics. Otherwise it will use ' |
| 'a cached result when available.')) |
| parser.add_argument('-v', '--verbose', action='store_true') |
| args = parser.parse_args() |
| return args |
| |
| |
| class ValidatorContext(object): |
| def __init__(self, args): |
| with open(SIMPLE_CONFIG_PATH) as f: |
| simple_configs = ast.literal_eval(f.read()) |
| validator_name = args.validator |
| if validator_name in simple_configs: |
| self.validator = importlib.import_module('cli_tools.tbmv3.validators.' |
| 'simple_validator') |
| self.simple_config = SimpleConfig(validator_name, |
| simple_configs[validator_name]) |
| else: |
| self.validator = importlib.import_module('cli_tools.tbmv3.validators.' + |
| args.validator) |
| self.simple_config = None |
| |
| self.trace_processor_path = args.trace_processor_path |
| if self.trace_processor_path and not os.path.exists( |
| self.trace_processor_path): |
| raise Exception("Trace processor does not exist at %s" % |
| args.trace_processor_path) |
| |
| self.traces_dir = args.traces_dir |
| self.force_recompute_tbmv2 = args.force_recompute_tbmv2 |
| |
| |
| class TraceInfo(object): |
| def __init__(self, json_trace, proto_trace): |
| self.json_trace = json_trace |
| self.proto_trace = proto_trace |
| # If present, holds additional info about trace like bot, cloud url etc. |
| self.trace_metadata = None |
| |
| def __repr__(self): |
| output = { |
| 'json_trace_path': self.json_trace, |
| 'proto_trace_path': self.proto_trace, |
| } |
| if self.trace_metadata: |
| output.update(self.trace_metadata) |
| return pprint.pformat(output) |
| |
| |
| def CreateTraceInfoFromCsvRow(row, traces_dir): |
| message = 'Fetching traces...' |
| PrintNoLn(message) |
| html_trace_url = row['Trace Link'] |
| html_trace = trace_downloader.DownloadHtmlTrace(html_trace_url, |
| download_dir=traces_dir) |
| proto_trace = trace_downloader.DownloadProtoTrace(html_trace_url, |
| download_dir=traces_dir) |
| |
| trace_info = TraceInfo(html_trace, proto_trace) |
| trace_info.trace_metadata = { |
| 'Bot': row['Bot'], |
| 'Benchmark': row['Benchmark'], |
| 'Cloud Trace URL': html_trace_url |
| } |
| CursorErase(len(message)) |
| return trace_info |
| |
| |
| def CreateTraceInfoFromArgs(args): |
| json_trace = os.path.expanduser(args.json_trace) |
| if json_trace is None: |
| raise Exception('You must supply a --json_trace if you do not use ' |
| '--tracelist-csv.') |
| if not os.path.exists(json_trace): |
| raise Exception('Json trace %s does not exist' % json_trace) |
| |
| proto_trace = os.path.expanduser(args.proto_trace) |
| if proto_trace is None: |
| raise Exception('You must supply a --proto_trace if you do not use ' |
| '--tracelist-csv.') |
| if not os.path.exists(proto_trace): |
| raise Exception('Proto trace %s does not exist' % proto_trace) |
| |
| return TraceInfo(json_trace, proto_trace) |
| |
| |
| def GetV2CachedResultPath(tbmv2_metric, json_trace): |
| dirname = os.path.dirname(json_trace) |
| basename = os.path.basename(json_trace) + '.' + tbmv2_metric + '.json' |
| return os.path.join(dirname, basename) |
| |
| |
| def RunTBMv2Metric(tbmv2_metric, json_trace, force_recompute=False): |
| message = 'Running TBMv2 Metric...' |
| PrintNoLn(message) |
| hset = histogram_set.HistogramSet() |
| |
| cached_results = GetV2CachedResultPath(tbmv2_metric, json_trace) |
| |
| if not force_recompute and os.path.exists(cached_results): |
| with open(cached_results) as f: |
| hset.ImportDicts(json.load(f)) |
| CursorErase(len(message)) |
| return hset |
| |
| metrics = [tbmv2_metric] |
| TEN_MINUTES = 60 * 10 |
| trace_abspath = os.path.abspath(json_trace) |
| mre_result = metric_runner.RunMetricOnSingleTrace(trace_abspath, |
| metrics, |
| timeout=TEN_MINUTES) |
| histograms = mre_result.pairs.get('histograms') |
| if mre_result.failures: |
| raise Exception("Error computing TBMv2 metric for %s" % json_trace) |
| if 'histograms' not in mre_result.pairs: |
| raise Exception("Metric %s is empty for trace %s" % |
| (tbmv2_metric, json_trace)) |
| histograms = mre_result.pairs['histograms'] |
| hset.ImportDicts(histograms) |
| with open(cached_results, 'w') as f: |
| json.dump(histograms, f) |
| |
| CursorErase(len(message)) |
| return hset |
| |
| |
| def RunTBMv3Metric(tp_path, tbmv3_metric, proto_trace): |
| message = 'Running TBMv3 Metric...' |
| PrintNoLn(message) |
| histograms = trace_processor.RunMetric(tp_path, |
| proto_trace, |
| tbmv3_metric, |
| retain_all_samples=True) |
| CursorErase(len(message)) |
| return histograms |
| |
| |
| def ValidateSingleTrace(ctx, trace_info): |
| class ValidatorTestCase(unittest.TestCase): |
| def setUp(self): |
| self.trace_info = trace_info |
| if ctx.simple_config: |
| self.simple_config = ctx.simple_config |
| |
| def RunTBMv2(self, metric): |
| return RunTBMv2Metric(metric, |
| trace_info.json_trace, |
| force_recompute=ctx.force_recompute_tbmv2) |
| |
| def RunTBMv3(self, metric): |
| return RunTBMv3Metric(ctx.trace_processor_path, metric, |
| trace_info.proto_trace) |
| |
| def runTest(self): |
| ctx.validator.CompareHistograms(self) |
| |
| result = unittest.TestResult() |
| validator_tc = ValidatorTestCase() |
| validator_tc.run(result) |
| return result |
| |
| |
| def ValidateAllCsvTraces(ctx, tracelist_csv, results): |
| with open(os.path.expanduser(tracelist_csv)) as f: |
| rows = list(csv.DictReader(f)) |
| |
| for (i, row) in enumerate(rows, start=1): |
| PrintNoLn('Validating trace %d of %d: ' % (i, len(rows))) |
| trace_info = CreateTraceInfoFromCsvRow(row, ctx.traces_dir) |
| result = ValidateSingleTrace(ctx, trace_info) |
| results.append(result) |
| if result.wasSuccessful(): |
| print('Success!') |
| else: |
| print('Failed.') |
| PrintErrorsOrFailures(result) |
| |
| |
| def PrintSingleFailure(error_or_failure): |
| trace_info = error_or_failure[0].trace_info |
| error_msg = error_or_failure[1] |
| print('-------------------------------------------') |
| print('Validator failure for the following trace:') |
| print(trace_info) |
| print('Error: ') |
| print(error_msg) |
| print('-------------------------------------------') |
| |
| |
| def PrintErrorsOrFailures(result): |
| if result.wasSuccessful(): |
| return |
| for error in result.errors: |
| PrintSingleFailure(error) |
| for failure in result.failures: |
| PrintSingleFailure(failure) |
| |
| |
| def CountFailures(results): |
| count = 0 |
| for result in results: |
| if not result.wasSuccessful(): |
| count += 1 |
| return count |
| |
| |
| def Main(): |
| args = ParseArgs() |
| loglevel = logging.DEBUG if args.verbose else logging.WARNING |
| SetUpLogging(level=loglevel) |
| |
| ctx = ValidatorContext(args) |
| |
| results = [] |
| try: |
| if args.tracelist_csv: |
| ValidateAllCsvTraces(ctx, args.tracelist_csv, results) |
| elif args.json_trace and args.proto_trace: |
| trace_info = CreateTraceInfoFromArgs(args) |
| result = ValidateSingleTrace(ctx, trace_info) |
| PrintErrorsOrFailures(result) |
| results.append(result) |
| else: |
| sys.stderr.write( |
| 'You must supply either --tracelist_csv to validate ' |
| 'traces, or both --proto-trace and --json-trace to validate a ' |
| 'single trace.') |
| sys.exit(1) |
| except KeyboardInterrupt: |
| print('\n') |
| failures = CountFailures(results) |
| successes = len(results) - failures |
| print('%d failed, %d succeeded' % (failures, successes)) |
| # Without this various child processes often hangs the terminal. |
| os._exit(1) # pylint: disable=protected-access |
| |
| failures = CountFailures(results) |
| if failures == 0: |
| print('All validations succeeded!') |
| else: |
| print('%d out of %d validations did not pass. See above for details.' % |
| (failures, len(results))) |