Again I'm a bit sceptical that a custom live collector is a good idea,
but given you don't support pipe mode for perf output it may be needed.

It has a lot of missing features compared to real perf record though!
For example a common use you don't support is profiling a cgroup aka
container.

On Thu, Sep 17, 2026 at 10:39:14AM +1000, Kugan Vivekanandarajah wrote:
> +{
> +  uint64_t total = 0;
> +  for (const profile_range &row : ranges)
> +    {
> +      if (row.end >= row.beg)
> +     total += row.count * (row.end - row.beg + 1);
> +    }
> +  return total;
> +}
> +
> +/* Cap on data pages per CPU, to bound total buffer memory.  */
> +constexpr size_t MAX_RING_DATA_PAGES = 256;

That seems far too big for an accumulating collector.

> +/* Online CPU ids, parsed from the kernel's list ("0-3,8").  Falls back to
> +   0 to N-1 when the list cannot be read.  */
> +std::vector<int>
> +online_cpu_ids ()
> +{
> +  std::vector<int> cpus;
> +  if (FILE *f = fopen ("/sys/devices/system/cpu/online", "r"))

Better use sched_getaffinity. it's easier and more truly gives you
the CPUs you can really access in containers etc..  The only case where it 
breaks
is when the workload widens the affinity, but I guess that's
acceptable.

> +
> +/* Data pages to request per CPU.  perf_event_mlock_kb is a per-CPU limit, so
> +   asking for more fails once every CPU is mapped.  */
> +size_t
> +ring_data_pages (size_t page_size)
> +{
> +  /* The kernel's default, if the sysctl cannot be read.  */
> +  unsigned long mlock_kb = 512;
> +  if (FILE *f = fopen ("/proc/sys/kernel/perf_event_mlock_kb", "r"))
> +    {
> +      unsigned long value = 0;
> +      if (fscanf (f, "%lu", &value) == 1 && value != 0)
> +     mlock_kb = value;
> +      fclose (f);
> +    }
> +
> +  /* One page goes to the metadata page, and the count must be a power of
> +     two.  */
> +  const size_t budget = (mlock_kb * 1024) / page_size;
> +  if (budget < 2)
> +    return 1;
> +  size_t pages = 1;
> +  while (pages * 2 <= budget - 1 && pages < MAX_RING_DATA_PAGES)
> +    pages *= 2;
> +  return pages;


This likely too big, so that parallel builds run out (gcc itself
has this problem) It definitely needs a user override.

Also when you just accumulate you shouldn't need that much anyways.

> +/* The component of PATH after its last separator, or PATH when it has none. 
>  */
> +static std::string
> +path_basename (const std::string &path)
> +{
> +  size_t slash = path.find_last_of ("/\\");
> +  return slash != std::string::npos ? path.substr (slash + 1) : path;
> +}

I'm sure we have utility functions for this.

> +  attr.type = PERF_TYPE_HARDWARE;
> +  attr.size = sizeof (attr);
> +  attr.config = PERF_COUNT_HW_CPU_CYCLES;
> +  attr.sample_period = sample_period;
> +  attr.sample_type
> +    = use_branch_stack_for_correlation_
> +     ? DEFAULT_BRANCH_STACK_SAMPLE_TYPE
> +     : DEFAULT_BRANCH_STACK_SAMPLE_TYPE & ~perf_sample_type::BRANCH_STACK;
> +  attr.branch_sample_type
> +    = use_branch_stack_for_correlation_ ? PERF_SAMPLE_BRANCH_ANY : 0;
> +  attr.exclude_kernel = 1;
> +  attr.exclude_hv = 1;
> +  attr.inherit = 1;
> +  attr.mmap = 1;
> +  attr.mmap2 = 1;

It might be useful to have a non inherit mode so that a single buffer
is used, in case when it is known the workload is single threaded
and you are really low on perf_mlock_kb space.

> +    {
> +      fprintf (stderr, _("Error: fork failed: %s\n"), strerror (errno));
> +      close (sync_pipe[0]);
> +      close (sync_pipe[1]);
> +      return false;
> +    }
> +
> +  if (pid == 0)
> +    {
> +      /* We start the command here; we block in read () until the parent
> +      attaches perf, then exec.  */
> +      close (sync_pipe[1]);
> +      char ignore;
> +      if (read (sync_pipe[0], &ignore, 1) != 1)
> +     _exit (126);
> +      close (sync_pipe[0]);
> +      std::vector<char *> argv;
> +      for (auto &s : command_argv)
> +     argv.push_back (const_cast<char *> (s.c_str ()));
> +      argv.push_back (nullptr);
> +      execvp (argv[0], argv.data ());
> +      fprintf (stderr, _("Error: execvp %s failed: %s\n"), argv[0],
> +            strerror (errno));
> +      _exit (127);
> +    }
> +
> +  /* Parent: attach perf to the child, which is blocked in read () and so
> +     cannot exit.  */
> +  close (sync_pipe[0]);
> +
> +  const long system_page_size = sysconf (_SC_PAGESIZE);
> +  if (system_page_size <= 0)
> +    {
> +      fprintf (stderr, _("Error: unable to determine system page size\n"));
> +      close (sync_pipe[1]);
> +      reap_child (pid);
> +      return false;
> +    }

This should never fail.

> +  for (const cpu_ring_buffer &buffer : buffers)
> +    pfds.push_back (pollfd{buffer.fd, POLLIN, 0});
> +  for (;;)
> +    {
> +      const pid_t reaped = waitpid (pid, &status, WNOHANG);
> +      if (reaped == pid)
> +     {
> +       have_status = true;
> +       break;
> +     }
> +      if (reaped < 0 && errno != EINTR)
> +     break;
> +      const int r = poll (pfds.data (), pfds.size (), 100);

It would be better to have some multi threading here, otherwise you
can bottleneck on the single thread on large machines. With that
you could also likely afford smaller buffers, making it work
better in parallel builds.

> +       /* Without poll () there is nothing left to pace this loop, so stop
> +          draining incrementally and wait for the workload instead.  */
> +       fprintf (stderr,
> +                _("Warning: poll on the perf event fds failed: %s\n"),
> +                strerror (errno));

the message would likely flood.

Reply via email to