From 45dc6fb33d83890c413ac4971eefa619a65dfa4f Mon Sep 17 00:00:00 2001 From: ThreeMonth03 Date: Tue, 26 May 2026 01:28:03 +0800 Subject: [PATCH] Profile callprofiler with different testcases --- .github/workflows/profiling.yml | 5 + CMakeLists.txt | 2 + Makefile | 24 +- profiling/CMakeLists.txt | 12 + profiling/cprof/CMakeLists.txt | 53 ++++ profiling/cprof/callprofiler_gprof.cpp | 131 +++++++++ profiling/cprof/callprofiler_workload.hpp | 53 ++++ profiling/cprof/generate_workload.py | 318 ++++++++++++++++++++++ profiling/cprof/run.py | 205 ++++++++++++++ 9 files changed, 801 insertions(+), 2 deletions(-) create mode 100644 profiling/CMakeLists.txt create mode 100644 profiling/cprof/CMakeLists.txt create mode 100644 profiling/cprof/callprofiler_gprof.cpp create mode 100644 profiling/cprof/callprofiler_workload.hpp create mode 100644 profiling/cprof/generate_workload.py create mode 100644 profiling/cprof/run.py diff --git a/.github/workflows/profiling.yml b/.github/workflows/profiling.yml index 8843f9400..9ec1944a5 100644 --- a/.github/workflows/profiling.yml +++ b/.github/workflows/profiling.yml @@ -74,6 +74,11 @@ jobs: run: | make pyprof + - name: make cprof + if: runner.os == 'Linux' + run: | + make cprof + send_email_on_failure: needs: [profile] if: ${{ always() && (needs.*.result == 'failure') && github.ref_name == 'master' && github.event.repository.fork == false }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 7e83ec258..c1f68eaa9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -228,6 +228,8 @@ else() endif() endif() +add_subdirectory(profiling) + add_subdirectory(cpp/binary/pymod_modmesh) if(BUILD_QT) add_subdirectory(cpp/binary/pilot) diff --git a/Makefile b/Makefile index d5d870ec9..d550c1bd6 100644 --- a/Makefile +++ b/Makefile @@ -73,6 +73,7 @@ ifneq ($(VERBOSE),) else PYTEST_OPTS ?= endif +GPROF ?= gprof .PHONY: default default: buildext @@ -143,6 +144,25 @@ pyprof: buildext $(PROFFILES) $(WHICH_PYTHON) $${fn} > $${outfn} || exit 1; \ done +.PHONY: cprof +cprof: cmake + @test "$$(uname -s)" = "Linux" || { \ + echo "Error: make cprof is only supported on Linux."; \ + exit 1; \ + } + @command -v $(GPROF) >/dev/null 2>&1 || { \ + echo "Error: '$(GPROF)' not found in PATH."; \ + exit 1; \ + } + cmake --build $(BUILD_PATH) --target callprofiler_gprof VERBOSE=$(VERBOSE) $(MAKE_PARALLEL) + mkdir -p profiling/results + rm -f profiling/results/profile_profiler.output + env $(RUNENV) $(WHICH_PYTHON) $(MODMESH_ROOT)/profiling/cprof/run.py \ + --executable $(MODMESH_ROOT)/$(BUILD_PATH)/profiling/cprof/callprofiler_gprof \ + --gprof $(GPROF) \ + --result-dir $(MODMESH_ROOT)/profiling/results \ + --working-dir $(MODMESH_ROOT)/$(BUILD_PATH)/profiling/cprof + .PHONY: pilot pilot: cmake cmake --build $(BUILD_PATH) --target $@ VERBOSE=$(VERBOSE) $(MAKE_PARALLEL) @@ -198,7 +218,7 @@ AUTOPEP8_OPTS ?= --recursive --max-line-length=79 \ --ignore=E121,E123,E126,E201,E202,E203,E226,E241,E301,E303,E501,W503,W504 \ --exclude=thirdparty,tmp,_deps -CFFILES = $(shell find cpp gtests -type f -name '*.[ch]pp' | sort) +CFFILES = $(shell find cpp gtests profiling -type f -name '*.[ch]pp' | sort) ifeq ($(FORCE_CLANG_FORMAT),inplace) CFCMD ?= $(CLANG_FORMAT) -i else @@ -226,7 +246,7 @@ cformat: $(CFFILES) .PHONY: cinclude cinclude: $(CFFILES) - @if grep -rnE '^[[:space:]]*#[[:space:]]*include[[:space:]]*"' cpp/ gtests/ 2>/dev/null; then \ + @if grep -nE '^[[:space:]]*#[[:space:]]*include[[:space:]]*"' $(CFFILES) 2>/dev/null; then \ echo "Error: use angle brackets for #include, not quotes (see lines above)."; \ exit 1; \ fi diff --git a/profiling/CMakeLists.txt b/profiling/CMakeLists.txt new file mode 100644 index 000000000..146697afa --- /dev/null +++ b/profiling/CMakeLists.txt @@ -0,0 +1,12 @@ +# Copyright (c) 2026, modmesh contributors +# BSD-style license; see COPYING + +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + find_program(GPROF_EXECUTABLE gprof) +endif() + +if(GPROF_EXECUTABLE) + add_subdirectory(cprof) +endif() + +# vim: set ff=unix fenc=utf8 nobomb et sw=4 ts=4 sts=4: diff --git a/profiling/cprof/CMakeLists.txt b/profiling/cprof/CMakeLists.txt new file mode 100644 index 000000000..8b7a5d209 --- /dev/null +++ b/profiling/cprof/CMakeLists.txt @@ -0,0 +1,53 @@ +# Copyright (c) 2026, modmesh contributors +# BSD-style license; see COPYING + +set(CPROF_GENERATED_DIR "${CMAKE_CURRENT_BINARY_DIR}/generated") +set(CPROF_GENERATOR + "${CMAKE_CURRENT_SOURCE_DIR}/generate_workload.py") +set(CPROF_SHARD_COUNT 32) +set(CPROF_GENERATED_SOURCES + "${CPROF_GENERATED_DIR}/callprofiler_workload.cpp" + "${CPROF_GENERATED_DIR}/callprofiler_workload_functions.hpp") + +math(EXPR CPROF_LAST_SHARD "${CPROF_SHARD_COUNT} - 1") +foreach(index RANGE 0 ${CPROF_LAST_SHARD}) + list(APPEND CPROF_GENERATED_SOURCES + "${CPROF_GENERATED_DIR}/callprofiler_workload_${index}.cpp") +endforeach() + +add_custom_command( + OUTPUT ${CPROF_GENERATED_SOURCES} + COMMAND "${PYTHON_EXECUTABLE}" "${CPROF_GENERATOR}" + --output-dir "${CPROF_GENERATED_DIR}" + --shards "${CPROF_SHARD_COUNT}" + DEPENDS "${CPROF_GENERATOR}" + VERBATIM +) + +add_executable( + callprofiler_gprof + callprofiler_gprof.cpp + callprofiler_workload.hpp + ${CPROF_GENERATED_SOURCES} + ${PROJECT_SOURCE_DIR}/cpp/modmesh/toggle/RadixTree.cpp +) + +target_include_directories( + callprofiler_gprof PRIVATE + "${CMAKE_CURRENT_SOURCE_DIR}" + "${CPROF_GENERATED_DIR}" +) + +target_compile_options( + callprofiler_gprof PRIVATE + ${COMMON_COMPILER_OPTIONS} + -pg + -fno-omit-frame-pointer +) + +target_link_options(callprofiler_gprof PRIVATE -pg) +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_options(callprofiler_gprof PRIVATE -no-pie) +endif() + +# vim: set ff=unix fenc=utf8 nobomb et sw=4 ts=4 sts=4: diff --git a/profiling/cprof/callprofiler_gprof.cpp b/profiling/cprof/callprofiler_gprof.cpp new file mode 100644 index 000000000..777633fc4 --- /dev/null +++ b/profiling/cprof/callprofiler_gprof.cpp @@ -0,0 +1,131 @@ +/* + * Copyright (c) 2026, modmesh contributors + * BSD-style license; see COPYING + */ + +#define MODMESH_PROFILE 1 +#include + +#include +#include +#include +#include +#include +#include +#include + +#if defined(__linux__) +#include +#endif + +namespace profiling +{ + +namespace workload = modmesh::profiling; + +using clock_type = std::chrono::steady_clock; +using profiler_type = modmesh::CallProfiler; +using runner_type = void (*)(std::size_t); + +struct case_definition +{ + std::string_view m_label; + runner_type m_runner; +}; + +std::array const case_definitions{{ + {"wide_siblings", &workload::run_wide_siblings}, + {"deep_chain", &workload::run_deep_chain}, + {"balanced_tree", &workload::run_balanced_tree}, + {"hot_name_reuse", &workload::run_hot_name_reuse}, +}}; + +void configure_large_stack() +{ +#if defined(__linux__) + rlimit limit{}; + if (getrlimit(RLIMIT_STACK, &limit) == 0) + { + if (RLIM_INFINITY == limit.rlim_max || limit.rlim_cur < limit.rlim_max) + { + limit.rlim_cur = limit.rlim_max; + static_cast(setrlimit(RLIMIT_STACK, &limit)); + } + } +#endif +} + +template +void run_case(std::string_view label, std::size_t operation_count, std::size_t repeat_count, Runner && runner) +{ + profiler_type & profiler = profiler_type::instance(); + std::chrono::duration elapsed{0.0}; + + for (std::size_t repeat = 0; repeat < repeat_count; ++repeat) + { + profiler.reset(); + + auto const start_time = clock_type::now(); + std::forward(runner)(); + auto const stop_time = clock_type::now(); + + elapsed += stop_time - start_time; + } + + std::cout << "RESULT workload=" << label + << " operations=" << operation_count + << " repeats=" << repeat_count + << " workload_seconds=" << elapsed.count() + << '\n'; + + profiler.reset(); +} + +std::size_t parse_size(char const * value) +{ + return static_cast(std::strtoull(value, nullptr, 10)); +} + +case_definition const * find_case(std::string_view label) +{ + for (case_definition const & definition : case_definitions) + { + if (definition.m_label == label) + { + return &definition; + } + } + return nullptr; +} + +bool run_named_case(std::string_view label, std::size_t size, std::size_t repeat_count) +{ + case_definition const * definition = find_case(label); + if (definition == nullptr) + { + return false; + } + + run_case(definition->m_label, size, repeat_count, [definition, size]() + { definition->m_runner(size); }); + return true; +} + +} /* namespace profiling */ + +int main(int argc, char ** argv) +{ + if (argc == 4) + { + profiling::configure_large_stack(); + bool const completed = profiling::run_named_case( + argv[1], + profiling::parse_size(argv[2]), + profiling::parse_size(argv[3])); + return completed ? 0 : 2; + } + + return 2; +} + +// vim: set ff=unix fenc=utf8 et sw=4 ts=4 sts=4: diff --git a/profiling/cprof/callprofiler_workload.hpp b/profiling/cprof/callprofiler_workload.hpp new file mode 100644 index 000000000..191c43c03 --- /dev/null +++ b/profiling/cprof/callprofiler_workload.hpp @@ -0,0 +1,53 @@ +#pragma once + +/* + * Copyright (c) 2026, modmesh contributors + * BSD-style license; see COPYING + */ + +#ifndef MODMESH_PROFILE +#define MODMESH_PROFILE 1 +#endif + +#include + +#include + +#if defined(__clang__) || defined(__GNUC__) +#define MODMESH_CPROF_NOINLINE __attribute__((noinline)) +#define MODMESH_CPROF_NOINST __attribute__((no_instrument_function)) +#else +#define MODMESH_CPROF_NOINLINE +#define MODMESH_CPROF_NOINST +#endif + +namespace modmesh::profiling +{ + +void run_wide_siblings(std::size_t size); +void run_deep_chain(std::size_t size); +void run_balanced_tree(std::size_t size); +void run_hot_name_reuse(std::size_t size); + +namespace detail +{ + +enum class WorkloadShape +{ + flat, + list, + tree, +}; + +using profile_function_type = void (*)(std::size_t, std::size_t); + +extern WorkloadShape active_shape; +extern std::size_t active_size; + +void call_profile_function(std::size_t index, std::size_t begin, std::size_t end); + +} /* namespace detail */ + +} /* namespace modmesh::profiling */ + +// vim: set ff=unix fenc=utf8 et sw=4 ts=4 sts=4: diff --git a/profiling/cprof/generate_workload.py b/profiling/cprof/generate_workload.py new file mode 100644 index 000000000..277336e25 --- /dev/null +++ b/profiling/cprof/generate_workload.py @@ -0,0 +1,318 @@ +# Copyright (c) 2026, modmesh contributors +# BSD-style license; see COPYING + +import argparse +from dataclasses import dataclass +from pathlib import Path +from textwrap import dedent + + +def block(text): + return dedent(text).strip("\n") + "\n" + + +@dataclass(frozen=True) +class GeneratorConfig: + benchmark_cases: tuple[tuple[int, int], ...] = ( + (100, 10_000), + (1_000, 1_000), + (10_000, 5), + (50_000, 1), + ) + operation_counts: tuple[int, ...] = (100, 1_000, 10_000, 50_000) + max_call_count: int = 50_000 + repeated_visit_count: int = 100 + default_shard_count: int = 32 + namespace: str = "modmesh::profiling" + detail_namespace: str = "modmesh::profiling::detail" + prologue: str = """\ +/* + * Generated by profiling/cprof/generate_workload.py. + * Do not edit manually. + */ + +""" + + +class GeneratedFileWriter: + def __init__(self, output_dir, config): + self.output_dir = output_dir + self.config = config + + @staticmethod + def function_name(index): + return f"profile_empty_{index:06d}" + + def write_generated(self, filename, content): + path = self.output_dir / filename + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(self.config.prologue + content, encoding="utf-8") + + +class FunctionHeaderGenerator(GeneratedFileWriter): + + def generate(self): + declarations = "".join( + f"void {self.function_name(index)}" + "(std::size_t begin, std::size_t end);\n" + for index in range(self.config.max_call_count) + ) + detail_namespace = self.config.detail_namespace + self.write_generated( + "callprofiler_workload_functions.hpp", + block(f""" + #pragma once + + #include + + namespace {detail_namespace} + {{ + """) + + declarations + + block(f""" + }} /* namespace {detail_namespace} */ + """), + ) + + +class RunnerSourceGenerator(GeneratedFileWriter): + public_template = block(""" + namespace + { + void validate_size(std::size_t size) + { + if (size == 0 || @max_call_count@ < size) + { + throw std::invalid_argument("unsupported cprof size"); + } + } + void set_shape(detail::WorkloadShape shape, std::size_t size) + { + validate_size(size); + detail::active_shape = shape; + detail::active_size = size; + } + } /* namespace */ + MODMESH_CPROF_NOINLINE void run_wide_siblings(std::size_t size) + { + MODMESH_PROFILE_FUNCTION(); + set_shape(detail::WorkloadShape::flat, size); + for (std::size_t index = 0; index < size; ++index) + { + detail::call_profile_function(index, 0, 0); + } + } + MODMESH_CPROF_NOINLINE void run_deep_chain(std::size_t size) + { + MODMESH_PROFILE_FUNCTION(); + set_shape(detail::WorkloadShape::list, size); + detail::call_profile_function(0, 0, 0); + } + MODMESH_CPROF_NOINLINE void run_balanced_tree(std::size_t size) + { + MODMESH_PROFILE_FUNCTION(); + set_shape(detail::WorkloadShape::tree, size); + std::size_t const root = size / 2; + detail::call_profile_function(root, 0, size); + } + MODMESH_CPROF_NOINLINE void run_hot_name_reuse(std::size_t size) + { + MODMESH_PROFILE_FUNCTION(); + validate_size(size); + std::size_t const width = size / @repeated_visit_count@; + detail::active_shape = detail::WorkloadShape::flat; + detail::active_size = width; + @repeat_loop@ + { + for (std::size_t index = 0; index < width; ++index) + { + detail::call_profile_function(index, 0, 0); + } + } + } + """) + + def function_table(self): + entries = "".join( + f" &{self.function_name(index)},\n" + for index in range(self.config.max_call_count) + ) + return ( + "std::array const profile_functions = " + "{{\n" + f"{entries}" + "}};\n\n" + "void call_profile_function(std::size_t index, " + "std::size_t begin, std::size_t end)\n" + "{\n" + " profile_functions[index](begin, end);\n" + "}\n" + ) + + def generate(self): + detail_section = ( + "WorkloadShape active_shape = WorkloadShape::flat;\n" + "std::size_t active_size = 0;\n\n" + f"{self.function_table()}" + ) + public_section = ( + self.public_template + .replace("@max_call_count@", str(self.config.max_call_count)) + .replace( + "@repeat_loop@", + "for (std::size_t repeat = 0; " + f"repeat < {self.config.repeated_visit_count}; ++repeat)", + ) + .replace( + "@repeated_visit_count@", + str(self.config.repeated_visit_count), + ) + ) + detail_namespace = self.config.detail_namespace + namespace = self.config.namespace + self.write_generated( + "callprofiler_workload.cpp", + block(f""" + #include + #include + + #include + #include + + namespace {detail_namespace} + {{ + """) + + detail_section + + block(f""" + }} /* namespace {detail_namespace} */ + + namespace {namespace} + {{ + """) + + public_section + + block(f""" + }} /* namespace {namespace} */ + """), + ) + + +class ShardSourceGenerator(GeneratedFileWriter): + function_template = block(""" + @signature@ + { + MODMESH_PROFILE_FUNCTION(); + if (WorkloadShape::list == active_shape) + { + constexpr std::size_t next = @next@; + if (next < active_size) + { + call_profile_function(next, 0, 0); + } + } + else if (WorkloadShape::tree == active_shape) + { + std::size_t const middle = begin + (end - begin) / 2; + if (begin < middle) + { + std::size_t const left_middle = begin + (middle - begin) / 2; + call_profile_function(left_middle, begin, middle); + } + std::size_t const right_begin = middle + 1; + if (right_begin < end) + { + @right_middle@ + call_profile_function(right_middle, right_begin, end); + } + } + } + """) + + def function_definition(self, index): + signature = ( + "MODMESH_CPROF_NOINLINE MODMESH_CPROF_NOINST void " + f"{self.function_name(index)}" + "(std::size_t begin, std::size_t end)" + ) + return ( + self.function_template + .replace("@signature@", signature) + .replace("@next@", str(index + 1)) + "\n" + ).replace( + "@right_middle@", + "std::size_t const right_middle = " + "right_begin + (end - right_begin) / 2;", + ) + + def shard_indices(self, shard_count): + return [ + list(range(shard_index, self.config.max_call_count, shard_count)) + for shard_index in range(shard_count) + ] + + def generate(self, shard_count): + detail_namespace = self.config.detail_namespace + for shard_index, indices in enumerate(self.shard_indices(shard_count)): + shard_content = "".join( + self.function_definition(index) for index in indices + ) + self.write_generated( + f"callprofiler_workload_{shard_index}.cpp", + block(f""" + #include + + namespace {detail_namespace} + {{ + """) + + shard_content + + block(f""" + }} /* namespace {detail_namespace} */ + """), + ) + + +class WorkloadGenerator: + + def __init__(self, output_dir, config): + self.output_dir = output_dir + self.config = config + + def generate(self, shard_count): + self.validate(shard_count) + FunctionHeaderGenerator(self.output_dir, self.config).generate() + RunnerSourceGenerator(self.output_dir, self.config).generate() + ShardSourceGenerator(self.output_dir, self.config).generate( + shard_count + ) + + def validate(self, shard_count): + if shard_count < 1: + raise ValueError("--shards must be positive") + if any( + size % self.config.repeated_visit_count + for size in self.config.operation_counts + ): + raise ValueError("case sizes must divide repeated visits") + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--output-dir", type=Path, required=True) + parser.add_argument( + "--shards", + type=int, + default=GeneratorConfig().default_shard_count, + ) + return parser.parse_args() + + +def main(): + args = parse_args() + generator = WorkloadGenerator(args.output_dir, GeneratorConfig()) + generator.generate(args.shards) + + +if __name__ == "__main__": + main() + +# vim: set ff=unix fenc=utf8 et sw=4 ts=4 sts=4: diff --git a/profiling/cprof/run.py b/profiling/cprof/run.py new file mode 100644 index 000000000..54dee4e2e --- /dev/null +++ b/profiling/cprof/run.py @@ -0,0 +1,205 @@ +# Copyright (c) 2026, modmesh contributors +# BSD-style license; see COPYING + +import argparse +import subprocess +from dataclasses import dataclass +from pathlib import Path + +from generate_workload import GeneratorConfig + + +@dataclass(frozen=True) +class CaseResult: + workload: str + operations: int + repeats: int + workload_seconds: float + + +@dataclass(frozen=True) +class CaseReport: + cpp_stat: CaseResult + gprof_stat: tuple[str, ...] + + +def parse_result(stdout): + values = dict( + word.split("=", 1) for word in stdout.split() if "=" in word + ) + return CaseResult( + values["workload"], + int(values["operations"]), + int(values["repeats"]), + float(values["workload_seconds"]), + ) + + +def format_summary_row(cpp_stat): + return ( + f"| {cpp_stat.operations} | {cpp_stat.repeats} | " + f"{cpp_stat.workload_seconds:.6E} |" + ) + + +def format_gprof_report(report): + cpp_stat = report.cpp_stat + gprof_stat = "\n".join(report.gprof_stat) + return ( + "### gprof top 5: " + f"operations `{cpp_stat.operations}`, " + f"repeats `{cpp_stat.repeats}`\n\n" + "```text\n" + f"{gprof_stat}\n" + "```" + ) + + +def format_workload_report(workload, reports): + summary = "\n".join( + format_summary_row(report.cpp_stat) for report in reports + ) + gprof_reports = "\n\n".join( + format_gprof_report(report) for report in reports + ) + return ( + f"## {workload}\n\n" + "| operations | repeats | workload seconds |\n" + "| ---------- | ------- | ---------------- |\n" + f"{summary}\n\n" + f"{gprof_reports}\n" + ) + + +class CprofRunner: + benchmark_cases = GeneratorConfig().benchmark_cases + excluded_gprof_functions = ( + "_init", + "_fini", + "modmesh::CallProfiler::reset(", + ) + gprof_function_limit = 5 + workloads = ( + "wide_siblings", + "deep_chain", + "balanced_tree", + "hot_name_reuse", + ) + + def __init__(self, executable, gprof, result_dir, working_dir): + self.executable = str(executable) + self.gprof = str(gprof) + self.result_dir = result_dir + self.working_dir = working_dir + + def run(self): + self.write_report(self.run_cases()) + + def run_command(self, command): + try: + completed = subprocess.run( + command, + cwd=self.working_dir, + check=True, + text=True, + capture_output=True, + ) + except subprocess.CalledProcessError as error: + command_text = " ".join(str(item) for item in command) + message = f"command failed: {command_text}\n{error.stderr}" + raise RuntimeError(message) from error + + return completed.stdout + + def run_case(self, workload, operations, repeats): + gmon_file = self.working_dir / "gmon.out" + gmon_file.unlink(missing_ok=True) + + stdout = self.run_command([ + self.executable, + workload, + str(operations), + str(repeats), + ]) + + if not gmon_file.is_file(): + raise RuntimeError(f"{gmon_file} was not generated") + + return parse_result(stdout), gmon_file + + def run_gprof(self, gmon_file): + stdout = self.run_command( + [self.gprof, "-p", "-b", self.executable, str(gmon_file)] + ) + gprof_stat = [] + lines = iter(stdout.splitlines()) + + for line in lines: + if line.startswith(" %"): + gprof_stat.append(line) + next_line = next(lines, None) + if next_line is not None: + gprof_stat.append(next_line) + break + + for line in lines: + if not line.strip(): + continue + + if any(name in line for name in self.excluded_gprof_functions): + continue + + gprof_stat.append(line) + if len(gprof_stat) == self.gprof_function_limit + 2: + break + + return tuple(gprof_stat) + + def run_cases(self): + reports = [] + + for workload in self.workloads: + workload_reports = [] + for operations, repeats in self.benchmark_cases: + cpp_stat, gmon_file = self.run_case( + workload, + operations, + repeats, + ) + gprof_stat = self.run_gprof(gmon_file) + workload_reports.append(CaseReport(cpp_stat, gprof_stat)) + reports.append(format_workload_report(workload, workload_reports)) + + return reports + + def write_report(self, reports): + self.result_dir.mkdir(parents=True, exist_ok=True) + report = self.result_dir / "profile_profiler.output" + content = "# CallProfiler gprof\n\n" + "\n\n".join(reports) + report.write_text(content + "\n", encoding="utf-8") + + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--executable", type=Path, required=True) + parser.add_argument("--gprof", type=Path, required=True) + parser.add_argument("--result-dir", type=Path, required=True) + parser.add_argument("--working-dir", type=Path, required=True) + return parser.parse_args() + + +def main(): + args = parse_args() + runner = CprofRunner( + args.executable, + args.gprof, + args.result_dir, + args.working_dir, + ) + runner.run() + + +if __name__ == "__main__": + main() + +# vim: set ff=unix fenc=utf8 et sw=4 ts=4 sts=4: