| # Copyright 2024 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| from collections import Counter |
| import json |
| import logging |
| import os |
| import subprocess |
| import traceback |
| |
| from .file_util import (GetBenchmarkDBSize, GetDataDir, |
| GetExpectedHistogramsDictionary, |
| GetExpectedHistogramsFile, GetHistogramCountDeltasFile, |
| GetProcessedFiles, GetProcessor, GetRawHistogramsFile, |
| GetRunPathFile, MovePreviousHistogramsFiles) |
| from .histogram_list import GetSharedStorageUmaHistograms |
| from .util import GetNonePlaceholder, JsonDump |
| |
| |
| def _ProcessHistograms(histogram_data): |
| if not isinstance(histogram_data, list): |
| raise RuntimeError( |
| 'Expected histogram_data to be type list but got type %s' % |
| type(histogram_data)) |
| |
| guid_map = {} |
| histogram_info = {} |
| expected_processed_all_guids = False |
| benchmark_name = None |
| |
| for item in histogram_data: |
| if not isinstance(item, dict): |
| raise RuntimeError('Expected item to be type dict but got type %s' % |
| type(item)) |
| |
| if 'guid' in item: |
| if expected_processed_all_guids: |
| info = ('; %s\nitem[`guid`]: %s' % |
| (JsonDump(item), item.get('guid', '[[Not Found]]'))) |
| msg = 'Expected to process all items with `guid` before any with `name`' |
| raise RuntimeError(msg + info) |
| if len(item) != 3: |
| raise RuntimeError( |
| 'Expected item with `guid` to have length 3; got item: %s' % |
| JsonDump(item)) |
| if 'type' not in item: |
| raise RuntimeError( |
| 'Expected item with `guid` to have key `type`; got: %s' % |
| JsonDump(item)) |
| [value_key] = [key for key in item if key not in ('guid', 'type')] |
| guid_map[item['guid']] = item[value_key] |
| |
| elif 'name' in item: |
| expected_processed_all_guids = True |
| name = item['name'] |
| if not name in GetSharedStorageUmaHistograms(): |
| continue |
| for key in ['diagnostics', 'running', 'sampleValues']: |
| if key not in item: |
| raise RuntimeError( |
| 'Expected item with `name` to have key `%s`; got: %s' % |
| (key, JsonDump(item))) |
| |
| histogram_info.setdefault(name, []) |
| item_diagnostics = item['diagnostics'] |
| diagnostics = { |
| key: guid_map.get(item_diagnostics[key], item_diagnostics[key]) |
| for key in item_diagnostics |
| } |
| item_info = { |
| 'diagnostics': diagnostics, |
| 'running': item['running'], |
| 'sampleValues': item['sampleValues'] |
| } |
| histogram_info[name].append(item_info) |
| if not benchmark_name: |
| if (not 'benchmarks' in diagnostics |
| or len(diagnostics['benchmarks']) != 1): |
| RuntimeError( |
| 'Expected `diagnostics` to have `benchmarks` of length 1; got %s' |
| % JsonDump(diagnostics)) |
| benchmark_name = diagnostics['benchmarks'][0] |
| |
| agg_histograms = {} |
| by_story = {} |
| counts = {} |
| |
| for histogram, info_list in histogram_info.items(): |
| agg_histograms.setdefault(histogram, []) |
| |
| for entry in info_list: |
| for key in ['diagnostics', 'sampleValues']: |
| if key not in entry: |
| raise RuntimeError('Entry %s does not have key `%s`' % |
| (JsonDump(entry), key)) |
| for key in ['stories', 'storysetRepeats']: |
| if key not in entry['diagnostics']: |
| raise RuntimeError('Entry diagnostics %s does not have key `%s`' % |
| (JsonDump(entry), key)) |
| if len(entry['diagnostics'][key]) != 1: |
| raise RuntimeError( |
| 'Entry diagnostics %s expected `%s` to lave length 1' % |
| (JsonDump(entry), key)) |
| |
| agg_histograms[histogram] += entry['sampleValues'] |
| |
| [story] = entry['diagnostics']['stories'] |
| [rep] = entry['diagnostics']['storysetRepeats'] |
| |
| by_story.setdefault(story, {}).setdefault(rep, |
| {}).setdefault(histogram, []) |
| by_story[story][rep][histogram] += entry['sampleValues'] |
| counts.setdefault(story, {}).setdefault(rep, Counter()) |
| counts[story][rep][histogram] += len(entry['sampleValues']) |
| |
| runs_all_match, deltas = _CheckHistogramCounts(counts) |
| results = [histogram_info, agg_histograms, by_story, counts, deltas] |
| size = GetBenchmarkDBSize(benchmark_name) |
| return runs_all_match, size, list(map(JsonDump, results)) |
| |
| |
| def _CheckHistogramCounts(actual_counts): |
| counts_match = True |
| expected_counts = GetExpectedHistogramsDictionary() |
| missed_stories = (set(expected_counts.keys()).difference( |
| set(actual_counts.keys()))) |
| if len(missed_stories) > 0: |
| counts_match = False |
| logging.warning('Missed recording for stories %s' % |
| repr(list(missed_stories))) |
| unexpected_stories = (set(actual_counts.keys()).difference( |
| set(expected_counts.keys()))) |
| if len(missed_stories) > 0: |
| counts_match = False |
| logging.warning('Unexpectedly recorded for stories %s' % |
| repr(list(unexpected_stories))) |
| |
| deltas = {} |
| for story, repeats in actual_counts.items(): |
| for repeat, histogram_counts in repeats.items(): |
| if histogram_counts == expected_counts[story]: |
| continue |
| counts_match = False |
| deltas.setdefault(story, {}).setdefault(repeat, Counter()) |
| missed_histograms = (set(expected_counts[story].keys()).difference( |
| set(histogram_counts.keys()))) |
| for histogram in missed_histograms: |
| expected_count = expected_counts[story][histogram] |
| deltas[story][repeat][histogram] = -expected_count |
| for histogram, count in histogram_counts.items(): |
| expected_count = expected_counts[story].get(histogram, 0) |
| if count != expected_count: |
| deltas[story][repeat][histogram] = count - expected_count |
| logging.warning( |
| 'Story %s, repeat %s failed to record expected histogram counts' % |
| (story, repeat)) |
| logging.warning('Expected counts: %s' % JsonDump(expected_counts[story])) |
| logging.warning('Actual counts: %s' % JsonDump(histogram_counts)) |
| |
| return counts_match, deltas |
| |
| |
| def _ProcessResultsInternal(): |
| if not os.path.exists(GetDataDir()) or not os.path.isdir(GetDataDir()): |
| raise RuntimeError( |
| 'Data directory %s does not exist or is not a directory' % |
| (GetDataDir())) |
| |
| if (not os.path.exists(GetRunPathFile()) |
| or not os.path.isfile(GetRunPathFile())): |
| raise RuntimeError('Run path file %s does not exist or is not a file' % |
| GetRunPathFile()) |
| |
| run_results_path = None |
| with open(GetRunPathFile(), 'r') as f: |
| run_results_path = f.read() |
| none_placeholder = GetNonePlaceholder() |
| if not run_results_path or run_results_path.find(none_placeholder) > -1: |
| raise RuntimeError('Directory path of perf results not found') |
| |
| MovePreviousHistogramsFiles() |
| |
| subprocess.check_call([ |
| GetProcessor(), "--output-format=histograms", |
| "--intermediate-dir=" + run_results_path |
| ]) |
| |
| if (not os.path.exists(GetRawHistogramsFile()) |
| or not os.path.isfile(GetRawHistogramsFile())): |
| raise RuntimeError('Histogram results %s not found or is not a file' % |
| GetRawHistogramsFile()) |
| |
| histogram_data = None |
| with open(GetRawHistogramsFile(), 'r') as f: |
| histogram_data = json.load(f) |
| |
| runs_all_match, size, results = _ProcessHistograms(histogram_data) |
| print("Benchmark's initial database size: %s" % size) |
| if runs_all_match: |
| print('SUCCESS: Actual histogram counts are equal to expected counts ' + |
| 'for all stories and repeats.') |
| else: |
| print('WARNING: Actual histogram counts differ from expected counts for ' + |
| 'at least one story/repeat. See file://%s for the deltas.' % |
| GetHistogramCountDeltasFile()) |
| print('View expected histogram counts at file://%s' % |
| GetExpectedHistogramsFile()) |
| for (result, filename) in zip(results, GetProcessedFiles()): |
| if filename == GetHistogramCountDeltasFile(): |
| json_result = json.loads(result) |
| if runs_all_match and len(json_result) == 0: |
| continue |
| if runs_all_match: |
| logging.warning(('Unexpectedly got histogram count deltas %s when ' % |
| result) + |
| 'all runs were reported to have counts match') |
| elif len(json_result) == 0: |
| logging.warning( |
| 'A discrepancy in run counts was reported, but the returned ' + |
| 'deltas object was empty') |
| with open(filename, 'w') as f: |
| f.write(result) |
| print('View processed results at file://%s' % filename) |
| |
| |
| def ProcessResults(): |
| try: |
| _ProcessResultsInternal() |
| return 0 |
| except Exception as e: # pylint: disable=broad-except |
| logging.warning('Encountered exception: %s\n%s' % |
| (repr(e), traceback.format_exc())) |
| return 1 |