Files
TheRock/rocm-systems/projects/rocprofiler-systems/source/bin/common/tool_runner.cpp
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

621 lines
20 KiB
C++

// Copyright (c) Advanced Micro Devices, Inc.
// SPDX-License-Identifier: MIT
#include "common/tool_runner.hpp"
#include "common/argument_registration.hpp"
#include "common/common_utils.hpp"
#include "common/defines.h"
#include "common/env_vars.hpp"
#include "common/environment.hpp"
#include "common/path.hpp"
#include "core/argparse.hpp"
#include "core/mproc.hpp"
#include "core/timemory.hpp"
#include <spdlog/fmt/ranges.h>
#include <timemory/log/macros.hpp>
#include <timemory/signals/signal_handlers.hpp>
#include <timemory/utility/argparse.hpp>
#include <algorithm>
#include <array>
#include <atomic>
#include <cstdio>
#include <cstdlib>
#include <exception>
#include <iostream>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <sys/ioctl.h>
#include <unistd.h>
#include <unordered_map>
#include <utility>
#include <vector>
namespace argparse = ::tim::argparse;
namespace signals = ::tim::signals;
namespace path = rocprofsys::common::path;
namespace env_vars = rocprofsys::env_vars;
namespace utils = rocprofsys::common_utils;
using settings = ::rocprofsys::settings;
using parser_data_t = rocprofsys::argparse::parser_data;
namespace
{
using rocprofsys::common::update_mode;
using parser_t = argparse::argument_parser;
using parser_err_t = typename parser_t::result_type;
constexpr int HELP_PADDING = 8;
constexpr int MAX_DESC_WIDTH = 120;
constexpr size_t LAUNCHER_INJECT_SLOTS = 2;
constexpr int EXEC_FAILURE_STATUS = 127;
constexpr std::string_view ANSI_FATAL = "\033[31m"; // red
constexpr std::string_view ANSI_RESET = "\033[0m";
std::atomic<bool>&
monochrome_flag()
{
static std::atomic<bool> flag{ false };
return flag;
}
std::string_view
fatal_color()
{
return monochrome_flag().load(std::memory_order_relaxed) ? std::string_view{}
: ANSI_FATAL;
}
std::string_view
reset_color()
{
return monochrome_flag().load(std::memory_order_relaxed) ? std::string_view{}
: ANSI_RESET;
}
std::string_view
basename_of(std::string_view path)
{
const auto slash = path.rfind('/');
return (slash == std::string_view::npos) ? path : path.substr(slash + 1);
}
int
terminal_columns()
{
struct winsize ws
{};
if(ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) return ws.ws_col;
return 0; // unknown / not a tty
}
std::string
getenv_string(std::string_view key, std::string_view default_value)
{
const auto* val = std::getenv(std::string{ key }.c_str());
return (val != nullptr) ? std::string{ val } : std::string{ default_value };
}
// All string_view fields point at static-storage data (string literals from the
// make_*_config factories). Do not assign from temporaries.
struct tool_config
{
rocprofsys::common_utils::tool_mode mode = rocprofsys::common_utils::tool_mode::run;
std::string_view tool_name;
std::string_view version_name;
std::string_view summary;
std::string_view workflow;
std::string_view output_prefix = {};
bool force_sampling = false;
bool enable_fork = false;
bool enable_launcher = false;
bool show_sample_flag = false;
bool disable_cputime_on_realtime_only = false;
std::unordered_map<std::string, std::string> deprecated_flags = {};
};
tool_config
make_run_config()
{
tool_config cfg{};
cfg.mode = rocprofsys::common_utils::tool_mode::run;
cfg.tool_name = "run";
cfg.version_name = "rocprof-sys-run";
cfg.summary = "Execute instrumented binaries with ROCm Systems Profiler "
"configuration.";
cfg.workflow = R"(INSTRUMENTATION WORKFLOW:
1. Instrument: rocprof-sys-instrument -o app.inst -- ./app
2. Run: rocprof-sys-run --preset=balanced -- ./app.inst
3. Analyze: cat rocprof-sys-output/wall_clock.txt)";
cfg.output_prefix = "ROCPROFSYS: ";
cfg.enable_fork = true;
cfg.enable_launcher = true;
cfg.show_sample_flag = true;
return cfg;
}
tool_config
make_sample_config()
{
tool_config cfg{};
cfg.mode = rocprofsys::common_utils::tool_mode::sample;
cfg.tool_name = "sample";
cfg.version_name = "rocprof-sys-sample";
cfg.summary = "Call-stack sampling profiler for applications without binary "
"instrumentation.";
cfg.workflow = R"(PROFILING WORKFLOW:
1. Profile: rocprof-sys-sample --preset=balanced -- ./app
2. Analyze: cat rocprof-sys-output/wall_clock.txt
3. Visualize: Open rocprof-sys-output/perfetto-trace.proto in ui.perfetto.dev)";
cfg.force_sampling = true;
cfg.disable_cputime_on_realtime_only = true;
cfg.deprecated_flags = {
{ "--cputime", "--sample-cputime" },
{ "--realtime", "--sample-realtime" },
{ "--freq", "--sampling-freq" },
};
return cfg;
}
std::string
replace_all(std::string str, std::string_view from, std::string_view replacement)
{
auto pos = str.find(from);
while(pos != std::string::npos)
{
str.replace(pos, from.size(), replacement);
pos = str.find(from, pos + replacement.size());
}
return str;
}
auto
toggle_suppression(std::tuple<bool, bool> incoming)
{
auto previous =
std::make_tuple(settings::suppress_config(), settings::suppress_parsing());
std::tie(settings::suppress_config(), settings::suppress_parsing()) = incoming;
return previous;
}
// Suppresses timemory env-parsing during static init; restored in parse_args.
//
// Why a global: timemory reads ROCPROFSYS_CONFIG_FILE / settings during its
// own translation-unit init, which fires before main(). We must beat that
// init by setting suppress_{config,parsing} as early as possible. Moving this
// into run() is too late.
//
// Static-init order safety: tim::settings::suppress_{config,parsing}() are
// function-local statics inside timemory, so accessing them here triggers
// their construction — no order-of-init UB across translation units.
//
// Captured value holds the prior settings so they can be restored once we
// have control inside main() and can decide when parsing should run.
const auto pre_main_suppression_guard = toggle_suppression({ true, true });
bool
needs_full_parse(int argc, char** argv)
{
for(int arg_idx = 1; arg_idx < argc; ++arg_idx)
{
if(argv[arg_idx] == nullptr) continue;
auto arg = std::string_view{ argv[arg_idx] };
if(arg == "--" || arg == "-?" || arg == "-h" || arg == "--help" ||
arg == "--version" || arg == "--export-config" ||
arg.find("--export-config=") == 0 || arg == "--list-presets" ||
arg == "--explain" || arg.find("--explain=") == 0)
{
return true;
}
}
return argc > 1 && argv[1] != nullptr && std::string_view{ argv[1] }.find('-') == 0;
}
bool
help_requested(const parser_t& parser, int argc, char** argv)
{
constexpr std::array<std::string_view, 3> help_args{ "-h", "--help", "-?" };
if(parser.exists("help") || argc == 1) return true;
if(argc <= 1 || argv[1] == nullptr) return false;
return std::find(help_args.begin(), help_args.end(), std::string_view{ argv[1] }) !=
help_args.end();
}
class tool_runner
{
public:
tool_runner(int argc_, char** argv_, tool_config cfg)
: argc{ argc_ }
, argv{ argv_ }
, config{ std::move(cfg) }
{}
[[nodiscard]] int run();
private:
void print_usage() const;
void update_verbose_from_env();
void get_initial_environment();
void prepare_command(const char* exe);
void prepare_environment();
void parse_command_fast_path();
void configure_parser(parser_t& parser);
std::optional<int> apply_post_parse(parser_t& parser);
std::optional<int> do_full_parse();
std::optional<int> parse_args();
std::string build_description() const;
int argc;
char** argv;
tool_config config;
parser_data_t data{};
rocprofsys::common_utils::domain_flag_state domain_state{};
};
std::string
tool_runner::build_description() const
{
auto cmd = std::string{ "rocprof-sys-" }.append(config.tool_name);
auto desc = replace_all(
R"(
@SUMMARY@
QUICK REFERENCE:
Presets: --preset=balanced (default), --preset=profile-only, --preset=trace-hpc, --preset=workload-trace
Domains: --gpu, --rocm, --cpu, --parallel (composable with presets)
Output: Results saved to rocprof-sys-output/ directory
Visualize: Open perfetto-trace.proto in https://ui.perfetto.dev
EXAMPLES:
Quick Start:
@CMD@ --preset=balanced -- ./myapp
Workload-Specific Presets:
@CMD@ --preset=trace-hpc -- ./hpc_app # HPC/MPI/OpenMP
@CMD@ --preset=workload-trace -- python train.py # AI/ML/GPU workloads
@CMD@ --preset=profile-only -- ./myapp # Minimal overhead
Domain Flags (composable):
@CMD@ --gpu -- ./myapp # GPU metrics
@CMD@ --preset=balanced --gpu=temp,power -- ./app # Preset + specific GPU metrics
@CMD@ --rocm=hip,kernel --cpu=100 -- ./app # ROCm APIs + CPU sampling
Custom Configuration File:
@CMD@ --preset=./my-config.json -- ./myapp
Export Configuration:
@CMD@ --preset=balanced --gpu --export-config > my-config.json
@WORKFLOW@
)",
"@CMD@", cmd);
desc = replace_all(std::move(desc), "@SUMMARY@", config.summary);
desc = replace_all(std::move(desc), "@WORKFLOW@", config.workflow);
return desc;
}
void
tool_runner::print_usage() const
{
std::cerr << fatal_color() << "Usage: " << argv[0] << " [OPTIONS] -- <COMMAND> <ARGS>"
<< reset_color() << '\n';
}
void
tool_runner::update_verbose_from_env()
{
const auto* log_level = std::getenv(env_vars::LOG_LEVEL);
if(log_level != nullptr) data.out.verbose = env_vars::log_level_to_verbose(log_level);
}
void
tool_runner::get_initial_environment()
{
// Single-threaded assumption: this runs from main() before any worker thread
// is spawned, so iterating `environ` and calling getenv/setenv is safe.
if(environ != nullptr)
{
for(int idx = 0; environ[idx] != nullptr; ++idx)
{
data.env.initial.emplace(environ[idx]);
data.env.current.emplace_back(environ[idx]);
}
}
auto libexec_path = path::realpath(path::get_internal_script_path());
if(!libexec_path.empty()) data.env.set(env_vars::SCRIPT_PATH, libexec_path);
update_verbose_from_env();
if(auto llvm_dir = rocprofsys::common::discover_llvm_libdir_for_ompt();
!llvm_dir.empty())
{
data.env.set("LD_LIBRARY_PATH", llvm_dir, update_mode::APPEND);
// Also mutate the live process env: any dlopen() that happens before
// execvpe (e.g. OMPT runtime discovery) reads the real environ, not
// data.env.current.
const auto* current_ld = std::getenv("LD_LIBRARY_PATH");
const auto new_ld =
(current_ld != nullptr) ? (llvm_dir + ":" + current_ld) : llvm_dir;
setenv("LD_LIBRARY_PATH", new_ld.c_str(), 1);
}
if(config.force_sampling)
{
auto mode = getenv_string(env_vars::MODE, "sampling");
// Bool value flows through update_env's to_env_string(bool) overload,
// becoming "true"/"false" in the env string.
data.env.set(env_vars::USE_SAMPLING, (mode != "causal"));
}
}
void
tool_runner::prepare_command(const char* exe)
{
if(data.out.launcher.empty()) return;
bool injected = false;
std::vector<std::string> new_argv;
new_argv.reserve(data.out.command.size() + LAUNCHER_INJECT_SLOTS);
for(const auto& arg : data.out.command)
{
if(!injected &&
basename_of(arg).find(data.out.launcher) != std::string_view::npos)
{
new_argv.emplace_back(exe);
new_argv.emplace_back("--");
injected = true;
}
new_argv.emplace_back(arg);
}
if(!injected)
{
throw std::runtime_error(fmt::format(
R"(Unable to match launcher "{}" to any arguments on the command line: "{}")",
data.out.launcher, fmt::join(data.out.command, " ")));
}
data.out.command = std::move(new_argv);
}
void
tool_runner::prepare_environment()
{
// launcher mode re-injects LD_PRELOAD itself, so skip it here
if(!config.enable_launcher || data.out.launcher.empty())
{
rocprofsys::argparse::add_ld_preload(data);
rocprofsys::argparse::add_ld_library_path(data);
}
rocprofsys::argparse::add_torch_library_path(data);
rocprofsys::common::consolidate_env_entries(data.env.current);
}
void
tool_runner::parse_command_fast_path()
{
toggle_suppression(pre_main_suppression_guard);
rocprofsys::argparse::init_parser(data);
signals::disable_signal_detection(signals::signal_settings::get_enabled());
bool past_separator = false;
for(int arg_idx = 1; arg_idx < argc; ++arg_idx)
{
if(argv[arg_idx] == nullptr) continue;
if(past_separator)
data.out.command.emplace_back(argv[arg_idx]);
else if(std::string_view{ argv[arg_idx] } == "--")
past_separator = true;
}
}
void
tool_runner::configure_parser(parser_t& parser)
{
parser.on_error([](parser_t&, const parser_err_t& err) {
// Throw rather than exit() so RAII destructors run (parse_data, parser, args).
// Caught at the top of run() and converted to EXIT_FAILURE.
throw std::runtime_error(err.what());
});
parser.enable_help().count(-1).min_count(0).max_count(1).dtype("topic");
parser.enable_version(std::string{ config.version_name },
ROCPROFSYS_ARGPARSE_VERSION_INFO);
if(auto cols = terminal_columns(); cols > parser.get_help_width() + HELP_PADDING)
parser.set_description_width(
std::min<int>(cols - parser.get_help_width() - HELP_PADDING, MAX_DESC_WIDTH));
data.reg.processed_groups.emplace("causal");
if(!config.show_sample_flag) data.reg.processed_environs.emplace("sampling");
if(!config.enable_launcher) data.reg.processed_environs.emplace("launcher");
rocprofsys::argparse::add_core_arguments(parser, data);
rocprofsys::argparse::add_extended_arguments(parser, data);
rocprofsys::common_utils::register_preset_and_domain_arguments(
parser, config.tool_name, domain_state,
[this](std::string_view key, std::string_view val) {
data.env.set(key, std::string{ val });
});
if(config.enable_fork)
{
parser.start_group("EXECUTION OPTIONS", "");
parser.add_argument({ "--fork" }, "Execute via fork + execvpe instead of execvpe")
.min_count(0)
.max_count(1)
.dtype("boolean")
.action([this](parser_t& parser_ref) {
data.out.fork_exec = parser_ref.get<bool>("fork");
});
}
}
std::optional<int>
tool_runner::apply_post_parse(parser_t& parser)
{
if(config.disable_cputime_on_realtime_only)
{
if(parser.exists("sample-realtime") && !parser.exists("sample-cputime"))
data.env.set(env_vars::SAMPLING_CPUTIME, false);
}
if(parser.exists("profile") && parser.exists("flat-profile"))
throw std::runtime_error(
"Error! '--profile' argument conflicts with '--flat-profile' argument");
if(domain_state.export_config_requested)
{
rocprofsys::common_utils::export_config(
data.env.current, data.env.initial, domain_state.active_preset_name,
config.tool_name, domain_state.export_config_file);
return EXIT_SUCCESS;
}
rocprofsys::common_utils::run_post_parse_validation(config.tool_name, domain_state,
data.out.verbose);
return std::nullopt;
}
std::optional<int>
tool_runner::do_full_parse()
{
toggle_suppression(pre_main_suppression_guard);
rocprofsys::argparse::init_parser(data);
signals::disable_signal_detection(signals::signal_settings::get_enabled());
auto parser = parser_t{ std::string{ basename_of(argv[0]) }, build_description() };
configure_parser(parser);
auto args = rocprofsys::common_utils::translate_arguments(
argc, argv, domain_state.registry, config.deprecated_flags);
data.out.command = std::move(args.command);
auto parse_err =
parser.parse_args(static_cast<int>(args.argv_ptrs.size()), args.argv_ptrs.data());
if(help_requested(parser, argc, argv))
return rocprofsys::common_utils::dispatch_help(parser, config.tool_name,
EXIT_SUCCESS);
if(parse_err) throw std::runtime_error(parse_err.what());
if(domain_state.early_exit) return domain_state.early_exit;
monochrome_flag().store(data.out.monochrome, std::memory_order_relaxed);
// Keep timemory's own logger in sync — its background paths still use its color.
tim::log::monochrome() = data.out.monochrome;
return apply_post_parse(parser);
}
std::optional<int>
tool_runner::parse_args()
{
get_initial_environment();
if(!needs_full_parse(argc, argv))
{
parse_command_fast_path();
return std::nullopt;
}
return do_full_parse();
}
int
tool_runner::run()
try
{
if(argc == 1)
{
print_usage();
return EXIT_FAILURE;
}
if(auto exit_code = parse_args()) return *exit_code;
if(config.enable_launcher) prepare_command(argv[0]);
prepare_environment();
if(data.out.command.empty())
{
print_usage();
return EXIT_FAILURE;
}
update_verbose_from_env();
const auto verbose = data.out.verbose;
if(verbose >= 0)
utils::print_environment(data.env.current, data.env.updated, verbose >= 1,
config.output_prefix);
if(verbose >= 1) utils::print_command(data.out.command, config.output_prefix);
auto argv_ptrs = utils::to_c_argv(data.out.command);
auto envp_ptrs = utils::to_c_argv(data.env.current);
if(data.out.fork_exec)
{
auto pid = fork();
if(pid < 0)
{
std::perror("fork");
return EXIT_FAILURE;
}
if(pid == 0)
{
execvpe(argv_ptrs.front(), argv_ptrs.data(), envp_ptrs.data());
// execvpe only returns on failure; use _exit to skip parent atexit handlers
std::perror("execvpe");
_exit(EXEC_FAILURE_STATUS);
}
auto status = rocprofsys::mproc::wait_pid(pid);
auto exit_code = rocprofsys::mproc::diagnose_status(pid, status);
if(exit_code != 0 && data.out.verbose >= 0)
{
std::fprintf(stderr,
"[rocprof-sys][fatal] process %i exiting with non-zero exit "
"code: %i\n",
pid, exit_code);
}
else if(data.out.verbose >= 2)
{
std::fprintf(stderr,
"[rocprof-sys][fatal] rocprof-sys run in process %i completed. "
"exit code: %i\n",
pid, exit_code);
}
return exit_code;
}
execvpe(argv_ptrs.front(), argv_ptrs.data(), envp_ptrs.data());
std::perror("execvpe");
return EXEC_FAILURE_STATUS;
} catch(const std::exception& ex)
{
std::cerr << "[rocprof-sys] error: " << ex.what() << '\n';
return EXIT_FAILURE;
}
} // namespace
namespace rocprofsys::common_utils
{
int
run_tool(int argc, char** argv, tool_mode mode)
{
auto config = (mode == tool_mode::sample) ? make_sample_config() : make_run_config();
return tool_runner{ argc, argv, std::move(config) }.run();
}
} // namespace rocprofsys::common_utils