Add the library that combines multiple AutoFDO profiles, and the
afdo_merge command that drives it. The library remaps names and function
identifiers, merges counts and inline trees, and recomputes the resulting
profile summary. The command validates inputs, writes the merged profile,
and reports merge or write failures.
afdo_tools/ChangeLog:
* src/gcov_merge.cc: New file.
* src/gcov_merge.h: New file.
* src/afdo_merge.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_merge.cc | 107 ++++++++
afdo_tools/src/gcov_merge.cc | 455 +++++++++++++++++++++++++++++++++++
afdo_tools/src/gcov_merge.h | 60 +++++
3 files changed, 622 insertions(+)
create mode 100644 afdo_tools/src/afdo_merge.cc
create mode 100644 afdo_tools/src/gcov_merge.cc
create mode 100644 afdo_tools/src/gcov_merge.h
diff --git a/afdo_tools/src/afdo_merge.cc b/afdo_tools/src/afdo_merge.cc
new file mode 100644
index 00000000000..fa467764654
--- /dev/null
+++ b/afdo_tools/src/afdo_merge.cc
@@ -0,0 +1,107 @@
+/* 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 "gcov_merge.h"
+#include "version.h"
+
+#include <cstdio>
+#include <getopt.h>
+#include <string>
+#include <vector>
+
+static void
+print_usage (const char *prog, FILE *stream)
+{
+ fprintf (stream, _("Usage:\n"
+ " %s [OPTIONS] <a.gcov> [<b.gcov> ...]\n"
+ "\n"
+ "Merge AutoFDO profiles (sum counts). With -o, a single "
+ "input is\n"
+ "allowed.\n"
+ "Options:\n"
+ " -o, --output_file FILE Write merged profile to FILE\n"
+ " -h, --help Print this help, then exit\n"
+ " -v, --version Print version information, then exit\n"
+ "\nFor bug reporting instructions, please see:\n"
+ "%s.\n"),
+ prog, bug_report_url);
+}
+
+int
+main (int argc, char *argv[])
+{
+ afdo_init_libintl ();
+
+ std::string output_path;
+ static option longopts[] = {{"output_file", required_argument, nullptr, 'o'},
+ {"help", no_argument, nullptr, 'h'},
+ {"version", no_argument, nullptr, 'v'},
+ {nullptr, 0, nullptr, 0}};
+
+ int c, idx = 0;
+ while ((c = getopt_long_only (argc, argv, "o:hv", longopts, &idx)) != -1)
+ {
+ switch (c)
+ {
+ case 'o':
+ output_path = optarg;
+ break;
+ case 'h':
+ print_usage (argv[0], stdout);
+ return 0;
+ case 'v':
+ printf ("afdo_merge %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"));
+ return 0;
+ default:
+ print_usage (argv[0], stderr);
+ return 1;
+ }
+ }
+
+ std::vector<std::string> inputs;
+ for (int i = optind; i < argc; ++i)
+ inputs.emplace_back (argv[i]);
+
+ if (output_path.empty ())
+ {
+ /* An explicit output file is required: never treat a positional
+ argument as the output (that would silently overwrite an input). */
+ fprintf (stderr, _("error: no output file; use -o FILE\n"));
+ print_usage (argv[0], stderr);
+ return 1;
+ }
+ if (inputs.empty ())
+ {
+ /* Nothing to merge. A lone input is accepted, since with -o that is a
+ faithful version conversion rather than an accidental overwrite. */
+ print_usage (argv[0], stderr);
+ return 1;
+ }
+
+ gcov::gcov_merger merger;
+ if (!merger.merge (inputs, output_path))
+ return 1;
+
+ return 0;
+}
diff --git a/afdo_tools/src/gcov_merge.cc b/afdo_tools/src/gcov_merge.cc
new file mode 100644
index 00000000000..a9dcea14287
--- /dev/null
+++ b/afdo_tools/src/gcov_merge.cc
@@ -0,0 +1,455 @@
+/* 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 "gcov_merge.h"
+#include "gcov_reader.h"
+#include "gcov_summary.h"
+#include "gcov_writer.h"
+
+#include <algorithm>
+#include <cstdint>
+#include <cstdio>
+#include <map>
+#include <memory>
+#include <set>
+#include <tuple>
+#include <utility>
+#include <vector>
+
+namespace gcov {
+namespace {
+using location_key = std::tuple<uint32_t, uint32_t, bool, uint32_t>;
+
+/* Match key for merge_locations. All positions (normal/call) at one
+ (offset, disc) share a slot; each inline callsite is keyed by its callee id
+ as well, since several functions may be inlined at the same (offset, disc).
+
+ CALLEE_ID must be expressed in the merged name pool for both operands. The
+ destination is already remapped, the source is remapped by the caller. */
+location_key
+location_slot (const gcov_location &l, uint32_t callee_id)
+{
+ const bool is_inline = l.type == gcov_location::kind::inlined;
+ return {l.offset, l.discriminator, is_inline, is_inline ? callee_id : 0};
+}
+
+/* The callee id keyed into location_slot, in L's own name pool: an inline
+ callsite's inlined symbol, else 0. Keying across pools must remap it. */
+uint32_t
+location_callee_id (const gcov_location &l)
+{
+ if (l.type != gcov_location::kind::inlined)
+ return 0;
+ return static_cast<const gcov_location_inline &> (l).inlined_sym.id;
+}
+
+/* Order two locations by their merge slot (offset, discriminator, callee). */
+bool
+location_slot_less (const std::unique_ptr<gcov_location> &a,
+ const std::unique_ptr<gcov_location> &b)
+{
+ return location_slot (*a, location_callee_id (*a))
+ < location_slot (*b, location_callee_id (*b));
+}
+
+/* Sort by location_slot, recursively into inline subtrees. */
+void
+sort_locations (gcov_function &f)
+{
+ std::stable_sort (
+ f.locations.begin (), f.locations.end (), location_slot_less);
+ for (auto &loc : f.locations)
+ if (loc->type == gcov_location::kind::inlined)
+ sort_locations (
+ static_cast<gcov_location_inline *> (loc.get ())->inlined_sym);
+}
+
+/* True when ID names a real symbol rather than the empty slot 0 or the
+ all-ones sentinel. */
+bool
+valid_gcov_name_index (uint32_t id)
+{
+ return id != 0 && id != INVALID_INDEX;
+}
+
+/* Return the symbol identity at NAME_INDEX, or an empty identity for the
+ reserved index zero or an out-of-range index. */
+symbol_identity
+symbol_identity_for_name_index (const gcov_profile &profile,
+ size_t name_index)
+{
+ if (name_index == 0
+ || name_index >= profile.get_function_names ().size ())
+ return {};
+ std::string file;
+ const std::vector<uint32_t> &file_name_map = profile.get_file_name_map ();
+ const std::vector<std::string> &file_names = profile.get_file_names ();
+ if (name_index < file_name_map.size ())
+ {
+ const uint32_t file_index = file_name_map[name_index];
+ if (file_index < file_names.size ())
+ file = file_names[file_index];
+ }
+ return {profile.get_function_names ()[name_index], std::move (file)};
+}
+
+} /* anonymous namespace. */
+
+/* Map every symbol id in SRC_PROFILE's name pool to its merged id, so the
+ remapping below costs an array index rather than a keyed lookup. Ids with
+ no merged counterpart map to INVALID_INDEX. */
+std::vector<uint32_t>
+gcov_merger::build_remap (const gcov_profile &src_profile) const
+{
+ const size_t names = src_profile.get_function_names ().size ();
+ std::vector<uint32_t> remap (names, INVALID_INDEX);
+ for (size_t id = 1; id < names; ++id)
+ {
+ const symbol_identity identity
+ = symbol_identity_for_name_index (src_profile, id);
+ if (identity.name.empty ())
+ continue;
+ const auto it = identity_to_id.find (identity);
+ if (it != identity_to_id.end () && it->second != 0)
+ remap[id] = it->second;
+ }
+ return remap;
+}
+
+/* Deep-copy SRC into the merged name pool. Returns nullptr when the location
+ is an inline callsite whose callee cannot be remapped. */
+std::unique_ptr<gcov_location>
+gcov_merger::clone_location (const gcov_location &src,
+ const std::vector<uint32_t> &remap)
+{
+ std::unique_ptr<gcov_location> out;
+ switch (src.type)
+ {
+ case gcov_location::kind::normal:
+ out = std::make_unique<gcov_location_normal> (
+ static_cast<const gcov_location_normal &> (src).count);
+ break;
+ case gcov_location::kind::call:
+ {
+ const auto &c = static_cast<const gcov_location_call &> (src);
+ auto call = std::make_unique<gcov_location_call> (c.count);
+ for (const auto &kv : c.callee_map)
+ {
+ const uint32_t remapped
+ = kv.first < remap.size () ? remap[kv.first] : INVALID_INDEX;
+ if (valid_gcov_name_index (remapped))
+ call->callee_map[remapped]
+ = gcov_add_count (call->callee_map[remapped], kv.second);
+ }
+ out = std::move (call);
+ break;
+ }
+ case gcov_location::kind::inlined:
+ {
+ auto inl = std::make_unique<gcov_location_inline> ();
+ /* Callee cannot be remapped into the merged pool. Drop the callsite
+ rather than fabricate a bogus offset-0 position count. */
+ if (!build_inlined (
+ inl->inlined_sym,
+ static_cast<const gcov_location_inline &> (src).inlined_sym,
+ remap))
+ return nullptr;
+ out = std::move (inl);
+ break;
+ }
+ }
+ out->offset = src.offset;
+ out->discriminator = src.discriminator;
+ return out;
+}
+
+/* Populate the empty DST with a remapped copy of SRC. Returns false, leaving
+ DST unusable, when SRC's own symbol is not in the merged pool. */
+bool
+gcov_merger::build_inlined (gcov_function &dst, const gcov_function &src,
+ const std::vector<uint32_t> &remap)
+{
+ const uint32_t mid = src.id < remap.size () ? remap[src.id] : INVALID_INDEX;
+ if (!valid_gcov_name_index (mid))
+ return false;
+ dst.id = mid;
+ dst.desc.name_index = mid;
+ dst.timestamp = src.timestamp;
+ dst.head_count = src.head_count;
+ for (const auto &loc : src.locations)
+ if (auto cloned = clone_location (*loc, remap))
+ dst.locations.push_back (std::move (cloned));
+ dst.total_count = gcov_function_subtree_count (dst);
+ return true;
+}
+
+/* Accumulate SRC into DST slot by slot, appending any location DST lacks. */
+void
+gcov_merger::merge_locations (
+ std::vector<std::unique_ptr<gcov_location>> &dst,
+ const std::vector<std::unique_ptr<gcov_location>> &src,
+ const std::vector<uint32_t> &remap)
+{
+ /* DST inline ids are already in the merged pool. SRC ids must be remapped
to
+ the merged pool before keying so identical callsites match. */
+ const auto src_callee = [&] (const gcov_location &l) -> uint32_t {
+ if (l.type != gcov_location::kind::inlined)
+ return 0;
+ const uint32_t id
+ = static_cast<const gcov_location_inline &> (l).inlined_sym.id;
+ return id < remap.size () ? remap[id] : INVALID_INDEX;
+ };
+
+ std::map<location_key, size_t> slot;
+ for (size_t i = 0; i < dst.size (); ++i)
+ slot[location_slot (*dst[i], location_callee_id (*dst[i]))] = i;
+
+ for (const auto &sloc : src)
+ {
+ auto it = slot.find (location_slot (*sloc, src_callee (*sloc)));
+ if (it == slot.end ())
+ {
+ auto cloned = clone_location (*sloc, remap);
+ if (!cloned)
+ continue;
+ dst.push_back (std::move (cloned));
+ slot[location_slot (*dst.back (), location_callee_id (*dst.back ()))]
+ = dst.size () - 1;
+ continue;
+ }
+
+ const size_t di = it->second;
+ if (sloc->type == gcov_location::kind::inlined)
+ {
+ merge_function (
+ static_cast<gcov_location_inline *> (dst[di].get ())->inlined_sym,
+ static_cast<const gcov_location_inline &> (*sloc).inlined_sym,
+ remap);
+ continue;
+ }
+
+ /* Position slot: a call (indirect targets) in either operand makes the
+ merged position a call. Upgrade a normal DST in place if needed. */
+ const bool src_is_call = sloc->type == gcov_location::kind::call;
+ if (src_is_call && dst[di]->type == gcov_location::kind::normal)
+ {
+ auto up = std::make_unique<gcov_location_call> (
+ static_cast<const gcov_location_normal &> (*dst[di]).count);
+ up->offset = dst[di]->offset;
+ up->discriminator = dst[di]->discriminator;
+ dst[di] = std::move (up);
+ }
+
+ if (dst[di]->type == gcov_location::kind::call)
+ {
+ auto *dc = static_cast<gcov_location_call *> (dst[di].get ());
+ if (src_is_call)
+ {
+ const auto &sc = static_cast<const gcov_location_call &> (*sloc);
+ dc->count = gcov_add_count (dc->count, sc.count);
+ for (const auto &kv : sc.callee_map)
+ {
+ const uint32_t remapped
+ = kv.first < remap.size () ? remap[kv.first]
+ : INVALID_INDEX;
+ if (valid_gcov_name_index (remapped))
+ dc->callee_map[remapped]
+ = gcov_add_count (dc->callee_map[remapped], kv.second);
+ }
+ }
+ else
+ dc->count
+ = gcov_add_count (
+ dc->count,
+ static_cast<const gcov_location_normal &> (*sloc).count);
+ }
+ else
+ {
+ auto *dn = static_cast<gcov_location_normal *> (dst[di].get ());
+ dn->count
+ = gcov_add_count (
+ dn->count,
+ static_cast<const gcov_location_normal &> (*sloc).count);
+ }
+ }
+}
+
+/* Fold SRC into the already-merged DST: counts add, and the timestamp keeps
+ the earliest non-zero value. */
+void
+gcov_merger::merge_function (gcov_function &dst, const gcov_function &src,
+ const std::vector<uint32_t> &remap)
+{
+ dst.head_count = gcov_add_count (dst.head_count, src.head_count);
+ if (dst.timestamp == 0
+ || (src.timestamp != 0 && src.timestamp < dst.timestamp))
+ dst.timestamp = src.timestamp;
+ merge_locations (dst.locations, src.locations, remap);
+ dst.total_count = gcov_function_subtree_count (dst);
+}
+
+/* Read every profile in INPUTS, sum them onto a common name pool and write the
+ result to OUTPUT. Returns false on any read, merge or write failure. */
+bool
+gcov_merger::merge (const std::vector<std::string> &inputs,
+ const std::string &output)
+{
+ if (inputs.empty ())
+ {
+ fprintf (stderr, _("Error: no input profiles\n"));
+ return false;
+ }
+
+ std::vector<std::unique_ptr<gcov_reader_base>> readers;
+ readers.reserve (inputs.size ());
+ for (const auto &path : inputs)
+ {
+ std::unique_ptr<gcov_reader_base> r = make_gcov_reader (path);
+ if (!r)
+ {
+ fprintf (stderr, _("Error: unrecognized profile format: %s\n"),
+ path.c_str ());
+ return false;
+ }
+ if (!r->read ())
+ {
+ fprintf (stderr, _("Error: Failed to load %s\n"), path.c_str ());
+ return false;
+ }
+ fprintf (stderr,
+ _("Loaded %s with %zu functions\n"),
+ path.c_str (),
+ r->get_profile ()->get_functions ().size ());
+ readers.push_back (std::move (r));
+ }
+
+ /* merge () may be called more than once. Reset the incremental maps that
the
+ passes below only insert into. */
+ identity_to_id.clear ();
+ fn_index.clear ();
+
+ /* Pass 1: union of symbol identities (sorted, deterministic) -> name
+ pool. */
+ std::set<symbol_identity> all_identities;
+ for (const auto &r : readers)
+ {
+ const gcov_profile &profile = *r->get_profile ();
+ for (size_t id = 1; id < profile.get_function_names ().size (); ++id)
+ {
+ symbol_identity identity
+ = symbol_identity_for_name_index (profile, id);
+ if (!identity.name.empty ())
+ all_identities.insert (std::move (identity));
+ }
+ }
+
+ std::vector<std::string> function_names;
+ function_names.push_back ("");
+ std::vector<std::string> symbol_paths;
+ symbol_paths.push_back ("");
+ uint32_t next_id = 1;
+ for (const symbol_identity &identity : all_identities)
+ {
+ function_names.push_back (identity.name);
+ symbol_paths.push_back (identity.file);
+ identity_to_id[identity] = next_id++;
+ }
+
+ std::vector<std::string> file_names;
+ std::vector<uint32_t> file_name_map;
+ gcov_profile::build_file_name_table (symbol_paths, file_names,
+ file_name_map);
+ result.set_names (std::move (file_names), std::move (function_names),
+ std::move (file_name_map));
+
+ /* Pass 2: materialize and merge function records. */
+ std::vector<gcov_function> functions;
+ functions.reserve (identity_to_id.size ());
+ for (const auto &r : readers)
+ {
+ const gcov_profile *in = r->get_profile ();
+ const std::vector<uint32_t> remap = build_remap (*in);
+ for (const auto &f : in->get_functions ())
+ {
+ const symbol_identity identity
+ = symbol_identity_for_name_index (*in, f.desc.name_index);
+ if (identity.name.empty ())
+ continue;
+ auto fit = fn_index.find (identity);
+ if (fit != fn_index.end ())
+ {
+ merge_function (functions[fit->second], f, remap);
+ continue;
+ }
+ functions.emplace_back ();
+ gcov_function &dst = functions.back ();
+ if (!build_inlined (dst, f, remap))
+ {
+ functions.pop_back ();
+ continue;
+ }
+ const uint32_t fidx
+ = (dst.id < result.get_file_name_map ().size ())
+ ? result.get_file_name_map ()[dst.id]
+ : INVALID_INDEX;
+ dst.desc.file_index
+ = (fidx < result.get_file_names ().size ()) ? fidx : INVALID_INDEX;
+ fn_index[identity] = functions.size () - 1;
+ }
+ }
+
+ /* Canonical location order within each function (merge appends in input
+ order; sort to the offset order profile_builder emits). */
+ for (auto &f : functions)
+ sort_locations (f);
+
+ /* Deterministic output order. Name pool ids were handed out in sorted
+ symbol-identity order, so ordering by id is ordering by identity. */
+ std::sort (functions.begin (),
+ functions.end (),
+ [] (const gcov_function &a, const gcov_function &b)
+ {
+ return a.desc.name_index < b.desc.name_index;
+ });
+
+ gcov_summary summary;
+ compute_summary (functions, summary);
+ result.set_summary (std::move (summary));
+ result.set_functions (std::move (functions));
+
+ fprintf (stderr, _("Merged data has %zu functions\n"),
+ result.get_functions ().size ());
+
+ gcov_writer_options options;
+ std::unique_ptr<gcov_writer_base> writer
+ = make_gcov_writer (output, &result, options);
+ if (!writer)
+ {
+ fprintf (stderr, _("Error: unsupported output profile format\n"));
+ return false;
+ }
+
+ if (!writer->write ())
+ {
+ fprintf (stderr, _("Error: Failed to write merged profile\n"));
+ return false;
+ }
+ return true;
+}
+
+} /* namespace gcov. */
diff --git a/afdo_tools/src/gcov_merge.h b/afdo_tools/src/gcov_merge.h
new file mode 100644
index 00000000000..82f757e8273
--- /dev/null
+++ b/afdo_tools/src/gcov_merge.h
@@ -0,0 +1,60 @@
+/* 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/>. */
+
+#ifndef GCC_GCOV_MERGE_H
+#define GCC_GCOV_MERGE_H
+
+#include "gcov.h"
+
+#include <cstdint>
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace gcov {
+
+/* Merge several AutoFDO profiles (sum counts) on the gcov_profile model. */
+class gcov_merger
+{
+ gcov_profile result;
+ /* Merged symbol identity -> function id. */
+ std::map<symbol_identity, uint32_t> identity_to_id;
+ /* Merged top-level symbol identity -> index into result.functions. */
+ std::map<symbol_identity, size_t> fn_index;
+
+ std::vector<uint32_t> build_remap (const gcov_profile &src_profile) const;
+ std::unique_ptr<gcov_location>
+ clone_location (const gcov_location &src,
+ const std::vector<uint32_t> &remap);
+ bool build_inlined (gcov_function &dst, const gcov_function &src,
+ const std::vector<uint32_t> &remap);
+ void merge_function (gcov_function &dst, const gcov_function &src,
+ const std::vector<uint32_t> &remap);
+ void merge_locations (std::vector<std::unique_ptr<gcov_location>> &dst,
+ const std::vector<std::unique_ptr<gcov_location>> &src,
+ const std::vector<uint32_t> &remap);
+
+public:
+ bool merge (const std::vector<std::string> &inputs,
+ const std::string &output);
+};
+
+} /* namespace gcov. */
+
+#endif /* GCC_GCOV_MERGE_H. */
--
2.34.1