From a929eeab6d1c57804004242b162e68a910822123 Mon Sep 17 00:00:00 2001 From: resserops Date: Thu, 4 Jun 2026 15:25:00 +0800 Subject: [PATCH] feat(profiling/tool): support timeline resolution scaling and range slicing in monitor report --- module/profiling/tool/monitor/monitor.cpp | 263 +++++++++++++++++++--- 1 file changed, 235 insertions(+), 28 deletions(-) diff --git a/module/profiling/tool/monitor/monitor.cpp b/module/profiling/tool/monitor/monitor.cpp index e361391..d55e49b 100644 --- a/module/profiling/tool/monitor/monitor.cpp +++ b/module/profiling/tool/monitor/monitor.cpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include #include @@ -39,14 +40,22 @@ inline struct Args { struct { pid_t pid{}; double itv{}; - } measure; + } measure{}; struct { - std::vector input; // 可能是glob pattern - std::vector dir; - // 根据dir和input形成最终的文件列表 - std::vector valid_input; - } report; + std::vector input; // 可能是glob pattern + std::vector dir; // 搜索glob pattern的根目录 + std::vector valid_input; // 根据dir和input形成最终的文件列表 + + std::size_t resolution{}; // 横轴显示几个时间点 + struct { + // 从TOTAL_START_TIME开始的duration + std::chrono::system_clock::duration start{0}; + std::chrono::system_clock::duration end{std::chrono::system_clock::duration::max()}; + } range{}; + + bool show{}; + } report{}; } ARGS; // 测量全局变量 @@ -117,6 +126,22 @@ std::string MakeDataRow(const std::vector &values) { return res; } +std::atomic STOP{false}; +void SignalHandler(int sig) { + STOP.store(true, std::memory_order_relaxed); + std::signal(sig, SIG_DFL); +} + +void RegisterSignalHandler() { + struct sigaction act {}; + act.sa_handler = SignalHandler; + sigemptyset(&act.sa_mask); + act.sa_flags = 0; + + sigaction(SIGINT, &act, nullptr); // Ctrl+C + sigaction(SIGTERM, &act, nullptr); // kill +} + void Measure() { std::cout << std::fixed << std::setprecision(2); std::cout << "Pid: " << getpid() << std::endl; @@ -127,6 +152,9 @@ void Measure() { // itv转换为毫秒定点数 std::chrono::milliseconds itv{static_cast(1000 * ARGS.measure.itv)}; + // 注册信号 + RegisterSignalHandler(); + // 系统时钟盖时间戳,单调时钟算时间差 auto system_now{std::chrono::system_clock::now()}; oops::CpuTimer cpu_timer(ARGS.measure.pid); // 用于测量cpu时间 @@ -139,7 +167,7 @@ void Measure() { std::cout << MakeHeaderRow() << std::endl; std::vector values(oops::ToUnderlying(Metrics::COUNT)); - while (ProcExist(ARGS.measure.pid)) { + while (!STOP.load(std::memory_order_relaxed) && ProcExist(ARGS.measure.pid)) { std::this_thread::sleep_until(next_time); // 根据选项配置完成测量 @@ -173,13 +201,31 @@ struct MetricDataEntry { struct RawDataEntry { RawDataEntry(std::string s) : raw_file_name{std::move(s)} {} + std::size_t MetricDataSize() const { + if (metric_data_table.empty()) { + return 0; + } + return metric_data_table.front().values.size(); + } + + auto EndTime() const { + return start_time + std::chrono::duration_cast( + std::chrono::duration{MetricDataSize() * interval}); + } + + auto Duration() const { return EndTime() - start_time; } + std::string raw_file_name; pid_t pid{}; - double interval{}; + double interval{}; // TODO(resserops): 必须大于0,parse时增加校验 std::chrono::system_clock::time_point start_time; std::vector metric_data_table; + + // 计算得到 + std::size_t downsample_rate{}; // 将原始数据的N个点合并为1个点 }; +inline std::chrono::system_clock::time_point TOTAL_START_TIME{}; inline std::vector RAW_DATA_TABLE; // 报告功能 @@ -217,7 +263,7 @@ void SearchValidInput() { } else { assert(input_path.is_relative()); for (const auto &dir : ARGS.report.dir) { - assert(!fs::is_directory(dir)); + assert(fs::is_directory(dir)); if (input.find_first_of("*?") != std::string::npos) { // glob相对路径,那么在dir中递归查找glob patterns.push_back((dir / "**" / input_path).string()); @@ -349,8 +395,8 @@ void ParseRawData(std::istream &is, RawDataEntry &entry) { break; } } - assert(step == Step::DATA_ROW); } + assert(step == Step::DATA_ROW); } void ParseRawData(const fs::path &path, RawDataEntry &entry) { @@ -358,6 +404,71 @@ void ParseRawData(const fs::path &path, RawDataEntry &entry) { ParseRawData(ifs, entry); } +void AlignTimeline() { + // 计算所有raw data的公共start time + auto f_min_start = [](const auto &lhs, const auto &rhs) { return lhs.start_time < rhs.start_time; }; + TOTAL_START_TIME = std::min_element(RAW_DATA_TABLE.begin(), RAW_DATA_TABLE.end(), f_min_start)->start_time; + + // 计算timeline range截断 + for (auto &r_entry : RAW_DATA_TABLE) { + assert(r_entry.interval > .0); + std::size_t size{r_entry.MetricDataSize()}; + if (size == 0) { + continue; + } + + // 计算从TOTAL_START_TIME到当前raw data左右边界的duration + auto raw_l{r_entry.start_time - TOTAL_START_TIME}; + auto raw_r{r_entry.EndTime() - TOTAL_START_TIME}; + assert(raw_l.count() >= 0); + assert(raw_r >= raw_l); + + // 与range取交集,TOTAL_START_TIME到range左右边界的duration + auto range_l{std::max(raw_l, ARGS.report.range.start)}; + auto range_r{std::min(raw_r, ARGS.report.range.end)}; + assert(range_l.count() >= 0); + assert(range_r >= range_l); + + // 减得raw data到range左右边界的duration,计算range在metric data中的左右下标 + double range_l_fidx{std::ceil(std::chrono::duration{range_l - raw_l}.count()) / r_entry.interval}; + double range_r_fidx{std::ceil(std::chrono::duration{range_r - raw_l}.count()) / r_entry.interval}; + + std::cout << "lidx: " << range_l_fidx << std::endl; + std::cout << "ridx: " << range_r_fidx << std::endl; + + // 计算range对应的下标,左闭右开 + std::size_t range_l_idx{std::max(range_l_fidx, 0)}; + std::size_t range_r_idx{std::min(range_r_fidx, size)}; + assert(range_l_idx <= range_r_idx); + + // range切片 + r_entry.start_time += std::chrono::duration_cast( + std::chrono::duration{range_l_idx * r_entry.interval}); + for (auto &metric : r_entry.metric_data_table) { + if (range_l_idx > 0) { + metric.values.erase(metric.values.begin(), metric.values.begin() + range_l_idx); + } + metric.values.resize(range_r_idx - range_l_idx); + } + } + + // 重新计算range切片后的公共start time和end time,获取数据区间总长,用于计算向下采样倍率 + auto total_range_start_time{ + std::min_element(RAW_DATA_TABLE.begin(), RAW_DATA_TABLE.end(), f_min_start)->start_time}; + auto f_max_end = [](const auto &lhs, const auto &rhs) { return lhs.EndTime() < rhs.EndTime(); }; + auto total_range_end_time{std::max_element(RAW_DATA_TABLE.begin(), RAW_DATA_TABLE.end(), f_max_end)->EndTime()}; + auto total_range_duration{total_range_end_time - total_range_start_time}; + + // 根据时间轴分辨率,计算每份raw data的向下采样倍率 + for (auto &r_entry : RAW_DATA_TABLE) { + // 每downsample_rate个点合并为1个点,如何合并由每项指标自己定义 + std::size_t range_resolution{ + ARGS.report.resolution * r_entry.Duration().count() / total_range_duration.count()}; + assert(range_resolution > 0); + r_entry.downsample_rate = r_entry.MetricDataSize() / range_resolution; + } +} + void PlotCpu() { if (RAW_DATA_TABLE.empty()) { return; @@ -378,10 +489,29 @@ void PlotCpu() { return; } - // 最早start_time - std::chrono::system_clock::time_point start_time{std::chrono::system_clock::time_point::max()}; - for (const auto &p : valid_idxs) { - start_time = std::min(start_time, RAW_DATA_TABLE[p.first].start_time); + // 根据downsample_rate,合并数据点,cpu利用率采取平均值 + for (const auto &[r_idx, m_idx] : valid_idxs) { + auto &r_entry{RAW_DATA_TABLE[r_idx]}; + if (r_entry.downsample_rate <= 1) { + // 不需要合并 + continue; + } + + std::cout << r_entry.downsample_rate << std::endl; + + // 计算合并后的新数据点数 + auto &m_entry{r_entry.metric_data_table[m_idx]}; + std::size_t new_i{0}; + for (std::size_t i{0}; i + r_entry.downsample_rate - 1 < m_entry.values.size(); i += r_entry.downsample_rate) { + double sum{}; + for (std::size_t j{i}; j < i + r_entry.downsample_rate; j++) { + sum += m_entry.values[j]; + } + m_entry.values[new_i++] = sum / r_entry.downsample_rate; + std::cout << "avg: " << sum / r_entry.downsample_rate << std::endl; + } + m_entry.values.resize(new_i); + r_entry.interval *= r_entry.downsample_rate; } // 最大数值 @@ -391,6 +521,7 @@ void PlotCpu() { max_value = std::max(max_value, value); } } + std::cout << "max_value: " << max_value << std::endl; py::scoped_interpreter guard{}; // 必须放在try-catch块外侧,否则无法捕获py::error_already_set异常 try { @@ -410,7 +541,7 @@ void PlotCpu() { for (std::size_t i{0}; i < valid_idxs.size(); ++i) { const auto &r_entry{RAW_DATA_TABLE[valid_idxs[i].first]}; const auto &m_entry{r_entry.metric_data_table[valid_idxs[i].second]}; - double offset{std::chrono::duration{r_entry.start_time - start_time}.count()}; + double offset{std::chrono::duration{r_entry.start_time - TOTAL_START_TIME}.count()}; // 构造时间数组,由于offset差异,每个raw data需要单独构造 std::vector times; @@ -425,7 +556,8 @@ void PlotCpu() { py::arg("linewidth") = 1.5); ax.attr("margins")(py::arg("x") = 0); ax.attr("grid")(true, py::arg("linestyle") = "--", py::arg("alpha") = 0.5); - double ylim{std::max(100., std::ceil(max_value / 100) * 100)}; + double ylim{std::max(100., max_value)}; + std::cout << "ylim " << ylim << std::endl; ax.attr("set_ylim")(-ylim / 20, ylim + ylim / 20); // 对齐每个子图y坐标范围 // 绘制每个子图左侧描述raw data的标签 ax.attr("set_ylabel")( @@ -439,7 +571,12 @@ void PlotCpu() { "CPU Utilization Time Series (%)", py::arg("fontsize") = 16, py::arg("fontweight") = "bold"); plt.attr("xlabel")("Time (s)"); plt.attr("tight_layout")(); - plt.attr("show")(); + plt.attr("savefig")("cpu_usage.png", py::arg("dpi") = 100, py::arg("bbox_inches") = "tight"); + + if (ARGS.report.show) { + plt.attr("show")(); + } + plt.attr("close")("all"); } catch (const py::error_already_set &e) { std::cerr << "Error: " << e.what() << std::endl; } } @@ -453,6 +590,9 @@ void Report() { ParseRawData(p, RAW_DATA_TABLE.back()); } + // 对齐时间轴,range裁剪,分辨率缩放 + AlignTimeline(); + // 绘制CPU利用率折线图 PlotCpu(); } @@ -462,22 +602,31 @@ void ParseArgs(int argc, char *argv[]) { argparse::ArgumentParser program{"monitor", "1.0"}; argparse::ArgumentParser measure{"measure"}; + measure.add_description("measure runtime performance metrics"); measure.add_argument("pid").help("target process id to monitor").scan<'i', pid_t>().required(); measure.add_argument("-i", "--itv") .help("sampling interval in secondes (e.g., 0.5 for 500ms)") - .default_value(1.) - .scan<'g', double>(); + .scan<'g', double>() + .default_value(1.); measure.add_argument("-m", "--metric") - .help("comma-separated sequence of metrics to monitor (e.g., cpu[c],memory[m])") + .help("comma-separated sequence of metrics to monitor (supported metrics: cpu[c], memory[m])") .default_value("all"); program.add_subparser(measure); + constexpr std::size_t AUTO_RESOLUTION{960}; argparse::ArgumentParser report{"report"}; report.add_description("process and visualize monitor raw data"); report.add_argument("input").help("comma-separated sequence of raw data file").required(); report.add_argument("-d", "--dir") .help("the root directory to search for files matching the 'input' pattern") .default_value("."); + report.add_argument("--resolution") + .help("number of data points plotted on timeline ('auto', 'max' or an integer)") + .default_value("auto"); + report.add_argument("-r", "--range") + .help("subset range for the plot timeline specified as 'start:end', where boundaries must be real or empty") + .default_value(":"); + report.add_argument("--show").help("display the generated plot in a GUI window").flag(); program.add_subparser(report); try { @@ -524,7 +673,8 @@ void ParseArgs(int argc, char *argv[]) { matched = true; } if (!matched) { - std::cerr << "Warning: ignore unexpected metric " << token << std::endl; + std::cerr << "Error: unexpected metric '" << token << "'" << std::endl; + exit(1); } } if (ENABLED_METRIC_GROUP.none()) { @@ -544,20 +694,77 @@ void ParseArgs(int argc, char *argv[]) { // --dir std::string dir{report.get("--dir")}; - auto tokens{oops::Split(dir, ',')}; - for (auto token : tokens) { + auto dir_tokens{oops::Split(dir, ',')}; + for (auto token : dir_tokens) { fs::path dir_path{token}; if (!fs::is_directory(dir_path)) { - std::cerr << "Warning: \"" << token << "\" in --dir is not a directory. ignored" << std::endl; - continue; + std::cerr << "Error: '" << token << "' in --dir is not a directory" << std::endl; + exit(1); } ARGS.report.dir.emplace_back(token); } if (ARGS.report.dir.empty()) { - std::cerr << "Warning: no valid directories provided in --dir. using default current directory" - << std::endl; - ARGS.report.dir.emplace_back(fs::current_path()); + std::cerr << "Error: no valid directories provided in --dir" << std::endl; + exit(1); + } + + // --resolution + std::string resolution{report.get("--resolution")}; + if (resolution == "auto") { + ARGS.report.resolution = AUTO_RESOLUTION; + } else if (resolution == "max") { + ARGS.report.resolution = std::numeric_limits::max(); + } else { + auto [ptr, errc]{ + std::from_chars(resolution.data(), resolution.data() + resolution.size(), ARGS.report.resolution)}; + if (errc != std::errc{}) { + std::cerr << "Error: unexpected resolution '" << resolution << "'" << std::endl; + exit(1); + } + } + + // --range + std::string range{report.get("--range")}; + auto range_tokens{oops::Split(range, ':').To()}; + std::cout << range << " " << range_tokens.size() << std::endl; + if (range_tokens.size() != 2) { + std::cerr << "Error: unexpected range format '" << range << "'" << std::endl; + exit(1); + } + double range_start{}; + if (!oops::Strip(range_tokens[0]).empty()) { + try { + range_start = std::stod(std::string{range_tokens[0]}); // TODO(resserops): 后续使用fast float库代替 + } catch (...) { + std::cerr << "Error: unexpected range start '" << range_tokens[0] << "'" << std::endl; + exit(1); + } + if (range_start < 0) { + std::cerr << "Error: range start '" << range_start << "' must be >= 0" << std::endl; + exit(1); + } + ARGS.report.range.start = std::chrono::duration_cast( + std::chrono::duration{range_start}); + } + if (!oops::Strip(range_tokens[1]).empty()) { + double range_end{}; + try { + range_end = std::stod(std::string{range_tokens[1]}); // TODO(resserops): 后续使用fast float库代替 + } catch (...) { + std::cerr << "Error: unexpected range end '" << range_tokens[1] << "'" << std::endl; + exit(1); + } + if (range_end < range_start) { + std::cerr << "Error: range end '" << range_end << "' must be >= range start '" << range_start << "'" + << std::endl; + exit(1); + } + ARGS.report.range.end = std::chrono::duration_cast( + std::chrono::duration{range_end}); } + + // --show + ARGS.report.show = report.is_used("--show"); } }