Files
TheRock/rocm-systems/projects/rocprofiler-compute/tests/test_tty.py
T
derek.holloway 7502bf64a4
CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

342 lines
11 KiB
Python

# Copyright (c) Advanced Micro Devices, Inc.
# SPDX-License-Identifier: MIT
"""Unit tests for src/utils/tty.py."""
import argparse
import pandas as pd
import pytest
from utils.tty import convert_time_columns, format_table_output, has_time_data
from utils.utils_common import is_gfx115x
TIME_UNITS = {"s": 10**9, "ms": 10**6, "us": 10**3, "ns": 1}
def make_args() -> argparse.Namespace:
"""Minimal args for the plain-table render path."""
return argparse.Namespace(decimal=2, view=None, normal_unit="per_wave")
def _sample_time_data() -> pd.DataFrame:
"""Metric table mixing a time row, a cycle row, and a count row."""
return pd.DataFrame({
"Metric_ID": ["7.2.0", "7.2.1", "7.2.2"],
"Metric": ["Kernel Time", "Kernel Time (Cycles)", "Non-Time Metric"],
"Avg": [3446.64, 64499.39, 1000.0],
"Min": [1769.25, 17269.25, 500.0],
"Max": [12532.12, 337030.50, 2000.0],
"Unit": ["ns", "Cycle", "Count"],
})
def _original_ns_values() -> dict[str, float]:
"""Original nanosecond values for the time row of _sample_time_data."""
return {"Avg": 3446.64, "Min": 1769.25, "Max": 12532.12}
def test_format_table_output_suppresses_empty_column() -> None:
"""A non-PC-sampling table with an all-'N/A' column is suppressed."""
df = pd.DataFrame({"Metric": ["a", "b"], "Value": ["N/A", "N/A"]})
content = format_table_output(
make_args(),
{"id": 1101, "title": "Some Table"},
df,
"metric_table",
runs={"only": object()},
)
assert content == ""
def test_format_table_output_keeps_pc_sampling_table_21_1() -> None:
"""PC sampling table 21.1 is shown even with an all-'N/A' source column."""
df = pd.DataFrame({
"source_line": ["N/A", "N/A"],
"instruction": ["v_mov", "v_add"],
"count": [3, 1],
})
content = format_table_output(
make_args(),
{"id": 2101, "title": "PC Sampling"},
df,
"pc_sampling_table",
runs={"only": object()},
)
assert content != ""
assert "v_mov" in content
def test_has_time_data_detection() -> None:
"""has_time_data is True only when a 'ns' Unit column is present."""
assert has_time_data(_sample_time_data())
no_time_data = pd.DataFrame({
"Metric": ["Non-Time Metric"],
"Avg": [1000.0],
"Unit": ["Count"],
})
assert not has_time_data(no_time_data)
no_unit_column = pd.DataFrame({"Metric": ["Some Metric"], "Avg": [1000.0]})
assert not has_time_data(no_unit_column)
def test_default_unit_is_nanoseconds() -> None:
"""The fixture's time row defaults to nanoseconds."""
sample_time_data = _sample_time_data()
time_rows = sample_time_data["Unit"].str.lower().str.contains("ns", na=False)
assert time_rows.any()
assert sample_time_data.loc[0, "Unit"] == "ns"
def test_conversion_to_seconds() -> None:
"""Converting to seconds divides the time row and leaves others untouched."""
original_ns_values = _original_ns_values()
converted_df = convert_time_columns(_sample_time_data(), "s")
assert converted_df.loc[0, "Unit"] == "s"
assert converted_df.loc[0, "Avg"] == pytest.approx(
original_ns_values["Avg"] / TIME_UNITS["s"], abs=1e-10
)
assert converted_df.loc[0, "Min"] == pytest.approx(
original_ns_values["Min"] / TIME_UNITS["s"], abs=1e-10
)
assert converted_df.loc[0, "Max"] == pytest.approx(
original_ns_values["Max"] / TIME_UNITS["s"], abs=1e-10
)
assert converted_df.loc[1, "Unit"] == "Cycle"
assert converted_df.loc[2, "Unit"] == "Count"
def test_conversion_to_milliseconds() -> None:
"""Converting to milliseconds divides the time row by 10^6."""
original_ns_values = _original_ns_values()
converted_df = convert_time_columns(_sample_time_data(), "ms")
assert converted_df.loc[0, "Unit"] == "ms"
assert converted_df.loc[0, "Avg"] == pytest.approx(
original_ns_values["Avg"] / TIME_UNITS["ms"], abs=1e-6
)
assert converted_df.loc[0, "Min"] == pytest.approx(
original_ns_values["Min"] / TIME_UNITS["ms"], abs=1e-6
)
assert converted_df.loc[0, "Max"] == pytest.approx(
original_ns_values["Max"] / TIME_UNITS["ms"], abs=1e-6
)
def test_conversion_to_microseconds() -> None:
"""Converting to microseconds divides the time row by 10^3."""
original_ns_values = _original_ns_values()
converted_df = convert_time_columns(_sample_time_data(), "us")
assert converted_df.loc[0, "Unit"] == "us"
assert converted_df.loc[0, "Avg"] == pytest.approx(
original_ns_values["Avg"] / TIME_UNITS["us"], abs=1e-3
)
assert converted_df.loc[0, "Min"] == pytest.approx(
original_ns_values["Min"] / TIME_UNITS["us"], abs=1e-3
)
assert converted_df.loc[0, "Max"] == pytest.approx(
original_ns_values["Max"] / TIME_UNITS["us"], abs=1e-3
)
def test_conversion_to_nanoseconds() -> None:
"""Converting to nanoseconds leaves the time row unchanged."""
original_ns_values = _original_ns_values()
converted_df = convert_time_columns(_sample_time_data(), "ns")
assert converted_df.loc[0, "Unit"] == "ns"
assert converted_df.loc[0, "Avg"] == pytest.approx(
original_ns_values["Avg"], abs=1e-10
)
assert converted_df.loc[0, "Min"] == pytest.approx(
original_ns_values["Min"], abs=1e-10
)
assert converted_df.loc[0, "Max"] == pytest.approx(
original_ns_values["Max"], abs=1e-10
)
def test_non_time_rows_unchanged() -> None:
"""Cycle and Count rows keep their unit and value after conversion."""
converted_df = convert_time_columns(_sample_time_data(), "ms")
assert converted_df.loc[1, "Unit"] == "Cycle"
assert converted_df.loc[2, "Unit"] == "Count"
assert converted_df.loc[1, "Avg"] == 64499.39
assert converted_df.loc[2, "Avg"] == 1000.0
def test_invalid_time_unit_is_noop() -> None:
"""An unrecognised target unit leaves the frame unchanged."""
sample_time_data = _sample_time_data()
original_df = sample_time_data.copy()
converted_df = convert_time_columns(sample_time_data, "invalid_unit")
pd.testing.assert_frame_equal(converted_df, original_df)
def test_missing_unit_column_is_noop() -> None:
"""A frame with no Unit column is returned unchanged."""
df_no_unit = pd.DataFrame({"Metric": ["Test Metric"], "Avg": [1000.0]})
converted_df = convert_time_columns(df_no_unit, "ms")
pd.testing.assert_frame_equal(converted_df, df_no_unit)
def test_conversion_with_missing_columns() -> None:
"""Conversion works when Min/Max columns are absent."""
original_ns_values = _original_ns_values()
df_partial = _sample_time_data()[["Metric_ID", "Metric", "Avg", "Unit"]].copy()
converted_df = convert_time_columns(df_partial, "ms")
assert converted_df.loc[0, "Unit"] == "ms"
assert converted_df.loc[0, "Avg"] == pytest.approx(
original_ns_values["Avg"] / TIME_UNITS["ms"], abs=1e-6
)
def test_mathematical_correctness_all_units() -> None:
"""Every supported unit divides the time row by the correct factor."""
original_ns_values = _original_ns_values()
for target_unit, divisor in TIME_UNITS.items():
converted_df = convert_time_columns(_sample_time_data(), target_unit)
assert converted_df.loc[0, "Avg"] == pytest.approx(
original_ns_values["Avg"] / divisor, abs=1e-10
)
assert converted_df.loc[0, "Min"] == pytest.approx(
original_ns_values["Min"] / divisor, abs=1e-10
)
assert converted_df.loc[0, "Max"] == pytest.approx(
original_ns_values["Max"] / divisor, abs=1e-10
)
assert converted_df.loc[0, "Unit"] == target_unit
def test_integration_conversion_flow() -> None:
"""has_time_data gates convert_time_columns in the show_all flow."""
args = argparse.Namespace(time_unit="ms", decimal=2)
sample_df = pd.DataFrame({
"Metric_ID": ["7.2.0"],
"Metric": ["Kernel Time"],
"Avg": [3446640.0],
"Min": [1769250.0],
"Max": [12532120.0],
"Unit": ["ns"],
})
if has_time_data(sample_df):
converted_df = convert_time_columns(sample_df, args.time_unit)
else:
converted_df = sample_df
assert converted_df.loc[0, "Unit"] == "ms"
assert converted_df.loc[0, "Avg"] == pytest.approx(3.44664, abs=1e-5)
assert converted_df.loc[0, "Min"] == pytest.approx(1.76925, abs=1e-5)
assert converted_df.loc[0, "Max"] == pytest.approx(12.53212, abs=1e-5)
def test_show_all_with_time_unit_conversion() -> None:
"""Mixed-case 'Ns' unit converts correctly across every target unit."""
test_data = pd.DataFrame({
"Metric_ID": ["7.2.0"],
"Metric": ["Kernel Time"],
"Avg": [3446.64],
"Min": [1769.25],
"Max": [12532.12],
"Unit": ["Ns"],
})
for time_unit in ["s", "ms", "us", "ns"]:
converted_df = convert_time_columns(test_data, time_unit)
assert converted_df.loc[0, "Unit"] == time_unit
assert converted_df.loc[0, "Avg"] == pytest.approx(
3446.64 / TIME_UNITS[time_unit], abs=1e-10
)
def test_edge_cases_and_error_handling() -> None:
"""Empty, NaN, and mixed-case unit frames convert without error."""
empty_df = pd.DataFrame()
assert convert_time_columns(empty_df, "ms").empty
nan_df = pd.DataFrame({
"Avg": [float("nan"), 1000.0],
"Unit": ["ns", "Count"],
})
result = convert_time_columns(nan_df, "ms")
assert result.loc[0, "Unit"] == "ms"
mixed_case_df = pd.DataFrame({
"Avg": [1000.0, 2000.0],
"Unit": ["ns", "NS"],
})
result = convert_time_columns(mixed_case_df, "ms")
assert result.loc[0, "Unit"] == "ms"
assert result.loc[1, "Unit"] == "ms"
@pytest.mark.parametrize(
"gpu_arch",
[
pytest.param("gfx1151", id="rdna35"),
pytest.param("gfx942", id="cdna"),
],
)
def test_format_table_output_dispatches_memory_chart_renderer(
monkeypatch: pytest.MonkeyPatch,
gpu_arch: str,
) -> None:
"""Memory Chart output uses the architecture renderer and shared heading."""
calls: dict[str, dict] = {}
def record(name: str, return_value: str):
def stub(mem_data: dict, *, chart_title: str) -> str:
calls[name] = {
"mem_data": mem_data,
"chart_title": chart_title,
}
return return_value
return stub
monkeypatch.setattr(
"utils.tty.mem_chart_gfx11.plot_mem_chart",
record("gfx11", "rendered RDNA3.5 memory chart"),
)
monkeypatch.setattr(
"utils.tty.mem_chart_gfx9.plot_mem_chart",
record("gfx9", "rendered CDNA memory chart"),
)
df = pd.DataFrame({"Metric": ["Metric A"], "Value": [1]})
content = format_table_output(
make_args(),
{
"id": 701,
"title": "Memory Chart",
"cli_style": "mem_chart",
},
df,
"metric_table",
runs={"only": object()},
gpu_arch=gpu_arch,
)
expected = "gfx11" if is_gfx115x(gpu_arch) else "gfx9"
unexpected = "gfx9" if is_gfx115x(gpu_arch) else "gfx11"
assert calls[expected] == {
"mem_data": {"Metric A": 1},
"chart_title": "7. Memory Chart (Normalization: per_wave)",
}
assert unexpected not in calls
return_value = (
"rendered RDNA3.5 memory chart"
if is_gfx115x(gpu_arch)
else "rendered CDNA memory chart"
)
assert content == f"{return_value}\n"