diff --git a/usr.sbin/pmc/cmd_pmc_record.cc b/usr.sbin/pmc/cmd_pmc_record.cc new file mode 100644 index 000000000000..a8373b330705 --- /dev/null +++ b/usr.sbin/pmc/cmd_pmc_record.cc @@ -0,0 +1,750 @@ +/*- + * SPDX-License-Identifier: BSD-2-Clause + * + * Copyright (c) 2026, Netflix, Inc. + * + * This software was developed by Ali Mashtizadeh under the sponsorship from + * Netflix, Inc. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include "cmd_pmc.h" + +#include +#include +#include +#include + +#include "display.hh" +#include "headers.hh" + +#define DEFAULT_RATE 65536 + +class pmc_config +{ +public: + std::string event; + uint64_t count; + cpuset_t cpumask; + uint32_t caps; + pmc_config() : event(), count(0), ids() + { + CPU_ZERO(&cpumask); + } + pmc_config(const std::string &event, uint64_t count, cpuset_t cpumask) + : event(event), count(count), ids() + { + CPU_COPY(&cpumask, &this->cpumask); + } + /* + * Read the capabilities and fills in the cpumask for a given event + * counter. The way that the hwpmc module currently works we do not + * know which pmc class will back our counter until we look it up. The + * easiest way is to program the counter into the first CPU present in + * the mask and then retrieve the capabilities. + */ + void getcaps() { + int status; + int testcpu; + pmc_id_t id; + + testcpu = CPU_FFS(&cpumask) - 1; + + status = pmc_allocate(event.c_str(), PMC_MODE_SS, PMC_F_CALLCHAIN, + testcpu, &id, count); + if (status < 0) + err(EX_OSERR, "ERROR: Cannot allocate event '%s'", event.c_str()); + + status = pmc_capabilities(id, &caps); + if (status < 0) + err(EX_OSERR, "ERROR: Cannot get pmc capabilities"); + + pmc_release(id); + + if (caps & PMC_CAP_SYSWIDE) { + CPU_ZERO(&cpumask); + CPU_SET(0, &cpumask); + } + if (caps & PMC_CAP_DOMWIDE) { + int domains; + size_t len; + + CPU_ZERO(&cpumask); + + len = sizeof(domains); + if (sysctlbyname("vm.ndomains", &domains, &len, NULL, 0) == -1) + err(EX_OSERR, "ERROR: Cannot get number of domains"); + + for (int i = 1; i < domains; i++) { + cpuset_t dmask; + + CPU_ZERO(&dmask); + status = cpuset_getaffinity(CPU_LEVEL_WHICH, CPU_WHICH_DOMAIN, i, + sizeof(dmask), &dmask); + if (status < 0) + err(EX_OSERR, "ERROR: Cannot get domain mask"); + CPU_SET(CPU_FFS(&dmask) - 1, &cpumask); + } + } + } + /* + * Allocate the PMC across all CPUs. + */ + void allocate() { + int cpu; + int status; + pmc_id_t id; + + for (cpu = 0; cpu < CPU_SETSIZE; cpu++) { + if (!CPU_ISSET(cpu, &cpumask)) + continue; + + status = pmc_allocate(event.c_str(), PMC_MODE_SS, PMC_F_CALLCHAIN, + cpu, &id, count); + if (status < 0) + err(EX_OSERR, "ERROR: Cannot allocate event '%s'", + event.c_str()); + + status = pmc_set(id, count); + if (status < 0) + err(EX_OSERR, "ERROR: Cannot set sampling count for event '%s'", + event.c_str()); + + ids.push_back(id); + } + } + /* + * Release all PMC instances (one per CPU). + */ + void release() { + for (auto i : ids) { + if (pmc_release(i) < 0) { + perror("pmc_start"); + } + } + } + /* + * Start all PMC instances (one per CPU). + */ + void start() { + for (auto i : ids) { + if (pmc_start(i) < 0) { + perror("pmc_start"); + } + } + } + /* + * Stop all PMC instances (one per CPU). + */ + void stop() { + for (auto i : ids) { + if (pmc_stop(i) < 0) { + perror("pmc_start"); + } + } + } +private: + std::vector ids; +}; + +static std::vector pmcs = std::vector(); +static struct option longopts[] = { + { "rate", required_argument, NULL, 'n' }, + { "counter", required_argument, NULL, 'c' }, + { "time", required_argument, NULL, 't' }, + { "study", required_argument, NULL, 's' }, + { NULL, 0, NULL, 0 } +}; +static struct timespec start; +static int timelimit = 0; + +static void +usage(void) +{ + printf("Usage: pmc record [options] [output.pmc]\n\n"); + printf("Record a study\n\n"); + printf("Options:\n"); + printf("\t-c Sampling counter\n"); + printf("\t-r Sample rate (default: %d)\n", DEFAULT_RATE); + printf("\t-s Specify a study\n"); + printf("\t-t Run the study for specified amount of time\n"); + printf("\nStudies:\n"); +#if defined(__i386__) || defined(__amd64__) + printf("\tbranches Study branch misprediction (Requires AMD IBS)\n"); + printf("\tc2c Study cache to cache communications (Requires AMD IBS)\n"); +#endif + printf("\tdefault Instruction sampling for general analysis\n"); + printf("\tflamegraph Instruction sampling for general analysis\n"); +#if defined(__i386__) || defined(__amd64__) + printf("\tfrontend Study frontend stalls (Requires AMD IBS)\n"); + printf("\tmemory Study memory operations (Requires AMD IBS)\n"); +#endif +} + +int +writelog(int logfd, const void *buf, size_t len) +{ + int status; + const char *cur = (const char *)buf; + + while (len != 0) { + status = write(logfd, cur, len); + if (status < 0) { + if (errno == EINTR || errno == EAGAIN) + continue; + else + return status; + } + if (status == 0) + return status; + + cur += status; + len -= status; + } + + return 0; +} + +/* + * Write the PMC header. Each of the header payloads have their own header + * containing the payloads size. Thus the pmc tools can just skip over regions + * that they do not know how to handle. + */ +int +write_header(int logfd) +{ + int status; + pmchdr_header hdr; + + hdr.magic = PMC_HEADER_MAGIC; + hdr.version = PMC_HEADER_VERSION; +#if defined(__amd64__) + hdr.arch = PMC_ARCH_AMD64; +#elif defined(__aarch64__) + hdr.arch = PMC_ARCH_ARM64; +#elif defined(__powerpc64__) + hdr.arch = PMC_ARCH_PPC64; +#elif defined(__riscv) + hdr.arch = PMC_ARCH_RISCV64; +#elif defined(__arm__) + hdr.arch = PMC_ARCH_ARM; +#else + hdr.arch = 0; +#endif + + status = writelog(logfd, &hdr, sizeof(hdr)); + if (status < 0) { + perror("writelog"); + } + + return status; +} + +/* + * Write out the sysinfo header. + */ +int +write_sysinfo(int logfd) +{ + int status; + pmchdr_infohdr hdr; + pmchdr_sysinfo sys; + char val[64]; + size_t valsz; + + valsz = sizeof(val); + status = sysctlbyname("hw.model", &val, &valsz, NULL, 0); + if (status < 0) { + perror("sysctlbyname"); + return status; + } + strlcpy(sys.cpumodel, val, sizeof(sys.cpumodel)); + + valsz = sizeof(val); + status = sysctlbyname("kern.osrelease", &val, &valsz, NULL, 0); + if (status < 0) { + perror("sysctlbyname"); + return status; + } + strlcpy(sys.osrelease, val, sizeof(sys.osrelease)); + + valsz = sizeof(val); + status = sysctlbyname("kern.build_id", &val, &valsz, NULL, 0); + if (status < 0) { + perror("sysctlbyname"); + return status; + } + strlcpy(sys.buildid, val, sizeof(sys.buildid)); + + hdr.type = INFOHDR_TYPE_SYSINFO; + hdr.length = sizeof(sys); + status = writelog(logfd, &hdr, sizeof(hdr)); + if (status < 0) { + err(EX_IOERR, "writelog"); + } + + status = writelog(logfd, &sys, sizeof(sys)); + if (status < 0) { + err(EX_IOERR, "writelog"); + } + + return status; +}; + +#if defined(__i386__) || defined(__amd64__) +#define CPUID_ROOT_BASE 0x00000000 +#define CPUID_ROOT_VM 0x40000000 +#define CPUID_ROOT_EXT 0x80000000 + +/* + * Write out the CPU Info block. After the standard header that declares the + * size of the payload, we write out all the CPUID root and leafs for each of + * the three major roots: base, VM, and extended artribute space. On all + * modern processors the root contains the maximum leaf as the first value + * allowing us to decode how many leafs there are. + */ +int +write_cpuinfo(int logfd) +{ + int status; + u_int tmp[4]; + uint32_t base_max, vm_max, ext_max; + uint32_t len; + pmchdr_infohdr *hdr; + uint32_t *buf, *off; + + /* + * Find the length of the main, VM, and extended CPUID leafs + */ + do_cpuid(CPUID_ROOT_BASE, tmp); + base_max = tmp[0]; + len = base_max + 1; + + do_cpuid(CPUID_ROOT_VM, tmp); + vm_max = tmp[0]; + if (vm_max != 0) + len += vm_max - CPUID_ROOT_VM + 1; + + do_cpuid(CPUID_ROOT_EXT, tmp); + ext_max = tmp[0]; + if (ext_max != 0) + len += ext_max - CPUID_ROOT_EXT + 1; + + // 4 Registers per Leaf x 4 Bytes per Register + len *= 4; + + buf = (uint32_t *)new uint32_t[len + sizeof(pmchdr_infohdr) / 4]; + hdr = (pmchdr_infohdr *)buf; + hdr->type = INFOHDR_TYPE_CPUID; + hdr->length = 4 * len; + off = (uint32_t *)(hdr + 1); + + for (uint32_t i = 0; i <= base_max; i++) { + do_cpuid(i, off); + off += 4; + } + + if (vm_max) { + for (uint32_t i = CPUID_ROOT_VM; i <= vm_max; i++) { + do_cpuid(i, off); + off += 4; + } + } + if (ext_max) { + for (uint32_t i = CPUID_ROOT_EXT; i <= ext_max; i++) { + do_cpuid(i, off); + off += 4; + } + } + + status = writelog(logfd, buf, 4 * len + sizeof(*hdr)); + if (status < 0) { + delete[] buf; + err(EX_IOERR, "writelog"); + } + + delete[] buf; + + return 0; +} +#elif defined(__aarch64__) +int +write_cpuinfo(__unused int logfd) +{ + return 0; +} +#elif defined(__powerpc64__) +int +write_cpuinfo(__unused int logfd) +{ + return 0; +} +#else +int +write_cpuinfo(__unused int logfd) +{ + return 0; +} +#endif + +int +write_footer(int logfd) +{ + int status; + pmchdr_infohdr hdr; + + hdr.type = INFOHDR_TYPE_DONE; + hdr.length = 0; + + status = writelog(logfd, &hdr, sizeof(hdr)); + if (status < 0) { + perror("writelog"); + } + + return status; +} + +#if defined(__i386__) || defined(__amd64__) +int +setup_study(const std::string &study, uint64_t rate, cpuset_t mask) +{ + u_int tmp[4]; + alignas(4) char vendor[16]; + std::string event; + uint32_t ext_max; + uint32_t ibs_features; + bool is_amd, is_intel; + + do_cpuid(CPUID_ROOT_BASE, tmp); + + /* Find the brand */ + is_intel = false; + is_amd = false; + + /* i386 complains without explicit alignment */ + ((u_int *)&vendor)[0] = tmp[1]; + ((u_int *)&vendor)[1] = tmp[3]; + ((u_int *)&vendor)[2] = tmp[2]; + vendor[12] = 0; + if (strncmp(vendor, INTEL_VENDOR_ID, 12) == 0) + is_intel = true; + if (strncmp(vendor, AMD_VENDOR_ID, 12) == 0) + is_amd = true; + if (strncmp(vendor, HYGON_VENDOR_ID, 12) == 0) + is_amd = true; + + ibs_features = 0; + do_cpuid(CPUID_ROOT_EXT, tmp); + ext_max = tmp[0]; + if (is_amd && ext_max >= CPUID_IBSID) { + do_cpuid(CPUID_IBSID, tmp); + ibs_features = tmp[0]; + } + + if (study == "default" || study == "flamegraph") { + pmcs.push_back(pmc_config("unhalted-cycles", rate, mask)); + return 0; + } + + if (is_intel) { + err(EX_SOFTWARE, "ERROR: Study is unsupported on Intel"); + } + + if (study == "frontend") { + event = "ibs-fetch,randomize"; + } else if (study == "branches" || study == "memory") { + event = "ibs-op"; + if (ibs_features & CPUID_IBSID_OPCNT) + event += ",opcount"; + } else if (study == "c2c") { + event = "ibs-op"; + if (ibs_features & CPUID_IBSID_IBSLOADLATENCYFILT) + event += ",l3miss"; + if (ibs_features & CPUID_IBSID_OPCNT) + event += ",opcount"; + } else { + err(EX_USAGE, "ERROR: Study '%s' unknown", study.c_str()); + } + + pmcs.push_back(pmc_config(event, rate, mask)); + + return 0; +} +#else +int +setup_study(const std::string &study, uint64_t rate, cpuset_t mask) +{ + if (study == "default" || study == "flamegraph") { + pmcs.push_back(pmc_config("unhalted-cycles", rate, mask)); + return 0; + } + + err(EX_SOFTWARE, "ERROR: Study is unsupported on this architecture"); +} +#endif + +/* + * Compute the current runtime and print it to the screen, if it exceeds + * timelimit return 1 otherwise 0. + */ +int +update_status(int logfd) +{ + int status; + long sec, nsec; + struct timespec end; + struct stat sb; + std::string filesz; + + status = clock_gettime(CLOCK_MONOTONIC, &end); + if (status < 0) { + err(EX_OSERR, "Could not read current time"); + } + + sec = end.tv_sec - start.tv_sec; + nsec = end.tv_nsec - start.tv_nsec; + if (nsec < 0) { + sec--; + nsec += 1000000000L; + } + + // Do this another way for sockets + status = fstat(logfd, &sb); + if (status < 0) { + err(EX_OSERR, "Failed to fstat log file"); + } + + filesz = format_binprefix(sb.st_size); + + /* + * Use %-10s to ensure we print spaces after in case the size string + * shrinks. + */ + printf("Elapsed: %ld.%03ld seconds, Log file size: %-10s\r", + sec, nsec / 1000000, + filesz.c_str()); + fflush(stdout); + + if (timelimit != 0 && sec >= timelimit) { + printf("\nElapsed time completed\n"); + return 1; + } + + return 0; +} + +int +cmd_pmc_record(int argc, char **argv) +{ + struct kevent kev; + cpuset_t mask; + uint64_t rate = DEFAULT_RATE; + const char *logfile = "default.log"; + int status; + int option, logfd, kq; + + CPU_ZERO(&mask); + if (cpuset_getaffinity(CPU_LEVEL_ROOT, CPU_WHICH_PID, -1, + sizeof(mask), &mask) == -1) + err(EX_OSERR, "ERROR: Cannot determine the available CPUs"); + + if (pmc_init() < 0) + err(EX_SOFTWARE, "ERROR: Failed to initialize libpmc"); + + while ((option = getopt_long(argc, argv, "c:r:s:t:", longopts, NULL)) != -1) { + switch (option) { + case 'c': + pmcs.push_back(pmc_config(optarg, rate, mask)); + break; + case 'r': + rate = strtoull(optarg, NULL, 0); + break; + case 's': + setup_study(optarg, rate, mask); + break; + case 't': + timelimit = atoi(optarg); + break; + case '?': + default: + usage(); + } + } + argc -= optind; + argv += optind; + if (argc != 0 && argc != 1) { + usage(); + exit(EX_USAGE); + } + if (argc == 1) + logfile = argv[0]; + + if (pmcs.size() == 0) { + errx(EX_NOINPUT, "ERROR: Please select one or more counters or performance studies."); + } + + if ((logfd = open(logfile, O_CREAT|O_EXCL|O_WRONLY, + S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)) < 0) { + errx(EX_OSERR, "ERROR: Cannot open \"%s\" for writing: %s.", logfile, + strerror(errno)); + } + + setup_screen(); + + for (auto &p : pmcs) { + p.getcaps(); + } + + write_header(logfd); + write_sysinfo(logfd); + write_cpuinfo(logfd); + write_footer(logfd); + + // Record pmclog + kq = kqueue(); + if (kq < 0) + err(EX_OSERR, "ERROR: kqueue creation failed"); + + EV_SET(&kev, fileno(stdin), EVFILT_READ, EV_ADD, 0, 0, NULL); + if (kevent(kq, &kev, 1, NULL, 0, NULL) < 0) + err(EX_OSERR, "ERROR: kevent failed to register stdin"); + + EV_SET(&kev, SIGINT, EVFILT_SIGNAL, EV_ADD, 0, 0, NULL); + if (kevent(kq, &kev, 1, NULL, 0, NULL) < 0) + err(EX_OSERR, "ERROR: kevent failed to register SIGINT"); + EV_SET(&kev, SIGIO, EVFILT_SIGNAL, EV_ADD, 0, 0, NULL); + if (kevent(kq, &kev, 1, NULL, 0, NULL) < 0) + err(EX_OSERR, "ERROR: kevent failed to register SIGIO"); + + EV_SET(&kev, 0, EVFILT_TIMER, EV_ADD, 0, 100, NULL); + if (kevent(kq, &kev, 1, NULL, 0, NULL) < 0) + err(EX_OSERR, "ERROR: Cannot register kevent for timer"); + + pmc_configure_logfile(logfd); + + for (auto &p : pmcs) { + p.allocate(); + } + + for (auto &p : pmcs) { + p.start(); + } + + if (clock_gettime(CLOCK_MONOTONIC, &start) < 0) + err(EX_OSERR, "ERROR: Could not get the current time"); + + printf("Recording performance trace press Ctrl-C to stop\n"); + + /* + * loop till either the target process (if any) exits, or we + * are killed by a SIGINT or we reached the time duration. + */ + while (1) { + status = kevent(kq, NULL, 0, &kev, 1, NULL); + if (status <= 0) { + if (errno != EINTR) + err(EX_OSERR, "ERROR: kevent failed"); + else + continue; + } + + if (kev.flags & EV_ERROR) + errc(EX_OSERR, kev.data, "ERROR: kevent failed"); + + switch (kev.filter) { + case EVFILT_READ: /* log file data is present */ + if (kev.ident == (unsigned)fileno(stdin)) { + // Check for exit key + } + break; + case EVFILT_SIGNAL: + if (kev.ident == SIGIO) { + fprintf(stderr, "ERROR: IO error"); + status = EX_OSERR; + goto done; + } else if (kev.ident == SIGINT) { + status = EX_OK; + goto done; + } else { + err(EX_OSERR, "Unknown signal recieved"); + } + break; + case EVFILT_TIMER: + if (update_status(logfd) == 1) + goto done; + break; + } + } + +done: + for (auto &p : pmcs) { + p.stop(); + p.release(); + } + close(logfd); + + return (status); +} + diff --git a/usr.sbin/pmc/headers.hh b/usr.sbin/pmc/headers.hh index 29f76cd7cc2c..6bdabc4fbe6c 100644 --- a/usr.sbin/pmc/headers.hh +++ b/usr.sbin/pmc/headers.hh @@ -39,6 +39,7 @@ #define PMC_ARCH_ARM64 0x02 #define PMC_ARCH_PPC64 0x03 #define PMC_ARCH_RISCV64 0x04 +#define PMC_ARCH_ARM 0x05 struct pmchdr_header {