TUI: Kernel Selection (#769)

Bu işleme şunda yer alıyor:
xuchen-amd
2025-08-01 11:31:43 -04:00
işlemeyi yapan: GitHub
ebeveyn 81daaaecc7
işleme 9ddee8de2e
21 değiştirilmiş dosya ile 520 ekleme ve 889 silme
+3
Dosyayı Görüntüle
@@ -25,6 +25,8 @@ Full documentation for ROCm Compute Profiler is available at [https://rocm.docs.
* CLI analysis mode baseline comparison will now only compare common metrics across workloads and will not show Metric ID
* Remove metrics from analysis configuration files which are explicitly marked as empty or None
* Change the basic view of TUI from aggregated analysis data to individual kernel analysis data
### Resolved issues
* Fixed not detecting memory clock issue when using amd-smi
@@ -42,6 +44,7 @@ Full documentation for ROCm Compute Profiler is available at [https://rocm.docs.
* Usage of rocm-smi
* Hardware IP block based filtering has been removed in favor of analysis report block based filtering
* Remove aggregated analysis view from TUI mode
## ROCm Compute Profiler 3.2.1 for ROCm 7.0.0
İkili dosya gösterilmiyor.

Sonra

Genişlik:  |  Yükseklik:  |  Boyut: 78 KiB

İkili dosya gösterilmiyor.

Sonra

Genişlik:  |  Yükseklik:  |  Boyut: 141 KiB

+2
Dosyayı Görüntüle
@@ -22,6 +22,7 @@
# SOFTWARE.
##############################################################################el
import re
from pathlib import Path
# NB: Creating a new module to share global vars across modules
@@ -30,6 +31,7 @@ PROJECT_NAME = "rocprofiler-compute"
HIDDEN_COLUMNS = ["coll_level"]
HIDDEN_COLUMNS_CLI = ["Description", "coll_level"]
HIDDEN_COLUMNS_TUI = ["Description", "coll_level"]
HIDDEN_SECTIONS = [400, 1900, 2000]
TIME_UNITS = {"s": 10**9, "ms": 10**6, "us": 10**3, "ns": 1}
+35 -57
Dosyayı Görüntüle
@@ -23,11 +23,13 @@
##############################################################################el
import copy
import sys
from pathlib import Path
from rocprof_compute_analyze.analysis_base import OmniAnalyze_Base
from rocprof_compute_tui.utils.tui_utils import process_panels_to_dataframes
from rocprof_compute_tui.utils.tui_utils import (
get_top_kernels_and_dispatch_ids,
process_panels_to_dataframes,
)
from utils import file_io, parser, schema
from utils.kernel_name_shortener import kernel_name_shortener
from utils.logger import console_error, demarcate
@@ -38,23 +40,21 @@ class tui_analysis(OmniAnalyze_Base):
super().__init__(args, supported_archs)
self.path = str(path)
self.arch = None
self.raw_dfs = {}
self.kernel_dfs = {}
# -----------------------
# Required child methods
# -----------------------
@demarcate
def pre_processing(self):
"""Perform any pre-processing steps prior to analysis."""
# Read profiling config
self._profiling_config = file_io.load_profiling_config(self.path)
# initalize runs
self._runs = self.initalize_runs()
if self.get_args().random_port:
console_error("--gui flag is required to enable --random-port")
# create 'mega dataframe'
self._runs[self.path].raw_pmc = file_io.create_df_pmc(
self.path,
self.get_args().nodes,
@@ -80,22 +80,33 @@ class tui_analysis(OmniAnalyze_Base):
kernel_verbose=self.get_args().kernel_verbose,
)
# demangle and overwrite original 'Kernel_Name'
kernel_name_shortener(
self._runs[self.path].raw_pmc, self.get_args().kernel_verbose
)
# create the loaded table
parser.load_table_data(
workload=self._runs[self.path],
dir=self.path,
is_gui=False,
args=self.get_args(),
config=self._profiling_config,
# 1. load top kernel
parser.load_kernel_top(
workload=self._runs[self.path], dir=self.path, args=self.get_args()
)
# 2. load table data for each kernel
self.raw_dfs.clear()
for idx in self._runs[self.path].raw_pmc.index:
kernel_df = self._runs[self.path].raw_pmc.loc[[idx]]
kernel_name = kernel_df.pmc_perf["Kernel_Name"].loc[idx]
this_dfs = copy.deepcopy(self._runs[self.path].dfs)
parser.eval_metric(
this_dfs,
self._runs[self.path].dfs_type,
self._runs[self.path].sys_info.iloc[0],
kernel_df,
self.get_args().debug,
self._profiling_config,
)
self.raw_dfs[kernel_name] = this_dfs
def initalize_runs(self, normalization_filter=None):
# load required configs
sysinfo_path = Path(self.path)
sys_info = file_io.load_sys_info(sysinfo_path.joinpath("sysinfo.csv"))
self.arch = sys_info.iloc[0]["gpu_arch"]
@@ -111,10 +122,6 @@ class tui_analysis(OmniAnalyze_Base):
self.load_options(normalization_filter)
w = schema.Workload()
# FIXME:
# For regular single node case, load sysinfo.csv directly
# For multi-node, either the default "all", or specified some,
# pick up the one in the 1st sub_dir. We could fix it properly later.
w.sys_info = file_io.load_sys_info(sysinfo_path.joinpath("sysinfo.csv"))
mspec = self.get_socs()[self.arch]._mspec
if args.specs_correction:
@@ -127,43 +134,14 @@ class tui_analysis(OmniAnalyze_Base):
return self._runs
@demarcate
def run_analysis(self):
"""Run TUI analysis."""
super().run_analysis()
roof_plot = None
# 1. check if not baseline && compatible soc:
if self.arch in [
# >= MI200
"gfx90a",
"gfx940",
"gfx941",
"gfx942",
"gfx950",
]:
# add roofline plot to cli output
self.get_socs()[self.arch].analysis_setup(
roofline_parameters={
"workload_dir": self.path,
"device_id": 0,
"sort_type": "kernels",
"mem_level": "ALL",
"include_kernel_names": False,
"is_standalone": False,
"roofline_data_type": "FP32",
}
def run_kernel_analysis(self):
self.kernel_dfs.clear()
for kernel_name, df in self.raw_dfs.items():
self.kernel_dfs[kernel_name] = process_panels_to_dataframes(
self.get_args(), df, self._arch_configs[self.arch], roof_plot=None
)
roof_obj = self.get_socs()[self.arch].roofline_obj
return self.kernel_dfs
if roof_obj:
# NOTE: using default data type
roof_plot = roof_obj.cli_generate_plot(roof_obj.get_dtype()[0])
results = process_panels_to_dataframes(
self.get_args(),
self._runs,
self._arch_configs[self.arch],
self._profiling_config,
roof_plot=roof_plot,
)
return results
@demarcate
def run_top_kernel(self):
return get_top_kernels_and_dispatch_ids(self._runs)
-1
Dosyayı Görüntüle
@@ -30,7 +30,6 @@ Central configuration for the application.
# Application settings
APP_TITLE = "ROCm Compute Profiler TUI"
VERSION = "3.2.0"
# Widget configurations
DEFAULT_COLLAPSIBLE_STATE = True # True = collapsed by default
+6 -2
Dosyayı Görüntüle
@@ -39,15 +39,18 @@ from textual.binding import Binding
from textual.widgets import Button, Footer, Header
from textual_fspicker import SelectDirectory
from rocprof_compute_tui.config import APP_TITLE, VERSION
import config
from rocprof_compute_tui.config import APP_TITLE
from rocprof_compute_tui.views.main_view import MainView
from rocprof_compute_tui.widgets.menu_bar.menu_bar import DropdownMenu
from utils.specs import MachineSpecs, generate_machine_specs
from utils.utils import get_version
class RocprofTUIApp(App):
"""Main application for the performance analysis tool."""
VERSION = get_version(config.rocprof_compute_home)["version"]
TITLE = f"{APP_TITLE} v{VERSION}"
SUB_TITLE = "Workload Analysis Tool"
@@ -55,7 +58,8 @@ class RocprofTUIApp(App):
BINDINGS = [
Binding(key="q", action="quit", description="Quit"),
Binding(key="r", action="refresh", description="Refresh"),
Binding(key="a", action="analyze", description="Analyze"),
# TODO
# Binding(key="a", action="analyze", description="Analyze"),
]
def __init__(
-51
Dosyayı Görüntüle
@@ -1,51 +0,0 @@
sections:
- title: "📊 Summaries"
collapsed: true
class: "summary-section"
subsections:
- title: "Top Kernels"
data_path: ["0. Top Stats", "0.1 Top Kernels"]
collapsed: true
header_label: "Top Kernels by Duration (ns):"
header_class: "section-header"
- title: "Dispatch List"
data_path: ["0. Top Stats", "0.2 Dispatch List"]
collapsed: true
- title: "System Info"
data_path: ["1. System Info", "1.1"]
collapsed: true
- title: "⚡ High Level Analysis"
collapsed: true
class: "sysinfo-section"
subsections:
- title: "System Speed-of-Light"
data_path: ["2. System Speed-of-Light", "2.1 Speed-of-Light"]
collapsed: true
- title: "Roofline"
collapsed: true
tui_style: "roofline"
widget_id: "roofline-plot"
- title: "Memory Chart"
data_path: ["3. Memory Chart", "3.1 Memory Chart"]
collapsed: true
tui_style: "mem_chart"
- title: "🔍 Detailed Block Analysis"
collapsed: true
class: "kernels-section"
dynamic_sections: true
skip_sections:
- "0. Top Stats"
- "1. System Info"
- "2. System Speed-of-Light"
- "3. Memory Chart"
- "4. Roofline"
- title: "🚧 Source Level Analysis"
collapsed: true
class: "source-section"
subsections:
- title: "PC Sampling"
data_path: ["21. PC Sampling", "21.1 PC Sampling"]
collapsed: true
+35
Dosyayı Görüntüle
@@ -0,0 +1,35 @@
# TODO: add System Info
# - title: "System Info"
# data_path: ["1. System Info", "1.1"]
# collapsed: true
sections:
- title: "High Level Analysis"
collapsed: true
class: "sysinfo-section"
subsections:
- title: "System Speed-of-Light"
data_path: ["2. System Speed-of-Light", "2.1 System Speed-of-Light"]
collapsed: true
- title: "Memory Chart"
data_path: ["3. Memory Chart", "3.1 Memory Chart"]
collapsed: true
tui_style: "mem_chart"
- title: "Detailed Block Analysis"
collapsed: true
class: "kernels-section"
dynamic_sections: true
skip_sections:
- "0. Top Stats"
- "1. System Info"
- "2. System Speed-of-Light"
- "3. Memory Chart"
- "4. Roofline"
- title: "Source Level Analysis"
collapsed: true
class: "source-section"
subsections:
- title: "PC Sampling"
data_path: ["21. PC Sampling", "21.1 PC Sampling"]
collapsed: true
+77 -497
Dosyayı Görüntüle
@@ -1,87 +1,27 @@
import copy
import logging
import os
import re
from collections import defaultdict
from datetime import datetime
from enum import Enum
from pathlib import Path
import numpy as np
import pandas as pd
from config import HIDDEN_COLUMNS, HIDDEN_SECTIONS
supported_field = [
"Value",
"Minimum",
"Maximum",
"Average",
"Median",
"Min",
"Max",
"Avg",
"Pct of Peak",
"Peak",
"Count",
"Mean",
"Pct",
"Std Dev",
"Q1",
"Q3",
"Expression",
# Special keywords for L2 channel
"Channel",
"L2 Cache Hit Rate",
"Requests",
"L2 Read",
"L2 Write",
"L2 Atomic",
"L2-Fabric Requests",
"L2-Fabric Read",
"L2-Fabric Write and Atomic",
"L2-Fabric Atomic",
"L2 Read Req",
"L2 Write Req",
"L2 Atomic Req",
"L2-Fabric Read Req",
"L2-Fabric Write and Atomic Req",
"L2-Fabric Atomic Req",
"L2-Fabric Read Latency",
"L2-Fabric Write Latency",
"L2-Fabric Atomic Latency",
"L2-Fabric Read Stall (PCIe)",
"L2-Fabric Read Stall (Infinity Fabric™)",
"L2-Fabric Read Stall (HBM)",
"L2-Fabric Write Stall (PCIe)",
"L2-Fabric Write Stall (Infinity Fabric™)",
"L2-Fabric Write Stall (HBM)",
"L2-Fabric Write Starve",
]
import config
class LogLevel(str, Enum):
"""Log levels for consistent logging."""
INFO = "info"
WARNING = "warning"
ERROR = "error"
SUCCESS = "success" # Maintained for UI compatibility
SUCCESS = "success"
class Logger:
"""Centralized logging handler for the application."""
def __init__(self, output_area=None):
"""
Initialize the logger.
"""
self.output_area = output_area
self._setup_logger()
def _setup_logger(self):
"""
Setup the Python logger with proper formatting.
"""
self.logger = logging.getLogger("app")
self.logger.setLevel(logging.INFO)
@@ -94,15 +34,9 @@ class Logger:
self.logger.addHandler(handler)
def set_output_area(self, output_area):
"""
Set or update the output area for displaying logs.
"""
self.output_area = output_area
def log(self, message, level=LogLevel.INFO, update_ui=True):
"""
Log a message with the specified level.
"""
level_map = {
LogLevel.INFO: logging.INFO,
LogLevel.SUCCESS: logging.INFO,
@@ -145,151 +79,28 @@ class Logger:
self.log(message, LogLevel.ERROR, update_ui)
def split_table_line(line):
"""
Splits a table row line into a list of cell strings (trimmed). For example:
def get_top_kernels_and_dispatch_ids(runs):
if not runs:
return None
│ │ Kernel_Name │ Count │ ...
"""
base_run = next(iter(runs.values()))
if not hasattr(base_run, "dfs"):
return None
cells = line.split("")
if cells and cells[0] == "":
cells = cells[1:]
if cells and cells[-1] == "":
cells = cells[:-1]
return [cell.strip() for cell in cells]
top_kernel_df = base_run.dfs.get(1)
dispatch_id_df = base_run.dfs.get(2)
if top_kernel_df is None or dispatch_id_df is None:
return None
merged_df = pd.merge(
top_kernel_df, dispatch_id_df, on="Kernel_Name", how="outer"
).sort_values("Pct", ascending=False)
return merged_df.to_dict("records")
def parse_ascii_table(table_lines):
"""
Given a list of lines belonging to one ASCII table (including border rows),
return a tuple (header, data_rows) where header is a list of column names and
data_rows is a list of rows (each a list of cell strings).
Skips border/separator lines and also checks for continuation
rows (which have an empty first cell). Continuation rows get merged into the previous row.
"""
header = None
data_rows = []
for line in table_lines:
if re.match(r"^[╒╞╘├└─]+", line):
continue
if "" not in line:
continue
cells = split_table_line(line)
if header is None:
header = cells
continue
if cells and cells[0] == "":
if data_rows: # There should be at least one row already.
for i, cell in enumerate(cells):
if cell:
data_rows[-1][i] += " " + cell
else:
continue
else:
data_rows.append(cells)
return header, data_rows
def parse_file(filename):
"""
Returns nested structure:
{
"0. Top Stats": {
"0.1 Top Kernels": {header: [...], data: [...]},
"0.2 Dispatch List": {header: [...], data: [...]}
},
"1. System Info": {
"1.1 System Information": {header: [...], data: [...]}
},
...
}
"""
with open(filename, "r", encoding="utf-8") as f:
lines = f.readlines()
sections = {}
current_section = None
current_subsection = None
table_lines = []
in_table = False
for line in lines:
line = line.rstrip("\n")
# Skip separator lines
if line.startswith(
"--------------------------------------------------------------------------------"
):
continue
# Check for section header (e.g., "0. Top Stats")
section_match = re.match(r"^\s*(\d+\. .+)$", line)
if section_match:
current_section = section_match.group(1).strip()
sections[current_section] = {}
continue
# Check for subsection header (e.g., "0.1 Top Kernels")
# FIXME: 1. System Info is an exception, no subsection
subsection_match = re.match(r"^\s*(\d+\.\d+ .+)$", line)
if subsection_match:
current_subsection = subsection_match.group(1).strip()
if current_section is None:
current_section = "Uncategorized"
sections[current_section] = {}
continue
# Table parsing logic
if line.startswith(""):
in_table = True
table_lines = [line]
continue
if in_table:
table_lines.append(line)
if line.startswith(""):
if current_section and current_subsection:
header, data = parse_ascii_table(table_lines)
sections[current_section][current_subsection] = {
"header": header,
"data": data,
}
in_table = False
table_lines = []
return sections
def get_table_dfs():
filename = str(Path(os.getcwd()).joinpath("analyze_output.csv"))
sections_info = parse_file(filename)
# Convert to DataFrames while maintaining nested structure
section_dfs = {}
for section_name, subsections in sections_info.items():
section_dfs[section_name] = {}
for subsection_name, table_data in subsections.items():
if table_data and table_data["data"]:
try:
df = pd.DataFrame(table_data["data"], columns=table_data["header"])
section_dfs[section_name][subsection_name] = df
except Exception as e:
print(f"Error creating DataFrame for {subsection_name}: {e}")
continue
return section_dfs
def process_panels_to_dataframes(
args, runs, archConfigs, profiling_config, roof_plot=None
):
def process_panels_to_dataframes(args, kernel_df, archConfigs, roof_plot=None):
"""
Process panel data into pandas DataFrames.
Returns a nested dictionary structure with DataFrames and tui_style information.
@@ -305,318 +116,87 @@ def process_panels_to_dataframes(
}
"""
comparable_columns = build_comparable_columns(args.time_unit)
filter_panel_ids = profiling_config.get("filter_blocks", [])
if isinstance(filter_panel_ids, dict):
# For backward compatibility
filter_panel_ids = [
name for name, type in filter_panel_ids.items() if type == "metric_id"
]
filter_panel_ids = [
int(convert_metric_id_to_panel_info(metric_id)[0])
for metric_id in filter_panel_ids
]
# TODO: add individual kernel roofline logic
# TODO: implement args logic:
# args.filter_metrics
# args.cols
# args.max_stat_num
# args.df_file_dir
# Initialize the result structure
result_structure = defaultdict(dict)
decimal_precision = getattr(args, "decimal", 2) if args else 2
for panel_id, panel in archConfigs.panel_configs.items():
# Skip panels that don't support baseline comparison
if panel_id in HIDDEN_SECTIONS:
if panel_id in config.HIDDEN_SECTIONS:
continue
# Get section name (e.g., "0. Top Stats")
section_name = f"{panel_id // 100}. {panel['title']}"
for data_source in panel["data source"]:
for type, table_config in data_source.items():
# Check for filtering conditions
if (
not args.filter_metrics
and filter_panel_ids
and table_config["id"] not in filter_panel_ids
and panel_id not in filter_panel_ids
and panel_id > 100
):
table_id_str = (
str(table_config["id"] // 100)
+ "."
+ str(table_config["id"] % 100)
)
table_id = table_config["id"]
if table_id not in kernel_df:
continue
# Process the data
base_run, base_data = next(iter(runs.items()))
base_df = base_data.dfs[table_config["id"]]
base_df = kernel_df[table_id]
if base_df is None or base_df.empty:
continue
df = pd.DataFrame(index=base_df.index)
# Process columns
for header in list(base_df.keys()):
if should_process_column(header, args, type):
if header in HIDDEN_COLUMNS:
pass
elif header not in comparable_columns:
df = process_non_comparable_column(
df, header, base_df, type, table_config, runs
)
else:
df = process_comparable_column(
df,
header,
base_df,
table_config,
runs,
base_run,
type,
args,
HIDDEN_COLUMNS,
)
for header in list(base_df.columns):
if header in config.HIDDEN_COLUMNS_TUI:
continue
else:
df[header] = base_df[header]
if not df.empty:
# Check for empty columns
is_empty_columns_exist = check_empty_columns(df)
df = apply_rounding_logic(df, decimal_precision)
if not is_empty_columns_exist:
# Get subsection name
table_id_str = (
str(table_config["id"] // 100)
+ "."
+ str(table_config["id"] % 100)
)
subsection_name = table_id_str
if "title" in table_config and table_config["title"]:
subsection_name += " " + table_config["title"]
subsection_name = (
str(table_config["id"] // 100) + "." + str(table_config["id"] % 100)
)
if "title" in table_config and table_config["title"]:
subsection_name += " " + table_config["title"]
# Handle special cases for top stats
if type == "raw_csv_table" and (
table_config["source"] == "pmc_kernel_top.csv"
or table_config["source"] == "pmc_dispatch_info.csv"
):
df = df.head(args.max_stat_num)
result_structure[section_name][subsection_name] = {
"df": df,
"tui_style": None,
}
# Check for transpose requirement
transpose = (
type != "raw_csv_table"
and "columnwise" in table_config
and table_config.get("columnwise") == True
)
if type == "metric_table" and "tui_style" in table_config:
result_structure[section_name][subsection_name]["tui_style"] = (
table_config["tui_style"]
)
if transpose:
df = df.T
# Store the DataFrame with tui_style as separate keys
result_structure[section_name][subsection_name] = {
"df": df,
"tui_style": None,
}
# Set tui_style if available
if type == "metric_table" and "tui_style" in table_config:
result_structure[section_name][subsection_name][
"tui_style"
] = table_config["tui_style"]
# Save to CSV if requested
if args.df_file_dir:
save_dataframe_to_csv(df, table_id_str, table_config, args)
result_structure["4. Roofline"] = roof_plot
return dict(result_structure)
def should_process_column(header, args, type):
"""Check if a column should be processed based on arguments."""
return (
(not args.cols)
or (
args.cols and header in args.cols
) # Assuming args.cols is now a list of column names
or (type == "raw_csv_table")
)
def apply_rounding_logic(df, decimal_precision):
df_copy = df.copy()
for column in df_copy.columns:
if column in ["Metric", "Tips", "coll_level", "Unit", "Kernel_Name", "Info"]:
continue
def process_non_comparable_column(df, header, base_df, type, table_config, runs):
"""Process columns that are not comparable across runs."""
if (
type == "raw_csv_table"
and (
table_config["source"] == "pmc_kernel_top.csv"
or table_config["source"] == "pmc_dispatch_info.csv"
)
and header == "Kernel_Name"
):
# Adjust kernel name width based on source
if table_config["source"] == "pmc_kernel_top.csv":
adjusted_name = base_df["Kernel_Name"].apply(
lambda x: string_multiple_lines(x, 40, 3)
)
if df_copy[column].dtype in ["float64", "float32", "int64", "int32"]:
df_copy[column] = df_copy[column].round(decimal_precision)
else:
adjusted_name = base_df["Kernel_Name"].apply(
lambda x: string_multiple_lines(x, 80, 4)
)
df = pd.concat([df, adjusted_name], axis=1)
elif type == "raw_csv_table" and header == "Info":
for run, data in runs.items():
cur_df = data.dfs[table_config["id"]]
df = pd.concat([df, cur_df[header]], axis=1)
else:
df = pd.concat([df, base_df[header]], axis=1)
try:
numeric_series = pd.to_numeric(df_copy[column], errors="coerce")
if not numeric_series.isna().all():
rounded_series = numeric_series.round(decimal_precision)
return df
if df_copy[column].dtype == "object":
df_copy[column] = df_copy[column].combine(
rounded_series,
lambda orig, rounded: rounded if pd.notna(rounded) else orig,
)
else:
df_copy[column] = rounded_series
except (ValueError, TypeError):
continue
def process_comparable_column(
df, header, base_df, table_config, runs, base_run, type, args, hidden_columns
):
"""Process columns that can be compared across runs."""
for run, data in runs.items():
cur_df = data.dfs[table_config["id"]]
if (type == "raw_csv_table") or (
type == "metric_table" and (header not in hidden_columns)
):
if run != base_run:
# Calculate percentage over the baseline
base_values = [float(x) if x != "" else float(0) for x in base_df[header]]
cur_values = [float(x) if x != "" else float(0) for x in cur_df[header]]
base_df[header] = base_values
cur_df[header] = cur_values
t_df = pd.concat(
[base_df[header], cur_df[header]],
axis=1,
)
absolute_diff = (t_df.iloc[:, 1] - t_df.iloc[:, 0]).round(args.decimal)
t_df = absolute_diff / t_df.iloc[:, 0].replace(0, 1)
t_df_pretty = t_df.astype(float).mul(100).round(args.decimal)
# Show value + percentage
t_df = (
cur_df[header].astype(float).round(args.decimal).map(str).astype(str)
+ " ("
+ t_df_pretty.map(str)
+ "%)"
)
df = pd.concat([df, t_df], axis=1)
# Check for threshold violations
if (
header in ["Value", "Count", "Avg"]
and t_df_pretty.abs().gt(args.report_diff).any()
):
df["Abs Diff"] = absolute_diff
if args.report_diff:
violation_idx = t_df_pretty.index[
t_df_pretty.abs() > args.report_diff
]
else:
cur_df_copy = copy.deepcopy(cur_df)
cur_df_copy[header] = [
(round(float(x), args.decimal) if x != "" else x)
for x in base_df[header]
]
df = pd.concat([df, cur_df_copy[header]], axis=1)
return df
def check_empty_columns(df):
"""Check if any column in the DataFrame is empty."""
return any(
[
df.columns[col_idx]
for col_idx in range(len(df.columns))
if df.replace("", None).iloc[:, col_idx].isnull().all()
]
)
def save_dataframe_to_csv(df, table_id_str, table_config, args):
"""Save DataFrame to CSV file if directory is specified."""
p = Path(args.df_file_dir)
if not p.exists():
p.mkdir()
if p.is_dir():
filename = table_id_str
if "title" in table_config and table_config["title"]:
filename += "_" + table_config["title"]
df.to_csv(
p.joinpath(filename.replace(" ", "_") + ".csv"),
index=False,
)
def string_multiple_lines(source, width, max_rows):
"""
Adjust string with multiple lines by inserting '\n'
"""
idx = 0
lines = []
while idx < len(source) and len(lines) < max_rows:
lines.append(source[idx : idx + width])
idx += width
if idx < len(source):
last = lines[-1]
lines[-1] = last[0:-3] + "..."
return "\n".join(lines)
def convert_metric_id_to_panel_info(metric_id):
"""
Convert metric id into panel information.
Output is a tuples of the form (file_id, panel_id, metric_id).
For example:
Input: "2"
Output: ("0200", None, None)
Input: "11"
Output: ("1100", None, None)
Input: "11.1"
Output: ("1100", 1101, None)
Input: "11.1.1"
Output: ("1100", 1101, 1)
Raises exception for invalid metric id.
"""
tokens = metric_id.split(".")
if 0 < len(tokens) < 4:
# File id
file_id = str(int(tokens[0]))
# 4 -> 04
if len(file_id) < 2:
file_id = f"0{file_id}"
# Multiply integer by 100
file_id = f"{file_id}00"
# Panel id
if len(tokens) > 1:
panel_id = int(tokens[0]) * 100
panel_id += int(tokens[1])
else:
panel_id = None
# Metric id
if len(tokens) > 2:
metric_id = int(tokens[2])
else:
metric_id = None
return (file_id, panel_id, metric_id)
else:
raise Exception(f"Invalid metric id: {metric_id}")
def build_comparable_columns(time_unit):
"""
Build comparable columns/headers for display
"""
comparable_columns = supported_field
top_stat_base = ["Count", "Sum", "Mean", "Median", "Standard Deviation"]
for h in top_stat_base:
comparable_columns.append(h + "(" + time_unit + ")")
return comparable_columns
return df_copy
+203
Dosyayı Görüntüle
@@ -0,0 +1,203 @@
"""
Panel Widget Modules
-------------------
Contains the panel widgets used in the main layout.
"""
from typing import Optional
from textual import on
from textual.containers import Container, VerticalScroll
from textual.widgets import Label, RadioButton, RadioSet
from config import rocprof_compute_home
from rocprof_compute_tui.widgets.collapsibles import build_all_sections
class KernelView(Container):
"""Center panel with analysis results split into two scrollable sections."""
DEFAULT_CSS = """
KernelView {
layout: vertical;
}
#top-container {
height: 1fr;
border: none;
margin-top: 1;
}
#bottom-container {
height: 4fr;
border: none;
margin-top: 2;
}
.kernel-table-header {
background: $primary;
color: $text;
text-style: bold;
padding: 0 1;
offset: 5 0;
margin-top: 1;
}
.kernel-row {
padding: 0 1;
border-bottom: solid $border;
}
RadioSet {
border: solid $border;
}
"""
def __init__(self, config_path: Optional[str] = None):
super().__init__(id="kernel-view")
self.status_label = None
self.dfs = {}
self.top_kernel = []
if rocprof_compute_home:
config_path = (
rocprof_compute_home
/ "rocprof_compute_tui"
/ "utils"
/ "kernel_view_config.yaml"
)
self.config_path = config_path
self.keys = None
self.current_selection = None
def compose(self):
"""
Compose the split panel layout with two scrollable containers.
"""
with VerticalScroll(id="top-container"):
yield Label(
"Open a workload directory to run analysis and view individual kernel analysis results.",
classes="placeholder",
)
with VerticalScroll(id="bottom-container"):
# empty on init
pass
def update_results(self, per_kernel_dfs, top_kernels) -> None:
self.dfs = per_kernel_dfs
self.top_kernel = top_kernels
top_container = self.query_one("#top-container", VerticalScroll)
top_container.remove_children()
if self.top_kernel:
try:
header = self.build_header()
top_container.mount(header)
selector = self.build_selector()
top_container.mount(selector)
except Exception as e:
top_container.mount(
Label(f"Error displaying kernel list: {str(e)}", classes="error")
)
else:
top_container.mount(Label("No kernels available", classes="placeholder"))
self.current_selection = self.top_kernel[0]["Kernel_Name"]
self._update_bottom_content()
def update_view(self, message: str, log_level: str) -> None:
"""
Update the view with a status message.
"""
if self.status_label is None:
self.status_label = Label(f"{message}", classes=log_level)
self.mount(self.status_label)
else:
self.status_label.update(f"{message}")
self.status_label.set_classes(log_level)
def reload_config(self, config_path: str = None) -> None:
if config_path:
self.config_path = config_path
if self.dfs and self.top_kernel:
self.update_results()
def build_header(self):
all_keys = set()
for kernel in self.top_kernel:
all_keys.update(kernel.keys())
self.keys = sorted(all_keys)
if "Kernel_Name" in self.keys:
self.keys.remove("Kernel_Name")
self.keys.insert(0, "Kernel_Name")
header_text = " | ".join(f"{key:25}" for key in self.keys)
header_label = Label(header_text, classes="kernel-table-header")
return header_label
def build_selector(self):
radio_buttons = []
for i, kernel in enumerate(self.top_kernel):
row_data = []
for key in self.keys:
value = str(kernel.get(key, "N/A"))
if len(value) > 18:
value = value[:15] + "..."
row_data.append(f"{value:25}")
row_text = " | ".join(row_data)
radio_button = RadioButton(row_text, id=f"kernel-{i}")
radio_button.kernel_data = kernel
radio_buttons.append(radio_button)
selector = RadioSet(*radio_buttons)
return selector
@on(RadioSet.Changed)
def on_radio_changed(self, event: RadioSet.Changed) -> None:
if event.pressed:
kernel_data = getattr(event.pressed, "kernel_data", None)
if kernel_data and "Kernel_Name" in kernel_data:
selected_kernel = kernel_data["Kernel_Name"]
self.current_selection = selected_kernel
self._update_bottom_content()
def _update_bottom_content(self):
bottom_container = self.query_one("#bottom-container", VerticalScroll)
bottom_container.remove_children()
bottom_container.mount(
Label(f"Toggle kernel selection to view detailed analysis.")
)
if self.current_selection and self.current_selection in self.dfs:
bottom_container.mount(
Label(f"Current kernel selection: {self.current_selection}")
)
filtered_dfs = self.dfs[self.current_selection]
try:
sections = build_all_sections(filtered_dfs, self.config_path)
for section in sections:
bottom_container.mount(section)
except Exception as e:
bottom_container.mount(
Label(f"Error displaying results: {str(e)}", classes="error")
)
else:
bottom_container.mount(
Label(
f"No data available for kernel: {self.current_selection}",
classes="error",
)
)
+32 -31
Dosyayı Görüntüle
@@ -50,10 +50,10 @@ class MainView(Horizontal):
"""Main view layout for the application."""
selected_path = reactive(None)
dfs = reactive({})
per_kernel_dfs = reactive({})
top_kernels = reactive([])
def __init__(self):
"""Initialize the main view."""
super().__init__(id="main-container")
self.start_path = (
# NOTE: is cwd the best choice?
@@ -70,7 +70,6 @@ class MainView(Horizontal):
pass
def compose(self) -> ComposeResult:
"""Compose the main view layout."""
self.logger.info("Composing main view layout", update_ui=False)
yield MenuBar()
@@ -80,7 +79,6 @@ class MainView(Horizontal):
# Center Panel - Analysis results display
center_panel = CenterPanel()
yield center_panel
self.center = center_panel
# Bottom Panel - Output, terminal, and metric description
@@ -91,7 +89,6 @@ class MainView(Horizontal):
self.metric_description = tabs.description_area
self.output = tabs.output_area
# Now set the output area for the logger
self.logger.set_output_area(self.output)
self.logger.info("Main view layout composed")
@@ -107,8 +104,9 @@ class MainView(Horizontal):
try:
row_data = table.get_row_at(row_idx)
content = f"Selected Row {row_idx}:\n"
content += "\n".join(f"{val}" for val in row_data)
content = f"Selected Metric ID: {row_data[0]}\n"
content += f"Selected Metric: {row_data[1]}\n"
# content += f"Metric Description:\n\t{row_data[-1]}"
self.metric_description.text = content
self.logger.info(f"Row {row_idx} data displayed in metric_description")
@@ -122,7 +120,8 @@ class MainView(Horizontal):
@work(thread=True)
def run_analysis(self) -> None:
self.dfs = {}
self.per_kernel_dfs = {}
self.top_kernels = []
if not self.selected_path:
error_msg = "No directory selected for analysis"
@@ -173,7 +172,6 @@ class MainView(Horizontal):
self.logger.info(
f"Step 3: sys_info_df shape = {sys_info_df.shape if hasattr(sys_info_df, 'shape') else 'No shape attribute'}"
)
self.logger.info(f"Step 3: sys_info_df = {sys_info_df}")
except Exception as e:
self.logger.error(f"Step 3 failed - Error loading sys_info: {str(e)}")
@@ -196,7 +194,6 @@ class MainView(Horizontal):
raise TypeError(f"Unexpected type for sys_info: {type(sys_info_df)}")
self.logger.info(f"Step 4: sys_info converted = {sys_info}")
self.logger.info(f"Step 4: sys_info type = {type(sys_info)}")
except Exception as e:
self.logger.error(f"Step 4 failed - Error converting sys_info: {str(e)}")
@@ -231,18 +228,19 @@ class MainView(Horizontal):
# Step 8: Run analysis
try:
self.logger.info("Step 8: Running analysis")
self.dfs = analyzer.run_analysis()
if not self.dfs:
warning_msg = "Step 8: Analysis completed but no data was returned"
self.per_kernel_dfs = analyzer.run_kernel_analysis()
self.top_kernels = analyzer.run_top_kernel()
# TODO: add per kernel Roofline support when available
if not self.per_kernel_dfs or not self.top_kernels:
warning_msg = "Step 8: Per Kernel Analysis completed but not all data was returned"
self._update_view(warning_msg, LogLevel.WARNING)
self.logger.warning(warning_msg)
else:
self.app.call_from_thread(self.refresh_results)
self.logger.info("Step 8: Analysis completed successfully")
if self.dfs.get("4. Roofline"):
self.logger.info("Step 8: Roofline data available")
else:
self.logger.info("Step 8: Roofline data not available")
self.logger.info("Step 8: Kernel Analysis completed successfully")
# self.logger.info(f"{self.per_kernel_dfs}")
except Exception as e:
self.logger.error(f"Step 8 failed - Error running analysis: {str(e)}")
raise
@@ -257,17 +255,15 @@ class MainView(Horizontal):
def _update_view(self, message: str, log_level: LogLevel) -> None:
try:
# Use call_from_thread to safely update UI from background thread
self.app.call_from_thread(self._safe_update_view, message, log_level)
except Exception as e:
# Capture errors that might occur when scheduling the UI update
self.logger.error(f"View update scheduling error: {str(e)}")
def _safe_update_view(self, message: str, log_level: LogLevel) -> None:
try:
analyze_view = self.query_one("#analyze-view")
if analyze_view:
analyze_view.update_view(message, log_level)
kernel_view = self.query_one("#kernel-view")
if kernel_view:
kernel_view.update_view(message, log_level)
else:
self.logger.warning("Analysis view not found when updating log")
except Exception as e:
@@ -275,24 +271,29 @@ class MainView(Horizontal):
def refresh_results(self) -> None:
try:
self.logger.info("Refreshing analysis results")
analyze_view = self.query_one("#analyze-view")
if not analyze_view:
self.logger.error("Analysis view not found")
self.logger.info("Refreshing kernel results")
kernel_view = self.query_one("#kernel-view")
if not kernel_view:
self.logger.error("Kernel view not found")
return
if not hasattr(self, "dfs") or self.dfs is None:
self.logger.error("No analysis data available to display")
if (
not hasattr(self, "per_kernel_dfs")
or self.per_kernel_dfs is None
or not hasattr(self, "top_kernels")
or self.top_kernels is None
):
self.logger.error("No kernel analysis data available to display")
return
analyze_view.update_results(self.dfs)
kernel_view.update_results(self.per_kernel_dfs, self.top_kernels)
self.logger.success(f"Results displayed successfully.")
except Exception as e:
self.logger.error(f"Error refreshing results: {str(e)}")
def refresh_view(self) -> None:
self.logger.info("Refreshing view...")
if self.dfs:
if self.top_kernels:
self.refresh_results()
else:
self.logger.warning("No data available for refresh")
-74
Dosyayı Görüntüle
@@ -1,74 +0,0 @@
"""
Panel Widget Modules
-------------------
Contains the panel widgets used in the main layout.
"""
from importlib import resources
from typing import Any, Dict, Optional
from textual.containers import ScrollableContainer
from textual.widgets import Label
from rocprof_compute_tui.widgets.collapsibles import build_all_sections
class AnalyzeView(ScrollableContainer):
"""Center panel with analysis results."""
def __init__(self, config_path: Optional[str] = None):
super().__init__(id="analyze-view")
self.dfs = {}
if config_path is None:
config_path = (
resources.files("rocprof_compute_tui.utils") / "analyze_config.yaml"
)
self.config_path = str(config_path)
def compose(self):
"""
Compose the initial center panel state.
"""
yield Label(
"Open a workload directory to run analysis and view results",
classes="placeholder",
)
def update_results(self, dfs: Dict[str, Any]) -> None:
"""
Update the center panel with analysis results.
"""
self.dfs = dfs
self.remove_children()
try:
sections = build_all_sections(self.dfs, self.config_path)
# Mount all sections
for section in sections:
self.mount(section)
except Exception as e:
self.mount(Label(f"Error displaying results: {str(e)}", classes="error"))
def update_view(self, message: str, log_level: str) -> None:
"""
Update the view with a status message.
"""
self.remove_children()
try:
self.mount(Label(f"{message}", classes=log_level))
except Exception as e:
self.mount(Label(f"Error displaying results: {str(e)}", classes="error"))
def reload_config(self, config_path: str = None) -> None:
"""
Reload the configuration and update the view.
"""
if config_path:
self.config_path = config_path
if self.dfs:
self.update_results(self.dfs)
+6 -9
Dosyayı Görüntüle
@@ -29,9 +29,9 @@ Contains the panel widgets used in the main layout.
"""
from textual.containers import Vertical
from textual.widgets import Label, TabPane
from textual.widgets import TabPane
from rocprof_compute_tui.widgets.center_panel.analyze_view import AnalyzeView
from rocprof_compute_tui.views.kernel_view import KernelView
from rocprof_compute_tui.widgets.tabbed_content import TabsTabbedContent
@@ -48,15 +48,12 @@ class CenterPanel(Vertical):
super().__init__()
self.default_tab = "center-analyze"
self.analyze_view = AnalyzeView()
self.kernel_view = KernelView()
def compose(self):
with TabsTabbedContent(initial="tab-analyze"):
with TabPane("Basic View", id="tab-analyze"):
yield self.analyze_view
# TODO:
# with TabPane("placeholder (🚧)", id="tab-1"):
# yield Label("🚧 Under Construction")
with TabsTabbedContent(initial="tab-kernel"):
with TabPane("Basic View", id="tab-kernel"):
yield self.kernel_view
def on_mount(self) -> None:
self.add_class("section")
+32 -48
Dosyayı Görüntüle
@@ -68,7 +68,10 @@ def simple_bar(df, title=None):
w *= 100
plt.simple_bar(list(metric_dict.keys()), list(metric_dict.values()), width=w)
# plt.show()
return "\n" + plt.build() + "\n"
plot_content = plt.build()
if not plot_content or plot_content.strip() == "":
return None
return "\n" + plot_content + "\n"
def simple_multiple_bar(df, title=None):
@@ -100,10 +103,13 @@ def simple_multiple_bar(df, title=None):
h *= 300
plt.plot_size(height=h)
plt.multiple_bar(labels, data, color=["blue", "blue+", 68, 63])
plt.multiple_bar(labels, data)
# plt.show()
return "\n" + plt.build() + "\n"
plot_content = plt.build()
if not plot_content or plot_content.strip() == "":
return None
return "\n" + plot_content + "\n"
def simple_box(df, orientation="v", title=None):
@@ -173,7 +179,10 @@ def simple_box(df, orientation="v", title=None):
plt.theme("pro")
# plt.show()
return "\n" + plt.build() + "\n"
plot_content = plt.build()
if not plot_content or plot_content.strip() == "":
return None
return "\n" + plot_content + "\n"
def px_simple_bar(df, title: str = None, id=None, style: dict = None, orientation="h"):
@@ -284,18 +293,8 @@ class RooflinePlot(Static):
super().__init__("", classes="roofline", **kwargs)
self.df = df
# Disable markup rendering
self._render_markup = False
try:
plot_str = ""
try:
result = self.df["4. Roofline"]
if result:
plot_str = str(result)
except:
plot_str = "No roofline data generated"
plot_str = str(self.df.get("4. Roofline", "No roofline data generated"))
self.update(plot_str)
except Exception as e:
error_message = f"Roofline plot error: {str(e)}\n{traceback.format_exc()}"
@@ -319,41 +318,37 @@ class MemoryChart(Static):
"""
def __init__(self, df: pd.DataFrame, **kwargs):
"""Initialize the memory chart."""
super().__init__("", classes="mem-chart", **kwargs)
self.df = df
# Generate the chart content on initialization
try:
# Prepare data
metric_dict = (
self.df[["Metric", "Value"]].set_index("Metric").to_dict()["Value"]
)
if self.df is None or self.df.empty:
self.update("No chart data generated")
return
if not {"Metric", "Value"}.issubset(self.df.columns):
self.update("Error: Missing required columns")
return
metric_dict = dict(zip(self.df["Metric"], self.df["Value"]))
# Capture stdout
original_stdout = sys.stdout
string_buffer = StringIO()
sys.stdout = string_buffer
try:
# Generate the chart
result = plot_mem_chart("", "per_kernel", metric_dict)
stdout_output = string_buffer.getvalue()
if stdout_output:
plot_str = stdout_output
elif result:
plot_str = str(result)
else:
plot_str = "No chart data generated"
with StringIO() as string_buffer:
sys.stdout = string_buffer
result = plot_mem_chart("", "per_kernel", metric_dict)
stdout_output = string_buffer.getvalue()
finally:
sys.stdout = original_stdout
plot_str = next(
(x for x in [stdout_output, str(result) if result else None] if x),
"No chart data generated",
)
self.update(plot_str)
except Exception as e:
error_message = f"Memory chart error: {str(e)}\n{traceback.format_exc()}"
self.update(f"Error: {str(error_message)}")
self.update(f"Memory chart error: {str(e)}")
class SimpleBar(Static):
@@ -372,7 +367,6 @@ class SimpleBar(Static):
"""
def __init__(self, df: pd.DataFrame, **kwargs):
"""Initialize the simple bar."""
super().__init__("", classes="simple-bar", **kwargs)
self.df = df
@@ -381,13 +375,8 @@ class SimpleBar(Static):
if result:
plot_str = str(result)
# Escape markup characters
escaped_content = plot_str.replace("[", r"\[").replace("]", r"\]")
self.update(escaped_content)
# Alternative - wrap in [pre] tags for preformatted text
# self.update(f"[pre]{plot_str}[/pre]")
else:
self.update("No simple bar data generated")
@@ -398,7 +387,6 @@ class SimpleBar(Static):
class SimpleBox(Static):
"""Simple Box visualization widget."""
DEFAULT_CSS = """
SimpleBox {
@@ -413,7 +401,6 @@ class SimpleBox(Static):
"""
def __init__(self, df: pd.DataFrame, **kwargs):
"""Initialize the simple box."""
super().__init__("", classes="simple-box", **kwargs)
self.df = df
@@ -422,7 +409,6 @@ class SimpleBox(Static):
if result:
plot_str = str(result)
# Escape markup characters
escaped_content = plot_str.replace("[", r"\[").replace("]", r"\]")
self.update(escaped_content)
else:
@@ -450,7 +436,6 @@ class SimpleMultiBar(Static):
"""
def __init__(self, df: pd.DataFrame, **kwargs):
"""Initialize the simple multiple bar."""
super().__init__("", classes="simple-multi-bar", **kwargs)
self.df = df
@@ -459,7 +444,6 @@ class SimpleMultiBar(Static):
if result:
plot_str = str(result)
# Escape markup characters
escaped_content = plot_str.replace("[", r"\[").replace("]", r"\]")
self.update(escaped_content)
else:
+8 -10
Dosyayı Görüntüle
@@ -26,7 +26,6 @@ from typing import Any, Dict, List, Optional
import pandas as pd
import yaml
from textual.containers import VerticalScroll
from textual.widgets import Collapsible, DataTable, Label
from rocprof_compute_tui.widgets.charts import (
@@ -41,12 +40,9 @@ from rocprof_compute_tui.widgets.charts import (
def create_table(df: pd.DataFrame) -> DataTable:
table = DataTable(zebra_stripes=True)
# Clean the DataFrame - remove NaN and empty cells
df = df.reset_index()
df = df.dropna(how="any")
df = df[~df.apply(lambda row: row.astype(str).str.strip().eq("").any(), axis=1)]
# Add columns and rows
str_columns = [str(col) for col in df.columns]
table.add_columns(*str_columns)
table.add_rows([tuple(str(x) for x in row) for row in df.itertuples(index=False)])
@@ -59,7 +55,9 @@ def load_config(config_path) -> Dict[str, Any]:
with open(config_path, "r") as file:
return yaml.safe_load(file)
except FileNotFoundError:
raise FileNotFoundError(f"Configuration file {config_path} not found")
raise FileNotFoundError(
f"Configuration file {config_path} not found, \nplease populate the analysis_config.yaml file."
)
except yaml.YAMLError as e:
raise ValueError(f"Error parsing YAML configuration: {e}")
@@ -167,7 +165,7 @@ def build_subsection(
return collapsible
def build_dynamic_kernel_sections(
def build_kernel_sections(
dfs: Dict[str, Any], skip_sections: List[str]
) -> List[Collapsible]:
children = []
@@ -198,9 +196,10 @@ def build_dynamic_kernel_sections(
return None
try:
df = data["df"]
if data["df"] is None or data["df"].empty:
return None
tui_style = data.get("tui_style")
widget = create_widget_from_data(df, tui_style)
widget = create_widget_from_data(data["df"], tui_style)
if widget is None:
add_warning(f"Widget creation returned None for '{subsection_name}'")
@@ -277,7 +276,7 @@ def build_section_from_config(
# Handle dynamic sections (like kernel sections)
elif section_config.get("dynamic_sections", False):
skip_sections = section_config.get("skip_sections", [])
children = build_dynamic_kernel_sections(dfs, skip_sections)
children = build_kernel_sections(dfs, skip_sections)
# Handle regular sections with subsections
elif "subsections" in section_config:
@@ -290,7 +289,6 @@ def build_section_from_config(
except Exception as e:
error_msg = f"{subsection_config.get('title', 'Unknown')} error: {str(e)}"
children.append(Label(error_msg, classes="warning"))
else:
children = [Label("No configuration provided for this section")]
-39
Dosyayı Görüntüle
@@ -1,39 +0,0 @@
##############################################################################bl
# MIT License
#
# Copyright (c) 2025 Advanced Micro Devices, Inc. All Rights Reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
##############################################################################el
"""
Specialized Widget Modules
-------------------------
Contains custom widget implementations for the application.
"""
from textual.widgets import DirectoryTree
class FolderOnlyDirectory(DirectoryTree):
"""Directory tree that only shows folders."""
def filter_paths(self, paths):
"""Filter to only show directories."""
return [path for path in paths if path.is_dir()]
+57 -57
Dosyayı Görüntüle
@@ -106,8 +106,8 @@ def format_text(
)
key_str = (
"{key:{key_format}}".format(key=key, key_format=key_format)
if key is not None
else None
if key and isinstance(key, (int, float))
else str(key) if key else None
)
unit_string = post_description_with_space if not "N/A" in value_str else ""
@@ -1013,8 +1013,8 @@ class MemChart:
block_instr_buff.y_max = self.y_max - 5.0
block_instr_buff.y_min = block_instr_buff.y_max - 24.0
block_instr_buff.wave_occupancy = metric_dict["Wavefront Occupancy"]
block_instr_buff.wave_life = metric_dict["Wave Life"]
block_instr_buff.wave_occupancy = metric_dict.get("Wavefront Occupancy", "n/a")
block_instr_buff.wave_life = metric_dict.get("Wave Life", "n/a")
block_instr_buff.draw(canvas)
@@ -1037,14 +1037,14 @@ class MemChart:
block_instr_disp.y_max = block_instr_buff.y_max
block_instr_disp.y_min = block_instr_buff.y_min
block_instr_disp.instrs["SALU"] = metric_dict["SALU"]
block_instr_disp.instrs["SMEM"] = metric_dict["SMEM"]
block_instr_disp.instrs["VALU"] = metric_dict["VALU"]
block_instr_disp.instrs["MFMA"] = metric_dict["MFMA"]
block_instr_disp.instrs["VMEM"] = metric_dict["VMEM"]
block_instr_disp.instrs["LDS"] = metric_dict["LDS"]
block_instr_disp.instrs["GWS"] = metric_dict["GWS"]
block_instr_disp.instrs["BRANCH"] = metric_dict["BR"]
block_instr_disp.instrs["SALU"] = metric_dict.get("SALU", "n/a")
block_instr_disp.instrs["SMEM"] = metric_dict.get("SMEM", "n/a")
block_instr_disp.instrs["VALU"] = metric_dict.get("VALU", "n/a")
block_instr_disp.instrs["MFMA"] = metric_dict.get("MFMA", "n/a")
block_instr_disp.instrs["VMEM"] = metric_dict.get("VMEM", "n/a")
block_instr_disp.instrs["LDS"] = metric_dict.get("LDS", "n/a")
block_instr_disp.instrs["GWS"] = metric_dict.get("GWS", "n/a")
block_instr_disp.instrs["BRANCH"] = metric_dict.get("BR", "n/a")
block_instr_disp.draw(canvas)
@@ -1056,14 +1056,14 @@ class MemChart:
block_exec.y_min = block_instr_disp.y_min - 6
block_exec.y_max = block_instr_disp.y_max
block_exec.active_cus = metric_dict["Active CUs"]
block_exec.num_cus = metric_dict["Num CUs"]
block_exec.vgprs = metric_dict["VGPR"]
block_exec.sgprs = metric_dict["SGPR"]
block_exec.lds_alloc = metric_dict["LDS Allocation"]
block_exec.scratch_alloc = metric_dict["Scratch Allocation"]
block_exec.wavefronts = metric_dict["Wavefronts"]
block_exec.workgroups = metric_dict["Workgroups"]
block_exec.active_cus = metric_dict.get("Active CUs", "n/a")
block_exec.num_cus = metric_dict.get("Num CUs", "n/a")
block_exec.vgprs = metric_dict.get("VGPR", "n/a")
block_exec.sgprs = metric_dict.get("SGPR", "n/a")
block_exec.lds_alloc = metric_dict.get("LDS Allocation", "n/a")
block_exec.scratch_alloc = metric_dict.get("Scratch Allocation", "n/a")
block_exec.wavefronts = metric_dict.get("Wavefronts", "n/a")
block_exec.workgroups = metric_dict.get("Workgroups", "n/a")
block_exec.draw(canvas)
@@ -1075,11 +1075,11 @@ class MemChart:
wires_E_GLV.y_min = block_instr_disp.y_min
wires_E_GLV.y_max = block_instr_disp.y_max
wires_E_GLV.lds_req = metric_dict["LDS Req"]
wires_E_GLV.vl1_rd = metric_dict["VL1 Rd"]
wires_E_GLV.vl1_wr = metric_dict["VL1 Wr"]
wires_E_GLV.vl1_atomic = metric_dict["VL1 Atomic"]
wires_E_GLV.sl1_rd = metric_dict["sL1D Rd"]
wires_E_GLV.lds_req = metric_dict.get("LDS Req", "n/a")
wires_E_GLV.vl1_rd = metric_dict.get("VL1 Rd", "n/a")
wires_E_GLV.vl1_wr = metric_dict.get("VL1 Wr", "n/a")
wires_E_GLV.vl1_atomic = metric_dict.get("VL1 Atomic", "n/a")
wires_E_GLV.sl1_rd = metric_dict.get("VL1D Rd", "n/a")
wires_E_GLV.draw(canvas)
@@ -1093,7 +1093,7 @@ class MemChart:
y_max=block_instr_buff.y_min,
)
wire_InstrBuff_IL1Cache.il1_fetch = metric_dict["IL1 Fetch"]
wire_InstrBuff_IL1Cache.il1_fetch = metric_dict.get("IL1 Fetch", "n/a")
wire_InstrBuff_IL1Cache.draw(canvas)
@@ -1118,8 +1118,8 @@ class MemChart:
block_lds.y_max = wires_E_GLV.y_max
block_lds.y_min = block_lds.y_max - 5
block_lds.util = metric_dict["LDS Util"]
block_lds.latency = metric_dict["LDS Latency"]
block_lds.util = metric_dict.get("LDS Util", "n/a")
block_lds.latency = metric_dict.get("LDS Latency", "n/a")
block_lds.draw(canvas)
@@ -1131,10 +1131,10 @@ class MemChart:
block_vector_L1.y_max = block_lds.y_min - 3
block_vector_L1.y_min = block_vector_L1.y_max - 9
block_vector_L1.hit = metric_dict["VL1 Hit"]
block_vector_L1.latency = metric_dict["VL1 Lat"]
block_vector_L1.coales = metric_dict["VL1 Coalesce"]
block_vector_L1.stall = metric_dict["VL1 Stall"]
block_vector_L1.hit = metric_dict.get("VL1 Hit", "n/a")
block_vector_L1.latency = metric_dict.get("VL1 Lat", "n/a")
block_vector_L1.coales = metric_dict.get("VL1 Coalesce", "n/a")
block_vector_L1.stall = metric_dict.get("VL1 Stall", "n/a")
block_vector_L1.draw(canvas)
@@ -1146,8 +1146,8 @@ class MemChart:
block_const_L1.y_max = block_vector_L1.y_min - 3
block_const_L1.y_min = block_const_L1.y_max - 5
block_const_L1.hit = metric_dict["sL1D Hit"]
block_const_L1.latency = metric_dict["sL1D Lat"]
block_const_L1.hit = metric_dict.get("sL1D Hit", "n/a")
block_const_L1.latency = metric_dict.get("sL1D Lat", "n/a")
block_const_L1.draw(canvas)
@@ -1159,8 +1159,8 @@ class MemChart:
block_instr_L1.y_max = block_const_L1.y_min - 3
block_instr_L1.y_min = block_instr_L1.y_max - 5
block_instr_L1.hit = metric_dict["IL1 Hit"]
block_instr_L1.latency = metric_dict["IL1 Lat"]
block_instr_L1.hit = metric_dict.get("IL1 Hit", "n/a")
block_instr_L1.latency = metric_dict.get("IL1 Lat", "n/a")
block_instr_L1.draw(canvas)
@@ -1171,13 +1171,13 @@ class MemChart:
wires_L1_L2.x_max = wires_L1_L2.x_min + 14
wires_L1_L2.y_min = block_instr_L1.y_min
wires_L1_L2.y_max = block_vector_L1.y_max
wires_L1_L2.vl1_l2_rd = metric_dict["VL1_L2 Rd"]
wires_L1_L2.vl1_l2_wr = metric_dict["VL1_L2 Wr"]
wires_L1_L2.vl1_l2_atomic = metric_dict["VL1_L2 Atomic"]
wires_L1_L2.sl1_l2_rd = metric_dict["sL1D_L2 Rd"]
wires_L1_L2.sl1_l2_wr = metric_dict["sL1D_L2 Wr"]
wires_L1_L2.sl1_l2_atomic = metric_dict["sL1D_L2 Atomic"]
wires_L1_L2.il1_l2_req = metric_dict["IL1_L2 Rd"]
wires_L1_L2.vl1_l2_rd = metric_dict.get("VL1_L2 Rd", "n/a")
wires_L1_L2.vl1_l2_wr = metric_dict.get("VL1_L2 Wr", "n/a")
wires_L1_L2.vl1_l2_atomic = metric_dict.get("VL1_L2 Atomic", "n/a")
wires_L1_L2.sl1_l2_rd = metric_dict.get("VL1D_L2 Rd", "n/a")
wires_L1_L2.sl1_l2_wr = metric_dict.get("VL1D_L2 Wr", "n/a")
wires_L1_L2.sl1_l2_atomic = metric_dict.get("VL1D_L2 Atomic", "n/a")
wires_L1_L2.il1_l2_req = metric_dict.get("IL1_L2 Rd", "n/a")
wires_L1_L2.draw(canvas)
@@ -1190,12 +1190,12 @@ class MemChart:
block_L2.y_min = block_instr_L1.y_min
block_L2.y_max = block_lds.y_max
block_L2.hit = metric_dict["L2 Hit"]
block_L2.rd = metric_dict["L2 Rd"]
block_L2.wr = metric_dict["L2 Wr"]
block_L2.atomic = metric_dict["L2 Atomic"]
block_L2.rd_lat = metric_dict["L2 Rd Lat"]
block_L2.wr_lat = metric_dict["L2 Wr Lat"]
block_L2.hit = metric_dict.get("L2 Hit", "n/a")
block_L2.rd = metric_dict.get("L2 Rd", "n/a")
block_L2.wr = metric_dict.get("L2 Wr", "n/a")
block_L2.atomic = metric_dict.get("L2 Atomic", "n/a")
block_L2.rd_lat = metric_dict.get("L2 Rd Lat", "n/a")
block_L2.wr_lat = metric_dict.get("L2 Wr Lat", "n/a")
block_L2.draw(canvas)
@@ -1209,9 +1209,9 @@ class MemChart:
y_max=block_L2.y_max - 10,
)
wires_L2_Fabric.rd = metric_dict["Fabric_L2 Rd"]
wires_L2_Fabric.wr = metric_dict["Fabric_L2 Wr"]
wires_L2_Fabric.atomic = metric_dict["Fabric_L2 Atomic"]
wires_L2_Fabric.rd = metric_dict.get("Fabric_L2 Rd", "n/a")
wires_L2_Fabric.wr = metric_dict.get("Fabric_L2 Wr", "n/a")
wires_L2_Fabric.atomic = metric_dict.get("Fabric_L2 Atomic", "n/a")
wires_L2_Fabric.draw(canvas)
@@ -1236,9 +1236,9 @@ class MemChart:
y_min=block_xgmi_pcie.y_min - 5 - 11,
)
block_fabric.lat["Rd"] = metric_dict["Fabric Rd Lat"]
block_fabric.lat["Wr"] = metric_dict["Fabric Wr Lat"]
block_fabric.lat["Atomic"] = metric_dict["Fabric Atomic Lat"]
block_fabric.lat["Rd"] = metric_dict.get("Fabric Rd Lat", "n/a")
block_fabric.lat["Wr"] = metric_dict.get("Fabric Wr Lat", "n/a")
block_fabric.lat["Atomic"] = metric_dict.get("Fabric Atomic Lat", "n/a")
block_fabric.draw(canvas)
@@ -1264,8 +1264,8 @@ class MemChart:
y_max=block_fabric.y_max - 4,
)
wires_Fabric_HBM.rd = metric_dict["HBM Rd"]
wires_Fabric_HBM.wr = metric_dict["HBM Wr"]
wires_Fabric_HBM.rd = metric_dict.get("HBM Rd", "n/a")
wires_Fabric_HBM.wr = metric_dict.get("HBM Wr", "n/a")
wires_Fabric_HBM.draw(canvas)
+22 -7
Dosyayı Görüntüle
@@ -137,14 +137,28 @@ def to_max(*args):
def to_avg(a):
if str(type(a)) == "<class 'NoneType'>":
return np.nan
elif np.isnan(a).all():
return np.nan
elif a.empty:
return np.nan
elif isinstance(a, pd.core.series.Series):
return a.mean()
if a.empty:
return np.nan
elif np.isnan(a).all():
return np.nan
else:
return a.mean()
elif isinstance(a, (np.ndarray, list)):
arr = np.array(a)
if arr.size == 0:
return np.nan
elif np.isnan(arr).all():
return np.nan
else:
return np.nanmean(arr)
elif isinstance(a, (int, float, np.number)):
if np.isnan(a):
return np.nan
else:
return float(a)
else:
raise Exception("to_avg: unsupported type.")
raise Exception(f"to_avg: unsupported type: {type(a)}")
def to_median(a):
@@ -313,6 +327,7 @@ def build_eval_string(equation, coll_level, config):
s = re.sub(r"\'\]\[(\d+)\]", r"[\g<1>]']", s)
# use .get() to catch any potential KeyErrors
s = re.sub(r"raw_pmc_df\['(.*?)']", r'raw_pmc_df.get("\1")', s)
# print("--- intermediate string: ", s)
# apply coll_level
if config.get("format_rocprof_output") == "rocpd":
# Replace SQ_ACCUM_PREV_HIRES with coll_level_ACCUM then ignore coll_level df
@@ -1448,7 +1463,7 @@ def load_kernel_top(workload, dir, args):
def load_table_data(workload, dir, is_gui, args, config, skipKernelTop=False):
"""
- Load data for all "raw_csv_table"
- Load dat for "pc_sampling_table"
- Load data for "pc_sampling_table"
- Calculate mertric value for all "metric_table"
"""
if not skipKernelTop:
+1 -1
Dosyayı Görüntüle
@@ -24,7 +24,7 @@
import os
import shutil
from unittest.mock import Mock, patch
from unittest.mock import Mock
import pandas as pd
import pytest
+1 -5
Dosyayı Görüntüle
@@ -24,11 +24,7 @@
import logging
import shutil
import sys
import tempfile
from pathlib import Path
from unittest.mock import MagicMock, Mock, call, patch
from unittest.mock import MagicMock, Mock, patch
import pandas as pd
import pytest