Add the afdo_create command-line frontend for producing GCOV AutoFDO
profiles from an executable and its samples. It validates options, selects
between offline perf.data, live capture and ARM SPE sources, and drives the
profiler pipeline introduced by the preceding patches. create_gcov option
spellings are accepted so existing AutoFDO scripts keep working.
afdo_tools/ChangeLog:
* src/afdo_create.cc: New file.
Signed-off-by: Kugan Vivekanandarajah <[email protected]>
Co-authored-by: Dhruv Chawla <[email protected]>
Co-authored-by: Rahul Shinde <[email protected]>
---
afdo_tools/src/afdo_create.cc | 471 ++++++++++++++++++++++++++++++++++
1 file changed, 471 insertions(+)
create mode 100644 afdo_tools/src/afdo_create.cc
diff --git a/afdo_tools/src/afdo_create.cc b/afdo_tools/src/afdo_create.cc
new file mode 100644
index 00000000000..139531edb1c
--- /dev/null
+++ b/afdo_tools/src/afdo_create.cc
@@ -0,0 +1,471 @@
+/* Copyright The GNU Toolchain Authors
+
+This file is part of GCC.
+
+GCC is free software; you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free
+Software Foundation; either version 3, or (at your option) any later
+version.
+
+GCC is distributed in the hope that it will be useful, but WITHOUT ANY
+WARRANTY; without even the implied warranty of MERCHANTABILITY or
+FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+for more details.
+
+You should have received a copy of the GNU General Public License
+along with GCC; see the file COPYING3. If not see
+<http://www.gnu.org/licenses/>. */
+
+#include "autofdo_profiler.h"
+#include "gcov.h"
+#include "gcov_writer.h"
+#include "perf_file_reader.h"
+#include "profile_timer.h"
+#include "version.h"
+
+#include <cctype>
+#include <cerrno>
+#include <climits>
+#include <cmath>
+#include <cstdint>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <getopt.h>
+#include <memory>
+#include <string>
+#include <sys/stat.h>
+#include <vector>
+
+/* afdo_create -- build an AutoFDO profile from a binary plus perf samples. */
+
+namespace {
+
+/* Default cycles between samples during live capture. */
+constexpr uint64_t DEFAULT_SAMPLE_PERIOD = 100000;
+
+/* Reject a numeric option argument unless it is the whole of TEXT. strto*
+ skips leading blanks and stops at the first unconvertible character, so
+ both have to be rejected explicitly for "12abc" and " 12" to fail. */
+bool
+parse_uint64_option (const char *text, uint64_t min_value, uint64_t max_value,
+ uint64_t &out)
+{
+ if (text == nullptr || *text == '\0' || isspace ((unsigned char) *text)
+ || *text == '-' || *text == '+')
+ return false;
+ errno = 0;
+ char *end = nullptr;
+ const unsigned long long value = std::strtoull (text, &end, 10);
+ if (errno == ERANGE || end == text || *end != '\0')
+ return false;
+ if (value < min_value || value > max_value)
+ return false;
+ out = static_cast<uint64_t> (value);
+ return true;
+}
+
+/* As parse_uint64_option, but for a finite fraction. NaN and infinity are
+ rejected here: every ordered comparison against NaN is false, so a range
+ check alone would let it through. */
+bool
+parse_double_option (const char *text, double min_value, double max_value,
+ double &out)
+{
+ if (text == nullptr || *text == '\0' || isspace ((unsigned char) *text))
+ return false;
+ errno = 0;
+ char *end = nullptr;
+ const double value = std::strtod (text, &end);
+ if (errno == ERANGE || end == text || *end != '\0')
+ return false;
+ if (!std::isfinite (value) || value < min_value || value > max_value)
+ return false;
+ out = value;
+ return true;
+}
+
+struct program_options
+{
+ std::string perf_data_file;
+ std::string binary_file;
+ std::string output_file;
+ std::vector<std::string> command_argv;
+ autofdo::profiler_run_options run;
+ perf_data::sampling_mode sampling_mode
+ = perf_data::sampling_mode::BRANCH_STACK;
+ bool sample_period_set = false;
+ bool print_help = false;
+ bool print_version = false;
+};
+
+static void
+print_usage (const char *program_name)
+{
+ printf (_("Usage:\n"
+ " %s -b <binary> -o <output> [OPTIONS] -- <command> "
+ "[args...]\n"
+ " %s -p <perf.data> -b <binary> -o <output> [OPTIONS]\n\n"
+ " -b, --binary FILE Binary with DWARF (and command to run)\n"
+ " -o, --output FILE Output profile path\n"
+ " -- Separator; following args are the "
+ "command\n\n"
+ "Perf.data mode (optional): build profile from existing perf.data.\n"
+ " -p, --perf FILE Path to perf.data (branch-stack: "
+ "x86 LBR, ARM BRBE)\n"
+ " -b, --binary FILE Binary with DWARF\n"
+ " -o, --output FILE Output profile path\n\n"
+ "Profile options:\n"
+ " --sample-threshold-frac F Skip below F * total profile "
+ "mass\n"
+ " (create_gcov "
+ "--sample_threshold_frac;\n"
+ " default 0 = "
+ "MIN_SAMPLES_FOR_GCOV_EMIT floor only).\n"
+ " -j, --jobs N Parallel DWARF parse + correlate "
+ "threads\n"
+ " (default 8).\n"
+ " --sample-period N Live capture only: cycles between "
+ "samples\n"
+ " (default 100000; lower samples more "
+ "often).\n"
+ " --no-branch-stack Per-PC counts only (create_gcov "
+ "use_branch_stack=false).\n"
+ " --no-lbr Alias for --no-branch-stack.\n"
+ " Default: branch-stack range expansion.\n"
+ " -s, --spe Use ARM SPE reader with -p "
+ "(perf.data only)\n\n"
+ "Validation:\n"
+ " -m, --dump-addr-mapping FILE\n"
+ " One line per correlated PC: file offset, "
+ "STT_FUNC\n"
+ " name, file:line:discriminator, inline "
+ "stack.\n"
+ " -D, --dump-perf-maps FILE\n"
+ " After perf load, dump address_count, "
+ "range_count,\n"
+ " and branch_count maps (file offsets).\n\n"
+ "General:\n"
+ " -h, --help Show this help\n"
+ " -v, --version Show version\n\n"
+ "create_gcov compatibility options:\n"
+ " --gcov FILE Output GCOV file\n"
+ " --profile FILE Path to perf.data\n"
+ " --profiler ARG Profile type (Branch-stack ('perf') or "
+ "SPE ('perf_spe'))\n"
+ "\nFor bug reporting instructions, please see:\n"
+ "%s.\n"),
+ program_name, program_name, bug_report_url);
+}
+
+/* Fill OPTIONS from ARGC/ARGV. Returns false when the command line is
+ rejected, and also for --help and --version, which OPTIONS records so that
+ the caller can print the requested text and exit successfully. */
+static bool
+parse_arguments (int argc, char *argv[], program_options &options)
+{
+ options.run.live_sample_period = DEFAULT_SAMPLE_PERIOD;
+
+ static option long_options[]
+ = {{"perf", required_argument, nullptr, 'p'},
+ {"binary", required_argument, nullptr, 'b'},
+ {"output", required_argument, nullptr, 'o'},
+ {"dump-addr-mapping", required_argument, nullptr, 'm'},
+ {"dump-perf-maps", required_argument, nullptr, 'D'},
+ {"no-branch-stack", no_argument, nullptr, 'L'},
+ {"no-lbr", no_argument, nullptr, 'L'},
+ {"gcov", required_argument, nullptr, 0},
+ {"profile", required_argument, nullptr, 0},
+ {"profiler", required_argument, nullptr, 0},
+ {"spe", no_argument, nullptr, 's'},
+ {"sample-threshold-frac", required_argument, nullptr, 'F'},
+ {"sample-period", required_argument, nullptr, 0},
+ {"jobs", required_argument, nullptr, 'j'},
+ {"help", no_argument, nullptr, 'h'},
+ {"version", no_argument, nullptr, 'v'},
+ {nullptr, 0, nullptr, 0}};
+
+ int c;
+ int option_index = 0;
+
+ /* create_gcov and SPEC pass long options with one dash. */
+ while ((c = getopt_long_only (
+ argc, argv, "p:b:o:m:D:LsF:j:hv", long_options, &option_index))
+ != -1)
+ {
+ switch (c)
+ {
+ case 'p':
+ options.perf_data_file = optarg;
+ break;
+ case 'b':
+ options.binary_file = optarg;
+ break;
+ case 'o':
+ options.output_file = optarg;
+ break;
+ case 's':
+ options.sampling_mode = perf_data::sampling_mode::SPE;
+ break;
+ case 'm':
+ options.run.dump_address_mapping = optarg;
+ break;
+ case 'D':
+ options.run.dump_perf_maps = optarg;
+ break;
+ case 'F':
+ {
+ double v = 0.0;
+ if (!parse_double_option (optarg, 0.0, 1.0, v))
+ {
+ fprintf (stderr,
+ _("Invalid --sample-threshold-frac (need 0 <= F <= 1)"
+ ": %s\n"),
+ optarg);
+ return false;
+ }
+ options.run.sample_threshold_frac = v;
+ }
+ break;
+ case 'j':
+ {
+ uint64_t n = 0;
+ if (!parse_uint64_option (optarg, 1, UINT_MAX, n))
+ {
+ fprintf (stderr,
+ _("Invalid -j/--jobs (need 1 <= N <= %u): %s\n"),
+ UINT_MAX, optarg);
+ return false;
+ }
+ options.run.correlate_threads = static_cast<unsigned> (n);
+ }
+ break;
+ case 'L':
+ options.run.use_branch_stack = false;
+ break;
+ case 'v':
+ options.print_version = true;
+ return false;
+ case 'h':
+ options.print_help = true;
+ return false;
+ case '?':
+ fprintf (stderr, _("Try '%s --help' for more information.\n"),
+ argv[0]);
+ return false;
+ default:
+ if (c != 0)
+ return false;
+
+ if (!strcmp (long_options[option_index].name, "profile"))
+ options.perf_data_file = optarg;
+ else if (!strcmp (long_options[option_index].name, "profiler"))
+ {
+ if (!strcmp (optarg, "perf"))
+ options.sampling_mode = perf_data::sampling_mode::BRANCH_STACK;
+ else if (!strcmp (optarg, "perf_spe"))
+ options.sampling_mode = perf_data::sampling_mode::SPE;
+ else
+ {
+ fprintf (stderr,
+ _("Invalid option for --profiler: expected 'perf' "
+ "or 'perf_spe', got: %s\n"),
+ optarg);
+ return false;
+ }
+ }
+ else if (!strcmp (long_options[option_index].name, "gcov"))
+ options.output_file = optarg;
+ else if (!strcmp (long_options[option_index].name, "sample-period"))
+ {
+ uint64_t n = 0;
+ if (!parse_uint64_option (optarg, 1, UINT64_MAX, n))
+ {
+ fprintf (stderr,
+ _("Invalid --sample-period (need N >= 1): %s\n"),
+ optarg);
+ return false;
+ }
+ options.run.live_sample_period = n;
+ options.sample_period_set = true;
+ }
+ }
+ }
+
+ if (options.perf_data_file.empty () && optind < argc)
+ for (int j = optind; j < argc; ++j)
+ options.command_argv.push_back (argv[j]);
+
+ if (options.binary_file.empty ())
+ {
+ fprintf (stderr, _("Error: binary is required (-b option)\n"));
+ return false;
+ }
+ if (options.output_file.empty ())
+ {
+ fprintf (stderr, _("Error: output is required (-o option)\n"));
+ return false;
+ }
+ if (options.perf_data_file.empty () && options.command_argv.empty ())
+ {
+ fprintf (stderr,
+ _("Error: either -p <perf.data> or a command after -- "
+ "is required\n"));
+ fprintf (stderr,
+ _("Example (direct): %s -b ./myapp -o myapp.afdo -- ./myapp\n"),
+ argv[0]);
+ return false;
+ }
+ if (!options.perf_data_file.empty () && !options.command_argv.empty ())
+ {
+ fprintf (stderr,
+ _("Error: use either -p <perf.data> or command (--), not "
+ "both\n"));
+ return false;
+ }
+ if (options.perf_data_file.empty ()
+ && options.sampling_mode == perf_data::sampling_mode::SPE)
+ {
+ fprintf (stderr,
+ _("Error: -s/--spe requires -p <perf.data> (live SPE profiling "
+ "is not supported)\n"));
+ return false;
+ }
+ if (!options.perf_data_file.empty () && options.sample_period_set)
+ {
+ fprintf (stderr,
+ _("Error: --sample-period applies to live capture only;"
+ " perf.data carries its own rate\n"));
+ return false;
+ }
+
+ return true;
+}
+
+/* Build a profiler reading an existing perf.data, or one that captures live
+ samples from the command given after "--". */
+static std::unique_ptr<autofdo::autofdo_profiler>
+make_profiler (const program_options &options)
+{
+ if (!options.perf_data_file.empty ())
+ return std::make_unique<autofdo::autofdo_profiler> (options.binary_file,
+ options.run,
+ options.perf_data_file,
+ options.sampling_mode);
+ return std::make_unique<autofdo::autofdo_profiler> (
+ options.binary_file, options.run, options.command_argv);
+}
+
+/* Write PROFILE in the version and encoding requested by OPTIONS. */
+static bool
+write_gcov_output (const program_options &options,
+ const gcov::gcov_profile &profile)
+{
+ gcov::gcov_writer_options writer_options;
+ std::unique_ptr<gcov::gcov_writer_base> generator
+ = gcov::make_gcov_writer (options.output_file, &profile, writer_options);
+ if (!generator)
+ {
+ fprintf (stderr, _("Error: unsupported output profile format\n"));
+ return false;
+ }
+
+ if (!generator->write ())
+ {
+ fprintf (stderr, _("Error: Failed to generate GCOV file\n"));
+ return false;
+ }
+ return true;
+}
+
+/* Check that the inputs named in OPTIONS exist; a binary without execute
+ permission only draws a warning. */
+static bool
+validate_input_files (const program_options &options)
+{
+ struct stat st;
+
+ if (!options.perf_data_file.empty ())
+ {
+ if (stat (options.perf_data_file.c_str (), &st) != 0)
+ {
+ fprintf (stderr,
+ _("Error: perf.data file does not exist: %s\n"),
+ options.perf_data_file.c_str ());
+ return false;
+ }
+ }
+
+ if (stat (options.binary_file.c_str (), &st) != 0)
+ {
+ fprintf (stderr,
+ _("Error: binary file does not exist: %s\n"),
+ options.binary_file.c_str ());
+ return false;
+ }
+
+ if ((st.st_mode & S_IXUSR) == 0)
+ fprintf (stderr,
+ _("Warning: binary file is not executable: %s\n"),
+ options.binary_file.c_str ());
+
+ return true;
+}
+
+} /* anonymous namespace. */
+
+int
+main (int argc, char *argv[])
+{
+ afdo_init_libintl ();
+
+ program_options options;
+
+ if (!parse_arguments (argc, argv, options))
+ {
+ if (options.print_help)
+ print_usage (argv[0]);
+ else if (options.print_version)
+ {
+ printf ("afdo_create %s%s\n", pkgversion_string, version_string);
+ printf (_("Copyright %s 2026 Free Software Foundation, Inc.\n"),
+ _("(C)"));
+ printf (_("This is free software; see the source for copying "
+ "conditions. There is NO\n"
+ "warranty; not even for MERCHANTABILITY or FITNESS FOR A "
+ "PARTICULAR PURPOSE.\n"));
+ }
+ else
+ return 1;
+ return 0;
+ }
+
+ if (!validate_input_files (options))
+ return 1;
+
+ /* Resolve the binary to an absolute, symlink-free path, keeping the path as
+ given if that is not possible. */
+ char *resolved = realpath (options.binary_file.c_str (), nullptr);
+ if (resolved != nullptr)
+ {
+ options.binary_file.assign (resolved);
+ free (resolved);
+ }
+
+ autofdo::profile_timer::start ();
+
+ auto profiler = make_profiler (options);
+
+ if (!profiler->generate_profile ())
+ {
+ fprintf (stderr, _("Error: Failed to generate profile\n"));
+ return 1;
+ }
+
+ autofdo::profile_timer::begin_stage (_("write gcov"));
+ if (!write_gcov_output (options, profiler->get_gcov_profile ()))
+ return 1;
+ autofdo::profile_timer::end_stage ();
+
+ return 0;
+}
--
2.34.1