| #!/usr/bin/env python |
| # Copyright (C) 2018 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import argparse |
| import difflib |
| import glob |
| import importlib |
| import json |
| import os |
| import subprocess |
| import sys |
| import tempfile |
| |
| from itertools import chain |
| from google.protobuf import descriptor, descriptor_pb2, message_factory |
| from google.protobuf import reflection, text_format |
| |
| ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| |
| |
| class PerfResult(object): |
| |
| def __init__(self, trace_name, query_or_metric, ingest_time, real_time): |
| self.trace_name = trace_name |
| self.query_or_metric = query_or_metric |
| self.ingest_time = ingest_time |
| self.real_time = real_time |
| |
| |
| def create_message_factory(descriptor_file_path, proto_type): |
| with open(descriptor_file_path, 'rb') as descriptor_file: |
| descriptor_content = descriptor_file.read() |
| |
| file_desc_set_pb2 = descriptor_pb2.FileDescriptorSet() |
| file_desc_set_pb2.MergeFromString(descriptor_content) |
| |
| desc_by_path = {} |
| for f_desc_pb2 in file_desc_set_pb2.file: |
| f_desc_pb2_encode = f_desc_pb2.SerializeToString() |
| f_desc = descriptor.FileDescriptor( |
| name=f_desc_pb2.name, |
| package=f_desc_pb2.package, |
| serialized_pb=f_desc_pb2_encode) |
| |
| for desc in f_desc.message_types_by_name.values(): |
| desc_by_path[desc.full_name] = desc |
| |
| return message_factory.MessageFactory().GetPrototype(desc_by_path[proto_type]) |
| |
| |
| def create_trace_message_factory(trace_descriptor_path): |
| return create_message_factory(trace_descriptor_path, 'perfetto.protos.Trace') |
| |
| |
| def create_metrics_message_factory(metrics_descriptor_path): |
| return create_message_factory(metrics_descriptor_path, |
| 'perfetto.protos.TraceMetrics') |
| |
| |
| def serialize_text_proto_to_file(proto_descriptor_path, text_proto_path, |
| output_file): |
| trace_message_factory = create_trace_message_factory(proto_descriptor_path) |
| proto = trace_message_factory() |
| with open(text_proto_path, 'r') as text_proto_file: |
| text_format.Merge(text_proto_file.read(), proto) |
| output_file.write(proto.SerializeToString()) |
| output_file.flush() |
| |
| |
| def write_diff(expected, actual): |
| expected_lines = expected.splitlines(True) |
| actual_lines = actual.splitlines(True) |
| diff = difflib.unified_diff( |
| expected_lines, actual_lines, fromfile='expected', tofile='actual') |
| for line in diff: |
| sys.stderr.write(line) |
| |
| |
| class TestResult(object): |
| |
| def __init__(self, test_type, input_name, cmd, expected, actual): |
| self.test_type = test_type |
| self.input_name = input_name |
| self.cmd = cmd |
| self.expected = expected |
| self.actual = actual |
| |
| |
| def run_metrics_test(trace_processor_path, gen_trace_path, metric, |
| expected_path, perf_path, metrics_message_factory): |
| with open(expected_path, 'r') as expected_file: |
| expected = expected_file.read() |
| |
| json_output = os.path.basename(expected_path).endswith('.json.out') |
| cmd = [ |
| trace_processor_path, |
| '--run-metrics', |
| metric, |
| '--metrics-output=%s' % ('json' if json_output else 'binary'), |
| gen_trace_path, |
| '--perf-file', |
| perf_path, |
| ] |
| actual = subprocess.check_output(cmd) |
| |
| if json_output: |
| expected_text = expected |
| actual_text = actual |
| else: |
| # Expected will be in text proto format and we'll need to parse it to a real |
| # proto. |
| expected_message = metrics_message_factory() |
| text_format.Merge(expected, expected_message) |
| |
| # Actual will be the raw bytes of the proto and we'll need to parse it into |
| # a message. |
| actual_message = metrics_message_factory() |
| actual_message.ParseFromString(actual) |
| |
| # Convert both back to text format. |
| expected_text = text_format.MessageToString(expected_message) |
| actual_text = text_format.MessageToString(actual_message) |
| |
| return TestResult('metric', metric, cmd, expected_text, actual_text) |
| |
| |
| def run_query_test(trace_processor_path, gen_trace_path, query_path, |
| expected_path, perf_path): |
| with open(expected_path, 'r') as expected_file: |
| expected = expected_file.read() |
| |
| cmd = [ |
| trace_processor_path, |
| '-q', |
| query_path, |
| gen_trace_path, |
| '--perf-file', |
| perf_path, |
| ] |
| actual = subprocess.check_output(cmd).decode('utf-8') |
| |
| return TestResult('query', query_path, cmd, expected, actual) |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser() |
| parser.add_argument('--test-type', type=str, default='queries') |
| parser.add_argument('--trace-descriptor', type=str) |
| parser.add_argument('--metrics-descriptor', type=str) |
| parser.add_argument('--perf-file', type=str) |
| parser.add_argument( |
| 'trace_processor', type=str, help='location of trace processor binary') |
| args = parser.parse_args() |
| |
| test_dir = os.path.join(ROOT_DIR, 'test') |
| if args.test_type == 'queries': |
| index = os.path.join(test_dir, 'trace_processor', 'index') |
| elif args.test_type == 'metrics': |
| index = os.path.join(test_dir, 'metrics', 'index') |
| else: |
| print('Unknown test type {}. Supported: queries, metircs'.format( |
| args.test_type)) |
| return 1 |
| |
| with open(index, 'r') as file: |
| index_lines = file.readlines() |
| |
| if args.trace_descriptor: |
| trace_descriptor_path = args.trace_descriptor |
| else: |
| out_path = os.path.dirname(args.trace_processor) |
| trace_protos_path = os.path.join(out_path, 'gen', 'protos', 'perfetto', |
| 'trace') |
| trace_descriptor_path = os.path.join(trace_protos_path, 'trace.descriptor') |
| |
| if args.metrics_descriptor: |
| metrics_descriptor_path = args.metrics_descriptor |
| else: |
| out_path = os.path.dirname(args.trace_processor) |
| metrics_protos_path = os.path.join(out_path, 'gen', 'protos', 'perfetto', |
| 'metrics') |
| metrics_descriptor_path = os.path.join(metrics_protos_path, |
| 'metrics.descriptor') |
| |
| metrics_message_factory = create_metrics_message_factory( |
| metrics_descriptor_path) |
| |
| perf_data = [] |
| test_failure = 0 |
| index_dir = os.path.dirname(index) |
| for line in index_lines: |
| stripped = line.strip() |
| if stripped.startswith('#'): |
| continue |
| elif not stripped: |
| continue |
| |
| [trace_fname, query_fname_or_metric, expected_fname] = stripped.split(' ') |
| |
| trace_path = os.path.abspath(os.path.join(index_dir, trace_fname)) |
| expected_path = os.path.abspath(os.path.join(index_dir, expected_fname)) |
| if not os.path.exists(trace_path): |
| print('Trace file not found {}'.format(trace_path)) |
| return 1 |
| elif not os.path.exists(expected_path): |
| print('Expected file not found {}'.format(expected_path)) |
| return 1 |
| |
| if trace_path.endswith('.py'): |
| gen_trace_file = tempfile.NamedTemporaryFile() |
| python_cmd = ['python', trace_path, trace_descriptor_path] |
| subprocess.check_call(python_cmd, stdout=gen_trace_file) |
| gen_trace_path = os.path.realpath(gen_trace_file.name) |
| elif trace_path.endswith('.textproto'): |
| gen_trace_file = tempfile.NamedTemporaryFile() |
| serialize_text_proto_to_file(trace_descriptor_path, trace_path, |
| gen_trace_file) |
| gen_trace_path = os.path.realpath(gen_trace_file.name) |
| else: |
| gen_trace_file = None |
| gen_trace_path = trace_path |
| |
| with tempfile.NamedTemporaryFile() as tmp_perf_file: |
| tmp_perf_path = tmp_perf_file.name |
| if args.test_type == 'queries': |
| query_path = os.path.abspath( |
| os.path.join(index_dir, query_fname_or_metric)) |
| if not os.path.exists(query_path): |
| print('Query file not found {}'.format(query_path)) |
| return 1 |
| |
| result = run_query_test(args.trace_processor, gen_trace_path, |
| query_path, expected_path, tmp_perf_path) |
| elif args.test_type == 'metrics': |
| result = run_metrics_test(args.trace_processor, gen_trace_path, |
| query_fname_or_metric, expected_path, |
| tmp_perf_path, metrics_message_factory) |
| else: |
| assert False |
| |
| perf_lines = tmp_perf_file.readlines() |
| |
| if gen_trace_file: |
| gen_trace_file.close() |
| |
| if result.expected == result.actual: |
| assert len(perf_lines) == 1 |
| perf_numbers = perf_lines[0].split(',') |
| |
| trace_shortpath = os.path.relpath(trace_path, test_dir) |
| |
| assert len(perf_numbers) == 2 |
| perf_result = PerfResult(trace_shortpath, query_fname_or_metric, |
| perf_numbers[0], perf_numbers[1]) |
| perf_data.append(perf_result) |
| else: |
| sys.stderr.write( |
| 'Expected did not match actual for trace {} and {} {}\n'.format( |
| trace_path, result.test_type, result.input_name)) |
| sys.stderr.write('Expected file: {}\n'.format(expected_path)) |
| sys.stderr.write('Command line: {}\n'.format(' '.join(result.cmd))) |
| |
| write_diff(result.expected, result.actual) |
| |
| test_failure += 1 |
| |
| if test_failure == 0: |
| print('All tests passed successfully') |
| |
| if args.perf_file: |
| metrics = [[{ |
| 'metric': 'tp_perf_test_ingest_time', |
| 'value': float(perf_args.ingest_time) / 1.0e9, |
| 'unit': 's', |
| 'tags': { |
| 'test_name': |
| '{}-{}'.format(perf_args.trace_name, |
| perf_args.query_or_metric), |
| 'test_type': |
| args.test_type, |
| }, |
| 'labels': {}, |
| }, |
| { |
| 'metric': 'perf_test_real_time', |
| 'value': float(perf_args.real_time) / 1.0e9, |
| 'unit': 's', |
| 'tags': { |
| 'test_name': |
| '{}-{}'.format(perf_args.trace_name, |
| perf_args.query_or_metric), |
| 'test_type': |
| args.test_type, |
| }, |
| 'labels': {}, |
| }] for perf_args in sorted(perf_data)] |
| output_data = {'metrics': list(chain.from_iterable(metrics))} |
| with open(args.perf_file, 'w+') as perf_file: |
| perf_file.write(json.dumps(output_data, indent=2)) |
| return 0 |
| else: |
| print('Total failures: {}'.format(test_failure)) |
| return 1 |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main()) |