| #!/usr/bin/env python3 |
| |
| # Copyright 2021 The Chromium Authors |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import argparse |
| import json |
| import logging |
| import os |
| import pandas as pd |
| import plistlib |
| |
| |
| def GetDictionaryKeys(value, keys): |
| """ Returns a dictionary containing `keys` from `value` if present. """ |
| return {key: value[key] for key in keys if key in value} |
| |
| |
| def FlattenDictionary(value, keys=[]): |
| """ Returns a flattened version of the dictionary `value`, with nested keys |
| combined as a.b.c. """ |
| result = {} |
| if type(value) is dict: |
| for key in value: |
| result.update(FlattenDictionary(value[key], keys + [key])) |
| return result |
| else: |
| key = '.'.join(keys) |
| return {key: value} |
| |
| |
| def GetCoalition(coalitions_data, browser_identifier: str): |
| """ Returns the coalition data whose name matches the identifier |
| `browser_identifier`""" |
| for coalition in coalitions_data: |
| if coalition['name'] == browser_identifier: |
| return coalition |
| return None |
| |
| |
| def ReadPowerMetricsData(scenario_dir, browser_identifier: str): |
| with open(os.path.join(scenario_dir, "powermetrics.plist"), |
| "r") as plist_file: |
| data = plist_file.read().split('\0') |
| results = [] |
| for raw_sample in data: |
| if raw_sample == "": |
| continue |
| parsed_sample = plistlib.loads(str.encode(raw_sample)) |
| out_sample = {'elapsed_ns': parsed_sample['elapsed_ns']} |
| |
| # Processing output of the 'tasks' sampler. |
| coalition_keys = [ |
| "energy_impact", "gputime_ms", "diskio_byteswritten", |
| "diskio_bytesread", "idle_wakeups", "intr_wakeups", |
| "cputime_sample_ms_per_s", "cputime_ns" |
| ] |
| coalitions_data = parsed_sample['coalitions'] |
| if browser_identifier is not None: |
| browser_coalition_data = GetCoalition(coalitions_data, |
| browser_identifier) |
| out_sample['browser'] = GetDictionaryKeys(browser_coalition_data, |
| coalition_keys) |
| out_sample['all'] = GetDictionaryKeys(parsed_sample['all_tasks'], |
| coalition_keys) |
| |
| # Add information for coalitions that could be of interest since they |
| # might execute code on behalf of the browser. |
| out_sample['window_server'] = GetDictionaryKeys( |
| GetCoalition(coalitions_data, "com.apple.WindowServer"), |
| coalition_keys) |
| out_sample['kernel_coalition'] = GetDictionaryKeys( |
| GetCoalition(coalitions_data, "kernel_coalition"), coalition_keys) |
| |
| # Processing output of the 'cpu_power' sampler. |
| # Expected processor fields on Intel. |
| out_sample['processor'] = GetDictionaryKeys(parsed_sample['processor'], [ |
| 'freq_hz', |
| 'package_joules', |
| ]) |
| # Expected processor fields on M1. |
| out_sample['processor'].update( |
| GetDictionaryKeys(parsed_sample['processor'], [ |
| 'ane_energy', |
| 'dram_energy', |
| 'cpu_energy', |
| 'gpu_energy', |
| 'package_energy', |
| ])) |
| if 'clusters' in parsed_sample['processor']: |
| for cluster in parsed_sample['processor']['clusters']: |
| out_sample['processor'][cluster['name']] = GetDictionaryKeys( |
| cluster, ['power', 'idle_ns', 'freq_hz']) |
| results.append(FlattenDictionary(out_sample)) |
| return results |
| |
| |
| def NormalizeMicrosecondsSampleTime(sample_time: pd.Series): |
| # Round sample time to .1s to aggregate similar rows across different sources. |
| return (sample_time / 1000000.0).round(1) |
| |
| |
| def main(): |
| parser = argparse.ArgumentParser( |
| description='Parses, aggregates and summarizes power results') |
| parser.add_argument("--data_dir", |
| help="Directory containing benchmark data.", |
| required=True) |
| parser.add_argument('--verbose', |
| action='store_true', |
| help='Print verbose output.') |
| args = parser.parse_args() |
| |
| if args.verbose: |
| log_level = logging.DEBUG |
| else: |
| log_level = logging.INFO |
| logging.basicConfig(format='%(levelname)s: %(message)s', level=log_level) |
| |
| for name in os.listdir(args.data_dir): |
| subdir = os.path.join(args.data_dir, name) |
| if not os.path.isdir(subdir): |
| continue |
| metadata_path = os.path.join(subdir, "metadata.json") |
| if not os.path.isfile(metadata_path): |
| continue |
| with open(metadata_path, 'r') as metadata_file: |
| metadata = json.load(metadata_file) |
| logging.info(f'Found scenario {name}') |
| if 'browser' in metadata: |
| browser_identifier = metadata['browser']['identifier'] |
| logging.debug(f'Scenario running with {browser_identifier}') |
| else: |
| browser_identifier = None |
| powermetrics_data = ReadPowerMetricsData(subdir, browser_identifier) |
| powermetrics_dataframe = pd.DataFrame.from_records(powermetrics_data) |
| # Add sample_time to powermetrics. |
| powermetrics_dataframe["sample_time"] = NormalizeMicrosecondsSampleTime( |
| powermetrics_dataframe['elapsed_ns'].cumsum() / 1000.0) |
| powermetrics_dataframe.set_index('sample_time', inplace=True) |
| |
| with open(os.path.join(subdir, "power_sampler.json")) as power_file: |
| power_data = json.load(power_file) |
| power_dataframe = pd.DataFrame.from_records( |
| power_data['data_rows'], |
| columns=[key for key in power_data['column_labels']] + ["sample_time"]) |
| power_dataframe['sample_time'] = NormalizeMicrosecondsSampleTime( |
| power_dataframe['sample_time']) |
| power_dataframe.set_index('sample_time', inplace=True) |
| |
| # Join all data sources by sample_time. |
| scenario_data = powermetrics_dataframe.join(power_dataframe, how='outer') |
| |
| summary_path = os.path.join(subdir, 'summary.csv') |
| logging.info(f'Outputing results in {os.path.abspath(summary_path)}') |
| scenario_data.to_csv(summary_path) |
| |
| |
| if __name__ == "__main__": |
| main() |