| #!/usr/bin/env python3 | |
| ## Copyright (c) 2023, Alliance for Open Media. All rights reserved | |
| ## | |
| ## This source code is subject to the terms of the BSD 3-Clause Clear License and the | |
| ## Alliance for Open Media Patent License 1.0. If the BSD 3-Clause Clear License was | |
| ## not distributed with this source code in the LICENSE file, you can obtain it | |
| ## at aomedia.org/license/software-license/bsd-3-c-c/. If the Alliance for Open Media Patent | |
| ## License 1.0 was not distributed with this source code in the PATENTS file, you | |
| ## can obtain it at aomedia.org/license/patent-license/. | |
| ## | |
| from __future__ import annotations | |
| from functools import partial | |
| import gc | |
| import glob | |
| import pathlib | |
| import sys | |
| import tempfile | |
| from typing import Sequence, Type | |
| from absl import app | |
| from absl import flags | |
| from absl import logging | |
| from avm_stats.extract_proto import * | |
| from avm_stats.frame_visualizations import * | |
| from avm_stats.proto_helpers import * | |
| from avm_stats.stats_aggregation import * | |
| import matplotlib.pyplot as plt | |
| import multiprocessing | |
| import pandas as pd | |
| from extractors.partition_type_extractor import PartitionTypeExtractor | |
| from extractors.prediction_mode_extractor import PredictionModeExtractor | |
| from extractors.symbol_bits_extractor import SymbolBitsExtractor | |
| from extractors.tx_type_extractor import TxTypeExtractor | |
| _EXTRACTORS = { | |
| "partition_type": PartitionTypeExtractor, | |
| "prediction_mode": PredictionModeExtractor, | |
| "symbol_bits": SymbolBitsExtractor, | |
| "tx_type": TxTypeExtractor, | |
| } | |
| _STREAM_GLOB = flags.DEFINE_multi_string( | |
| "stream_glob", None, "Path to AVM streams." | |
| ) | |
| flags.mark_flag_as_required("stream_glob") | |
| _EXTRACT_PROTO_BIN = flags.DEFINE_string( | |
| "extract_proto_bin", None, "Path to extract_proto binary." | |
| ) | |
| flags.mark_flag_as_required("extract_proto_bin") | |
| _OUTPUT_CSV = flags.DEFINE_string( | |
| "output_csv", None, "Path to output CSV file (optional)." | |
| ) | |
| _AGGREGATED_FIELD = flags.DEFINE_string( | |
| "aggregated_field", "count", "Field to aggregate on. By default will use the count." | |
| ) | |
| _GROUP_BY = flags.DEFINE_string( | |
| "group_by", None, "Group by these fields (comma separated)." | |
| ) | |
| _EXTRACTOR = flags.DEFINE_enum( | |
| "extractor", None, _EXTRACTORS.keys(), "Data extractor to use." | |
| ) | |
| flags.mark_flag_as_required("extractor") | |
| _THREADS = flags.DEFINE_integer( | |
| "threads", 1, "Number of parallel workers to spawn." | |
| ) | |
| _FRAME_LIMIT = flags.DEFINE_integer( | |
| "frame_limit", None, "Use at most this many frames from each stream." | |
| ) | |
| _PLOT = flags.DEFINE_multi_string( | |
| "plot", None, """Plot command args, e.g.: --plot 'title:"Block Sizes", field:"block_size", filter:"not is_intra_frame", limit:10'""" | |
| ) | |
| _TMP_DIR = flags.DEFINE_string( | |
| "tmp_dir", None, "Temp working dir." | |
| ) | |
| @dataclasses.dataclass | |
| class PlotArgs: | |
| title: str = "" | |
| field: str = "" | |
| filter: str = "" | |
| limit: str = "" | |
| def __init__(self, args_str: str): | |
| def kv_pair(kv: str) -> tuple[str, str]: | |
| k, v = [i.strip() for i in kv.split(":")] | |
| if v.startswith('"') and v.endswith('"'): | |
| v = v[1:-1] | |
| return (k, v) | |
| for k, v in [kv_pair(kv) for kv in args_str.split(",")]: | |
| if not hasattr(self, k): | |
| raise ValueError(f"Unknown plot arg: {k}") | |
| setattr(self, k, v) | |
| def filter_dataframe(df: pd.DataFrame, *, group_by: list[str], aggregated_field: str = "count", filt: str = "", limit: int | None = None): | |
| if filt: | |
| filtered_df = df.query(filt)[group_by + [aggregated_field]].groupby(group_by, as_index=False) | |
| else: | |
| filtered_df = df[group_by + [aggregated_field]].groupby(group_by, as_index=False) | |
| filtered_df = filtered_df.sum() | |
| filtered_df["percent"] = filtered_df[aggregated_field].transform(lambda x: x / x.sum() * 100) | |
| filtered_df = filtered_df.sort_values(by=[aggregated_field], ascending=False) | |
| if limit is not None: | |
| filtered_df_top_n = filtered_df.iloc[:limit, :] | |
| other_total = filtered_df.iloc[limit:, :][aggregated_field].sum() | |
| other_percent = filtered_df.iloc[limit:, :]["percent"].sum() | |
| others = pd.DataFrame({group_by[0]: ["Other"], aggregated_field: other_total, "percent": other_percent}) | |
| filtered_df = pd.concat([filtered_df_top_n, others]) | |
| return filtered_df | |
| def sum_dataframe(df: pd.DataFrame, *, group_by: list[str], aggregated_field: str = "count"): | |
| aggregated_df = df[group_by + [aggregated_field]].groupby(group_by, as_index=False) | |
| aggregated_df = aggregated_df.sum() | |
| return aggregated_df | |
| def create_plot( | |
| df: pd.DataFrame, | |
| plot_args: PlotArgs, | |
| ax: plt.Axes, | |
| legend_ax: plt.Axes, | |
| aggregated_field: str, | |
| ): | |
| limit = int(plot_args.limit) if plot_args.limit else None | |
| aggregated_df = filter_dataframe( | |
| df, group_by=[plot_args.field], aggregated_field=aggregated_field, filt=plot_args.filter, limit=limit) | |
| plot_title = plot_args.title or plot_args.field | |
| ax.set_title(f"{plot_title}") | |
| patches, _ = ax.pie( | |
| x=aggregated_df[aggregated_field], labels=aggregated_df[plot_args.field], autopct=None) | |
| labels = [f"{n} - {p:.1f}% ({v:.1f})" for n, p, v in zip(aggregated_df[plot_args.field], | |
| aggregated_df["percent"], aggregated_df[aggregated_field])] | |
| legend_ax.legend(patches, labels, loc="best", ncol=2) | |
| legend_ax.axis("off") | |
| def extract_to_temp_dir(stream_path: pathlib.Path, frame_limit: int | None = None) -> Iterator[proto_helpers.Frame]: | |
| with tempfile.TemporaryDirectory(dir=_TMP_DIR.value) as tmp_dir: | |
| tmp_path = pathlib.Path(tmp_dir) | |
| extract_proto_path = pathlib.Path(_EXTRACT_PROTO_BIN.value) | |
| stream_name = stream_path.stem | |
| output_path = tmp_path / stream_name | |
| try: | |
| output_path.mkdir() | |
| except FileExistsError: | |
| logging.fatal(f"Duplicate stream name: {stream_name}") | |
| yield from extract_and_load_protos( | |
| extract_proto_path=extract_proto_path, | |
| stream_path=stream_path, | |
| output_path=output_path, | |
| frame_limit=frame_limit, | |
| # Preserve the entire stream path so that the CTC config and class can be retrieved if available from the path. | |
| extra_args = ["--preserve_stream_path_depth", "-1"] | |
| ) | |
| def process_stream(stream_path: pathlib.Path, *, extractor_class: Type[Extractor]) -> pd.DataFrame: | |
| group_by = _GROUP_BY.value.split(",") | |
| frames = extract_to_temp_dir(stream_path, _FRAME_LIMIT.value) | |
| df = None | |
| for frame in frames: | |
| frame_df = aggregate_to_dataframe([frame], extractor_class()) | |
| frame_df["count"] = 1 | |
| if df is None: | |
| df = frame_df | |
| else: | |
| df = pd.concat([df, frame_df]) | |
| if len(df): | |
| df = sum_dataframe(df, group_by=group_by, aggregated_field=_AGGREGATED_FIELD.value) | |
| gc.collect() | |
| if len(df): | |
| df = sum_dataframe(df, group_by=group_by, aggregated_field=_AGGREGATED_FIELD.value) | |
| gc.collect() | |
| with open(f"/opt/tmp/progress/{stream_path.stem}.txt", "w") as f: | |
| f.write("Done") | |
| return df | |
| def main(argv: Sequence[str]) -> None: | |
| if len(argv) > 1: | |
| raise app.UsageError("Too many command-line arguments.") | |
| extractor_name = _EXTRACTOR.value | |
| try: | |
| extractor_class = _EXTRACTORS[extractor_name] | |
| except KeyError: | |
| print(f"Unknown extractor: {extractor_name}", file=sys.stderr) | |
| sys.exit(1) | |
| stream_paths = [] | |
| for stream_glob in _STREAM_GLOB.value: | |
| for stream in glob.glob(stream_glob, recursive=True): | |
| path = pathlib.Path(stream) | |
| stream_paths.append(path) | |
| stream_paths = sorted(stream_paths) | |
| with multiprocessing.Pool(_THREADS.value) as pool: | |
| dfs = pool.map( | |
| partial(process_stream, extractor_class=extractor_class), stream_paths) | |
| df = pd.concat(dfs) | |
| group_by = _GROUP_BY.value.split(",") | |
| aggregated_df = sum_dataframe(df, group_by=group_by, aggregated_field=_AGGREGATED_FIELD.value) | |
| if _OUTPUT_CSV.value: | |
| aggregated_df.to_csv(_OUTPUT_CSV.value) | |
| plots = _PLOT.value | |
| if plots: | |
| _, axes = plt.subplots(2, len(plots), squeeze=False, figsize=(10, 5)) | |
| for i, plot in enumerate(plots): | |
| args = PlotArgs(plot) | |
| create_plot(df, args, axes[0, i], axes[1, i], aggregated_field=_AGGREGATED_FIELD.value) | |
| plt.tight_layout() | |
| plt.show() | |
| if __name__ == "__main__": | |
| app.run(main) |