// SPDX-License-Identifier: (LGPL-2.1 OR BSD-2-Clause) #include "counts.h" #include "debug.h" #include "evsel.h" #include "hashmap.h" #include "hwmon_pmu.h" #include "pmu.h" #include <internal/xyarray.h> #include <internal/threadmap.h> #include <perf/threadmap.h> #include <sys/types.h> #include <assert.h> #include <ctype.h> #include <dirent.h> #include <fcntl.h> #include <stddef.h> #include <stdlib.h> #include <string.h> #include <api/fs/fs.h> #include <api/io.h> #include <linux/kernel.h> #include <linux/string.h> #include <linux/zalloc.h> /** Strings that correspond to enum hwmon_type. */ static const char * const hwmon_type_strs[HWMON_TYPE_MAX] = { NULL, "cpu", "curr", "energy", "fan", "humidity", "in", "intrusion", "power", "pwm", "temp", }; #define LONGEST_HWMON_TYPE_STR "intrusion" /** Strings that correspond to enum hwmon_item. */ static const char * const hwmon_item_strs[HWMON_ITEM__MAX] = { NULL, "accuracy", "alarm", "auto_channels_temp", "average", "average_highest", "average_interval", "average_interval_max", "average_interval_min", "average_lowest", "average_max", "average_min", "beep", "cap", "cap_hyst", "cap_max", "cap_min", "crit", "crit_hyst", "div", "emergency", "emergency_hist", "enable", "fault", "freq", "highest", "input", "label", "lcrit", "lcrit_hyst", "lowest", "max", "max_hyst", "min", "min_hyst", "mod", "offset", "pulses", "rated_max", "rated_min", "reset_history", "target", "type", "vid", }; #define LONGEST_HWMON_ITEM_STR "average_interval_max" static const char *const hwmon_units[HWMON_TYPE_MAX] = { NULL, "V", /* cpu */ "A", /* curr */ "J", /* energy */ "rpm", /* fan */ "%", /* humidity */ "V", /* in */ "", /* intrusion */ "W", /* power */ "Hz", /* pwm */ "'C", /* temp */ }; struct hwmon_pmu { struct perf_pmu pmu; struct hashmap events; int hwmon_dir_fd; }; /** * union hwmon_pmu_event_key: Key for hwmon_pmu->events as such each key * represents an event. * * Related hwmon files start <type><number> that this key represents. */ union hwmon_pmu_event_key { long type_and_num; struct { int num :16; enum hwmon_type type :8; }; }; /** * struct hwmon_pmu_event_value: Value in hwmon_pmu->events. * * Hwmon files are of the form <type><number>_<item> and may have a suffix * _alarm. */ struct hwmon_pmu_event_value { /** @items: which item files are present. */ DECLARE_BITMAP(items, HWMON_ITEM__MAX); /** @alarm_items: which item files are present. */ DECLARE_BITMAP(alarm_items, HWMON_ITEM__MAX); /** @label: contents of <type><number>_label if present. */ char *label; /** @name: name computed from label of the form <type>_<label>. */ char *name; }; bool perf_pmu__is_hwmon(const struct perf_pmu *pmu) { return pmu && pmu->type >= PERF_PMU_TYPE_HWMON_START && pmu->type <= PERF_PMU_TYPE_HWMON_END; } bool evsel__is_hwmon(const struct evsel *evsel) { return perf_pmu__is_hwmon(evsel->pmu); } static size_t hwmon_pmu__event_hashmap_hash(long key, void *ctx __maybe_unused) { return ((union hwmon_pmu_event_key)key).type_and_num; } static bool hwmon_pmu__event_hashmap_equal(long key1, long key2, void *ctx __maybe_unused) { return ((union hwmon_pmu_event_key)key1).type_and_num == ((union hwmon_pmu_event_key)key2).type_and_num; } static int hwmon_strcmp(const void *a, const void *b) { const char *sa = a; const char * const *sb = b; return strcmp(sa, *sb); } bool parse_hwmon_filename(const char *filename, enum hwmon_type *type, int *number, enum hwmon_item *item, bool *alarm) { char fn_type[24]; const char **elem; const char *fn_item = NULL; size_t fn_item_len; assert(strlen(LONGEST_HWMON_TYPE_STR) < sizeof(fn_type)); strlcpy(fn_type, filename, sizeof(fn_type)); for (size_t i = 0; fn_type[i] != '\0'; i++) { if (fn_type[i] >= '0' && fn_type[i] <= '9') { fn_type[i] = '\0'; *number = strtoul(&filename[i], (char **)&fn_item, 10); if (*fn_item == '_') fn_item++; break; } if (fn_type[i] == '_') { fn_type[i] = '\0'; *number = -1; fn_item = &filename[i + 1]; break; } } if (fn_item == NULL || fn_type[0] == '\0' || (item != NULL && fn_item[0] == '\0')) { pr_debug3("hwmon_pmu: not a hwmon file '%s'\n", filename); return false; } elem = bsearch(&fn_type, hwmon_type_strs + 1, ARRAY_SIZE(hwmon_type_strs) - 1, sizeof(hwmon_type_strs[0]), hwmon_strcmp); if (!elem) { pr_debug3("hwmon_pmu: not a hwmon type '%s' in file name '%s'\n", fn_type, filename); return false; } *type = elem - &hwmon_type_strs[0]; if (!item) return true; *alarm = false; fn_item_len = strlen(fn_item); if (fn_item_len > 6 && !strcmp(&fn_item[fn_item_len - 6], "_alarm")) { assert(strlen(LONGEST_HWMON_ITEM_STR) < sizeof(fn_type)); strlcpy(fn_type, fn_item, fn_item_len - 5); fn_item = fn_type; *alarm = true; } elem = bsearch(fn_item, hwmon_item_strs + 1, ARRAY_SIZE(hwmon_item_strs) - 1, sizeof(hwmon_item_strs[0]), hwmon_strcmp); if (!elem) { pr_debug3("hwmon_pmu: not a hwmon item '%s' in file name '%s'\n", fn_item, filename); return false; } *item = elem - &hwmon_item_strs[0]; return true; } static void fix_name(char *p) { char *s = strchr(p, '\n'); if (s) *s = '\0'; while (*p != '\0') { if (strchr(" :,/\n\t", *p)) *p = '_'; else *p = tolower(*p); p++; } } static int hwmon_pmu__read_events(struct hwmon_pmu *pmu) { DIR *dir; struct dirent *ent; int dup_fd, err = 0; struct hashmap_entry *cur, *tmp; size_t bkt; if (pmu->pmu.sysfs_aliases_loaded) return 0; /* * Use a dup-ed fd as closedir will close it. Use openat so that the * directory contents are refreshed. */ dup_fd = openat(pmu->hwmon_dir_fd, ".", O_DIRECTORY); if (dup_fd == -1) return -ENOMEM; dir = fdopendir(dup_fd); if (!dir) { close(dup_fd); return -ENOMEM; } while ((ent = readdir(dir)) != NULL) { enum hwmon_type type; int number; enum hwmon_item item; bool alarm; union hwmon_pmu_event_key key = { .type_and_num = 0 }; struct hwmon_pmu_event_value *value; if (ent->d_type != DT_REG) continue; if (!parse_hwmon_filename(ent->d_name, &type, &number, &item, &alarm)) { pr_debug3("Not a hwmon file '%s'\n", ent->d_name); continue; } key.num = number; key.type = type; if (!hashmap__find(&pmu->events, key.type_and_num, &value)) { value = zalloc(sizeof(*value)); if (!value) { err = -ENOMEM; goto err_out; } err = hashmap__add(&pmu->events, key.type_and_num, value); if (err) { free(value); err = -ENOMEM; goto err_out; } } __set_bit(item, alarm ? value->alarm_items : value->items); if (item == HWMON_ITEM_LABEL) { char buf[128]; int fd = openat(pmu->hwmon_dir_fd, ent->d_name, O_RDONLY); ssize_t read_len; if (fd < 0) continue; read_len = read(fd, buf, sizeof(buf)); while (read_len > 0 && buf[read_len - 1] == '\n') read_len--; if (read_len > 0) buf[read_len] = '\0'; if (buf[0] == '\0') { pr_debug("hwmon_pmu: empty label file %s %s\n", pmu->pmu.name, ent->d_name); close(fd); continue; } value->label = strdup(buf); if (!value->label) { pr_debug("hwmon_pmu: memory allocation failure\n"); close(fd); continue; } snprintf(buf, sizeof(buf), "%s_%s", hwmon_type_strs[type], value->label); fix_name(buf); value->name = strdup(buf); if (!value->name) pr_debug("hwmon_pmu: memory allocation failure\n"); close(fd); } } if (hashmap__size(&pmu->events) == 0) pr_debug2("hwmon_pmu: %s has no events\n", pmu->pmu.name); hashmap__for_each_entry_safe((&pmu->events), cur, tmp, bkt) { union hwmon_pmu_event_key key = { .type_and_num = cur->key, }; struct hwmon_pmu_event_value *value = cur->pvalue; if (!test_bit(HWMON_ITEM_INPUT, value->items)) { pr_debug("hwmon_pmu: %s removing event '%s%d' that has no input file\n", pmu->pmu.name, hwmon_type_strs[key.type], key.num); hashmap__delete(&pmu->events, key.type_and_num, &key, &value); zfree(&value->label); zfree(&value->name); free(value); } } pmu->pmu.sysfs_aliases_loaded = true; err_out: closedir(dir); return err; } struct perf_pmu *hwmon_pmu__new(struct list_head *pmus, int hwmon_dir, const char *sysfs_name, const char *name) { char buf[32]; struct hwmon_pmu *hwm; hwm = zalloc(sizeof(*hwm)); if (!hwm) return NULL; hwm->hwmon_dir_fd = hwmon_dir; hwm->pmu.type = PERF_PMU_TYPE_HWMON_START + strtoul(sysfs_name + 5, NULL, 10); if (hwm->pmu.type > PERF_PMU_TYPE_HWMON_END) { pr_err("Unable to encode hwmon type from %s in valid PMU type\n", sysfs_name); goto err_out; } snprintf(buf, sizeof(buf), "hwmon_%s", name); fix_name(buf + 6); hwm->pmu.name = strdup(buf); if (!hwm->pmu.name) goto err_out; hwm->pmu.alias_name = strdup(sysfs_name); if (!hwm->pmu.alias_name) goto err_out; hwm->pmu.cpus = perf_cpu_map__new("0"); if (!hwm->pmu.cpus) goto err_out; INIT_LIST_HEAD(&hwm->pmu.format); INIT_LIST_HEAD(&hwm->pmu.aliases); INIT_LIST_HEAD(&hwm->pmu.caps); hashmap__init(&hwm->events, hwmon_pmu__event_hashmap_hash, hwmon_pmu__event_hashmap_equal, /*ctx=*/NULL); list_add_tail(&hwm->pmu.list, pmus); return &hwm->pmu; err_out: free((char *)hwm->pmu.name); free(hwm->pmu.alias_name); free(hwm); close(hwmon_dir); return NULL; } void hwmon_pmu__exit(struct perf_pmu *pmu) { struct hwmon_pmu *hwm = container_of(pmu, struct hwmon_pmu, pmu); struct hashmap_entry *cur, *tmp; size_t bkt; hashmap__for_each_entry_safe((&hwm->events), cur, tmp, bkt) { struct hwmon_pmu_event_value *value = cur->pvalue; zfree(&value->label); zfree(&value->name); free(value); } hashmap__clear(&hwm->events); close(hwm->hwmon_dir_fd); } static size_t hwmon_pmu__describe_items(struct hwmon_pmu *hwm, char *out_buf, size_t out_buf_len, union hwmon_pmu_event_key key, const unsigned long *items, bool is_alarm) { size_t bit; char buf[64]; size_t len = 0; for_each_set_bit(bit, items, HWMON_ITEM__MAX) { int fd; if (bit == HWMON_ITEM_LABEL || bit == HWMON_ITEM_INPUT) continue; snprintf(buf, sizeof(buf), "%s%d_%s%s", hwmon_type_strs[key.type], key.num, hwmon_item_strs[bit], is_alarm ? "_alarm" : ""); fd = openat(hwm->hwmon_dir_fd, buf, O_RDONLY); if (fd > 0) { ssize_t read_len = read(fd, buf, sizeof(buf)); while (read_len > 0 && buf[read_len - 1] == '\n') read_len--; if (read_len > 0) { long long val; buf[read_len] = '\0'; val = strtoll(buf, /*endptr=*/NULL, 10); len += snprintf(out_buf + len, out_buf_len - len, "%s%s%s=%g%s", len == 0 ? " " : ", ", hwmon_item_strs[bit], is_alarm ? "_alarm" : "", (double)val / 1000.0, hwmon_units[key.type]); } close(fd); } } return len; } int hwmon_pmu__for_each_event(struct perf_pmu *pmu, void *state, pmu_event_callback cb) { struct hwmon_pmu *hwm = container_of(pmu, struct hwmon_pmu, pmu); struct hashmap_entry *cur; size_t bkt; if (hwmon_pmu__read_events(hwm)) return false; hashmap__for_each_entry((&hwm->events), cur, bkt) { static const char *const hwmon_scale_units[HWMON_TYPE_MAX] = { NULL, "0.001V", /* cpu */ "0.001A", /* curr */ "0.001J", /* energy */ "1rpm", /* fan */ "0.001%", /* humidity */ "0.001V", /* in */ NULL, /* intrusion */ "0.001W", /* power */ "1Hz", /* pwm */ "0.001'C", /* temp */ }; static const char *const hwmon_desc[HWMON_TYPE_MAX] = { NULL, "CPU core reference voltage", /* cpu */ "Current", /* curr */ "Cumulative energy use", /* energy */ "Fan", /* fan */ "Humidity", /* humidity */ "Voltage", /* in */ "Chassis intrusion detection", /* intrusion */ "Power use", /* power */ "Pulse width modulation fan control", /* pwm */ "Temperature", /* temp */ }; char alias_buf[64]; char desc_buf[256]; char encoding_buf[128]; union hwmon_pmu_event_key key = { .type_and_num = cur->key, }; struct hwmon_pmu_event_value *value = cur->pvalue; struct pmu_event_info info = { .pmu = pmu, .name = value->name, .alias = alias_buf, .scale_unit = hwmon_scale_units[key.type], .desc = desc_buf, .long_desc = NULL, .encoding_desc = encoding_buf, .topic = "hwmon", .pmu_name = pmu->name, .event_type_desc = "Hwmon event", }; int ret; size_t len; len = snprintf(alias_buf, sizeof(alias_buf), "%s%d", hwmon_type_strs[key.type], key.num); if (!info.name) { info.name = info.alias; info.alias = NULL; } len = snprintf(desc_buf, sizeof(desc_buf), "%s in unit %s named %s.", hwmon_desc[key.type], pmu->name + 6, value->label ?: info.name); len += hwmon_pmu__describe_items(hwm, desc_buf + len, sizeof(desc_buf) - len, key, value->items, /*is_alarm=*/false); len += hwmon_pmu__describe_items(hwm, desc_buf + len, sizeof(desc_buf) - len, key, value->alarm_items, /*is_alarm=*/true); snprintf(encoding_buf, sizeof(encoding_buf), "%s/config=0x%lx/", pmu->name, cur->key); ret = cb(state, &info); if (ret) return ret; } return 0; } size_t hwmon_pmu__num_events(struct perf_pmu *pmu) { struct hwmon_pmu *hwm = container_of(pmu, struct hwmon_pmu, pmu); hwmon_pmu__read_events(hwm); return hashmap__size(&hwm->events); } bool hwmon_pmu__have_event(struct perf_pmu *pmu, const char *name) { struct hwmon_pmu *hwm = container_of(pmu, struct hwmon_pmu, pmu); enum hwmon_type type; int number; union hwmon_pmu_event_key key = { .type_and_num = 0 }; struct hashmap_entry *cur; size_t bkt; if (!parse_hwmon_filename(name, &type, &number, /*item=*/NULL, /*is_alarm=*/NULL)) return false; if (hwmon_pmu__read_events(hwm)) return false; key.type = type; key.num = number; if (hashmap_find(&hwm->events, key.type_and_num, /*value=*/NULL)) return true; if (key.num != -1) return false; /* Item is of form <type>_ which means we should match <type>_<label>. */ hashmap__for_each_entry((&hwm->events), cur, bkt) { struct hwmon_pmu_event_value *value = cur->pvalue; key.type_and_num = cur->key; if (key.type == type && value->name && !strcasecmp(name, value->name)) return true; } return false; } static int hwmon_pmu__config_term(const struct hwmon_pmu *hwm, struct perf_event_attr *attr, struct parse_events_term *term, struct parse_events_error *err) { if (term->type_term == PARSE_EVENTS__TERM_TYPE_USER) { enum hwmon_type type; int number; if (parse_hwmon_filename(term->config, &type, &number, /*item=*/NULL, /*is_alarm=*/NULL)) { if (number == -1) { /* * Item is of form <type>_ which means we should * match <type>_<label>. */ struct hashmap_entry *cur; size_t bkt; attr->config = 0; hashmap__for_each_entry((&hwm->events), cur, bkt) { union hwmon_pmu_event_key key = { .type_and_num = cur->key, }; struct hwmon_pmu_event_value *value = cur->pvalue; if (key.type == type && value->name && !strcasecmp(term->config, value->name)) { attr->config = key.type_and_num; break; } } if (attr->config == 0) return -EINVAL; } else { union hwmon_pmu_event_key key = { .type_and_num = 0, }; key.type = type; key.num = number; attr->config = key.type_and_num; } return 0; } } if (err) { char *err_str; parse_events_error__handle(err, term->err_val, asprintf(&err_str, "unexpected hwmon event term (%s) %s", parse_events__term_type_str(term->type_term), term->config) < 0 ? strdup("unexpected hwmon event term") : err_str, NULL); } return -EINVAL; } int hwmon_pmu__config_terms(const struct perf_pmu *pmu, struct perf_event_attr *attr, struct parse_events_terms *terms, struct parse_events_error *err) { struct hwmon_pmu *hwm = container_of(pmu, struct hwmon_pmu, pmu); struct parse_events_term *term; int ret; ret = hwmon_pmu__read_events(hwm); if (ret) return ret; list_for_each_entry(term, &terms->terms, list) { if (hwmon_pmu__config_term(hwm, attr, term, err)) return -EINVAL; } return 0; } int hwmon_pmu__check_alias(struct parse_events_terms *terms, struct perf_pmu_info *info, struct parse_events_error *err) { struct parse_events_term *term = list_first_entry(&terms->terms, struct parse_events_term, list); if (term->type_term == PARSE_EVENTS__TERM_TYPE_USER) { enum hwmon_type type; int number; if (parse_hwmon_filename(term->config, &type, &number, /*item=*/NULL, /*is_alarm=*/NULL)) { info->unit = hwmon_units[type]; if (type == HWMON_TYPE_FAN || type == HWMON_TYPE_PWM || type == HWMON_TYPE_INTRUSION) info->scale = 1; else info->scale = 0.001; } return 0; } if (err) { char *err_str; parse_events_error__handle(err, term->err_val, asprintf(&err_str, "unexpected hwmon event term (%s) %s", parse_events__term_type_str(term->type_term), term->config) < 0 ? strdup("unexpected hwmon event term") : err_str, NULL); } return -EINVAL; } int perf_pmus__read_hwmon_pmus(struct list_head *pmus) { char *line = NULL; DIR *class_hwmon_dir; struct dirent *class_hwmon_ent; char buf[PATH_MAX]; const char *sysfs = sysfs__mountpoint(); if (!sysfs) return 0; scnprintf(buf, sizeof(buf), "%s/class/hwmon/", sysfs); class_hwmon_dir = opendir(buf); if (!class_hwmon_dir) return 0; while ((class_hwmon_ent = readdir(class_hwmon_dir)) != NULL) { size_t line_len; int hwmon_dir, name_fd; struct io io; if (class_hwmon_ent->d_type != DT_LNK) continue; scnprintf(buf, sizeof(buf), "%s/class/hwmon/%s", sysfs, class_hwmon_ent->d_name); hwmon_dir = open(buf, O_DIRECTORY); if (hwmon_dir == -1) { pr_debug("hwmon_pmu: not a directory: '%s/class/hwmon/%s'\n", sysfs, class_hwmon_ent->d_name); continue; } name_fd = openat(hwmon_dir, "name", O_RDONLY); if (name_fd == -1) { pr_debug("hwmon_pmu: failure to open '%s/class/hwmon/%s/name'\n", sysfs, class_hwmon_ent->d_name); close(hwmon_dir); continue; } io__init(&io, name_fd, buf, sizeof(buf)); io__getline(&io, &line, &line_len); if (line_len > 0 && line[line_len - 1] == '\n') line[line_len - 1] = '\0'; hwmon_pmu__new(pmus, hwmon_dir, class_hwmon_ent->d_name, line); close(name_fd); } free(line); closedir(class_hwmon_dir); return 0; } #define FD(e, x, y) (*(int *)xyarray__entry(e->core.fd, x, y)) int evsel__hwmon_pmu_open(struct evsel *evsel, struct perf_thread_map *threads, int start_cpu_map_idx, int end_cpu_map_idx) { struct hwmon_pmu *hwm = container_of(evsel->pmu, struct hwmon_pmu, pmu); union hwmon_pmu_event_key key = { .type_and_num = evsel->core.attr.config, }; int idx = 0, thread = 0, nthreads, err = 0; nthreads = perf_thread_map__nr(threads); for (idx = start_cpu_map_idx; idx < end_cpu_map_idx; idx++) { for (thread = 0; thread < nthreads; thread++) { char buf[64]; int fd; snprintf(buf, sizeof(buf), "%s%d_input", hwmon_type_strs[key.type], key.num); fd = openat(hwm->hwmon_dir_fd, buf, O_RDONLY); FD(evsel, idx, thread) = fd; if (fd < 0) { err = -errno; goto out_close; } } } return 0; out_close: if (err) threads->err_thread = thread; do { while (--thread >= 0) { if (FD(evsel, idx, thread) >= 0) close(FD(evsel, idx, thread)); FD(evsel, idx, thread) = -1; } thread = nthreads; } while (--idx >= 0); return err; } int evsel__hwmon_pmu_read(struct evsel *evsel, int cpu_map_idx, int thread) { char buf[32]; int fd; ssize_t len; struct perf_counts_values *count, *old_count = NULL; if (evsel->prev_raw_counts) old_count = perf_counts(evsel->prev_raw_counts, cpu_map_idx, thread); count = perf_counts(evsel->counts, cpu_map_idx, thread); fd = FD(evsel, cpu_map_idx, thread); len = pread(fd, buf, sizeof(buf), 0); if (len <= 0) { count->lost++; return -EINVAL; } buf[len] = '\0'; if (old_count) { count->val = old_count->val + strtoll(buf, NULL, 10); count->run = old_count->run + 1; count->ena = old_count->ena + 1; } else { count->val = strtoll(buf, NULL, 10); count->run++; count->ena++; } return 0; }