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
347 lines
12 KiB
Python
347 lines
12 KiB
Python
# Copyright (c) Advanced Micro Devices, Inc.
|
|
# SPDX-License-Identifier: MIT
|
|
|
|
"""
|
|
Layered test environment management for rocprofiler-systems tests.
|
|
|
|
The environment handed to a runner is composed of three layers, ordered from
|
|
lowest to highest precedence:
|
|
|
|
- ``base``: framework defaults for the test type (the ``*_environment``
|
|
presets), NOT inherited verbatim from the shell.
|
|
- ``test``: settings from the test itself plus framework-injected values,
|
|
including ``LD_LIBRARY_PATH`` and (for sanitizer builds) ``LD_PRELOAD``.
|
|
- ``user``: the inherited shell environment (``os.environ``). The user layer
|
|
wins, so anything exported in the shell overrides the base and test layers.
|
|
|
|
Separately, a handful of variables are read directly by config discovery
|
|
(``RocprofsysConfig``) rather than flowing through these layers:
|
|
``ROCPROFSYS_INSTALL_DIR``, ``ROCPROFSYS_BUILD_DIR``, ``ROCM_PATH``, and
|
|
``ROCPROFSYS_PYTHON_HINTS``. They can be overridden by the user via the shell,
|
|
but they are tightly coupled with config discovery (they locate the install,
|
|
build, ROCm, and Python trees).
|
|
|
|
The following environment variables are excluded from the user layer overrides:
|
|
- ``LD_LIBRARY_PATH``: Shell's value is folded in via ``config.get_library_path()``
|
|
- ``LD_PRELOAD``: Shell's value is folded in via the runner for sanitizer builds
|
|
- ``ROCPROFSYS_OUTPUT_PATH``: Testing and validation depends on this path being
|
|
set by the framework.
|
|
- ``ROCPROFSYS_CONFIG_FILE``: Testing and validation depends on this config file being
|
|
set by the framework.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
import os
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from .config import RocprofsysConfig
|
|
|
|
# Environment layers ordered from lowest to highest precedence
|
|
ENV_LAYER_ORDER = ("base", "test", "user")
|
|
|
|
|
|
class TestEnvKind(str, Enum):
|
|
"""Base-environment preset selected for a test run."""
|
|
|
|
NONE = "none"
|
|
BASE = "base"
|
|
BINARY = "binary"
|
|
PYTHON = "python"
|
|
CAUSAL = "causal"
|
|
|
|
|
|
# Fixed selection of fundamental shell variables surfaced in the test-session
|
|
# header. This is intentionally small
|
|
FUNDAMENTAL_SYSTEM_ENV_KEYS = (
|
|
"PATH",
|
|
"HOME",
|
|
"USER",
|
|
"SHELL",
|
|
"TERM",
|
|
"LANG",
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class TestEnvironment:
|
|
"""Class that encapsulates the three layers of environments used for a single test run"""
|
|
|
|
base: dict[str, str] = field(default_factory=dict)
|
|
test: dict[str, str] = field(default_factory=dict)
|
|
user: dict[str, str] = field(default_factory=dict)
|
|
|
|
def set_base_environment(
|
|
self,
|
|
config: "RocprofsysConfig",
|
|
test_type: TestEnvKind = TestEnvKind.BASE,
|
|
) -> None:
|
|
"""Set the base layer to the preset for ``test_type`` (default: BASE)."""
|
|
try:
|
|
kind = TestEnvKind(test_type)
|
|
except ValueError as exc:
|
|
valid = ", ".join(k.value for k in TestEnvKind)
|
|
raise ValueError(
|
|
f"Invalid test type: {test_type!r}. Expected one of: {valid}"
|
|
) from exc
|
|
|
|
if kind is TestEnvKind.NONE:
|
|
self.base = {}
|
|
elif kind is TestEnvKind.BASE:
|
|
self.base = base_environment()
|
|
elif kind is TestEnvKind.BINARY:
|
|
self.base = base_binary_environment()
|
|
elif kind is TestEnvKind.PYTHON:
|
|
self.base = base_python_environment(config)
|
|
elif kind is TestEnvKind.CAUSAL:
|
|
self.base = base_causal_environment()
|
|
|
|
def set_test_environment(self, test_env: dict[str, str]) -> None:
|
|
self.test.update(test_env)
|
|
|
|
def merge(self) -> tuple[dict[str, str], dict[str, str]]:
|
|
"""Merge the layers into ``(env, origin)``.
|
|
|
|
Layers are applied in precedence order (base -> test -> user), so each
|
|
variable's value comes from, and its origin is, the highest-precedence
|
|
layer that set it.
|
|
"""
|
|
merged: dict[str, str] = {}
|
|
origin: dict[str, str] = {}
|
|
for layer in ENV_LAYER_ORDER:
|
|
for key, value in getattr(self, layer).items():
|
|
merged[key] = value
|
|
origin[key] = layer
|
|
return merged, origin
|
|
|
|
def get_merged_environment(self, config: "RocprofsysConfig") -> dict[str, str]:
|
|
"""Return the effective merged environment (highest-precedence layer wins).
|
|
|
|
ROCm-derived defaults (:func:`derived_defaults`) are applied at the
|
|
lowest precedence, so any base/test/user value overrides them.
|
|
"""
|
|
merged = self.merge()[0]
|
|
for key, value in derived_defaults(config).items():
|
|
merged.setdefault(key, value)
|
|
return merged
|
|
|
|
def format_layers(self) -> list[str]:
|
|
"""Format the environment grouped by ``[base]``, ``[test]``, ``[user]``.
|
|
|
|
Each variable is shown only once, under the highest-precedence layer
|
|
that sets it, so an entry overridden by a higher layer is not repeated
|
|
lower down. The ``[user]`` layer is limited to variables that explicitly
|
|
override a ``[base]`` or ``[test]`` setting; other purely shell-inherited
|
|
values are omitted to keep the per-test output concise. Layers with no
|
|
entries to show are omitted entirely.
|
|
"""
|
|
sections = {
|
|
"base": [k for k in self.base if k not in self.test and k not in self.user],
|
|
"test": [k for k in self.test if k not in self.user],
|
|
"user": [k for k in self.user if k in self.base or k in self.test],
|
|
}
|
|
lines: list[str] = []
|
|
for layer in ENV_LAYER_ORDER:
|
|
keys = sorted(sections[layer])
|
|
if not keys:
|
|
continue
|
|
lines.append(f"[{layer}]")
|
|
for key in keys:
|
|
lines.append(self._format_entry(layer, key))
|
|
return lines
|
|
|
|
def _format_entry(self, layer: str, key: str) -> str:
|
|
"""Format one `` key=value`` line for ``format_layers``.
|
|
|
|
For user entries that also exist in ``base``/``test``, append a note
|
|
showing the framework value the shell value replaced, e.g.
|
|
``ROCPROFSYS_TRACE=OFF (overrides test=ON)``.
|
|
"""
|
|
value = getattr(self, layer)[key]
|
|
if layer == "user":
|
|
if key in self.test:
|
|
return f" {key}={value} (overrides test={self.test[key]})"
|
|
if key in self.base:
|
|
return f" {key}={value} (overrides base={self.base[key]})"
|
|
return f" {key}={value}"
|
|
|
|
def set_user_environment(self) -> None:
|
|
"""Capture the invoking shell environment as the user layer.
|
|
|
|
Certain environment variables are excluded (see
|
|
the module docstring).
|
|
"""
|
|
owned = (
|
|
"LD_LIBRARY_PATH",
|
|
"LD_PRELOAD",
|
|
"ROCPROFSYS_OUTPUT_PATH",
|
|
"ROCPROFSYS_CONFIG_FILE",
|
|
)
|
|
self.user.update({k: v for k, v in os.environ.items() if k not in owned})
|
|
|
|
|
|
def derived_defaults(config: "RocprofsysConfig") -> dict[str, str]:
|
|
"""ROCm-derived defaults, applied at the lowest precedence by
|
|
:meth:`TestEnvironment.get_merged_environment` (any layer overrides them).
|
|
Each entry is only produced when its derived target exists on disk.
|
|
"""
|
|
defaults: dict[str, str] = {}
|
|
|
|
# LIBVA_DRIVERS_PATH: fall back to ROCm's bundled VA drivers.
|
|
if config.rocm_path:
|
|
sysdeps = (config.rocm_path / "lib" / "rocm_sysdeps" / "lib").resolve()
|
|
if sysdeps.is_dir():
|
|
defaults["LIBVA_DRIVERS_PATH"] = str(sysdeps)
|
|
|
|
return defaults
|
|
|
|
|
|
# Settings shared by every ``base_*`` environment preset.
|
|
COMMON_BASE_DEFAULT_VARS = {
|
|
"ROCPROFSYS_CI": "ON",
|
|
"ROCPROFSYS_CI_TIMEOUT": "300",
|
|
"ROCPROFSYS_USE_PID": "OFF",
|
|
"ROCPROFSYS_CONFIG_FILE": "",
|
|
}
|
|
|
|
# OpenMP thread-pinning shared by presets that exercise OpenMP workloads.
|
|
OMP_DEFAULT_VARS = {
|
|
"OMP_PROC_BIND": "spread",
|
|
"OMP_PLACES": "threads",
|
|
"OMP_NUM_THREADS": "2",
|
|
}
|
|
|
|
|
|
def base_environment() -> dict[str, str]:
|
|
"""Framework default environment for instrumented test execution."""
|
|
return {
|
|
**COMMON_BASE_DEFAULT_VARS,
|
|
**OMP_DEFAULT_VARS,
|
|
"ROCPROFSYS_DEFAULT_MIN_INSTRUCTIONS": "64",
|
|
"ROCPROFSYS_TRACE": "ON",
|
|
"ROCPROFSYS_PROFILE": "ON",
|
|
"ROCPROFSYS_USE_SAMPLING": "ON",
|
|
"ROCPROFSYS_USE_PROCESS_SAMPLING": "ON",
|
|
"ROCPROFSYS_TIME_OUTPUT": "OFF",
|
|
"ROCPROFSYS_FILE_OUTPUT": "ON",
|
|
"ROCPROFSYS_LOG_LEVEL": "info",
|
|
"ROCPROFSYS_SAMPLING_FREQ": "300",
|
|
"ROCPROFSYS_SAMPLING_DELAY": "0.05",
|
|
"ROCPROFSYS_SAMPLING_GPUS": "all",
|
|
}
|
|
|
|
|
|
def base_binary_environment() -> dict[str, str]:
|
|
"""Framework default environment for rocprof-sys binary test execution."""
|
|
return {
|
|
**COMMON_BASE_DEFAULT_VARS,
|
|
"ROCPROFSYS_TRACE": "ON",
|
|
"ROCPROFSYS_PROFILE": "ON",
|
|
"ROCPROFSYS_USE_SAMPLING": "ON",
|
|
"ROCPROFSYS_TIME_OUTPUT": "OFF",
|
|
"ROCPROFSYS_LOG_LEVEL": "info",
|
|
}
|
|
|
|
|
|
def base_python_environment(config: "RocprofsysConfig") -> dict[str, str]:
|
|
"""Framework default environment for Python test execution."""
|
|
return {
|
|
**COMMON_BASE_DEFAULT_VARS,
|
|
"ROCPROFSYS_TRACE": "ON",
|
|
"ROCPROFSYS_PROFILE": "ON",
|
|
"ROCPROFSYS_USE_SAMPLING": "OFF",
|
|
"ROCPROFSYS_USE_PROCESS_SAMPLING": "ON",
|
|
"ROCPROFSYS_TIME_OUTPUT": "OFF",
|
|
"ROCPROFSYS_TREE_OUTPUT": "OFF",
|
|
"ROCPROFSYS_TIMEMORY_COMPONENTS": "wall_clock,trip_count",
|
|
"PYTHONPATH": (
|
|
str(config.rocprofsys_site_packages)
|
|
if config.rocprofsys_site_packages
|
|
else ""
|
|
),
|
|
}
|
|
|
|
|
|
def base_causal_environment() -> dict[str, str]:
|
|
"""Framework default environment for causal profiling test execution."""
|
|
return {
|
|
**COMMON_BASE_DEFAULT_VARS,
|
|
"ROCPROFSYS_THREAD_POOL_SIZE": "0",
|
|
"ROCPROFSYS_VERBOSE": "1",
|
|
"ROCPROFSYS_LOG_LEVEL": "info",
|
|
"ROCPROFSYS_DL_VERBOSE": "0",
|
|
"ROCPROFSYS_DEBUG_SETTINGS": "0",
|
|
}
|
|
|
|
|
|
def flat_environment() -> dict[str, str]:
|
|
"""Environment for flat-profile tests."""
|
|
return {
|
|
**OMP_DEFAULT_VARS,
|
|
"ROCPROFSYS_TRACE": "ON",
|
|
"ROCPROFSYS_PROFILE": "ON",
|
|
"ROCPROFSYS_TIME_OUTPUT": "OFF",
|
|
"ROCPROFSYS_COUT_OUTPUT": "ON",
|
|
"ROCPROFSYS_FLAT_PROFILE": "ON",
|
|
"ROCPROFSYS_TIMELINE_PROFILE": "OFF",
|
|
"ROCPROFSYS_COLLAPSE_PROCESSES": "ON",
|
|
"ROCPROFSYS_COLLAPSE_THREADS": "ON",
|
|
"ROCPROFSYS_SAMPLING_FREQ": "50",
|
|
"ROCPROFSYS_TIMEMORY_COMPONENTS": "wall_clock,trip_count",
|
|
}
|
|
|
|
|
|
def lock_environment() -> dict[str, str]:
|
|
"""Environment for thread-lock tracing tests."""
|
|
return {
|
|
"ROCPROFSYS_USE_SAMPLING": "ON",
|
|
"ROCPROFSYS_USE_PROCESS_SAMPLING": "OFF",
|
|
"ROCPROFSYS_SAMPLING_FREQ": "750",
|
|
"ROCPROFSYS_COLLAPSE_THREADS": "ON",
|
|
"ROCPROFSYS_TRACE_THREAD_LOCKS": "ON",
|
|
"ROCPROFSYS_TRACE_THREAD_SPIN_LOCKS": "ON",
|
|
"ROCPROFSYS_TRACE_THREAD_RW_LOCKS": "ON",
|
|
"ROCPROFSYS_COUT_OUTPUT": "ON",
|
|
"ROCPROFSYS_TIME_OUTPUT": "OFF",
|
|
"ROCPROFSYS_TIMELINE_PROFILE": "OFF",
|
|
"ROCPROFSYS_LOG_LEVEL": "info",
|
|
}
|
|
|
|
|
|
def perfetto_environment() -> dict[str, str]:
|
|
"""Environment for perfetto-only tests."""
|
|
return {
|
|
**OMP_DEFAULT_VARS,
|
|
"ROCPROFSYS_TRACE": "ON",
|
|
"ROCPROFSYS_PROFILE": "OFF",
|
|
"ROCPROFSYS_USE_SAMPLING": "ON",
|
|
"ROCPROFSYS_USE_PROCESS_SAMPLING": "ON",
|
|
"ROCPROFSYS_TIME_OUTPUT": "OFF",
|
|
"ROCPROFSYS_PERFETTO_BACKEND": "inprocess",
|
|
"ROCPROFSYS_PERFETTO_FILL_POLICY": "ring_buffer",
|
|
}
|
|
|
|
|
|
def timemory_environment() -> dict[str, str]:
|
|
"""Environment for timemory-only tests."""
|
|
return {
|
|
**OMP_DEFAULT_VARS,
|
|
"ROCPROFSYS_TRACE": "OFF",
|
|
"ROCPROFSYS_PROFILE": "ON",
|
|
"ROCPROFSYS_USE_SAMPLING": "ON",
|
|
"ROCPROFSYS_USE_PROCESS_SAMPLING": "ON",
|
|
"ROCPROFSYS_TIME_OUTPUT": "OFF",
|
|
"ROCPROFSYS_TIMEMORY_COMPONENTS": "wall_clock,trip_count,peak_rss",
|
|
}
|
|
|
|
|
|
def fundamental_system_environment() -> dict[str, str]:
|
|
"""Return the curated selection of shell env vars shown in the session header.
|
|
|
|
Keys are returned in :data:`FUNDAMENTAL_SYSTEM_ENV_KEYS` order; missing
|
|
variables map to an empty string.
|
|
"""
|
|
return {key: os.environ.get(key, "") for key in FUNDAMENTAL_SYSTEM_ENV_KEYS}
|