Add mmap-backed parsing of perf.data headers, event records, attributes, and
address mappings.  The parser exposes normalized event streams while keeping
sampling-mode interpretation in later readers.

afdo_tools/ChangeLog:

        * src/perf_file_reader.cc: New file.
        * src/perf_file_reader.h: 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/perf_file_reader.cc | 1101 ++++++++++++++++++++++++++++
 afdo_tools/src/perf_file_reader.h  |  176 +++++
 2 files changed, 1277 insertions(+)
 create mode 100644 afdo_tools/src/perf_file_reader.cc
 create mode 100644 afdo_tools/src/perf_file_reader.h

diff --git a/afdo_tools/src/perf_file_reader.cc 
b/afdo_tools/src/perf_file_reader.cc
new file mode 100644
index 00000000000..1c49ec24a15
--- /dev/null
+++ b/afdo_tools/src/perf_file_reader.cc
@@ -0,0 +1,1101 @@
+/* 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 "perf_file_reader.h"
+
+#include <algorithm>
+#include <cinttypes>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <cerrno>
+#include <unordered_map>
+#include <vector>
+
+#include <fcntl.h>
+#include <sys/mman.h>
+#include <unistd.h>
+
+#include <elf.h>
+#include <gelf.h>
+#include <libelf.h>
+
+namespace perf_data {
+namespace {
+#ifndef NT_GNU_BUILD_ID
+#define NT_GNU_BUILD_ID 3
+#endif
+
+/* Perf file format constants.  */
+constexpr uint64_t PERF_MAGIC = 0x32454c4946524550ULL; /* "PERFILE2".  */
+constexpr uint64_t PERF_MAGIC_SWAPPED
+  = 0x50455246494c4532ULL; /* Byte-swapped PERFILE2.  */
+
+/* Lowercase hexadecimal digits for build IDs.  */
+constexpr char HEX_DIGITS[] = "0123456789abcdef";
+
+/* User-space address validation constants.  */
+constexpr uint64_t MIN_USER_ADDRESS = 0x1000;
+constexpr uint64_t KERNEL_ADDRESS_MASK = 0xFFFF000000000000ULL;
+
+/* Linux perf_event_attr field offsets.  */
+constexpr size_t PERF_EVENT_ATTR_SAMPLE_TYPE_OFF = 24;
+constexpr size_t PERF_EVENT_ATTR_SIZE_FIELD_OFF = 4;
+constexpr size_t PERF_EVENT_ATTR_MIN_BYTES_FOR_SAMPLE_TYPE = 32;
+constexpr size_t PERF_EVENT_ATTR_SAMPLE_PERIOD_OFF = 16;
+constexpr size_t PERF_EVENT_ATTR_MIN_BYTES_THROUGH_SAMPLE_PERIOD = 24;
+constexpr size_t PERF_EVENT_ATTR_READ_FORMAT_OFF = 32;
+constexpr size_t PERF_EVENT_ATTR_MIN_BYTES_THROUGH_READ_FORMAT = 40;
+constexpr size_t PERF_EVENT_ATTR_FLAGS_OFF = 40;
+constexpr size_t PERF_EVENT_ATTR_MIN_BYTES_THROUGH_FLAGS = 48;
+constexpr uint64_t PERF_EVENT_ATTR_FREQ = 1ULL << 10;
+constexpr size_t PERF_EVENT_ATTR_BRANCH_SAMPLE_TYPE_OFF = 72;
+constexpr size_t PERF_EVENT_ATTR_MIN_BYTES_THROUGH_BRANCH_SAMPLE_TYPE = 80;
+constexpr size_t PERF_EVENT_ATTR_SAMPLE_REGS_USER_OFF = 80;
+constexpr size_t PERF_EVENT_ATTR_MIN_BYTES_THROUGH_SAMPLE_REGS_USER = 88;
+constexpr size_t PERF_EVENT_ATTR_SAMPLE_REGS_INTR_OFF = 96;
+constexpr size_t PERF_EVENT_ATTR_MIN_BYTES_THROUGH_SAMPLE_REGS_INTR = 104;
+constexpr uint16_t PERF_RECORD_MISC_PROC_MAP_PARSE_TIMEOUT = 1U << 12;
+/* The kernel reuses misc bit 13 with a meaning per record type.  */
+constexpr uint16_t PERF_RECORD_MISC_MMAP_DATA = 1U << 13;
+constexpr uint16_t PERF_RECORD_MISC_COMM_EXEC = 1U << 13;
+constexpr uint16_t PERF_RECORD_MISC_MMAP_BUILD_ID = 1U << 14;
+constexpr uint16_t PERF_RECORD_MISC_BUILD_ID_SIZE = 1U << 15;
+constexpr size_t PERF_EVENT_ATTR_TYPE_OFF = 0;
+constexpr size_t PERF_EVENT_ATTR_CONFIG_OFF = 8;
+constexpr size_t PERF_EVENT_ATTR_MIN_BYTES_THROUGH_CONFIG = 16;
+constexpr uint32_t PERF_TYPE_SOFTWARE = 1;
+constexpr uint64_t PERF_COUNT_SW_DUMMY = 9;
+constexpr unsigned HEADER_COMPRESSED_FEATURE_BIT = 27;
+
+/* Bytes of ATTR that both the file and this reader agree are present.  STRIDE
+   is the file's per-attribute stride, which also covers the trailing id
+   section, so the attribute's own size field bounds it.  */
+uint64_t
+effective_attr_size (const uint8_t *attr, uint64_t stride)
+{
+  if (stride < PERF_EVENT_ATTR_SIZE_FIELD_OFF + sizeof (uint32_t))
+    return stride;
+  const uint32_t published = read_u32 (attr + PERF_EVENT_ATTR_SIZE_FIELD_OFF);
+  return published > 0 ? std::min<uint64_t> (stride, published) : stride;
+}
+
+/* True for the software dummy event perf adds to track mmaps for AUX and
+   system-wide records.  It emits side-band records but never samples.  */
+bool
+attr_is_dummy (const uint8_t *attr, uint64_t eff_size)
+{
+  if (eff_size < PERF_EVENT_ATTR_MIN_BYTES_THROUGH_CONFIG)
+    return false;
+  return read_u32 (attr + PERF_EVENT_ATTR_TYPE_OFF) == PERF_TYPE_SOFTWARE
+        && read_u64 (attr + PERF_EVENT_ATTR_CONFIG_OFF) == PERF_COUNT_SW_DUMMY;
+}
+
+attr_sample_layout
+read_attr_sample_layout (const uint8_t *attr, uint64_t eff_size)
+{
+  attr_sample_layout layout;
+  layout.eff_size = eff_size;
+  if (eff_size >= PERF_EVENT_ATTR_MIN_BYTES_THROUGH_SAMPLE_PERIOD)
+    layout.sample_period = read_u64 (attr + PERF_EVENT_ATTR_SAMPLE_PERIOD_OFF);
+  if (eff_size >= PERF_EVENT_ATTR_MIN_BYTES_FOR_SAMPLE_TYPE)
+    layout.sample_type = read_u64 (attr + PERF_EVENT_ATTR_SAMPLE_TYPE_OFF);
+  if (eff_size >= PERF_EVENT_ATTR_MIN_BYTES_THROUGH_READ_FORMAT)
+    layout.read_format = read_u64 (attr + PERF_EVENT_ATTR_READ_FORMAT_OFF);
+  if (eff_size >= PERF_EVENT_ATTR_MIN_BYTES_THROUGH_BRANCH_SAMPLE_TYPE)
+    layout.branch_sample_type
+      = read_u64 (attr + PERF_EVENT_ATTR_BRANCH_SAMPLE_TYPE_OFF);
+  if (eff_size >= PERF_EVENT_ATTR_MIN_BYTES_THROUGH_SAMPLE_REGS_USER)
+    layout.sample_regs_user
+      = read_u64 (attr + PERF_EVENT_ATTR_SAMPLE_REGS_USER_OFF);
+  if (eff_size >= PERF_EVENT_ATTR_MIN_BYTES_THROUGH_SAMPLE_REGS_INTR)
+    layout.sample_regs_intr
+      = read_u64 (attr + PERF_EVENT_ATTR_SAMPLE_REGS_INTR_OFF);
+  if (eff_size >= PERF_EVENT_ATTR_MIN_BYTES_THROUGH_FLAGS)
+    layout.fixed_sample_period
+      = layout.sample_period != 0
+       && (layout.sample_type & perf_sample_type::PERIOD) != 0
+       && (read_u64 (attr + PERF_EVENT_ATTR_FLAGS_OFF)
+           & PERF_EVENT_ATTR_FREQ) == 0;
+  return layout;
+}
+
+/* True when A and B's samples decode the same way.  The period is not part
+   of this: a hybrid CPU gives each PMU its own default period.  */
+bool
+sample_layouts_agree (const attr_sample_layout &a, const attr_sample_layout &b)
+{
+  return a.sample_type == b.sample_type && a.read_format == b.read_format
+        && a.branch_sample_type == b.branch_sample_type
+        && a.sample_regs_user == b.sample_regs_user
+        && a.sample_regs_intr == b.sample_regs_intr;
+}
+
+/* Perf appends this marker to mappings of unlinked files.  */
+std::string
+normalize_mmap_filename (std::string filename)
+{
+  const size_t deleted = filename.find (" (deleted)");
+  if (deleted != std::string::npos)
+    filename.erase (deleted);
+  return filename;
+}
+
+struct segment_info
+{
+  uint64_t p_offset;
+  uint64_t p_filesz;
+  uint64_t p_vaddr;
+};
+
+struct elf_segment_cache
+{
+  std::vector<segment_info> segments;
+  bool is_pie = false;
+};
+
+std::unordered_map<std::string, elf_segment_cache> segment_cache;
+
+/* PT_LOAD table from ELF; empty on failure.  */
+std::vector<segment_info>
+build_segment_table (Elf *elf)
+{
+  std::vector<segment_info> segments;
+  size_t phnum = 0;
+  if (elf_getphdrnum (elf, &phnum) != 0)
+    return segments;
+  for (size_t i = 0; i < phnum; ++i)
+    {
+      GElf_Phdr phdr;
+      if (gelf_getphdr (elf, static_cast<int> (i), &phdr) != &phdr)
+       continue;
+      if (phdr.p_type != PT_LOAD)
+       continue;
+      segments.push_back ({static_cast<uint64_t> (phdr.p_offset),
+                          static_cast<uint64_t> (phdr.p_filesz),
+                          static_cast<uint64_t> (phdr.p_vaddr)});
+    }
+  return segments;
+}
+
+/* p_vaddr for pgoff from the segment table.  */
+bool
+lookup_vaddr (const std::vector<segment_info> &segments, uint64_t pgoff,
+             uint64_t &out)
+{
+  if (segments.empty ())
+    return false;
+
+  uint64_t fallback = 0;
+  bool have_fallback = false;
+  uint64_t first_nonzero_p_vaddr = 0;
+  bool have_first_nonzero = false;
+
+  for (const auto &s : segments)
+    {
+      if (!have_first_nonzero && s.p_vaddr != 0)
+       {
+         first_nonzero_p_vaddr = s.p_vaddr;
+         have_first_nonzero = true;
+       }
+
+      if (s.p_offset == pgoff)
+       {
+         out = s.p_vaddr;
+         return true;
+       }
+
+      if (s.p_offset <= pgoff && pgoff - s.p_offset < s.p_filesz)
+       {
+         const uint64_t offset = pgoff - s.p_offset;
+         if (!have_fallback && offset <= UINT64_MAX - s.p_vaddr)
+           {
+             fallback = s.p_vaddr + offset;
+             have_fallback = true;
+           }
+       }
+    }
+  if (have_fallback)
+    {
+      out = fallback;
+      return true;
+    }
+  /* pgoff 0 fallback: first nonzero PT_LOAD p_vaddr.  */
+  if (pgoff == 0 && have_first_nonzero)
+    {
+      out = first_nonzero_p_vaddr;
+      return true;
+    }
+  return false;
+}
+
+/* Lowercase hex text for the BUILD_ID_LEN bytes at BUILD_ID.  */
+std::string
+hex_encode_build_id (const uint8_t *build_id, size_t build_id_len)
+{
+  if (build_id == nullptr || build_id_len > SIZE_MAX / 2)
+    return {};
+  std::string encoded (build_id_len * 2, '\0');
+  for (size_t i = 0; i < build_id_len; i++)
+    {
+      encoded[2 * i] = HEX_DIGITS[build_id[i] >> 4];
+      encoded[2 * i + 1] = HEX_DIGITS[build_id[i] & 0x0f];
+    }
+  return encoded;
+}
+
+/* Parse .note.gnu.build-id from SHT_NOTE section payload.  */
+bool
+gnu_build_id_from_note_data (Elf_Data *data, std::string &out)
+{
+  size_t offset = 0;
+  GElf_Nhdr nhdr;
+  size_t name_off, desc_off;
+  while ((offset = gelf_getnote (data, offset, &nhdr, &name_off, &desc_off))
+        != 0)
+    {
+      if (nhdr.n_type != NT_GNU_BUILD_ID)
+       continue;
+      const char *note_name
+       = static_cast<const char *> (data->d_buf) + name_off;
+      if (nhdr.n_namesz < 4 || memcmp (note_name, "GNU", 4) != 0)
+       continue;
+
+      const uint8_t *build_id
+       = static_cast<const uint8_t *> (data->d_buf) + desc_off;
+      size_t build_id_len = nhdr.n_descsz;
+      out = hex_encode_build_id (build_id, build_id_len);
+      return true;
+    }
+  return false;
+}
+
+/* Open BINARY_PATH read-only, retrying a relative path through realpath.
+   Returns -1 on failure.  */
+int
+open_binary_read_only (const std::string &binary_path)
+{
+  int fd = open (binary_path.c_str (), O_RDONLY);
+  if (fd < 0 && !binary_path.empty () && binary_path[0] != '/')
+    {
+      char *resolved = realpath (binary_path.c_str (), nullptr);
+      if (resolved)
+       {
+         fd = open (resolved, O_RDONLY);
+         std::free (resolved);
+       }
+    }
+  return fd;
+}
+
+/* ELF .note.gnu.build-id as hex.  */
+std::string
+get_buildid_from_binary (const std::string &binary_path)
+{
+  if (elf_version (EV_CURRENT) == EV_NONE)
+    return {};
+
+  int fd = open_binary_read_only (binary_path);
+  if (fd < 0)
+    return {};
+
+  Elf *elf = elf_begin (fd, ELF_C_READ, nullptr);
+  if (!elf)
+    {
+      close (fd);
+      return {};
+    }
+
+  if (elf_kind (elf) != ELF_K_ELF)
+    {
+      elf_end (elf);
+      close (fd);
+      return {};
+    }
+
+  Elf_Scn *scn = nullptr;
+  while ((scn = elf_nextscn (elf, scn)) != nullptr)
+    {
+      GElf_Shdr shdr;
+      if (gelf_getshdr (scn, &shdr) == nullptr)
+       continue;
+      if (shdr.sh_type != SHT_NOTE)
+       continue;
+
+      Elf_Data *data = elf_getdata (scn, nullptr);
+      if (data == nullptr || data->d_size == 0)
+       continue;
+
+      std::string decrypted;
+      if (gnu_build_id_from_note_data (data, decrypted))
+       {
+         elf_end (elf);
+         close (fd);
+         return decrypted;
+       }
+    }
+
+  elf_end (elf);
+  close (fd);
+  return {};
+}
+
+} /* anonymous namespace.  */
+
+/* PT_LOAD p_vaddr for pgoff; false if not ELF or no match.  */
+bool
+get_elf_segment_vaddr_for_pgoff (const std::string &binary_path, uint64_t 
pgoff,
+                                uint64_t &out)
+{
+  if (elf_version (EV_CURRENT) == EV_NONE)
+    return false;
+
+  auto it = segment_cache.find (binary_path);
+  if (it == segment_cache.end ())
+    {
+      int fd = open_binary_read_only (binary_path);
+      if (fd < 0)
+       return false;
+
+      /* libelf reads lazily, so FD must outlive every call up to elf_end.  */
+      Elf *elf = elf_begin (fd, ELF_C_READ, nullptr);
+      if (!elf)
+       {
+         close (fd);
+         return false;
+       }
+
+      if (elf_kind (elf) != ELF_K_ELF)
+       {
+         elf_end (elf);
+         close (fd);
+         return false;
+       }
+
+      elf_segment_cache cache;
+      cache.segments = build_segment_table (elf);
+      GElf_Ehdr ehdr;
+      if (gelf_getehdr (elf, &ehdr) == &ehdr)
+       cache.is_pie = (ehdr.e_type == ET_DYN);
+      elf_end (elf);
+      close (fd);
+      it = segment_cache.emplace (binary_path, std::move (cache)).first;
+    }
+
+  return lookup_vaddr (it->second.segments, pgoff, out);
+}
+
+/* True if binary is PIE (ET_DYN); requires prior segment cache fill.  */
+bool
+get_elf_is_pie (const std::string &binary_path)
+{
+  auto it = segment_cache.find (binary_path);
+  return it != segment_cache.end () && it->second.is_pie;
+}
+
+perf_file_reader::~perf_file_reader ()
+{
+  cleanup ();
+}
+
+bool
+perf_file_reader::load ()
+{
+  cleanup ();
+  if (!open_and_map_file ())
+    return false;
+
+  if (!parse_header ())
+    return false;
+
+  if (!parse_perf_data_section ())
+    return false;
+
+  if (lost_events_ != 0)
+    fprintf (stderr,
+            _("Warning: the kernel dropped %" PRIu64 " event(s) while "
+              "recording %s; the profile is incomplete\n"),
+            lost_events_,
+            perf_data_path.c_str ());
+
+  if (target_mmaps_by_pid_.empty ())
+    {
+      std::string base
+       = binary_path.substr (binary_path.find_last_of ("/\\") + 1);
+      fprintf (stderr,
+              _("Error: No memory mappings found for '%s'. perf.data may be "
+                "from a different binary.  Ensure it was collected while "
+                "running: %s\n"),
+              base.c_str (),
+              binary_path.c_str ());
+      return false;
+    }
+
+  /* Samples were translated through their own PID's mappings as they were
+     ingested, so the mappings are no longer needed.  */
+  clear_runtime_translation_state ();
+  return true;
+}
+
+bool
+perf_file_reader::open_and_map_file ()
+{
+  if (!mapped_file_.map (perf_data_path))
+    {
+      fprintf (stderr,
+              _("Failed to mmap perf.data file: %s (errno: %d)\n"),
+              perf_data_path.c_str (),
+              errno);
+      return false;
+    }
+  return true;
+}
+
+void
+perf_file_reader::cleanup ()
+{
+  mapped_file_.unmap ();
+  reset_profile_state ();
+  address_frequencies.clear ();
+  target_mmaps_by_pid_.clear ();
+  target_pid_by_tid_.clear ();
+  executable_mmaps_by_pid_.clear ();
+  target_mapping_pids_.clear ();
+  unstable_target_pids_.clear ();
+  buildid_table_.clear ();
+  buildid_table_valid_ = false;
+  cached_target_build_id_.clear ();
+  cached_target_build_id_valid_ = false;
+  reported_build_id_mismatch_ = false;
+  total_samples = 0;
+  lost_events_ = 0;
+  header = {};
+  sample_layout = attr_sample_layout ();
+}
+
+/* Parse perf.data file header.  */
+bool
+perf_file_reader::parse_header ()
+{
+  if (mapped_file_.size () < sizeof (header))
+    {
+      fprintf (stderr, _("File too small to contain perf header\n"));
+      return false;
+    }
+
+  std::memcpy (&header, mapped_file_.data (), sizeof (header));
+
+  if (header.magic != PERF_MAGIC)
+    {
+      if (header.magic == PERF_MAGIC_SWAPPED)
+       fprintf (stderr,
+                _("Error: big-endian perf.data is not supported\n"));
+      else
+       fprintf (stderr,
+                _("Invalid perf.data magic number: 0x%" PRIx64 "\n"),
+                header.magic);
+      return false;
+    }
+
+  const uint8_t *const base = mapped_file_.data ();
+  if ((header.features >> HEADER_COMPRESSED_FEATURE_BIT) & 1ULL)
+    {
+      fprintf (stderr, _("Error: compressed perf.data is not supported\n"));
+      return false;
+    }
+
+  uint64_t sampling_attrs = 0;
+  if (header.attrs_size > 0 && header.attrs_offset > 0)
+    {
+      if (header.attrs_offset >= mapped_file_.size ()
+         || header.attrs_size > mapped_file_.size () - header.attrs_offset)
+       {
+         fprintf (stderr,
+                  _("Error: perf.data attribute table is truncated\n"));
+         return false;
+       }
+
+      if (header.attr_size == 0)
+       {
+         fprintf (stderr,
+                  _("Error: perf.data declares a zero attribute size\n"));
+         return false;
+       }
+
+      /* SAMPLE records carry no attribute id, so every sampling event has to
+        decode the same way.  A hybrid CPU records one event per PMU.  */
+      const uint8_t *const attrs = base + header.attrs_offset;
+      const uint64_t num_attrs = header.attrs_size / header.attr_size;
+      attr_sample_layout chosen;
+      bool periods_differ = false;
+
+      for (uint64_t i = 0; i < num_attrs; ++i)
+       {
+         const uint8_t *const attr = attrs + i * header.attr_size;
+         const uint64_t eff_size
+           = effective_attr_size (attr, header.attr_size);
+         if (attr_is_dummy (attr, eff_size))
+           continue;
+
+         const attr_sample_layout layout
+           = read_attr_sample_layout (attr, eff_size);
+         if (sampling_attrs != 0 && !sample_layouts_agree (layout, chosen))
+           {
+             fprintf (stderr,
+                      _("Error: perf.data records sampling events that "
+                        "disagree on sample layout; record one event at a "
+                        "time\n"));
+             return false;
+           }
+         if (sampling_attrs != 0
+             && (layout.sample_period != chosen.sample_period
+                 || layout.fixed_sample_period != chosen.fixed_sample_period))
+           periods_differ = true;
+         ++sampling_attrs;
+         chosen = layout;
+       }
+
+      sample_layout = chosen;
+      /* TODO: weight each sample by its own event's period.  The events are
+        told apart by the id that PERF_SAMPLE_IDENTIFIER puts in every
+        sample.  */
+      if (periods_differ)
+       fprintf (stderr,
+                _("Warning: perf.data records sampling events with "
+                  "different periods; samples are still counted as equal "
+                  "mass\n"));
+    }
+
+  if (sampling_attrs == 0)
+    {
+      fprintf (stderr, _("Error: perf.data has no sampling event\n"));
+      return false;
+    }
+
+  if ((sample_layout.sample_type & perf_sample_type::TID) == 0)
+    {
+      fprintf (stderr,
+              _("Error: perf.data samples must include PERF_SAMPLE_TID\n"));
+      return false;
+    }
+  /* Correlation gives every sample equal mass, which only reflects the
+     workload when the event counts on a fixed period.  */
+  if (use_branch_stack_for_correlation_
+      && (sample_layout.sample_type & perf_sample_type::BRANCH_STACK) != 0
+      && !sample_layout.fixed_sample_period)
+    fprintf (stderr,
+            _("Warning: perf.data was not recorded with a fixed sampling "
+              "period; samples are still counted as equal mass\n"));
+
+  return true;
+}
+
+void
+perf_file_reader::clear_runtime_translation_state ()
+{
+  target_mmaps_by_pid_.clear ();
+  target_mmaps_by_pid_.rehash (0);
+  target_pid_by_tid_.clear ();
+  target_pid_by_tid_.rehash (0);
+  executable_mmaps_by_pid_.clear ();
+  executable_mmaps_by_pid_.rehash (0);
+  target_mapping_pids_.clear ();
+  unstable_target_pids_.clear ();
+  buildid_table_.clear ();
+  buildid_table_valid_ = false;
+  cached_target_build_id_.clear ();
+  cached_target_build_id_valid_ = false;
+}
+
+/* Invoke FN for every record of the perf.data data section.  LOG selects the
+   diagnostic prefix.  Returns false when the file is truncated or a malformed
+   record ends the scan early, so callers do not accept a prefix of the
+   profile as complete.  Kernel-reported loss is tallied in LOST_EVENTS_.  */
+bool
+perf_file_reader::for_each_mapped_perf_data_record (
+  const std::function<void (const perf_event_header &hdr,
+                           const uint8_t *payload, size_t payload_size,
+                           size_t skip_whole)> &fn,
+  perf_scan_log log)
+{
+  const char *const tag = (log == perf_scan_log::SPE ? "SPE" : "PERF");
+  if (header.data_offset > mapped_file_.size ())
+    {
+      fprintf (stderr,
+              _("%s: perf.data data section starts past end of file.\n"),
+              tag);
+      return false;
+    }
+  const size_t data_offset = static_cast<size_t> (header.data_offset);
+  const size_t available = mapped_file_.size () - data_offset;
+
+  bool complete = true;
+  if (header.data_size > available)
+    {
+      fprintf (stderr,
+              _("%s: perf.data is truncated: header declares %" PRIu64
+                " data bytes, %zu present.\n"),
+              tag,
+              header.data_size,
+              available);
+      complete = false;
+    }
+
+  const uint8_t *local_ptr = mapped_file_.data () + data_offset;
+  size_t local_rem = header.data_size > available
+                      ? available
+                      : static_cast<size_t> (header.data_size);
+  size_t total_records = 0;
+  uint64_t lost = 0;
+
+  while (local_rem >= sizeof (perf_event_header))
+    {
+      total_records++;
+      perf_event_header record_header;
+      std::memcpy (&record_header, local_ptr, sizeof (perf_event_header));
+      if (record_header.size == 0
+         || record_header.size < sizeof (perf_event_header)
+         || record_header.size > local_rem)
+       {
+         fprintf (stderr,
+                  _("%s: Invalid record size (%u) at record %zu.  "
+                    "Stopping.\n"),
+                  tag,
+                  (unsigned) record_header.size,
+                  total_records);
+         complete = false;
+         break;
+       }
+
+      const uint8_t *payload_ptr = local_ptr + sizeof (perf_event_header);
+      const size_t payload_size
+       = record_header.size - sizeof (perf_event_header);
+
+      if (record_header.type == perf_record_type::COMPRESSED
+         || record_header.type == perf_record_type::COMPRESSED2)
+       {
+         fprintf (stderr,
+                  _("%s: compressed perf records are not supported\n"),
+                  tag);
+         return false;
+       }
+      if ((record_header.type == perf_record_type::MMAP
+          || record_header.type == perf_record_type::MMAP2)
+         && (record_header.misc & PERF_RECORD_MISC_PROC_MAP_PARSE_TIMEOUT))
+       {
+         fprintf (stderr,
+                  _("%s: process mapping scan timed out; the mapping set is "
+                    "incomplete\n"),
+                  tag);
+         return false;
+       }
+
+      const size_t skip
+       = perf_record_skip_bytes (record_header, payload_ptr, local_rem);
+      if (skip == 0)
+       {
+         fprintf (stderr,
+                  _("%s: Invalid record advance at record %zu (type %u). "
+                    "Stopping.\n"),
+                  tag,
+                  total_records,
+                  (unsigned) record_header.type);
+         complete = false;
+         break;
+       }
+
+      /* PERF_RECORD_LOST is { u64 id; u64 lost; }; PERF_RECORD_LOST_SAMPLES
+        is { u64 lost; }.  */
+      if (record_header.type == perf_record_type::LOST
+         && payload_size >= 2 * sizeof (uint64_t))
+       lost += read_u64 (payload_ptr + sizeof (uint64_t));
+      else if (record_header.type == perf_record_type::LOST_SAMPLES
+              && payload_size >= sizeof (uint64_t))
+       lost += read_u64 (payload_ptr);
+
+      local_ptr += skip;
+      local_rem -= skip;
+      fn (record_header, payload_ptr, payload_size, skip);
+    }
+
+  /* A well-formed data section ends on a record boundary.  Only report the
+     tail when the scan itself found nothing else to complain about.  */
+  if (complete && local_rem != 0)
+    {
+      fprintf (stderr,
+              _("%s: %zu trailing byte(s) after the last record.\n"),
+              tag,
+              local_rem);
+      complete = false;
+    }
+
+  /* Each pass sees the same records, so keep the largest tally.  */
+  if (lost > lost_events_)
+    lost_events_ = lost;
+  return complete;
+}
+
+/* Bytes to advance past this record, counting the AUXTRACE inline tail.
+   Returns 0 when the record does not fit in DATA_REMAINING.  */
+size_t
+perf_file_reader::perf_record_skip_bytes (
+  const perf_event_header &record_header, const uint8_t *payload,
+  size_t data_remaining) const
+{
+  if (record_header.size < sizeof (perf_event_header))
+    return 0;
+  if (record_header.size > data_remaining)
+    return 0;
+
+  if (record_header.type != static_cast<uint32_t> (perf_record_type::AUXTRACE))
+    return record_header.size;
+
+  const size_t payload_size = record_header.size - sizeof (perf_event_header);
+  if (payload_size < sizeof (uint64_t))
+    return record_header.size;
+
+  uint64_t inline_tail = read_u64 (payload);
+  const size_t base = record_header.size;
+  if (inline_tail > SIZE_MAX - base)
+    return 0;
+  const size_t total = base + static_cast<size_t> (inline_tail);
+  if (total > data_remaining)
+    return 0;
+  return total;
+}
+
+void
+perf_file_reader::observe_executable_mmap (uint32_t pid, uint64_t start,
+                                          uint64_t end, uint64_t pgoff,
+                                          bool is_target)
+{
+  std::vector<mmap_observation> &history = executable_mmaps_by_pid_[pid];
+  for (const mmap_observation &old : history)
+    {
+      if (start >= old.end || end <= old.start)
+       continue;
+      if (old.is_target != is_target
+         || (is_target
+             && (start != old.start || end != old.end
+                 || pgoff != old.pgoff)))
+       unstable_target_pids_.insert (pid);
+    }
+  history.push_back ({start, end, pgoff, is_target});
+}
+
+void
+perf_file_reader::note_process_lifetime_record (
+  const perf_event_header &record_header, const uint8_t *payload,
+  size_t payload_size)
+{
+  if (payload == nullptr || payload_size < 2 * sizeof (uint32_t))
+    return;
+  if (record_header.type == perf_record_type::COMM
+      && (record_header.misc & PERF_RECORD_MISC_COMM_EXEC) != 0)
+    {
+      const uint32_t pid = read_u32 (payload);
+      if (target_mapping_pids_.count (pid) != 0)
+       unstable_target_pids_.insert (pid);
+    }
+}
+
+bool
+perf_file_reader::target_mapping_lifetime_valid () const
+{
+  if (unstable_target_pids_.empty ())
+    return true;
+  fprintf (stderr,
+          _("Error: perf.data changes the executable mapping lifetime for "
+            "a target process; this is not supported\n"));
+  /* TODO: Use sample_id_all timestamps and ordered per-PID mapping history
+     to support exec, unmap, PID reuse and cross-CPU record reordering.  */
+  return false;
+}
+
+/* Record the target binary's half-open START to END range and resolve its
+   link-time address.  Keep each mapping separate: pgoff alone does not
+   establish that two runtime intervals have the same affine translation.  */
+void
+perf_file_reader::add_target_mmap (uint32_t pid, uint32_t tid,
+                                  const std::string &mmap_filename,
+                                  uint64_t start, uint64_t end, uint64_t pgoff)
+{
+  target_mapping_pids_.insert (pid);
+  std::string elf_path = normalize_mmap_filename (mmap_filename);
+  if (elf_path.empty ())
+    elf_path = binary_path;
+
+  uint64_t vaddr = 0;
+  bool have_vaddr
+    = get_elf_segment_vaddr_for_pgoff (elf_path, pgoff, vaddr);
+  if (!have_vaddr && elf_path != binary_path)
+    get_elf_segment_vaddr_for_pgoff (binary_path, pgoff, vaddr);
+  const bool is_pie
+    = get_elf_is_pie (elf_path) || get_elf_is_pie (binary_path);
+  target_mmaps_by_pid_[pid].emplace_back (
+    start, end, pgoff, vaddr, is_pie);
+  target_pid_by_tid_[tid] = pid;
+}
+
+bool
+perf_file_reader::parse_mmap_record (const uint8_t *record_data,
+                                    size_t record_size, uint16_t misc)
+{
+  if (record_size < sizeof (perf_mmap_record))
+    return false; /* Record too small.  */
+
+  perf_mmap_record mmap;
+  std::memcpy (&mmap, record_data, sizeof (mmap));
+
+  /* Filename follows the fixed-size header; copy its bounded record bytes.  */
+  const char *filename
+    = reinterpret_cast<const char *> (record_data + sizeof (perf_mmap_record));
+  const size_t filename_max_len = record_size - sizeof (perf_mmap_record);
+  const std::string mmap_filename
+    = perf_mmap_filename_view (filename, filename_max_len);
+
+  if (misc & PERF_RECORD_MISC_MMAP_DATA)
+    return true;
+  if (!is_valid_user_address (mmap.addr))
+    return true;
+
+  uint64_t end = 0;
+  if (!try_compute_mmap_end (mmap.addr, mmap.len, &end))
+    return true;
+  const bool is_target = is_target_binary (mmap_filename, "");
+  observe_executable_mmap (
+    mmap.pid, mmap.addr, end, mmap.pgoff, is_target);
+  if (!is_target)
+    return true;
+
+  add_target_mmap (
+    mmap.pid, mmap.tid, mmap_filename, mmap.addr, end, mmap.pgoff);
+  return true;
+}
+
+bool
+perf_file_reader::parse_mmap2_record (const uint8_t *record_data,
+                                     size_t record_size, uint16_t misc)
+{
+  if (record_size < sizeof (perf_mmap2_record))
+    return false; /* Record too small.  */
+
+  perf_mmap2_record mmap2;
+  std::memcpy (&mmap2, record_data, sizeof (mmap2));
+
+  const char *filename
+    = reinterpret_cast<const char *> (record_data + sizeof 
(perf_mmap2_record));
+  const size_t filename_max_len = record_size - sizeof (perf_mmap2_record);
+  const std::string mmap_filename
+    = perf_mmap_filename_view (filename, filename_max_len);
+
+  if ((mmap2.prot & PROT_EXEC) == 0)
+    return true;
+  if (!is_valid_user_address (mmap2.addr))
+    return true;
+
+  uint64_t end = 0;
+  if (!try_compute_mmap_end (mmap2.addr, mmap2.len, &end))
+    return true;
+
+  std::string build_id;
+  if (misc & PERF_RECORD_MISC_MMAP_BUILD_ID)
+    {
+      const size_t build_id_size = std::min<size_t> (
+       mmap2.id.build.build_id_size, sizeof (mmap2.id.build.build_id));
+      build_id = hex_encode_build_id (mmap2.id.build.build_id, build_id_size);
+    }
+  const bool is_target = is_target_binary (mmap_filename, build_id);
+  observe_executable_mmap (
+    mmap2.pid, mmap2.addr, end, mmap2.pgoff, is_target);
+  if (!is_target)
+    return true;
+
+  add_target_mmap (
+    mmap2.pid, mmap2.tid, mmap_filename, mmap2.addr, end, mmap2.pgoff);
+  return true;
+}
+
+/* Match a mapping against the profiled binary, by build ID where one is
+   known and by basename otherwise.  */
+bool
+perf_file_reader::is_target_binary (const std::string &mmap_filename,
+                                   const std::string &build_id) const
+{
+  if (mmap_filename.empty ())
+    return false;
+  const std::string mmap_path = normalize_mmap_filename (mmap_filename);
+
+  if (!cached_target_build_id_valid_)
+    {
+      cached_target_build_id_ = get_buildid_from_binary (binary_path);
+      cached_target_build_id_valid_ = true;
+    }
+  if (!buildid_table_valid_)
+    parse_build_id_table ();
+
+  size_t tp = binary_path.find_last_of ("/\\");
+  std::string target_basename
+    = (tp != std::string::npos) ? binary_path.substr (tp + 1) : binary_path;
+  size_t mp = mmap_path.find_last_of ("/\\");
+  std::string mmap_basename
+    = (mp != std::string::npos) ? mmap_path.substr (mp + 1) : mmap_path;
+  const bool basename_matches = mmap_basename == target_basename;
+
+  /* Prefer the build ID from MMAP2, else the one HEADER_BUILD_ID recorded for
+     this path, which also covers a renamed or relocated binary.  */
+  std::string recorded_build_id = build_id;
+  if (recorded_build_id.empty ())
+    {
+      const auto it = buildid_table_.find (mmap_path);
+      if (it != buildid_table_.end ())
+       recorded_build_id = it->second;
+    }
+
+  /* A recorded build ID decides the match whenever the requested binary has
+     one too; falling back to the basename would accept a different build.  */
+  if (!recorded_build_id.empty () && !cached_target_build_id_.empty ())
+    {
+      if (recorded_build_id == cached_target_build_id_)
+       return true;
+      if (basename_matches)
+       report_build_id_mismatch (mmap_path, recorded_build_id);
+      return false;
+    }
+
+  return basename_matches;
+}
+
+/* Report once that perf.data profiled a different build of the binary named
+   by MMAP_PATH, which is otherwise the requested one.  */
+void
+perf_file_reader::report_build_id_mismatch (
+  const std::string &mmap_path, const std::string &recorded_build_id) const
+{
+  if (reported_build_id_mismatch_)
+    return;
+  reported_build_id_mismatch_ = true;
+  fprintf (stderr,
+          _("Warning: perf.data profiled a different build of %s\n"
+            "  build ID in perf.data: %s\n"
+            "  build ID of %s: %s\n"),
+          mmap_path.c_str (),
+          recorded_build_id.c_str (),
+          binary_path.c_str (),
+          cached_target_build_id_.c_str ());
+}
+
+/* Reject kernel and very low addresses.  */
+bool
+perf_file_reader::is_valid_user_address (uint64_t addr) const
+{
+  const bool is_kernel = (addr & KERNEL_ADDRESS_MASK) == KERNEL_ADDRESS_MASK;
+  return addr >= MIN_USER_ADDRESS && !is_kernel;
+}
+
+const std::vector<simple_mmap> *
+perf_file_reader::target_mmaps_for_pid (uint32_t pid) const
+{
+  const auto it = target_mmaps_by_pid_.find (pid);
+  return it == target_mmaps_by_pid_.end () ? nullptr : &it->second;
+}
+
+size_t
+perf_file_reader::target_mmap_count () const
+{
+  size_t count = 0;
+  for (const auto &entry : target_mmaps_by_pid_)
+    count += entry.second.size ();
+  return count;
+}
+
+/* Parse HEADER_BUILD_ID into a filename-to-hex-build-id map.  */
+void
+perf_file_reader::parse_build_id_table () const
+{
+  buildid_table_valid_ = true;
+  if (!mapped_file_)
+    return;
+
+  constexpr int BUILD_ID_FEATURE_BIT = 2;
+  const uint64_t feats = header.features;
+  if (!((feats >> BUILD_ID_FEATURE_BIT) & 1ULL))
+    return;
+
+  const uint8_t *const base = mapped_file_.data ();
+  const size_t file_size = mapped_file_.size ();
+  if (header.data_offset > file_size
+      || header.data_size > file_size - header.data_offset)
+    return;
+  const uint64_t sec_table = header.data_offset + header.data_size;
+  /* Section index = number of feature bits set below bit 2.  */
+  int index = 0;
+  for (int b = 0; b < BUILD_ID_FEATURE_BIT; ++b)
+    if ((feats >> b) & 1ULL)
+      ++index;
+  const uint64_t entry_off
+    = sec_table + static_cast<uint64_t> (index) * 16; /* {u64 off; u64 size;} 
*/
+  if (entry_off > file_size || 16 > file_size - entry_off)
+    return;
+  uint64_t bid_off = read_u64 (base + entry_off);
+  uint64_t bid_size = read_u64 (base + entry_off + 8);
+  if (bid_size == 0 || bid_off > file_size || bid_size > file_size - bid_off)
+    return;
+
+  const uint8_t *p = base + bid_off;
+  const uint8_t *const end = p + bid_size;
+  /* build_id_event: hdr(8) + pid(4) + build_id[24] + filename.  */
+  constexpr size_t BUILD_ID_NAME_OFF = sizeof (perf_event_header) + 4 + 24;
+  constexpr size_t BUILD_ID_OFF = sizeof (perf_event_header) + 4;
+  constexpr size_t BUILD_ID_DATA_BYTES = 20;
+  constexpr size_t BUILD_ID_SIZE_OFF = BUILD_ID_OFF + BUILD_ID_DATA_BYTES;
+  while (static_cast<size_t> (end - p) >= sizeof (perf_event_header))
+    {
+      perf_event_header h;
+      std::memcpy (&h, p, sizeof h);
+      if (h.size < sizeof (perf_event_header)
+         || h.size > static_cast<size_t> (end - p))
+       break;
+      if (h.size > BUILD_ID_NAME_OFF)
+       {
+         size_t build_id_len = BUILD_ID_DATA_BYTES;
+         if (h.misc & PERF_RECORD_MISC_BUILD_ID_SIZE)
+           {
+             build_id_len = p[BUILD_ID_SIZE_OFF];
+             if (build_id_len == 0 || build_id_len > BUILD_ID_DATA_BYTES)
+               {
+                 p += h.size;
+                 continue;
+               }
+           }
+         std::string hex
+           = hex_encode_build_id (p + BUILD_ID_OFF, build_id_len);
+         const char *fname
+           = reinterpret_cast<const char *> (p + BUILD_ID_NAME_OFF);
+         const size_t maxlen = h.size - BUILD_ID_NAME_OFF;
+         const size_t flen = ::strnlen (fname, maxlen);
+         if (flen != 0)
+           buildid_table_.emplace (
+             normalize_mmap_filename (std::string (fname, flen)),
+             std::move (hex));
+       }
+      p += h.size;
+    }
+}
+
+} /* namespace perf_data.  */
diff --git a/afdo_tools/src/perf_file_reader.h 
b/afdo_tools/src/perf_file_reader.h
new file mode 100644
index 00000000000..5fed76efe8f
--- /dev/null
+++ b/afdo_tools/src/perf_file_reader.h
@@ -0,0 +1,176 @@
+/* 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_PERF_FILE_READER_H
+#define GCC_PERF_FILE_READER_H
+
+#include "file_mmap.h"
+#include "perf.h"
+#include "profile_reader.h"
+
+#include <cstdint>
+#include <functional>
+#include <map>
+#include <memory>
+#include <string>
+#include <unordered_map>
+#include <unordered_set>
+#include <utility>
+#include <vector>
+
+namespace perf_data {
+
+enum class sampling_mode : uint8_t
+{
+  BRANCH_STACK,
+  SPE
+};
+
+/* Scan-log prefix for record diagnostics.  */
+enum class perf_scan_log : uint8_t
+{
+  GENERIC,
+  SPE
+};
+
+/* perf_event_attr fields that determine the SAMPLE record layout.  */
+struct attr_sample_layout
+{
+  uint64_t eff_size = 0;
+  uint64_t sample_type = DEFAULT_BRANCH_STACK_SAMPLE_TYPE;
+  uint64_t read_format = 0;
+  uint64_t branch_sample_type = 0;
+  uint64_t sample_regs_user = 0;
+  uint64_t sample_regs_intr = 0;
+  uint64_t sample_period = 0;
+  /* True when every sample carries the same known mass: the event counts on a
+     fixed period and each sample reports that period.  */
+  bool fixed_sample_period = false;
+};
+
+/* Offline perf.data loader; subclasses ingest samples.  */
+class perf_file_reader : public profile_reader
+{
+public:
+  perf_file_reader (const std::string &_perf_data_path,
+                   const std::string &_binary_path)
+    : perf_data_path (_perf_data_path), binary_path (_binary_path),
+      total_samples (0), header{}
+  {}
+
+  ~perf_file_reader () override;
+
+  bool load () override;
+  const std::unordered_map<uint64_t, uint64_t> &
+  get_address_frequencies () const override
+  {
+    return address_frequencies;
+  }
+  uint64_t get_total_samples () const override { return total_samples; }
+
+protected:
+  /* Lifecycle / I/O.  */
+  bool open_and_map_file ();
+  void cleanup ();
+  bool parse_header ();
+  virtual bool parse_perf_data_section () = 0;
+  void clear_runtime_translation_state ();
+
+  /* Record scan.  Returns false on truncation or a malformed record.  */
+  bool for_each_mapped_perf_data_record (
+    const std::function<void (const perf_event_header &hdr,
+                             const uint8_t *payload, size_t payload_size,
+                             size_t skip_whole)> &fn,
+    perf_scan_log log = perf_scan_log::GENERIC);
+  size_t perf_record_skip_bytes (const perf_event_header &record_header,
+                                const uint8_t *payload,
+                                size_t data_remaining) const;
+  bool parse_mmap_record (const uint8_t *record_data, size_t record_size,
+                         uint16_t misc);
+  bool parse_mmap2_record (const uint8_t *record_data, size_t record_size,
+                          uint16_t misc);
+  void add_target_mmap (uint32_t pid, uint32_t tid,
+                       const std::string &mmap_filename, uint64_t start,
+                       uint64_t end, uint64_t pgoff);
+  void note_process_lifetime_record (const perf_event_header &header,
+                                    const uint8_t *payload,
+                                    size_t payload_size);
+  void observe_executable_mmap (uint32_t pid, uint64_t start, uint64_t end,
+                               uint64_t pgoff, bool is_target);
+  bool target_mapping_lifetime_valid () const;
+
+  /* Target binary / address filters.  */
+  bool is_target_binary (const std::string &mmap_filename,
+                        const std::string &build_id) const;
+  void report_build_id_mismatch (const std::string &mmap_path,
+                                const std::string &recorded_build_id) const;
+  bool is_valid_user_address (uint64_t addr) const;
+  const std::vector<simple_mmap> *target_mmaps_for_pid (uint32_t pid) const;
+  size_t target_mmap_count () const;
+  void parse_build_id_table () const;
+
+  const uint8_t *mapped_bytes () const { return mapped_file_.data (); }
+  size_t mapped_size () const { return mapped_file_.size (); }
+
+  /* Paths and sample mass.  */
+  std::string perf_data_path;
+  std::string binary_path;
+  uint64_t total_samples;
+  std::unordered_map<uint64_t, uint64_t> address_frequencies;
+  pid_mmap_map target_mmaps_by_pid_;
+  std::unordered_map<uint32_t, uint32_t> target_pid_by_tid_;
+
+  struct mmap_observation
+  {
+    uint64_t start;
+    uint64_t end;
+    uint64_t pgoff;
+    bool is_target;
+  };
+  std::unordered_map<uint32_t, std::vector<mmap_observation>>
+    executable_mmaps_by_pid_;
+  std::unordered_set<uint32_t> target_mapping_pids_;
+  std::unordered_set<uint32_t> unstable_target_pids_;
+
+  /* Build-id cache and HEADER_BUILD_ID table.  */
+  mutable std::string cached_target_build_id_;
+  mutable bool cached_target_build_id_valid_ = false;
+  mutable std::map<std::string, std::string, std::less<>> buildid_table_;
+  mutable bool buildid_table_valid_ = false;
+  mutable bool reported_build_id_mismatch_ = false;
+
+  /* Mapped perf.data and sample attributes.  */
+  afdo_tools::file_mmap mapped_file_;
+  perf_file_header header;
+  attr_sample_layout sample_layout;
+  /* Events the kernel dropped while recording (PERF_RECORD_LOST*).  */
+  uint64_t lost_events_ = 0;
+};
+
+std::unique_ptr<profile_reader>
+create_perf_reader (const std::string &perf_data_path,
+                   const std::string &binary_path, sampling_mode mode);
+
+bool get_elf_segment_vaddr_for_pgoff (const std::string &binary_path,
+                                     uint64_t pgoff, uint64_t &out);
+
+bool get_elf_is_pie (const std::string &binary_path);
+
+} /* namespace perf_data.  */
+
+#endif /* GCC_PERF_FILE_READER_H.  */
-- 
2.34.1

Reply via email to