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
1136 lines
36 KiB
Python
1136 lines
36 KiB
Python
# Copyright (c) Advanced Micro Devices, Inc.
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
"""
|
|
Tests rocprof-sys binaries
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
import json
|
|
import re
|
|
import pytest
|
|
import os
|
|
import shutil
|
|
from conftest import RocprofsysTest
|
|
|
|
pytestmark = [pytest.mark.rocprof_binary]
|
|
|
|
|
|
def get_sleep_cmd() -> str:
|
|
"""Return the path to sleep, or skip the test if it isn't on PATH."""
|
|
sleep_cmd = shutil.which("sleep")
|
|
if not sleep_cmd:
|
|
pytest.skip("sleep command not found")
|
|
return sleep_cmd
|
|
|
|
|
|
# ============================================================================
|
|
# Avail format consistency data
|
|
# ============================================================================
|
|
|
|
# Env vars intentionally excluded from the JSON preset schema.
|
|
# These are internal/session-specific settings documented in
|
|
# json_config.cpp::env_vars_to_json_schema(). If this set needs
|
|
# updating, that comment should be updated too.
|
|
EXCLUDED_FROM_JSON_SCHEMA: frozenset[str] = frozenset(
|
|
{
|
|
"ROCPROFSYS_CI",
|
|
"ROCPROFSYS_CONFIG_FILE",
|
|
"ROCPROFSYS_ENABLED",
|
|
"ROCPROFSYS_LOG_LEVEL",
|
|
"ROCPROFSYS_OUTPUT_PREFIX",
|
|
"ROCPROFSYS_SUPPRESS_CONFIG",
|
|
"ROCPROFSYS_SUPPRESS_PARSING",
|
|
"ROCPROFSYS_TMPDIR",
|
|
}
|
|
)
|
|
|
|
# Mapping from ROCPROFSYS_* env var names to their expected JSON schema
|
|
# path. Used to verify that the JSON config export covers the same
|
|
# settings as the TXT config export.
|
|
ENV_VAR_TO_JSON_PATH: dict[str, str] = {
|
|
# --- Tracing ---
|
|
"ROCPROFSYS_TRACE": "tracing.enabled",
|
|
"ROCPROFSYS_TRACE_LEGACY": "tracing.legacy",
|
|
"ROCPROFSYS_PERFETTO_BUFFER_SIZE_KB": "tracing.buffer_size_kb",
|
|
"ROCPROFSYS_PERFETTO_FILL_POLICY": "tracing.fill_policy",
|
|
"ROCPROFSYS_PERFETTO_BACKEND": "tracing.backend",
|
|
"ROCPROFSYS_PERFETTO_FLUSH_PERIOD_MS": "tracing.flush_period_ms",
|
|
"ROCPROFSYS_SELECTED_REGIONS": "tracing.region",
|
|
# --- Profiling ---
|
|
"ROCPROFSYS_PROFILE": "profiling.enabled",
|
|
"ROCPROFSYS_FLAT_PROFILE": "profiling.flat_profile",
|
|
# --- Sampling ---
|
|
"ROCPROFSYS_USE_SAMPLING": "sampling.enabled",
|
|
"ROCPROFSYS_SAMPLING_FREQ": "sampling.frequency_hz",
|
|
"ROCPROFSYS_SAMPLING_DELAY": "sampling.delay_sec",
|
|
"ROCPROFSYS_SAMPLING_DURATION": "sampling.duration_sec",
|
|
"ROCPROFSYS_SAMPLING_CPUS": "sampling.cpus",
|
|
"ROCPROFSYS_SAMPLING_GPUS": "sampling.gpus",
|
|
"ROCPROFSYS_SAMPLING_AINICS": "sampling.ainics",
|
|
"ROCPROFSYS_SAMPLING_OVERFLOW_EVENT": "sampling.overflow_event",
|
|
# --- Domains: GPU ---
|
|
"ROCPROFSYS_USE_AMD_SMI": "domains.gpu.enabled",
|
|
"ROCPROFSYS_AMD_SMI_METRICS": "domains.gpu.metrics",
|
|
"ROCPROFSYS_USE_AINIC": "domains.gpu.ainic",
|
|
"ROCPROFSYS_USE_UNIFIED_MEMORY_PROFILING": "domains.gpu.unified_memory_profiling",
|
|
"ROCPROFSYS_USE_PROCESS_SAMPLING": "domains.gpu.process_sampling",
|
|
"ROCPROFSYS_PROCESS_SAMPLING_FREQ": "domains.gpu.process_sampling_freq",
|
|
"ROCPROFSYS_PROCESS_SAMPLING_DURATION": "domains.gpu.process_sampling_duration",
|
|
"ROCPROFSYS_CPU_FREQ_ENABLED": "domains.cpu.cpu_freq_enabled",
|
|
"ROCPROFSYS_CPU_METRICS": "domains.cpu.metrics",
|
|
# --- Domains: ROCm ---
|
|
"ROCPROFSYS_ROCM_DOMAINS": "domains.rocm.api_domains",
|
|
"ROCPROFSYS_ROCM_GROUP_BY_QUEUE": "domains.rocm.group_by_queue",
|
|
# --- Domains: Parallel ---
|
|
"ROCPROFSYS_USE_MPIP": "domains.parallel.runtimes.mpi",
|
|
"ROCPROFSYS_USE_OMPT": "domains.parallel.runtimes.openmp",
|
|
"ROCPROFSYS_USE_KOKKOSP": "domains.parallel.runtimes.kokkos",
|
|
"ROCPROFSYS_USE_RCCLP": "domains.parallel.runtimes.rccl",
|
|
"ROCPROFSYS_USE_OPENSHMEM": "domains.parallel.runtimes.shmem",
|
|
"ROCPROFSYS_USE_UCX": "domains.parallel.runtimes.ucx",
|
|
# --- Output ---
|
|
"ROCPROFSYS_OUTPUT_PATH": "output.path",
|
|
"ROCPROFSYS_UNIFIED_MEMORY_OUTPUT_PATH": "output.unified_memory_output_path",
|
|
"ROCPROFSYS_TIME_OUTPUT": "output.time_output",
|
|
"ROCPROFSYS_FILE_OUTPUT": "output.file_output",
|
|
"ROCPROFSYS_USE_ROCPD": "output.rocpd_output",
|
|
"ROCPROFSYS_USE_PID": "output.use_pid",
|
|
# --- Hardware counters ---
|
|
"ROCPROFSYS_ROCM_EVENTS": "hardware_counters.rocm_events",
|
|
"ROCPROFSYS_PAPI_EVENTS": "hardware_counters.papi_events",
|
|
"ROCPROFSYS_PAPI_MULTIPLEXING": "hardware_counters.papi_multiplexing",
|
|
"ROCPROFSYS_GPU_PERF_COUNTERS": "hardware_counters.gpu_perf_counters",
|
|
# --- Causal ---
|
|
"ROCPROFSYS_USE_CAUSAL": "causal.enabled",
|
|
"ROCPROFSYS_CAUSAL_MODE": "causal.mode",
|
|
"ROCPROFSYS_CAUSAL_BACKEND": "causal.backend",
|
|
"ROCPROFSYS_CAUSAL_BINARY_SCOPE": "causal.binary_scope",
|
|
"ROCPROFSYS_CAUSAL_BINARY_EXCLUDE": "causal.binary_exclude",
|
|
"ROCPROFSYS_CAUSAL_FUNCTION_SCOPE": "causal.function_scope",
|
|
"ROCPROFSYS_CAUSAL_FUNCTION_EXCLUDE": "causal.function_exclude",
|
|
"ROCPROFSYS_CAUSAL_SOURCE_SCOPE": "causal.source_scope",
|
|
"ROCPROFSYS_CAUSAL_SOURCE_EXCLUDE": "causal.source_exclude",
|
|
"ROCPROFSYS_CAUSAL_DELAY": "causal.delay_sec",
|
|
"ROCPROFSYS_CAUSAL_DURATION": "causal.duration_sec",
|
|
"ROCPROFSYS_CAUSAL_RANDOM_SEED": "causal.random_seed",
|
|
# --- Advanced ---
|
|
"ROCPROFSYS_VERBOSE": "advanced.verbose",
|
|
"ROCPROFSYS_DEBUG": "advanced.debug",
|
|
"ROCPROFSYS_MAX_DEPTH": "advanced.max_depth",
|
|
"ROCPROFSYS_TRACE_DELAY": "advanced.trace_delay_sec",
|
|
"ROCPROFSYS_TRACE_DURATION": "advanced.trace_duration_sec",
|
|
"ROCPROFSYS_CPU_AFFINITY": "advanced.cpu_affinity",
|
|
"ROCPROFSYS_COLLAPSE_THREADS": "advanced.collapse_threads",
|
|
"ROCPROFSYS_TIMEMORY_COMPONENTS": "advanced.timemory_components",
|
|
"ROCPROFSYS_NETWORK_INTERFACE": "advanced.network_interface",
|
|
"ROCPROFSYS_TRACE_PERIODS": "advanced.trace_periods",
|
|
"ROCPROFSYS_TRACE_PERIOD_CLOCK_ID": "advanced.trace_period_clock_id",
|
|
}
|
|
|
|
|
|
def flatten_json_keys(
|
|
obj: dict | list | str | int | float | bool, prefix: str = ""
|
|
) -> set[str]:
|
|
"""Recursively collect all leaf paths in a JSON object."""
|
|
keys: set[str] = set()
|
|
if isinstance(obj, dict):
|
|
for k, v in obj.items():
|
|
keys |= flatten_json_keys(v, f"{prefix}.{k}" if prefix else k)
|
|
else:
|
|
keys.add(prefix)
|
|
return keys
|
|
|
|
|
|
def validate_format_consistency(txt_vars: set[str], json_paths: set[str]) -> list[str]:
|
|
"""Check that every TXT env var has a JSON schema mapping.
|
|
|
|
Returns a list of error messages for vars without coverage.
|
|
"""
|
|
missing = []
|
|
for var in sorted(txt_vars):
|
|
if var in EXCLUDED_FROM_JSON_SCHEMA:
|
|
continue
|
|
if var in ENV_VAR_TO_JSON_PATH:
|
|
json_key = ENV_VAR_TO_JSON_PATH[var]
|
|
if not any(json_key in path for path in json_paths):
|
|
missing.append(f"{var} -> expected JSON path '{json_key}' not found")
|
|
else:
|
|
missing.append(f"{var} -> no JSON schema mapping defined")
|
|
return missing
|
|
|
|
|
|
# ============================================================================
|
|
# Helper functions
|
|
# ============================================================================
|
|
|
|
|
|
def get_ls_command() -> tuple[str, list[str]]:
|
|
"""Get ls binary name and args (handles RedHat coreutils wrapper).
|
|
|
|
Returns:
|
|
Tuple of (binary_name, args_list)
|
|
"""
|
|
if os.path.exists("/usr/bin/coreutils"):
|
|
return "coreutils", ["--coreutils-prog=ls"]
|
|
return "ls", []
|
|
|
|
|
|
# ============================================================================
|
|
# rocprof-sys-instrument tests
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.mark.instrument
|
|
@pytest.mark.class_name("rocprofiler-systems-instrument")
|
|
class TestRocprofilerSystemsInstrument(RocprofsysTest):
|
|
"""Tests for rocprof-sys-instrument binary."""
|
|
|
|
target = "rocprof-sys-instrument"
|
|
|
|
@pytest.mark.timeout(45)
|
|
def test_help(self):
|
|
pass_regex = [
|
|
r"\[rocprof-sys-instrument\] Usage:[\s\S]*"
|
|
r"\[DEBUG OPTIONS\][\s\S]*"
|
|
r"\[MODE OPTIONS\][\s\S]*"
|
|
r"\[LIBRARY OPTIONS\][\s\S]*"
|
|
r"\[SYMBOL SELECTION OPTIONS\][\s\S]*"
|
|
r"\[RUNTIME OPTIONS\][\s\S]*"
|
|
r"\[GRANULARITY OPTIONS\][\s\S]*"
|
|
r"\[DYNINST OPTIONS\]"
|
|
]
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=["--help"],
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(result, pass_regex=pass_regex)
|
|
|
|
@pytest.mark.timeout(240)
|
|
def test_simulate_ls(self):
|
|
ls_name, ls_args = get_ls_command()
|
|
|
|
test_args = [
|
|
"--simulate",
|
|
"--dump-info",
|
|
"--print-format",
|
|
"json",
|
|
"txt",
|
|
"xml",
|
|
"-v",
|
|
"2",
|
|
"--all-functions",
|
|
"--",
|
|
ls_name,
|
|
*ls_args,
|
|
]
|
|
|
|
expected_files = [
|
|
"available.json",
|
|
"available.txt",
|
|
"available.xml",
|
|
"excluded.json",
|
|
"excluded.txt",
|
|
"excluded.xml",
|
|
"instrumented.json",
|
|
"instrumented.txt",
|
|
"instrumented.xml",
|
|
"overlapping.json",
|
|
"overlapping.txt",
|
|
"overlapping.xml",
|
|
]
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=test_args,
|
|
fail_on_not_found=True,
|
|
)
|
|
|
|
self.assert_regex(result)
|
|
expected_files_paths = [
|
|
result.output_dir / "instrumentation" / f for f in expected_files
|
|
]
|
|
self.assert_file_exists(expected_files_paths)
|
|
|
|
@pytest.mark.timeout(120)
|
|
def test_simulate_lib(self, rocprof_config):
|
|
user_lib = rocprof_config.rocprofsys_lib_dir / "librocprof-sys-user.so"
|
|
if not user_lib.exists():
|
|
pytest.fail("librocprof-sys-user.so not found")
|
|
|
|
pass_regex = [
|
|
r"\[rocprof-sys\]\[exe\] Runtime instrumentation is not possible![\s\S]*"
|
|
r"\[rocprof-sys\]\[exe\] Switching to binary rewrite mode and assuming '--simulate --all-functions'"
|
|
]
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=["--print-available", "functions", "-v", "2", "--", str(user_lib)],
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(result, pass_regex=pass_regex)
|
|
|
|
@pytest.mark.timeout(120)
|
|
def test_simulate_lib_basename(self, rocprof_config, test_output_dir):
|
|
"""Test instrument with library basename.
|
|
|
|
This MUST be run from a tmp directory, NOT from the actual lib directory.
|
|
Running from the lib directory causes Dyninst to modify the library in-place,
|
|
contaminating it with instrumentation markers. This breaks all subsequent
|
|
binary rewrite tests with "unable to reinstrument previously instrumented
|
|
binary" errors.
|
|
"""
|
|
lib_basename = "librocprof-sys-user.so"
|
|
user_lib = rocprof_config.rocprofsys_lib_dir / lib_basename
|
|
if not user_lib.exists():
|
|
pytest.skip(f"{lib_basename} not built")
|
|
|
|
tmp_dir = test_output_dir / "tmp"
|
|
tmp_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
output_lib = test_output_dir / lib_basename
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=[
|
|
"--print-available",
|
|
"functions",
|
|
"-v",
|
|
"2",
|
|
"-o",
|
|
str(output_lib),
|
|
"--",
|
|
lib_basename,
|
|
],
|
|
working_directory=tmp_dir,
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(result)
|
|
|
|
@pytest.mark.timeout(120)
|
|
def test_write_log(self):
|
|
"""Test instrument writing to log file."""
|
|
ls_name, ls_args = get_ls_command()
|
|
|
|
pass_regex = [r"Opening .*/instrumentation/user\.log"]
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=[
|
|
"--print-instrumented",
|
|
"functions",
|
|
"-v",
|
|
"1",
|
|
"--log-file",
|
|
"user.log",
|
|
"--",
|
|
ls_name,
|
|
*ls_args,
|
|
],
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(result, pass_regex=pass_regex)
|
|
self.assert_file_exists(result.output_dir / "instrumentation" / "user.log")
|
|
|
|
@pytest.mark.timeout(60)
|
|
def test_exe_only(self):
|
|
"""Test --exe-only excludes shared libraries from instrumentation."""
|
|
ls_name, ls_args = get_ls_command()
|
|
|
|
pass_regex = [r"\[filter\] skipping shared lib '.*' \(--exe-only\)"]
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=[
|
|
"--simulate",
|
|
"--exe-only",
|
|
"--",
|
|
ls_name,
|
|
*ls_args,
|
|
],
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(result, pass_regex=pass_regex)
|
|
|
|
@pytest.mark.timeout(60)
|
|
def test_max_library_functions(self):
|
|
"""Test --max-library-functions skips shared libs exceeding the threshold."""
|
|
ls_name, ls_args = get_ls_command()
|
|
|
|
pass_regex = [
|
|
r"\[filter\] skipping shared lib '.*' "
|
|
r"\(\d+ functions > --max-library-functions=10\)"
|
|
]
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=[
|
|
"--simulate",
|
|
"--max-library-functions",
|
|
"10",
|
|
"--",
|
|
ls_name,
|
|
*ls_args,
|
|
],
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(result, pass_regex=pass_regex)
|
|
|
|
|
|
# ============================================================================
|
|
# rocprof-sys-avail tests
|
|
# ============================================================================
|
|
|
|
|
|
@pytest.mark.avail
|
|
@pytest.mark.class_name("rocprofiler-systems-avail")
|
|
class TestRocprofilerSystemsAvail(RocprofsysTest):
|
|
"""Tests for rocprof-sys-avail binary."""
|
|
|
|
target = "rocprof-sys-avail"
|
|
|
|
@pytest.mark.timeout(45)
|
|
def test_help(self):
|
|
pass_regex = [
|
|
r"\[rocprof-sys-avail\] Usage:[\s\S]*"
|
|
r"\[DEBUG OPTIONS\][\s\S]*"
|
|
r"\[INFO OPTIONS\][\s\S]*"
|
|
r"\[FILTER OPTIONS\][\s\S]*"
|
|
r"\[COLUMN OPTIONS\][\s\S]*"
|
|
r"\[DISPLAY OPTIONS\][\s\S]*"
|
|
r"\[OUTPUT OPTIONS\][\s\S]*"
|
|
]
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=["--help"],
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(result, pass_regex=pass_regex)
|
|
|
|
@pytest.mark.timeout(45)
|
|
def test_all(self):
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=["--all"],
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(result)
|
|
|
|
@pytest.mark.timeout(45)
|
|
def test_all_expand_keys(self):
|
|
fail_regex = [r"%[a-zA-Z_]%"]
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=["--all", "--expand-keys"],
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(result, fail_regex=fail_regex)
|
|
|
|
@pytest.mark.timeout(45)
|
|
def test_all_only_available_alphabetical(self, test_output_dir):
|
|
log_file = (
|
|
test_output_dir / "rocprof-sys-avail-all-only-available-alphabetical.log"
|
|
)
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=[
|
|
"--all",
|
|
"--available",
|
|
"--alphabetical",
|
|
"--debug",
|
|
"--output",
|
|
str(log_file),
|
|
],
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(result)
|
|
self.assert_file_exists(log_file)
|
|
|
|
@pytest.mark.timeout(45)
|
|
def test_all_csv(self):
|
|
pass_regex = [
|
|
r"COMPONENT#AVAILABLE#VALUE_TYPE#STRING_IDS#FILENAME#DESCRIPTION#CATEGORY#[\s\S]*"
|
|
r"ENVIRONMENT VARIABLE#VALUE#DATA TYPE#DESCRIPTION#CATEGORIES#[\s\S]*"
|
|
r"HARDWARE COUNTER#DEVICE#AVAILABLE#DESCRIPTION#"
|
|
]
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=["--all", "--csv", "--csv-separator", "#"],
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(result, pass_regex=pass_regex)
|
|
|
|
@pytest.mark.timeout(45)
|
|
def test_filter_wall_clock_available(self):
|
|
pass_regex = [
|
|
r"\|[-]+\|[\s\S]*"
|
|
r"\|[ ]+COMPONENT[ ]+\|[\s\S]*"
|
|
r"\|[-]+\|[\s\S]*"
|
|
r"\| (wall_clock)[ ]+\|[\s\S]*"
|
|
r"\|[-]+\|"
|
|
]
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=["-r", "wall_clock", "-C", "--available"],
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(result, pass_regex=pass_regex)
|
|
|
|
@pytest.mark.timeout(45)
|
|
def test_category_filter_rocprofiler_systems(self):
|
|
pass_regex = [r"ROCPROFSYS_(SETTINGS_DESC|OUTPUT_FILE|OUTPUT_PREFIX)"]
|
|
fail_regex = [
|
|
r"ROCPROFSYS_(ADD_SECONDARY|SCIENTIFIC|PRECISION|MEMORY_PRECISION|TIMING_PRECISION)",
|
|
]
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=["--categories", "settings::rocprofsys", "--brief"],
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(result, pass_regex=pass_regex, fail_regex=fail_regex)
|
|
|
|
@pytest.mark.timeout(45)
|
|
def test_category_filter_timemory(self):
|
|
pass_regex = [
|
|
r"ROCPROFSYS_(ADD_SECONDARY|SCIENTIFIC|PRECISION|MEMORY_PRECISION|TIMING_PRECISION)"
|
|
]
|
|
fail_regex = [r"ROCPROFSYS_(SETTINGS_DESC|OUTPUT_FILE)"]
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=["--categories", "settings::timemory", "--brief", "--advanced"],
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(result, pass_regex=pass_regex, fail_regex=fail_regex)
|
|
|
|
@pytest.mark.timeout(45)
|
|
def test_regex_negation(self):
|
|
pass_regex = [
|
|
r"ENVIRONMENT VARIABLE,[\s\S]*"
|
|
r"ROCPROFSYS_THREAD_POOL_SIZE,[\s\S]*"
|
|
r"ROCPROFSYS_USE_PID,"
|
|
]
|
|
fail_regex = [r"ROCPROFSYS_TRACE"]
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=[
|
|
"-R",
|
|
"rocprofsys",
|
|
"~timemory",
|
|
"-r",
|
|
"_P",
|
|
"~PERFETTO",
|
|
"~PROCESS_SAMPLING",
|
|
"~KOKKOSP",
|
|
"~PAGE",
|
|
"--csv",
|
|
"--brief",
|
|
"--advanced",
|
|
],
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(result, pass_regex=pass_regex, fail_regex=fail_regex)
|
|
|
|
@pytest.mark.timeout(45)
|
|
def test_write_config(self, test_output_dir):
|
|
config_base = test_output_dir / "rocprof-sys-test"
|
|
|
|
avail_cfg_path = test_output_dir / "rocprof-sys-"
|
|
avail_cfg_path = str(avail_cfg_path).replace("+", r"\+")
|
|
|
|
pass_regex = [
|
|
rf"Outputting JSON configuration file '{avail_cfg_path}test\.json'"
|
|
r"[\s\S]*"
|
|
rf"Outputting XML configuration file '{avail_cfg_path}test\.xml'"
|
|
r"[\s\S]*"
|
|
rf"Outputting text configuration file '{avail_cfg_path}test\.cfg'"
|
|
]
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=[
|
|
"-G",
|
|
str(config_base) + ".cfg",
|
|
"-F",
|
|
"txt",
|
|
"json",
|
|
"xml",
|
|
"--force",
|
|
"--all",
|
|
"-c",
|
|
"rocprofsys",
|
|
],
|
|
fail_on_not_found=True,
|
|
)
|
|
|
|
self.assert_regex(result, pass_regex=pass_regex)
|
|
|
|
config_files = [
|
|
test_output_dir / f"rocprof-sys-test.{ext}" for ext in ["cfg", "json", "xml"]
|
|
]
|
|
self.assert_file_exists(
|
|
config_files, subtest_name="Config file existence validation"
|
|
)
|
|
|
|
@pytest.mark.timeout(45)
|
|
def test_write_config_tweak(self, test_output_dir):
|
|
config_base = test_output_dir / "rocprof-sys-tweak"
|
|
|
|
env_overrides = {
|
|
"ROCPROFSYS_TRACE": "OFF",
|
|
"ROCPROFSYS_PROFILE": "ON",
|
|
"ROCPROFSYS_USE_SAMPLING": "OFF",
|
|
"ROCPROFSYS_TIME_OUTPUT": "OFF",
|
|
}
|
|
|
|
avail_cfg_path = test_output_dir / "rocprof-sys-"
|
|
avail_cfg_path = str(avail_cfg_path).replace("+", r"\+")
|
|
|
|
pass_regex = [
|
|
rf"Outputting JSON configuration file '{avail_cfg_path}tweak\.json'"
|
|
r"[\s\S]*"
|
|
rf"Outputting XML configuration file '{avail_cfg_path}tweak\.xml'"
|
|
r"[\s\S]*"
|
|
rf"Outputting text configuration file '{avail_cfg_path}tweak\.cfg'"
|
|
]
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=[
|
|
"-G",
|
|
str(config_base) + ".cfg",
|
|
"-F",
|
|
"txt",
|
|
"json",
|
|
"xml",
|
|
"--force",
|
|
],
|
|
fail_on_not_found=True,
|
|
env=env_overrides,
|
|
)
|
|
self.assert_regex(result, pass_regex=pass_regex)
|
|
|
|
config_files = [
|
|
test_output_dir / f"rocprof-sys-tweak.{ext}" for ext in ["cfg", "json", "xml"]
|
|
]
|
|
self.assert_file_exists(
|
|
config_files, subtest_name="Config file existence validation"
|
|
)
|
|
|
|
@pytest.mark.timeout(45)
|
|
def test_format_consistency(self, test_output_dir):
|
|
"""Validate that JSON and TXT config formats cover the same env vars.
|
|
|
|
Generates both JSON (hierarchical schema) and TXT (flat key=value)
|
|
configs, then verifies that every ROCPROFSYS_* env var in the TXT
|
|
output has a corresponding mapping in the JSON schema — except for
|
|
documented internal/session-specific vars in EXCLUDED_FROM_JSON_SCHEMA.
|
|
"""
|
|
config_base = test_output_dir / "format-check"
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=[
|
|
"-G",
|
|
str(config_base) + ".cfg",
|
|
"-F",
|
|
"txt",
|
|
"json",
|
|
"--force",
|
|
],
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(result)
|
|
|
|
txt_file = test_output_dir / "format-check.cfg"
|
|
json_file = test_output_dir / "format-check.json"
|
|
|
|
self.assert_file_exists(
|
|
[txt_file, json_file],
|
|
subtest_name="Config files generated",
|
|
)
|
|
|
|
# Parse TXT: extract all ROCPROFSYS_* env var names
|
|
txt_vars = set()
|
|
with open(txt_file) as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if line.startswith("#") or not line:
|
|
continue
|
|
match = re.match(r"(ROCPROFSYS_\w+)\s*=", line)
|
|
if match:
|
|
txt_vars.add(match.group(1))
|
|
|
|
# Parse JSON: flatten to find all leaf paths
|
|
with open(json_file) as f:
|
|
json_paths = flatten_json_keys(json.load(f))
|
|
|
|
missing = validate_format_consistency(txt_vars, json_paths)
|
|
if missing:
|
|
pytest.fail(
|
|
f"TXT config has {len(missing)} env var(s) without JSON "
|
|
f"schema coverage:\n" + "\n".join(f" {m}" for m in missing)
|
|
)
|
|
|
|
@pytest.mark.timeout(45)
|
|
def test_list_keys(self):
|
|
pass_regex = [r"Output Keys:[\s\S]*%argv%[\s\S]*%argv_hash%"]
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=["--list-keys", "--expand-keys"],
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(result, pass_regex=pass_regex)
|
|
|
|
@pytest.mark.timeout(45)
|
|
def test_list_keys_markdown(self):
|
|
pass_regex = [r"`%argv%`[\s\S]*`%argv_hash%`"]
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=["--list-keys", "--expand-keys", "--markdown"],
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(result, pass_regex=pass_regex)
|
|
|
|
@pytest.mark.timeout(45)
|
|
def test_list_categories(self):
|
|
pass_regex = [r" component::[\s\S]* hw_counters::[\s\S]* settings::"]
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=["--list-categories"],
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(result, pass_regex=pass_regex)
|
|
|
|
@pytest.mark.timeout(45)
|
|
def test_core_categories(self):
|
|
pass_regex = [
|
|
r"ROCPROFSYS_CONFIG_FILE[\s\S]*ROCPROFSYS_ENABLED[\s\S]*"
|
|
r"ROCPROFSYS_SUPPRESS_CONFIG[\s\S]*ROCPROFSYS_SUPPRESS_PARSING[\s\S]*ROCPROFSYS_VERBOSE"
|
|
]
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=["-c", "core"],
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(result, pass_regex=pass_regex)
|
|
|
|
@pytest.mark.timeout(45)
|
|
def test_list_domains(self):
|
|
"""Test that list-domains command works."""
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=["--list-domains"],
|
|
)
|
|
self.assert_regex(
|
|
result,
|
|
pass_regex=["Available ROCm domains with operations:", "scratch_memory"],
|
|
)
|
|
|
|
@pytest.mark.timeout(45)
|
|
@pytest.mark.parametrize(
|
|
"run_args, pass_regex",
|
|
[
|
|
pytest.param(
|
|
["--list-operations"],
|
|
["Error: '--list-operations' requires a domain name."],
|
|
id="no-domain",
|
|
),
|
|
pytest.param(
|
|
["--list-operations", "scratch_memory"],
|
|
["SCRATCH_MEMORY_ALLOC"],
|
|
id="found",
|
|
),
|
|
pytest.param(
|
|
["--list-operations", "megaman"],
|
|
["Error: Domain 'megaman' not found."],
|
|
id="error",
|
|
),
|
|
],
|
|
)
|
|
def test_list_operations(self, run_args, pass_regex):
|
|
"""Test that list-operations command works."""
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=run_args,
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(result, pass_regex=pass_regex)
|
|
|
|
@pytest.mark.timeout(45)
|
|
def test_settings_no_gpu(self):
|
|
"""Test that settings query works without GPU initialization.
|
|
|
|
This test validates the lightweight initialization mode that allows
|
|
rocprof-sys-avail to query settings without initializing the ROCm runtime,
|
|
reducing startup time and allowing operation in environments without GPU/ROCm.
|
|
"""
|
|
pass_regex = [r"ROCPROFSYS_TRACE"]
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=["--settings", "--brief"],
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(result, pass_regex=pass_regex)
|
|
|
|
@pytest.mark.timeout(45)
|
|
def test_components_no_gpu(self):
|
|
"""Test that component query works without GPU initialization.
|
|
|
|
This test validates that timemory component introspection works without
|
|
requiring GPU/ROCm access, using the lightweight initialization path.
|
|
"""
|
|
pass_regex = [r"COMPONENT"]
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=["--components", "--brief"],
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(result, pass_regex=pass_regex)
|
|
|
|
@pytest.mark.timeout(45)
|
|
def test_settings_description_no_gpu(self):
|
|
"""Test that settings with descriptions works without GPU initialization.
|
|
|
|
This test validates that detailed settings queries (with descriptions)
|
|
work without GPU/ROCm initialization.
|
|
"""
|
|
pass_regex = [r"ROCPROFSYS_OUTPUT_PATH"]
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=["--settings", "--description", "--brief"],
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(result, pass_regex=pass_regex)
|
|
|
|
@pytest.mark.timeout(45)
|
|
@pytest.mark.gpu
|
|
def test_settings_rocm_available(self, rocprof_config):
|
|
"""Test that ROCm-specific settings are present.
|
|
|
|
This test validates that settings like ROCPROFSYS_ROCM_DOMAINS and
|
|
ROCPROFSYS_AMD_SMI_METRICS are registered and visible.
|
|
"""
|
|
|
|
# These settings should always be present when ROCm is enabled
|
|
pass_regex = [
|
|
r"ROCPROFSYS_AMD_SMI_METRICS",
|
|
r"ROCPROFSYS_ROCM_DOMAINS",
|
|
]
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=["--settings", "--brief"],
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(result, pass_regex=pass_regex)
|
|
|
|
|
|
# ============================================================================
|
|
# rocprof-sys-run tests
|
|
# ============================================================================
|
|
|
|
|
|
def kitchen_sink_args(
|
|
*,
|
|
empty_cfg,
|
|
output_dir,
|
|
output_subdir: str,
|
|
trace_file: str,
|
|
tmpdir,
|
|
sleep_cmd: str,
|
|
run_only: bool = False,
|
|
) -> list[str]:
|
|
"""Shared arg list for the run/sample kitchen-sink tests.
|
|
|
|
Most flags are the same for both launchers. Pass run_only=True for the
|
|
rocprof-sys-run extras (-S and --fork). Callers just change the output
|
|
subdir and trace-file name.
|
|
"""
|
|
args = [
|
|
"--monochrome",
|
|
"--debug=false",
|
|
"-v",
|
|
"1",
|
|
"-c",
|
|
str(empty_cfg),
|
|
"-o",
|
|
str(output_dir),
|
|
output_subdir,
|
|
"-TPHD",
|
|
]
|
|
if run_only:
|
|
args += ["-S", "cputime", "realtime"]
|
|
args += [
|
|
"--trace-wait=1.0e-12",
|
|
"--trace-duration=5.0",
|
|
"--wait=1.0",
|
|
"--duration=3.0",
|
|
f"--trace-file={trace_file}",
|
|
"--trace-buffer-size=100",
|
|
"--trace-fill-policy=ring_buffer",
|
|
"--profile-format",
|
|
"console",
|
|
"json",
|
|
"text",
|
|
"--process-freq",
|
|
"1000",
|
|
"--process-wait",
|
|
"0.0",
|
|
"--process-duration",
|
|
"10",
|
|
"--cpus",
|
|
"0-4",
|
|
"--gpus",
|
|
"0",
|
|
"-f",
|
|
"1000",
|
|
"--sampling-wait",
|
|
"1.0",
|
|
"--sampling-duration",
|
|
"10",
|
|
"-t",
|
|
"0-3",
|
|
"--sample-cputime",
|
|
"1000",
|
|
"1.0",
|
|
"0-3",
|
|
"--sample-realtime",
|
|
"10",
|
|
"0.5",
|
|
"0-3",
|
|
"-I",
|
|
"all",
|
|
"-E",
|
|
"mutex-locks",
|
|
"rw-locks",
|
|
"spin-locks",
|
|
"-C",
|
|
"perf::INSTRUCTIONS",
|
|
"-G",
|
|
"GRBM_COUNT",
|
|
"--inlines",
|
|
"--hsa-interrupt",
|
|
"0",
|
|
"--use-causal=false",
|
|
"--use-kokkosp",
|
|
"--tmpdir",
|
|
str(tmpdir),
|
|
"--timemory-components",
|
|
"wall_clock",
|
|
"cpu_clock",
|
|
"peak_rss",
|
|
"page_rss",
|
|
"--ci",
|
|
"--num-threads-hint=4",
|
|
"--sampling-allocator-size=32",
|
|
"--dl-verbose=3",
|
|
"--perfetto-annotations=off",
|
|
"--perfetto-backend",
|
|
"inprocess",
|
|
"--kokkosp-kernel-logger",
|
|
"--kokkosp-name-length-max=1024",
|
|
'--kokkosp-prefix="[kokkos]"',
|
|
"--merge-perfetto-files",
|
|
"--use-pid",
|
|
"false",
|
|
"--time-output",
|
|
"off",
|
|
"--thread-pool-size",
|
|
"0",
|
|
]
|
|
if run_only:
|
|
args += ["--fork"]
|
|
args += ["--", sleep_cmd, "5"]
|
|
return args
|
|
|
|
|
|
@pytest.mark.sys_run
|
|
@pytest.mark.timeout(45)
|
|
@pytest.mark.class_name("rocprofiler-systems-run")
|
|
class TestRocprofilerSystemsRun(RocprofsysTest):
|
|
"""Tests for rocprof-sys-run binary."""
|
|
|
|
target = "rocprof-sys-run"
|
|
|
|
def test_help(self):
|
|
"""Test rocprof-sys-run --help output."""
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=["--help"],
|
|
fail_on_not_found=True,
|
|
)
|
|
|
|
self.assert_regex(result)
|
|
|
|
def test_args(self, test_output_dir):
|
|
"""Test rocprof-sys-run with comprehensive arguments."""
|
|
sleep_cmd = get_sleep_cmd()
|
|
|
|
config_dir = test_output_dir / "config"
|
|
config_dir.mkdir(parents=True, exist_ok=True)
|
|
empty_cfg = config_dir / "empty.cfg"
|
|
empty_cfg.write_text("#\n# empty config file\n#\n")
|
|
|
|
tmpdir = test_output_dir / "tmpdir"
|
|
tmpdir = tmpdir.resolve()
|
|
tmpdir.mkdir(parents=True, exist_ok=True)
|
|
|
|
args = kitchen_sink_args(
|
|
empty_cfg=empty_cfg,
|
|
output_dir=test_output_dir,
|
|
output_subdir="run-args-output/",
|
|
trace_file="perfetto-run-args-trace.proto",
|
|
tmpdir=tmpdir,
|
|
sleep_cmd=sleep_cmd,
|
|
run_only=True,
|
|
)
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=args,
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(result)
|
|
|
|
|
|
@pytest.mark.sampling
|
|
@pytest.mark.timeout(45)
|
|
@pytest.mark.class_name("rocprofiler-systems-sample")
|
|
class TestRocprofilerSystemsSample(RocprofsysTest):
|
|
"""Tests for rocprof-sys-sample binary."""
|
|
|
|
target = "rocprof-sys-sample"
|
|
|
|
def test_args(self, test_output_dir):
|
|
"""Kitchen-sink: many combined rocprof-sys-sample flags in one command.
|
|
This test validates the comprehensive set of rocprof-sys-sample flags and their combinations.
|
|
"""
|
|
sleep_cmd = get_sleep_cmd()
|
|
|
|
config_dir = test_output_dir / "config"
|
|
config_dir.mkdir(parents=True, exist_ok=True)
|
|
empty_cfg = config_dir / "empty.cfg"
|
|
empty_cfg.write_text("#\n# empty config file\n#\n")
|
|
|
|
tmpdir = (test_output_dir / "tmpdir").resolve()
|
|
tmpdir.mkdir(parents=True, exist_ok=True)
|
|
|
|
args = kitchen_sink_args(
|
|
empty_cfg=empty_cfg,
|
|
output_dir=test_output_dir,
|
|
output_subdir="sample-args-output/",
|
|
trace_file="perfetto-sample-args-trace.proto",
|
|
tmpdir=tmpdir,
|
|
sleep_cmd=sleep_cmd,
|
|
)
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=args,
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(
|
|
result,
|
|
fail_regex=[r"Unrecognized command line option"],
|
|
)
|
|
|
|
def test_args_rejects_s_flag(self):
|
|
"""Negative: a run-only flag (-S) is rejected by the sample parser.
|
|
|
|
Documents the intentional exclusion from the kitchen-sink and proves the
|
|
parser does not silently accept rocprof-sys-run-only options.
|
|
"""
|
|
sleep_cmd = get_sleep_cmd()
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=["-S", "cputime", "realtime", "--", sleep_cmd, "1"],
|
|
fail_on_pass=True,
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(
|
|
result,
|
|
pass_regex=[r"Unrecognized command line option"],
|
|
use_abort_fail_regex=False, # negative test intentionally exits non-zero
|
|
)
|
|
|
|
def test_args_stops_at_separator(self):
|
|
"""The ``--`` separator must split tool options from the program to run.
|
|
|
|
``-E`` takes a list of values, so the parser keeps reading words until it
|
|
hits something that isn't a value. We put ``-E`` right before ``--`` to
|
|
make sure the parser stops at ``--`` instead of greedily swallowing it.
|
|
If it worked correctly, everything after ``--`` is treated as the program
|
|
to launch (``sleep 1``) and the run succeeds with no parser error.
|
|
"""
|
|
sleep_cmd = get_sleep_cmd()
|
|
|
|
result = self.run_test(
|
|
"baseline",
|
|
target=self.target,
|
|
run_args=[
|
|
"-E",
|
|
"mutex-locks",
|
|
"rw-locks",
|
|
"spin-locks",
|
|
"--",
|
|
sleep_cmd,
|
|
"1",
|
|
],
|
|
fail_on_not_found=True,
|
|
)
|
|
self.assert_regex(
|
|
result,
|
|
fail_regex=[r"Unrecognized command line option"],
|
|
)
|