| # -*- coding: utf-8 -*- |
| # Copyright (c) 2013 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """A module to handle the report format.""" |
| from __future__ import print_function |
| |
| import datetime |
| import functools |
| import itertools |
| import json |
| import os |
| import re |
| import time |
| |
| from cros_utils.tabulator import AmeanResult |
| from cros_utils.tabulator import Cell |
| from cros_utils.tabulator import CoeffVarFormat |
| from cros_utils.tabulator import CoeffVarResult |
| from cros_utils.tabulator import Column |
| from cros_utils.tabulator import SamplesTableGenerator |
| from cros_utils.tabulator import Format |
| from cros_utils.tabulator import IterationResult |
| from cros_utils.tabulator import GmeanRatioResult |
| from cros_utils.tabulator import LiteralResult |
| from cros_utils.tabulator import MaxResult |
| from cros_utils.tabulator import MinResult |
| from cros_utils.tabulator import PValueFormat |
| from cros_utils.tabulator import PValueResult |
| from cros_utils.tabulator import RatioFormat |
| from cros_utils.tabulator import RawResult |
| from cros_utils.tabulator import StdResult |
| from cros_utils.tabulator import TableFormatter |
| from cros_utils.tabulator import TableGenerator |
| from cros_utils.tabulator import TablePrinter |
| from update_telemetry_defaults import TelemetryDefaults |
| |
| from column_chart import ColumnChart |
| from results_organizer import OrganizeResults |
| |
| import results_report_templates as templates |
| |
| |
| def ParseChromeosImage(chromeos_image): |
| """Parse the chromeos_image string for the image and version. |
| |
| The chromeos_image string will probably be in one of two formats: |
| 1: <path-to-chroot>/src/build/images/<board>/<ChromeOS-version>.<datetime>/ \ |
| chromiumos_test_image.bin |
| 2: <path-to-chroot>/chroot/tmp/<buildbot-build>/<ChromeOS-version>/ \ |
| chromiumos_test_image.bin |
| |
| We parse these strings to find the 'chromeos_version' to store in the |
| json archive (without the .datatime bit in the first case); and also |
| the 'chromeos_image', which would be all of the first case, but only the |
| part after '/chroot/tmp' in the second case. |
| |
| Args: |
| chromeos_image: string containing the path to the chromeos_image that |
| crosperf used for the test. |
| |
| Returns: |
| version, image: The results of parsing the input string, as explained |
| above. |
| """ |
| # Find the Chromeos Version, e.g. R45-2345.0.0..... |
| # chromeos_image should have been something like: |
| # <path>/<board-trybot-release>/<chromeos-version>/chromiumos_test_image.bin" |
| if chromeos_image.endswith('/chromiumos_test_image.bin'): |
| full_version = chromeos_image.split('/')[-2] |
| # Strip the date and time off of local builds (which have the format |
| # "R43-2345.0.0.date-and-time"). |
| version, _ = os.path.splitext(full_version) |
| else: |
| version = '' |
| |
| # Find the chromeos image. If it's somewhere in .../chroot/tmp/..., then |
| # it's an official image that got downloaded, so chop off the download path |
| # to make the official image name more clear. |
| official_image_path = '/chroot/tmp' |
| if official_image_path in chromeos_image: |
| image = chromeos_image.split(official_image_path, 1)[1] |
| else: |
| image = chromeos_image |
| return version, image |
| |
| |
| def _AppendUntilLengthIs(gen, the_list, target_len): |
| """Appends to `list` until `list` is `target_len` elements long. |
| |
| Uses `gen` to generate elements. |
| """ |
| the_list.extend(gen() for _ in range(target_len - len(the_list))) |
| return the_list |
| |
| |
| def _FilterPerfReport(event_threshold, report): |
| """Filters out entries with `< event_threshold` percent in a perf report.""" |
| |
| def filter_dict(m): |
| return { |
| fn_name: pct for fn_name, pct in m.items() if pct >= event_threshold |
| } |
| |
| return {event: filter_dict(m) for event, m in report.items()} |
| |
| |
| class _PerfTable(object): |
| """Generates dicts from a perf table. |
| |
| Dicts look like: |
| {'benchmark_name': {'perf_event_name': [LabelData]}} |
| where LabelData is a list of perf dicts, each perf dict coming from the same |
| label. |
| Each perf dict looks like {'function_name': 0.10, ...} (where 0.10 is the |
| percentage of time spent in function_name). |
| """ |
| |
| def __init__(self, |
| benchmark_names_and_iterations, |
| label_names, |
| read_perf_report, |
| event_threshold=None): |
| """Constructor. |
| |
| read_perf_report is a function that takes a label name, benchmark name, and |
| benchmark iteration, and returns a dictionary describing the perf output for |
| that given run. |
| """ |
| self.event_threshold = event_threshold |
| self._label_indices = {name: i for i, name in enumerate(label_names)} |
| self.perf_data = {} |
| for label in label_names: |
| for bench_name, bench_iterations in benchmark_names_and_iterations: |
| for i in range(bench_iterations): |
| report = read_perf_report(label, bench_name, i) |
| self._ProcessPerfReport(report, label, bench_name, i) |
| |
| def _ProcessPerfReport(self, perf_report, label, benchmark_name, iteration): |
| """Add the data from one run to the dict.""" |
| perf_of_run = perf_report |
| if self.event_threshold is not None: |
| perf_of_run = _FilterPerfReport(self.event_threshold, perf_report) |
| if benchmark_name not in self.perf_data: |
| self.perf_data[benchmark_name] = {event: [] for event in perf_of_run} |
| ben_data = self.perf_data[benchmark_name] |
| label_index = self._label_indices[label] |
| for event in ben_data: |
| _AppendUntilLengthIs(list, ben_data[event], label_index + 1) |
| data_for_label = ben_data[event][label_index] |
| _AppendUntilLengthIs(dict, data_for_label, iteration + 1) |
| data_for_label[iteration] = perf_of_run[event] if perf_of_run else {} |
| |
| |
| def _GetResultsTableHeader(ben_name, iterations): |
| benchmark_info = ('Benchmark: {0}; Iterations: {1}'.format( |
| ben_name, iterations)) |
| cell = Cell() |
| cell.string_value = benchmark_info |
| cell.header = True |
| return [[cell]] |
| |
| |
| def _GetDSOHeader(cwp_dso): |
| info = 'CWP_DSO: %s' % cwp_dso |
| cell = Cell() |
| cell.string_value = info |
| cell.header = False |
| return [[cell]] |
| |
| |
| def _ParseColumn(columns, iteration): |
| new_column = [] |
| for column in columns: |
| if column.result.__class__.__name__ != 'RawResult': |
| new_column.append(column) |
| else: |
| new_column.extend( |
| Column(LiteralResult(i), Format(), str(i + 1)) |
| for i in range(iteration)) |
| return new_column |
| |
| |
| def _GetTables(benchmark_results, columns, table_type): |
| iter_counts = benchmark_results.iter_counts |
| result = benchmark_results.run_keyvals |
| tables = [] |
| for bench_name, runs in result.items(): |
| iterations = iter_counts[bench_name] |
| ben_table = _GetResultsTableHeader(bench_name, iterations) |
| |
| all_runs_empty = all(not dict for label in runs for dict in label) |
| if all_runs_empty: |
| cell = Cell() |
| cell.string_value = ('This benchmark contains no result.' |
| ' Is the benchmark name valid?') |
| cell_table = [[cell]] |
| else: |
| table = TableGenerator(runs, benchmark_results.label_names).GetTable() |
| parsed_columns = _ParseColumn(columns, iterations) |
| tf = TableFormatter(table, parsed_columns) |
| cell_table = tf.GetCellTable(table_type) |
| tables.append(ben_table) |
| tables.append(cell_table) |
| return tables |
| |
| |
| def _GetPerfTables(benchmark_results, columns, table_type): |
| p_table = _PerfTable(benchmark_results.benchmark_names_and_iterations, |
| benchmark_results.label_names, |
| benchmark_results.read_perf_report) |
| |
| tables = [] |
| for benchmark in p_table.perf_data: |
| iterations = benchmark_results.iter_counts[benchmark] |
| ben_table = _GetResultsTableHeader(benchmark, iterations) |
| tables.append(ben_table) |
| benchmark_data = p_table.perf_data[benchmark] |
| table = [] |
| for event in benchmark_data: |
| tg = TableGenerator( |
| benchmark_data[event], |
| benchmark_results.label_names, |
| sort=TableGenerator.SORT_BY_VALUES_DESC) |
| table = tg.GetTable(ResultsReport.PERF_ROWS) |
| parsed_columns = _ParseColumn(columns, iterations) |
| tf = TableFormatter(table, parsed_columns) |
| tf.GenerateCellTable(table_type) |
| tf.AddColumnName() |
| tf.AddLabelName() |
| tf.AddHeader(str(event)) |
| table = tf.GetCellTable(table_type, headers=False) |
| tables.append(table) |
| return tables |
| |
| |
| def _GetSamplesTables(benchmark_results, columns, table_type): |
| tables = [] |
| dso_header_table = _GetDSOHeader(benchmark_results.cwp_dso) |
| tables.append(dso_header_table) |
| (table, new_keyvals, iter_counts) = SamplesTableGenerator( |
| benchmark_results.run_keyvals, benchmark_results.label_names, |
| benchmark_results.iter_counts, benchmark_results.weights).GetTable() |
| parsed_columns = _ParseColumn(columns, 1) |
| tf = TableFormatter(table, parsed_columns, samples_table=True) |
| cell_table = tf.GetCellTable(table_type) |
| tables.append(cell_table) |
| return (tables, new_keyvals, iter_counts) |
| |
| |
| class ResultsReport(object): |
| """Class to handle the report format.""" |
| MAX_COLOR_CODE = 255 |
| PERF_ROWS = 5 |
| |
| def __init__(self, results): |
| self.benchmark_results = results |
| |
| def _GetTablesWithColumns(self, columns, table_type, summary_type): |
| if summary_type == 'perf': |
| get_tables = _GetPerfTables |
| elif summary_type == 'samples': |
| get_tables = _GetSamplesTables |
| else: |
| get_tables = _GetTables |
| ret = get_tables(self.benchmark_results, columns, table_type) |
| # If we are generating a samples summary table, the return value of |
| # get_tables will be a tuple, and we will update the benchmark_results for |
| # composite benchmark so that full table can use it. |
| if isinstance(ret, tuple): |
| self.benchmark_results.run_keyvals = ret[1] |
| self.benchmark_results.iter_counts = ret[2] |
| ret = ret[0] |
| return ret |
| |
| def GetFullTables(self, perf=False): |
| ignore_min_max = self.benchmark_results.ignore_min_max |
| columns = [ |
| Column(RawResult(), Format()), |
| Column(MinResult(), Format()), |
| Column(MaxResult(), Format()), |
| Column(AmeanResult(ignore_min_max), Format()), |
| Column(StdResult(ignore_min_max), Format(), 'StdDev'), |
| Column(CoeffVarResult(ignore_min_max), CoeffVarFormat(), 'StdDev/Mean'), |
| Column(GmeanRatioResult(ignore_min_max), RatioFormat(), 'GmeanSpeedup'), |
| Column(PValueResult(ignore_min_max), PValueFormat(), 'p-value') |
| ] |
| return self._GetTablesWithColumns(columns, 'full', perf) |
| |
| def GetSummaryTables(self, summary_type=''): |
| ignore_min_max = self.benchmark_results.ignore_min_max |
| columns = [] |
| if summary_type == 'samples': |
| columns += [Column(IterationResult(), Format(), 'Iterations [Pass:Fail]')] |
| columns += [ |
| Column( |
| AmeanResult(ignore_min_max), Format(), |
| 'Weighted Samples Amean' if summary_type == 'samples' else ''), |
| Column(StdResult(ignore_min_max), Format(), 'StdDev'), |
| Column(CoeffVarResult(ignore_min_max), CoeffVarFormat(), 'StdDev/Mean'), |
| Column(GmeanRatioResult(ignore_min_max), RatioFormat(), 'GmeanSpeedup'), |
| Column(PValueResult(ignore_min_max), PValueFormat(), 'p-value') |
| ] |
| return self._GetTablesWithColumns(columns, 'summary', summary_type) |
| |
| |
| def _PrintTable(tables, out_to): |
| # tables may be None. |
| if not tables: |
| return '' |
| |
| if out_to == 'HTML': |
| out_type = TablePrinter.HTML |
| elif out_to == 'PLAIN': |
| out_type = TablePrinter.PLAIN |
| elif out_to == 'CONSOLE': |
| out_type = TablePrinter.CONSOLE |
| elif out_to == 'TSV': |
| out_type = TablePrinter.TSV |
| elif out_to == 'EMAIL': |
| out_type = TablePrinter.EMAIL |
| else: |
| raise ValueError('Invalid out_to value: %s' % (out_to,)) |
| |
| printers = (TablePrinter(table, out_type) for table in tables) |
| return ''.join(printer.Print() for printer in printers) |
| |
| |
| class TextResultsReport(ResultsReport): |
| """Class to generate text result report.""" |
| |
| H1_STR = '===========================================' |
| H2_STR = '-------------------------------------------' |
| |
| def __init__(self, results, email=False, experiment=None): |
| super(TextResultsReport, self).__init__(results) |
| self.email = email |
| self.experiment = experiment |
| |
| @staticmethod |
| def _MakeTitle(title): |
| header_line = TextResultsReport.H1_STR |
| # '' at the end gives one newline. |
| return '\n'.join([header_line, title, header_line, '']) |
| |
| @staticmethod |
| def _MakeSection(title, body): |
| header_line = TextResultsReport.H2_STR |
| # '\n' at the end gives us two newlines. |
| return '\n'.join([header_line, title, header_line, body, '\n']) |
| |
| @staticmethod |
| def FromExperiment(experiment, email=False): |
| results = BenchmarkResults.FromExperiment(experiment) |
| return TextResultsReport(results, email, experiment) |
| |
| def GetStatusTable(self): |
| """Generate the status table by the tabulator.""" |
| table = [['', '']] |
| columns = [ |
| Column(LiteralResult(iteration=0), Format(), 'Status'), |
| Column(LiteralResult(iteration=1), Format(), 'Failing Reason') |
| ] |
| |
| for benchmark_run in self.experiment.benchmark_runs: |
| status = [ |
| benchmark_run.name, |
| [benchmark_run.timeline.GetLastEvent(), benchmark_run.failure_reason] |
| ] |
| table.append(status) |
| cell_table = TableFormatter(table, columns).GetCellTable('status') |
| return [cell_table] |
| |
| def GetTotalWaitCooldownTime(self): |
| """Get cooldown wait time in seconds from experiment benchmark runs. |
| |
| Returns: |
| Dictionary {'dut': int(wait_time_in_seconds)} |
| """ |
| waittime_dict = {} |
| for dut in self.experiment.machine_manager.GetMachines(): |
| waittime_dict[dut.name] = dut.GetCooldownWaitTime() |
| return waittime_dict |
| |
| def GetReport(self): |
| """Generate the report for email and console.""" |
| output_type = 'EMAIL' if self.email else 'CONSOLE' |
| experiment = self.experiment |
| |
| sections = [] |
| if experiment is not None: |
| title_contents = "Results report for '%s'" % (experiment.name,) |
| else: |
| title_contents = 'Results report' |
| sections.append(self._MakeTitle(title_contents)) |
| |
| if not self.benchmark_results.cwp_dso: |
| summary_table = _PrintTable(self.GetSummaryTables(), output_type) |
| else: |
| summary_table = _PrintTable( |
| self.GetSummaryTables(summary_type='samples'), output_type) |
| sections.append(self._MakeSection('Summary', summary_table)) |
| |
| if experiment is not None: |
| table = _PrintTable(self.GetStatusTable(), output_type) |
| sections.append(self._MakeSection('Benchmark Run Status', table)) |
| |
| if not self.benchmark_results.cwp_dso: |
| perf_table = _PrintTable( |
| self.GetSummaryTables(summary_type='perf'), output_type) |
| sections.append(self._MakeSection('Perf Data', perf_table)) |
| |
| if experiment is not None: |
| experiment_file = experiment.experiment_file |
| sections.append(self._MakeSection('Experiment File', experiment_file)) |
| |
| cpu_info = experiment.machine_manager.GetAllCPUInfo(experiment.labels) |
| sections.append(self._MakeSection('CPUInfo', cpu_info)) |
| |
| totaltime = (time.time() - |
| experiment.start_time) if experiment.start_time else 0 |
| totaltime_str = 'Total experiment time:\n%d min' % (totaltime // 60) |
| cooldown_waittime_list = ['Cooldown wait time:'] |
| # When running experiment on multiple DUTs cooldown wait time may vary |
| # on different devices. In addition its combined time may exceed total |
| # experiment time which will look weird but it is reasonable. |
| # For this matter print cooldown time per DUT. |
| for dut, waittime in sorted(self.GetTotalWaitCooldownTime().items()): |
| cooldown_waittime_list.append('DUT %s: %d min' % (dut, waittime // 60)) |
| cooldown_waittime_str = '\n'.join(cooldown_waittime_list) |
| sections.append( |
| self._MakeSection('Duration', |
| '\n\n'.join([totaltime_str, |
| cooldown_waittime_str]))) |
| |
| return '\n'.join(sections) |
| |
| |
| def _GetHTMLCharts(label_names, test_results): |
| charts = [] |
| for item, runs in test_results.items(): |
| # Fun fact: label_names is actually *entirely* useless as a param, since we |
| # never add headers. We still need to pass it anyway. |
| table = TableGenerator(runs, label_names).GetTable() |
| columns = [ |
| Column(AmeanResult(), Format()), |
| Column(MinResult(), Format()), |
| Column(MaxResult(), Format()) |
| ] |
| tf = TableFormatter(table, columns) |
| data_table = tf.GetCellTable('full', headers=False) |
| |
| for cur_row_data in data_table: |
| test_key = cur_row_data[0].string_value |
| title = '{0}: {1}'.format(item, test_key.replace('/', '')) |
| chart = ColumnChart(title, 300, 200) |
| chart.AddColumn('Label', 'string') |
| chart.AddColumn('Average', 'number') |
| chart.AddColumn('Min', 'number') |
| chart.AddColumn('Max', 'number') |
| chart.AddSeries('Min', 'line', 'black') |
| chart.AddSeries('Max', 'line', 'black') |
| cur_index = 1 |
| for label in label_names: |
| chart.AddRow([ |
| label, cur_row_data[cur_index].value, |
| cur_row_data[cur_index + 1].value, cur_row_data[cur_index + 2].value |
| ]) |
| if isinstance(cur_row_data[cur_index].value, str): |
| chart = None |
| break |
| cur_index += 3 |
| if chart: |
| charts.append(chart) |
| return charts |
| |
| |
| class HTMLResultsReport(ResultsReport): |
| """Class to generate html result report.""" |
| |
| def __init__(self, benchmark_results, experiment=None): |
| super(HTMLResultsReport, self).__init__(benchmark_results) |
| self.experiment = experiment |
| |
| @staticmethod |
| def FromExperiment(experiment): |
| return HTMLResultsReport( |
| BenchmarkResults.FromExperiment(experiment), experiment=experiment) |
| |
| def GetReport(self): |
| label_names = self.benchmark_results.label_names |
| test_results = self.benchmark_results.run_keyvals |
| charts = _GetHTMLCharts(label_names, test_results) |
| chart_javascript = ''.join(chart.GetJavascript() for chart in charts) |
| chart_divs = ''.join(chart.GetDiv() for chart in charts) |
| |
| if not self.benchmark_results.cwp_dso: |
| summary_table = self.GetSummaryTables() |
| perf_table = self.GetSummaryTables(summary_type='perf') |
| else: |
| summary_table = self.GetSummaryTables(summary_type='samples') |
| perf_table = None |
| full_table = self.GetFullTables() |
| |
| experiment_file = '' |
| if self.experiment is not None: |
| experiment_file = self.experiment.experiment_file |
| # Use kwargs for code readability, and so that testing is a bit easier. |
| return templates.GenerateHTMLPage( |
| perf_table=perf_table, |
| chart_js=chart_javascript, |
| summary_table=summary_table, |
| print_table=_PrintTable, |
| chart_divs=chart_divs, |
| full_table=full_table, |
| experiment_file=experiment_file) |
| |
| |
| def ParseStandardPerfReport(report_data): |
| """Parses the output of `perf report`. |
| |
| It'll parse the following: |
| {{garbage}} |
| # Samples: 1234M of event 'foo' |
| |
| 1.23% command shared_object location function::name |
| |
| 1.22% command shared_object location function2::name |
| |
| # Samples: 999K of event 'bar' |
| |
| 0.23% command shared_object location function3::name |
| {{etc.}} |
| |
| Into: |
| {'foo': {'function::name': 1.23, 'function2::name': 1.22}, |
| 'bar': {'function3::name': 0.23, etc.}} |
| """ |
| # This function fails silently on its if it's handed a string (as opposed to a |
| # list of lines). So, auto-split if we do happen to get a string. |
| if isinstance(report_data, str): |
| report_data = report_data.splitlines() |
| # When switching to python3 catch the case when bytes are passed. |
| elif isinstance(report_data, bytes): |
| raise TypeError() |
| |
| # Samples: N{K,M,G} of event 'event-name' |
| samples_regex = re.compile(r"#\s+Samples: \d+\S? of event '([^']+)'") |
| |
| # We expect lines like: |
| # N.NN% command samples shared_object [location] symbol |
| # |
| # Note that we're looking at stripped lines, so there is no space at the |
| # start. |
| perf_regex = re.compile(r'^(\d+(?:.\d*)?)%' # N.NN% |
| r'\s*\d+' # samples count (ignored) |
| r'\s*\S+' # command (ignored) |
| r'\s*\S+' # shared_object (ignored) |
| r'\s*\[.\]' # location (ignored) |
| r'\s*(\S.+)' # function |
| ) |
| |
| stripped_lines = (l.strip() for l in report_data) |
| nonempty_lines = (l for l in stripped_lines if l) |
| # Ignore all lines before we see samples_regex |
| interesting_lines = itertools.dropwhile(lambda x: not samples_regex.match(x), |
| nonempty_lines) |
| |
| first_sample_line = next(interesting_lines, None) |
| # Went through the entire file without finding a 'samples' header. Quit. |
| if first_sample_line is None: |
| return {} |
| |
| sample_name = samples_regex.match(first_sample_line).group(1) |
| current_result = {} |
| results = {sample_name: current_result} |
| for line in interesting_lines: |
| samples_match = samples_regex.match(line) |
| if samples_match: |
| sample_name = samples_match.group(1) |
| current_result = {} |
| results[sample_name] = current_result |
| continue |
| |
| match = perf_regex.match(line) |
| if not match: |
| continue |
| percentage_str, func_name = match.groups() |
| try: |
| percentage = float(percentage_str) |
| except ValueError: |
| # Couldn't parse it; try to be "resilient". |
| continue |
| current_result[func_name] = percentage |
| return results |
| |
| |
| def _ReadExperimentPerfReport(results_directory, label_name, benchmark_name, |
| benchmark_iteration): |
| """Reads a perf report for the given benchmark. Returns {} on failure. |
| |
| The result should be a map of maps; it should look like: |
| {perf_event_name: {function_name: pct_time_spent}}, e.g. |
| {'cpu_cycles': {'_malloc': 10.0, '_free': 0.3, ...}} |
| """ |
| raw_dir_name = label_name + benchmark_name + str(benchmark_iteration + 1) |
| dir_name = ''.join(c for c in raw_dir_name if c.isalnum()) |
| file_name = os.path.join(results_directory, dir_name, 'perf.data.report.0') |
| try: |
| with open(file_name) as in_file: |
| return ParseStandardPerfReport(in_file) |
| except IOError: |
| # Yes, we swallow any IO-related errors. |
| return {} |
| |
| |
| # Split out so that testing (specifically: mocking) is easier |
| def _ExperimentToKeyvals(experiment, for_json_report): |
| """Converts an experiment to keyvals.""" |
| return OrganizeResults( |
| experiment.benchmark_runs, experiment.labels, json_report=for_json_report) |
| |
| |
| class BenchmarkResults(object): |
| """The minimum set of fields that any ResultsReport will take.""" |
| |
| def __init__(self, |
| label_names, |
| benchmark_names_and_iterations, |
| run_keyvals, |
| ignore_min_max=False, |
| read_perf_report=None, |
| cwp_dso=None, |
| weights=None): |
| if read_perf_report is None: |
| |
| def _NoPerfReport(*_args, **_kwargs): |
| return {} |
| |
| read_perf_report = _NoPerfReport |
| |
| self.label_names = label_names |
| self.benchmark_names_and_iterations = benchmark_names_and_iterations |
| self.iter_counts = dict(benchmark_names_and_iterations) |
| self.run_keyvals = run_keyvals |
| self.ignore_min_max = ignore_min_max |
| self.read_perf_report = read_perf_report |
| self.cwp_dso = cwp_dso |
| self.weights = dict(weights) if weights else None |
| |
| @staticmethod |
| def FromExperiment(experiment, for_json_report=False): |
| label_names = [label.name for label in experiment.labels] |
| benchmark_names_and_iterations = [(benchmark.name, benchmark.iterations) |
| for benchmark in experiment.benchmarks] |
| run_keyvals = _ExperimentToKeyvals(experiment, for_json_report) |
| ignore_min_max = experiment.ignore_min_max |
| read_perf_report = functools.partial(_ReadExperimentPerfReport, |
| experiment.results_directory) |
| cwp_dso = experiment.cwp_dso |
| weights = [(benchmark.name, benchmark.weight) |
| for benchmark in experiment.benchmarks] |
| return BenchmarkResults(label_names, benchmark_names_and_iterations, |
| run_keyvals, ignore_min_max, read_perf_report, |
| cwp_dso, weights) |
| |
| |
| def _GetElemByName(name, from_list): |
| """Gets an element from the given list by its name field. |
| |
| Raises an error if it doesn't find exactly one match. |
| """ |
| elems = [e for e in from_list if e.name == name] |
| if len(elems) != 1: |
| raise ValueError('Expected 1 item named %s, found %d' % (name, len(elems))) |
| return elems[0] |
| |
| |
| def _Unlist(l): |
| """If l is a list, extracts the first element of l. Otherwise, returns l.""" |
| return l[0] if isinstance(l, list) else l |
| |
| |
| class JSONResultsReport(ResultsReport): |
| """Class that generates JSON reports for experiments.""" |
| |
| def __init__(self, |
| benchmark_results, |
| benchmark_date=None, |
| benchmark_time=None, |
| experiment=None, |
| json_args=None): |
| """Construct a JSONResultsReport. |
| |
| json_args is the dict of arguments we pass to json.dumps in GetReport(). |
| """ |
| super(JSONResultsReport, self).__init__(benchmark_results) |
| |
| defaults = TelemetryDefaults() |
| defaults.ReadDefaultsFile() |
| summary_field_defaults = defaults.GetDefault() |
| if summary_field_defaults is None: |
| summary_field_defaults = {} |
| self.summary_field_defaults = summary_field_defaults |
| |
| if json_args is None: |
| json_args = {} |
| self.json_args = json_args |
| |
| self.experiment = experiment |
| if not benchmark_date: |
| timestamp = datetime.datetime.strftime(datetime.datetime.now(), |
| '%Y-%m-%d %H:%M:%S') |
| benchmark_date, benchmark_time = timestamp.split(' ') |
| self.date = benchmark_date |
| self.time = benchmark_time |
| |
| @staticmethod |
| def FromExperiment(experiment, |
| benchmark_date=None, |
| benchmark_time=None, |
| json_args=None): |
| benchmark_results = BenchmarkResults.FromExperiment( |
| experiment, for_json_report=True) |
| return JSONResultsReport(benchmark_results, benchmark_date, benchmark_time, |
| experiment, json_args) |
| |
| def GetReportObjectIgnoringExperiment(self): |
| """Gets the JSON report object specifically for the output data. |
| |
| Ignores any experiment-specific fields (e.g. board, machine checksum, ...). |
| """ |
| benchmark_results = self.benchmark_results |
| label_names = benchmark_results.label_names |
| summary_field_defaults = self.summary_field_defaults |
| final_results = [] |
| for test, test_results in benchmark_results.run_keyvals.items(): |
| for label_name, label_results in zip(label_names, test_results): |
| for iter_results in label_results: |
| passed = iter_results.get('retval') == 0 |
| json_results = { |
| 'date': self.date, |
| 'time': self.time, |
| 'label': label_name, |
| 'test_name': test, |
| 'pass': passed, |
| } |
| final_results.append(json_results) |
| |
| if not passed: |
| continue |
| |
| # Get overall results. |
| summary_fields = summary_field_defaults.get(test) |
| if summary_fields is not None: |
| value = [] |
| json_results['overall_result'] = value |
| for f in summary_fields: |
| v = iter_results.get(f) |
| if v is None: |
| continue |
| # New telemetry results format: sometimes we get a list of lists |
| # now. |
| v = _Unlist(_Unlist(v)) |
| value.append((f, float(v))) |
| |
| # Get detailed results. |
| detail_results = {} |
| json_results['detailed_results'] = detail_results |
| for k, v in iter_results.items(): |
| if k == 'retval' or k == 'PASS' or k == ['PASS'] or v == 'PASS': |
| continue |
| |
| v = _Unlist(v) |
| if 'machine' in k: |
| json_results[k] = v |
| elif v is not None: |
| if isinstance(v, list): |
| detail_results[k] = [float(d) for d in v] |
| else: |
| detail_results[k] = float(v) |
| return final_results |
| |
| def GetReportObject(self): |
| """Generate the JSON report, returning it as a python object.""" |
| report_list = self.GetReportObjectIgnoringExperiment() |
| if self.experiment is not None: |
| self._AddExperimentSpecificFields(report_list) |
| return report_list |
| |
| def _AddExperimentSpecificFields(self, report_list): |
| """Add experiment-specific data to the JSON report.""" |
| board = self.experiment.labels[0].board |
| manager = self.experiment.machine_manager |
| for report in report_list: |
| label_name = report['label'] |
| label = _GetElemByName(label_name, self.experiment.labels) |
| |
| img_path = os.path.realpath(os.path.expanduser(label.chromeos_image)) |
| ver, img = ParseChromeosImage(img_path) |
| |
| report.update({ |
| 'board': board, |
| 'chromeos_image': img, |
| 'chromeos_version': ver, |
| 'chrome_version': label.chrome_version, |
| 'compiler': label.compiler |
| }) |
| |
| if not report['pass']: |
| continue |
| if 'machine_checksum' not in report: |
| report['machine_checksum'] = manager.machine_checksum[label_name] |
| if 'machine_string' not in report: |
| report['machine_string'] = manager.machine_checksum_string[label_name] |
| |
| def GetReport(self): |
| """Dump the results of self.GetReportObject() to a string as JSON.""" |
| # This exists for consistency with the other GetReport methods. |
| # Specifically, they all return strings, so it's a bit awkward if the JSON |
| # results reporter returns an object. |
| return json.dumps(self.GetReportObject(), **self.json_args) |